一起深度bug分析

业务背景

我们有一个业务是快速订单。快速订单提交审批后,会自动的生成发货单,然后自动占用库存,占用库存的时候订单微服务会调用库存微服务。
这是一个较长的事务。业务上我们分成了2段本地事务。生成发货单是订单服务的本地事务,占用库存是库存服务的本地事务。发货单通过feign调用库存服务成功后,本地提交事务,写发货单占用记录。(这里面涉及到2阶段事务以及事务补偿,我们暂时放在一边)。

bug现象

对于某批次商品,发现分别发货单占用记录的占用总数与库存账分配的占用数不一致。发货单占用总数大于库存账占用。

分析过程

  • 1 首先我们怀疑事务出了问题,如果订单事务没有回滚,那么是可能出现占用多的情况。 但是review代码以及检查日志,占用不成功的事务都成功的进行了回滚。

  • 2 订单服务和库存服务的数据库事务都要写binlog,我们又从binlog入手分析。统计后发现,在该时间段内的单据,出现问题批次的商品,库存占用业务对数据库更新了99次,如果一致的话,那么发货单占用记录应该也是99条,但是发货单占用记录却为100条。又仔细看了库存占用binlog日志,库存服务的占用记录正常,没有突然不一致的情况,所以数据库执行上看起来没有问题。

  • 3 问题已经大致定位到就是写入不一致导致的。继续review代码发现,订单业务先去查询库存,计算配货占用,写入占用记录表,然后feign调用库存业务申请占用,如果库存占用成功,则提交事务。那么为什么会出现写入不一致的情况呢?

  • 4 继续分析binlog日志,发现某一个订单有10条占用明细,但是库存服务只有9条update的binlog,然而订单占用记录却有10条。又回头看代码,发现代码逻辑漏洞一个,因为是批量申请占用的,例如发给库存服务10个商品以及占用数量,库存服务对10个商品进行占用,执行update语句,如果10条update,有9条update,另外一条没有匹配到或者没有更新,只要不是占用不成功(申请占用数量大于可占用数量),库存服务不给出异常,那么订单服务也认为成功了。

验证binlog

The binary log contains “events” that describe database changes such as table creation operations or changes to table data. It also contains events for statements that potentially could have made changes (for example, a DELETE which matched no rows), unless row-based logging is used. The binary log also contains information about how long each statement took that updated data.

实验1

update film set title='ACADEMY DINOSAUR' where film_id=1;
1

这条语句没有对数据库产生更改,因此也没有生成binlog

实验2

在这个实验中,我们批量更新3条记录。

begin;
UPDATE film 
SET title =
CASE
 WHEN film_id = 1 THEN
 'ACADEMY DINOSAUR_2' 
 WHEN film_id = 2 THEN
 'ACE GOLDFINGER_2' 
 WHEN film_id = 3 THEN
 'ACE GOLDFINGER_2' 
END
where film_id in (1,2,3);
commit;
1
2
3
4
5
6
7
8
9
10
11
12
13

这条语句binlog日志为

BEGIN
/*!*/;
# at 122620053
#201230  2:33:42 server id 1  end_log_pos 122620136 CRC32 0x0f2ed94d    Table_map: `freshalwms_test`.`film` mapped to number 316
# at 122620136
#201230  2:33:42 server id 1  end_log_pos 122620998 CRC32 0x5fa28f9c    Update_rows: table id 316 flags: STMT_END_F

