Skip to content

Commit d0dd660

Browse files
Add query separation design analysis with diagrams
- Add thoughts.md with design critique for Chapter 2 (Query Separation) - Identify 3 key design flaws in original approach: 1. Poor message design (trigger-only signals) 2. Incomplete timing/ordering solution (non-atomic checks) 3. Weak idempotency guarantees (boolean flag only) - Document improved design with version-based approach - Add 9 architecture diagrams illustrating problems and solutions Co-Authored-By: Claude <noreply@anthropic.com>
1 parent be103f8 commit d0dd660

16 files changed

+276
-0
lines changed
1.53 MB
Loading
1.53 MB
Loading
1.48 MB
Loading
1.71 MB
Loading
1.64 MB
Loading
1.54 MB
Loading

docs/data-persistence/thoughts.md

Lines changed: 276 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,276 @@
1+
---
2+
sidebar_position: 99
3+
title: 思考与改进
4+
description: 对本部分内容的思考、问题分析与设计改进
5+
---
6+
7+
# 数据持久化层:思考与改进
8+
9+
## 概述
10+
11+
本页记录对本部分各章节的思考与改进建议。
12+
13+
---
14+
15+
## 第1章 冷热分离 设计改进
16+
17+
### 原设计问题
18+
19+
(描述原设计存在的问题)
20+
21+
### 改进方案
22+
23+
(描述你的改进设计)
24+
25+
---
26+
27+
## 第2章 查询分离 设计改进
28+
29+
### 原设计问题
30+
31+
原文档中的查询分离方案存在以下设计缺陷:
32+
33+
#### 1. 消息体设计不合理
34+
35+
![消息体设计问题](/img/chapter2-improved/problem-1-message-design.png)
36+
37+
原设计中 MQ 消息仅作为"触发信号",不携带具体业务数据:
38+
39+
> "MQ 的消费者获取信号后,先批量查询待更新的主数据,然后批量更新查询数据"
40+
41+
这种设计存在严重问题:
42+
43+
- **效率低下**:Consumer 收到消息后需要扫描数据库查找 `NeedUpdateQueryData=true` 的记录,而非直接处理特定记录
44+
- **并发时重复更新**:多个业务操作同时触发多个信号,每个 Consumer 都会扫描整个待更新列表,导致同一条记录被多个 Consumer 重复处理,浪费资源
45+
- **延迟增加**:额外的数据库扫描步骤增加了同步延迟
46+
- **数据库压力**:频繁的批量扫描对数据库造成不必要的负载
47+
- **无法精确追踪**:无法知道是哪条记录触发了这次同步,难以监控和排查问题
48+
49+
**正确做法**:消息体应包含 `order_id``version`,Consumer 可直接定位到具体记录进行处理。
50+
51+
#### 2. 时序性问题解决方案不完善
52+
53+
![时序性问题](/img/chapter2-improved/problem-2-timing-issue.png)
54+
55+
原文提到的解决方案是:在更新查询数据前,检查当前工单的 `last_update_time` 是否与线程刚开始获得的时间相同。
56+
57+
这个方案存在问题:
58+
59+
- **缺乏原子性**:检查和写入不是原子操作,检查通过后、写入前仍可能被其他线程抢先更新,存在竞态条件
60+
- **依赖时间戳**`last_update_time` 精度有限,高并发下可能出现相同时间戳的情况,无法区分先后顺序
61+
62+
#### 3. 幂等性保证薄弱
63+
64+
![幂等性问题](/img/chapter2-improved/problem-3-idempotency.png)
65+
66+
原设计的幂等性设计过于简单:
67+
68+
- **仅依赖标识位**:使用 `NeedUpdateQueryData` 布尔标识,无法区分不同版本的更新
69+
- **未利用 ES 能力**:Elasticsearch 提供了 `version_type=external` 的乐观锁机制,原设计未加以利用
70+
71+
---
72+
73+
### 改进方案
74+
75+
针对上述问题,我们设计了基于**版本号**的改进方案,通过多层保护机制确保数据一致性和系统可靠性。
76+
77+
#### 改进后的整体架构
78+
79+
![整体架构](/img/chapter2-improved/01-overall-architecture.png)
80+
81+
**核心改进点:**
82+
83+
1. **引入版本号机制**:MySQL 主表增加 `version` 字段,每次更新时递增
84+
2. **版本携带**:MQ 消息携带 `order_id``version` 信息
85+
3. **版本写入**:Consumer 使用 `version_type=external` 写入 ES
86+
4. **定时兜底**:Timer Task 定期扫描 `need_update=true` 的记录进行补偿同步
87+
88+
**数据流说明:**
89+
90+
```
91+
1. Client → Service # 客户端请求
92+
2. Service → MySQL (update data) # 更新主数据,version++
93+
3. Service → MQ (send order_id, version) # 发送消息到队列
94+
4. MQ → Consumer # 消费者接收消息
95+
5. Consumer → MySQL (query) # 查询完整数据和当前版本
96+
6. Consumer → ES (write with version) # 带版本写入 ES
97+
7. Consumer → MySQL (update flag) # 更新 need_update=false
98+
```
99+
100+
---
101+
102+
#### 消息处理流程
103+
104+
![消息处理流程](/img/chapter2-improved/02-message-flow.png)
105+
106+
**版本比较逻辑:**
107+
108+
```
109+
收到消息 (order_id, msg_version)
110+
111+
查询 MySQL 获取 db_version
112+
113+
比较版本:
114+
├── msg_version < db_version → Skip(已有更新消息会处理)
115+
├── msg_version = db_version → Write ES (version_type=external)
116+
│ ├── Success → Update MySQL need_update=false
117+
│ ├── Version Conflict → Skip(ES 已有更新版本)
118+
│ └── Other Error → Wait for retry
119+
└── msg_version > db_version → Exception(不应发生,需告警)
120+
```
121+
122+
**关键设计:**
123+
124+
- 利用 ES 的 `version_type=external` 实现乐观锁
125+
- 版本冲突时直接跳过,因为 ES 中已有更新的数据
126+
- 异常情况记录日志并触发告警
127+
128+
---
129+
130+
#### 并发场景处理
131+
132+
![并发挑战](/img/chapter2-improved/03-concurrency-challenge.png)
133+
134+
**场景说明:**
135+
136+
```
137+
Timeline:
138+
Business A: Update version=3 → Send Message (v=3)
139+
Business B: Update version=4 → Send Message (v=4)
140+
141+
Consumer A 收到 v=4 (先到) Consumer B 收到 v=3 (后到)
142+
↓ ↓
143+
Query: version=4 Query: version=4
144+
↓ ↓
145+
Write ES (v=4) ✓ Compare: 3 < 4 → Skip ✓
146+
```
147+
148+
**结果:** 虽然消息乱序到达,但通过版本比较,最终 ES 中保存的是 version=4 的数据,实现**最终一致性**
149+
150+
---
151+
152+
#### 三层幂等性保证
153+
154+
![幂等性保证](/img/chapter2-improved/04-idempotency.png)
155+
156+
**Layer 1: Consumer 版本检查**
157+
```
158+
if msg_version < db_version:
159+
skip # 过时消息,直接跳过
160+
```
161+
162+
**Layer 2: ES External Version**
163+
```
164+
ES 会拒绝 version <= 当前存储版本 的写入
165+
返回 409 Version Conflict → Skip
166+
```
167+
168+
**Layer 3: MySQL CAS 更新**
169+
```sql
170+
UPDATE orders
171+
SET need_update = false
172+
WHERE order_id = ?
173+
AND version = ?
174+
AND affected_rows > 0
175+
```
176+
177+
**三层保护确保:**
178+
- 重复消息不会导致重复处理
179+
- 乱序消息不会覆盖新数据
180+
- 数据库状态更新的原子性
181+
182+
---
183+
184+
#### 容错与降级机制
185+
186+
![容错与降级](/img/chapter2-improved/05-fault-tolerance.png)
187+
188+
**故障场景及处理:**
189+
190+
| 故障类型 | 现象 | 处理机制 |
191+
|---------|------|---------|
192+
| MQ 宕机 | 消息无法发送 | `need_update=true` 保留,Timer Task 兜底 |
193+
| 消息丢失 | Consumer 未收到 | 同上 |
194+
| Consumer 崩溃 | 处理中断 | `need_update=true` 未更新,Timer Task 兜底 |
195+
| ES 写入失败 | 返回错误 | 不更新标识,消息重试 / Timer Task 兜底 |
196+
197+
**双路径设计:**
198+
199+
```
200+
正常路径(快): Business → MQ → Consumer → ES
201+
降级路径(慢): Timer Task → MySQL scan(need_update=true) → ES
202+
```
203+
204+
**Timer Task 设计要点:**
205+
- 定期扫描 `need_update=true AND update_time < NOW() - 30s` 的记录
206+
- 批量处理,控制并发
207+
- 同样遵循版本控制规则
208+
209+
---
210+
211+
#### 数据一致性分析
212+
213+
![数据一致性](/img/chapter2-improved/06-data-consistency.png)
214+
215+
**写入时序与不一致窗口:**
216+
217+
```
218+
时间轴:
219+
T0: BEGIN 事务
220+
T1: UPDATE MySQL (need_update=true, version++)
221+
T2: COMMIT 事务
222+
T3: Send MQ 消息
223+
T4: Consumer 处理
224+
T5: ES 更新完成
225+
226+
不一致窗口: [T2, T5]
227+
```
228+
229+
**各时间点数据状态:**
230+
231+
| 时间点 | MySQL (Source of Truth) | ES (Search View) | 说明 |
232+
|-------|-------------------------|------------------|------|
233+
| T1 (COMMIT前) | Old Version | Old Version | 事务未提交 |
234+
| T2 (COMMIT后) | **New Version**, need_update=true | Old Version | 不一致开始 |
235+
| T5 (ES更新后) | New Version, need_update=false | **New Version** | 一致性恢复 |
236+
237+
**关键工程原则:**
238+
239+
1. **提交前设置标识**:确保数据存在即有标识,不会遗漏
240+
```sql
241+
BEGIN;
242+
UPDATE orders SET ..., need_update=true, version=version+1;
243+
COMMIT;
244+
-- 然后发 MQ
245+
```
246+
247+
2. **版本单调递增**:永不回退,拒绝旧版本写入
248+
249+
3. **最终一致性保证**
250+
- 正常路径:毫秒级延迟
251+
- 降级路径:Timer Task 周期(如 30 秒)
252+
- 客户端可显示:"数据同步中,如有不一致请稍后刷新"
253+
254+
---
255+
256+
### 改进前后对比
257+
258+
| 维度 | 原设计 | 改进设计 |
259+
|-----|-------|---------|
260+
| 消息体设计 | 仅触发信号,需扫描数据库 | 携带 `order_id` + `version`,直接定位处理 |
261+
| 时序性保证 | 检查 `last_update_time`,缺乏原子性 | 版本号 + ES external version,原子性保证 |
262+
| 幂等性保证 | 单层(标识位) | 三层(Consumer + ES + MySQL CAS) |
263+
264+
---
265+
266+
## 第3章 分表分库 设计改进
267+
268+
### 原设计问题
269+
270+
(描述原设计存在的问题)
271+
272+
### 改进方案
273+
274+
(描述你的改进设计)
275+
276+
---
1.53 MB
Loading
1.53 MB
Loading
1.48 MB
Loading

0 commit comments

Comments
 (0)