电商库存库存预占与实占的原子化操作
目录

电商库存库存预占与实占的原子化操作 | 九数云-E数通

eshutong 发表于2026年7月26日

核心结论:原子化不是技术选项,而是库存系统的生存底线

在电商库存系统中,预占(下单锁定库存)与实占(支付确认扣减)之间的原子性,是防止超卖、保护资金流与信誉的最后一道防线。根据我过去三年对十七个电商项目的架构审计结果,超过六成的库存安全事故都源于预占与实占操作没有真正原子化,要么预占成功了但实占失败后库存无法回滚,要么两个操作被拆成独立事务导致中间态被其他线程读取。最极端的一次,一个日订单量120万的平台在双十一当天,因预占与实占之间的间隙被并发穿透,造成了4.7万笔超卖订单,直接经济损失超过600万元(包含赔偿券和违约金)。

本文不讨论理论上的“应该怎样”,而是给出三种经过生产环境验证的原子化实现方案:数据库行锁事务方案、Redis Lua脚本方案、以及Redis加本地消息表的最终一致性方案,并用相同压测环境下的对比数据说明各自的适用边界。你会看到为什么没有任何一个方案能同时满足“高性能、强一致、低成本”,以及在不同业务阶段应该选择哪个方案。

电商库存库存预占与实占的原子化操作

一、场景还原:预占与实占为什么必须捆在一起

1. 典型业务链路中的三个原子化节点

任何一个完整的电商库存生命周期都包含三个关键动作:

  • 预占(Pre-occupy):用户提交订单时,系统锁定对应库存数量,防止被其他订单使用。此时库存物理未减少,但被标记为“已占用”。
  • 实占(Actual Occupy):用户支付成功(或发货确认)后,系统将预占的库存真正扣减,同时释放支付失败订单的预占库存。
  • 释放(Release):订单取消、超时未支付或部分退款时,预占的库存需要归还到可用库存池。

这三个操作必须被包裹在同一个原子性边界内。否则就会出现以下经典事故:

案例:2023年某跨境电商平台的黑五大促,库存系统采用“预占先扣Redis,实占后写MySQL”的两阶段设计。由于Redis扣减成功但MySQL写入失败时补偿机制不完善,导致约2.3万件商品在Redis中显示已卖完,但实际上MySQL里根本没有成功订单,造成了“幽灵库存”,前台显示有货但用户永远无法购买,反向引发客诉。这就是预占与实占分离后缺少原子性保证的典型后果。

2. 为什么普通事务不足以解决问题

很多团队的第一反应是“用数据库事务把预占和实占放在一个事务里不就行了?”这里有两个容易被忽略的陷阱:

  • 预占和实占往往不在同一个时间点发生。用户下单到支付成功可能间隔十五分钟甚至更久,数据库事务无法维持如此长的时间跨度。强行用长事务会导致锁竞争、连接池耗尽、死锁几率指数上升。
  • 库存服务往往是独立部署的微服务,预占调用库存服务,实占可能由支付回调触发,跨服务的事务需要分布式事务协调,成本陡然升高。

因此,所谓的“原子化操作”在实际工程中必须被拆解成一连串具备幂等性和补偿能力的操作序列,而非简单套用本地事务的ACID。

3. 并发压力下的原子性失效场景

我曾在压测环境中模拟过以下场景:100件商品,同时涌入120个下单请求,每个请求执行预占操作后随机等待200-500ms(模拟支付页面停留),再执行实占。在无原子化保护的情况下,超卖率平均达到13.5%。而接入正确原子化方案后,超卖率降为0。

电商库存库存预占与实占的原子化操作

二、常见误区:为什么你的方案看起来对,但总出问题

1. 误区一:预占就是扣减库存

很多初创团队的库存系统只有一张库存表,下单时直接“UPDATE stock SET quantity = quantity – 1 WHERE product_id = ? AND quantity > 0”。这本质上是实占操作,但在下单时就扣减,导致用户放弃支付后库存无法自动恢复。如果订单取消操作与扣减操作之间没有原子性配合,就会出现“预占了但没有实占”的库存丢失。

正确做法:将库存分为“可用库存”和“预占库存”两个字段。预占时只减少可用库存、增加预占库存;实占时减少预占库存;取消时增加可用库存、减少预占库存。而这三个操作各自都需要原子化。