'/*!*/;
### UPDATE `freshalwms_test`.`film`
### WHERE
###   @1=1 /* SHORTINT meta=0 nullable=0 is_null=0 */
###   @2='ACADEMY DINOSAUR' /* VARSTRING(765) meta=765 nullable=0 is_null=0 */
###   @3='A Epic Drama of a Feminist And a Mad Scientist who must Battle a Teacher in The Canadian Rockies' /* BLOB/TEXT meta=2 nullable=1 is_null=0 */
###   @4=2006 /* YEAR meta=0 nullable=1 is_null=0 */
###   @5=1 /* TINYINT meta=0 nullable=0 is_null=0 */
###   @6=NULL /* TINYINT meta=0 nullable=1 is_null=1 */
###   @7=6 /* TINYINT meta=0 nullable=0 is_null=0 */
###   @8=0.99 /* DECIMAL(4,2) meta=1026 nullable=0 is_null=0 */
###   @9=86 /* SHORTINT meta=0 nullable=1 is_null=0 */
###   @10=20.99 /* DECIMAL(5,2) meta=1282 nullable=0 is_null=0 */
###   @11=2 /* ENUM(1 byte) meta=63233 nullable=1 is_null=0 */
###   @12=b'00001100' /* SET(1 bytes) meta=63489 nullable=1 is_null=0 */
###   @13=1609266788 /* TIMESTAMP(0) meta=0 nullable=0 is_null=0 */
### SET
###   @1=1 /* SHORTINT meta=0 nullable=0 is_null=0 */
###   @2='ACADEMY DINOSAUR_2' /* VARSTRING(765) meta=765 nullable=0 is_null=0 */
###   @3='A Epic Drama of a Feminist And a Mad Scientist who must Battle a Teacher in The Canadian Rockies' /* BLOB/TEXT meta=2 nullable=1 is_null=0 */
###   @4=2006 /* YEAR meta=0 nullable=1 is_null=0 */
###   @5=1 /* TINYINT meta=0 nullable=0 is_null=0 */
###   @6=NULL /* TINYINT meta=0 nullable=1 is_null=1 */
###   @7=6 /* TINYINT meta=0 nullable=0 is_null=0 */
###   @8=0.99 /* DECIMAL(4,2) meta=1026 nullable=0 is_null=0 */
###   @9=86 /* SHORTINT meta=0 nullable=1 is_null=0 */
###   @10=20.99 /* DECIMAL(5,2) meta=1282 nullable=0 is_null=0 */
###   @11=2 /* ENUM(1 byte) meta=63233 nullable=1 is_null=0 */
###   @12=b'00001100' /* SET(1 bytes) meta=63489 nullable=1 is_null=0 */
###   @13=1609266822 /* TIMESTAMP(0) meta=0 nullable=0 is_null=0 */
### UPDATE `freshalwms_test`.`film`
### WHERE
###   @1=2 /* SHORTINT meta=0 nullable=0 is_null=0 */
###   @2='ACE GOLDFINGER' /* VARSTRING(765) meta=765 nullable=0 is_null=0 */
###   @3='A Astounding Epistle of a Database Administrator And a Explorer who must Find a Car in Ancient China' /* BLOB/TEXT meta=2 nullable=1 is_null=0 */
###   @4=2006 /* YEAR meta=0 nullable=1 is_null=0 */
###   @5=1 /* TINYINT meta=0 nullable=0 is_null=0 */
###   @6=NULL /* TINYINT meta=0 nullable=1 is_null=1 */
###   @7=3 /* TINYINT meta=0 nullable=0 is_null=0 */
###   @8=4.99 /* DECIMAL(4,2) meta=1026 nullable=0 is_null=0 */
###   @9=48 /* SHORTINT meta=0 nullable=1 is_null=0 */
###   @10=12.99 /* DECIMAL(5,2) meta=1282 nullable=0 is_null=0 */
###   @11=1 /* ENUM(1 byte) meta=63233 nullable=1 is_null=0 */
###   @12=b'00000101' /* SET(1 bytes) meta=63489 nullable=1 is_null=0 */
###   @13=1139951022 /* TIMESTAMP(0) meta=0 nullable=0 is_null=0 */
### SET
###   @1=2 /* SHORTINT meta=0 nullable=0 is_null=0 */
###   @2='ACE GOLDFINGER_2' /* VARSTRING(765) meta=765 nullable=0 is_null=0 */
###   @3='A Astounding Epistle of a Database Administrator And a Explorer who must Find a Car in Ancient China' /* BLOB/TEXT meta=2 nullable=1 is_null=0 */
###   @4=2006 /* YEAR meta=0 nullable=1 is_null=0 */
###   @5=1 /* TINYINT meta=0 nullable=0 is_null=0 */
###   @6=NULL /* TINYINT meta=0 nullable=1 is_null=1 */
###   @7=3 /* TINYINT meta=0 nullable=0 is_null=0 */
###   @8=4.99 /* DECIMAL(4,2) meta=1026 nullable=0 is_null=0 */
###   @9=48 /* SHORTINT meta=0 nullable=1 is_null=0 */
###   @10=12.99 /* DECIMAL(5,2) meta=1282 nullable=0 is_null=0 */
###   @11=1 /* ENUM(1 byte) meta=63233 nullable=1 is_null=0 */
###   @12=b'00000101' /* SET(1 bytes) meta=63489 nullable=1 is_null=0 */
###   @13=1609266822 /* TIMESTAMP(0) meta=0 nullable=0 is_null=0 */
### UPDATE `freshalwms_test`.`film`
### WHERE
###   @1=3 /* SHORTINT meta=0 nullable=0 is_null=0 */
###   @2='ADAPTATION HOLES' /* VARSTRING(765) meta=765 nullable=0 is_null=0 */
###   @3='A Astounding Reflection of a Lumberjack And a Car who must Sink a Lumberjack in A Baloon Factory' /* BLOB/TEXT meta=2 nullable=1 is_null=0 */
###   @4=2006 /* YEAR meta=0 nullable=1 is_null=0 */
###   @5=1 /* TINYINT meta=0 nullable=0 is_null=0 */
###   @6=NULL /* TINYINT meta=0 nullable=1 is_null=1 */
###   @7=7 /* TINYINT meta=0 nullable=0 is_null=0 */
###   @8=2.99 /* DECIMAL(4,2) meta=1026 nullable=0 is_null=0 */
###   @9=50 /* SHORTINT meta=0 nullable=1 is_null=0 */
###   @10=18.99 /* DECIMAL(5,2) meta=1282 nullable=0 is_null=0 */
###   @11=5 /* ENUM(1 byte) meta=63233 nullable=1 is_null=0 */
###   @12=b'00000101' /* SET(1 bytes) meta=63489 nullable=1 is_null=0 */
###   @13=1139951022 /* TIMESTAMP(0) meta=0 nullable=0 is_null=0 */
### SET
###   @1=3 /* SHORTINT meta=0 nullable=0 is_null=0 */
###   @2='ACE GOLDFINGER_2' /* VARSTRING(765) meta=765 nullable=0 is_null=0 */
###   @3='A Astounding Reflection of a Lumberjack And a Car who must Sink a Lumberjack in A Baloon Factory' /* BLOB/TEXT meta=2 nullable=1 is_null=0 */
###   @4=2006 /* YEAR meta=0 nullable=1 is_null=0 */
###   @5=1 /* TINYINT meta=0 nullable=0 is_null=0 */
###   @6=NULL /* TINYINT meta=0 nullable=1 is_null=1 */
###   @7=7 /* TINYINT meta=0 nullable=0 is_null=0 */
###   @8=2.99 /* DECIMAL(4,2) meta=1026 nullable=0 is_null=0 */
###   @9=50 /* SHORTINT meta=0 nullable=1 is_null=0 */
###   @10=18.99 /* DECIMAL(5,2) meta=1282 nullable=0 is_null=0 */
###   @11=5 /* ENUM(1 byte) meta=63233 nullable=1 is_null=0 */
###   @12=b'00000101' /* SET(1 bytes) meta=63489 nullable=1 is_null=0 */
###   @13=1609266822 /* TIMESTAMP(0) meta=0 nullable=0 is_null=0 */
# at 122620998
#201230  2:33:42 server id 1  end_log_pos 122621029 CRC32 0xa38697a9    Xid = 2069954
COMMIT/*!*/;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98

