Spring Data Redis 两种编程模型详解:同步 vs 响应式

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;
    }
}

设计要点:

  • 普通业务项目中,自定义声明 RedisTemplate Bean 通常就够了,不必依赖自动配置排序
  • 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>

可以看到,两种模板的方法名几乎一样,唯一的区别是返回值 :同步直接返回结果,响应式返回 MonoFlux

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 序列化

十、延伸阅读

相关推荐
小郑加油1 小时前
python学习Day12:pandas安装与实际运用
开发语言·python·学习
AC赳赳老秦1 小时前
投标合规提效:用 OpenClaw 实现标书 / 合同自动审核、关键词校验、格式优化,降低废标风险
开发语言·前端·python·eclipse·emacs·deepseek·openclaw
海兰2 小时前
【第27篇】Micrometer + Zipkin
人工智能·spring boot·alibaba·spring ai
.柒宇.2 小时前
AI掘金头条项目-K8s部署实战教程
python·云原生·容器·kubernetes·fastapi
phltxy2 小时前
Spring Cloud 分布式服务部署实战:从 0 到 1 实现微服务上线
spring·spring cloud·微服务
观北海2 小时前
从 Sim2Sim 到 Sim2Real:以 ONNX 为核心的机器人策略实机落地全指南
python·机器人
wbs_scy2 小时前
Linux线程同步与互斥(三):线程同步深度解析之POSIX 信号量与环形队列生产者消费者模型,从原理到源码彻底吃透
java·开发语言
KmSH8umpK3 小时前
Redis分布式锁从原生手写到Redisson高阶落地,附线上死锁复盘优化方案进阶第七篇
数据库·redis·分布式
海兰3 小时前
【第28篇】可观测性实战:LangFuse 方案详解
人工智能·spring boot·alibaba·spring ai