多线程下 到底是事务内部开启锁 还是先加锁再开启事务?
前言
不知大家是否有观察到一个最常见的错误:
先开启事务,然后针对资源加锁,操作资源,然后释放锁,最后提交事务
你是否发现了在这样的场景下会出现并发安全的问题?
(提示:一个线程A在事务内部释放锁,另一个线程B拿到了锁,线程B看不到线程A的操作 导致 线程B 重复执行线程A已经对资源进行的操作)
用一个业务场景去说明
用一个电商系统的订单处理场景来具体说明这个“事务没有完全被锁包住”会导致的问题。
需求: 用户点击“下单”按钮后,后台要:
检查该用户是否已经提交过该订单(防重复下单)
如果没有,就创建订单
并扣减库存
错误的业务代码:
@Transactional
public void createOrder(String userId, String orderNo) {if (!orderService.hasOrdered(orderNo)) {synchronized (("lock:" + orderNo).intern()) {// 锁内逻辑// 此处加锁}// 锁释放了,但事务还没提交orderService.save(orderNo); // 保存订单productService.decreaseStock(); // 扣减库存}
}
1、线程A
开启事务
查询:是否已下单?→ 查询不到(因为数据库未提交)
执行下单逻辑:准备插入订单
!! 锁在事务内部,提前释放
事务还没提交!
2、线程B
紧接着执行相同操作
开启事务
查询:是否已下单?→ 同样查询不到(线程A没提交)
执行下单逻辑:插入重复订单、扣减库存
提交事务
最终结果
因为 数据库在**“读已提交**”隔离级别下,线程B看不到线程A未提交的插入
又因为加锁只包了业务逻辑而不是整个事务范围
所以锁一旦提前释放,线程B就能并发进来了
线程A和线程B都成功下了单
结果就是 重复支付 / 重复下单
改进
public void createOrderSafe(String userId, String orderNo) {synchronized (("lock:" + orderNo).intern()) { // 线程B被阻塞doCreateOrder(userId, orderNo); // 锁保护整个事务 内部是本地事务}
}
或者微服务中 使用redis分布式锁
RLock lock = redissonClient.getLock("order:" + orderNo);
if (lock.tryLock()) {try {// 事务中执行订单判断与插入} finally {lock.unlock(); // 锁直到事务结束才释放}
}
总结
锁放在事务外部