Spring事务详解
文章目录
什么是事务
- 事务是逻辑上的一组操作,要么都执行,要么都不执行。
savePerson()
中就有两个原子性的数据库操作。这些原子性的数据库操作是有依赖的,它们要么都执行,要不就都不执行。
public void savePerson() {
personDao.save(person);
personDetailDao.save(personDetail);
}
事务能否生效数据库引擎是否支持事务是关键,
MySQL
数据库默认使用支持事务的innodb
引擎
事务实际业务
- 例如银行存钱的时候,假如你的钱已经进入了
Atm
中但是,后面你查询余额的时候发现,你的钱并没有存进去。 - 假如你给某人转账,你的账户显示转账成功,但是对方并没有收到钱,这种重要的操作就应该考虑使用事务操作。
事务的特性(ACID)
- 原子性(Atomicity): 一个事务(
transaction
)中的所有操作,或者全部完成,或者全部不完成,不会结束在中间某个环节。事务在执行过程中发生错误,会被回滚(Rollback
)到事务开始前的状态,就像这个事务从来没有执行过一样。即,事务不可分割、不可约简。 - 一致性(Consistency): 在事务开始之前和事务结束以后,数据库的完整性没有被破坏。这表示写入的资料必须完全符合所有的预设约束、触发器、级联回滚等。
- 隔离性(Isolation): 数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。事务隔离分为不同级别,包括未提交读(
Read uncommitted
)、提交读(read committed
)、可重复读(repeatable read
)和串行化(Serializable
)。 - 持久性(Durability): 事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。
事务隔离级别
- 面对不同的事务
- 脏读:读到的数据是未确认的数据 ( 未提交 ) , 如果另一个客户端在操作过程中,回滚了操作,那么用户读到的数据其实是无效数据。
- 不可重复读:指一个客户端在同一个事务中多次读取相同的数据 , 结果不一致
- 幻读:一个客户端多次读取相同的数据,每次得到的结果都跟第一次得到的数据一样,但其实数据已经发生了变化.但是查不到结果。
隔离级别 | 说明 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|---|
DEFAULT | 使用数据库的默认级别,Mysql (可重复读),Oracle (读已提交) | |||
READ_UNCOMMITTED | 读未提交(脏读)最低的隔离级别 | 是 | 是 | 是 |
READ_COMMITTED | 读已提交,ORACLE 默认隔离级别,有幻读以及不可重复读风险 | 否 | 是 | 是 |
REPEATABLE_READ | 可重复读,解决不可重复读的隔离级别,但还是有幻读风险 | 否 | 否 | 是 |
SERIALIZABLE | 最高的事务隔离级别,不管多少事务,挨个运行完一个事务的所有子事务之后才可以执行另外一个事务里面的所有子事务,这样就解决了脏读、不可重复读和幻读的问题了 | 否 | 否 | 否 |
常用数据库事务
数据库 | 类型 | 支持事务 |
---|---|---|
Oracle | 关系型数据库 | 是 |
MySQL | 关系型数据库 | 是 |
Microsoft SQL Server | 关系型数据库 | 是 |
PostgreSQL | 关系型数据库 | 是 |
MongoDB | 文档数据库 | 是 |
IBM Db2 | 关 系型数据库 | 是 |
Spring 对事务的支持
编程式事务管理
使用TransactionTemplate
或者使用底层的PlatformTransactionManager
使用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
之上,对方法前后进行拦截,然后方法开始之前创建或者加入一个事务,在执行完目标方法后更具执行情况提交或者回滚事务。声明式事务一种基于tx
和aop
的xml
配置文件,另一种是基于@Transactional
注解。
Spring 事务管理接口
Spring
框架中,事务管理相关最重要的 3
个接口如下:
PlatformTransactionManager
: (平台)事务管理器,Spring
事务策略的核心。TransactionDefinition
: 事务定义信息(事务隔离级别、传播行为、超时、只读、回滚规则)。TransactionStatus
: 事务运行状态。
我们可以把 PlatformTransactionManager
接口可以被看作是事务上层的管理者,而 TransactionDefinition
和 TransactionStatus
这两个接口可以看作是事务的描述。
PlatformTransactionManager
会根据 TransactionDefinition
的定义比如事务超时时间、隔离级别、传播行为等来进行事务管理 ,而 TransactionStatus
接口则提供了一些方法来获取事务相应的状态比如是否新事务、是否可以回滚等等。
PlatformTransactionManager:事务管理接口
Spring 并不直接管理事务,而是提供了多种事务管理器 。Spring
事务管理器的接口是: PlatformTransactionManager
。
通过这个接口,Spring
为各个平台如 JDBC
(DataSourceTransactionManager
)、Hibernate
(HibernateTransactionManager
)、JPA
(JpaTransactionManager
)等都提供了对应的事务管理器,但是具体的实现就是各个平台自己的事情了。
PlatformTransactionManager
接口的具体实现如下:
PlatformTransactionManager
接口中定义了三个方法:
package org.springframework.transaction;
import org.springframework.lang.Nullable;
public interface PlatformTransactionManager {
//获得事务
TransactionStatus getTransaction(@Nullable TransactionDefinition var1) throws TransactionException;
//提交事务
void commit(TransactionStatus var1) throws TransactionException;
//回滚事务
void rollback(TransactionStatus var1) throws TransactionException;
}
PlatformTransactionManager
接口:将事务管理行为抽象出来,然后不同的平台去实现它,保证提供给外部的行为不变,方便扩展。
TransactionDefinition:事务属性
事务管理器接口 PlatformTransactionManager
通过 getTransaction(TransactionDefinition definition)
方法来得到一个事务,这个方法里面的参数是 TransactionDefinition
类 ,这个类就定义了一些基本的事务属性:事务的一些基本配置,描述了事务策略如何应用到方法上。
TransactionDefinition.java
:接口中定义了5
个方法以及一些表示事务属性的常量比如隔离级别、传播行为等等
public interface TransactionDefinition {
int PROPAGATION_REQUIRED = 0;
int PROPAGATION_SUPPORTS = 1;
int PROPAGATION_MANDATORY = 2;
int PROPAGATION_REQUIRES_NEW = 3;
int PROPAGATION_NOT_SUPPORTED = 4;
int PROPAGATION_NEVER = 5;
int PROPAGATION_NESTED = 6;
int ISOLATION_DEFAULT = -1;
int ISOLATION_READ_UNCOMMITTED = 1;
int ISOLATION_READ_COMMITTED = 2;
int ISOLATION_REPEATABLE_READ = 4;
int ISOLATION_SERIALIZABLE = 8;
int TIMEOUT_DEFAULT = -1;
// 返回事务的传播行为,默认值为 REQUIRE
default int getPropagationBehavior() {
return 0;
}
//返回事务的隔离级别,默认值是 DEFAULT
default int getIsolationLevel() {
return -1;
}
// 返回事务的超时时间,默认值为-1。如果超过该时间限制但事务还没有完成,则自动回滚事务。
default int getTimeout() {
return -1;
}
// 返回是否为只读事务,默认值为 false
default boolean isReadOnly() {
return false;
}
@Nullable
default String getName() {
return null;
}
static TransactionDefinition withDefaults() {
return StaticTransactionDefinition.INSTANCE;
}
}
TransactionStatus:事务状态
TransactionStatus
接口用来记录事务的状态 该接口定义了一组方法,用来获取或判断事务的相应状态信息。
PlatformTransactionManager.getTransaction(…)
方法返回一个 TransactionStatus
对象。
TransactionStatus.java
public interface TransactionStatus extends TransactionExecution{
// 是否有恢复点
boolean hasSavepoint();
}
public interface TransactionExecution {
// 是否是新的事务
boolean isNewTransaction();
// 设置为只回滚
void setRollbackOnly();
// 是否为只回滚
boolean isRollbackOnly();
// 是否已完成
boolean isCompleted();
}
事务传播机制
Spring
提供七种级别的事务传播机制,在开发中可以结合实际业务需要进行使用。
事务传播行为类型 | 说明 |
---|---|
REQUIRED | 如果当前没有事务,就新建一个事务,如果已经存在一个事务中,加入到这个事务中。默认是这个级别。 |
SUPPORTS | 如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。 |
MANDATORY | 如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。 |
REQUIRES_NEW | 新建事务,如果当前存在事务,把当前事务挂起 |
NOT_SUPPORTED | 以非事务方式执行操作,如果当前存在事务,就把当前事务挂起 |
NEVER | 以非事务方式执行,如果当前存在事务,则抛出异常 |
NESTED | 和REQUIRES_NEW 区别是,所有事务成功才提交,只要一个事务失败都回滚 |
@Transactional注解
- 方法加上
@Transactional(rollbackFor=Exception.class)
肯定会回滚。 - 如果
@Transactional
不加属性那么事务只有在遇到RuntimeException
的时候才会回滚。
@Override
public boolean rollbackOn(Throwable ex) {
return (ex instanceof RuntimeException || ex instanceof Error);
}
@Transactional 的作用范围
- 方法 :推荐将注解使用于方法上,不过需要注意的是:该注解只能应用到
public
方法上,否则不生效。 - 类 :如果这个注解使用在类上的话,表明该注解对该类中所有的
public
方法都生效。 - 接口 :不推荐在接口上使用。
@Transaction注解常用参数
参数名称 | 功能描述 |
---|---|
readOnly | 该属性用于设置当前事务是否为只读事务,设置为true 表示只读,false 则表示可读写,默认值为false 。例如:@Transactional(readOnly=true) |
rollbackFor | 该属性用于设置需要进行回滚的异常类数组,当方法中抛出指定异常数组中的异常时,则进行事务回滚。例如:指定单一异常类:@Transactional(rollbackFor=RuntimeException.class) 指定多个异常类:@Transactional(rollbackFor={RuntimeException.class, Exception.class}) |
rollbackForClassName | 该属性用于设置需要进行回滚的异常类名称数组,当方法中抛出指定异常名称数组中的异常时,则进行事务回滚。例如:指定单一异常类名称:@Transactional(rollbackForClassName="RuntimeException") 指定多个异常类名称:@Transactional(rollbackForClassName={"RuntimeException","Exception"}) |
noRollbackFor | 该属性用于设置不需要进行回滚的异常类数组,当方法中抛出指定异常数组中的异常时,不进行事务回滚。例如:指定单一异常类:@Transactional(noRollbackFor=RuntimeException.class) 指定多个异常类:@Transactional(noRollbackFor={RuntimeException.class, Exception.class}) |
noRollbackForClassName | 该属性用于设置不需要进行回滚的异常类名称数组,当方法中抛出指定异常名称数组中的异常时,不进行事务回滚。例如:指定单一异常类名称:@Transactional(noRollbackForClassName="RuntimeException") 指定多个异常类名称:@Transactional(noRollbackForClassName={"RuntimeException","Exception"}) |
propagation | 该属性用于设置事务的传播行为 |
isolation | 该属性用于设置底层数据库的事务隔离级别,事务隔离级别用于处理多事务并发的情况,通常使用数据库的默认隔离级别即可,基本不需要进行设置 |
timeout | 该属性用于设置事务的超时秒数,默认值为-1表示永不超时 |
@Transactional 事务注解原理
- 基于
AOP
实现的,AOP
又是使用动态代理实现的。如果目标对象实现了接口,默认情况下会采用JDK
的动态代理,如果目标对象没有实现了接口,会使用CGLIB
动态代理。
public class DefaultAopProxyFactory implements AopProxyFactory, Serializable {
public DefaultAopProxyFactory() {
}
public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException {
if (NativeDetector.inNativeImage() || !config.isOptimize() && !config.isProxyTargetClass() && !this.hasNoUserSuppliedProxyInterfaces(config)) {
return new JdkDynamicAopProxy(config);
} else {
Class<?> targetClass = config.getTargetClass();
if (targetClass == null) {
throw new AopConfigException("TargetSource cannot determine target class: Either an interface or a target is required for proxy creation.");
} else {
return (AopProxy)(!targetClass.isInterface() && !Proxy.isProxyClass(targetClass) ? new ObjenesisCglibAopProxy(config) : new JdkDynamicAopProxy(config));
}
}
}
}
- 如果一个类或者一个类中的
public
方法上被标注@Transactional
注解的话,Spring
容器就会在启动的时候为其创建一个代理类,在调用被@Transactional
注解的public
方法的时候,实际调用的是,TransactionInterceptor
类中的invoke()
方法。这个方法的作用就是在目标方法之前开启事务,方法执行过程中如果遇到异常的时候回滚事务,方法调用完成之后提交事务。
invokeWithinTransaction()
Spring AOP 自调用问题
- 若同一类中的其他没有
@Transactional
注解的方法内部调用有@Transactional
注解的方法,有@Transactional
注解的方法的事务会失效。 - 这是由于
Spring AOP
代理的原因造成的,因为只有当@Transactional
注解的方法在类以外被调用的时候,Spring
事务管理才生效。
// MyService 类中的method1()调用method2()就会导致method2()的事务失效。
@Service
public class MyService {
private void method1() {
method2();
//......
}
@Transactional
public void method2() {
//......
}
}
@Transactional 的使用注意事项总结
@Transactional
注解只有作用到public
方法上事务才生效,不推荐在接口上使用;- 避免同一个类中调用
@Transactional
注解的方法,这样会导致事务失效; - 正确的设置
@Transactional
的rollbackFor
和propagation
属性,否则事务可能会回滚失败; - 被
@Transactional
注解的方法所在的类必须被Spring
管理,否则不生效; - 底层使用的数据库必须支持事务机制,否则不生效;
@Transactional 示例代码
try catch捕获异常
- 在执行的过程中,异常被捕获了所以,无法进行回滚。
@RequestMapping("/test1")
@Transactional(rollbackFor = Exception.class)
public String test1() {
try {
Menu menu = new Menu();
menu.setName("事务测试2");
menu.setFlavor("事务测试2");
menu.setPrice((double) 34);
menu.setCount(23);
menu.setId(2);
menuRepository.update(menu);
int a=10/0;
return "sucess";
} catch (Exception e) {
e.printStackTrace();
return "fail";
}
}
- 要想在
try catch
中也能回滚操作,只需要在catch
中加上如下的语句。此时不管有没有异常都会进行回滚操作。
@RequestMapping("/test1")
@Transactional(rollbackFor = Exception.class)
public String test1() {
try {
menuRepository.update(menu);
int a=10/0;
return "sucess";
} catch (Exception e) {
e.printStackTrace();
//只要抛异常则回滚
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
return "fail";
}
}
不捕获异常
@Transaction
加上roolbackfor
参数,会进行回滚操作。
@RequestMapping("/test2")
@ApiOperation(value = "事务回滚测试2", notes = "不捕获异常不抛出异常")
@Transactional(rollbackFor = Exception.class)
public String test2() {
menuRepository.update(menu);
int a = 10 / 0;
return "sucess";
}
- 抛出
RuntimeException
,事务进行回滚
@RequestMapping("/test3")
@ApiOperation(value = "事务回滚测试3", notes = "不加参数")
@Transactional
public String test3() {
menuRepository.update(menu);
int a = 10 / 0;
throw new RuntimeException();
}
- 抛出其他异常事务不回滚
@RequestMapping("/test3")
@Transactional
public String test4() {
menuRepository.update(menu);
int a = 10 / 0;
throw new ArithmeticException();
}
SpringBoot 手动提交事务示例代码
@Api("事务手动提交测试")
@RestController
@RequestMapping("/thing")
public class TransactionsController {
@Autowired
MenuRepository menuRepository;
@Autowired
DataSourceTransactionManager dataSourceTransactionManager;
@Autowired
TransactionDefinition transactionDefinition;
@RequestMapping("/test1")
@ApiOperation(value = "事务回滚测试1", notes = "加上Transactional的属性")
public String test1() {
TransactionStatus transactionStatus = null;
try {
//手动开启事务
transactionStatus = dataSourceTransactionManager.getTransaction(transactionDefinition);
Menu menu = new Menu();
menu.setName("事务测试2");
menu.setFlavor("事务测试2");
menu.setPrice((double) 34);
menu.setCount(23);
menu.setId(2);
menuRepository.update(menu);
//手动提交事务
dataSourceTransactionManager.commit(transactionStatus);
int a = 10 / 0;
return "sucess";
} catch (Exception e) {
if (transactionStatus != null) {
//手动回滚
dataSourceTransactionManager.rollback(transactionStatus);
}
e.printStackTrace();
return "fail";
}
}
}
Mybatis事务
- 在
mybatis
中提供了两种事务管理机制:JDBC(jdbc)
MANAGED(managed)
Jdbc 事务管理器
mybatis
框架自己管理事务,自己采用原生的JDBC
代码去管理事务:- 使用
JDBC
事务管理器的话,底层创建的事务管理器对象JdbcTransaction
对象。
protected void openConnection() throws SQLException {
if (log.isDebugEnabled()) {
log.debug("Opening JDBC Connection");
}
this.connection = this.dataSource.getConnection();
if (this.level != null) {
this.connection.setTransactionIsolation(this.level.getLevel());
}
// 设置是否自动提交事务
this.setDesiredAutoCommit(this.autoCommit);
}
// 提交和回滚自己处理
@Override
public void commit() throws SQLException {
if (connection != null && !connection.getAutoCommit()) {
if (log.isDebugEnabled()) {
log.debug("Committing JDBC Connection [" + connection + "]");
}
connection.commit();
}
}
@Override
public void rollback() throws SQLException {
if (connection != null && !connection.getAutoCommit()) {
if (log.isDebugEnabled()) {
log.debug("Rolling back JDBC Connection [" + connection + "]");
}
connection.rollback();
}
}
Managed事务管理器
mybatis
不再负责事务的管理了,事务管理交给其它容器来负责。例如:spring
- 对于我们当前的单纯的只有
mybatis
的情况下,如果配置为MANAGED
,那么事务这块是没人管的。没有人管理事务表示事务压根没有开启。 SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH,false);
执行过程
// openSession
@Override
public SqlSession openSession(ExecutorType execType, boolean autoCommit) {
return openSessionFromDataSource(execType, null, autoCommit);
}
private SqlSession openSessionFromDataSource(ExecutorType execType, TransactionIsolationLevel level, boolean autoCommit) {
Transaction tx = null;
try {
final Environment environment = configuration.getEnvironment();
final TransactionFactory transactionFactory = getTransactionFactoryFromEnvironment(environment);
tx = transactionFactory.newTransaction(environment.getDataSource(), level, autoCommit);
final Executor executor = configuration.newExecutor(tx, execType);
return new DefaultSqlSession(configuration, executor, autoCommit);
} catch (Exception e) {
closeTransaction(tx); // may have fetched a connection so lets call close()
throw ExceptionFactory.wrapException("Error opening session. Cause: " + e, e);
} finally {
ErrorContext.instance().reset();
}
}
@Override
public Transaction newTransaction(DataSource ds, TransactionIsolationLevel level, boolean autoCommit) {
// Silently ignores autocommit and isolation level, as managed transactions are entirely
// controlled by an external manager. It's silently ignored so that
// code remains portable between managed and unmanaged configurations.
return new ManagedTransaction(ds, level, closeConnection);
}
protected void openConnection() throws SQLException {
if (log.isDebugEnabled()) {
log.debug("Opening JDBC Connection");
}
this.connection = this.dataSource.getConnection();
if (this.level != null) {
this.connection.setTransactionIsolation(this.level.getLevel());
}
}
// 提交回滚没有具体的实现
@Override
public void commit() throws SQLException {
// Does nothing
}
@Override
public void rollback() throws SQLException {
// Does nothing
}
事务提交报错
两个不同的事务提交报错
- 操作代码:通过不同的事务更新一张表中的数据
@Override
@Transactional(rollbackFor = Throwable.class)
public void transactionFour() {
User user = new User();
user.setUsername("张三");
user.setId(1);
userMapper.updateUser(user);
DataSource dataSource = SpringContextUtils.getBean(DataSource.class);
Connection connection = null;
try {
connection = dataSource.getConnection();
connection.prepareStatement("update user set username ='王五', gender='2' where id ='1'").executeUpdate();
} catch (SQLException throwables) {
logger.error("Error Occur:{}", throwables.getMessage(), throwables);
}
}
- 报错如下:
------------------------------------------------------------------------------------------------------------------
2021-07-17 14:13:51.532 ERROR 22436 --- [io-8001-exec-10] c.l.s.s.t.TransactionServiceImpl : Error Occur:Lock wait timeout exceeded; try restarting transaction
com.mysql.jdbc.exceptions.jdbc4.MySQLTransactionRollbackException: Lock wait timeout exceeded; try restarting transaction
at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method) ~[na:1.8.0_201]
at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62) ~[na:1.8.0_201]
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45) ~[na:1.8.0_201]
at java.lang.reflect.Constructor.newInstance(Constructor.java:423) ~[na:1.8.0_201]
- 错误原因:对同一张表没有进行原子性的操作发生锁表。
- 解决思路:想操作放在同一个原子操作内。
- 解决方案:手动提交第一个事务,第二个事务读已经提交的数据
/**
* 手动提交事务
* 读已经提交的数据
*/
@Override
public void transactionFive() {
TransactionStatus transaction = transactionManager.getTransaction(transactionDefinition);
User user = new User();
user.setUsername("张三");
user.setId(1);
userMapper.updateUser(user);
// 手动提交
transactionManager.commit(transaction);
updateUserOne(user);
}
public void updateUserOne(User user) {
DataSource dataSource = SpringContextUtils.getBean(DataSource.class);
Connection connection = null;
try {
connection = dataSource.getConnection();
connection.setAutoCommit(true);
// 设置为读已经提交了的
connection.setTransactionIsolation(Connection.TRANSACTION_READ_COMMITTED);
connection.prepareStatement("update user set username ='王五', gender='2' where id ='1'").executeUpdate();
} catch (SQLException throwables) {
logger.error("Error Occur:{}", throwables.getMessage(), throwables);
} finally {
if (Objects.nonNull(connection)) {
try {
connection.close();
} catch (SQLException throwables) {
logger.error("Error Occur:{}", throwables.getMessage(), throwables);
}
}
}
}