多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

【架构实战】Redis 与 MySQL 双写一致性终极解法(附生产级代码与避坑指南)

【架构实战】Redis 与 MySQL 双写一致性终极解法(附生产级代码与避坑指南) 一、为什么双写一致性这么难并发时序漏洞剖析在电商、内容社区以及金融交易系统中“缓存 数据库”是抗住高并发流量的标准读写分离架构。然而在引入 Redis 作为加速层后系统面临最棘手的问题就是当数据库发生更新时如何保证 Redis 缓存与 MySQL 数据的一致性如果在面试中或者实际技术方案设计时你只回答了“先更数据库再删缓存”那么在并发量达到数千 QPS 时你的系统大概率会出现严重的脏数据问题。1. 先更新数据库再更新缓存❌ 严禁使用时序漏洞线程A更新DB为1线程B更新DB为2并写入Cache为2随后网络延迟的线程A将旧值1写入Cache造成永久脏数据。2. 先删除缓存再更新数据库❌ 不推荐时序漏洞线程A删除缓存线程B读未命中查库拿旧值1回写Cache线程A随后更新DB为2导致缓存滞留旧数据。二、业界主流的 4 种解决方案深度对比方案一致性保证系统复杂度适用并发场景生产推荐度1. 延迟双删 TTL 兜底最终一致存在短暂时间窗口低中低并发 3000 QPS⭐⭐⭐2. 异步消息队列重试 (MQ)最终一致保证重试成功中中高并发系统⭐⭐⭐⭐3. Canal Binlog 监听最终一致解耦业务代码较高需维护中间件生产级分布式核心链路⭐⭐⭐⭐⭐推荐三、方案一延迟双删优化版轻量级方案代码Service public class ProductService { Autowired private StringRedisTemplate redisTemplate; Autowired private ProductMapper productMapper; Autowired private ThreadPoolTaskExecutor asyncExecutor; public void updateProduct(Product product) { String cacheKey product:info: product.getId(); // 1. 第一次删除缓存 redisTemplate.delete(cacheKey); // 2. 更新数据库 productMapper.updateById(product); // 3. 异步延迟第二次删除避免阻塞写请求耗时 asyncExecutor.execute(() - { try { Thread.sleep(500); redisTemplate.delete(cacheKey); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } }); } }四、方案二生产级标准解法 —— Canal 监听 Binlog 异步解耦在大中型分布式系统中业务代码只负责操作数据库缓存的失效完全解耦交给 Canal 监听 Binlog MQ 消费零业务侵入业务开发人员只需要操作 MySQL不用到处写删除缓存代码天然重试保证Redis 出现网络抖动时MQ 自动触发退避重试绝不丢失失效事件写性能极致写库操作完全不占用业务接口 RT 耗时。五、生产环境必备的 4 大防暴毙军规绝对不要省略 TTL 兜底所有缓存 Key 必须设置合理的过期时间作为极端异常下的安全兜底防缓存穿透查询不存在的 ID 时向 Redis 写入空值并设 60s 短期过期防止恶意攻击打崩数据库加随机抖动防止缓存雪崩TTL 基础时间 随机时间1~5分钟避免同一时间集中失效MySQL 开启 ROW 模式 Binlog确保 Canal 能精确获取每行被修改数据的主键 ID。
返回列表