下面我将详细解答redis与数据库双写一致性问题,结合故事举例说明,最后用思维导图总结核心解决方案。
问题解答
核心问题:当同时更新redis缓存和数据库时,由于网络延迟、操作顺序等问题导致数据不一致。
解决方案(含故事举例)
假设电商平台有商品库存系统:
// 商品服务类 public class productservice { private db db = new mysql(); // 数据库 private cache cache = new redis(); // 缓存 }
1. 先更新数据库,再删除缓存(推荐方案)
public void updateproduct(product product) { db.update(product); // 1. 更新数据库 cache.delete(product.getid());// 2. 删除缓存 }
故事场景:
管理员修改商品价格(100→80元):
- 数据库先更新为80元 ✅
- 删除redis中该商品的缓存
用户查询时:
- 缓存未命中 → 读数据库(80元) ✅
- 回填缓存(80元) ✅
优势:
- 避免"更新缓存失败导致永久不一致"
- 缓存删除失败可重试(通过消息队列)
风险:
并发时短暂不一致(概率低):
- 用户a读缓存(空) → 读数据库(100元)
- 管理员更新数据库(100→80) → 删缓存
- 用户a写缓存(100元) ❌(旧数据)
- 解决方案:延时双删(下文说明)
2. 延时双删策略
public void updatewithdelay(product product) { cache.delete(product.getid()); // 1. 先删缓存 db.update(product); // 2. 更新数据库 thread.sleep(500); // 3. 等待500ms cache.delete(product.getid()); // 4. 再删缓存 }
故事场景:
解决上述并发问题:
- 首次删除:清空旧缓存
- 更新数据库 ✅
- 等待期间可能写入的旧缓存被二次删除 ❌→✅
3. 监听数据库变更(最终一致性)
// 使用canal监听mysql binlog canal.subscribe(event -> { if (event.isupdate()) { cache.delete(event.getkey()); // 异步删除缓存 } });
故事场景:
订单系统库存变更:
- 数据库减库存 ✅
- canal捕获变更事件
- 自动删除redis库存缓存
- 下次查询回填最新值 ✅
4. 加分布式锁(强一致)
public void safeupdate(product product) { lock lock = redisson.getlock("product_" + product.getid()); lock.lock(); try { db.update(product); cache.update(product); // 同时更新缓存 } finally { lock.unlock(); } }
适用场景:
金融账户余额等强一致性要求:
- 读写操作串行化
- 性能较低(非高并发场景)
总结对比
方案 | 一致性 | 性能 | 复杂度 | 适用场景 |
---|---|---|---|---|
先db后删缓存 | 最终 | ★★★★ | 低 | 大部分业务 |
延时双删 | 最终 | ★★★ | 中 | 高并发场景 |
监听binlog | 最终 | ★★★★ | 高 | 异构系统同步 |
分布式锁 | 强一致 | ★★ | 高 | 金融/账户系统 |
核心原则:
- 优先保证数据库正确性
- 缓存操作可失败/重试
- 强一致性需牺牲性能
以上为个人经验,希望能给大家一个参考,也希望大家多多支持代码网。
发表评论