事务传播行为引发的问题

事务传播行为引发的问题

有了前文的基础篇对事物传播行为和隔离级别有了清楚的认识,我们在看下实战中会遇到哪些问题。

业务背景:
一个商品可以领取券码,功能是这样的,一个商品预先设置库存总量,并且导入库存券码。商品维度的信息和数量是缓存到redis中的,用户领取的时候将redis中的商品总数减去1,然后进入到详情页这个时候有个分配库存的动作,将库存改为状态已用,领取流水改为已分配,填充券码,就如下图所示:
分配库存详情页
看起来非常简单,rest请求为product/detail/{productId}
service逻辑:

@Transaction(propagation = Propagation.REQUIRED)
public Object assignStoreIfNecessary(String uid, Long productId){
    Product product = productService.getProductById(productId);
....
    ProductFlow productFlow = flowService.getFlow(uid,productId);
    if(productFlow.getStatus != 0){
        ....
        return result;
    }
    //加上分布式锁分配库存
    lockTemplate.doBiz(new LockedCallback<Store>() {
            @Override
            public Store callback() {
                Store unusedStore = storeService.findUnusedStore(productId);
                if(unusedStore == null){
                    throw new BusinessException("3","未能获取到一个库存 uid="+uid+",productId="+productId);
                }
                flowService.update2Used(unusedStore.getId());
                log.info("uid={},assign storeId={},productId={}",uid,unusedStore.getId(),productId);
                return unusedStore;
            }
        },"assign-store","assign-store"+rightId, "assignstoreKey",4);
}
flow.setStoreId(unusedStore.getId());
flow.setStoreTicketCode(unusedStore.getTicketCode());
flow.setStatus(1);
flowService.update(flow);

获取商品的券码SQL执行如下:

SELECT * FROM `store` WHERE `productId` = #{productId}  and `status` =0 ORDER BY `id` LIMIT 1

为了防止多个线程并发导致,同一个商品不同用户获取到相同的券码我们加入了分布式锁,关于分布式锁可以参考我之前的文章一行代码分布式锁
在看下更新操作:

@Transactional(propagation = Propagation.REQUIRES)
@Override
 public int update2Used(Long storeId) {
     Assert.notNull(storeId,"update2Used storeId="+storeId);
     return storeMapper.updateStatusById(storeId, 1);
 }

整个的流程就是根据productId和uid获取一个库存券码(这一步是有分布式锁),将这个券码置为已使用状态,领取流水的填充库存和修改状态,有什么问题???????

然而线上还真就出现了同一个商品多个用户分配到了同一个券码!!!!!

当时怀疑的几个点:

1)分布式锁没有起到作用,多个线程同时执行一条SQL获取同一行库存(同一个券码)
2)如果锁没问题,那么线程肯定是读取未提交的数据,查看隔离级别
3)事务传播(组内一个同事提出的)

拿到分配重复的券码,去流水表查看确实有两个用户分配了同一个券码
看了先线上日志两个线程获取锁的是串行的 第一个释放锁,晚于第二个获取锁的时间,中间相差几毫秒,所以分布式锁没有问题1)排除掉了。
查看了线上Mysql隔离级别为readCommit,所以2)排除了。
只剩下事务传播了,我们将上面的代码简化下:

@Transaction(propagation = Propagation.REQUIRED)
public Object assignStoreIfNecessary(String uid, Long productId){
    before();// (A)
    //lock start
    @Transaction(propagation = Propagation.REQUIRED)
    flowService.update2Used(unusedStore.getId());
    //lock end
    after(); //(B)
}

现如今明显的问题是,A线程读取到B线程没有提交的数据,但是B执行获取一个商品的券码并将其设置为已使用了(lockStart和lockEnd部分的代码),但是A没有感知到。所以处理方式就是让B执行的(lockStart和lockEnd部分的代码)能让A感知到。
再回到Propagation.REQUIRED 和Propagation.REQUIRES_NEW的,不熟悉的基础篇看下,上文的代码中外层Service使用的Propagation.REQUIRED内层的Service也是Propagation.REQUIRED,没有开启使用则创建事务,所以内层事务用的是外层的事务,真个方法全部完成后才是事务才提交,其他线程才能感知到。

有一种方式让这个方法加上锁,真个事务提交,其他线程也就能感知到了,但是这种方式不可取,影响性能。
再有如果能有一种方式让方法内部B执行的(lockStart和lockEnd)最为一个独立的事务,不受外层影响,在配合上分布式锁,就可以了。所以只需要做一点改动:

@Transaction(propagation = Propagation.REQUIRED)
flowService.update2Used(unusedStore.getId());
改成
@Transaction(propagation = Propagation.REQUIRES_NEW)
flowService.update2Used(unusedStore.getId());

就可以了,改完线上确实好了。

那么以前为什么没有碰到类似的事情?service调用service很常见啊,以前也是这样写的。
因为以前写的都是跟个人有关(每个人都会处理不同的行,并发操作的同一张表的不同行,而且事务是个人的事务,不会有并发–当然多个pc同一个用户一起访问除外–不法分子),不像这类多个线程共享的库存(可同时查询和修改,非个人级的),对于共享的我们通常是lockStart查询后修改lockEnd,但是方法如果被外部调用就要注意这个部分要作为独立的事务,否则还会出现一行代码导致看似是读未提交,实质是因为查询修改是所有用户都能直接访问共享行)要做独立的事务的问题。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值