同一车次里的部分发货单未发货

[ 场景描述 ]
1.一个车次里可以添加多个发货单
2.一个发货单只能属于一个车次
3.一次可以向车次里添加多个发货单
4.发车前   车次状态=未发车, 发货单状态=待发货
5.发车后   车次状态=已发车, 发货单状态=已发货, 订货单状态=全部发货/部分发货
6.车次发车处理耗时大概需要 7-10 秒(根据实际单据数量而定)
7.创建车次之后, 会发送一个延时MQ消息 . 当发车时间一到, MQ消息被消费, 会将车次状态变成已发车, 车次里的所有发货单状态变成已发货, 对应的订货单状态变成全部发货或部分发货.

[ 问题描述 ]
已发车的同一车次( 241119-01141 )里有一个发货单( FH-241119-004183 )状态是待发货, 而对应的订货单状态是已发货

车次发车时间 2024-11-19 23:28:28.000
发货单创建时间 2024-11-19 23:28:28.007 与 发车时间同一秒
订货单最后更新时间 2024-11-19 23:28:28.059










[ 排查工具 ]

[ 排查思路 ]
车次发车逻辑伪代码如下
          
@Transactional(rollbackFor = Exception.class)
public void doTran() {
    1.更新车次状态为已发车
    2.查询该车次里待发货的所有发货单 send_order_list
    3.更新发货单状态为已发货
    4.循环处理每个发货单
    for send_order in send_order_list {
        4.1更新发货单明细
        4.2更新订货单明细
        4.3更新订货单状态为全部发货/部分发货
    }
}
          
        


根据发货单号 FH-241119-004183 查询日志系统, 分析得知该发货单是通过 批量城配发货 功能被添加到车次( 241119-01141 )里.
创建发货单逻辑伪代码如下
          
@Transactional(rollbackFor = Exception.class)
public void doSend() {
    send_order['send_order_status'] = "待发货"
    if (当前时间 > 车次发车时间) { // #1
        send_order['send_order_status'] = "已发货"
    }
    send_order['tms_id'] = 车次ID // 发货单存储车次ID
    1.保存发货单 // #2

    2.创建发货明细
    3.更新订货明细

    if (当前时间 > 车次发车时间) { // #3
        4.更新订货单状态为已发货
    }
}
          
        
根据以上业务逻辑, 它是考虑了车次已经发车但仍然向车次里添加发货单的场景, 会将发货单状态变成已发货, 订货单状态变成已发货.
然而在车次发车的临界时间点(2024-11-19 23:28:28.000), #1处不满足条件, 发货单状态是默认待发货, 2和3操作会访问数据库, 在执行#3处时满足条件, 订货单状态变成已发货. 在 #2 和 #3 之间, doTran 方法开始执行, 没有查询到向车次里添加的该发货单 (数据库隔离级别RC), 便发生了文章中所说的这种情况.

虽然一次向车次里添加单个发货单时,发生文章所说情况的概率极低, 但是当一次向车次里添加很多个发货单时,这个概率就会被放大.
虽然发货单的创建时间 2024-11-19 23:28:28.007 大于 车次发车时间 2024-11-19 23:28:28.000, 那是因为发货单的创建时间是在 #2处生成的, 而不是在#2之前生成.


一图概之


[ 如何解决 ]
1.车次发车逻辑(doTran) 和 向车次添加发货单逻辑(doSend) 采用同一个分布式锁, 可能会造成车次发车一直拿不到锁,造成车次迟迟未发车. 毕竟两个逻辑不对等, 不能使用分布式锁.
2.#1和#3两处的判断必须保持一致
3.默认延迟30秒再处理发车逻辑. 便会把添加到车次里的所有发货单进行处理. (并未根本解决)

4.如果向车次添加发货单逻辑(doSend)事务处理时长超过30秒,依然出现问题.