实验3

在这个实验中,我们批量更新3条记录,其中有2条可以更新,另外一条更新不到

begin;
UPDATE film 
SET title =
CASE
 WHEN film_id = 1 THEN
 'ACADEMY DINOSAUR_2' 
 WHEN film_id = 2 THEN
 'ACE GOLDFINGER_2' 
 WHEN film_id = 3 THEN
 'ACE GOLDFINGER_2' 
END
where film_id in (1,2,3);
commit;
1
2
3
4
5
6
7
8
9
10
11
12
13

binlog

这条语句binlog日志为

BEGIN
/*!*/;
# at 140854194
#201230 14:01:17 server id 1  end_log_pos 140854277 CRC32 0x38ebc8a3 	Table_map: `freshalwms_test`.`film` mapped to number 316
# at 140854277
#201230 14:01:17 server id 1  end_log_pos 140854867 CRC32 0x94e3bc40 	Update_rows: table id 316 flags: STMT_END_F
### UPDATE `freshalwms_test`.`film`
### WHERE
###   @1=1 /* SHORTINT meta=0 nullable=0 is_null=0 */
###   @2='ACADEMY DINOSAUR_2' /* VARSTRING(765) meta=765 nullable=0 is_null=0 */
###   @3='A Epic Drama of a Feminist And a Mad Scientist who must Battle a Teacher in The Canadian Rockies' /* BLOB/TEXT meta=2 nullable=1 is_null=0 */
###   @4=2006 /* YEAR meta=0 nullable=1 is_null=0 */
###   @5=1 /* TINYINT meta=0 nullable=0 is_null=0 */
###   @6=NULL /* TINYINT meta=0 nullable=1 is_null=1 */
###   @7=6 /* TINYINT meta=0 nullable=0 is_null=0 */
###   @8=0.99 /* DECIMAL(4,2) meta=1026 nullable=0 is_null=0 */
###   @9=86 /* SHORTINT meta=0 nullable=1 is_null=0 */
###   @10=20.99 /* DECIMAL(5,2) meta=1282 nullable=0 is_null=0 */
###   @11=2 /* ENUM(1 byte) meta=63233 nullable=1 is_null=0 */
###   @12=b'00001100' /* SET(1 bytes) meta=63489 nullable=1 is_null=0 */
###   @13=1609266822 /* TIMESTAMP(0) meta=0 nullable=0 is_null=0 */
### SET
###   @1=1 /* SHORTINT meta=0 nullable=0 is_null=0 */
###   @2='ACADEMY DINOSAUR' /* VARSTRING(765) meta=765 nullable=0 is_null=0 */
###   @3='A Epic Drama of a Feminist And a Mad Scientist who must Battle a Teacher in The Canadian Rockies' /* BLOB/TEXT meta=2 nullable=1 is_null=0 */
###   @4=2006 /* YEAR meta=0 nullable=1 is_null=0 */
###   @5=1 /* TINYINT meta=0 nullable=0 is_null=0 */
###   @6=NULL /* TINYINT meta=0 nullable=1 is_null=1 */
###   @7=6 /* TINYINT meta=0 nullable=0 is_null=0 */
###   @8=0.99 /* DECIMAL(4,2) meta=1026 nullable=0 is_null=0 */
###   @9=86 /* SHORTINT meta=0 nullable=1 is_null=0 */
###   @10=20.99 /* DECIMAL(5,2) meta=1282 nullable=0 is_null=0 */
###   @11=2 /* ENUM(1 byte) meta=63233 nullable=1 is_null=0 */
###   @12=b'00001100' /* SET(1 bytes) meta=63489 nullable=1 is_null=0 */
###   @13=1609308077 /* TIMESTAMP(0) meta=0 nullable=0 is_null=0 */
### UPDATE `freshalwms_test`.`film`
### WHERE
###   @1=2 /* SHORTINT meta=0 nullable=0 is_null=0 */
###   @2='ACE GOLDFINGER_2' /* VARSTRING(765) meta=765 nullable=0 is_null=0 */
###   @3='A Astounding Epistle of a Database Administrator And a Explorer who must Find a Car in Ancient China' /* BLOB/TEXT meta=2 nullable=1 is_null=0 */
###   @4=2006 /* YEAR meta=0 nullable=1 is_null=0 */
###   @5=1 /* TINYINT meta=0 nullable=0 is_null=0 */
###   @6=NULL /* TINYINT meta=0 nullable=1 is_null=1 */
###   @7=3 /* TINYINT meta=0 nullable=0 is_null=0 */
###   @8=4.99 /* DECIMAL(4,2) meta=1026 nullable=0 is_null=0 */
###   @9=48 /* SHORTINT meta=0 nullable=1 is_null=0 */
###   @10=12.99 /* DECIMAL(5,2) meta=1282 nullable=0 is_null=0 */
###   @11=1 /* ENUM(1 byte) meta=63233 nullable=1 is_null=0 */
###   @12=b'00000101' /* SET(1 bytes) meta=63489 nullable=1 is_null=0 */
###   @13=1609266822 /* TIMESTAMP(0) meta=0 nullable=0 is_null=0 */
### SET
###   @1=2 /* SHORTINT meta=0 nullable=0 is_null=0 */
###   @2='ACE GOLDFINGER' /* VARSTRING(765) meta=765 nullable=0 is_null=0 */
###   @3='A Astounding Epistle of a Database Administrator And a Explorer who must Find a Car in Ancient China' /* BLOB/TEXT meta=2 nullable=1 is_null=0 */
###   @4=2006 /* YEAR meta=0 nullable=1 is_null=0 */
###   @5=1 /* TINYINT meta=0 nullable=0 is_null=0 */
###   @6=NULL /* TINYINT meta=0 nullable=1 is_null=1 */
###   @7=3 /* TINYINT meta=0 nullable=0 is_null=0 */
###   @8=4.99 /* DECIMAL(4,2) meta=1026 nullable=0 is_null=0 */
###   @9=48 /* SHORTINT meta=0 nullable=1 is_null=0 */
###   @10=12.99 /* DECIMAL(5,2) meta=1282 nullable=0 is_null=0 */
###   @11=1 /* ENUM(1 byte) meta=63233 nullable=1 is_null=0 */
###   @12=b'00000101' /* SET(1 bytes) meta=63489 nullable=1 is_null=0 */
###   @13=1609308077 /* TIMESTAMP(0) meta=0 nullable=0 is_null=0 */
# at 140854867
#201230 14:01:17 server id 1  end_log_pos 140854898 CRC32 0xde887e33 	Xid = 2377876
COMMIT/*!*/;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67

