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
// 存储对象字段
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)
// 写入(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("请求过于频繁");
}相关链接
- Redis 基础命令
- Redis 专题
- Redis 客户端线程池与连接池
- 缓存 — Spring Cache 抽象 + Redis 作为缓存后端
- 分布式锁 — Redisson / SETNX 分布式锁实现
- Session管理 — Spring Session 将 Session 存储到 Redis
- 限流 — Redis + Lua 实现滑动窗口限流
- 接口幂等性 — 基于 Redis SETNX 的幂等 Token