Spring是web框架,它其实并不与MySQL数据库进行直连,一般都是通过ORM层框架,诸如 JDBC
、 iBatis
、 MyBatis
、 Hibernate
、 JPA
等,与数据库进行连接的,而事务的管理都是交由各持久层框架自行处理的,Spring只是出具了事务管理器接口 org.springframework.transaction.PlatformTransactionManager
及不同的 PlatformTransactionManager
实现类,为不同的持久层框架提供对应的实现类,将管理事务的职责下放到各持久层框架,交由持久层框架处理。
org.springframework.jdbc.datasource.DataSourceTransactionManager
:使用 JDBC
、 iBatis
、 MyBatis
进行持久化数据时使用 org.springframework.orm.hibernate5.HibernateTransactionManager
:使用 Hibernate5
版本进行持久化数据时使用 org.springframework.orm.jpa.JpaTransactionManager
:使用 JPA
进行持久化数据时使用 org.springframework.jdo.JdoTransactionManager
:当持久化机制是 jdo
时使用 org.springframework.transaction.jta.JtaTransactionManager
:使用一个 JTA
实现来管理事务,在一个事务跨越多个资源时必须使用 我不知道该怎么称呼它,因为自从工作以来,也没有遇到过,但是不代表人家不存在,也许很老的项目里依然存在。主要依靠手动处理来管理事务,对业务代码有侵入性。
1)编程式,可以使用 TransactionTemplate
或者直接使用底层的 PlatformTransactionManager
。对于编程式事务管理,Spring推荐使用 TransactionTemplate
代码不做展示,太冗余了
声明式事务本质是建立在AOP基础之上的,在代理类实例里拦截目标方法,在方法执行前,开启或者加入一个事务,执行完毕或者发生异常后,根据具体情况进行提交或者回滚。
相比较编程式事务,其最大的特点就是对业务代码没有侵入性,只需在配置文件中对事务进行配置管理(或者基于 @Transactional
注解的方式)即可,这样就可以将业务代码和事务管理隔离开。
1) TransactionProxyFactoryBean
声明式事务管理
此方式是最传统,配置文件最臃肿、难以阅读的一种方式
代码不做展示,太冗余了
2) @Transactional
注解事务管理
基于注解的事务管理,在需要开启事务的类或方法上添加 @Transactional
,这种方式比较灵活,随用随加,但是容易到处乱用,凌乱不堪。更偏重灵活性。
<!-- 开启注解管理事务 --> <tx:annotation-driven transaction-manager="transactionManager"/> 复制代码
3) Aspectj AOP
配置事务管理
<!-- 事务AOP --> <aop:config> <!-- pointcut:切入点 aop:advisor 适配器,是要注入的方法和pointcut连接的桥梁 --> <aop:advisor pointcut="execution(* cn.x.x..*.service..*.*(..))" advice-ref="txAdvice" /> </aop:config> <!-- 需要注入的方法,可以使用通配符进行匹配 --> <tx:advice id="txAdvice" transaction-manager="transactionManager"> <tx:attributes> <tx:method name="get*" isolation="REPEATABLE_READ" propagation="REQUIRED" read-only="true" /> <tx:method name="insert*" isolation="REPEATABLE_READ" propagation="REQUIRED" rollback-for="Exception"/> <tx:method name="update" isolation="REPEATABLE_READ" propagation="REQUIRED" rollback-for="Exception"/> <tx:method name="delete*" isolation="REPEATABLE_READ" propagation="REQUIRED" rollback-for="Exception"/> </tx:attributes> </tx:advice> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource" /> </bean> 复制代码
首先重温一下:在上一篇 《MySQL事务及隔离级别》
里,我们介绍了MySQL事务如果没有设置隔离级别,并发可能产生的几个安全问题:
一些大佬,对此设计出了数据库事务的隔离级别,来确保根据具体情况,适时适当选用对应的策略,这种策略我们称之为 事务的隔离级别
,如下:
<tx:method name="insert*" isolation="REPEATABLE_READ" propagation="REQUIRED" rollback-for="Exception" read-only="false" no-rollback-for="RuntimeException" timeout="xxx"/> 复制代码
可以使用通配符(*)关联一批相同事务属性的方法
默认值:DEFAULT
默认值:REQUIRED
新建一个嵌套事务
,该事务可以有多个回滚点,如果没有事务,则按 REQUIRED
属性执行,此方法对应的事务回滚不会对外部(嵌套)事务造成影响,但是外部事务回滚会影响内部事务。 默认值 -1
挺有意思的一个小东西,如果项目里使用 MyBatis
,即使设置了超时时间,也是没有任何效果的,暂时的解决办法是,使用MyBatis的超时设置或者使用 JdbcTemplate
进行数据库的操作。
默认值 false
事务是否只读
如果当前事务是只读,且在事务内进行增、删、改操作,则会抛出异常(Cannot execute statement in a READ ONLY transaction。)
只读属性有什么用?可以有多个,以逗号相隔,如果是自定义异常,则需要类的完全限定名,我查阅很多资料,发现大家都趋向于下面的结论:
如果发生的是检查时异常,那么事务不会自动回滚,如果是运行时异常,事务会自动回滚
经过实际测试后发现,如果我们指定 rollback-for="Exception"
,那么,抛出检查型异常一样可以触发事务的回滚。
官方文档给出的信息是:
默认情况下,检查时异常不会引起事务回滚,但是RuntimeException及其子类会引起事务回滚
和上面的类似,请自我揣摩~
<!-- 事务AOP --> <aop:config> <!-- pointcut:切入点 aop:advisor 适配器,是要注入的方法和pointcut连接的桥梁 --> <aop:advisor pointcut="execution(* x.x..*.service..*.*(..))" advice-ref="txAdvice" /> </aop:config> <!-- 需要注入的方法,可以使用通配符进行匹配 --> <tx:advice id="txAdvice" transaction-manager="transactionManager"> <tx:attributes> <tx:method name="get*" isolation="REPEATABLE_READ" propagation="REQUIRED" read-only="true" /> <tx:method name="insert*" isolation="REPEATABLE_READ" propagation="REQUIRED" rollback-for="Exception"/> </tx:attributes> </tx:advice> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource" /> </bean> 复制代码
public interface StudentService { List<Student> getStudents(String name); void insertStudent(Student student); deleteStudent(int id); } 复制代码
@Service public class StudentServiceImpl implements StudentService { @Autowired private StudentDao studentDao; @Override public List<Student> getStudents(String name) { Map<String, Object> params = Maps.newHashMap(); params.put("name", name); return studentDao.getStudents(params); } @Override public void insertStudent(Student student) { studentDao.insert(student); } @Override public void deleteStudent(int id) { studentDao.delete(id); } } 复制代码
@RestController @RequestMapping("/students") public class StudentController { @Autowired private StudentService studentService; @GetMapping("/test") public void run() { student.getStudents("张三"); Student student = Student.builder() .id(1) .name("张三") .age(19) .build(); student.insertStudent(student); student.deleteStudent(student.getId()); } } 复制代码
如上,StudentService 的 insert和get方法均开启了事务,但是deleteStudent方法没有开启事务。
问题:
1)在 deleteStudent()
里调用 insertStudent()
,试问, insertStudent()
是否开启事务?为什么?
@Service public class StudentServiceImpl { @Autowired private StudentDao studentDao; public void insertStudent(Student student) { studentDao.insert(student); } public void deleteStudent(int id) { // 删除逻辑代码 // 调用 insertStudent(student); } } 复制代码
答案:
不开启事务
① 当 StudentService.java
文件进行编译的时候,编译器会将 deleteStudent()
方法里的 insertStudent(student);
编译成 this.insertStudent(student);
② Controller层使用 @Autowired
注解进行 StudentService
的自动装配,但装配的实例并不是StudentServiceImpl类的实例,而是代理类的实例! 声明式事务是建立在AOP基础上的
,而AOP的实现原理就是代理模式!所以我们在Controller层所调用的方法会被代理类拦截。
③ 在代理类的拦截方法内,如果发现要调用的目标类方法是要开启事务的,那么就会在调用目标类方法之前开启事务,在目标类方法执行完毕或者发生异常后,根据情况执行回滚或不执行回滚。回归正题,当代理类发现 deleteStudent()
没有开启事务的要求,那么代理类是不会开启事务的。
当我们在 deleteStudent()
里调用 insertStudent()
时,重点来了,jvm执行的是 this.insertStudent()
这个this是目标类本身,而不是代理类!但是事务的管理却在代理类里,所以, insertStudent()
方法并不会开启事务!
思考:
通过上面的解读,我们知道了service上的事务是由代理类来管理的,类似的像@Async异步标签,其实也是由代理类进行管理的,在service内部调用打上@Async标签的内部方法,异步也不会起作用的。
如果想调用内部方法,还想让事务/异步起作用,我们可以使用:xml配置方式暴露代理对象,通过代理对象AopContext.currentProxy()去调用方法。
xml:
<aop:aspectj-autoproxy proxy-target-class="true" expose-proxy="true"/> 复制代码
@Service public class StudentServiceImpl { @Autowired private StudentDao studentDao; public void insertStudent(Student student) { studentDao.insert(student); } public void deleteStudent(int id) { // 删除逻辑代码 // 调用 (StudentService) AopContext.currentProxy()).insertStudent(student); } } 复制代码
2)在 insertStudent()
里调用 deleteStudent()
,试问,如果 deleteStudent()
抛出运行时异常, insertStudent()
对应的操作是否回滚?为什么?
@Service public class StudentServiceImpl { @Autowired private StudentDao studentDao; public void insertStudent(Student student) { studentDao.insert(student); deleteStudent(student.getId()); } public void deleteStudent(int id) { studentDao.delete(id); if (1 == 1) throw new RuntimeException("测试回滚"); } } 复制代码
答案:
回滚
① 当 StudentService.java
文件进行编译的时候, insertStudent(student)
方法里的 deleteStudent();
变成 this.deleteStudent();
,没毛病,继续。
② Controller层调用 insertStudent()
时,会开启事务,一样没毛病,继续。
③ 在 insertStudent()
里调用 this.deleteStudent()
,相当于将 deleteStudent()
的代码块加入到当前事务中,所以如果 deleteStudent()
抛出运行时异常,那么 insertStudent()
对应的操作会回滚。