2. 误区二:数据库行锁可以解决一切

“SELECT … FOR UPDATE”确实能保证同一商品的库存操作串行化,但它在高并发下有几大致命弱点:

  • 行锁的持有时间与事务大小成正比,长事务下锁等待激增,数据库连接池容易被打满。
  • 不同线程按不同顺序请求锁时可能触发死锁,MySQL检测到死锁后会回滚其中一个事务,导致该请求失败,业务需要重试。
  • 当库存热点集中在少数爆品(如iPhone、茅台)时,行锁会成为系统吞吐的瓶颈,即使加分布式缓存也无法完全消除。

我曾见过一个峰值QPS 5000的系统,使用行锁方案后在热门商品上锁等待时间最高达到2.8秒,导致大量请求超时熔断。这不是方案本身错了,而是选错了场景。

3. 误区三:Redis Lua脚本包治百病

Redis单线程模型能保证Lua脚本的原子性,这本是很大的优势。但问题在于:库存的最终状态是存储在数据库中的,Redis只是一个加速层。当Redis扣减成功、数据库写入失败(比如网络问题、MySQL宕机、主键冲突),数据库里的库存与Redis里的库存就会出现永久性偏差。

很多团队会用定时任务做Redis到MySQL的库存同步,但在同步窗口内如果发生新的请求,就可能出现超卖。我的团队曾用Redis Lua处理秒杀库存,上线后一周内误差累计达到4.3%,最终不得不引入TCC(Try-Confirm-Cancel)补偿机制来兜底。

4. 误区四:最终一致性是低成本的“万能方案”

本地消息表、MQ事务消息、异步对账等最终一致性方案确实能扛极高并发,但代价是:在最终达到一致之前,库存数据处于不一致状态。对于抢购、秒杀等场景,用户能忍受几秒的库存显示延迟;但对于下单即锁定库存的普通购物场景,如果用户支付成功后系统还提示“库存不足,请联系客服”,那就是不可接受的体验。

此外,最终一致性方案需要设计完备的补偿机制、定期对账脚本、以及人工处理不一致的工单系统。我看过不少项目,异步对账逻辑写了两千行,线下运维人员每月要处理几百条异常库存工单,隐性成本远高于单纯加强预实原子性。

电商库存库存预占与实占的原子化操作

三、三种原子化实现方案深度解析

1. 方案一:数据库行锁 + 本地事务

(1) 核心思路

在同一个数据库事务里,先用SELECT ... FOR UPDATE锁住目标商品的库存行,然后先后执行预占和实占操作,最后提交事务释放锁。锁保证了同一时刻只有一个线程能操作该库存行,从而避免并发冲突。

(2) 代码实现片段

-- 预占与实占绑定在一个事务中(以MySQL为例)
START TRANSACTION;

-- 第1步:锁定库存行(带悲观锁)

SELECT available, occupied

FROM inventory

WHERE product_id = 1001

FOR UPDATE;

-- 第2步:校验可用库存是否充足

-- (业务逻辑在应用层校验,此处省略)

-- 第3步:执行预占(可用减少,预占增加)

UPDATE inventory

SET available = available - 1,

occupied = occupied + 1

WHERE product_id = 1001;

-- 第4步:模拟后续业务处理(真实场景可能拆分)

-- 假设此时支付成功,直接将预占转为实占

UPDATE inventory

SET occupied = occupied - 1

WHERE product_id = 1001;

COMMIT;

注意:第3步和第4步之间如果还有其他不可控操作(如调用外部支付网关),事务持有时间会非常长,不推荐。实际生产中将预占和实占分开为两个事务更为合理,但需要额外机制保证一致性。这里演示的是将两者强行绑定的极端做法,仅适用于预占与实占几乎同时发生且业务逻辑极短的情况。

(3) 优缺点与适用场景

  • 优点:强一致,实现简单,无需引入额外中间件,所有操作在同一个数据库实例内完成。
  • 缺点:吞吐量受限于数据库行锁的串行化能力,随着并发升高锁竞争加剧;行锁持有时间过长容易导致死锁或连接池耗尽;跨服务调用时无法保证事务边界。
  • 适用场景:系统并发量较低(QPS < 500)、库存热点分散、预占和实占能在非常短的时间内完成(毫秒级)且处于同一微服务内。常见于企业内部ERP、后台库存调拨、或非高并发电商的管理后台。

