1、概述
Spring
针对Java Transaction API (JTA)
、JDBC
、Hibernate
和Java Persistence API(JPA)
等事务 API,实现了一致的编程模型,而Spring
的声明式事务功能更是提供了极其方便的事务配置方式,配合Spring Boot
的自动配置,大多数Spring Boot
项目只需要在方法上标记@Transactional
注解,即可一键开启方法的事务性配置。
但是,事务如果没有被正确出,很有可能会导致事务的失效,带来意想不到的数据不一致问题,随后就是大量的人工接入查看和修复数据,该篇主要分享Spring
事务在技术上的正确使用方式,避免因为事务处理不当导致业务逻辑产生大量偶发性BUG
。
2、事务原理
在讨论事务失效的场景之前,首先简要介绍一下Spring事务的原理。Spring通过AOP(Aspect-Oriented Programming,面向切面编程)来管理事务,主要依赖于@Transactional注解来定义事务边界。事务的传播机制、隔离级别、超时设置等通过该注解进行配置。
3、事务传播类型
//如果有事务, 那么加入事务, 没有的话新建一个(默认)
@Transactional(propagation=Propagation.REQUIRED)
//容器不为这个方法开启事务
@Transactional(propagation=Propagation.NOT_SUPPORTED)
//不管是否存在事务, 都创建一个新的事务, 原来的挂起, 新的执行完毕, 继续执行老的事务
@Transactional(propagation=Propagation.REQUIRES_NEW)
//必须在一个已有的事务中执行, 否则抛出异常
@Transactional(propagation=Propagation.MANDATORY)
//必须在一个没有的事务中执行, 否则抛出异常(与Propagation.MANDATORY相反)
@Transactional(propagation=Propagation.NEVER)
//如果其他bean调用这个方法, 在其他bean中声明事务, 那就用事务, 如果其他bean没有声明事务, 那就不用事务
@Transactional(propagation=Propagation.SUPPORTS)
4、失效场景
4.1、方法没有被public修饰
如果事务所在的方法没有被public修饰,此时Spring的事务会失效,例如,如下代码所示。
@Service
public class ProductService {
@Autowired
private ProductDao productDao;
@Transactional(propagation = Propagation.REQUIRES_NEW)
private void updateProductStockCountById(Integer stockCount, Long id){
productDao.updateProductStockCountById(stockCount, id);
}
}
虽然ProductService上标注了@Service注解,同时updateProductStockCountById()方法上标注了@Transactional(propagation = Propagation.REQUIRES_NEW)注解。
但是,由于updateProductStockCountById()方法为内部的私有方法(使用private修饰),那么此时updateProductStockCountById()方法的事务在Spring中会失效。
4.2、同一类中方法调用
如果同一个类中的两个方法分别为A和B,方法A上没有添加事务注解,方法B上添加了 @Transactional事务注解,方法A调用方法B,则方法B的事务会失效。例如,如下代码所示。
@Service
public class OrderService {
@Autowired
private OrderDao orderDao;
@Autowired
private ProductDao productDao;
public void submitOrder(){
//生成订单
Order order = new Order();
long number = Math.abs(new Random().nextInt(500));
order.setId(number);
order.setOrderNo("order_" + number);
orderDao.saveOrder(order);
//减库存
this.updateProductStockCountById(1, 1L);
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void updateProductStockCountById(Integer stockCount, Long id){
productDao.updateProductStockCountById(stockCount, id);
}
}
submitOrder()方法和updateProductStockCountById()方法都在OrderService类中,submitOrder()方法上没有标注事务注解,updateProductStockCountById()方法上标注了事务注解,submitOrder()方法调用了updateProductStockCountById()方法,此时,updateProductStockCountById()方法的事务在Spring中会失效。
4.3、未配置事务管理器
如果在项目中没有配置Spring的事务管理器,即使使用了Spring的事务管理功能,Spring的事务也不会生效。
例如,没有在项目的配置类中配置如下代码。
@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource) {
return new DataSourceTransactionManager(dataSource);
}
此时,Spring的事务就会失效。
4.4、异常类型不匹配
默认情况下,Spring事务只会在未检查异常(继承自RuntimeException)和Error时回滚。如果抛出的是受检查异常(继承自Exception但不是RuntimeException),事务不会回滚。
@Service
public class PaymentService {
@Transactional
public void processPayment() throws Exception {
// 受检查异常,不会导致事务回滚
throw new Exception("支付失败");
}
}
4.5、事务传播行为配置不当
事务的传播行为决定了事务方法之间的关系。如果配置不当,可能导致事务失效。
@Service
public class InventoryService {
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void updateStock() {
// 更新库存
}
}
4.6、多线程问题
Spring事务是线程绑定的,多线程环境中事务会失效。
@Service
public class ReportService {
@Transactional
public void generateReport() {
new Thread(() -> {
// 新线程中事务不生效
updateDatabase();
}).start();
}
private void updateDatabase() {
// 更新数据库
}
}
4.7、异常被内部catch,程序生吞异常
@Service
public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements IOrderService {
@Autowired
private OrderMapper orderMapper;
@Autowired
private ProductMapper productMapper;
@Override
@Transactional(propagation = Propagation.REQUIRES_NEW)
public ResponseEntity submitOrder(Order order) {
long orderNo = Math.abs(ThreadLocalRandom.current().nextLong(1000));
order.setOrderNo("ORDER_" + orderNo);
orderMapper.insert(order);
// 扣减库存
this.updateProductStockById(order.getProductId(), 1L);
return new ResponseEntity(HttpStatus.OK);
}
/**
* 扣减库存方法事务类型声明为NOT_SUPPORTED不支持事务的传播
*/
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public void updateProductStockById(Integer num, Long productId) {
try {
productMapper.updateProductStockById(num, productId);
} catch (Exception e) {
// 这里仅仅是捕获异常之后的打印(相当于程序吞掉了异常)
log.error("Error updating product Stock: {}", e);
}
}
}
4.8、方法内部直接调用
如果先调用deleteUser()
,那么deleteUserA()
是不会回滚的,其原因就是@Transactional
根本没生成代理,如果直接调用deleteUser2()
那么没问题,deleteUserA()
会回滚。
public void deleteUser() throws MyException{
deleteUser2();
}
@Transactional
public void deleteUser2() throws MyException{
userMapper.deleteUserA();
int i = 1 / 0;
userMapper.deleteUserB();
}