2、Redis缓存基础:Redis数据结构回顾、缓存读写模式、TTL与过期策略
2.1 Redis核心数据结构回顾
Redis 提供了丰富的数据结构,每种结构都有其特定的应用场景和底层编码方式。在缓存一致性设计中,选择正确的数据结构直接影响性能与一致性保障的复杂度。
| 数据结构 | 底层编码 | 典型缓存场景 | 一致性关注点 |
|---|---|---|---|
| String | int / embstr / raw | Session、计数器、简单KV | 原子操作(INCR/DECR)避免竞态 |
| Hash | ziplist / hashtable | 对象属性缓存(用户信息) | 字段级更新,减少大对象全量覆盖 |
| List | quicklist | 消息队列、最新列表 | LPUSH/RPOP 与数据库顺序一致 |
| Set | intset / hashtable | 标签、去重集合 | SADD/SREM 与关系型数据同步 |
| Sorted Set | ziplist / skiplist | 排行榜、延迟队列 | Score 与业务时间戳对齐 |
选型建议: 缓存对象属性频繁部分更新时,优先使用 Hash 而非 String 序列化整个对象,可减少网络传输和序列化开销,同时降低并发写冲突概率。
2.2 缓存读写模式
缓存读写模式决定了应用程序如何与缓存及数据库交互。不同的模式在一致性、复杂度和吞吐量上各有取舍。
2.2.1 Cache-Aside(旁路缓存)
Cache-Aside 是最常用的模式,应用程序同时感知缓存和数据库的存在。
- 读流程: 先查缓存,命中则直接返回;未命中则查数据库,将结果写入缓存后返回。
- 写流程: 先更新数据库,再删除缓存(或更新缓存)。
// 读流程伪代码
public Object get(String key) {
Object value = redis.get(key);
if (value == null) {
value = db.query(key);
if (value != null) {
redis.set(key, value, ttl);
}
}
return value;
}
// 写流程伪代码(推荐:先更新DB,再删除缓存)
public void update(String key, Object newValue) {
db.update(key, newValue);
redis.del(key); // 删除缓存,而非更新
}
常见陷阱: 先删除缓存再更新数据库会导致并发读请求在删除后、更新前读到旧数据并回填缓存,造成长期不一致。务必采用 先更新数据库,后删除缓存 的顺序。
2.2.2 Read-Through / Write-Through(穿透读写)
Read-Through 和 Write-Through 将缓存抽象为数据源,应用程序只与缓存层交互,由缓存层负责与数据库同步。
| 模式 | 读行为 | 写行为 | 一致性保障 |
|---|---|---|---|
| Read-Through | 缓存未命中时,缓存组件自动加载数据库数据并返回 | 通常配合 Write-Through 或 Write-Behind | 缓存层控制加载逻辑,减少应用复杂度 |
| Write-Through | — | 写操作先写入缓存,缓存同步写入数据库,成功后返回 | 强一致性(写缓存与写数据库在同一事务边界) |
| Write-Behind | — | 写操作只更新缓存,异步批量写入数据库 | 最终一致性,吞吐量高,但存在数据丢失风险 |
适用场景: Read-Through + Write-Through 适合对一致性要求高、写频率适中的场景(如用户账户余额)。Write-Behind 适合写密集型、可容忍短时间不一致的场景(如访问计数、日志聚合)。
2.3 TTL与过期策略
TTL(Time To Live)是缓存一致性的最后一道防线。即使业务逻辑出现遗漏,合理的过期策略也能自动恢复一致性。
2.3.1 TTL 设置原则
- 短 TTL(秒级~分钟级): 适用于频繁变化的数据(如实时库存、在线状态),一致性要求高。
- 中 TTL(分钟级~小时级): 适用于变化较慢的数据(如商品详情、用户资料),平衡命中率与一致性。
- 长 TTL(天级): 适用于几乎不变的数据(如配置信息、字典表),需配合主动失效机制。
经验值: 对于大多数业务缓存,建议 TTL 设置在 5~30 分钟之间。过短的 TTL 会导致缓存命中率下降,增加数据库压力;过长的 TTL 会放大不一致窗口。
2.3.2 Redis 过期策略
Redis 采用 定期删除 + 惰性删除 的组合策略,兼顾 CPU 与内存效率。
| 策略 | 触发时机 | 行为 | 优缺点 |
|---|---|---|---|
| 惰性删除 | 每次访问 key 时 | 检查 key 是否过期,过期则删除并返回 nil | CPU 友好,但过期 key 可能长期占用内存 |
| 定期删除 | 每 100ms 随机抽查 | 从设置了过期时间的 key 集合中随机抽取一批,删除其中过期的 key | 平衡内存与 CPU,但无法保证即时清理 |
| 内存淘汰 | 内存达到 maxmemory 时 | 根据淘汰策略(LRU/LFU/TTL/Random)移除 key | 防止 OOM,但可能误删未过期 key |
# 设置 TTL 示例(单位:秒)
SET user:1001:session "abc123" EX 3600 # 1小时后过期
EXPIRE user:1001:session 3600 # 对已有 key 设置 TTL
TTL user:1001:session # 查看剩余存活时间
# 内存淘汰策略配置(redis.conf)
maxmemory 2gb
maxmemory-policy allkeys-lru # 常用:最近最少使用淘汰
注意: 依赖 TTL 自动恢复一致性时,必须考虑 过期时间窗口。例如,数据库更新后缓存 TTL 还剩 10 分钟,这 10 分钟内应用会读到旧数据。对于强一致性场景,应主动删除或更新缓存,而非仅依赖 TTL。
2.3.3 过期策略对一致性的影响
- 惰性删除 可能导致过期 key 在未被访问前一直存在,增加不一致窗口。
- 定期删除 无法保证过期 key 被立即清理,但结合 TTL 上限,不一致时间可控。
- 内存淘汰 在内存压力下可能淘汰未过期的热 key,导致缓存击穿,需配合缓存预热或互斥锁。
在实际系统中,建议将 TTL 作为 兜底机制,主动失效(删除/更新缓存)作为 主要手段,两者结合才能达到高可用与最终一致性的平衡。