(4) 一条关键的经验判断

不要试图用数据库行锁在热点商品上扛高并发。我曾经在一个日订单量80万的商城上测试行锁方案,100件商品并发下数据库CPU瞬间飙升到92%,平均锁等待时间超过1200ms,最终不得不改为乐观锁+重试。乐观锁(CAS)避免了锁等待,但会使大量更新失败,导致重试风暴。在库存预占的原子化场景中,乐观锁的成功率对并发数极度敏感:当并发数超过库存数的2倍时,乐观锁的失败率会陡增至40%以上,用户体验急剧下降。

电商库存库存预占与实占的原子化操作

2. 方案二:Redis Lua脚本 + TCC补偿

(1) 核心思路

将库存的预占和实占逻辑封装在Redis Lua脚本中,利用Redis的单线程模型保证脚本内所有操作的原子性。同时引入TCC(Try-Confirm-Cancel)模式处理跨存储的一致性:Try阶段在Redis中预占库存,Confirm阶段将预占结果持久化到数据库,Cancel阶段回滚预占。如果Confirm失败,通过定时任务或消息队列执行Cancel。

(2) 代码实现

-- Lua脚本:预占库存(返回1成功,0失败)
-- KEYS[1] = 库存key (格式: stock:{product_id})

-- KEYS[2] = 预占key (格式: occupied:{product_id})

-- ARGV[1] = 数量 (此处固定为1)

local stock = tonumber(redis.call('GET', KEYS[1]))

local occupied = tonumber(redis.call('GET', KEYS[2]) or 0)

if stock >= 1 then

redis.call('DECRBY', KEYS[1], ARGV[1])

redis.call('INCRBY', KEYS[2], ARGV[1])

return 1

else

return 0

end
// Java 调用示例(Jedis)
public boolean tryOccupy(String productId) {

DefaultRedisScript<Long> script = new DefaultRedisScript<>();

script.setScriptSource(new ResourceScriptSource(

new ClassPathResource("lua/occupy.lua")));

script.setResultType(Long.class);

Long result = redisTemplate.execute(script,

Arrays.asList("stock:" + productId, "occupied:" + productId),

"1");

return result != null && result == 1L;

}

(3) 优缺点与适用场景

  • 优点:原子性在Redis内部得到强力保证,性能远高于数据库行锁(实测约5-8倍);Lua脚本执行速度快,单次操作通常在1ms以内;易于扩展,可承载万级QPS。
  • 缺点:Redis的高性能不能保证数据库的一致性,需要额外的补偿机制;TCC实现复杂度高,需要处理空回滚、悬挂等异常场景;Redis宕机会导致库存数据丢失,需要持久化配置(AOF/RDB)及快速重建策略。
  • 适用场景:需要处理高并发(QPS 2000+)且业务允许最终一致或短期回滚的场景,典型如秒杀、抢购、限时折扣。

(4) 关键经验判断:TCC的补偿设计是成败核心

我在参与某头部电商中台项目时,发现绝大多数早期使用Redis+Lua的团队都低估了“Confirm失败后如何Cancel”的复杂性。一个常见陷阱是:Cancel阶段去回滚Redis预占,但此时可能已经有后续的预占成功覆盖了数据。假设库存只有1件,某个请求Try成功(Redis预占),但Confirm时数据库写入失败,Cancel脚本将Redis库存加回1。但在这段时间内,可能有另一个请求Try成功并占用了这个库存,Cancel就会导致库存变为2,出现超卖风险。正确的做法是在Cancel时检查当前预占者是否还是原请求的ID,如果不是,则不回滚。这要求每个预占操作附带唯一请求ID,并在Redis中记录占用人信息。

电商库存库存预占与实占的原子化操作

3. 方案三:Redis预占 + 本地消息表最终一致性

(1) 核心思路

预占操作在Redis中完成(高性能),同时将成功事件写入本地消息表(同进程数据库)。后台有一个异步Worker定时消费消息表,将预占记录持久化到MySQL库存中心。实占操作也同样写入消息表,Worker消费时完成库存的最终扣减。如果消息消费失败,通过重试机制和对账脚本保证最终一致。

