理解了Spring事务实现原理, 现在来解决一个实际问题。

在实际业务开发中,我们经常会遇到在一个事务内执行多个业务操作保证原子性, 同时希望这段逻辑是并发安全的。

比如在商品售卖场景中,非常重要的一件事就是防止超卖,且要保证多个业务操作之间的原子性。
以下示例代码,明显会出现超卖问题(查询是否有库存时有库存并不代码真正扣减库存时还有库存,假设扣减库存时SQL 中没有乐观锁)

1
2
3
4
5
6
7
8
9
10
11
12
13
@Transactional(rollbackFor = Exception.class)  
public void buy() {
// 查看是商品否有库存
Integer count = getProductCount();
if(count <= 0) {
throw new RuntimeException("库存为 0");
}
// 减库存
productRepository.reductCount();

// 生成订单
createOrder();
}

那么修改该段代码的一个常见思路就是给buy加锁,确保buy 在任意时刻只会被一个线程操作,从而保证库存的正确扣减。

1
2
3
4
5
6
7
8
9
10
11
12
13
@Transactional(rollbackFor = Exception.class)  
public synchronized void buy() {
// 查看是商品否有库存
Integer count = getProductCount();
if(count <= 0) {
throw new RuntimeException("库存为 0");
}
// 减库存
productRepository.reductCount();

// 生成订单
createOrder();
}

我们想要实现的逻辑是: 加锁-开始事务-执行方法-事务commit-释放锁

但是这段加锁后的代码,其实际执行效果是,开始事务-(加锁-执行方法-释放锁)- 事务commit。 依然有可能出现

也就是说在事务commit 之前,synchronized锁就已经释放,其他线程已经可以进入到该段逻辑,读到旧数据的快照库存,发现还有库存然后进行库存扣减。

如果此时库存为1 ,那么又会出现超卖现象。造成超卖的原因就是事务的开启和提交并不是在 Synchronized 锁定的范围内

下面就加锁后的代码,其实际执行效果为什么是,开始事务-(加锁-执行方法-释放锁)- 事务commit 进行解释。

Synchronized关键字

Synchronized是Java语言级别的关键字,用于实现线程同步,确保多个线程对共享资源的访问是有序的。它主要通过以下两种方式实现同步:

  1. 同步方法:在方法声明上使用Synchronized关键字,确保同一时间只有一个线程可以执行该方法。
  2. 同步代码块:在方法内部使用Synchronized关键字对特定代码块进行同步,指定一个对象锁。

Synchronized仅仅是在JVM层面上对对象加锁,与数据库的锁机制无关。

基于事务的动态代理对象

Spring事务实现原理中已经详细介绍过基于事务的动态代理对象的生成和执行目标方法的过程。

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
public abstract class TransactionAspectSupport implements BeanFactoryAware, InitializingBean {
@Nullable
protected Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass,
final InvocationCallback invocation) throws Throwable {
// 处理标准事务管理, 这是本次重点关心的
PlatformTransactionManager ptm = asPlatformTransactionManager(tm);
final String joinpointIdentification = methodIdentification(method, targetClass, txAttr);
// 如果事务属性为null或者事务管理器不支持回调
if (txAttr == null || !(ptm instanceof CallbackPreferringPlatformTransactionManager)) {
// Standard transaction demarcation with getTransaction and commit/rollback calls.
// 则执行标准的事务边界控制(即通过`getTransaction`和`commit/rollback`调用)。
//开始一个新的事务或者加入现有的事务。(如果有必要)
TransactionInfo txInfo = createTransactionIfNecessary(ptm, txAttr, joinpointIdentification);
Object retVal;
try {
// 执行目标方法,这是一个环绕通知,通常会导致目标对象的方法被调用。
retVal = invocation.proceedWithInvocation();
}
catch (Throwable ex) {
// 如果方法抛出异常,调用`completeTransactionAfterThrowing`方法来处理事务回滚,并重新抛出异常。
completeTransactionAfterThrowing(txInfo, ex);
throw ex;
}
finally {
// - 在finally块中调用`cleanupTransactionInfo`来清理事务信息。
cleanupTransactionInfo(txInfo);
}

if (retVal != null && vavrPresent && VavrDelegate.isVavrTry(retVal)) {
// Set rollback-only in case of Vavr failure matching our rollback rules...
TransactionStatus status = txInfo.getTransactionStatus();
if (status != null && txAttr != null) {
retVal = VavrDelegate.evaluateTryFailure(retVal, txAttr, status);
}
}

commitTransactionAfterReturning(txInfo);
return retVal;
}
}
}

结果以上信息可以看出事务是在代理对象中开始和结束的, 但是锁是加在目标对象方法上的,也就是说只有在真正执行目标方法时才会加锁。

总结一句话, 就是事务的开启与提交并不是在 加锁范围内的,锁在事务的范围内就释放了,所以会出现加锁锁不住的问题。要解决就应该确保 事务的开启与提交在锁的范围内。

加ReentrantLock 锁也是同样的道理

解决方案

使用数据库乐观锁

乐观锁通过版本号机制控制并发,在更新数据时检查版本号是否一致,防止并发更新导致的数据不一致问题。

使用悲观锁

悲观锁在读取数据时加锁,确保在事务执行期间其他事务无法并发访问和修改被锁定的数据。通常通过数据库的 SELECT FOR UPDATE 语句实现。

扩大加锁范围

将加锁范围扩大到事务开启之前,确保在事务开始之前就获取到锁。
在进入事务管理逻辑之前就获得锁,从而避免并发问题。
具体做法 可以将synchronized关键字应用于一个外层方法,然后在该方法内调用实际的事务方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 外层加锁方法 
public synchronized void buySynchronized(String productName, int quantity) {
// 调用实际事务方法
buy(productName, quantity);
}

@Transactional(rollbackFor = Exception.class)
public synchronized void buy() {
// 查看是商品否有库存
Integer count = getProductCount();
if(count <= 0) {
throw new RuntimeException("库存为 0");
}
// 减库存
productRepository.reductCount();

// 生成订单
createOrder();
}

手动管理事务

在加锁的方法内手动管理事务, 确保事务结束后才释放锁
通过手动管理事务,可以精确控制事务的开始和结束时机,并在加锁之前开启事务,从而确保事务的一致性和线程安全性。

手动管理事务可以使用Spring的PlatformTransactionManagerTransactionTemplate

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
@Service
public class InventoryService {

@Autowired
private ProductRepository productRepository;

@Autowired
private PlatformTransactionManager transactionManager;

// 外层加锁方法
public synchronized void buySynchronized(String productName, int quantity) {
// 手动管理事务
TransactionTemplate transactionTemplate = new TransactionTemplate(transactionManager);
transactionTemplate.execute(status -> {
try {
performBuy(productName, quantity);
} catch (Exception e) {
status.setRollbackOnly();
throw e;
}
return null;
});
}

// 内层事务方法
public void performBuy(String productName, int quantity) {
// 查看商品是否有库存
Integer count = getProductCount(productName);
if (count <= 0) {
throw new RuntimeException("库存为 0");
}
// 减库存
productRepository.reduceCount(productName, quantity);

// 生成订单
createOrder(productName, quantity);
}

private Integer getProductCount(String productName) {
// 查询库存逻辑
return productRepository.findStockByProductName(productName);
}

private void createOrder(String productName, int quantity) {
// 生成订单逻辑
System.out.println("Order created for " + quantity + " of " + productName);
}
}