理解了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语言级别的关键字,用于实现线程同步,确保多个线程对共享资源的访问是有序的。它主要通过以下两种方式实现同步:
- 同步方法:在方法声明上使用
Synchronized
关键字,确保同一时间只有一个线程可以执行该方法。
- 同步代码块:在方法内部使用
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); if (txAttr == null || !(ptm instanceof CallbackPreferringPlatformTransactionManager)) { TransactionInfo txInfo = createTransactionIfNecessary(ptm, txAttr, joinpointIdentification); Object retVal; try { retVal = invocation.proceedWithInvocation(); } catch (Throwable ex) { completeTransactionAfterThrowing(txInfo, ex); throw ex; } finally { cleanupTransactionInfo(txInfo); }
if (retVal != null && vavrPresent && VavrDelegate.isVavrTry(retVal)) { 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(); }
|
注意 self-invocation
Self-invocation 会导致 @Transactional
注解失效,因为事务是通过代理对象管理的,而在自调用中代理不会介入。所以注意要在A类的buySynchronized方法中调用B类中的事务方法。
手动管理事务
在加锁的方法内手动管理事务, 确保事务结束后才释放锁
通过手动管理事务,可以精确控制事务的开始和结束时机,并在加锁之前开启事务,从而确保事务的一致性和线程安全性。
手动管理事务可以使用Spring的PlatformTransactionManager
和TransactionTemplate
。
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); } }
|