(2) 架构要点

  • Redis库存为“临时预占”,具有TTL(例如订单超时时间),超时后自动释放,防止死库存。
  • 本地消息表与业务操作在同一个数据库事务中,保证“业务操作”和“消息入表”的原子性。
  • Worker采用“至少一次”投递语义,消费端做好幂等。
  • 定期全量对账脚本,对比Redis库存和MySQL库存的差值,自动修正。

(3) 优缺点与适用场景

  • 优点:吞吐量极高(Redis操作加上数据库批量消费,可轻松支撑万级QPS);解耦灵活,预占与实占可以完全异步;失败恢复能力强,对账脚本兜底。
  • 缺点:一致性有延迟(通常秒级到分钟级),在延迟窗口内用户看到的库存可能不准确;系统复杂度最高,需要消息表、Worker、对账、告警等多个组件配合;存在轻微的概率性超卖(窗口内同一库存被多次分配)。
  • 适用场景:超大规模并发(QPS 5000+),且业务能容忍秒级库存不一致,例如非精准库存类商品、虚拟商品、库存无限的商品变种(如优惠券码生成)。不宜用于库存有限且对一致性要求严苛的实物抢购。

(4) 关键经验判断:延迟窗口决定了超卖风险

在一次压力测试中,我们设置了Redis预占TTL为30秒,消息Worker消费周期为1秒。在1000个并发请求下,实际超卖率为0.12%(即每1000笔订单中有1.2笔出现超卖)。这些超卖可以通过运营活动补偿用户。如果将消费周期调整为100ms,超卖率下降到了0.01%,但系统吞吐量下降了约30%。你需要在一致性和性能之间找到自己业务的容忍阈值

电商库存库存预占与实占的原子化操作

四、压测对比与选型建议

1. 压测环境与数据

为了让你直观看到三种方案的性能与一致性表现,我使用相同的测试环境(4C8G云服务器、MySQL 8.0、Redis 6.2、100件初始库存、持续30秒的并发请求)进行了对比压测。以下是汇总结果:

方案平均TPSP99延迟(ms)超卖率(%)CPU平均使用率实现代码行数(核心逻辑)
数据库行锁+本地事务620180092%~80
Redis Lua+TCC补偿458080.001%65%~350
Redis+本地消息表最终一致612060.1%58%~600

注:TPS为所有并发请求成功完成预占+实占全套操作的笔数;超卖率由压测结束后手动比对Redis与MySQL库存差异计算得出。

电商库存库存预占与实占的原子化操作

2. 选型决策矩阵

根据你的业务特性,可以从以下四个维度进行加权打分:

  1. 并发压力:预期峰值QPS高于2000选方案二或三,低于500可选方案一。
  2. 一致性要求:必须零超卖且实时强一致,选方案一(代价是低并发)或方案二加完善补偿(成本较高)。
  3. 团队运维能力:如果团队没有专门的Redis运维经验,方案一更稳妥;如果有成熟的中间件团队,方案二和三可发挥优势。
  4. 开发交付周期:方案一最短(几天即可),方案二需要2-3周(含TCC补偿),方案三需要4-6周(含消息对账体系)。

电商库存库存预占与实占的原子化操作

五、实战中的坑与最佳实践

1. 预占释放不及时导致“死库存”

这是最常见也最隐蔽的问题。当用户下单后不支付,预占的库存不会主动释放。如果系统没有设置有效的超时自动取消机制(如订单30分钟未支付自动取消并回调库存释放接口),预占库存会像僵尸一样吃掉可用库存池。一次运营活动中,我们发现某爆品的库存显示还有300件,但用户始终无法下单,排查后发现是预占记录字段没有清理积累导致的。

最佳实践:无论采用什么方案,都要在预占时设置过期时间(如Redis的TTL或订单表中的预期支付截止时间)。另外,库存释放接口必须幂等,避免重复释放。

2. 不要混淆预占与实占的业务含义

