Spring 事务详解 | JavaGuide(Java面试 + 学习指南)
面试突击89:事务隔离级别和传播机制有什么区别? (qq.com)
什么是事务
事务是指一组数据库操作,要么全部执行,要么全部不执行。在事务的控制下,一组操作要么全部成功,要么全部失败,不会出现部分成功、部分失败的情况。
Java Spring框架中的事务是指对数据库操作的一组操作,通过对这组操作的控制,保证操作的一致性和完整性。
注意!事务能否生效数据库引擎是否支持事务是关键。比如常用的 MySQL 数据库默认使用支持事务的
innodb
引擎。但是,如果把数据库引擎变为myisam
,那么程序也就不再支持事务了!
事务的特性(ACID)
- 原子性(
Atomicity
):事务是最小的执行单位,不允许分割。事务的原子性确保动作要么全部完成,要么完全不起作用; - 一致性(
Consistency
):执行事务前后,数据保持一致,例如转账业务中,无论事务是否成功,转账者和收款人的总额应该是不变的; - 隔离性(
Isolation
):并发访问数据库时,一个用户的事务不被其他事务所干扰,各并发事务之间数据库是独立的; - 持久性(
Durability
):一个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库发生故障也不应该对其有任何影响。
只有保证了事务的持久性、原子性、隔离性之后,一致性才能得到保障。
A、I、D 是手段,C 是目的!
MySQL 怎么保证原子性的?
在 MySQL 中,恢复机制是通过 回滚日志(undo log) 实现的,所有事务进行的修改都会先记录到这个回滚日志中,然后再执行相关的操作。如果执行过程中遇到异常的话,我们直接利用 回滚日志 中的信息将数据回滚到修改之前的样子即可!并且,回滚日志会先于数据持久化到磁盘上。这样就保证了即使遇到数据库突然宕机等情况,当用户再次启动数据库的时候,数据库还能够通过查询回滚日志来回滚之前未完成的事务。
@Transactional
的常用配置参数总结
(只列出了 5 个平时比较常用的):
属性名 | 说明 |
---|---|
propagation | 事务的传播行为,默认值为 REQUIRED,可选的值在上面介绍过 |
isolation | 事务的隔离级别,默认值采用 DEFAULT,可选的值在上面介绍过 |
timeout | 事务的超时时间,默认值为-1(不会超时)。如果超过该时间限制但事务还没有完成,则自动回滚事务。 |
readOnly | 指定事务是否为只读事务,默认值为 false。 |
rollbackFor | 用于指定能够触发事务回滚的异常类型,并且可以指定多个异常类型。 |
@Transactional
事务注解原理
面试中在问 AOP 的时候可能会被问到的一个问题。简单说下吧!
我们知道,@Transactional
的工作机制是基于 AOP 实现的,AOP 又是使用动态代理实现的。如果目标对象实现了接口,默认情况下会采用 JDK 的动态代理,如果目标对象没有实现了接口,会使用 CGLIB 动态代理。
如果一个类或者一个类中的 public 方法上被标注@Transactional
注解的话,Spring 容器就会在启动的时候为其创建一个代理类,在调用被@Transactional
注解的 public 方法的时候,实际调用的是,TransactionInterceptor
类中的 invoke()
方法。这个方法的作用就是在目标方法之前开启事务,方法执行过程中如果遇到异常的时候回滚事务,方法调用完成之后提交事务。
Spring 事务管理方式
编程式事务管理
通过 TransactionTemplate
或者TransactionManager
手动管理事务,实际应用中很少使用,但是对于你理解 Spring 事务管理原理有帮助。
使用TransactionTemplate
进行编程式事务管理的示例代码如下:
@Autowired
private TransactionTemplate transactionTemplate;
public void testTransaction() {
transactionTemplate.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus transactionStatus) {
try {
// .... 业务代码
} catch (Exception e){
//回滚
transactionStatus.setRollbackOnly();
}
}
});
}
使用 TransactionManager
进行编程式事务管理的示例代码如下:
@Autowired
private PlatformTransactionManager transactionManager;
public void testTransaction() {
TransactionStatus status = transactionManager.getTransaction(new DefaultTransactionDefinition());
try {
// .... 业务代码
transactionManager.commit(status);
} catch (Exception e) {
transactionManager.rollback(status);
}
}
声明式事务管理
推荐使用(代码侵入性最小),实际是通过 AOP 实现(基于@Transactional
的全注解方式使用最多)。
使用 @Transactional
注解进行事务管理的示例代码如下:
@Transactional(propagation = Propagation.REQUIRED)
public void aMethod {
//do something
B b = new B();
C c = new C();
b.bMethod();
c.cMethod();
}
Spring 事务隔离级别
Spring 中的事务隔离级别有 5 种,它们分别是:
- DEFAULT:Spring 中默认的事务隔离级别,以连接的数据库的事务隔离级别为准;
- READ_UNCOMMITTED:读未提交,也叫未提交读,该隔离级别的事务可以看到其他事务中未提交的数据。// 该隔离级别因为可以读取到其他事务中未提交的数据,而未提交的数据可能会发生回滚,因此我们把该级别读取到的数据称之为脏数据,把这个问题称之为脏读;
- READ_COMMITTED:读已提交,也叫提交读,该隔离级别的事务能读取到已经提交事务的数据,因此它不会有脏读问题。// 但由于在事务的执行中可以读取到其他事务提交的结果,所以在不同时间的相同 SQL 查询中,可能会得到不同的结果,这种现象叫做不可重复读;
- REPEATABLE_READ:可重复读,它能确保同一事务多次查询的结果一致。// 但也会有新的问题,比如此级别的事务正在执行时,另一个事务成功的插入了某条数据,但因为它每次查询的结果都是一样的,所以会导致查询不到这条数据,自己重复插入时又失败(因为唯一约束的原因)。明明在事务中查询不到这条信息,但自己就是插入不进去,这就叫幻读 (Phantom Read);
- SERIALIZABLE:串行化,最高的事务隔离级别,它会强制事务排序,使之不会发生冲突,从而解决了脏读、不可重复读和幻读问题,但因为执行效率低,所以真正使用的场景并不多。
- 脏读:一个事务读取到了另一个事务修改的数据之后,后一个事务又进行了回滚操作,从而导致第一个事务读取的数据是错误的。
- 不可重复读:一个事务两次查询得到的结果不同,因为在两次查询中间,有另一个事务把数据修改了。
- 幻读:一个事务两次查询中得到的结果集不同,因为在两次查询中另一个事务有新增了一部分数据。
Spring 中,事务隔离级别可以通过 @Transactional(isolation = Isolation.DEFAULT) 来设置。
Spring 事务传播机制
**Spring 事务传播机制是指,包含多个事务的方法在相互调用时,事务是如何在这些方法间传播的。**Spring 事务传播机制可使用 @Transactional(propagation=Propagation.REQUIRED) 来设置,Spring 事务传播机制的级别包含以下 7 种:
- Propagation.REQUIRED:默认的事务传播级别,它表示如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。
- Propagation.SUPPORTS:如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。
- Propagation.MANDATORY:(mandatory:强制性)如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。
- Propagation.REQUIRES_NEW:表示创建一个新的事务,如果当前存在事务,则把当前事务挂起。也就是说不管外部方法是否开启事务,Propagation.REQUIRES_NEW 修饰的内部方法会新开启自己的事务,且开启的事务相互独立,互不干扰。
- Propagation.NOT_SUPPORTED:以非事务方式运行,如果当前存在事务,则把当前事务挂起。
- Propagation.NEVER:以非事务方式运行,如果当前存在事务,则抛出异常。
- Propagation.NESTED:如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于 PROPAGATION_REQUIRED。
以上 7 种传播机制,可分为以下 3 类:
Spring 事务只读
package org.springframework.transaction;
import org.springframework.lang.Nullable;
public interface TransactionDefinition {
......
// 返回是否为只读事务,默认值为 false
boolean isReadOnly();
}
对于只有读取数据查询的事务,可以指定事务类型为 readonly,即只读事务。只读事务不涉及数据的修改,数据库会提供一些优化手段,适合用在有多条数据库查询操作的方法中。
很多人就会疑问了,为什么我一个数据查询操作还要启用事务支持呢?
MySQL 默认对每一个新建立的连接都启用了
autocommit
模式。在该模式下,每一个发送到 MySQL 服务器的sql
语句都会在一个单独的事务中进行处理,执行结束后会自动提交事务,并开启一个新的事务。
但是,如果你给方法加上了Transactional
注解的话,这个方法执行的所有sql
会被放在一个事务中。如果声明了只读事务的话,数据库就会去优化它的执行,并不会带来其他的什么收益。
如果不加Transactional
,每条sql
会开启一个单独的事务,中间被其它事务改了数据,都会实时读取到最新值。
分享一下关于事务只读属性,其他人的解答:
- 如果你一次执行单条查询语句,则没有必要启用事务支持,数据库默认支持 SQL 执行期间的读一致性;
- 如果你一次执行多条查询语句,例如统计查询,报表查询,在这种场景下,多条查询 SQL 必须保证整体的读一致性,否则,在前条 SQL 查询之后,后条 SQL 查询之前,数据被其他用户改变,则该次整体的统计查询将会出现读数据不一致的状态,此时,应该启用事务支持。
Spring 事务超时
所谓事务超时,就是指一个事务所允许执行的最长时间,如果超过该时间限制但事务还没有完成,则自动回滚事务。在 TransactionDefinition
中以 int 的值来表示超时时间,其单位是秒,默认值为-1,这表示事务的超时时间取决于底层事务系统或者没有超时时间
Spring 事务回滚规则
这些规则定义了哪些异常会导致事务回滚而哪些不会。默认情况下,事务只有遇到运行期异常(RuntimeException
的子类)时才会回滚,Error
也会导致事务回滚,但是,在遇到检查型(Checked)异常时不会回滚。
如果你想要回滚你定义的特定的异常类型的话,可以这样:
@Transactional(rollbackFor= MyException.class)
事务失效10场景
10 种 Spring 事务失效的常见场景,你知道几个? (qq.com)
1. 事务方法未被Spring管理
如果事务方法所在的类没有注册到Spring IOC容器中,也就是说,事务方法所在类并没有被Spring管理,则Spring事务会失效,举个例子🌰:
/**
* 商品业务实现层
*/
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);
}
}
OrderServiceImpl
的cancel取消订单方法被final修饰符修饰,Spring事务底层使用了AOP,也就是通过JDK动态代理或者cglib,帮我们生成了代理类,在代理类中实现的事务功能。但如果某个方法用final修饰了,那么在它的代理类中,就无法重写该方法,从而无法添加事务功能。这种情况事务就会在Spring中失效。
💡Tips: 如果某个方法是static的,同样无法通过动态代理将方法声明为事务方法。
3. 非public修饰的方法
如果事务方式不是public修饰,此时Spring事务会失效,举个例子🌰:
/**
* 商品业务实现层
*
*/
@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 Boot 中只要引入了 spring-boot-starter-data-jdbc
启动器依赖就会自动配置DataSourceTransactionManager数据源事务管理器,所以 Spring Boot框架不存在这个问题,但在传统的 Spring 框架中需要注意。
如果是以往的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来保证线程安全的,事务和当前线程绑定,多个线程自然会让事务失效。