Skip to content

整合 Redis

Java 项目整合 Redis,不只是“能 set/get 成功”。真正需要理解的是:客户端连接怎么管理、序列化怎么选、Key 怎么设计、缓存异常怎么处理。

为什么需要客户端封装

应用服务不能直接操作 Redis 协议细节,通常会使用客户端库:

mermaid
flowchart TD
    A["业务代码"] --> B["RedisTemplate / StringRedisTemplate"]
    B --> C["RedisConnectionFactory"]
    C --> D["Lettuce 或 Jedis"]
    D --> E["Redis Server"]

如果不通过成熟客户端,自己处理连接、重连、序列化、超时会非常容易出错。Spring Boot 默认通过 Spring Data Redis 封装常用操作。

Jedis 和 Lettuce

客户端特点注意点
JedisAPI 简单,早期常用,连接不是线程安全多线程要使用连接池
Lettuce基于 Netty,连接可被多个线程共享Spring Boot 2.x 后默认使用

可以简单理解:

  1. Jedis 更像传统阻塞 IO,每个线程最好拿自己的连接。
  2. Lettuce 使用 Netty,适合异步和共享连接。
  3. Spring Boot 默认选择 Lettuce,普通项目优先使用默认方案即可。

Spring Boot 整合流程

mermaid
flowchart TD
    A["添加 starter"] --> B["配置 Redis 地址"]
    B --> C["注入 StringRedisTemplate"]
    C --> D["读写缓存"]
    D --> E["设置 TTL"]
    E --> F["处理缓存穿透和异常降级"]

添加依赖

xml
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

配置连接

Spring Boot 2.x 常见配置:

properties
spring.redis.host=127.0.0.1
spring.redis.port=6379
spring.redis.timeout=3s
spring.redis.lettuce.pool.max-active=16
spring.redis.lettuce.pool.max-idle=8
spring.redis.lettuce.pool.min-idle=2

如果使用 Spring Boot 3.x,新版本配置前缀通常是:

properties
spring.data.redis.host=127.0.0.1
spring.data.redis.port=6379
spring.data.redis.timeout=3s

具体以前项目的 Spring Boot 版本为准。

StringRedisTemplate Demo

如果只是存字符串或 JSON,优先使用 StringRedisTemplate,它的 key 和 value 默认都是字符串序列化,更直观。

java
@Service
public class ArticleCacheService {

    private final StringRedisTemplate stringRedisTemplate;
    private final ObjectMapper objectMapper;

    public ArticleCacheService(StringRedisTemplate stringRedisTemplate,
                               ObjectMapper objectMapper) {
        this.stringRedisTemplate = stringRedisTemplate;
        this.objectMapper = objectMapper;
    }

    public void cacheArticle(Article article) throws JsonProcessingException {
        String key = "article:detail:" + article.getId();
        String value = objectMapper.writeValueAsString(article);
        stringRedisTemplate.opsForValue().set(key, value, Duration.ofMinutes(30));
    }

    public Article getArticle(Long id) throws JsonProcessingException {
        String key = "article:detail:" + id;
        String value = stringRedisTemplate.opsForValue().get(key);
        if (value == null) {
            return null;
        }
        return objectMapper.readValue(value, Article.class);
    }
}

为什么推荐显式设置 TTL:缓存如果没有过期时间,旧数据可能长期存在,内存也可能不断增长。

RedisTemplate 序列化为什么重要

默认 RedisTemplate<Object,Object> 可能使用 JDK 序列化,Redis 中看到的 key/value 不直观,也不利于跨语言访问。

常见问题:

问题后果
Key 使用 JDK 序列化Redis CLI 看到乱码 key
Value 类型不统一反序列化失败
使用过期的 default typing存在安全风险
多个服务序列化策略不同同一个 key 互相读不出来

自定义配置示例:

java
@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(factory);

        StringRedisSerializer stringSerializer = new StringRedisSerializer();
        GenericJackson2JsonRedisSerializer jsonSerializer =
                new GenericJackson2JsonRedisSerializer();

        template.setKeySerializer(stringSerializer);
        template.setHashKeySerializer(stringSerializer);
        template.setValueSerializer(jsonSerializer);
        template.setHashValueSerializer(jsonSerializer);
        template.afterPropertiesSet();
        return template;
    }
}

注意:如果项目已经大量使用某种序列化方式,不要直接切换,否则旧缓存可能无法读取。需要灰度或清理旧 key。

缓存查询 Demo

下面是典型旁路缓存写法。

java
public Article getArticle(Long id) throws JsonProcessingException {
    String key = "article:detail:" + id;
    String cache = stringRedisTemplate.opsForValue().get(key);

    if (cache != null) {
        if ("NULL".equals(cache)) {
            return null;
        }
        return objectMapper.readValue(cache, Article.class);
    }

    Article article = articleMapper.selectById(id);
    if (article == null) {
        stringRedisTemplate.opsForValue().set(key, "NULL", Duration.ofMinutes(5));
        return null;
    }

    stringRedisTemplate.opsForValue().set(
            key,
            objectMapper.writeValueAsString(article),
            Duration.ofMinutes(30)
    );
    return article;
}

这个 Demo 解决两个问题:

  1. 正常数据命中缓存,减少数据库压力。
  2. 不存在的数据缓存空值,减少缓存穿透。

更新数据时怎么处理缓存

常见策略是“先更新数据库,再删除缓存”。

mermaid
flowchart TD
    A["修改文章"] --> B["更新 MySQL"]
    B --> C["删除 Redis 缓存"]
    C --> D["下一次读取未命中"]
    D --> E["查 MySQL 并重建缓存"]

示例:

java
@Transactional
public void updateArticle(Article article) {
    articleMapper.updateById(article);
    stringRedisTemplate.delete("article:detail:" + article.getId());
}

为什么不是先删缓存再更新数据库:先删缓存后,如果数据库还没更新完成,另一个请求可能立刻查库并把旧值写回缓存。

严格一致性很难只靠缓存保证。多数业务采用短 TTL、删除缓存、异步补偿等方式达到最终一致。

常见坑

问题后果建议
不设置超时Redis 卡住拖垮应用线程配置连接和命令超时
不设置 TTL内存持续增长,旧数据不失效缓存必须有过期策略
Key 没有前缀难排查,容易冲突使用 业务:模块:id
直接缓存大对象网络慢、Redis 内存压力大缓存必要字段,拆分大 Key
Redis 异常直接报错缓存故障影响主流程核心查询要考虑降级
锁没有 token可能误删别人的锁使用唯一 token + Lua 删除

小结

Spring Boot 整合 Redis 要掌握四件事:

  1. 连接:优先使用 Spring Boot 默认 Lettuce。
  2. 序列化:key 用字符串,value 明确 JSON 或统一序列化。
  3. 缓存:读缓存、未命中查库、写缓存、设置 TTL。
  4. 治理:处理穿透、击穿、一致性、超时和降级。

set/get 只是第一步,真正上线要考虑序列化兼容、缓存一致性和 Redis 异常时系统是否还能保护数据库。