
MySQL 实战精通系列 · 第11篇Redis 缓存与 MySQL 一致性实战本篇目标缓存和数据库怎么配合、数据不一致怎么解决、缓存击穿/穿透/雪崩怎么防——从缓存策略到分布式锁系统化掌握缓存一致性方案。文章目录MySQL 实战精通系列 · 第11篇Redis 缓存与 MySQL 一致性实战一、为什么需要缓存1.1 缓存的核心价值1.2 一张图看懂缓存位置二、缓存更新策略2.1 四种更新策略对比2.2 Cache Aside推荐方案2.3 先删缓存还是先更新 DB三、缓存一致性深入3.1 不一致的根本原因3.2 强一致方案3.3 缓存一致性实战代码四、缓存三大问题4.1 缓存穿透4.2 缓存击穿4.3 缓存雪崩五、分布式锁5.1 为什么需要分布式锁5.2 Redis 分布式锁实现5.3 Redisson生产级分布式锁5.4 分布式锁对比六、Redis 高可用6.1 三种架构对比6.2 哨兵模式6.3 集群模式七、实战任务任务清单自检问题八、本篇小结一、为什么需要缓存1.1 缓存的核心价值缓存的价值 │ ├── ① 降低数据库压力 ← 读请求走缓存MySQL 只扛写 ├── ② 提升响应速度 ← Redis 内存操作微秒级 ├── ③ 扛住高并发 ← 缓存层水平扩展容易 └── ④ 保护后端服务 ← 限流、降级的基础原则缓存是性能优化的第一手段但缓存引入了一致性问题必须提前设计。1.2 一张图看懂缓存位置用户请求 │ ↓ ① 本地缓存Caffeine / Guava │ 未命中 ↓ ② 分布式缓存Redis │ 未命中 ↓ ③ MySQL 数据库 │ ↓ 回填缓存多级缓存原则层级存储速度容量一致性本地缓存JVM 内存纳秒级小差Redis内存微秒级大较好MySQL磁盘毫秒级最大最好二、缓存更新策略2.1 四种更新策略对比策略原理一致性复杂度适用场景Cache Aside先更新 DB再删缓存较好低通用首选Read/Write Through缓存层代理 DB 读写好中缓存中间件Write Behind只写缓存异步刷 DB差中写多读少双写同时写 DB 和缓存差低不推荐2.2 Cache Aside推荐方案读流程 ① 读缓存 ② 命中 → 返回 ③ 未命中 → 读 DB ④ 写入缓存 ⑤ 返回 写流程 ① 更新 DB ② 删除缓存为什么是删除缓存而不是更新缓存更新缓存的问题 ├── 并发写时缓存值可能被旧数据覆盖 ├── 缓存值可能是复杂计算结果更新成本高 └── 如果缓存值很少被读更新是浪费 删除缓存的优势 ├── 简单下次读时自然回填 ├── 避免并发写覆盖问题 └── 懒加载只缓存热数据2.3 先删缓存还是先更新 DB方案 A先删缓存再更新 DB 问题并发读时读线程在删缓存后、更新 DB 前读到旧数据回填旧值 方案 B先更新 DB再删缓存推荐 问题极小概率下读线程在更新 DB 前读缓存未命中读到旧 DB 值 然后写线程更新 DB 并删缓存读线程回填旧值 → 概率极低且可通过延迟双删兜底延迟双删// 更新 DBupdateDB(data);// 删除缓存redis.delete(key);// 延迟 500ms 再删一次Thread.sleep(500);redis.delete(key);三、缓存一致性深入3.1 不一致的根本原因并发场景 时刻 线程A写 线程B读 T1 更新 DB 2 T2 读缓存 miss T3 读 DB 2读到新值 T4 写缓存 2 T5 删除缓存 → 缓存 2DB 2一致 ✓ 时刻 线程A写 线程B读 T1 读缓存 miss T2 读 DB 1旧值 T3 更新 DB 2 T4 删除缓存 T5 写缓存 1旧值 → 缓存 1DB 2不一致 ✗结论先更新 DB 再删缓存在极端并发下仍可能不一致但概率极低。3.2 强一致方案方案原理性能适用场景分布式锁读写都加锁差强一致要求订阅 binlogCanal 监听异步删缓存好最终一致延迟双删删两次缓存中兜底方案Canal 方案MySQL binlog → Canal → MQ → 缓存删除服务 → 删除 Redis 优势 ① 业务代码无侵入 ② 保证最终一致 ③ 异步解耦3.3 缓存一致性实战代码publicProductgetProduct(Longid){Stringkeyproduct:id;// ① 读缓存Stringcachedredis.get(key);if(cached!null){returnJSON.parseObject(cached,Product.class);}// ② 读 DBProductproductproductMapper.selectById(id);if(productnull){// 防穿透缓存空值redis.setex(key,60,);returnnull;}// ③ 写缓存redis.setex(key,3600,JSON.toJSONString(product));returnproduct;}TransactionalpublicvoidupdateProduct(Productproduct){// ① 更新 DBproductMapper.updateById(product);// ② 删除缓存redis.delete(product:product.getId());// ③ 延迟双删可选delayDelete(product:product.getId(),500);}四、缓存三大问题4.1 缓存穿透问题查询不存在的数据缓存和 DB 都没有 → 每次请求都打到 DB 场景恶意攻击用不存在的 ID 刷接口解决方案方案原理优点缺点缓存空值不存在也缓存设短过期简单占内存布隆过滤器预判 key 是否存在内存小有误判参数校验拦截非法请求从源头不通用布隆过滤器实现// 初始化布隆过滤器BloomFilterLongbloomFilterBloomFilter.create(Funnels.longFunnel(),1000000,// 预期元素数量0.01// 误判率);// 写入时加入bloomFilter.put(productId);// 查询前判断if(!bloomFilter.mightContain(productId)){returnnull;// 一定不存在}4.2 缓存击穿问题热点 key 过期瞬间大量请求同时打到 DB → DB 压力骤增 场景秒杀商品、热门新闻解决方案方案原理优点缺点互斥锁只让一个线程重建缓存简单有等待逻辑过期不设物理过期异步更新无等待实现复杂永不过期热点 key 不设过期简单需手动更新互斥锁实现publicProductgetProductWithLock(Longid){Stringkeyproduct:id;Stringcachedredis.get(key);if(cached!null){returnJSON.parseObject(cached,Product.class);}// 获取分布式锁StringlockKeylock:product:id;try{booleanlockedredis.setnx(lockKey,1,10);if(locked){// 双重检查cachedredis.get(key);if(cached!null){returnJSON.parseObject(cached,Product.class);}// 查 DB 并回填ProductproductproductMapper.selectById(id);redis.setex(key,3600,JSON.toJSONString(product));returnproduct;}else{// 未获取锁等待后重试Thread.sleep(50);returngetProductWithLock(id);}}finally{redis.delete(lockKey);}}4.3 缓存雪崩问题大量 key 同时过期或 Redis 宕机 → 所有请求打到 DBDB 崩溃 场景凌晨批量刷新缓存、Redis 集群故障解决方案方案原理过期时间加随机值避免同时过期多级缓存本地缓存兜底熔断降级DB 压力大时拒绝请求Redis 高可用主从 哨兵 集群过期时间加随机值// 错误所有 key 都是 3600 秒redis.setex(key,3600,value);// 正确加随机值避免同时过期intexpire3600RandomUtils.nextInt(0,600);redis.setex(key,expire,value);五、分布式锁5.1 为什么需要分布式锁场景秒杀扣库存 ① 读库存 10 ② 判断 0 ③ 扣减库存 9 问题并发时多个线程同时读到 10都扣减 → 超卖5.2 Redis 分布式锁实现基础版有缺陷// 加锁Booleanlockedredis.setnx(lock:stock,1);if(locked){try{// 业务逻辑}finally{redis.delete(lock:stock);// 问题可能删别人的锁}}改进版SET NX EX// 加锁原子操作设置过期时间StringrequestIdUUID.randomUUID().toString();Booleanlockedredis.set(lock:stock,requestId,NX,EX,10);if(locked){try{// 业务逻辑}finally{// 释放锁判断是自己的锁才删Lua 脚本保证原子性Stringscriptif redis.call(get, KEYS[1]) ARGV[1] then return redis.call(del, KEYS[1]) else return 0 end;redis.eval(script,Collections.singletonList(lock:stock),Collections.singletonList(requestId));}}5.3 Redisson生产级分布式锁// 加锁RLocklockredisson.getLock(lock:stock);try{// 尝试加锁最多等待 10 秒锁自动续期booleanlockedlock.tryLock(10,30,TimeUnit.SECONDS);if(locked){// 业务逻辑}}finally{if(lock.isHeldByCurrentThread()){lock.unlock();}}Redisson 的优势① 自动续期看门狗机制 → 业务未执行完锁不会过期 ② 可重入 → 同一线程可多次加锁 ③ 支持多种锁 → 公平锁、读写锁、联锁 ④ 高可用 → RedLock 算法多节点容错5.4 分布式锁对比方案优点缺点适用场景Redis SET NX简单、性能好需自行处理续期一般场景Redisson功能全、自动续期依赖 Redis生产首选ZooKeeper强一致、临时节点性能差强一致要求etcd强一致、租约生态弱K8s 环境六、Redis 高可用6.1 三种架构对比架构原理优点缺点主从一主多从读写分离简单故障需手动切换哨兵主从 自动故障转移自动切换写能力受限集群分片 主从水平扩展复杂度高6.2 哨兵模式哨兵架构 │ ├── Sentinel 1 ──┐ ├── Sentinel 2 ──┼── 监控 Master ├── Sentinel 3 ──┘ │ ├── Master写 ├── Slave 1读 └── Slave 2读 故障转移 ① Sentinel 发现 Master 宕机 ② 多数派确认 ③ 选举新 Master ④ 其他 Slave 指向新 Master ⑤ 通知客户端6.3 集群模式Redis Cluster │ ├── 分片16384 个槽位 ├── 每个 Master 负责一部分槽 ├── 每个 Master 有多个 Slave └── 客户端根据 key 计算槽位路由到对应节点 槽位计算 slot CRC16(key) % 16384七、实战任务任务清单实现 Cache Aside 模式的读写逻辑用布隆过滤器解决缓存穿透用互斥锁解决缓存击穿用随机过期时间解决缓存雪崩用 Redisson 实现分布式锁搭建 Redis 哨兵模式用 Canal 实现缓存自动删除自检问题缓存更新有哪四种策略为什么推荐 Cache Aside为什么是删除缓存而不是更新缓存先删缓存还是先更新 DB为什么什么是延迟双删解决什么问题缓存穿透、击穿、雪崩的区别各自的解决方案Redis 分布式锁的 SET NX EX 为什么比 SETNX 好Redisson 的看门狗机制是什么Redis 哨兵和集群的区别八、本篇小结第11篇 核心收获 │ ├── 缓存价值 │ ├── 降低 DB 压力 │ ├── 提升响应速度 │ └── 扛住高并发 │ ├── 更新策略 │ ├── Cache Aside先更新 DB再删缓存 │ ├── Read/Write Through缓存代理 │ ├── Write Behind异步刷 DB │ └── 双写不推荐 │ ├── 一致性 │ ├── 先更新 DB 再删缓存推荐 │ ├── 延迟双删兜底 │ ├── 分布式锁强一致 │ └── Canal binlog最终一致 │ ├── 三大问题 │ ├── 穿透缓存空值 / 布隆过滤器 │ ├── 击穿互斥锁 / 逻辑过期 │ └── 雪崩随机过期 / 多级缓存 │ ├── 分布式锁 │ ├── SET NX EX基础版 │ ├── Lua 脚本安全释放 │ └── Redisson生产级 │ └── Redis 高可用 ├── 主从读写分离 ├── 哨兵自动切换 └── 集群水平扩展下一篇第12篇《综合实战电商数据库全流程》