Redis 集成

Spring Boot 通过 spring-boot-starter-data-redis 提供 Redis 集成,底层可选 Lettuce(默认,基于 Netty,支持同步/异步/响应式)或 Jedis(同步阻塞)。Lettuce 普通命令默认可共享线程安全的 native connection,并非所有应用都需要连接池。

Redis 命令与 redis-cli 速查:Redis 基础命令。


依赖与配置

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- 仅在确实需要池化时加入:阻塞命令、事务连接或压测证明有收益 -->
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
</dependency>

单机

spring:
  data:
    redis:
      host: localhost
      port: 6379
      password: secret          # 无密码则省略
      database: 0
      timeout: 2s               # 命令超时
      connect-timeout: 1s       # 建连超时

可选:Lettuce 连接池

spring:
  data:
    redis:
      lettuce:
        pool:
          max-active: 16
          max-idle: 8
          min-idle: 2
          max-wait: 500ms

不要仅因为 Web 线程很多就等量扩大 Redis 连接池。Lettuce 可以在一条连接上复用多个普通在途命令;池更适合 BLPOP/阻塞 XREAD、MULTI/EXEC 等需要独占连接状态的操作。详见 线程池与客户端连接。

哨兵(Sentinel)

spring:
  data:
    redis:
      sentinel:
        master: mymaster
        nodes:
          - sentinel1:26379
          - sentinel2:26379
          - sentinel3:26379
      password: secret

集群(Cluster)

spring:
  data:
    redis:
      cluster:
        nodes:
          - node1:7001
          - node2:7002
          - node3:7003
        max-redirects: 3

客户端线程安全与连接模型

业务线程 → RedisTemplate → LettuceConnectionFactory
                              ├─ 普通命令:共享 native connection
                              └─ 阻塞/事务:独立或池化 connection
                                      │
                                      ▼
                              Netty EventLoop → Redis
对象能否跨线程共享说明
RedisTemplate / StringRedisTemplate可以由模板管理连接生命周期
Lettuce StatefulRedisConnection普通操作可以原生连接线程安全;阻塞/事务状态不应混用
Spring RedisConnection不可以包装层含事务/Pipeline 等可变状态,不要存为共享字段
Jedis connection不可以每次从 JedisPool 借用并在 finally/try-with-resources 归还
RedissonClient可以内部管理 Netty EventLoop、按节点命令池和订阅池

阻塞命令必须隔离

普通池/共享连接:GET、SET、DEL,短超时
阻塞连接:BLPOP、XREAD BLOCK,专用连接
订阅连接:Pub/Sub,长期占用
事务连接:WATCH、MULTI/EXEC,用完立即归还

阻塞命令不会把 Redis 主线程一直卡住,但会占住客户端 connection。若 XREAD BLOCK 0 与缓存查询共用一个小池,少量消费者就可能让所有 GET 等待连接。

异步 API 的线程边界

Lettuce future/reactive 回调可能运行在 Netty EventLoop。不要在回调中执行慢 SQL、同步 HTTP、文件 I/O 或大对象序列化;应切换到有界业务 Executor。异步只是不阻塞调用线程,不会增加 Redis 服务端命令执行核心,也不会自动限制在途请求。


RedisTemplate

Spring Boot 自动配置 RedisTemplate<Object, Object> 和 StringRedisTemplate。

推荐配置:JSON 序列化

默认使用 JDK 序列化(二进制,可读性差)。通常改为 JSON:

@Configuration
public class RedisConfig {
 
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(factory);
 
        Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer<>(
            new ObjectMapper().activateDefaultTyping(
                LaissezFaireSubTypeValidator.instance,
                ObjectMapper.DefaultTyping.NON_FINAL,
                JsonTypeInfo.As.PROPERTY
            ), Object.class
        );
 
        StringRedisSerializer stringSerializer = new StringRedisSerializer();
        template.setKeySerializer(stringSerializer);
        template.setHashKeySerializer(stringSerializer);
        template.setValueSerializer(serializer);
        template.setHashValueSerializer(serializer);
        template.afterPropertiesSet();
        return template;
    }
}

常用操作

String

@Service
public class UserCacheService {
 
    private final StringRedisTemplate redisTemplate;
 
    // 写入,带过期时间
    public void set(String key, String value, Duration ttl) {
        redisTemplate.opsForValue().set(key, value, ttl);
    }
 
    // 读取
    public String get(String key) {
        return redisTemplate.opsForValue().get(key);
    }
 
    // 原子递增(计数器、限流)
    public Long increment(String key) {
        return redisTemplate.opsForValue().increment(key);
    }
 
    // SETNX:不存在才写入(分布式锁原语)
    public Boolean setIfAbsent(String key, String value, Duration ttl) {
        return redisTemplate.opsForValue().setIfAbsent(key, value, ttl);
    }
}

Hash

→ Redis Hash 详解

// 存储对象字段
redisTemplate.opsForHash().put("user:1001", "name", "张三");
redisTemplate.opsForHash().put("user:1001", "age", "30");
 
// 读取单个字段
String name = (String) redisTemplate.opsForHash().get("user:1001", "name");
 
