Featured image of post Redis-MySQL 数据同步方案

Redis-MySQL 数据同步方案

Redis-MySQL 数据同步方案

系统架构师考试 | 数据库 + 缓存架构专题 对应 2021 年下半年案例分析 试题三 Q3 阅读时长:约 12 分钟

前言

Redis 作为内存数据库,读写性能是 MySQL 的 10-100 倍,几乎所有高并发系统都会用 Redis 做缓存。但缓存和数据库的"双写一致性问题" 是后端开发最常踩的坑——数据改了缓存没改、缓存删了下次又查到老的、并发写导致脏读。本篇系统梳理 Redis 五种数据类型、5 种同步方案、3 个一致性等级,以及 2024+ 的 CDC 自动化方案。


一、Redis 五种数据类型(软考必考)

类型结构典型命令典型场景
String字符串SET GET INCR缓存值、计数器、分布式锁
Hash字段-值映射HSET HGET HGETALL对象缓存(用户资料、商品详情)
List双向链表LPUSH RPOP LRANGE消息队列、最新列表
Set无序集合SADD SMEMBERS SINTER标签、共同好友、去重
ZSet有序集合ZADD ZRANGE ZINCRBY排行榜、延迟队列

1.1 实战:热销药品排名用 ZSet

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# 添加药品销量
ZADD hot:drug 156 drug:1001
ZADD hot:drug 89 drug:1002
ZADD hot:drug 234 drug:1003

# 销售 +1(原子操作)
ZINCRBY hot:drug 1 drug:1001

# 取 Top 10
ZRANGE hot:drug 0 9 WITHSCORES
# 1) "drug:1003"  # 234
# 2) "drug:1001"  # 156
# 3) "drug:1002"  # 89

2021 真题答案ZSet(有序集合)——天然支持按分数排序,ZADD/ZINCRBY/ZRANGE 三个命令搞定。


二、为什么需要缓存?

2.1 性能对比

操作MySQL(磁盘)Redis(内存)差距
5-50 ms0.1-1 ms50-500x
10-100 ms0.1-1 ms100-1000x
随机读10-100 ms0.1-1 ms100-1000x

2.2 典型缓存场景

  1. 读多写少:商品详情、用户资料、文章内容
  2. 排行榜 / 计数器:销量、点赞、播放量
  3. 分布式锁:SETNX、Redlock
  4. 限流:滑动窗口、令牌桶
  5. 消息队列:Stream、Pub/Sub
  6. 热点数据预热:秒杀库存、热门商品

三、5 种同步方案

3.1 Cache-Aside(旁路缓存,最常用)

思路:应用代码同时管理 DB 和 Cache。

1
2
3
4
5
6
7
8
读:
  1. 先读 Redis
  2. miss 则读 MySQL
  3. 回写 Redis

写:
  1. 先写 MySQL
  2. 再删 Redis(不是更新!)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// 读
public Product getProduct(Long id) {
    Product p = redis.get("product:" + id);
    if (p == null) {
        p = mysql.selectById(id);
        redis.setex("product:" + id, 3600, p);  // 1小时过期
    }
    return p;
}

// 写
public void updateProduct(Product p) {
    mysql.updateById(p);
    redis.del("product:" + p.getId());  // 删缓存,下次读 miss 再回填
}

优点:简单、成熟、90% 场景适用 缺点:存在"先删缓存再写 DB"的并发问题

并发问题

1
2
3
T1 写:删缓存 → 写 DB(未提交)
T2 读:miss → 读 DB(旧值)→ 写缓存(脏数据)
T1 提交

3.2 延迟双删(解决并发写脏读)

思路:写前删一次、写后延迟再删一次。

1
2
3
4
5
6
public void updateProduct(Product p) {
    redis.del("product:" + p.getId());  // 第一次删
    mysql.updateById(p);
    Thread.sleep(500);  // 延迟 500ms(经验值,看主从延迟)
    redis.del("product:" + p.getId());  // 第二次删
}

优点:解决大部分并发写问题 缺点:延迟时间难定、影响写性能

3.3 Write-Through(写穿透)

思路:先写 DB,再同步写 Cache(Cache 自己从 DB 加载)。

1
2
3
写:
  1. 应用写 DB
  2. Cache 自动同步写(或由 Cache 监听 DB 变更)

适用:读多写少 + 强一致 + 写不频繁(如金融账户)

3.4 Write-Behind(异步写回)

思路:先写 Cache,异步批量写 DB。

1
2
3
写:
  1. 只写 Cache(立即返回)
  2. 异步队列批量 flush 到 DB

适用:写多读少 + 允许短暂丢失(如点赞数、浏览量)

3.5 异步 binlog 订阅(CDC,解耦方案)

思路:用 Canal(阿里开源)订阅 MySQL binlog,异步同步到 Redis。

1
应用写 MySQL → MySQL 写 binlog → Canal 订阅 → 推送到 Redis

Canal 架构

1
2
3
MySQL Master ──binlog──> Canal Server ──Kafka──> 消费服务 ──> Redis
                                   MQ 解耦 / 重试 / 削峰

