开发者社区 > 博文 > WMS6.0爆品库存定位数据库并发更新行锁治理实践
分享
  • 打开微信扫码分享

  • 点击前往QQ分享

  • 点击前往微博分享

  • 点击复制链接

WMS6.0爆品库存定位数据库并发更新行锁治理实践

  • jd****
  • 2025-06-27
  • IP归属:北京
  • 55浏览

    导语

    本文在爆品库存定位场景下,分析了数据库热行并发更新,行锁竞争激烈的现象、原因、风险,在此情况下,制定了专项治理优化方案和措施,经过治理开发和压测验证,功能在多个集群上线推广,逐步切量,有效降低大促风险,保障系统稳定性。

    典型地,在订单定位时,经过库存排序规则和过滤规则,爆品SKU集中到相同的库存行竞争预占,MySQL数据库行锁抢占激烈,行锁等待时间长,MySQL 5.7 数据库默认开启开启AHI(自适应哈希索引),在爆品集中高并发定位时,容易诱发数据库连接泄露,造成数据崩溃。在这种场景下,本文按集群、环境、场景,拆分不同的消息队列分发流量,进行限流削峰;拆分事务粒度,大事务拆分为多个小事务,降低死锁概率,减少锁的时长,通过整合器保证整体数据的一致性。经过多项措施深度治理,多场景压测验证,该功能上线切量推广,爆品仓和典型的大单量仓切到新模式,在618大促期间有力保障了系统稳定性。


    背景

    在WMS系统中,库存是一个类似于基础服务的子系统,为入库、出库、在库提供库存服务。




    其中包含库存定位服务、库存转移服务、库存预占服务、库存批次服务、库存库容服务、库存补货服务、库存变更服务、库存异常处理服务、序列号库存服务、库存查询服务等。

    库存定位服务是其中业务比较复杂且重要的服务,具备以下特点:

    • 业务复杂,组合场景多。
    • 瞬时流量大,高并发,基本上是订单下发即自动定位。
    • 爆品订单很常见,按照周转规则排序后,同SKU优先争抢相同储位库存行,数据库行锁竞争严重。


    现状痛点

    在上图示例中,SKUA、SKUB、SKUC分布在多个储位上,而且储位中的库存数量不尽相同。另外有7个订单中分别包含SKUA、SKUB、SKUC,订单中需要SKU的数量也有所不同,代表不同的用户下了多个单子,其中包含相同的商品,也包含不同的商品。


    在这个示例中,我们也可以看到,同一个订单所需要的SKU可能分布在多个储位上,多个订单也会抢占同一个储位上的SKU。


    正常情况下,客单出库,会按照批次先进先出的规则进行有限定位,即相同SKU优先定位保质期更早的库存。假定储位1中,SKUA的批次更优先,那么需要SKUA的订单1、订单2、订单3、订单4、订单5、订单7,都会并发抢占储位上的SKUA库存,这些订单的抢占量可能会超过储位1中SKUA的库存量,会使得真正预占落库时,更新行数为0,预占失败。


    这种情况是因为并发定位场景下,从单个订单视角来看,储位1中SKUA的库存量是充足的,可以满足相应订单的需求量,但实际情况却是,预占落库前,预占储位库存预判满足可用量,但真正落库时,前面一部分真正满足订单需求量的订单预占成功,而后面的订单预占更新失败。


    这种爆品的订单,很多订单都包含相同的SKU,这些订单正常都按先进先出的定位规则,都会优先抢占保质期更早的库存行,导致数据行热行竞争抢占激烈,数据库行锁的竞争非常严重。定位策略执行时多个并发请求都会预判同一个储位库存行的可用库存量充足,但实际数据库更新时,却只有可用量确实满足的前部分订单可以预占成功,后面的订单只能重新执行定位策略,重新预占库存。


    风险

    在典型的导单仓或走自动出库流程的仓(比如白象仓、韩束等云仓爆品仓),有两个突出的特征:

    • 自动接口调用流速大。
    • 爆品集中,数据库热行更新竞争激烈。


    根据过往经验,当数据库行锁竞争激烈时,数据库的AHI(自适应哈希索引)会加剧锁争用,诱发数据库崩溃。





    请求打到库存上,由于爆品集中在库存热行上,数据库的行锁竞争严重 -> 行锁等待时间长 ->SQL执行时间变长,形成大量慢SQL -> 应用层面感知到connection is close异常 -> 应用创建新连接 -> 数据库活跃连接数飙升 -> 内存爆满,链接泄漏 -> ... -> 数据库崩溃。


    治理手段

    数据库账号隔离

    申请单独的数据库账密,定位专用,单独服务分组,单独别名,配置单独的数据库账密,机器配置为8C,JSF线程池配置为400。


    KA集群,6 * 10 * 8 = 480 连接数,该账号连接数上限设置为600

    整体线程池大小为 400 * 6 = 2400

    假定TP99为1000ms,则分钟级最大吞吐量为2400*60=144000=14.4W,可满足吞吐量需要


    JX集群,4 * 10 * 8 = 320 连接数,该账号连接数上限设置为600

    整体线程池大小为 320 * 6 = 1920

    假定TP99为1000ms,则分钟级最大吞吐量为1920*60=115200=11.52W,可满足吞吐量需要。


    定位明细维度限流

    限流阈值,结合压测情况进行评估阈值,确保系统稳定性,同时也要满足服务吞吐量要求。

    库存定位返回特定限流结果码,出库识别该限流码,定位触发限流后,出库转慢速队列,慢速队列设置相应的限流。


    请求合并

    出库合并请求,将订单指定相同SKU、等级、批次、包装代码的一定单量请求合并一起,请求数量累加,这样多笔请求合并成一笔请求,降低调用次数和热行更新的并发度,降低明细级别限流的概率。


    利用消息队列削峰,相同明细串行化执行

    以大化小,同一个请求,多个SKU,多明细拆分,大事务拆成小事务,减少死锁竞争,保证整体的一致性行,对结果进行集齐合并ACK回执给调用方。


    流水表去唯一索引,强依赖Redis进行并发防重

    流水表中的唯一索引,每次插入时需要进行数据唯一性约束校验,一定程度上增大了死锁的概率。

    去唯一索引,使用Redis作为并发防重的平替方案,必然强依赖于Redis,若Redis不可用,则无法做到并发防重,会引起重复数据的产生。

    有利有弊,目前评估暂不采取此方式。


    使用NoSQL进行库存秒杀预占

    将MySQL中的数据加载到Redis中,进行秒杀级并发预占定位,周期性刷盘至MySQL,需保持两者的数据一致性,避免超卖。

    WMS与交易库存不同,WMS的库存维度更细(业务主键:仓+SKU+等级+批次+包装代码+储位+一级容器+二级容器),相同件数下,WMS库存行更多,除了维度不同外,既管理了储位库存,还管理容器库存。

    WMS库存作业的场景较多,变化点较多:库存定位、预占、释放、增加、扣减、转移、盘点锁定、冻结、解冻等。

    鉴于WMS库存的维度更细、作业场景更多、业务逻辑更复杂,数据库与Redis一致性更难保障。暂未采取此方式。


    方案细化

    服务交互



    以出库定位限流治理为例,出库定位与库存以MQ方式异步交互,出库将订单请求通过MQ消息发给库存,库存整体执行定位策略,经过库存定位策略后,确定待更新的库存明细。

    按库存明细拆分,将库存表的更新以事件方式发送到顺序消息队列,每个明细事件是个基本子单元,里面的动作包含:

    • 库存更新。
    • 库存明细操作写入流水。

    调度器轮询尚未闭环的请求,未闭环指的是定位结果尚未返回给出库,轮询检查每个明细子单元的执行情况,如果所有的明细单元全部成功,整合返回值,通过MQ消息发送给出库。如果存在子明细单位处理失败,则回退已成功的明细子单元,整合处理失败的结构,通知给出库。

    若定位成功,出库进入下一个流程,如果定位失败,retry重定位。


    明细拆分,结果整合

    按明细拆分对应的基本子单元,记录整体UUID和子单元UUID之间的关系,每个子单元作为单独一个事件发送到MQ队列中。在最终结果整合时,检查整体UUID和子单元UUID的关系及处理状态,成功则可以ACK回执给调用方,标记为成功状态;若失败,则回退成功的分批数据,标记为失败状态。


    顺序消息队列,在异常重试方面存在问题,如何解决?

    顺序消息队列,Consumer端异常时,捕获异常,并转至新的业务retry消息独消息队列中,进行消费处理,避免阻塞顺序消息队列的消费处理。


    切量方案

    按仓维度进行切量,支持按单个仓、多个仓,通配符代表所有仓,通过动态开关切量,可以在MQ、JSF方式上互切。

    通过UUID来防重幂等,保证数据的最终一致性,不重不漏,exactly once。


    降级机制

    当MQ方式长时间无定位结果返回时,需降级至JSF交互方式,同时建设对应的监控预警功能。


    消息队列隔离维度

    集群之间不共享消息队列,每个集群使用独立的消息队列。

    TOPIC需要按场景拆分,隔离阈值,积压的调控也更方便处理。UAT、TEST这些场景都可以指向同一个,不用细分,生产和灰度需要隔离细分下,同一个集群的灰度和生产可以共享TOPIC。

    每个集群内,消息队列按作业方式不同,又分别细分为三类消息队列隔离独享:


    导单/全自动出库/爆品生产消息队列

    可设置限流,当限流阈值较多时,合理保护应用和数据库安全,同时也不影响正常必要的生产作业。


    普通消息队列

    可适当设置限流,灵活调整限流阈值,满足作业生效。


    高时效消息队列

    例如前置仓、秒送仓,基本上不设置限流,目前前置仓、秒送仓的场景下基本上也不会触发SKU粒度的限流。


    从对接节点上又分为两大类:

    • 出库与库存交互及回执的MQ队列。
    • 库存内部按SKU拆分更新数据库的MQ队列。

    其中每个大类下,又会按用途、集群、环境进行详细拆分,相互隔离,避免积压和重试的干扰影响,有效控制爆炸半径。即使某个集群的某个场景出现突增流量,也不会影响其他集群和环境的消息处理。


    出库与库存交互及回执的MQ队列规划如下:


    库存内部按SKU拆分更新数据库的MQ队列规划如下:


    消息队列的类型选择

    JMQ消息对接分为四种,这里我用的是普通消息和顺序消息。

    在顺序消息的选择上,又分为三种:

    • 严格顺序消息(创建主题时,消息类型选择“顺序消息”)
    • 局部有序消息(创建主题时,消息类型不需要选择“顺序消息”)
    • 分区顺序消息(创建主题时,消息类型不需要选择“顺序消息”)


    本文用到了局部有序消息,并没有采用严格顺序消息,选择前者是在吞吐量和顺序性上的trade-off。


    对于普通队列topic,既可以发送局部顺序消息,也可以发送非顺序消息。

    1、发送局部顺序消息的时候,businessId = 排队key,并且message.setOrdered(true)。

    2、发送非顺序消息的时候,businessId = 正常设置,比如orderNo,不需要setOrdered。


    治理开发

    67 commits,127 changed files with 8897 additions and 59 deletions.

    数个不眠之夜终于将治理改造开发落地。


    下楼寅时,到家卯时。




    异常场景

    字段JSON映射丢失

    {"resultCode":190100,"resultMessage":"locatingOrderDetailList[0].filterRuleConfig.lotAttributeFilterItemConfigList[0].lotAttributeFilterType:不能为null ","prompType":0,"success":false}
    


    入参正常报文



    异常报文


    复现手段

    LotAttributeFilterItemConfig lotAttributeFilterItemConfig = new LotAttributeFilterItemConfig();
    lotAttributeFilterItemConfig.setLotAttributeFilterType(LotAttributeFilterTypeEnum.shelfLifeType);
    System.out.println("lotAttributeFilterItemConfig = " + JsonUtil.toJsonString(lotAttributeFilterItemConfig));
    
    String json = "{\"lotAttributeFilterType\":\"shelfLiftType\",\"beforeDays\":0,\"afterDays\":0,\"unSaleableDays\":0}";
    LotAttributeFilterItemConfig lotAttributeFilterItemConfig2 = JsonUtil.parseObject(json, LotAttributeFilterItemConfig.class);
    
    LotAttributeFilterItemConfig lotAttributeFilterItemConfig3 = new Gson().fromJson(json, LotAttributeFilterItemConfig.class);
    


    解决方案



    我用的是gson工具,@SerializedName注解的作用是,可以将自定义的字段名与JSON中的字段对应起来,通过字段映射,问题解决。


    库存操作与流水明细不一致

    出库定位是按行号拆分明细,每个明细是一个小事务,当同一个订单中多明细存在相同的SKU,在操作数据库时相同SKU合并更新,每个小事务中都会写入库存流水明细,但有的小事务没有库存更新逻辑,有的有。这样库存明细记录与库存更新就存在了不一致。当多个小事务需要回退时,按库存流水明细回退库存,就不准确了。需要将库存更新按行号拆分,与同一行号的库存流水明细,闭环一体,在同一个小事务内落库,保证库存更新与库存流水明细的一致性。


    定位结果迟迟未发出

    前一个定位请求,定位失败,进行重试,重试的请求被前一个请求的分布式锁防住,导致第二次执行被拦住,通过优化分布式锁的使用,该场景问题解决。


    压测验证

    场景一 非爆品



    场景二 爆品,普通队列



    场景三 爆品,局部顺序队列



    场景四 爆品,严格顺序队列



    压测数据

    数据库 32C128G

    应用 4C8G * 20

    场景
    吞吐量
    数据库CPU利用率
    应用CPU利用率
    发压时长
    定位处理时长
    非爆品
    9100单/min
    52%
    66%
    10 min
    12 min
    爆品,普通队列
    前 5100单/min
    后 2800/min
    32%
    27%
    10 min
    17 min
    爆品,局部顺序队列
    前 5300单/min
    后 2400单/min
    40%
    21%
    10 min
    16 min
    爆品,严格顺序队列
    前 5100单/min
    后 1300单/min
    39%
    24%
    10 min
    25 min


    应用、数据库表现均正常,有效保障了系统的稳定性,降低了大促风险。

    目前TOPIC的分区数是48,后续随业务增长,需要扩容至128。


    切量推广

    已在爆品仓、TOP单量的大仓中率先进行了推广验证,有效性得到验证,后续会进一步推广切量。


    落地效果



    在爆品仓所在的多个分库中,数据库表现稳定,CPU无明显突刺,无风险,平稳度过大促。




    多个集群的应用分组随着业务流量在合理范围内正常变化。


    整体系统表现符合预期,消除大促风险,有力保障了大促系统的稳定性。