Spring Data Redis详解
定位:第 03 篇,讲透 RedisTemplate 体系与序列化配置、缓存模式与三大防护、分布式锁实现与局限
适用版本:Spring Data Redis 3.x(Lettuce 客户端)
说明:本篇只叙述 Spring 客户端用法与工程问题
目录
一、客户端抽象
1.1 RedisTemplate 家族
| 模板 | 场景 |
|---|---|
RedisTemplate<K,V> |
通用,键值类型自定义(取决于序列化配置) |
StringRedisTemplate |
键值都是 String,最常用、最不易踩序列化的坑 |
java
// 常用操作族
redis.opsForValue().set("k", "v", 30, TimeUnit.MINUTES);
redis.opsForHash().put("user:1", "name", "tom");
redis.opsForZSet().add("rank", "u1", 99);
redis.opsForList().leftPush("queue", "msg");
1.2 底层连接
Lettuce(默认):基于 Netty 非阻塞,单连接多路复用,线程安全
Jedis:阻塞式,需连接池
集群/哨兵模式:连接工厂相应配置(cluster/sentinel)
二、序列化
2.1 序列化器决定读写行为
RedisTemplate 有四个序列化点:键、值、Hash 键、Hash 值。默认 JdkSerializationRedisSerializer------字节不可读、跨语言不兼容、类变更易失败,生产必须显式配置:
java
@Bean
RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory cf) {
RedisTemplate<String, Object> t = new RedisTemplate<>();
t.setConnectionFactory(cf);
t.setKeySerializer(new StringRedisSerializer()); // 键可读
t.setValueSerializer(new GenericJackson2JsonRedisSerializer()); // JSON 值
t.setHashKeySerializer(new StringRedisSerializer());
t.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
return t;
}
2.2 序列化选型
| 序列化器 | 特点 |
|---|---|
| StringRedisSerializer | 字符串,键首选 |
| Jackson2JsonRedisSerializer | JSON 指定类型(需配类型) |
| GenericJackson2JsonRedisSerializer | JSON + 类型信息(@class),通用但有反序列化面 |
| Protobuf/Kryo | 紧凑高性能,跨语言/内部使用 |
纪律:值里带类型信息(Generic)时注意反序列化安全面(同序列化攻击话题);缓存类数据可用无类型 JSON + 明确反序列化目标类型。
三、缓存模式
3.1 Cache-Aside(旁路缓存,主流)
读:查缓存 → 命中返回;未命中 → 查数据库 → 回填缓存(带 TTL)→ 返回
写:更新数据库 → 删除缓存(而不是更新缓存)
为什么删而不是更新缓存:并发写时更新缓存有覆盖旧值风险;删除让下次读重建,天然拿最新。
3.2 三大防护
| 问题 | 现象 | 对策 |
|---|---|---|
| 缓存穿透 | 查不存在的 key,每次都打数据库 | 缓存空值(短 TTL)/ 布隆过滤器拦截 |
| 缓存击穿 | 热点 key 过期瞬间大量并发打库 | 互斥重建(加锁只放一个请求回源)/ 热点永不过期 + 异步刷新 |
| 缓存雪崩 | 大量 key 同时过期/Redis 故障 | 过期时间加随机抖动、多级缓存、限流降级 |
3.3 一致性说明
缓存与数据库的最终一致:
删缓存后短暂窗口可能回填旧值(并发读)→ 延迟双删 / 订阅 binlog 删缓存
强一致需求高的数据 → 不适合放缓存,或读主库
与 @Cacheable 注解缓存(SD-06)的关系:注解是 Cache-Aside 的声明式封装,工程问题(穿透/一致性)同样存在。
四、分布式锁
4.1 基本实现
加锁:SET lockKey requestId NX PX 30000
NX:不存在才设置(互斥)
PX:过期时间(防持有者崩溃后死锁)
requestId:持有者标识(释放时校验)
释放:Lua 脚本保证"比对 + 删除"原子
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
end
java
// Spring 侧(6.1+ 有 RedisLockRegistry,或自行实现)
Boolean ok = redis.opsForValue().setIfAbsent(key, requestId, 30, TimeUnit.SECONDS);
4.2 关键问题
| 问题 | 说明 | 对策 |
|---|---|---|
| 锁过期但业务未完成 | 锁提前释放,别人进入 | 看门狗续期(Redisson 内置) |
| 误删他人锁 | 不校验持有者直接删 | 比对 requestId + Lua 原子 |
| 主从切换丢锁 | 主节点写入未同步即宕机,锁丢失 | Redlock(多节点,有争议)/ 接受弱保证 |
4.3 选型纪律
一般互斥(防重复任务、并发重建缓存)→ 单节点 Redis 锁足够
强一致要求的锁(资金类互斥)→ 数据库唯一约束/悲观锁更可靠
生产直接用 Redisson(RLock 封装看门狗/可重入),少手写
五、Repository 形态
java
@RedisHash("users")
public class User {
@Id private Long id;
@Indexed private String email; // 二级索引
private String name;
}
public interface UserRedisRepo extends CrudRepository<User, Long> { }
- 实体存为 Hash(key =
users:{id}),@Indexed 字段建二级索引(users:email:{value}); - 适合简单对象存取;复杂操作(管道、Lua、多结构)直接用 RedisTemplate。
六、总结
- 客户端:RedisTemplate/StringRedisTemplate + 操作族;Lettuce 默认(非阻塞单连接);集群/哨兵配连接工厂。
- 序列化:四个序列化点必须显式配置;键用 String、值用 JSON(注意类型信息与安全)。
- 缓存:Cache-Aside 主流,写删不写更;穿透/击穿/雪崩三防护;一致性靠删缓存 + 双删/订阅兜底。
- 分布式锁:SET NX PX + Lua 释放 + 看门狗续期;主从切换丢锁是固有局限;生产用 Redisson,强一致场景用数据库。
- Repository:@RedisHash 简单存取 + @Indexed,复杂场景回模板。
七、常见高频面试题
1. RedisTemplate 为什么要自定义序列化?默认有什么问题?
要点:默认用 JdkSerializationRedisSerializer------序列化结果是二进制不可读(控制台排查困难)、依赖 Java 类定义(类结构变更易反序列化失败)、跨语言不兼容。生产应显式配置:键用 StringRedisSerializer(可读),值按需求用 JSON(GenericJackson2 带类型或指定类型)。四个序列化点(键/值/Hash 键/Hash 值)都要配。另外纯字符串场景直接用 StringRedisTemplate 最简单。
2. 描述 Cache-Aside 模式。为什么写操作是删缓存而不是更新缓存?
要点:读:先查缓存,命中返回;未命中查数据库,回填缓存(带 TTL)后返回。写:先更新数据库,再删除缓存(下次读重建)。删而不更新的原因:并发写时更新缓存有旧值覆盖风险------A、B 先后写库,若 A 的缓存更新晚于 B,缓存停留在旧值;删除让下次读取最新库数据天然重建。补充:删除后短暂窗口可能被并发读回填旧值,用延迟双删或订阅 binlog 删缓存缓解。
3. 缓存穿透、击穿、雪崩分别是什么?怎么防?
要点:穿透------查询不存在的 key,缓存永远未命中,请求全打数据库;防:缓存空值(短 TTL)或布隆过滤器前置拦截非法请求。击穿------某个热点 key 过期瞬间,大量并发同时回源打库;防:互斥重建(加锁只放一个请求回源其余等待)、热点数据逻辑过期 + 异步刷新。雪崩------大量 key 同时过期或 Redis 故障,请求集中涌向数据库;防:过期时间加随机抖动打散、多级缓存、熔断限流降级。
4. 用 Redis 实现分布式锁的要点和缺陷?
要点:实现:SET key requestId NX PX ttl(互斥 + 过期防死锁 + 持有者标识);释放用 Lua 脚本比对 requestId 再删(原子,防误删);业务可能超锁时长,需看门狗自动续期(Redisson 内置)。缺陷:① 主从异步复制下,主节点宕机锁未同步到新主,锁丢失(Redlock 多节点方案有争议);② 时钟与网络假设问题。结论:一般互斥够用;资金类强一致互斥建议数据库约束或悲观锁;生产直接用 Redisson 而非手写。
5. 缓存和数据库的一致性怎么保证?
要点:最终一致是现实目标。主流:先更新库再删缓存(Cache-Aside)。残余问题:删缓存瞬间并发读回填旧值------缓解:延迟双删(更新库 → 删缓存 → 延迟再删一次)、订阅 binlog(Canal)异步删缓存(可靠但架构变重)。选择:一致性要求高的数据不放缓存或读主库;可容忍秒级不一致的用删缓存即可。注意:双写(更新库 + 更新缓存)因并发覆盖问题不推荐。
6. Lettuce 和 Jedis 的区别?
要点:连接模型不同。Lettuce 基于 Netty 非阻塞------单连接多路复用,线程安全,RedisTemplate 共享一个连接即可,默认选择;支持集群/哨兵/响应式。Jedis 阻塞式------每操作占一个连接,需要连接池管理,实例非线程安全。选择:默认 Lettuce;Jedis 在特定老项目或对阻塞语义有偏好时使用。注意阻塞客户端在高并发下的连接池配置与超时。
7. 热点数据怎么缓存?
要点:热点(极少数 key 占大多数流量)的特殊处理:① 不过期策略------逻辑过期(值里存过期时间,读到过期异步刷新,期间返回旧值),避免过期瞬间击穿;② 互斥重建------过期时加锁只放一个请求回源,其余等待或返回旧值;③ 本地缓存叠加------多级缓存(Caffeine 本地 + Redis),热点在本地抗大部分流量;④ 监控热点 key(Redis 4.0+ hotkeys),提前预案。原则:热点的可用性优先于实时性。
8. StringRedisTemplate 和 RedisTemplate 的区别?
要点:StringRedisTemplate 是 RedisTemplate<String,String> 的特化------键值序列化固定为 StringRedisSerializer,存的就是字符串(数字也是字符串形式)。好处:无序列化配置坑、控制台可读、跨语言兼容。RedisTemplate 泛型通用,但必须自行配置序列化器(否则默认 JDK 序列化)。实践:纯字符串/JSON 字符串存取用 StringRedisTemplate(存 JSON 时自己序列化),需要对象直存再配泛型模板。
9. Redis 的 Pipeline(管道)解决什么问题?Spring 里怎么用?
要点:多个命令逐条往返的网络开销大(每条一个 RTT)。管道把一批命令打包一次发送、响应批量返回,把 N 个 RTT 降为约 1 个,显著提升批量操作吞吐。Spring 中用 redis.executePipelined(...),回调里批量执行命令。注意:管道不是原子操作(命令间可能插入他人命令);批量过大注意内存与超时;需要原子性用 Lua 脚本或事务(MULTI/EXEC)。
10. @RedisHash 的 Repository 适合什么场景?
要点:把实体以 Hash 结构存入 Redis(key = 前缀:{id},字段对应属性),配合 CrudRepository 提供简单存取;@Indexed 字段建二级索引支持按字段查找。适合:会话、短期对象存取、简单缓存实体等"整体存取 + 偶尔按索引查"的场景。不适合:复杂查询(无查询语言)、聚合操作、需要精细控制结构/过期策略的场景------这些直接用 RedisTemplate。定位是便利封装,不是 Redis 版 ORM。
