1、什么是事务
事务是数据库保证原子性的一种机制,所有的操作要么都成功,要么都失败。了解事务,一定绕不开的他的四个特点:ACID
- A(代表了事务的原子性)
- C(代表了事务的一致性)
- I(代表了事务的隔离性)
- D(代表了事务的持久性)
2、控制事务原理
使用JDBC的事务,我们控制事务的手段是Connection.setAutoCommit(false)开启事务,Connection.commit()提交事务,Connection.rollback()回滚事务
我们使用Mybatis用SqlSession.commit(),和SqlSession.rollback(),其实这都是MyBatis的SqlSession对Connection的封装,他的底层还是调用的Connection.commit()方法
控制事务的底层原理,就是使用Connection来进行操作
3、Spring如何控制事务
事务,其实和核心业务没有太大的关系,是一个可有可无的功能(对于业务)。所以,我们可以使用AOP来做这件事情,Spring也是通过AOP来实现这个控制业务的
既然是AOP,还是那四个步骤:
- 原始对象
原始对象就是我们具体的service业务逻辑的实现类
public class UserServiceImpl implements UserService { private UserDao userDao; //get、set方法,通过spring的注入,将userDao注入进来 public void save(User user) { userDao.save(user); } }
- 额外功能
额外功能有两种实现的方法,一种是实现MethodInterceptor接口,实现invoke方法;一种是通过@Aspect注解来实现
public class Around implements MethodInterceptor { @Override public Object invoke(MethodInvocation methodInvocation) throws Throwable { Object proceed ; try { //开启事务 proceed = methodInvocation.proceed(); //提交事务 }catch (RuntimeException e){ //回滚事务 } return proceed ; } }
这一段代码,Spring也给我们封装了一个类,DataSourceTransactionManager,这个类就是帮助我们控制事务的!因为事务的底层是用Connection来完成的,所以我们需要注入连接池。
- 切入点
Spring为我们提供了一个注解作为切入点@Transactional,这个注解可以放在类上,也可以放在方法上。
- 组装切面
切面 = 额外功能 + 切入点
<tx:annotation-driven transaction-manager=""></tx:annotation-driven>
4、Spring控制事务代码演示
引入maven依赖
<dependency> <groupId>org.springframework</groupId> <artifactId>spring-tx</artifactId> <version>5.1.14.RELEASE</version> </dependency>
接口
public interface UserService { void save(User user); }
原始类(实现类)
public class UserServiceImpl implements UserService { private UserDao userDao; public UserDao getUserDao() { return userDao; } public void setUserDao(UserDao userDao) { this.userDao = userDao; } public void save(User user) { userDao.save(user); } }
因为userDao这个对象,我们需要通过spring来进行注入,配置文件
<bean id="userSerivce" class="com.wx.mybatis.service.impl.UserServiceImpl"> <property name="userDao" ref="userDao"></property> </bean>
额外功能
Spring提供了DataSourceTransactionManager类,所以我们直接注入DataSource连接池即可
<!-- 额外功能--> <bean id="dataSourceTransactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"></property> </bean>
切入点
直接在类上添加@Transactional注解,作为切入点
@Transactional public class UserServiceImpl implements UserService { private UserDao userDao; public UserDao getUserDao() { return userDao; } public void setUserDao(UserDao userDao) { this.userDao = userDao; } public void save(User user) { userDao.save(user); } }
切面(切面 = 切入点 + 额外功能)
<tx:annotation-driven transaction-manager="dataSourceTransactionManager"></tx:annotation-driven>
完整的applicationContext.xml配置文件
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:tx="http://www.springframework.org/schema/tx" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/tx https://www.springframework.org/schema/tx/spring-tx.xsd"> <!-- 连接池--> <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"> <property name="driverClassName" value="com.mysql.jdbc.Driver"> </property> <property name="url" value="jdbc:mysql://localhost:3306/user?useSSL=false"></property> <property name="username" value="root"></property> <property name="password" value="root"></property> </bean> <!--创建SqlsessionFactoryBean--> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="dataSource"></property> <!-- 执行实体对应的包--> <property name="typeAliasesPackage" value="com.wx.mybatis.entity"></property> <property name="mapperLocations"> <list> <value>classpath*:mapper/*Mapper.xml</value> </list> </property> </bean> <bean id="scanner" class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"></property> <!--设置dao所在的包路径--> <property name="basePackage" value="com.wx.mybatis.dao"></property> </bean> <!-- 额外功能--> <bean id="dataSourceTransactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"></property> </bean> <bean id="userSerivce" class="com.wx.mybatis.service.impl.UserServiceImpl"> <property name="userDao" ref="userDao"></property> </bean> <tx:annotation-driven transaction-manager="dataSourceTransactionManager"></tx:annotation-driven> </beans>
测试
2021-04-29 22:50:19 DEBUG SqlSessionUtils:99 - Creating a new SqlSession 2021-04-29 22:50:19 DEBUG SqlSessionUtils:130 - Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6e9175d8] 2021-04-29 22:50:19 DEBUG SpringManagedTransaction:89 - JDBC Connection [com.mysql.jdbc.JDBC4Connection@51c668e3] will be managed by Spring 2021-04-29 22:50:19 DEBUG save:137 - ==> Preparing: insert into t_user(name, password) values (?, ?) 2021-04-29 22:50:19 DEBUG save:137 - ==> Parameters: zhaoliu(String), 123(String) 2021-04-29 22:50:19 DEBUG save:137 - <== Updates: 1 2021-04-29 22:50:19 DEBUG SqlSessionUtils:188 - Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6e9175d8] 2021-04-29 22:50:19 DEBUG SqlSessionUtils:286 - Transaction synchronization committing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6e9175d8] 2021-04-29 22:50:19 DEBUG SqlSessionUtils:312 - Transaction synchronization deregistering SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6e9175d8] 2021-04-29 22:50:19 DEBUG SqlSessionUtils:317 - Transaction synchronization closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6e9175d8] 2021-04-29 22:50:19 DEBUG DataSourceTransactionManager:743 - Initiating transaction commit 2021-04-29 22:50:19 DEBUG DataSourceTransactionManager:326 - Committing JDBC transaction on Connection [com.mysql.jdbc.JDBC4Connection@51c668e3] 2021-04-29 22:50:19 DEBUG DataSourceTransactionManager:384 - Releasing JDBC Connection [com.mysql.jdbc.JDBC4Connection@51c668e3] after transaction Process finished with exit code 0
通过输出的日志,可以看出插入成功了,DataSourceTransactionManager类也确实帮助我们提交了事务
那么我们再来证明下,我们的事务是在UserServiceImpl的save()方法上,而不是dao层中。我们可以在service层中抛出一个异常,看是否会回滚事务
@Transactional public class UserServiceImpl implements UserService { private UserDao userDao; public UserDao getUserDao() { return userDao; } public void setUserDao(UserDao userDao) { this.userDao = userDao; } public void save(User user) { userDao.save(user); throw new RuntimeException("异常了"); } }
2021-04-29 22:56:54 DEBUG SqlSessionUtils:99 - Creating a new SqlSession 2021-04-29 22:56:54 DEBUG SqlSessionUtils:130 - Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6e9175d8] 2021-04-29 22:56:54 DEBUG SpringManagedTransaction:89 - JDBC Connection [com.mysql.jdbc.JDBC4Connection@51c668e3] will be managed by Spring 2021-04-29 22:56:54 DEBUG save:137 - ==> Preparing: insert into t_user(name, password) values (?, ?) 2021-04-29 22:56:54 DEBUG save:137 - ==> Parameters: zhaoliu(String), 123(String) 2021-04-29 22:56:54 DEBUG save:137 - <== Updates: 1 2021-04-29 22:56:54 DEBUG SqlSessionUtils:188 - Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6e9175d8] 2021-04-29 22:56:54 DEBUG SqlSessionUtils:312 - Transaction synchronization deregistering SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6e9175d8] 2021-04-29 22:56:54 DEBUG SqlSessionUtils:317 - Transaction synchronization closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6e9175d8] 2021-04-29 22:56:54 DEBUG DataSourceTransactionManager:836 - Initiating transaction rollback 2021-04-29 22:56:54 DEBUG DataSourceTransactionManager:341 - Rolling back JDBC transaction on Connection [com.mysql.jdbc.JDBC4Connection@51c668e3] 2021-04-29 22:56:54 DEBUG DataSourceTransactionManager:384 - Releasing JDBC Connection [com.mysql.jdbc.JDBC4Connection@51c668e3] after transaction Exception in thread "main" java.lang.RuntimeException: 异常了
我们还可以设置我们的DataSourceTransactionManager的代理方式,是采用默认的jdk还是cglib代理
<tx:annotation-driven transaction-manager="dataSourceTransactionManager" proxy-target-class="false"></tx:annotation-driven>
5、Spring的事务属性
5.1、什么是事务属性
事务的五大特性:
- 隔离属性
- 传播属性
- 只读属性
- 超时属性
- 异常属性
5.2、添加事务
Spring的@Transactional注解就可以帮助我们添加事务
@Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Inherited @Documented public @interface Transactional { @AliasFor("transactionManager") String value() default ""; @AliasFor("value") String transactionManager() default ""; //传播属性 Propagation propagation() default Propagation.REQUIRED; //隔离属性 Isolation isolation() default Isolation.DEFAULT; //超时属性 int timeout() default -1; //只读属性 boolean readOnly() default false; //异常属性 Class<? extends Throwable>[] rollbackFor() default {}; String[] rollbackForClassName() default {}; //异常属性 Class<? extends Throwable>[] noRollbackFor() default {}; String[] noRollbackForClassName() default {}; }
6、隔离属性
隔离属性主要是为了解决并发下的问题,那么并发下会出现哪些问题呢?
脏读,幻读,不可重复读
那么出现这些问题,我们应该如何处理呢?这是时候我们就需要设置我们的隔离级别,来解决这个问题
6.1脏读
一个事务,读取到了另一个事务没有提交的数据
时间 事务A 事务B T1 开始事务 T2 开始事务 T3 查询余额(余额1000元) T4 取出余额1000元(余额0元) T5 查询余额(余额0元) T6 撤销事务,回滚余额(余额100元) T7 存入500元(月500元) T8 提交事务
我们可以看到,最后的结果是500元,而不是1500元。这个结果显然是错误的。造成这个问题的原因是,事务A读取到了事务B还没有提交的数据,这个就是脏读。
解决脏读的办法,可以在@Transactional注解中加上@Transactional(isolation = Isolation.READ_COMMITTED),将隔离级别设置为提交读,避免脏数据的发生。
6.2不可重复读
同一个事务中,多次读取相同的数据,但是读取到的结果不一样,在本事务中产生的数据不一致的问题
时间 事务A 事务B T1 开始事务 T2 开始事务 T3 查询余额(余额100元) T4 查询余额(余额1000元) T5 取出1000元(余额0元) T6 提交事务 T7 查询余额(余额0元) 事务A就查询了两次余额,其他的什么也没有干,但是两次查询出来的数据却不相同。因为事务B取出钱了后,将事务提交了,所以这个并不是脏数据,因为他无法回滚
解决不可重复读的办法,可以在@Transactional注解中加上@Transactional(isolation = Isolation.REPEATABLE_READ),将隔离级别设置为不可重复读,避免这种问题的发生。底层原理是对这行数据加上一把行级锁。保证在同一个事务中多次查询同一条数据,结果是相同的。
6.3幻读
一个事务中,多次对整表进行统计,但是结果不一样
时间 事务A 事务B T1 开始事务 T2 开始事务 T3 统计总存款1000元 T4 存入100元 T5 提交事务 T6 统计总存款1100元 事务A对表中多条数据继续统计,两次的结果不一致,这是由于其他的线程提交了事务导致的。
解决幻读的办法,可以在@Transactional注解中加上@Transactional(isolation = Isolation.SERIALIZABLE),将隔离级别设置为序列化。底层的原理是使用表锁。一个线程访问,就会将表进行锁住,其他线程都无法操作,只能等到锁释放才可以操作。
6.4默认隔离级别
我们使用注解的默认隔离级别
默认的隔离级别,是根据数据库的不同而设置不同
所以,这个离级别是和数据库相关的!
小结:
- 推荐使用spring设置的默认隔离级别,连接不同的数据库,使用的是不同的隔离级别
- 真的遇到了并发的情况,可以采用乐观锁,乐观锁是应用层面的解决方案,如果是隔离级别,属于物理隔离,会大大影响我们的数据库效率
7、传播属性
传播属性,用来解决事务嵌套问题
事务嵌套:是指线程A中嵌套着线程B这种情况
比如:我们的service业务类中,调用了其他service业务类中的方法。而这两个方法都加上了注解事务,这就形成了事务嵌套
业务场景:TA中调用了TB和TC两个方法,TB正常执行结束;并且提交了事务,TC出现了异常,进行了回滚,但是TB已经提交了事务,无法进行回滚。这就是典型的事务嵌套,一个大事务中包含了若干个小事务,从而无法保证大事务的原子性。
8、传播属性的值与用法
传播属性的值 外部不存在事务 外部存在事务 用法 备注 REQUIRED 开启新的事务 融合到外部事务中 @Transactional(propagation = Propagation.REQUIRED) 增删改方法 SUPPORTS 不开启事务 融合到外部事务中 @Transactional(propagation = Propagation.SUPPORTS) 查询方法 REQUIRES_NEW 开启新的事务 挂起外部事务,创建新的事务 @Transactional(propagation = Propagation.REQUIRES_NEW) NOT_SUPPORTED 不开启事务 挂起外部事务 @Transactional(propagation = Propagation.NOT_SUPPORTED) NEVER 不开启事务 抛出异常 @Transactional(propagation = Propagation.NEVER) MANDATORY 抛出异常 融合到外部事务中 @Transactional(propagation = Propagation.MANDATORY) 推荐日后开发使用
- 增删改:直接使用默认的Required
- 查询:显示指定默认值Supports
9、只读属性
针对于只是查询的业务逻辑,我们可以添加上只读属性,来提高效率
之前我们为了保证并发的安全性,选择了事务,底层一定会使用到各种各样的锁,只要使用锁,就会影响我们的性能。那加上了只读属性,就不会添加各种各样的锁,从而提高我们的效率
@Transactional(propagation = Propagation.MANDATORY, readOnly = true) public class UserServiceImpl implements UserService { private UserDao userDao; public UserDao getUserDao() { return userDao; } public void setUserDao(UserDao userDao) { this.userDao = userDao; } public void save(User user) { userDao.save(user); // throw new RuntimeException("异常了"); } }
如果不加这个注解,默认值是false
10、超时属性
如果线程A占用了数据库行锁,线程B就需要等待线程A释放锁,那么这个等待的时间,就是超时等待
我们设置超时属性为2s,我们在执行save的时候睡眠3s
@Transactional(timeout = 2) public class UserServiceImpl implements UserService { private UserDao userDao; public UserDao getUserDao() { return userDao; } public void setUserDao(UserDao userDao) { this.userDao = userDao; } public void save(User user) { try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } userDao.save(user); // throw new RuntimeException("异常了"); } }
演示效果:
如果不指定这个值,spring给的默认值是-1,表示这个值最后由数据库来决定。我们一般不会去设置这个值,就用spring的默认值即可
11、异常属性
在spring的事务异常中,只要是RuntimeException异常,就会进行回滚
@Transactional(timeout = 2) public class UserServiceImpl implements UserService { private UserDao userDao; public UserDao getUserDao() { return userDao; } public void setUserDao(UserDao userDao) { this.userDao = userDao; } public void save(User user) throws Exception { userDao.save(user); throw new Exception("eee"); } }
现象
我们可以看到Exception异常,直接提交了
- 想让RuntimeException不提交,直接回滚,就这么设置 rollbackFor = {java.lang.Exception,xxx,xxx}
- 想让Exception提交,不回滚,这么设置 noRollbackFor = {java.lang.RuntimeException,xxx,xx}
实际开发过程中,使用默认值即可
12、总结
隔离属性使用默认值
传播属性增删改使用Required(默认值),查询使用Supports
只读属性onlyread=false增删改,true为查询
超时属性默认值-1
异常属性默认值RuntimeException回滚
增删改@Transactional()
查询@Transactional(propagation = Propagation.SUPPORTS, readOnly = true)