深入专题:数据库、缓存与消息一致性¶
这篇解决一个经常被“用了 Redis、用了 MQ”掩盖的问题:
一份业务状态同时出现在数据库、缓存和消息里时,哪一份是真的?某一步失败后怎样收敛?
先建立三个角色¶
数据库:业务事实¶
例如:
- 工单是否存在;
- 命令当前状态;
- 用户是否拥有设备;
- Agent 某个副作用是否完成。
Redis:查询视图或短期协调¶
例如:
- 当前在线状态;
- 一页设备的热数据;
- 限流计数;
- 短期锁;
- 运行中任务视图。
Redis 也可以持久化,但在架构判断中仍要明确它是否是事实源、可重建视图,还是两者之一。
MQ:变化的传递¶
消息表达:
某个事实已经发生,其他组件可以据此更新自己的状态。
消息本身不一定是最终业务事实,消费者也可能晚到、重复或失败。
典型问题:更新数据库和缓存¶
用户修改设备名称:
怎样避免不一致?
方案一:先写缓存,再写数据库¶
缓存展示了不存在的新事实,危险。
方案二:先写数据库,再删缓存¶
这是常见 Cache-Aside。
仍有失败窗口:
可以接受短期旧数据时,使用:
- 缓存 TTL;
- 删除重试;
- CDC/事件修复;
- 版本校验。
如果业务不能接受旧数据,就不应把关键权限完全依赖弱一致缓存。
并发下的旧值回填¶
两个请求:
最终缓存又变旧。
改进思路:
- 较短 TTL;
- 带版本写缓存;
- 延迟双删;
- 串行化关键更新;
- 订阅数据库变更;
- 对真正关键判断直接查事实源。
没有一个通用方案,先确定旧数据可容忍多久。
数据库和 MQ 的双写问题¶
创建工单后发布事件:
失败窗口:
如果先发消息:
消费者看到了不存在的工单。
Transactional Outbox¶
在同一个数据库事务中:
BEGIN;
INSERT INTO tickets (...);
INSERT INTO outbox_events (
event_id,
aggregate_type,
aggregate_id,
event_type,
payload,
status
) VALUES (...);
COMMIT;
单独 Publisher:
数据库事务保证“工单和待发送事件同时存在或同时不存在”。
是否完全没有重复¶
没有。
Publisher 可能:
因此消费者仍需根据 event_id 幂等。
Outbox 解决“不丢变化”,幂等消费者解决“重复变化”。
消费者事务¶
收到 TicketCreated:
业务写入和 processed_event 最好在同一数据库事务中。
如果提交后 Ack 丢失,消息重投,event_id 告诉消费者已经处理过。
在线状态为什么又不同¶
设备在线状态是快速变化、允许短暂误差、可以从心跳重建的数据。
它可能采用:
- Redis 查询视图;
- Broker 连接事件;
- last_seen;
- 定时校准;
- 数据库只保存迁移历史。
不需要每个心跳都使用强事务写数据库。设计一致性等级要匹配业务:
一致性不是只有“强”和“弱”¶
需要明确:
- 读到旧值最多允许多久?
- 两个用户是否必须同时看到相同状态?
- 重复是否可以容忍?
- 丢失是否可以重建?
- 顺序是否只需同一设备内保证?
- 冲突由谁解决?
一个完整例子:设备离线后创建告警¶
1. 超时 Worker 判断设备离线
2. 数据库状态迁移 online → offline
3. 同一事务写 Outbox: DeviceOffline
4. Publisher 发布消息
5. 告警服务消费
6. 用 device_id + rule_id + offline_epoch 去重
7. 创建或更新告警
8. Ack
9. 通知服务异步发送
分别处理:
- Worker 重复执行:状态条件和版本控制;
- Outbox 重复发布:event_id;
- 告警重复消费:业务去重键;
- 通知失败:独立重试;
- Redis 在线集合漂移:状态事实校准。
深度自测¶
- Cache-Aside 为什么通常先写数据库再删缓存?
- 这样做为什么仍可能短暂不一致?
- Outbox 解决什么,没有解决什么?
- 消费者为什么要把业务写入和 processed_event 放在同一事务?
- 在线状态与支付为什么不能使用同一种一致性要求?
- Redis 是不是一定不能成为事实源?
参考答案要点
- 避免缓存先出现尚未提交的业务事实。
- 删除缓存可能失败,并发读可能把旧值重新写入。
- Outbox 避免数据库提交后事件丢失,但发布仍可能重复。
- 防止业务成功但去重记录失败,导致下次重复执行。
- 在线状态可重建且允许短暂延迟;支付副作用不可随意重复。
- 不是。关键是明确持久化、恢复、复制和业务语义,而不是按产品名绝对判断。
完成标准¶
- 能区分事实源、查询视图和变化事件
- 能画出 Cache-Aside 的并发失败窗口
- 能解释 Transactional Outbox
- 能设计幂等消费者事务
- 能为支付、在线状态和设备控制分别定义一致性要求