优点:业务代码零侵入、解耦、可靠、可重试 缺点:架构复杂、运维成本、有秒级延迟

2024+ 主流CDC(Change Data Capture) 事实标准:

  • Canal(阿里)→ MySQL 专用
  • Debezium → 多数据库支持
  • Maxwell → 轻量级
  • Flink CDC → 实时计算集成

四、3 个一致性等级

等级一致性性能适用
强一致写后立即读到新值金融、库存
弱一致最终一致(秒级)大部分业务
最终一致不保证时点,只保证最终最好排行榜、计数器

软考答题:除了答"哪种方案",还要答"对应什么一致性等级"。


五、5 种方案对比表

方案一致性性能复杂度适用
Cache-Aside弱一致90% 业务
延迟双删弱一致高并发写
Write-Through强一致金融账户
Write-Behind最终一致最高点赞、计数
CDC 异步同步最终一致大厂、解耦

六、实战:药品销售系统缓存设计

6.1 缓存选型

数据缓存类型同步方案一致性
药品详情Hash(字段级更新)Cache-Aside弱一致
热销排行ZSet(销量)Write-Behind最终一致
库存数量String(强实时)延迟双删弱一致
用户 SessionString + TTL不需要最终一致

6.2 完整流程

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// 1. 商品查询(Cache-Aside)
public Product getProduct(Long id) {
    String key = "product:" + id;
    Map<Object, Object> cached = redis.opsForHash().entries(key);
    if (cached.isEmpty()) {
        Product p = mysql.selectById(id);
        redis.opsForHash().putAll(key, beanToMap(p));
        redis.expire(key, 3600);
        return p;
    }
    return mapToBean(cached);
}

// 2. 库存扣减(延迟双删 + Lua 原子操作)
public boolean deductStock(Long productId, int qty) {
    String key = "stock:" + productId;
    String lua = """
        local current = tonumber(redis.call('GET', key))
        if current == nil or current < qty then
            return 0
        end
        redis.call('DECRBY', key, qty)
        return 1
    """.replace("qty", String.valueOf(qty));
    Long result = redis.execute(luaScript, Arrays.asList(key));
    if (result == 1) {
        mysql.updateStock(productId, -qty);  // 异步写 DB
    }
    return result == 1;
}

// 3. 销量排行(Write-Behind + ZSet)
@Async
public void onOrderCreated(Order order) {
    // 只写 Redis
    redis.opsForZSet().incrementScore("hot:drug", 
        "drug:" + order.getProductId(), 
        order.getQuantity());
    // 异步队列定时 flush 到 MySQL
    kafkaTemplate.send("sales-stat", order);
}

6.3 同步问题排查清单

现象原因解决
缓存偶尔读到旧值并发写延迟双删
缓存删了很快又有业务没改完又回填加版本号
Redis 内存爆炸缓存没设 TTL全部 key 加过期
Redis 击穿热点 key 过期互斥锁 / 永不过期
Redis 雪崩大量 key 同时过期过期时间加随机

七、2024+ 视角

7.1 CDC 主流方案对比

方案出品方特点
Canal阿里MySQL 专用、Java、国内最流行
DebeziumRed Hat多库支持、Kafka 集成
MaxwellZendesk轻量、JSON 输出
Flink CDC阿里实时计算一体化
Apache InLong腾讯大数据集成

7.2 Redis 新特性

  • Redis 7.x
    • Functions(替代 Lua 脚本管理)
    • Multi-part AOF(AOF 拆分)
    • RedisJSON(原生 JSON 支持)
    • RedisSearch(全文检索)
    • RedisTimeSeries(时序数据)
  • Redis 8(2024+):
    • 性能进一步提升(多线程 I/O)
    • Vector Search(向量检索,对接 RAG / LLM)
    • 与 LangChain / LlamaIndex 集成

7.3 一致性新方案

  • Outbox Pattern(事件外发模式):业务写 DB + outbox 表,异步发到 MQ 同步到 Redis → 解决分布式事务
  • Saga Pattern:长事务拆多个子事务 + 补偿 → 跨服务一致性
  • CRDT(Conflict-free Replicated Data Types):无冲突复制数据类型 → 多活数据中心
  • Spanner / CockroachDB:全球强一致分布式 SQL → 替代 MySQL + Redis 组合

7.4 软考答题趋势

  • 2024+ 更注重架构图——画出 DB + Cache + MQ + CDC 的完整链路
  • 经常给故障场景让你分析:缓存雪崩 / 击穿 / 穿透
  • 一致性话题会和分布式事务结合考

八、答题套路总结

题型套路
选 Redis 数据类型排行榜 → ZSet;对象 → Hash;队列 → List;标签 → Set;单值 → String
同步方案默认答 Cache-Aside + 延迟双删;进阶答 CDC
一致性等级强一致 / 弱一致 / 最终一致
缓存三大问题雪崩(大量同时过期)/ 击穿(热点过期)/ 穿透(绕过缓存)

参考资料

使用 Hugo 构建
主题 StackJimmy 设计