【数据库】MySQL 实战精通系列 · 第11篇:Redis 缓存与 MySQL 一致性实战

MySQL 实战精通系列 · 第11篇:Redis 缓存与 MySQL 一致性实战

本篇目标:缓存和数据库怎么配合、数据不一致怎么解决、缓存击穿/穿透/雪崩怎么防------从缓存策略到分布式锁,系统化掌握缓存一致性方案。


文章目录

  • [MySQL 实战精通系列 · 第11篇:Redis 缓存与 MySQL 一致性实战](#MySQL 实战精通系列 · 第11篇:Redis 缓存与 MySQL 一致性实战)
    • 一、为什么需要缓存
      • [1.1 缓存的核心价值](#1.1 缓存的核心价值)
      • [1.2 一张图看懂缓存位置](#1.2 一张图看懂缓存位置)
    • 二、缓存更新策略
      • [2.1 四种更新策略对比](#2.1 四种更新策略对比)
      • [2.2 Cache Aside:推荐方案](#2.2 Cache Aside:推荐方案)
      • [2.3 先删缓存还是先更新 DB](#2.3 先删缓存还是先更新 DB)
    • 三、缓存一致性深入
      • [3.1 不一致的根本原因](#3.1 不一致的根本原因)
      • [3.2 强一致方案](#3.2 强一致方案)
      • [3.3 缓存一致性实战代码](#3.3 缓存一致性实战代码)
    • 四、缓存三大问题
      • [4.1 缓存穿透](#4.1 缓存穿透)
      • [4.2 缓存击穿](#4.2 缓存击穿)
      • [4.3 缓存雪崩](#4.3 缓存雪崩)
    • 五、分布式锁
      • [5.1 为什么需要分布式锁](#5.1 为什么需要分布式锁)
      • [5.2 Redis 分布式锁实现](#5.2 Redis 分布式锁实现)
      • [5.3 Redisson:生产级分布式锁](#5.3 Redisson:生产级分布式锁)
      • [5.4 分布式锁对比](#5.4 分布式锁对比)
    • [六、Redis 高可用](#六、Redis 高可用)
      • [6.1 三种架构对比](#6.1 三种架构对比)
      • [6.2 哨兵模式](#6.2 哨兵模式)
      • [6.3 集群模式](#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 并删缓存,读线程回填旧值
  → 概率极低,且可通过延迟双删兜底

延迟双删:

java 复制代码
// 更新 DB
updateDB(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     删除缓存
  → 缓存 = 2,DB = 2,一致 ✓

  时刻   线程A(写)              线程B(读)
  T1                             读缓存 miss
  T2                             读 DB = 1(旧值)
  T3     更新 DB = 2
  T4     删除缓存
  T5                             写缓存 = 1(旧值)
  → 缓存 = 1,DB = 2,不一致 ✗

结论:先更新 DB 再删缓存,在极端并发下仍可能不一致,但概率极低。

3.2 强一致方案

方案 原理 性能 适用场景
分布式锁 读写都加锁 差 强一致要求
订阅 binlog Canal 监听,异步删缓存 好 最终一致
延迟双删 删两次缓存 中 兜底方案

Canal 方案:

复制代码
MySQL binlog → Canal → MQ → 缓存删除服务 → 删除 Redis

优势:
  ① 业务代码无侵入
  ② 保证最终一致
  ③ 异步解耦

3.3 缓存一致性实战代码

java 复制代码
public Product getProduct(Long id) {
    String key = "product:" + id;
    
    // ① 读缓存
    String cached = redis.get(key);
    if (cached != null) {
        return JSON.parseObject(cached, Product.class);
    }
    
    // ② 读 DB
    Product product = productMapper.selectById(id);
    if (product == null) {
        // 防穿透:缓存空值
        redis.setex(key, 60, "");
        return null;
    }
    
    // ③ 写缓存
    redis.setex(key, 3600, JSON.toJSONString(product));
    return product;
}

@Transactional
public void updateProduct(Product product) {
    // ① 更新 DB
    productMapper.updateById(product);
    
    // ② 删除缓存
    redis.delete("product:" + product.getId());
    
    // ③ 延迟双删(可选)
    delayDelete("product:" + product.getId(), 500);
}

四、缓存三大问题

4.1 缓存穿透

复制代码
问题:查询不存在的数据,缓存和 DB 都没有
  → 每次请求都打到 DB

场景:恶意攻击,用不存在的 ID 刷接口

解决方案:

方案 原理 优点 缺点
缓存空值 不存在也缓存,设短过期 简单 占内存
布隆过滤器 预判 key 是否存在 内存小 有误判
参数校验 拦截非法请求 从源头 不通用

布隆过滤器实现:

java 复制代码
// 初始化布隆过滤器
BloomFilter<Long> bloomFilter = BloomFilter.create(
    Funnels.longFunnel(),
    1000000,   // 预期元素数量
    0.01       // 误判率
);

// 写入时加入
bloomFilter.put(productId);

// 查询前判断
if (!bloomFilter.mightContain(productId)) {
    return null;  // 一定不存在
}

4.2 缓存击穿

复制代码
问题:热点 key 过期瞬间,大量请求同时打到 DB
  → DB 压力骤增

场景:秒杀商品、热门新闻

解决方案:

方案 原理 优点 缺点
互斥锁 只让一个线程重建缓存 简单 有等待
逻辑过期 不设物理过期,异步更新 无等待 实现复杂
永不过期 热点 key 不设过期 简单 需手动更新

互斥锁实现:

java 复制代码
public Product getProductWithLock(Long id) {
    String key = "product:" + id;
    String cached = redis.get(key);
    if (cached != null) {
        return JSON.parseObject(cached, Product.class);
    }
    
    // 获取分布式锁
    String lockKey = "lock:product:" + id;
    try {
        boolean locked = redis.setnx(lockKey, "1", 10);
        if (locked) {
            // 双重检查
            cached = redis.get(key);
            if (cached != null) {
                return JSON.parseObject(cached, Product.class);
            }
            
            // 查 DB 并回填
            Product product = productMapper.selectById(id);
            redis.setex(key, 3600, JSON.toJSONString(product));
            return product;
        } else {
            // 未获取锁,等待后重试
            Thread.sleep(50);
            return getProductWithLock(id);
        }
    } finally {
        redis.delete(lockKey);
    }
}

4.3 缓存雪崩

复制代码
问题:大量 key 同时过期,或 Redis 宕机
  → 所有请求打到 DB,DB 崩溃

场景:凌晨批量刷新缓存、Redis 集群故障

解决方案:

方案 原理
过期时间加随机值 避免同时过期
多级缓存 本地缓存兜底
熔断降级 DB 压力大时拒绝请求
Redis 高可用 主从 + 哨兵 + 集群

过期时间加随机值:

java 复制代码
// 错误:所有 key 都是 3600 秒
redis.setex(key, 3600, value);

// 正确:加随机值,避免同时过期
int expire = 3600 + RandomUtils.nextInt(0, 600);
redis.setex(key, expire, value);

五、分布式锁

5.1 为什么需要分布式锁

复制代码
场景:秒杀扣库存
  ① 读库存 = 10
  ② 判断 > 0
  ③ 扣减库存 = 9

问题:并发时,多个线程同时读到 10,都扣减 → 超卖

5.2 Redis 分布式锁实现

基础版(有缺陷):

java 复制代码
// 加锁
Boolean locked = redis.setnx("lock:stock", "1");
if (locked) {
    try {
        // 业务逻辑
    } finally {
        redis.delete("lock:stock");  // 问题:可能删别人的锁
    }
}

改进版(SET NX EX):

java 复制代码
// 加锁:原子操作,设置过期时间
String requestId = UUID.randomUUID().toString();
Boolean locked = redis.set("lock:stock", requestId, "NX", "EX", 10);
if (locked) {
    try {
        // 业务逻辑
    } finally {
        // 释放锁:判断是自己的锁才删(Lua 脚本保证原子性)
        String script = 
            "if 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:生产级分布式锁

java 复制代码
// 加锁
RLock lock = redisson.getLock("lock:stock");
try {
    // 尝试加锁,最多等待 10 秒,锁自动续期
    boolean locked = lock.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 实现缓存自动删除

自检问题

  1. 缓存更新有哪四种策略?为什么推荐 Cache Aside?
  2. 为什么是删除缓存而不是更新缓存?
  3. 先删缓存还是先更新 DB?为什么?
  4. 什么是延迟双删?解决什么问题?
  5. 缓存穿透、击穿、雪崩的区别?各自的解决方案?
  6. Redis 分布式锁的 SET NX EX 为什么比 SETNX 好?
  7. Redisson 的看门狗机制是什么?
  8. Redis 哨兵和集群的区别?

八、本篇小结

复制代码
第11篇 核心收获
│
├── 缓存价值
│   ├── 降低 DB 压力
│   ├── 提升响应速度
│   └── 扛住高并发
│
├── 更新策略
│   ├── Cache Aside:先更新 DB,再删缓存
│   ├── Read/Write Through:缓存代理
│   ├── Write Behind:异步刷 DB
│   └── 双写:不推荐
│
├── 一致性
│   ├── 先更新 DB 再删缓存(推荐)
│   ├── 延迟双删(兜底)
│   ├── 分布式锁(强一致)
│   └── Canal + binlog(最终一致)
│
├── 三大问题
│   ├── 穿透:缓存空值 / 布隆过滤器
│   ├── 击穿:互斥锁 / 逻辑过期
│   └── 雪崩:随机过期 / 多级缓存
│
├── 分布式锁
│   ├── SET NX EX:基础版
│   ├── Lua 脚本:安全释放
│   └── Redisson:生产级
│
└── Redis 高可用
    ├── 主从:读写分离
    ├── 哨兵:自动切换
    └── 集群:水平扩展

下一篇:第12篇《综合实战:电商数据库全流程》

相关推荐
Leo.yuan2 小时前
2026企业级数据集成平台市场报告:国内外厂商格局与技术路线
数据库·oracle
霸道流氓气质2 小时前
AI应用成本优化完全指南:Token压缩、语义缓存与模型路由的Java生产级实战
java·人工智能·缓存
guo_wen_qiang2 小时前
云服务器mysql分库分表环境搭建(2库4表)
数据库·mysql·docker
jianpeng的工程笔记2 小时前
EL8 / EL9 安装 Slurm 26.05.4:主备控制器、MariaDB 记账与作业验收
linux·数据库·mariadb·高性能计算·slurm
SelectDB技术团队2 小时前
ClickHouse 存日志的能力边界:并发、检索与 trace 回放的实测对照
数据结构·数据库·clickhouse·日志分析·apache doris·日志存储
倔强的小石头_2 小时前
Ubuntu部署Prometheus与Alertmanager:systemd配置、告警对接及cpolar远程访问
数据库·ubuntu·prometheus
茉莉玫瑰花茶3 小时前
GO [ 接口 ]
服务器·数据库·golang
babe小鑫3 小时前
金融工程专业秋招:金融类证书与数据分析类证书的搭配方案
大数据·数据库·人工智能