Spring Data Redis 两种编程模型详解:同步 vs 响应式
本文系统性地介绍 Spring Data Redis 的两种编程模型------同步阻塞(RedisTemplate)与响应式非阻塞(ReactiveRedisTemplate),通过通俗易懂的示例帮助你理解它们的区别、适用场景以及在实际项目中的选型策略。
一、为什么需要两种模型?
在 Java Web 开发中,存在两种主流的编程范式:
┌─────────────────────────────────────────────────────────────────┐
│ Java Web 两大编程范式 │
├───────────────────────────┬─────────────────────────────────────┤
│ 同步阻塞 (Servlet) │ 响应式非阻塞 (Reactive) │
│ │ │
│ 基于线程池模型 │ 基于事件循环模型 │
│ 一个请求占用一个线程 │ 少量线程处理大量请求 │
│ Spring MVC + Tomcat │ Spring WebFlux + Netty │
│ 代码简单直观 │ 吞吐量更高,编程模型更复杂 │
└───────────────────────────┴─────────────────────────────────────┘
Spring Data Redis 需要同时适配这两种范式,因此提供了两套 API:
| 模型 | 核心类 | 返回值 | 适用场景 |
|---|---|---|---|
| 同步(阻塞) | RedisTemplate |
直接返回结果 | Spring MVC(Tomcat 等线程池容器) |
| 响应式(非阻塞) | ReactiveRedisTemplate |
返回 Mono / Flux |
Spring WebFlux、Spring Cloud Gateway(Netty) |
二、同步阻塞模型:RedisTemplate
2.1 工作原理
线程 A ──── 发送 Redis 命令 ──── 等待中... ──── 收到结果 ──── 继续执行
│ │ │
└────── 线程被阻塞 ──────┘ │
(什么也做不了) │
调用 RedisTemplate 的方法时,当前线程会一直等待,直到 Redis 返回结果。这就像你去餐厅点餐,站在柜台前一直等,直到服务员把菜端上来。
2.2 基本使用
java
@Autowired
private RedisTemplate<String, Object> redisTemplate;
// 字符串操作
redisTemplate.opsForValue().set("user:name:1001", "张三");
String name = (String) redisTemplate.opsForValue().get("user:name:1001");
// 设置过期时间(常用于 Token、验证码等场景)
redisTemplate.opsForValue().set("token:abc123", "userId:1001", 30, TimeUnit.MINUTES);
// 判断 key 是否存在
Boolean exists = redisTemplate.hasKey("token:abc123");
// Hash 操作(适合存储对象的多个字段)
redisTemplate.opsForHash().put("user:1001", "name", "张三");
redisTemplate.opsForHash().put("user:1001", "age", 25);
Map<Object, Object> userMap = redisTemplate.opsForHash().entries("user:1001");
// 删除
redisTemplate.delete("user:name:1001");
2.3 配置 RedisTemplate
Spring Boot 默认的 RedisTemplate<Object, Object> 使用 JDK 序列化,存入 Redis 的 Key 和 Value 会是二进制乱码。实际开发中几乎都需要自定义序列化:
java
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
// Key 使用字符串序列化(避免乱码)
template.setKeySerializer(new StringRedisSerializer());
template.setHashKeySerializer(new StringRedisSerializer());
// Value 使用 JSON 序列化(方便存储 Java 对象)
GenericJackson2JsonRedisSerializer jsonSerializer = new GenericJackson2JsonRedisSerializer();
template.setValueSerializer(jsonSerializer);
template.setHashValueSerializer(jsonSerializer);
template.afterPropertiesSet();
return template;
}
}
设计要点:
- 普通业务项目中,自定义声明
RedisTemplateBean 通常就够了,不必依赖自动配置排序 - Key 用
StringRedisSerializer:Redis 中看到的是可读字符串,而非乱码 - Value 用 JSON 序列化:支持存储复杂 Java 对象
补充说明:
@AutoConfigureBefore(RedisAutoConfiguration.class)更多用于自定义自动配置场景。对大多数应用代码里的@Configuration类来说,并不是自定义RedisTemplate的必需条件。
Redis 中的存储效果对比:
默认 JDK 序列化(乱码,不可维护):
\xac\xed\x00\x05t\x00\x04name → \xac\xed\x00\x05t\x00\x06\xe5\xbc\xa0\xe4\xb8\x89
自定义序列化后(可读,方便调试):
user:name:1001 → "张三"
user:1001 → {"id":1001,"name":"张三","role":"admin"}
2.4 封装 RedisService 工具类
在实际项目中,通常会封装一个 RedisService,屏蔽底层 API 细节:
java
@Component
public class RedisService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
/**
* 缓存对象
*/
public <T> void set(String key, T value) {
redisTemplate.opsForValue().set(key, value);
}
/**
* 缓存对象(带过期时间)
*/
public <T> void set(String key, T value, long timeout, TimeUnit timeUnit) {
redisTemplate.opsForValue().set(key, value, timeout, timeUnit);
}
/**
* 获取缓存对象
*/
@SuppressWarnings("unchecked")
public <T> T get(String key) {
return (T) redisTemplate.opsForValue().get(key);
}
/**
* 判断 key 是否存在
*/
public boolean hasKey(String key) {
return Boolean.TRUE.equals(redisTemplate.hasKey(key));
}
/**
* 删除缓存
*/
public boolean delete(String key) {
return Boolean.TRUE.equals(redisTemplate.delete(key));
}
/**
* 设置过期时间
*/
public boolean expire(String key, long timeout, TimeUnit timeUnit) {
return Boolean.TRUE.equals(redisTemplate.expire(key, timeout, timeUnit));
}
}
好处:
- 业务代码只需注入
RedisService,不用关心底层配置细节 - 统一封装泛型方法,减少重复代码
- 集中处理空值安全(
Boolean.TRUE.equals())
使用示例:
java
@Service
public class UserService {
@Autowired
private RedisService redisService;
public User getUserById(Long userId) {
String key = "user:" + userId;
// 先查缓存
User user = redisService.get(key);
if (user != null) {
return user;
}
// 缓存未命中,查数据库
user = userMapper.selectById(userId);
if (user != null) {
// 写入缓存,30 分钟过期
redisService.set(key, user, 30, TimeUnit.MINUTES);
}
return user;
}
}
三、响应式模型:ReactiveRedisTemplate
3.1 工作原理
线程 A ──── 发送 Redis 命令 ──── 立刻返回 Mono ──── 去处理其他请求
│
│ (后台等待 Redis 返回)
│
└── Redis 返回后,回调触发 ──── 处理结果
调用 ReactiveRedisTemplate 时,线程不会等待 ,而是立刻拿到一个 Mono(表示"未来会有一个结果")。这就像你在餐厅扫码点餐后就回座位了,菜好了服务员会送过来。
3.2 核心概念:Mono 和 Flux
Mono<T> ──── 代表 0 或 1 个结果 ──── 类似 Optional<T>
Flux<T> ──── 代表 0 到 N 个结果 ──── 类似 Stream<T>
java
// Mono 示例
Mono<String> nameMono = reactiveRedisTemplate.opsForValue().get("user:name:1001");
// 此时还没有真正查询 Redis!只是定义了一个"操作计划"
// 订阅后才真正执行
nameMono.subscribe(name -> {
System.out.println("拿到结果:" + name);
});
// 在 WebFlux 中,通常直接返回 Mono,框架会自动订阅
@GetMapping("/user/name")
public Mono<String> getUserName() {
return reactiveRedisTemplate.opsForValue().get("user:name:1001");
}
3.3 基本使用
java
@Autowired
private ReactiveRedisTemplate<String, String> reactiveRedisTemplate;
// 字符串操作(所有操作返回 Mono/Flux,不会阻塞线程)
Mono<Boolean> setResult = reactiveRedisTemplate.opsForValue()
.set("user:name:1001", "张三");
Mono<String> getResult = reactiveRedisTemplate.opsForValue()
.get("user:name:1001");
// 判断 key 是否存在
Mono<Boolean> exists = reactiveRedisTemplate.hasKey("token:abc123");
// 设置过期时间
Mono<Boolean> expireResult = reactiveRedisTemplate
.expire("token:abc123", Duration.ofMinutes(30));
// 删除
Mono<Long> deleteResult = reactiveRedisTemplate.delete("user:name:1001");
// 链式操作(先写入,再设置过期时间)
reactiveRedisTemplate.opsForValue()
.set("token:abc123", "userId:1001")
.then(reactiveRedisTemplate.expire("token:abc123", Duration.ofMinutes(30)))
.subscribe();
3.4 配置 ReactiveRedisTemplate
java
@Configuration
public class ReactiveRedisConfig {
@Bean
public ReactiveRedisTemplate<String, String> reactiveRedisTemplate(
ReactiveRedisConnectionFactory connectionFactory) {
// 1. 创建序列化器
StringRedisSerializer serializer = new StringRedisSerializer();
// 2. 使用 Builder 模式构建序列化上下文
// 注意:ReactiveRedisTemplate 不能像 RedisTemplate 那样直接 setSerializer
// 必须通过 RedisSerializationContext 来配置
RedisSerializationContext<String, String> context =
RedisSerializationContext.<String, String>newSerializationContext()
.key(serializer)
.value(serializer)
.hashKey(serializer)
.hashValue(serializer)
.build();
// 3. 创建响应式模板
return new ReactiveRedisTemplate<>(connectionFactory, context);
}
}
与 RedisTemplate 配置的区别:
| 区别 | RedisTemplate | ReactiveRedisTemplate |
|---|---|---|
| 连接工厂 | RedisConnectionFactory |
ReactiveRedisConnectionFactory |
| 序列化配置 | 直接调用 setKeySerializer() 等方法 |
必须通过 RedisSerializationContext Builder |
| 初始化 | 需要调用 afterPropertiesSet() |
构造函数传入 context 即可 |
3.5 在 Spring Cloud Gateway 中的典型用法
Spring Cloud Gateway 是响应式框架(基于 WebFlux),最常见的场景是在网关过滤器中查询 Redis 校验 Token:
❌ 错误写法(阻塞式):
java
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String token = getTokenFromRequest(exchange.getRequest());
String key = "token:" + token;
// 这行代码会阻塞 Netty 的 EventLoop 线程!
Boolean exists = redisTemplate.hasKey(key);
if (Boolean.TRUE.equals(exists)) {
return chain.filter(exchange);
}
return unauthorizedResponse(exchange, "Token 无效");
}
✅ 正确写法(响应式):
java
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String token = getTokenFromRequest(exchange.getRequest());
String key = "token:" + token;
// 响应式写法:不阻塞线程
return reactiveRedisTemplate.hasKey(key)
.flatMap(exists -> {
if (exists) {
return chain.filter(exchange);
}
return unauthorizedResponse(exchange, "Token 无效");
});
}
四、核心对比
4.1 API 对比
| 操作 | RedisTemplate(同步) | ReactiveRedisTemplate(响应式) |
|---|---|---|
| 设置值 | template.opsForValue().set(key, value) |
template.opsForValue().set(key, value) → Mono<Boolean> |
| 获取值 | String val = template.opsForValue().get(key) |
Mono<String> val = template.opsForValue().get(key) |
| 判断存在 | Boolean b = template.hasKey(key) |
Mono<Boolean> b = template.hasKey(key) |
| 删除 | Boolean b = template.delete(key) |
Mono<Long> n = template.delete(key) |
| 设置过期 | template.expire(key, 30, TimeUnit.MINUTES) |
template.expire(key, Duration.ofMinutes(30)) → Mono<Boolean> |
可以看到,两种模板的方法名几乎一样,唯一的区别是返回值 :同步直接返回结果,响应式返回 Mono 或 Flux。
4.2 底层驱动对比
RedisTemplate
└── Lettuce / Jedis 驱动(同步模式)
└── 基于连接池,每次操作从池中获取连接
ReactiveRedisTemplate
└── Lettuce 驱动(异步模式)❗ 不支持 Jedis
└── 基于 Netty,共享连接,非阻塞 I/O
注意 :ReactiveRedisTemplate 只支持 Lettuce 驱动,不支持 Jedis。因为 Jedis 是同步阻塞的客户端,无法实现响应式。Spring Boot 2.x 起默认使用 Lettuce,所以大多数情况下不需要额外处理。
4.3 Maven 依赖对比
xml
<!-- 同步模式 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- 响应式模式 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>
五、线程模型深入对比
5.1 同步模型的线程模型
Tomcat 线程池(默认 200 个线程)
┌──────────────────────────────────────────────────┐
│ │
│ Thread-1: 请求A → 查 Redis(等待中...) → 返回 │
│ Thread-2: 请求B → 查 Redis(等待中...) → 返回 │
│ Thread-3: 请求C → 查 Redis(等待中...) → 返回 │
│ ... │
│ Thread-200: 请求N → 查 Redis(等待中...) → 返回 │
│ │
│ Thread-201: 请求X → ❌ 没有空闲线程了,排队等待 │
│ │
└──────────────────────────────────────────────────┘
问题:
- 每个请求独占一个线程
- Redis 响应慢时,线程全部阻塞
- 200 个线程满了,后续请求只能排队
- 高并发下容易出现线程耗尽
5.2 响应式模型的线程模型
Netty EventLoop(默认 CPU 核心数个线程)
┌──────────────────────────────────────────────────┐
│ │
│ EventLoop-1: │
│ → 收到请求A,发送 Redis 命令,注册回调,继续 │
│ → 收到请求B,发送 Redis 命令,注册回调,继续 │
│ → 请求A 的 Redis 回调到了,处理并返回 │
│ → 收到请求C,发送 Redis 命令,注册回调,继续 │
│ → 请求B 的 Redis 回调到了,处理并返回 │
│ → ... │
│ │
│ EventLoop-2: │
│ → (同样处理大量请求) │
│ │
└──────────────────────────────────────────────────┘
优势:
- 少量线程处理大量并发请求
- 线程永远不会被阻塞
- 内存占用更低(不需要大线程池)
- 吞吐量远高于同步模型
5.3 性能对比(示意)
场景:1000 并发请求,每个请求查一次 Redis(耗时 2ms)
同步模型(Tomcat 200 线程):
- 前 200 个请求立即处理
- 后 800 个排队
- 总耗时 ≈ 10ms(5 批 × 2ms)
响应式模型(Netty 8 线程):
- 可以用更少线程承载更多并发请求
- 请求通常不需要在线程上同步等待 Redis 返回
- 在高并发下,线程利用率和吞吐表现通常更好,但总耗时仍取决于 Redis 本身处理能力、网络 RTT、序列化成本和连接模型
注意:这里是帮助理解线程模型差异的示意,并不是严格压测结论。响应式的核心价值是减少阻塞、提升并发承载能力,而不是保证单次请求延迟一定更低。
六、常见问题与踩坑
6.1 踩坑一:在 WebFlux 中误用 RedisTemplate
java
// ❌ 错误:在 WebFlux/Gateway 中使用阻塞式 RedisTemplate
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 这行代码会阻塞 Netty 的 EventLoop 线程!
Boolean exists = redisTemplate.hasKey("token:xxx");
if (exists) {
return chain.filter(exchange);
}
return unauthorizedResponse(exchange);
}
后果:
- Netty 只有少量 EventLoop 线程(通常等于 CPU 核心数)
- 一旦阻塞,整个服务的吞吐量急剧下降
- 高并发时可能导致服务完全无响应
java
// ✅ 正确:使用响应式 ReactiveRedisTemplate
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
return reactiveRedisTemplate.hasKey("token:xxx")
.flatMap(exists -> {
if (exists) {
return chain.filter(exchange);
}
return unauthorizedResponse(exchange);
});
}
6.2 踩坑二:在 Spring MVC 中误用 ReactiveRedisTemplate
java
// ⚠️ 不推荐:在传统 Spring MVC 中使用 ReactiveRedisTemplate
@GetMapping("/user")
public User getUser() {
// 虽然能用,但需要 block() 才能拿到结果
String name = reactiveRedisTemplate.opsForValue().get("name").block();
return new User(name);
}
问题:
- 在纯 MVC / Servlet 链路里,
block()通常会把响应式调用重新变回阻塞式 - 这样往往拿不到响应式带来的收益,反而额外引入了 API 和调试复杂度
- 因此对传统 MVC 项目而言,一般直接使用
RedisTemplate更简单
补充说明:如果某段业务本身就在复用响应式组件,或上游已经是响应式组合链路,那么局部使用
ReactiveRedisTemplate并不天然错误。这里强调的是"通常不推荐",而不是绝对禁止。
6.3 踩坑三:两种模板混用
java
// ✅ 技术上可以共存,但同一模块中不建议
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
// ...
}
@Bean
public ReactiveRedisTemplate<String, String> reactiveRedisTemplate(
ReactiveRedisConnectionFactory factory) {
// ...
}
建议:同一个模块中统一选择一种,避免增加代码理解成本。如果是微服务架构,不同模块可以选不同模型。
七、选型决策树
你的项目用什么 Web 框架?
│
├── Spring MVC(Tomcat/Jetty/Undertow)
│ └── 用 RedisTemplate ✅
│
├── Spring WebFlux(Netty)
│ └── 用 ReactiveRedisTemplate ✅
│
├── Spring Cloud Gateway
│ └── 用 ReactiveRedisTemplate ✅
│ (Gateway 底层是 WebFlux)
│
└── 混合架构(多个微服务)
├── 普通业务服务 → RedisTemplate
└── 网关服务 → ReactiveRedisTemplate
八、完整配置示例
8.1 同步模式完整配置
pom.xml:
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
application.yml:
yaml
spring:
data:
redis:
host: localhost
port: 6379
password: your_password
database: 0
lettuce:
pool:
max-active: 8
max-idle: 8
min-idle: 0
RedisConfig.java:
java
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
StringRedisSerializer stringSerializer = new StringRedisSerializer();
GenericJackson2JsonRedisSerializer jsonSerializer = new GenericJackson2JsonRedisSerializer();
template.setKeySerializer(stringSerializer);
template.setHashKeySerializer(stringSerializer);
template.setValueSerializer(jsonSerializer);
template.setHashValueSerializer(jsonSerializer);
template.afterPropertiesSet();
return template;
}
}
8.2 响应式模式完整配置
pom.xml:
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>
application.yml:(与同步模式相同)
yaml
spring:
data:
redis:
host: localhost
port: 6379
password: your_password
database: 0
ReactiveRedisConfig.java:
java
@Configuration
public class ReactiveRedisConfig {
@Bean
public ReactiveRedisTemplate<String, String> reactiveRedisTemplate(
ReactiveRedisConnectionFactory connectionFactory) {
StringRedisSerializer serializer = new StringRedisSerializer();
RedisSerializationContext<String, String> context =
RedisSerializationContext.<String, String>newSerializationContext()
.key(serializer)
.value(serializer)
.hashKey(serializer)
.hashValue(serializer)
.build();
return new ReactiveRedisTemplate<>(connectionFactory, context);
}
}
九、最佳实践总结
1. 选型原则
- Spring MVC → RedisTemplate,简单直接
- Spring WebFlux / Gateway → ReactiveRedisTemplate,避免阻塞
- 不要混用,同一模块统一用一种
2. 序列化建议
java
// Key 永远用 StringRedisSerializer(保证可读性)
template.setKeySerializer(new StringRedisSerializer());
// Value 根据需求选择:
// - 只存字符串 → StringRedisSerializer
// - 存 Java 对象 → GenericJackson2JsonRedisSerializer
// - 绝对不要用默认的 JdkSerializationRedisSerializer(乱码 + 跨语言不兼容)
3. 封装建议
- 将 Redis 配置放在公共模块中,所有服务共享
- 封装
RedisService工具类,屏蔽底层 API 细节 - 网关模块如果需要响应式,单独配置
ReactiveRedisTemplate
4. 避免踩坑
- 不要在 WebFlux 中调用
redisTemplate.hasKey()(阻塞 EventLoop) - 不要在 MVC 中调用
reactiveRedisTemplate.xxx().block()(多此一举) - 不要用 Jedis 作为响应式驱动(Jedis 不支持异步)
- Gateway 中的 Redis 操作全部用响应式 API
- 永远自定义序列化器,不要用默认的 JDK 序列化