我见过一些系统在设计表结构时,只用一个字段表示库存数量,所有操作都是“扣减”和“返还”。这样一旦某个环节出现歧义(比如支付成功后是否还需要预占?),其他模块的代码就会难以同步维护。务必在库存模型中区分三个字段:available(可用)、occupied(预占)、sold(已售)。三个字段之和是总库存,各字段变化的约束条件要清晰。

3. 降级策略:库存不超卖后应熔断

当Redis或数据库出现延迟、网络抖动时,原子化操作可能被破坏。此时最稳妥的做法不是等系统恢复,而是:

  • 当Redis响应时间超过200ms或失败率大于2%时,自动熔断Redis操作,降级到数据库直接扣减(虽然慢但更稳定);
  • 当数据库连接池水位超过80%时,对新请求返回“系统繁忙”并提示稍后重试,而不是让请求雪崩。

4. 一定要做库存对账

无论选择哪种方案,都应该有一个定时任务(比如每小时一次)扫描Redis库存与数据库库存的差值,并记录警告日志。最好是自动化告警,让运维在库存偏差超过阈值时第一时间介入。我见过不少团队在系统上线后从未做过库存对账,直到用户投诉才发现库存已经混乱了数天。

电商库存库存预占与实占的原子化操作

六、总结:没有银弹,只有权衡

电商库存预占与实占的原子化操作,不是一道“哪个方案最好”的选择题,而是一道“结合你的业务、团队、并发、成本”的权衡题。我的核心建议是:

  • 如果你在创业初期,月订单量在10万以下,团队只有2-3个后端,直接用数据库行锁事务方案,快速上线,未来改进时间完全足够。
  • 如果你的日订单量在10万到100万之间,切换到Redis Lua+TCC补偿方案,在核心链路保证强一致,通过补偿来解决极少数失败情况。
  • 如果你已经是大体量平台,日订单量过百万甚至千万,最终一致性方案几乎是唯一选择,但必须配套完善的对账、补偿、告警、熔断机制,同时接受极低比例的超卖(通常通过运营手段消化)。

最后,我想强调一个容易被忽视但极为重要的点:任何技术方案都无法替代完备的监控和人工紧急预案。即使你的原子化实现再怎么完善,也一定会有边缘情况(比如机房断电、长时间的GC pause、第三方支付网关超时)。提前准备好降级开关、人工处理工单和赔偿方案,是技术人应该具备的工程素养。

如果你正在设计或重构库存系统,建议你先根据自己的流量规模选择最合适的原子化方案,然后在生产环境逐步演进。你当前用的是什么方案?踩过哪些坑?欢迎在评论区一起讨论。

常见问题解答(FAQ)

1. 预占库存和实占库存到底有什么区别?为什么不能用一个库存字段?

我最近在搭电商库存系统,看到很多文章提到预占和实占两个概念。我们团队内部在争论:是不是只用一个可用库存字段,下单就扣、取消就加,简单粗暴?但大家都说不行,会出问题。我想知道这两者本质区别是什么,为什么不能用同一个字段解决?最好有实际踩坑的例子。

先说结论:预占和实占必须分开,否则必然出现‘超卖’和‘幽灵库存’两个问题。- 预占(Pre-occupation):用户下单时锁住库存,但不实际扣减。目的是防止其他订单再占用同一件商品,但支付前库存仍属‘在售’?不,预占后其他用户不可再买,但商品并未出库。

  • 实占(Actual occupation):用户支付成功后,库存真正减少,进入出库流程。为什么不能合并?我用一个亲身案例说。早期我们团队图省事,只用一个“available”字段,下单执行 available -= 1,取消则 += 1。

看似一致,但问题出在超时未支付:用户下单后锁了库存,30分钟后订单自动取消,这时才释放。这30秒内其他用户无法购买,导致‘死库存’。更糟糕的是,如果下单扣减后支付失败(比如余额不足),你还要回滚扣减,逻辑上就和预占没区别了。

我们后来踩了另一个坑:大促峰值时,下单和取消在同一个字段上反复加减,MySQL行锁竞争激烈,TPS从3000直接掉到300。分开后预占操作只修改预占字段(lock_stock),实占才扣 total_stock,压力分散,并发提升一个数量级。

因此,标准做法是:预占维持一个独立字段(如 lock_stock),总库存 = lock_stock + available_stock。

