冰化了还有棍儿

JPA事务控制与外键死锁

字数统计: 434阅读时长: 1 min
2022/09/25

问题背景

在做app作业一的第二部分时,遇到了问题(自以为是问题,没想到老师是故意让我们发现这是个bug)。

flowchart LR;
A(OrderController::createOrder) --> B(OrderService::createOrder);
B --> C(OrderDao::addOne);
B --> D(OrderItemDao::addList);

如图,在电子书服务系统E-Book中,我们进行订单创建,一个订单order中包含多个订单项orderItem

现在我们要对OrderService::createOrderOrderDao::saveOneOrderItemDao::saveList进行事务传播控制,分别简称三部分为A,B,C。

若A和B设置为REQUIRED,而C设置为REQUIRES_NEW,则会出现死锁问题。

源代码大致如下。

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
// OrderServiceImpl.java
@Transactional
public void createOrder(List<Integer> bookIds, Long userId) {
Order order = new Order();
order.setUser(userDao.findOne(userId));
orderDao.addOne(order);

// 准备orderItems的List
List<OrderItem> orderItems = new ArrayList<>();
...;

orderItemDao.addList(orderItems);
}

// OrderDaoImpl.java
@Transactional
public void addOne(Order order) {
orderRepository.save(order);
}


// OrderItemDaoImpl.java
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void addList(List<OrderItem> orderItems) {
orderItemRepository.saveList(orderItems);
}

执行结果为死锁。


原因分析

在B执行后,由于与A处在同一事务一里,则事务一拿到了order表中新插入行的锁,不管事务隔离属性如何设置,其他事务均不可进行写操作。而在随后的C流程中,写入orderItems的时候,order_item表中有order的外键,于是他在给order_item新插入的数据上锁之外,也要在order表中对外键所指向的数据加锁。

然而C是拿不到的,因为B已经拿到了。

于是此时,C等待B放锁,但是同时,B又等待C结束返回,从而形成了死锁。


解决方法

该问题说明,并不是所有业务使用REQUIRES_NEW传播都可以,在此场景下,都使用默认的REQUIRED传播属性即可。

CATALOG
  1. 1. 问题背景
  2. 2. 原因分析
  3. 3. 解决方法