// 读取所有字段
Map<Object, Object> fields = redisTemplate.opsForHash().entries("user:1001");
 
// 批量写入(使用 Map)
Map<String, Object> map = Map.of("name", "李四", "age", 25);
redisTemplate.opsForHash().putAll("user:1002", map);

List

// 左推(队列头部)
redisTemplate.opsForList().leftPush("queue:task", "task1");
 
// 右推(队列尾部,FIFO 队列)
redisTemplate.opsForList().rightPush("queue:task", "task2");
 
// 左弹(消费)
String task = (String) redisTemplate.opsForList().leftPop("queue:task");
 
// 阻塞弹出(最多等待 5 秒)
String item = (String) redisTemplate.opsForList().leftPop("queue:task", Duration.ofSeconds(5));
 
// 范围查询
List<Object> items = redisTemplate.opsForList().range("queue:task", 0, -1);

Set

// 写入
redisTemplate.opsForSet().add("tags:post:1", "java", "spring", "redis");
 
// 判断是否存在
Boolean exists = redisTemplate.opsForSet().isMember("tags:post:1", "java");
 
// 集合运算(求并集)
Set<Object> union = redisTemplate.opsForSet().union("tags:post:1", "tags:post:2");

Sorted Set(ZSet)

→ Redis ZSet 详解

// 写入(score 用于排序)
redisTemplate.opsForZSet().add("rank:score", "player1", 9500);
redisTemplate.opsForZSet().add("rank:score", "player2", 8800);
 
// 递增 score(积分累加)
redisTemplate.opsForZSet().incrementScore("rank:score", "player1", 200);
 
// 按 score 降序取 Top 10
Set<ZSetOperations.TypedTuple<Object>> top10 =
    redisTemplate.opsForZSet().reverseRangeWithScores("rank:score", 0, 9);
 
// 查询排名(0-based,降序)
Long rank = redisTemplate.opsForZSet().reverseRank("rank:score", "player1");

过期时间

// 设置 key 过期
redisTemplate.expire("user:1001", Duration.ofMinutes(30));
 
// 查询剩余时间
Duration ttl = redisTemplate.getExpire("user:1001");
 
// 持久化(移除过期时间)
redisTemplate.persist("user:1001");
 
// 写入时同时设置过期(原子操作)
redisTemplate.opsForValue().set("token:" + userId, token, Duration.ofHours(2));

Pipeline(批量命令)

减少网络往返,适合批量写入:

List<Object> results = redisTemplate.executePipelined((RedisCallback<Object>) connection -> {
    for (int i = 0; i < 1000; i++) {
        connection.stringCommands().set(
            ("key:" + i).getBytes(),
            ("value:" + i).getBytes()
        );
    }
    return null;
});

Lua 脚本(原子操作)

多命令需要原子执行时使用 Lua,Redis 保证脚本执行不被中断:

// 原子地「读取 → 判断 → 删除」(用于锁释放)
String script = """
    if redis.call('get', KEYS[1]) == ARGV[1] then
        return redis.call('del', KEYS[1])
    else
        return 0
    end
    """;
 
Long result = redisTemplate.execute(
    new DefaultRedisScript<>(script, Long.class),
    List.of("lock:order:123"),
    "unique-lock-value"
);

Pub/Sub(发布订阅)

// 发布消息
redisTemplate.convertAndSend("channel:notify", "用户登录事件");
 
// 订阅(需在配置类中注册)
@Configuration
public class RedisSubscribeConfig {
 
    @Bean
    public RedisMessageListenerContainer listenerContainer(
            RedisConnectionFactory factory,
            MessageListenerAdapter adapter) {
        RedisMessageListenerContainer container = new RedisMessageListenerContainer();
        container.setConnectionFactory(factory);
        container.addMessageListener(adapter, new ChannelTopic("channel:notify"));
        return container;
    }
 
    @Bean
    public MessageListenerAdapter messageListenerAdapter(NotifySubscriber subscriber) {
        return new MessageListenerAdapter(subscriber, "onMessage");
    }
}
 
@Component
public class NotifySubscriber {
    public void onMessage(String message) {
        System.out.println("收到消息: " + message);
    }
}

Pub/Sub 消息不持久化,消费者离线期间的消息会丢失。需要可靠消息传递时,考虑使用 Stream 或 消息队列。


Redisson(分布式高级特性)

Redisson 在 Redis 基础上封装了分布式锁、信号量、限流器等:

<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
</dependency>

Redisson 的 nettyThreads、用户回调 threads、命令连接池和 Pub/Sub 订阅连接池是四类资源;Cluster 下连接池还会按节点展开。不要把 connectionPoolSize 当作“Redis 命令执行线程数”。

详见 分布式锁,此处仅示例限流器(RRateLimiter):

RRateLimiter limiter = redissonClient.getRateLimiter("limiter:api");
// 初始化:每秒允许 10 次
limiter.trySetRate(RateType.OVERALL, 10, 1, RateIntervalUnit.SECONDS);
 
if (!limiter.tryAcquire()) {
    throw new TooManyRequestsException("请求过于频繁");
}

相关链接