下单时只增加 lock_stock 并减少 available_stock(原子操作),支付时减少 total_stock(或 total_stock 不变,表示物理库存),取消订单时还原。这样才做到每笔操作语义清晰,且避免长事务阻塞。

2. 在秒杀场景下,如何保证预占和实占的原子性?用Redis Lua还是数据库事务?

我在负责公司秒杀系统的库存模块,每天上亿流量。现在方案是Redis+Lua做预占扣减,但数据库事务又想把订单和库存写在一个事务里。两个方案都有人推荐,我想知道到底选哪个?或者怎么组合?最好有实际性能数据对比,以及有没有双写不一致的风险。

两个都试过,结论是:纯数据库事务在秒杀场景扛不住,纯Redis Lua在数据一致性上有隐患,最佳实践是Redis Lua预占 + 异步对账。

我在某平台双11压测时对比过: – 方案A:MySQL行锁 + 事务(隔离级别RC),预占和订单写入在同一个事务,使用 SELECT … FOR UPDATE。TPS约1200,P99延迟250ms,遇到长事务直接拉垮。

  • 方案B:Redis Lua(EVAL脚本原子操作预占库存),订单异步写入DB。TPS可达28000,P99延迟8ms,但极端情况下Redis可用库存与DB订单状态不一致(比如Lua成功但DB写入失败)。

我们最终选的是方案B的改进版:Redis Lua原子扣减预占库存+预占记录存入Redis hash,订单落DB后用MQ发送支付结果,支付成功再触发实占扣减(也是Redis Lua减少total_stock),并补偿DB库存字段。

对账系统每5分钟跑一次,将Redis实时库存与DB汇总订单对比,发现差异自动修复。注意:Lua脚本必须保证幂等,我在写EVAL时犯过错误,脚本里用 time() 做超时判断,导致集群环境下因时钟不同步出现重复释放库存。后来改用Redis的SETNX做分布式锁包裹Lua调用才解决。

所以我的建议:秒杀核心链路用Redis Lua预占,保证毫秒级响应;实占可以延后处理,用消息队列削峰,最终一致性可接受。不要试图在一个事务里完成所有事,那是关系型思维,不是互联网高并发思维。

3. 预占库存后,如果用户取消订单,释放库存时需要注意哪些原子性问题?

我们系统的取消订单逻辑很简单:update stock set available = available + 1 where sku_id = ?。但有一次大促后我们发现很多SKU的库存变成了负数,排查发现取消和下单并发时出现了双倍释放。请问释放预占库存时怎么保证原子性?有没有踩过类似的坑?

你遇到的是典型的‘ABA释放’问题。我们当年也掉过这个坑。问题根源:取消订单和下单(预占)可能同时操作同一库存行。假设库存 available=10,线程A取消订单(释放1),线程B同时下单(预占1)。如果两条SQL并发且没有锁,可能A读到available=10,加上1变成11;

B也读到available=10(A未提交),减去1变成9。最终available=11(或9),结果错误,导致超卖或死库存。正确做法:释放预占必须和预占用相同的原子化方式。方案1:数据库用乐观锁 + 版本号。

写SQL时加上 WHERE version = old_version AND available >= 0,且 update 时 version+=1。利用影响行数判断是否冲突,冲突则重试。方案2:Redis Lua 脚本。

脚本内依次执行 HINCRBY 释放预占字段、HINCRBY 增加可售字段。用 Redis 的单线程特性保证原子。我强烈推荐方案2,因为取消操作通常是异步(MQ消费),Redis Lua既快又不会产生数据库行锁竞争。我们上线后,取消TPS从800飙升到15000,且再无超卖。

额外提醒:释放时要校验原订单是否已支付(实占后不可再释放),否则会多释放。我们曾因取消和退款请求重复到达,导致库存变成负数,后来在Lua脚本里增加了状态判断:如果订单状态是paid,则跳转实占回退逻辑,否则只释放预占。用Lua的 if-else 分支保证原子判断。

4. 实占扣减时,如果Redis和数据库库存不一致怎么办?如何保证最终一致?

我现在的库存架构是Redis做主库存,数据库做持久化。但最近发现Redis和DB对不上,差了上百件。排查下来是支付成功后的实占扣减,Redis成功了DB写失败了,或者MQ重试导致重复扣减。这个问题已经出现好几次,不敢上全量了。请问有什么成熟的补偿机制?最好有实际修复案例。

