【从零开始——Redis 进化日志|Day7】双写一致性难题:数据库与缓存如何不再“打架”?(附 Canal/读写锁实战)

兄弟们,欢迎来到 Redis 进化日志的第七天。

在 Day 6 里,我们全副武装,用布隆过滤器和互斥锁挡住了外部黑客和流量洪峰。现在的系统看起来固若金汤,外人根本打不进来。

但是,别高兴得太早! 堡垒往往是从内部攻破的。

场景复现:

你的用户“张三”改名叫“罗峰”,点击了保存。

  1. MySQL里的名字成功改成了“罗峰”。

  2. 但是!Redis里的缓存因为某种原因(网络抖动、逻辑错误)没更新,还是“张三”。

  3. 用户一刷新页面,看到的依然是“张三”。

  4. 用户怒了:“我明明改了啊?你们系统是不是坏了?”

这就是著名的双写一致性问题

面试官问这个问题时,如果你张口就来:“先改数据库,再改缓存呗”,或者简单一句“延时双删”,那基本就离“回去等通知”不远了。因为这里面全是并发留下的坑!

今天,咱们就抽丝剥茧,把这个“死锁题”彻底解开。


一、 经典错题集:那些年我们踩过的坑 🕳️

在寻找真理之前,我们先得把错误的道路堵死。很多初学者(甚至有经验的开发)都犯过以下错误。

❌ 错误姿势 A:先更新数据库,再更新缓存
// 错误示范 public void update(User user) { userMapper.update(user); // 1. 写库 redisTemplate.opsForValue().set("user:" + user.getId(), user); // 2. 更新缓存 }

Bug 分析:

假设线程 A 和线程 B 同时修改同一个数据。

  1. 线程 A把 DB 改成了 "V1"。

  2. 线程 B把 DB 改成了 "V2"(B 后发先至)。

  3. 关键点:线程 B 动作快,先把 Redis 改成了 "V2"。

  4. 线程 A 网络卡了一下,姗姗来迟,把 Redis 改回了 "V1"。

    结果:数据库是新的 "V2",Redis 却是旧的 "V1"。脏数据诞生!

先改数据库:如果数据库更新成功但缓存删除失败(或在删除前的短暂时间窗内),会导致缓存中依然保留旧数据,造成数据不一致。

❌ 错误姿势 B:先删除缓存,再更新数据库

这是著名的Cache Aside Pattern(旁路缓存模式)的一种变种,但它在高并发下有大坑。

// 错误示范 public void update(User user) { redisTemplate.delete("user:" + user.getId()); // 1. 删缓存 userMapper.update(user); // 2. 写库 }

Bug 分析

  1. 线程 A删除了缓存,正准备去改 DB(还没改完,比如卡了 100ms)。

  2. 线程 B 来了,想查数据。发现缓存空的(Cache Miss)。

  3. 线程 B去查 DB。注意!这时候 A 还没改完,B 查到的是旧数据

  4. 线程 B旧数据写入 Redis。

  5. 线程 A 终于改完 DB 了(新数据)。

    结果:数据库是新的,Redis 永远停留在了旧数据。这就是经典的“读写并发不一致”

先删缓存:在高并发场景下,读请求极易在数据库更新完成前读取到旧值并重新写回缓存,导致缓存中长期驻留脏数据(这是致命的“读写并发”问题)


二、 进阶方案:延时双删 (Delayed Double Deletion) ⏳

这是很多老博客推荐的方案,也是面试时的“标准答案”之一。它的核心目的是为了解决上面“错误姿势 B”中,线程 B 把旧数据写回缓存的问题。

核心逻辑:

既然线程 B 可能把旧数据写回去,那我改完数据库后,睡一会,再删一次,把 B 写进去的脏数据干掉!

生产级代码模拟

@Service public class UserService { @Autowired private StringRedisTemplate redisTemplate; @Autowired private UserMapper userMapper; public void updateUser(User user) { String key = "user:" + user.getId(); // 1. 先删除缓存 redisTemplate.delete(key); // 2. 更新数据库 userMapper.update(user); // 3. 延时删除 (使用异步线程,避免阻塞主线程) CompletableFuture.runAsync(() -> { try { // 为什么要睡?为了让线程 B (读请求) 有足够时间把脏数据写入缓存,我们再删 // 睡多久?通常建议:读业务逻辑耗时 + 几百毫秒 Thread.sleep(500); // 4. 再次删除缓存 (Double Delete) redisTemplate.delete(key); log.info("延时双删成功: {}", key); } catch (InterruptedException e) { log.error("延时双删异常", e); } }); } }

灵魂拷问

