副标题:Cache Aside、Read/Write Through、Write Behind,三大模式谁更强?🎯
🎬 开场:缓存不一致的噩梦
真实故障案例 💥:
某电商平台的惊魂一刻:
10:00 用户购买了最后一件商品
10:00 数据库:库存 = 0
10:00 缓存:库存 = 1 ⚠️ 还没更新!
10:01 另一个用户看到:还有1件库存
10:01 第二个用户下单成功
10:02 仓库发货时发现:没库存了!
10:03 客服电话被打爆...
问题根源:
缓存和数据库数据不一致!
后果:
├── 超卖
├── 用户投诉
├── 赔偿损失
└── 信誉受损
这就是缓存一致性问题的严重性!
📚 核心概念
什么是缓存一致性?
缓存一致性(Cache Consistency):
保证缓存中的数据与数据库中的数据保持一致
理想状态:
数据库:库存 = 100
缓存: 库存 = 100 ✅ 一致
不一致状态:
数据库:库存 = 100
缓存: 库存 = 105 ❌ 不一致
为什么会不一致?
原因1:并发写入
线程A:写数据库 → 写缓存
线程B:写数据库 → 写缓存
↓
顺序错乱,导致不一致
原因2:更新失败
写数据库成功 ✅
写缓存失败 ❌
↓
数据不一致
原因3:缓存过期
数据库已更新
缓存还是旧数据
↓
短暂不一致
🏗️ 三大缓存模式
1️⃣ Cache Aside(旁路缓存)⭐
最常用的模式!
读操作流程
┌─────────┐
│ 应用层 │
└────┬────┘
│
① 读取缓存
│
┌────▼────┐
│ Redis │
└────┬────┘
│
② 缓存命中?
│
├─ YES → 返回数据 ✅
│
└─ NO → ③ 查询数据库
│
┌────▼────┐
│ MySQL │
└────┬────┘
│
④ 写入缓存
│
⑤ 返回数据
代码实现:
@Service
public class ProductService {
@Autowired
private RedisTemplate<String, Product> redisTemplate;
@Autowired
private ProductMapper productMapper;
/**
* 读取商品(Cache Aside模式)
*/
public Product getProduct(Long productId) {
String cacheKey = "product:" + productId;
// 1. 先查缓存
Product product = redisTemplate.opsForValue().get(cacheKey);
if (product != null) {
log.info("缓存命中: {}", productId);
return product;
}
// 2. 缓存未命中,查数据库
product = productMapper.selectById(productId);
if (product != null) {
// 3. 写入缓存
redisTemplate.opsForValue().set(
cacheKey,
product,
30,
TimeUnit.MINUTES
);
}
return product;
}
}
写操作流程
方案1:先删缓存,再写数据库 ❌
┌─────────┐
│ 应用层 │
└────┬────┘
│
① 删除缓存
↓
② 更新数据库
问题:
如果在①②之间有读请求
会读到旧数据并写入缓存
导致脏数据长期存在!
方案2:先写数据库,再删缓存 ✅
┌─────────┐
│ 应用层 │
└────┬────┘
│
① 更新数据库
↓
② 删除缓存
优点:
即使②失败,下次读取时
会从数据库读取最新数据
最多只是一次脏读
正确的写操作代码:
@Service
public class ProductService {
/**
* 更新商品(推荐方式)
*/
@Transactional
public void updateProduct(Product product) {
// 1. 先更新数据库
productMapper.updateById(product);
// 2. 再删除缓存
String cacheKey = "product:" + product.getId();
redisTemplate.delete(cacheKey);
// 下次读取时会从数据库加载最新数据
}
/**
* 更新商品(更安全的方式 - 延迟双删)
*/
@Transactional
public void updateProductSafely(Product product) {
String cacheKey = "product:" + product.getId();
// 1. 先删除缓存
redisTemplate.delete(cacheKey);
// 2. 更新数据库
productMapper.updateById(product);
// 3. 延迟一段时间后再次删除缓存
CompletableFuture.runAsync(() -> {
try {
Thread.sleep(1000); // 延迟1秒
redisTemplate.delete(cacheKey);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
}
}
Cache Aside的优缺点
优点 ✅:
- 实现简单
- 应用层完全控制
- 缓存失败不影响数据库
- 最常用,经过验证
缺点 ❌:
- 存在短暂不一致
- 需要应用层处理逻辑
- 可能出现缓存穿透
2️⃣ Read/Write Through(读写穿透)
由缓存层统一处理!
架构图
┌─────────┐
│ 应用层 │
└────┬────┘
│ 只和缓存层交互
┌────▼────────┐
│ 缓存层 │ ← 封装了所有逻辑
│ (Cache) │
└────┬────────┘
│ 缓存层自动同步数据库
┌────▼────┐
│ MySQL │
└─────────┘
Read Through(读穿透)
/**
* Read Through实现
*
* 缓存层负责加载数据
*/
public class CacheThroughService {
private final LoadingCache<Long, Product> cache;
public CacheThroughService() {
this.cache = CacheBuilder.newBuilder()
.maximumSize(10000)
.expireAfterWrite(30, TimeUnit.MINUTES)
.build(new CacheLoader<Long, Product>() {
@Override
public Product load(Long productId) {
// 缓存未命中时,自动从数据库加载
return productMapper.selectById(productId);
}
});
}
/**
* 应用层只需要调用get
* 缓存层自动处理加载逻辑
*/
public Product getProduct(Long productId) {
try {
return cache.get(productId); // 简单!
} catch (ExecutionException e) {
throw new RuntimeException("加载商品失败", e);
}
}
}
Write Through(写穿透)
/**
* Write Through实现
*
* 同步写入缓存和数据库
*/
public class WriteThroughCache {
/**
* 写入数据
* 同步更新缓存和数据库
*/
public void put(String key, Product value) {
// 1. 同步写入缓存
cache.put(key, value);
// 2. 同步写入数据库
productMapper.updateById(value);
// 两个操作都成功才返回
}
}
完整实现(自定义缓存层):
/**
* 自定义缓存层实现Read/Write Through
*/
@Component
public class ProductCacheLayer {
@Autowired
private RedisTemplate<String, Product> redisTemplate;
@Autowired
private ProductMapper productMapper;
/**
* Read Through
*/
public Product get(Long productId) {
String key = "product:" + productId;
// 1. 先查缓存
Product product = redisTemplate.opsForValue().get(key);
if (product == null) {
// 2. 缓存未命中,查数据库
product = productMapper.selectById(productId);
if (product != null) {
// 3. 自动加载到缓存
redisTemplate.opsForValue().set(key, product, 30, TimeUnit.MINUTES);
}
}
return product;
}
/**
* Write Through
*/
@Transactional
public void put(Product product) {
String key = "product:" + product.getId();
// 1. 先写数据库
productMapper.updateById(product);
// 2. 再更新缓存
redisTemplate.opsForValue().set(key, product, 30, TimeUnit.MINUTES);
// 两步都成功才提交事务
}
/**
* 应用层使用
*/
}
@Service
public class ProductService {
@Autowired
private ProductCacheLayer cacheLayer;
public Product getProduct(Long productId) {
// 应用层不关心缓存逻辑
return cacheLayer.get(productId);
}
public void updateProduct(Product product) {
// 应用层不关心缓存逻辑
cacheLayer.put(product);
}
}
Read/Write Through的优缺点
优点 ✅:
- 应用层简单
- 缓存层统一处理
- 逻辑集中,易维护
缺点 ❌:
- 写操作慢(同步写两处)
- 缓存层复杂
- 强依赖缓存层
3️⃣ Write Behind(写后置/异步写)
最高性能的模式!
原理
写操作流程:
┌─────────┐
│ 应用层 │
└────┬────┘
│
① 写入缓存(立即返回)⚡ 超快!
│
┌────▼────┐
│ Cache │
└────┬────┘
│
② 异步批量写入数据库
│
┌────▼────┐
│ MySQL │
└─────────┘
特点:
- 写缓存立即返回
- 后台异步写数据库
- 性能最高!
代码实现
/**
* Write Behind实现
*/
@Component
public class WriteBehindCache {
@Autowired
private RedisTemplate<String, Product> redisTemplate;
@Autowired
private ProductMapper productMapper;
// 待写入数据库的队列
private final BlockingQueue<Product> writeQueue = new LinkedBlockingQueue<>(10000);
@PostConstruct
public void init() {
// 启动后台线程异步写数据库
startAsyncWriter();
}
/**
* 写入商品(立即返回)
*/
public void put(Product product) {
String key = "product:" + product.getId();
// 1. 先写缓存(立即返回)
redisTemplate.opsForValue().set(key, product, 30, TimeUnit.MINUTES);
// 2. 加入写队列
writeQueue.offer(product);
// 立即返回,不等待数据库写入 ⚡
}
/**
* 后台线程异步写数据库
*/
private void startAsyncWriter() {
// 启动多个写线程
for (int i = 0; i < 3; i++) {
new Thread(() -> {
while (true) {
try {
// 批量获取待写入的数据
List<Product> batch = new ArrayList<>();
Queues.drain(writeQueue, batch, 100, 1, TimeUnit.SECONDS);
if (!batch.isEmpty()) {
// 批量写入数据库
productMapper.batchUpdate(batch);
log.info("批量写入{}条数据", batch.size());
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
} catch (Exception e) {
log.error("异步写入失败", e);
}
}
}).start();
}
}
}
更完善的实现(带重试):
@Component
public class WriteBehindCacheAdvanced {
private final LinkedBlockingQueue<WriteTask> writeQueue = new LinkedBlockingQueue<>();
/**
* 写入任务
*/
static class WriteTask {
private final Product product;
private int retryCount = 0;
private final int maxRetry = 3;
public WriteTask(Product product) {
this.product = product;
}
public boolean canRetry() {
return retryCount < maxRetry;
}
public void incrementRetry() {
retryCount++;
}
}
/**
* 异步写入
*/
public void putAsync(Product product) {
// 1. 写缓存
redisTemplate.opsForValue().set("product:" + product.getId(), product);
// 2. 加入写队列
writeQueue.offer(new WriteTask(product));
}
/**
* 后台线程处理写队列
*/
@PostConstruct
public void startWriter() {
Executors.newFixedThreadPool(3).execute(() -> {
while (true) {
try {
WriteTask task = writeQueue.take();
try {
// 写数据库
productMapper.updateById(task.product);
} catch (Exception e) {
// 失败重试
if (task.canRetry()) {
task.incrementRetry();
writeQueue.offer(task); // 重新加入队列
log.warn("写入失败,第{}次重试", task.retryCount);
} else {
log.error("写入失败,放弃: {}", task.product.getId());
// 记录到失败日志表
recordFailure(task.product);
}
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
});
}
}
Write Behind的优缺点
优点 ✅:
- 写性能最高
- 减少数据库压力
- 支持批量操作
缺点 ❌:
- 数据可能丢失(缓存挂了)
- 实现复杂
- 不适合强一致性场景
🆚 三种模式对比
| 维度 | Cache Aside | Read/Write Through | Write Behind |
|---|---|---|---|
| 复杂度 | ⭐⭐ 简单 | ⭐⭐⭐ 中等 | ⭐⭐⭐⭐ 复杂 |
| 性能 | ⭐⭐⭐ 好 | ⭐⭐ 一般 | ⭐⭐⭐⭐⭐ 极好 |
| 一致性 | 最终一致 | 强一致 | 最终一致 |
| 可靠性 | 高 | 高 | 低 |
| 使用场景 | 通用 | 强一致性 | 高并发写 |
| 推荐度 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐ |
💡 一致性保证方案
1. 延迟双删
/**
* 延迟双删策略
*/
public void updateWithDoubleDelete(Product product) {
String key = "product:" + product.getId();
// 第1次删除缓存
redisTemplate.delete(key);
// 更新数据库
productMapper.updateById(product);
// 延迟后第2次删除缓存
CompletableFuture.runAsync(() -> {
try {
// 延迟时间取决于主从同步延迟
Thread.sleep(1000);
// 第2次删除
redisTemplate.delete(key);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
}
为什么要延迟双删?
时间线:
T1: 线程A删除缓存
T2: 线程B读缓存(未命中)
T3: 线程B读数据库(读到旧数据)
T4: 线程A更新数据库
T5: 线程B写入缓存(写入旧数据)❌ 脏数据!
T6: 延迟双删,删除脏数据 ✅
延迟时间 = 主从复制延迟 + 读数据库时间
2. 订阅Binlog
/**
* 监听MySQL Binlog,自动更新缓存
*/
@Component
public class BinlogCacheUpdater {
@Autowired
private RedisTemplate<String, Product> redisTemplate;
/**
* 使用Canal监听Binlog
*/
@PostConstruct
public void startListening() {
CanalConnector connector = CanalConnectors.newSingleConnector(
new InetSocketAddress("127.0.0.1", 11111),
"example",
"",
""
);
connector.connect();
connector.subscribe("mydb\\.product"); // 订阅product表
while (true) {
Message message = connector.get(100);
for (CanalEntry.Entry entry : message.getEntries()) {
if (entry.getEntryType() == CanalEntry.EntryType.ROWDATA) {
handleRowChange(entry);
}
}
}
}
/**
* 处理数据变更
*/
private void handleRowChange(CanalEntry.Entry entry) {
CanalEntry.RowChange rowChange = CanalEntry.RowChange.parseFrom(entry.getStoreValue());
for (CanalEntry.RowData rowData : rowChange.getRowDatasList()) {
if (rowChange.getEventType() == CanalEntry.EventType.UPDATE) {
// 数据更新,删除缓存
Long productId = getProductId(rowData);
String key = "product:" + productId;
redisTemplate.delete(key);
log.info("检测到数据变更,删除缓存: {}", key);
}
}
}
}
3. 设置合理的过期时间
/**
* 不同数据设置不同的过期时间
*/
public class CacheExpireStrategy {
/**
* 热点数据:过期时间长
*/
public void cacheHotData(String key, Object value) {
redisTemplate.opsForValue().set(key, value, 24, TimeUnit.HOURS);
}
/**
* 普通数据:过期时间中等
*/
public void cacheNormalData(String key, Object value) {
redisTemplate.opsForValue().set(key, value, 1, TimeUnit.HOURS);
}
/**
* 实时性要求高的数据:过期时间短
*/
public void cacheRealtimeData(String key, Object value) {
redisTemplate.opsForValue().set(key, value, 5, TimeUnit.MINUTES);
}
/**
* 随机过期时间,避免缓存雪崩
*/
public void cacheWithRandomExpire(String key, Object value, long baseSeconds) {
// 基础时间 + 随机时间(0-20%)
long randomSeconds = (long) (baseSeconds * Math.random() * 0.2);
long expireSeconds = baseSeconds + randomSeconds;
redisTemplate.opsForValue().set(key, value, expireSeconds, TimeUnit.SECONDS);
}
}
🎯 最佳实践
1. 选型建议
场景1:电商商品信息
└─ Cache Aside + 延迟双删
└─ 理由:读多写少,允许短暂不一致
场景2:用户余额
└─ Read/Write Through
└─ 理由:强一致性要求
场景3:用户行为日志
└─ Write Behind
└─ 理由:高并发写入,可接受丢失
场景4:库存扣减
└─ 直接操作Redis,定期同步DB
└─ 理由:超高并发,最终一致
2. 监控告警
@Component
public class CacheMonitor {
@Autowired
private MeterRegistry meterRegistry;
/**
* 监控缓存命中率
*/
public void recordCacheHit(boolean hit) {
Counter.builder("cache.hit")
.tag("hit", String.valueOf(hit))
.register(meterRegistry)
.increment();
}
/**
* 监控缓存不一致
*/
@Scheduled(fixedDelay = 60000) // 每分钟
public void checkConsistency() {
// 随机抽样检查
List<Long> sampleIds = getSampleIds(100);
int inconsistentCount = 0;
for (Long id : sampleIds) {
Object cacheValue = getFromCache(id);
Object dbValue = getFromDB(id);
if (!Objects.equals(cacheValue, dbValue)) {
inconsistentCount++;
log.warn("数据不一致: id={}, cache={}, db={}",
id, cacheValue, dbValue);
}
}
if (inconsistentCount > 5) { // 超过5%不一致
alertService.send("缓存一致性告警",
"不一致比例: " + (inconsistentCount * 100 / sampleIds.size()) + "%");
}
}
}
🎉 总结
核心要点 ✨
-
Cache Aside:
- 最常用
- 先更新DB,再删缓存
- 延迟双删更安全
-
Read/Write Through:
- 缓存层统一处理
- 强一致性
- 适合金融场景
-
Write Behind:
- 性能最高
- 异步写入
- 适合日志场景
记忆口诀 📝
缓存一致三模式,
场景不同选择异。
Cache Aside最常用,
先更新库再删缓存。
延迟双删更安全,
短暂不一致可接受。
Read Write Through强,
同步更新保一致。
应用简单缓存管,
金融场景适用它。
Write Behind性能高,
异步批量写数据库。
日志统计场景好,
丢失风险要知道!
愿你的缓存永远一致,数据永不出错! 🔄✨