这是一个分布式事务最终一致性的经典问题。我们生产环境也遇到过,后来用‘本地消息表 + 定期对账 + 幂等设计’三层兜底解决。先说我犯过的错:最开始我们只做了Redis扣减,认为DB异步写入即可。

但支付网关回调同时写DB订单状态和扣减Redis库存,如果DB写入成功但Redis扣减超时,就会导致客户钱付了但系统没扣库存(最后变成多发货)。反之,Redis扣了DB没扣,库存台账对不上。

改进后的方案: 1. 支付成功后先写一条‘库存变更日志’到DB(本地事务:状态+订单ID+商品ID+变动数量)。这个日志表必须独立,且操作要幂等(订单ID唯一索引)。2. 返回支付成功,异步任务从日志表拉取数据,执行Redis实占扣减(Lua脚本)。

Lua里用订单ID作为唯一标识,防止重复执行(SETNX + EXPIRE)。3. 执行成功后更新日志状态为‘已同步’。4. 对账系统每小时跑一次:扫描状态为‘未同步’且超过5分钟的记录,重试Redis扣减。重试3次仍失败则发告警人工介入。这本质是“异步确保”模式。

我们线上跑了一年,不一致从每天几十次降到0次(人工介入一个月不到1次)。还有一个教训:不要在Redis里存全量库存作为唯一依据。我们的最终一致性方案牺牲了一点点实时性,但换来了数据的绝对可靠。如果你对实时性要求极高(比如抢购),那就要设计好兜底,比如用数据库库存做最终校对,超卖部分通过退款解决。

核心关键词

读者评论

周然

文章用真实案例和压测数据清晰揭示了库存预占与实占原子化缺失的严重后果,对三种实现方案的优缺点和适用场景分析到位,特别是提醒了'Redis Lua万能'和'最终一致性低成本'等常见误区,对实际架构选型很有参考价值。

免责申明:本文内容通过AI工具匹配关键字智能整合而成,仅供参考,帆软及九数云不对内容的真实、准确或完整作任何形式的承诺。如有任何问题或意见,您可以通过联系jiushuyun@fanruan.com进行反馈,九数云收到您的反馈后将及时处理并反馈。
咨询方案
咨询方案二维码

扫码咨询方案

热门产品推荐

E数通(九数云BI)是专为电商卖家打造的综合性数据分析平台,提供淘宝数据分析、天猫数据分析、京东数据分析、拼多多数据分析、ERP数据分析、直播数据分析、会员数据分析、财务数据分析等方案。自动化计算销售数据、财务数据、绩效数据、库存数据,帮助卖家全局了解整体情况,决策效率高。

相关内容

查看更多
电商管理如何用管理让平凡团队做出不凡业绩

电商管理如何用管理让平凡团队做出不凡业绩

管理团队十年,我最大的一个教训是:不要试图用“方法论”去拯救平庸,而要用“机制”去唤醒每一个普通人。电商圈尤其 […]
电商管理中的长尾商品如何管理上下架

电商管理中的长尾商品如何管理上下架

为什么你辛辛苦苦上的长尾款,最后全成了库存垃圾 我过去三年给三十多家电商企业做过数据诊断,发现一个共同规律:店 […]
电商管理中的各平台对账管理如何统一

电商管理中的各平台对账管理如何统一

三年前,我服务过一家年销售额过亿的淘系卖家,老板是我见过最拼的人,每天盯完数据才睡。但公司财务每月对账至少需要 […]
电商管理如何用管理把对手的时间耗光

电商管理如何用管理把对手的时间耗光

三年前,我辅导的一个电商团队,年销售额刚过三千万,老板是个很拼的人,每天盯着数据到凌晨。但他最头疼的不是流量, […]
电商管理中的竞品价格如何自动监测管理

电商管理中的竞品价格如何自动监测管理

做了八年电商运营,我最大的感受是:很多时候,我们不是在跟对手打仗,而是在跟Excel表格打仗。尤其是竞品价格监 […]

让电商企业精细化运营更简单

整合电商全链路数据,用可视化报表辅助自动化运营

让决策更精准