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 ms | 0.1-1 ms | 50-500x |
| 写 | 10-100 ms | 0.1-1 ms | 100-1000x |
| 随机读 | 10-100 ms | 0.1-1 ms | 100-1000x |
2.2 典型缓存场景
- 读多写少:商品详情、用户资料、文章内容
- 排行榜 / 计数器:销量、点赞、播放量
- 分布式锁:SETNX、Redlock
- 限流:滑动窗口、令牌桶
- 消息队列:Stream、Pub/Sub
- 热点数据预热:秒杀库存、热门商品
三、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(强实时) | 延迟双删 | 弱一致 |
| 用户 Session | String + 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、国内最流行 |
| Debezium | Red Hat | 多库支持、Kafka 集成 |
| Maxwell | Zendesk | 轻量、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 |
| 一致性等级 | 强一致 / 弱一致 / 最终一致 |
| 缓存三大问题 | 雪崩(大量同时过期)/ 击穿(热点过期)/ 穿透(绕过缓存) |
参考资料