  • 延时多久?这是一个玄学。睡短了,B 还没写完你就删了,没用;睡长了,这期间的数据都是脏的。

  • 如果第二次删除失败了怎么办?虽然概率低,但一旦失败,脏数据就留下了。

结论延时双删只能降低不一致的概率,无法保证强一致性。适合对数据准确性要求没那么苛刻的业务。


三、 终极方案 A:强一致性 (ReadWriteLock)

如果你的业务是**“必须保证数据绝对一致”**(比如金融金额、商品库存、抢购资格),别整那些虚的,必须上锁!

但我们不能用简单的synchronized,因为读多写少,互斥锁太慢了。我们要用Redisson 的分布式读写锁 (ReadWriteLock)

原理

  • 读锁 (Read Lock):共享锁。大家都可以读,互不影响,并发性能高。

  • 写锁 (Write Lock):排他锁。只要有人在写,谁都不能读,也不能写。

生产级代码实战

@Service public class ProductService { @Autowired private RedissonClient redisson; @Autowired private StringRedisTemplate redisTemplate; @Autowired private ProductMapper productMapper; // 读操作:加读锁 public Product getProduct(Long id) { String key = "product:" + id; RReadWriteLock rwLock = redisson.getReadWriteLock("lock:product:" + id); RLock rLock = rwLock.readLock(); // 加读锁:允许多个线程同时读,但会阻塞写线程 rLock.lock(); try { // 1. 查缓存 String json = redisTemplate.opsForValue().get(key); if (StringUtils.isNotBlank(json)) { return JSON.parseObject(json, Product.class); } // 2. 查 DB Product product = productMapper.selectById(id); // 3. 回写 Redis (即便这里有延迟,因为有读锁,写线程进不来,数据是稳的) if (product != null) { redisTemplate.opsForValue().set(key, JSON.toJSONString(product)); } return product; } finally { rLock.unlock(); } } // 写操作:加写锁 public void updateProduct(Product product) { String key = "product:" + product.getId(); RReadWriteLock rwLock = redisson.getReadWriteLock("lock:product:" + product.getId()); RLock wLock = rwLock.writeLock(); // 加写锁:阻塞所有读线程和其他写线程 wLock.lock(); try { // 1. 更新数据库 productMapper.update(product); // 2. 删除缓存 (因为加了写锁,这期间没人能读,所以绝对安全) redisTemplate.delete(key); } finally { wLock.unlock(); } } }

优缺点

  • 强一致性:绝对不会有脏数据,逻辑闭环。

  • 性能损耗:写数据时会阻塞读请求。如果写操作很频繁,系统吞吐量会下降。


四、 终极方案 B:最终一致性 (Canal + MQ) 🚀

如果你的业务是“允许短暂延迟,但最终必须一致”(比如电商的商品详情页、朋友圈动态),那么异步同步是目前大厂最主流的选择。

核心思想:

让业务代码只负责改数据库,别管 Redis。Redis 的更新交给一个“旁观者”去处理。

架构组件:Canal + RocketMQ/Kafka

  1. 业务代码:只管db.update(),执行完直接返回成功。

  2. Canal:这是一个阿里开源的中间件,它把自己伪装成 MySQL 的 Slave(从库)。

  3. 监听:一旦 MySQL 数据变了,Binlog 就会推给 Canal。

  4. MQ:Canal 收到消息,发给 MQ。

  5. 消费者:专门的一个服务监听 MQ,收到消息后,去把 Redis 删掉(或者更新)。

为什么这是大厂首选?

  • 彻底解耦:业务代码里不需要写一堆redis.del(),也不用担心删失败影响主业务。

  • 重试机制 (Retry):如果删 Redis 失败了怎么办?MQ 自带重试机制!它会一直重试,直到成功为止。这就是最终一致性的保障。


面试可能会拷打你:

Q1: 为什么是“删除缓存”而不是“更新缓存”?

回答

  1. 懒加载思想:如果我很频繁地修改数据库(比如 1 分钟改 100 次),但我每次都去更新缓存,而这 1 分钟内其实没人来查。那这 100 次缓存更新就是浪费性能。删除它,等真正有人查的时候再去计算并加载,更省资源。

  2. 并发安全:更新缓存容易出现“A 改库 -> B 改库 -> B 改缓存 -> A 改缓存”的乱序问题,导致脏数据。删除缓存则简单粗暴,避免了复杂的覆盖逻辑。

Q2: 延时双删如果第二次删除失败了怎么办?

回答:

这是一个概率问题。如果必须保证成功,延时双删就不够用了。

这种情况下,我会引入 消息队列 (MQ)。将“删除缓存”这个动作丢进 MQ,如果失败了,利用 MQ 的 ACK 机制 进行重试,直到删除成功为止,保证最终一致性。

Q3: 你们项目中是怎么做的?

回答 (根据实际情况选一个)

