09. Java 客户端与 Spring Data Redis

Lettuce vs Jedis、Spring Data Redis Template、Cache 注解与连接池配置

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 提供的默认配置而无需显式声明客户端类型。

特性JedisLettuce
线程安全否(需连接池)✅(基于 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;
    }
}

延伸阅读

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「redis」更多文章

  1. 多租户隔离与资源配额:共享 Redis 的边界设计
  2. Key 设计与命名规范:Redis 里唯一的结构约束
  3. 代理与路由方案:客户端直连之外的另一种选择