连载中 12/15

数据对账:Redis 与 DB 的最终一致

2026-09-08 · 289 阅读 · 0 评论 · 0 赞

为什么会对不上

预扣链路上有三个存储:Redis 记资格、MQ 传消息、DB 记订单。每个环节都设计了失败出口——落库失败回补、超时关单回补、消息重投——但每一个失败出口同时也是差异的来源:回补消息丢了就是少卖,重复回补就是超放,落库成功但标记没打上就是重复回补。机制的目的是把差异概率压到极低,对账的目的是证明它真的低——没有对账的一致性,都是自我感觉良好。

恒等式:对账的基本量

秒杀库存有一条恒等式:总库存 = Redis 剩余 + 已成订单占用 + 已回补 + 预扣未成单(在途)。对账就是活动各阶段核对这个等式:预热后核一遍(剩余=总量),开抢中周期核(差异=在途量级),结束后终核(差异应为零)。等式右边每一项都要有流水可查:扣减流水、订单记录、回补记录、在途快照——对账的前提是记账,流水不齐,账就没法对。

实时对账与离线对账

两套节奏分工明确:实时对账——开抢进行中抽样比对(如每分钟抽 1% 的订单核对 Redis 与 DB 状态),发现系统性偏差立刻告警,是活动中的保险丝;离线对账——活动结束后 T+1 全量核对流水与恒等式,产出差异清单进人工处理,是最终裁决。实时要快、离线要全,两者的成本预算完全不同,别混在一起做。

// 终态核对:恒等式逐项核对
long total       = activityRepo.totalStock(skuId);        // 总库存
long redisLeft   = redis.sumBuckets("stock:" + skuId);    // Redis 各桶剩余
long orderUsed   = orderRepo.countPaid(skuId);            // 已成订单
long refunded    = refundRepo.countRefunded(skuId);       // 已回补
long inFlight    = deductRepo.countPending(skuId);        // 预扣未成单

if (total != redisLeft + orderUsed + refunded + inFlight) {
    alarmAndDrill(skuId, diffDetail());   // 对不上:出差异明细进人工
}

差异修复的分界线

对账发现差异后,能不能自动修?分界线是有没有明确的事实源:订单库是事实源——Redis 显示扣了、DB 无订单且回补记录缺失,可以放心按"回补缺失"自动补 Redis;反过来,两边都没记录的差异(未知来源的库存偏差),自动修复等于用猜测覆盖事实,必须进人工。自动修复的每一条都要留下修复流水,下次对账可复核——修复本身也要可对账。

对账是通用能力

把这套能力沉淀成平台,收益远超秒杀本身:库存对账、资金对账、优惠券对账、积分对账——凡是跨存储的最终一致,都是恒等式加流水加修复。对账平台是资损防控的地基:很多资损事故的根因不是某个 bug,而是"没有发现差异的机制",差异存在了三天没人知道。下一篇解决另一个层面的问题:系统真的挂了怎么办——降级与兜底。

☕
503

10 年全栈工程师 · 503咖啡馆主理人

#数据对账#最终一致#恒等式#资损防控#差异修复

评论 (0)

热门推荐

连载中 11/22

主从搭建实操:从零配出一主两从

光讲原理不过瘾?手把手搭一主两从:my.cnf 六个参数、复制账号、GTID、CHANGE REPLICATION SOURCE TO、SHOW REPLICA STATUS 验收,附翻车排查清单。

#MySQL#主从复制#GTID#主从搭建#高可用
2026-05-07 · 10101 阅读 · 0 评论 · 0 赞
连载中 16/22

连接池:HikariCP 参数与连接风暴

连接池不是越大越好:8 核机器配 1000 连接反而更慢的数学原理,HikariCP 四个必调参数,maxLifetime 与 wait_timeout 的隐形陷阱。

#MySQL#连接池#HikariCP#maxLifetime#连接风暴
2026-05-10 · 9873 阅读 · 0 评论 · 0 赞
连载中 4/16

缓存穿透:恶意 ID 打穿 MySQL 的四道防线

请求的数据在缓存和数据库里都不存在时,缓存形同虚设。聊聊参数校验、空值缓存、布隆过滤器、限流熔断四道防线的原理与组合打法。

#Redis#缓存穿透#布隆过滤器#高可用
2026-05-16 · 9294 阅读 · 21 评论 · 287 赞