2. 缓存基础:缓存的定义、作用与常见产品
2.1 缓存的定义
缓存(Cache)是计算机系统中一种用于临时存储数据副本的高速存储层。当系统需要读取数据时,会优先从缓存中获取;如果缓存命中,则直接返回,避免访问速度较慢的底层存储(如磁盘数据库)。
从本质上看,缓存是一种空间换时间的策略——用额外的存储空间换取更低的访问延迟。在数据库缓存场景中,缓存通常位于应用层与持久化数据库之间,作为中间层加速数据读取。
核心特征:缓存中的数据是原始数据的弱一致性副本,而非唯一数据源。这意味着缓存数据可能过期、不一致,需要配合一致性协议(如 TCC)来保证边界正确性。
2.2 缓存的作用
缓存主要解决两大核心问题:加速和削峰。下表从多个维度对比两者的差异:
| 维度 | 加速(Speed-up) | 削峰(Peak Shaving) |
|---|---|---|
| 核心目标 | 降低数据访问延迟 | 保护后端系统不被突发流量击穿 |
| 典型场景 | 热点数据查询、用户会话 | 秒杀、大促、突发流量 |
| 缓存命中率 | 追求高命中率(>95%) | 允许较低命中率,重在过滤流量 |
| 数据一致性要求 | 较高,需及时更新 | 可容忍短时不一致 |
| 缓存失效策略 | 主动更新 + TTL | TTL + 限流降级 |
加速:降低延迟
内存访问延迟通常在微秒级(Redis 平均 0.1ms),而磁盘数据库查询延迟在毫秒到几十毫秒级。缓存通过将热点数据驻留在内存中,将读取延迟降低 1~3 个数量级。
削峰:保护后端
当突发请求量超过数据库处理能力时,缓存可以吸收大部分读请求,避免数据库过载。例如在秒杀场景中,商品库存信息缓存在 Redis 中,99% 的查询请求由缓存直接返回,数据库只处理最终的扣减事务。
关键指标:缓存命中率 = 缓存命中次数 / 总请求次数。命中率低于 80% 时,需要检查缓存策略是否合理。
2.3 常见缓存产品对比
目前业界最主流的分布式缓存产品是 Redis 和 Memcached。下表从多个维度进行对比:
| 对比维度 | Redis | Memcached |
|---|---|---|
| 数据结构 | String、Hash、List、Set、Sorted Set、Bitmap、HyperLogLog、Stream | 仅支持 Key-Value(String) |
| 持久化 | 支持 RDB 快照 + AOF 日志 | 不支持持久化 |
| 集群模式 | Redis Cluster(去中心化)、主从复制、哨兵 | 客户端分片(一致性哈希) |
| 内存淘汰策略 | 8 种策略(LRU、LFU、TTL、随机等) | LRU(最近最少使用) |
| 事务支持 | MULTI/EXEC 事务 + Lua 脚本 | 不支持 |
| 典型延迟 | 0.1ms ~ 1ms(单机) | 0.1ms ~ 1ms(单机) |
| 适用场景 | 复杂数据结构、需要持久化、发布订阅、分布式锁 | 简单 KV 缓存、高吞吐纯缓存场景 |
Redis 核心优势
- 丰富的数据结构:支持 Hash、List、Set 等,可直接在缓存层完成部分业务逻辑(如排行榜、计数器)。
- 持久化能力:RDB 快照和 AOF 日志保证数据不因重启而丢失,适合缓存预热和灾备。
- 分布式锁:基于 SETNX 和 Redlock 算法,是实现 TCC 缓存一致性中 Try 阶段锁定的关键工具。
- Lua 脚本:原子性执行多个操作,适合实现复杂的缓存一致性校验逻辑。
Memcached 核心优势
- 极简设计:纯内存 KV 存储,无持久化开销,读写性能极高。
- 多线程架构:天然支持多核 CPU,高并发场景下吞吐量优于 Redis 单线程模型。
- 内存利用率:Slab 分配机制减少内存碎片,适合存储大量小对象。
选型建议:如果缓存场景需要复杂数据结构、持久化或分布式锁,优先选择 Redis;如果只是简单的 KV 缓存且追求极致吞吐,Memcached 是更轻量的选择。本课程后续的 TCC 缓存一致性实现将基于 Redis 展开。
2.4 缓存架构位置
下图展示了缓存层在典型 Web 应用架构中的位置:
2.5 缓存读写基本流程
缓存场景中最常用的读写模式是 Cache-Aside(旁路缓存),其流程如下:
- 读请求:应用先查询缓存,如果命中则直接返回;如果未命中,则查询数据库,将结果写入缓存并设置 TTL,最后返回给客户端。
- 写请求:应用先更新数据库,然后删除或更新缓存中的对应数据(延迟双删策略可进一步保证一致性)。
注意:在 TCC 缓存一致性模式中,写流程会拆分为 Try、Confirm、Cancel 三个阶段,每个阶段都会与缓存层交互,确保在分布式环境下缓存与数据库的最终一致。
2.6 缓存一致性问题的根源
缓存与数据库之间天然存在时间窗口:
- 更新数据库后,缓存尚未更新或删除,此时读请求可能拿到旧数据(脏读)。
- 并发写操作时,数据库和缓存的更新顺序不一致,导致数据错乱。
- 缓存 TTL 过期后,多个线程同时回源数据库,可能引发缓存雪崩。