1. Jedis vs Lettuce
在 Java 生态中,Jedis 与 Lettuce 是两类主流的 Redis 客户端实现,二者在底层线程模型、连接方式以及对 Spring Boot 的集成策略上存在本质差异,工程师在选型时应根据并发量级、部署拓扑以及集群管理需求做出权衡。
Jedis 采用传统的 BIO(Blocking I/O)模型,每一条 Redis 命令的执行都会独占一个连接,因此多线程并发场景下必须通过连接池(commons-pool2)来复用连接,否则将直接引发线程安全问题。Jedis 的优点在于 API 设计直观、学习曲线平缓、异常处理语义清晰,对小型项目或单机 Redis 场景已经足够。其不足则在于阻塞模型决定了单连接无法承载高吞吐并发,连接池的调优本身也是一门技术活,max-total、max-idle、min-idle、block-when-exhausted 等参数需要结合业务峰值流量反复测试验证。
Lettuce 则基于 Netty 框架实现,采用 NIO(Non-blocking I/O)结合事件驱动的异步架构。其核心优势是:单个连接天然线程安全,可以在多个线程间共享;命令发送与结果读取通过 Future 或 Reactive Stream 完成,极大降低了连接资源消耗。在集群模式下,Lettuce 内置了拓扑自动发现与重连机制,当集群发生节点迁移时能够自动更新 slot 映射,无需手动刷新连接池。Spring Boot 从 2.x 起将 Lettuce 设为默认客户端,意味着在现代 Spring 工程中使用 Redis,几乎可以直接依赖 Spring Boot Starter 提供的默认配置而无需显式声明客户端类型。
| 特性 | Jedis | Lettuce |
|---|---|---|
| 线程安全 | 否(需连接池) | ✅(基于 Netty) |
| 连接方式 | 阻塞 BIO | 异步非阻塞 NIO |
| 连接池 | 必须 | 可选(通常不用) |
| 集群支持 | 较好 | 更好 |
| Spring Boot 默认 | 2.x 以前 | 2.x+ |
| 异步 API | 不支持 | 支持 CompletableFuture / Reactive |
| 哨兵/集群拓扑刷新 | 手动 | 自动自适应 |
2. Spring Data Redis 配置
Spring Boot 对 Redis 的配置抽象集中在 spring.data.redis 命名空间下。Lettuce 作为默认驱动,其连接池参数与集群刷新策略均有独立配置节。以下是一份面向生产环境的完整 YAML 示例,覆盖了单机、密码认证、连接池、集群自适应刷新以及读写超时控制等关键参数。
spring:
data:
redis:
host: localhost
port: 6379
password: secret
database: 0
timeout: 2000ms
lettuce:
pool:
max-active: 8
max-idle: 8
min-idle: 0
max-wait: -1ms
shutdown-timeout: 100ms
cluster:
refresh:
adaptive: true
period: 60s
配置语义逐项说明:timeout 控制单次命令的网络超时,防止后端 Redis 偶发抖动导致线程长时间挂起。max-active 是连接池上限,应根据实际并发而非连接数盲目调大,因为 Lettuce 单连接已可支撑高并发;max-idle 与 min-idle 用于池内连接的保活与预热,避免流量突增时频繁创建连接。cluster.refresh.adaptive=true 使 Lettuce 在收到MOVED/ASK 错误时自动刷新集群拓扑,配合 period: 60s 的周期性兜底,确保发生节点扩缩容时客户端路由依然准确。对于 Sentinel 高可用模式,只需将 host/port 替换为 sentinel.master 和 sentinel.nodes 列表即可。
spring:
data:
redis:
sentinel:
master: mymaster
nodes: sentinel-1:26379,sentinel-2:26379,sentinel-3:26379
password: secret
3. RedisTemplate
RedisTemplate 是 Spring Data Redis 提供的中枢操作类,封装了对五种 Redis 数据结构(String、Hash、List、Set、ZSet)的完整存取接口。Spring Boot 自动配置会同时注册 StringRedisTemplate(Key 与 Value 均为字符串)和泛型的 RedisTemplate<String, Object>(需配置序列化器)。在业务层中,通常将 StringRedisTemplate 用于缓存简单字符串或 JSON 文本,而将 Object RedisTemplate 与适当的 ValueSerializer 配合,用于缓存 POJO 对象。
下面通过一个 CacheService 演示 Value、List、原子操作以及简易分布式锁的典型用法。特别注意:opsForValue().setIfAbsent 配合过期时间是实现 Redis 分布式锁最基础的原子原语,Spring Data Redis 在底层将其映射为 SET key value NX EX seconds 原子命令,避免了先 SET 再 EXPIRE 可能产生的竞态条件。
@Service
public class CacheService {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Autowired
private RedisTemplate<String, Object> objectRedisTemplate;
/**
* 缓存用户对象,TTL 30 分钟,使用 JSON 序列化
*/
public void cacheUser(String userId, User user) {
objectRedisTemplate.opsForValue().set(
"user:" + userId, user, Duration.ofMinutes(30)
);
}
/**
* 左推入队列并固定长度,实现固定窗口 Feed 流
*/
public void addToList(String key, String item) {
stringRedisTemplate.opsForList().leftPush(key, item);
stringRedisTemplate.opsForList().trim(key, 0, 99);
}
/**
* 原子递增,适用于计数器、限流流水号
*/
public Long increment(String key) {
return stringRedisTemplate.opsForValue().increment(key);
}
/**
* 简易分布式锁:SET NX EX
*/
public boolean tryLock(String key, String value, long seconds) {
Boolean locked = stringRedisTemplate.opsForValue()
.setIfAbsent(key, value, Duration.ofSeconds(seconds));
return Boolean.TRUE.equals(locked);
}
/**
* 释放锁时需比对 value,防止误删他人锁
*/
public void unlock(String key, String expectedValue) {
String script =
"if redis.call('get', KEYS[1]) == ARGV[1] then " +
"return redis.call('del', KEYS[1]) else return 0 end";
stringRedisTemplate.execute(
new DefaultRedisScript<>(script, Long.class),
Collections.singletonList(key),
expectedValue
);
}
}
4. @Cacheable 注解
Spring Cache 抽象是一套与存储介质无关的声明式缓存框架,通过 @EnableCaching 开启后,可在方法级别使用 @Cacheable、@CachePut、@CacheEvict 控制缓存的读取、更新与失效。其核心价值在于:缓存逻辑与业务逻辑解耦,开发者无需在 Service 中显式编写 Redis 存取代码;同时可以通过 CacheManager 的自定义配置统一管理序列化方式、TTL、Key 前缀以及空值缓存策略。
@Cacheable 的 key 属性支持 SpEL 表达式,可灵活组合方法参数;当希望按条件缓存时,可使用 condition 与 unless。例如 condition = "#id.length() > 3" 仅在 id 长度大于 3 时启用缓存。CacheEvict 的 allEntries = true 用于批量清除整片缓存,适合配置项刷新或全量重建场景。
@Configuration
@EnableCaching
public class CacheConfig {
@Bean
public CacheManager cacheManager(RedisConnectionFactory cf) {
RedisCacheConfiguration defaults = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(10))
.serializeKeysWith(RedisSerializationContext
.SerializationPair.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext
.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()))
.disableCachingNullValues();
return RedisCacheManager.builder(cf)
.cacheDefaults(defaults)
.transactionAware()
.build();
}
}
@Service
public class ProductService {
@Cacheable(value = "products", key = "#id")
public Product getProduct(String id) {
return productRepo.findById(id)
.orElseThrow(() -> new NotFoundException(id));
}
@CacheEvict(value = "products", key = "#product.id")
public void updateProduct(Product product) {
productRepo.save(product);
}
@CachePut(value = "products", key = "#product.id")
public Product refreshProduct(Product product) {
return productRepo.save(product);
}
@CacheEvict(value = "products", allEntries = true)
public void clearAllProducts() {
// 批量清除缓存,例如运营后台刷新全量数据
}
}
5. Redisson 分布式组件
Spring Data Redis 提供的简易锁仅能满足最基础的互斥需求,在分布式环境中缺乏可重入、看门狗自动续期以及公平队列等高级语义。Redisson 是一个基于 Redis 的 Java 驻内存数据网格,它对分布式锁、信号量、原子长整型、布隆过滤器、限流器均提供了与 java.util.concurrent 类似的 API,极大降低了分布式并发编程的心智负担。
Redisson 的分布式锁(RLock)具备以下关键特性:其一,加锁时可指定 leaseTime(租期),若未显式指定,Redisson 会启动内置的 Watch Dog 线程,默认以锁持有时间的三分之一为间隔自动续期,避免业务逻辑执行时间长于锁 TTL 而导致的误释放。其二,锁支持可重入,同一线程多次加锁会累计计数,只有解锁次数与加锁次数相等时才会真正释放。其三,Redisson 提供了基于 Redis 哈希结构的存储方式,将线程标识(UUID + threadId)作为字段,重入次数作为值,完美映射了可重入语义。
@Configuration
public class RedissonConfig {
@Bean
public RedissonClient redissonClient() {
Config config = new Config();
config.useSingleServer()
.setAddress("redis://localhost:6379")
.setPassword("secret")
.setConnectionMinimumIdleSize(4)
.setConnectionPoolSize(16);
return Redisson.create(config);
}
}
@Service
public class OrderService {
@Autowired
private RedissonClient redissonClient;
/**
* 看门狗自动续期分布式锁
*/
public void deduceStock(String productId) {
RLock lock = redissonClient.getLock("lock:stock:" + productId);
try {
// 等待 10 秒,锁自动续期(leaseTime = -1 时启用看门狗)
boolean locked = lock.tryLock(10, -1, TimeUnit.SECONDS);
if (!locked) {
throw new RuntimeException("获取库存锁失败");
}
// 执行业务扣减库存...
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
}
/**
* 限流器:每秒最多放行 N 个请求
*/
public boolean tryAcquire(String apiKey, long rate) {
RRateLimiter limiter = redissonClient.getRateLimiter("rate:" + apiKey);
limiter.trySetRate(RateType.OVERALL, rate, 1, RateIntervalUnit.SECONDS);
return limiter.tryAcquire(1);
}
/**
* 布隆过滤器:大规模去重与缓存穿透防护
*/
public void initBloomFilter(List<String> allUserIds) {
RBloomFilter<String> bloom = redissonClient.getBloomFilter("bf:user");
// 预期插入 1 亿条,误判率 0.01
bloom.tryInit(100_000_000L, 0.01);
for (String uid : allUserIds) {
bloom.add(uid);
}
}
public boolean mightContain(String userId) {
return redissonClient.getBloomFilter("bf:user").contains(userId);
}
}
6. Redis Pub/Sub 与 Stream 消息队列
Redis 不仅是缓存层,也具备轻量级消息通信能力。Redis Pub/Sub 采用基于频道的发布订阅模型,生产者向特定频道广播消息,所有在线订阅该频道的消费者均可实时接收。其特点是低延迟、即发即弃,但消息不做持久化,若消费者离线则消息永久丢失,因此仅适合对可靠性要求不高的实时通知、配置推送或室播场景。
Redis Stream 是 Redis 5.0 引入的日志型消息队列抽象,借鉴了 Kafka 的分区与消费者组概念。Stream 中的每条消息包含唯一 ID(时间戳 + 序列号),可持久化存储;消费者组支持 ACK 确认与未决列表(PEL)重传,天然具备故障恢复与削峰填谷能力。Spring Data Redis 对 Stream 提供了 ReactiveStreamOperations 与监听器容器 StreamMessageListenerContainer,可方便地集成到 Spring Boot 事件驱动架构中。
@Service
public class MessagePublisher {
@Autowired
private StringRedisTemplate redisTemplate;
public void publishEvent(String channel, String payload) {
redisTemplate.convertAndSend(channel, payload);
}
}
@Component
public class OrderEventListener implements MessageListener {
@Override
public void onMessage(Message message, byte[] pattern) {
String body = new String(message.getBody());
System.out.println("收到订单事件: " + body);
}
}
@Configuration
public class PubSubConfig {
@Bean
RedisMessageListenerContainer container(
RedisConnectionFactory cf, OrderEventListener listener) {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(cf);
container.addMessageListener(listener,
new PatternTopic("order:*"));
return container;
}
}
Stream 消费者组示例:
@Service
public class StreamConsumer {
@Autowired
private StringRedisTemplate redisTemplate;
public void consumeStream(String streamKey, String group, String consumer) {
StreamOperations<String, Object, Object> ops = redisTemplate.opsForStream();
List<MapRecord<String, Object, Object>> records = ops.read(
Consumer.from(group, consumer),
StreamReadOptions.empty().block(Duration.ofSeconds(2)),
StreamOffset.create(streamKey, ReadOffset.lastConsumed())
);
if (records == null || records.isEmpty()) return;
for (MapRecord<String, Object, Object> record : records) {
System.out.println("Stream ID: " + record.getId() +
", Payload: " + record.getValue());
// 业务处理成功后显式 ACK
ops.acknowledge(streamKey, group, record.getId());
}
}
}
7. Pipeline 批量操作与 Lua 脚本原子性
Redis 的单线程命令执行模型保证了每条命令的原子性,但在批量读写场景下,逐条发送命令产生的 RTT(Round-Trip Time)累积会显著拖慢吞吐量。Redis Pipeline 允许客户端将多条命令一次性打包发送到服务端,服务端顺序执行后再一次性返回结果,从而将 N 次网络往返压缩为 1 次,理想情况下吞吐可提升一个数量级。
Spring Data Redis 通过 executePiped 或 execute 的回调接口暴露 Pipeline 能力。Pipeline 本身不保证所有命令的整体原子性,它只是减少了网络交互次数;如果业务要求多条命令作为一个不可分割的整体执行,则应使用 Lua 脚本。Lua 脚本在 Redis 服务端以单线程顺序执行,天然隔离并发干扰,且 EVALSHA 可缓存脚本摘要,避免重复传输脚本体。
@Service
public class PipelineService {
@Autowired
private StringRedisTemplate redisTemplate;
/**
* Pipeline 批量写入 1000 条计数
*/
public List<Object> batchIncrement(List<String> keys) {
return redisTemplate.executePipelined((RedisCallback<Object>) connection -> {
StringRedisConnection strConn = (StringRedisConnection) connection;
for (String key : keys) {
strConn.incr(key.getBytes(StandardCharsets.UTF_8));
}
return null;
});
}
/**
* Lua 原子脚本:库存扣减,仅在充足时扣减并返回实际扣减数量
*/
public Long safeDecrement(String stockKey, long request) {
String lua =
"local stock = tonumber(redis.call('get', KEYS[1]) or 0); " +
"if stock >= tonumber(ARGV[1]) then " +
" redis.call('decrby', KEYS[1], ARGV[1]); " +
" return ARGV[1]; " +
"else " +
" return 0; " +
"end";
RedisScript<Long> script = new DefaultRedisScript<>(lua, Long.class);
return redisTemplate.execute(script,
Collections.singletonList(stockKey), String.valueOf(request));
}
}
8. Spring Session Redis 与多级缓存(Caffeine + Redis)
在分布式微服务架构中,HTTP Session 的状态共享是常见痛点。Spring Session 提供了与 Servlet 容器无关的 Session 抽象后端,将 Session 数据外置到 Redis 后,任何无状态服务实例均可访问同一份会话,从而实现水平扩展与优雅滚动发布。引入 spring-session-data-redis 后,只需在配置类上标注 @EnableRedisHttpSession,原有的 HttpSession 即自动委托给 Redis 存储,对业务代码零侵入。
然而纯 Redis 缓存的访问延迟相比本地内存仍有 0.5~2ms 差距,对于超高 QPS 的热点数据,业界通常采用 Caffeine(本地一级缓存)+ Redis(远程二级缓存)的多级缓存架构。本地缓存命中可降至纳秒级,极大地缓解 Redis 集群压力与网络带宽消耗。多级缓存的核心挑战在于一致性:写入时应先写数据库,再失效 Redis,最后广播或本地 TTL 驱动失效 Caffeine;读取时先查 Caffeine,再查 Redis,再回源数据库。
@Configuration
@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 1800,
redisNamespace = "app:session")
public class SessionConfig {
}
多级缓存 CacheManager 结合 Caffeine 与 Redis:
@Configuration
public class MultiLevelCacheConfig {
@Bean
public CacheManager cacheManager(RedisConnectionFactory cf) {
CaffeineCacheManager local = new CaffeineCacheManager();
local.setCaffeine(Caffeine.newBuilder()
.maximumSize(10_000)
.expireAfterWrite(Duration.ofMinutes(1)));
RedisCacheManager remote = RedisCacheManager.builder(cf)
.cacheDefaults(RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(10)))
.build();
return new CompositeCacheManager(local, remote);
}
}
@Service
public class MultiCacheService {
@Autowired
private CacheManager cacheManager;
@Autowired
private StringRedisTemplate redisTemplate;
public String getWithMultiLevel(String key) {
Cache local = cacheManager.getCache("local");
Cache remote = cacheManager.getCache("remote");
String value = local != null ? local.get(key, String.class) : null;
if (value != null) return value;
value = remote != null ? remote.get(key, String.class) : null;
if (value != null) {
if (local != null) local.put(key, value);
return value;
}
// 回源数据库(示例省略)
value = loadFromDatabase(key);
if (remote != null) remote.put(key, value);
if (local != null) local.put(key, value);
return value;
}
private String loadFromDatabase(String key) {
return "db-value-of-" + key;
}
}
9. Redis Sentinel 与 Cluster 高可用配置
生产环境对 Redis 的可用性要求远高于开发环境。Redis Sentinel 提供监控、通知、自动故障转移与配置提供者四大功能:多个 Sentinel 实例共同监控一组主从节点,当主节点宕机时通过 Raft 算法选举 Sentinel Leader,由 Leader 将某台 Slave 提升为 Master,并通知所有客户端更新主节点地址。Sentinel 适用于读写分离场景,数据量能够单机容纳,但对自动故障恢复有强烈需求。
Redis Cluster 则是水平分片方案,整个键空间被划分为 16384 个 slot,由多个主节点分摊持有;每个主节点配备若干从节点作为副本。Cluster 支持在线扩缩容,新增节点时只需迁移部分 slot 即可重新均衡负载。Lettuce 客户端内置了 slot 到节点的映射缓存,并能在收到 MOVED/ASK 重定向后自动刷新拓扑,因此 Spring Boot 工程接入 Cluster 的代码改动极小,仅需修改连接地址为集群节点列表即可。
# Sentinel 模式
spring:
data:
redis:
sentinel:
master: mymaster
nodes:
- sentinel-1:26379
- sentinel-2:26379
- sentinel-3:26379
password: secret
lettuce:
pool:
max-active: 16
# Cluster 模式
spring:
data:
redis:
cluster:
nodes:
- redis-node-1:6379
- redis-node-2:6379
- redis-node-3:6379
- redis-node-4:6379
- redis-node-5:6379
- redis-node-6:6379
max-redirects: 3
password: secret
lettuce:
cluster:
refresh:
adaptive: true
period: 30s
10. 生产监控与性能优化建议
将 Redis 引入生产体系后,监控与调优是保障稳定性的最后防线。关键监控指标应覆盖以下维度:内存使用率(used_memory、used_memory_rss、maxmemory 及淘汰策略触发次数)、连接数(connected_clients、rejected_connections)、命令耗时(慢查询日志与 latency-monitor)、持久化延迟(rdb_last_bgsave_status、aof_last_rewrite_status)、复制偏移量(master_repl_offset、slave_repl_offset 差异)。
性能优化实践方面,首先应避免大 Key 与热 Key。大 Key 会阻塞单线程事件循环,建议对集合类数据按业务维度分片或压缩存储;热 Key 可通过本地缓存、读写分离或 Redis 代理层(如 Twemproxy、Codis、Redis Cluster Proxy)进行负载分摊。其次,合理设置 TTL 与淘汰策略,对缓存类数据启用 allkeys-lru 或 allkeys-lfu,确保内存可控。最后,对于高并发写入,利用 Pipeline 与 Lua 脚本减少 RTT,对于复杂计算可前置到 Lua 中避免多次往返。
@Component
public class RedisHealthIndicator implements HealthIndicator {
@Autowired
private StringRedisTemplate redisTemplate;
@Autowired
private MeterRegistry meterRegistry;
@PostConstruct
public void initMetrics() {
Gauge.builder("redis.memory.used",
() -> info().get("used_memory"))
.register(meterRegistry);
}
@Override
public Health health() {
Properties info = redisTemplate.getConnectionFactory()
.getConnection().info("memory");
long used = Long.parseLong(info.getProperty("used_memory"));
return Health.up()
.withDetail("used_memory_bytes", used)
.build();
}
private Map<String, Long> info() {
Properties p = redisTemplate.getConnectionFactory()
.getConnection().info("memory");
Map<String, Long> map = new HashMap<>();
map.put("used_memory", Long.parseLong(p.getProperty("used_memory")));
return map;
}
}
延伸阅读
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。