可以看到只有两个update语句,而第三个没有update。

这证实了我们的怀疑。

继续review代码。iwm有2个服务实例,用了redis key作为lock锁。

  @Transactional(rollbackFor = Exception.class)
    public int addSummaryAllocQty(InvSummaryAllocQtyDTO summaryAllocQtyDTO) {
        //库存锁
        String redisLockKey = RedisLockKey.IWM_LOCK.replace(RedisLockKey.APP_INFO_PLACEHOLDER0, summaryAllocQtyDTO.getWarehouseID()+ ":" +summaryAllocQtyDTO.getCustomerID());
        if (redisLockService.lock(redisLockKey)) {
            ImInvSummary imInvSummary = imInvSummaryMapper.selectByPrimaryKey(summaryAllocQtyDTO.getInvSummaryID());
            try {
                if (imInvSummary == null) {
                    logger.error("占用数量调整异常, 库存明细-[{}] 查询为空", summaryAllocQtyDTO.getInvSummaryID());
                    throw new SystemException(BaseErrorCodes.NOTNULL, new Object[]{"库存明细-[" + summaryAllocQtyDTO.getInvSummaryID() + "]"});
                }
                //判断调整单调整前数量是否与库存数量一致 todo
//                if (summaryAllocQtyDTO.getDocType().equals(DocType.ADJUST.getValue())) {
//                    if (imInvSummary.getBalanceQty().compareTo(summaryAllocQtyDTO.getBeforeQty()) != 0) {
//                        logger.error("调整单过账异常, SKU-[{}] 过账前数量[{}] 与 库存余额[{}] 不一致", imInvSummary.getSkuID(), summaryAllocQtyDTO.getBeforeQty(), imInvSummary.getBalanceQty());
//                        throw new SystemException(OrderErrorCodes.QTY_ERROR, new Object[]{"sku-" + imInvSummary.getSkuID() + "-调整前"});
//                    }
//                }
                //调整后数量
                BigDecimal afterAllocQty = imInvSummary.getAllocQty().add(summaryAllocQtyDTO.getQty());
                if (afterAllocQty.compareTo(imInvSummary.getBalanceQty()) > 0) {
                    Sku sku = this.searchSKUByID(imInvSummary.getSkuID());
                    String skuName = StringUtils.isNotBlank(sku.getCommonName()) ? sku.getCommonName() : sku.getSkuStdName();
                    logger.error("占用数量调整异常, SKU-[{}]-[{}] 库存明细 -[{}] 调整后占用数量[{}] 大于库存余额数量[{}]", imInvSummary.getSkuID(), skuName, imInvSummary.getId(), afterAllocQty, imInvSummary.getBalanceQty());
                    throw new SystemException(OrderErrorCodes.QTY_ERROR, new Object[]{"占用数量调整异常, SKU-" + sku.getSkuCode() + "-" + skuName + "批次:" + imInvSummary.getLotNo() + ",调整后占用数量:" + afterAllocQty + " ,大于库存余额数量:" + imInvSummary.getBalanceQty()});
                }
                imInvSummary.setAllocQty(afterAllocQty);
                imInvSummary.preUpdate();
                return imInvSummaryMapper.updateByPrimaryKeySelective(imInvSummary);
            } catch (Exception e) {
                logger.error("占用数量调整异常:" + e.getMessage());
                throw e;
            } finally {
                redisLockService.unlock(redisLockKey);
            }
        } else {
            logger.error("占用数量调整异常, 未获取到redislock : {} 或调整明细-{}处于锁定状态", redisLockKey, summaryAllocQtyDTO.getInvSummaryID());
//            throw new RuntimeException("未获取到redislock : " + redisLockKey);
            throw new SystemException(OrderErrorCodes.OPERATION_FAILED, new Object[]{"需要调整明细处于锁定状态,稍后重试"});
        }
    }

    /**
    * 批量增加占用
    *
    * @author tongt.gao
    * @param
    * @createDate 2020/12/22 14:59
    **/
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void addBatchSummaryAllocQty(List<InvSummaryAllocQtyDTO> dtoList) {
        //根据仓库及货主信息分组
        Map<String, List<InvSummaryAllocQtyDTO>> customerMap = dtoList.stream().collect(Collectors.groupingBy(a -> a.getWarehouseID() + ":" + a.getCustomerID()));
        //循环处理每个货主的占用
        for (Map.Entry<String, List<InvSummaryAllocQtyDTO>> detail : customerMap.entrySet()) {
            long begin = System.currentTimeMillis();
            //redis锁key值
            logger.info("批量增加占用开始获取 RedisLockKey:warehouseID:customerID:{}", detail.getKey());
            String redisLockKey = RedisLockKey.IWM_LOCK.replace(RedisLockKey.APP_INFO_PLACEHOLDER0, detail.getKey());
            if (redisLockService.lock(redisLockKey)) {
                logger.info("批量增加占用RedisLockKey获取成功,RedisLockKey: {},耗时:{} ms", redisLockKey, System.currentTimeMillis() - begin);
                try {
                    List<ImInvSummary> summaryList = new ArrayList<>();
                    for (InvSummaryAllocQtyDTO invSummaryAllocQtyDTO : detail.getValue()) {
                        summaryList.add(this.checkSummary(invSummaryAllocQtyDTO));
                    }
                    //批量更新库存明细
                    imInvSummaryMapper.updateQtyBatch(summaryList);
                } catch (Exception e) {
                    logger.error("占用数量调整异常:" + e.getMessage());
                    throw e;
                } finally {
                    redisLockService.unlock(redisLockKey);
                    logger.info("批量增加占用结束,RedisLockKey释放成功,RedisLockKey: {},耗时:{}", redisLockKey, System.currentTimeMillis() - begin);
                }
            } else {
                logger.error("占用数量调整异常, redislock : {} 未获取到或处于锁定状态", redisLockKey);
                throw new SystemException(OrderErrorCodes.OPERATION_FAILED, new Object[]{"需要调整明细处于锁定状态,稍后重试"});
            }
        }
    }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83

由于数据库事务隔离级别为READ COMMITED,在并发高的情况下,会有概率出现

步骤 iwm1 iwm2
1 开始数据库事务
2 开始数据库事务
3 准备获得redis lock
4 准备获得redis lock
5 获得redis lock成功
6 等待redis lock
7 执行数据库事务 等待redis lock
8 释放锁 等待redis lock
9 释放成功 等待redis lock
10 获得redis lock锁成功
11 查询summary占用余额
12 提交数据库事务

上表,如果iwm1在还没有提交数据库事务的情况下,iwm2获得的占用余额就是未更新的,这样就出现了脏读,导致iwm可能也去更新同样数量的占用

select alloc from summary where id = 1
// alloc = 100
// 100-1 =99
update summary set alloc=99 where id=1
1
2
3
4

对于iwm2可能也是要去占用1个

select alloc from summary where id = 1
// alloc = 100
// 100-1 =99
update summary set alloc=99 where id=1
1
2
3
4

这个时候iwm2,因为alloc已经是99个了,就不会执行update