概述

Spring针对Java Transaction API (JTA)JDBCHibernateJava Persistence API(JPA)等业务 API,完结了共同的编程模型,而Spring的声明式业务功用更是提供了极端便利的业务装备办法,配合Spring Boot的主动装备,大多数Spring Boot项目只需要在办法上标记@Transactional注解,即可一键敞开办法的业务性装备。可是,业务假如没有被正确出,很有可能会导致业务的失效,带来意想不到的数据不共同问题,随后便是大量的人工接入检查和修复数据,该篇首要共享Spring业务在技术上的正确运用办法,避免由于业务处理不当导致业务逻辑发生大量偶发性BUG

在分析业务失效的常见场景之前,咱们先来了解一下:业务的传达类型@Transactionnal 注解的不同特点的意义。

业务的传达类型

//假如有业务, 那么参加业务, 没有的话新建一个(默许)
@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) 

isolation

该特点用于设置底层数据库的业务阻隔等级,业务的阻隔等级介绍:

// 读取未提交数据(会呈现脏读, 不可重复读) 根本不运用
@Transactional(isolation = Isolation.READ_UNCOMMITTED)
// 读取已提交数据(会呈现不可重复读和幻读) Oracle默许
@Transactional(isolation = Isolation.READ_COMMITTED)
// 可重复读(会呈现幻读) MySQL默许
@Transactional(isolation = Isolation.REPEATABLE_READ)
// 串行化
@Transactional(isolation = Isolation.SERIALIZABLE)

@Transactionnal注解特点

@Transactional注解能够作用于接口、接口办法、类以及类办法上,它能够经过不同的参数来选择什么类型Exception反常下履行回滚或许不回滚操作。

参数 阐明
rollbackFor 用于指定有必要履行业务回滚的反常类型,能够是一个也能够是多个,当经过rollbackFor指定对应反常之后,办法履行过程中只有抛出该类型反常,才会触发业务的回滚,比方:@Transactional(rollbackFor = BusinessException.class)
rollbackForClassName rollbackFor功用相同,能够为彻底限制类名的字符串类型,比方:@Transactional(rollbackForClass = {"BusinessException.class", "RuntimeException.class"})
noRollbackFor 用于指定不需要履行业务回滚的反常类型,能够是一个也能够是多个,当经过noRollbackFor指定对应反常之后,办法履行过程中抛出该类型反常,不触发业务的回滚
noRollbackForClassName noRollbackFor功用相同,能够为彻底限制类名的字符串类型
propagation 用户设置业务的传达行为,例如:@Transactional(propagation=Propagation.REQUIRED)

Spring业务失效的场景

1. 业务办法未被Spring办理

假如业务办法地点的类没有注册到Spring IOC容器中,也便是说,业务办法地点类并没有被Spring办理,则Spring业务会失效,举个比方:

/**
 * 产品业务完结层
 *
 * @author: austin
 * @since: 2023/2/10 14:19
 */
public class ProductServiceImpl extends ServiceImpl<ProductMapper, Product> implements IProductService {
    @Autowired
    private ProductMapper productMapper;
    @Override
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void updateProductStockById(Integer stockCount, Long productId) {
        productMapper.updateProductStockById(stockCount, productId);
    }
}

ProductServiceImpl完结类上没有增加@Service注解,Product的实例也就没有被加载到Spring IOC容器,此刻updateProductStockById()办法的业务就会在Spring中失效。

2. 办法运用final类型润饰

有时候,某个办法不想被子类重新,这时能够将该办法界说成final的。一般办法这样界说是没问题的,但假如将业务办法界说成final,例如:

@Service
public class OrderServiceImpl {
    @Transactional
    public final void cancel(OrderDTO orderDTO) {
        // 撤销订单
        cancelOrder(orderDTO);
    }
}

OrderServiceImplcancel撤销订单办法被final润饰符润饰,Spring业务底层运用了AOP,也便是经过JDK动态署理或许cglib,帮咱们生成了署理类,在署理类中完结的业务功用。但假如某个办法用final润饰了,那么在它的署理类中,就无法重写该办法,从而无法增加业务功用。这种情况业务就会在Spring中失效。

Tips: 假如某个办法是static的,相同无法经过动态署理将办法声明为业务办法。

3. 非public润饰的办法

假如业务办法不是public润饰,此刻Spring业务会失效,举个比方:

/**
 * 产品业务完结层
 *
 * @author: austin
 * @since: 2023/2/10 14:19
 */
@Service
public class ProductServiceImpl extends ServiceImpl<ProductMapper, Product> implements IProductService {
    @Autowired
    private ProductMapper productMapper;
    @Override
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    private void updateProductStockById(Integer stockCount, String productId) {
        productMapper.updateProductStockById(stockCount, productId);
    }
}

尽管ProductServiceImpl增加了@Service注解,一起updateProductStockById()办法上增加了@Transactional(propagation = Propagation.REQUIRES_NEW)注解,可是由于业务办法updateProductStockById()private 界说为办法内私有,相同Spring业务会失效。

4. 同一个类中的办法相互调用

@Service
public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements IOrderService {
    @Autowired
    private OrderMapper orderMapper;
    @Autowired
    private ProductMapper productMapper;
    @Override
    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);
    }
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void updateProductStockById(Integer num, Long productId) {
        productMapper.updateProductStockById(num, productId);
    }
}

