跳到主要内容

数据不一致排查

问题

线上发现缓存和数据库数据不一致、主从数据不同步等数据不一致问题,如何排查和修复?

答案

一、数据不一致的常见场景

二、缓存与数据库不一致

问题场景:更新数据库后删缓存失败,导致缓存是旧数据。

解决方案

// 方案 1:重试机制 + 消息队列保证最终一致
async function updateWithRetry(key: string, data: any) {
// 1. 先更新数据库
await db.update(data);

// 2. 删除缓存,失败则发到重试队列
try {
await redis.del(key);
} catch (err) {
// 发送到消息队列异步重试
await mq.publish('cache:invalidate', { key, retries: 0 });
}
}

// 消费者:异步重试删除缓存
async function handleCacheInvalidate(msg: { key: string; retries: number }) {
try {
await redis.del(msg.key);
} catch (err) {
if (msg.retries < 3) {
// 指数退避重试
await mq.publishWithDelay('cache:invalidate', {
key: msg.key,
retries: msg.retries + 1,
}, 1000 * Math.pow(2, msg.retries));
} else {
// 告警人工介入
await alert(`缓存删除失败: ${msg.key}`);
}
}
}
// 方案 2:Binlog 订阅(Canal)自动删缓存
// Canal 监听 MySQL binlog → 解析变更 → 删除对应缓存
// 优点:与业务代码解耦,不会遗漏

三、主从不一致

问题场景:写入主库后立即从从库读取,读到旧数据。

原因延迟范围解决方案
主从复制延迟毫秒~秒关键读走主库
大事务秒~分钟拆分大事务
从库负载高不定增加从库
// 关键场景强制读主库
async function getOrderAfterCreate(orderId: string) {
// 刚创建的订单,短时间内走主库查询
const order = await db.query(
'SELECT * FROM orders WHERE id = ?',
[orderId],
{ useMaster: true } // 强制主库
);
return order;
}

// 通用方案:写后设置标记,短时间内走主库
async function readAfterWrite(userId: string, queryFn: () => Promise<any>) {
const flag = await redis.get(`read_master:${userId}`);
if (flag) {
return queryFn({ useMaster: true });
}
return queryFn({ useMaster: false });
}

async function writeData(userId: string, updateFn: () => Promise<void>) {
await updateFn();
// 设置标记,3 秒内该用户读走主库
await redis.set(`read_master:${userId}`, '1', 'EX', 3);
}

四、并发写入导致不一致

解决方案:乐观锁

-- 使用 version 字段做乐观锁
UPDATE accounts
SET balance = balance - 30, version = version + 1
WHERE id = 1 AND version = 5;

-- 或直接用条件更新(CAS 风格)
UPDATE accounts
SET balance = balance - 30
WHERE id = 1 AND balance >= 30;

五、数据校验与修复

-- 对比两个库的数据差异
-- 方案 1:行数对比
SELECT COUNT(*) FROM db_old.orders;
SELECT COUNT(*) FROM db_new.orders;

-- 方案 2:聚合值对比
SELECT SUM(amount), MAX(updated_at) FROM db_old.orders;
SELECT SUM(amount), MAX(updated_at) FROM db_new.orders;

-- 方案 3:pt-table-checksum(行级校验)
-- 计算每个 chunk 的 checksum 并比对

常见面试问题

Q1: 缓存与数据库一致性最推荐哪种方案?

答案

场景推荐方案
一般业务Cache Aside + 重试
金融级一致性不用缓存,直接查 DB
大流量 + 最终一致Binlog 订阅删缓存(Canal)

大多数场景采用 Cache Aside + 消息队列重试删除即可满足要求。

Q2: 如何快速判断数据不一致的根因?

答案

  1. 看时间差:不一致数据是最新的还是延迟的?延迟 = 主从延迟或缓存过期
  2. 看操作日志:binlog / audit log 确认写入是否成功
  3. 看并发:是否有多个写入同一数据?→ 并发冲突
  4. 看缓存:直接查 Redis 和 DB 对比 → 缓存不一致
  5. 看从库SHOW SLAVE STATUS 查延迟 → 主从不一致

Q3: 分布式场景如何保证跨库数据一致性?

答案

方案一致性性能复杂度
分布式事务(Seata)强一致
本地消息表最终一致
TCC强一致
Saga最终一致

大部分业务场景选择最终一致性,使用本地消息表或 Saga 模式。

相关链接