  • 普通业务:我们用的是“先更新 DB,再删除缓存”。虽然理论上有极低概率发生不一致(读操作在 Cache Miss 时查到旧数据,且在写操作之后才写入缓存),但在实际生产中,写操作通常比读慢得多,这种情况很难发生。

  • 核心业务:对于一定要精准的数据(如库存),我们配合Redisson 读写锁来保证强一致性。


总结:一张表治好选择困难症

面试官问你怎么选,你直接甩出这个表格,显得非常专业:

方案特点适用场景复杂度
先删缓存 + 更新 DB简单,但有严重并发 Bug不推荐使用
先改 DB + 删缓存最通用,偶发不一致90% 的互联网业务
延时双删缓解并发问题,需把控睡眠时间对一致性有要求,无中间件⭐⭐
Redisson 读写锁强一致性,写多读少性能差金融、支付、强库存管理⭐⭐⭐
Canal + MQ最终一致性,高性能,解耦首页广告、商品详情、高并发大厂架构⭐⭐⭐⭐

一句话总结:

没有完美的架构,只有适合的架构。

  • 想省事且并发低?先改 DB + 删缓存

  • 数据绝对不能错?读写锁保平安。

  • 高并发且允许秒级延迟?Canal + MQ是王道。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/1198221.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Unity卡通渲染进阶秘籍:3大核心技术+5分钟实战指南

Unity卡通渲染进阶秘籍:3大核心技术5分钟实战指南 【免费下载链接】UnityToonShader Source code for Toon Shader tutorial for Unity. Has specular, rim lighting, and can cast and receive shadows. 项目地址: https://gitcode.com/gh_mirrors/un/UnityToonS…

AI小说生成器终极部署指南:5分钟搭建专属创作平台

AI小说生成器终极部署指南:5分钟搭建专属创作平台 【免费下载链接】AI_NovelGenerator 使用ai生成多章节的长篇小说,自动衔接上下文、伏笔 项目地址: https://gitcode.com/GitHub_Trending/ai/AI_NovelGenerator 还在为长篇小说创作中的剧情断裂、…

JustTrustMe:5分钟掌握Android SSL证书验证绕过技巧

JustTrustMe:5分钟掌握Android SSL证书验证绕过技巧 【免费下载链接】JustTrustMe An xposed module that disables SSL certificate checking for the purposes of auditing an app with cert pinning 项目地址: https://gitcode.com/gh_mirrors/ju/JustTrustMe …

基于粗略标注增强的BSHM,为何更适合落地

基于粗略标注增强的BSHM,为何更适合落地 1. 引言:人像抠图的现实挑战与BSHM的破局思路 在电商、影视后期、虚拟背景、AI换装等实际业务场景中,高质量的人像抠图是刚需。但传统方法往往面临两个核心矛盾: 精度高 → 成本高&…

WVP-GB28181-Pro:终极国标视频监控平台完整指南

WVP-GB28181-Pro:终极国标视频监控平台完整指南 【免费下载链接】wvp-GB28181-pro 项目地址: https://gitcode.com/GitHub_Trending/wv/wvp-GB28181-pro 想要快速搭建专业的国标视频监控系统吗?WVP-GB28181-Pro为您提供开箱即用的解决方案&#…

fft npainting lama结合OCR技术:智能识别并去除图片文字方案

fft npainting lama结合OCR技术:智能识别并去除图片文字方案 在处理图像时,我们经常会遇到需要移除文字的场景——比如去水印、清理广告信息、修复文档扫描件等。传统方式依赖手动标注,效率低且容易出错。今天要分享的这个方案,将…

批量处理不卡顿,这款卡通化工具太适合小白了

批量处理不卡顿,这款卡通化工具太适合小白了 1. 引言:为什么你需要一个简单好用的卡通化工具? 你有没有遇到过这种情况:手头有一堆朋友的照片,想做成卡通头像当社交平台头像,或者给孩子的照片加点趣味&am…

Glyph输出结果解读,如何评估推理质量?

Glyph输出结果解读,如何评估推理质量? 你有没有遇到过这样的情况:明明输入了一段清晰的图片和问题,模型返回的结果却让人摸不着头脑?或者生成的内容看似合理,细看却发现逻辑断裂、细节错乱?在使…

5个关键理由:为什么OpenEMR成为医疗机构的完美电子健康记录解决方案

5个关键理由:为什么OpenEMR成为医疗机构的完美电子健康记录解决方案 【免费下载链接】openemr The most popular open source electronic health records and medical practice management solution. 项目地址: https://gitcode.com/GitHub_Trending/op/openemr …

HOScrcpy鸿蒙投屏终极指南:环境配置到高级操作全解析

HOScrcpy鸿蒙投屏终极指南:环境配置到高级操作全解析 【免费下载链接】鸿蒙远程真机工具 该工具主要提供鸿蒙系统下基于视频流的投屏功能,帧率基本持平真机帧率,达到远程真机的效果。 项目地址: https://gitcode.com/OpenHarmonyToolkitsPl…

如何在本地搭建AI小说创作助手:从零开始构建专属写作平台

如何在本地搭建AI小说创作助手:从零开始构建专属写作平台 【免费下载链接】AI_NovelGenerator 使用ai生成多章节的长篇小说,自动衔接上下文、伏笔 项目地址: https://gitcode.com/GitHub_Trending/ai/AI_NovelGenerator 你是否曾经在创作长篇小说…

LunaTranslator Galgame翻译器终极安装配置指南

LunaTranslator Galgame翻译器终极安装配置指南 【免费下载链接】LunaTranslator Galgame翻译器,支持HOOK、OCR、剪贴板等。Visual Novel Translator , support HOOK / OCR / clipboard 项目地址: https://gitcode.com/GitHub_Trending/lu/LunaTranslator Lu…

从电子书到有声书:Calibre集成AI语音转换全攻略

从电子书到有声书:Calibre集成AI语音转换全攻略 【免费下载链接】ebook2audiobook Convert ebooks to audiobooks with chapters and metadata using dynamic AI models and voice cloning. Supports 1,107 languages! 项目地址: https://gitcode.com/GitHub_Tren…

ThinkPad X230黑苹果完整教程:从零开始安装macOS系统

ThinkPad X230黑苹果完整教程:从零开始安装macOS系统 【免费下载链接】X230-Hackintosh READMEs, OpenCore configurations, patches, and notes for the Thinkpad X230 Hackintosh 项目地址: https://gitcode.com/gh_mirrors/x2/X230-Hackintosh 还在为老旧…

解锁Windows 11最佳B站体验:Bili.UWP客户端深度评测与实用指南

解锁Windows 11最佳B站体验:Bili.UWP客户端深度评测与实用指南 【免费下载链接】Bili.Uwp 适用于新系统UI的哔哩 项目地址: https://gitcode.com/GitHub_Trending/bi/Bili.Uwp 在Windows 11平台上寻找完美的B站观影方案?Bili.UWP客户端或许就是你…

解锁数字取证新境界:免费开源工具完全指南 [特殊字符]

解锁数字取证新境界:免费开源工具完全指南 🚀 【免费下载链接】awesome-forensics A curated list of awesome forensic analysis tools and resources 项目地址: https://gitcode.com/gh_mirrors/aw/awesome-forensics 还在为网络安全事件调查而…

spotDL终极指南:高效下载Spotify音乐的完整解决方案

spotDL终极指南:高效下载Spotify音乐的完整解决方案 【免费下载链接】spotify-downloader Download your Spotify playlists and songs along with album art and metadata (from YouTube if a match is found). 项目地址: https://gitcode.com/GitHub_Trending/s…

新手友好!Qwen-Image-2512-ComfyUI让AI图像编辑更简单

新手友好!Qwen-Image-2512-ComfyUI让AI图像编辑更简单 你是不是也遇到过这样的情况:好不容易找到一张满意的商品图,结果右下角有个显眼的水印;或者想用一张素材图做海报,却被“Sample”字样破坏了整体美感&#xff1f…

MCP Inspector:让MCP服务器调试变得前所未有的简单![特殊字符]

MCP Inspector:让MCP服务器调试变得前所未有的简单!🚀 【免费下载链接】inspector Visual testing tool for MCP servers 项目地址: https://gitcode.com/gh_mirrors/inspector1/inspector 还在为复杂的MCP服务器调试而烦恼吗&#xf…

Qwen2.5-0.5B批处理优化:多请求并发响应策略

Qwen2.5-0.5B批处理优化:多请求并发响应策略 1. 背景与目标:让小模型也能高效服务多人对话 你有没有遇到过这种情况:本地部署了一个轻量AI模型,自己用起来挺流畅,但一来几个同事同时提问,系统就开始卡顿、…