submitOrder()办法和updateProductStockById()办法都在OrderService类中,然而submitOrder()办法没有增加业务注解,updateProductStockById()办法尽管增加了业务注解,这种情况updateProductStockById()会在Spring业务中失效。

5. 办法的业务传达类型不支撑业务

假如内部办法的业务传达类型为不支撑业务的传达类型,则内部办法的业务相同会在Spring中失效,举个比方:

@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) {
        productMapper.updateProductStockById(num, productId);
    }
}

6. 反常被内部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);
        }
    }
}

7. 数据库不支撑业务

Spring业务收效的前提是衔接的数据库支撑业务,假如底层的数据库都不支撑业务,则Spring业务肯定会失效的,例如:运用MySQL数据库,选用MyISAM存储引擎,由于MyISAM存储引擎自身不支撑业务,因而业务毫无疑问会失效。

8. 未装备敞开业务

假如项目中没有装备Spring的业务办理器,即使运用了Spring的业务办理功用,Spring的业务也不会收效,例如,假如你是Spring Boot项目,没有在SpringBoot项目中装备如下代码:

@Bean
public PlatformTransactionManager transactionManager(DataSource dataSource) {
    return new DataSourceTransactionManager(dataSource);
}

假如是以往的Spring MVC项目,假如没有装备下面的代码,Spring业务也不会收效,正常需要在applicationContext.xml文件中,手动装备业务相关参数,比方:

<!-- 装备业务办理器 -->
<bean class="org.springframework.jdbc.datasource.DataSourceTransactionManager" id="transactionManager"> 
    <property name="dataSource" ref="dataSource"></property> 
</bean> 
<tx:advice id="advice" transaction-manager="transactionManager"> 
    <tx:attributes> 
        <tx:method name="*" propagation="REQUIRED"/>
    </tx:attributes> 
</tx:advice> 
<!-- 用切点把业务切进去 --> 
<aop:config> 
    <aop:pointcut expression="execution(* com.universal.ubdk.*.*(..))" id="pointcut"/> 
    <aop:advisor advice-ref="advice" pointcut-ref="pointcut"/> 
</aop:config> 

9. 错误的传达特性

其实,咱们在运用@Transactional注解时,是能够指定propagation参数的。

该参数的作用是指定业务的传达特性,目前Spring支撑7种传达特性:

  • REQUIRED 假如当时上下文中存在业务,那么参加该业务,假如不存在业务,创立一个业务,这是默许的传达特点值。
  • SUPPORTS 假如当时上下文存在业务,则支撑业务参加业务,假如不存在业务,则运用非业务的办法履行。
  • MANDATORY 假如当时上下文中存在业务,不然抛出反常。
  • REQUIRES_NEW 每次都会新建一个业务,并且一起将上下文中的业务挂起,履行当时新建业务完结以后,上下文业务恢复再履行。
  • NOT_SUPPORTED 假如当时上下文中存在业务,则挂起当时业务,然后新的办法在没有业务的环境中履行。
  • NEVER 假如当时上下文中存在业务,则抛出反常,不然在无业务环境上履行代码。
  • NESTED 假如当时上下文中存在业务,则嵌套业务履行,假如不存在业务,则新建业务。

假如咱们在手动设置propagation参数的时候,把传达特性设置错了,比方:

@Service
public class OrderServiceImpl {
    @Transactional(propagation = Propagation.NEVER)
    public void cancelOrder(UserModel userModel) {
        // 撤销订单
        cancelOrder(orderDTO);
        // 复原库存
        restoreProductStock(orderDTO.getProductId(), orderDTO.getProductCount());
    }
}

咱们能够看到cancelOrder()办法的业务传达特性界说成了Propagation.NEVER,这种类型的传达特性不支撑业务,假如有业务则会抛反常。

10. 多线程调用

在实践项目开发中,多线程的运用场景还是挺多的。假如Spring业务用在多线程场景中运用不当,也会导致业务无法收效。

@Slf4j
@Service
public class OrderServiceImpl {
    @Autowired
    private OrderMapper orderMapper;
    @Autowired
    private MessageService messageService;
    @Transactional
    public void orderCommit(orderModel orderModel) throws Exception {
        orderMapper.insertOrder(orderModel);
        new Thread(() -> {
            messageService.sendSms();
        }).start();
    }
}
@Service
public class MessageService {
    @Transactional
    public void sendSms() {
        // 发送短信
    }
}

经过示例,咱们能够看到订单提交的业务办法orderCommit()中,调用了发送短信的业务办法sendSms(),可是发送短信的业务办法sendSms()是另起了一个线程调用的。

这样会导致两个办法不在同一个线程中,从而是两个不同的业务。假如是sendSms()办法中抛了反常,orderCommit()办法也回滚是不可能的。

实践上,Spring的业务是经过ThreadLocal来确保线程安全的,业务和当时线程绑定,多个线程自然会让业务失效。

我认真总结并分析了Spring事务失效的十种常见场景

总结

本篇文章首要是介绍Spring业务传达特性,阐明了@Transactional注解特点的运用办法,经过不同的代码示例演示了Spring业务失效的常见场景,假如文章对你有所帮助,欢迎点赞+谈论+收藏❤,我是:‍austin流川枫,咱们下期见~