一、事务基本概念
1.1 事务的四大特性
- 事务的四大特性(ACID): 原子性、一致性、持久性、隔离性
- 原子性(Atomicity):一个事务的操作,要么全部完成,要么全部不完成
- 一致性(Consistency):事务开始之前和事务结束之后,数据库的完整性没有被破坏
- 持久性(Isolation):事务提交后,对数据的修改就是永久的
- 隔离性(Durability):数据库允许多个并发事务同时对其数据读写和修改的能力,隔离性可以防止多个事务并发执行时导致的数据不一致
1.2 事务的隔离级别
事务的隔离级别: 在数据库中,多个事务并发执行时,各个事务之间是相互隔离的,每个事务之间都不能相互干扰。为了保证并发事务的正确性和一致性,数据库管理系统(DBMS)提供了四种事务隔离级别,分别为读未提交、读已提交、可重复读和串行化。
-
读未提交(Read Uncommitted)
在该隔离级别下,一个事务可以读取另一个事务尚未提交的数据,不仅如此,它可能还会读到一些脏数据(Dirty Read)。在该级别下,数据的一致性和完整性得不到保障,不推荐使用。 -
读已提交(Read Committed)
在该隔离级别下,一个事务只能读取另一个事务已经提交的数据,避免了脏读的问题,但是可能会出现不可重复读(Non-Repeatable Read)的问题。即同一事务中,多次读取同一数据可能会得到不同的结果,因为其他事务可能在两次读取之间提交了修改。 -
可重复读(Repeatable Read)
在该隔离级别下,一个事务在执行期间多次读取同一数据时,能够保证所读取的数据一定是事务开始时的状态,避免了不可重复读的问题,但是可能会出现幻读(Phantom Read)问题,即两次查询间新增数据的影响。 -
串行化(Serializable)
在该隔离级别下,所有事务串行执行,避免了脏读、不可重复读和幻读的问题,但是实际应用中可能会导致性能严重下降,因此该级别一般只在特殊情况下使用。
OREACLE 的默认的事务隔离级别为读已提交,mysql 的默认的事务隔离级别为 可重复读。
默认隔离级别为REPEATABLE READ
- 脏读:读取到别的事务未提交的数据;
- 不可重复读:同一事务多次读取,读取到的记录内容不一致
- 幻读:同一事务多次读取到的数据集条数记录不同。强调幻读在于某一个范围内的数据行变多或者是变少了,侧重说明的是数据集不一样导致了产生了幻读
幻读是说数据的条数发生了变化,原本不存在的数据存在了。不可重复读是说数据的内容发生了变化,原本存在的数据的内容发生了改变。
1.3 spring 七个事务的传播机制
Spring在TransactionDefinition接口中规定了7种类型的事务传播行为。Propagation枚举则引用了这些类型,开发过程中我们一般直接用Propagation枚举。 所谓事务传播行为就是多个事务方法相互调用时,事务如何在这些方法间传播。
1.3.1 Propagation源码
public enum Propagation {
REQUIRED(0),
SUPPORTS(1),
MANDATORY(2),
REQUIRES_NEW(3),
NOT_SUPPORTED(4),
NEVER(5),
NESTED(6);
private final int value;
private Propagation(int value) {
this.value = value;
}
public int value() {
return this.value;
}
}
Spring对事务控制的支持统一在TransactionDefinition类中描述,该类有以下 几个重要的接口方法:
- int getPropagationBehavior():事务的传播行为
- int getIsolationLevel():事务的隔离级别
- int getTimeout():事务的过期时间
- boolean isReadOnly():事务的读写特性
除了事务的传播行为外,事务的其他特性Spring是借助底层资源的功能来完成的,Spring无非只充当个代理的角色。但是事务的传播行为却是Spring凭借自身的框架提供的功能 ;
1.3.2、类型解析
- Propagation.REQUIRED(默认机制):如果当前没有事务,就新建一个事务,如果已经存在一个事务,就加入到这个事务中。这是最常见的选择。
- Propagation.SUPPORTS(支持事务):支持当前事务,如果当前没有事务,就以非事务方式执行。
- Propagation.MANDATORY(强制使用事务):使用当前的事务,如果当前没有事务,就抛出异常
- Propagation.REQUIRES_NEW(新建事务):新建事务,如果当前存在事务,把当前事务挂起。
- Propagation.NOT_SUPPORTED(不支持事务):以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。
- Propagation.NEVER(非事务):以非事务方式执行,如果当前存在事务,则抛出异常。
- Propagation.NESTED(事务嵌套):如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则执行与PROPAGATION_REQUIRED类似的操作。。
Spring默认的事务传播行为是PROPAGATION_REQUIRED,它适合绝大多数的情况,如果多个ServiveX#methodX()均工作在事务环境下(即均被Spring事务增强),且程序中存在如下的调用链:Service1#method1()->Service2#method2()->Service3#method3(),那么这3个服务类的3个方法通过Spring的事务传播机制都工作在同一个事务中。
二、spring 中事务的实现
2.1.spring 中事务的使用
Spring中的事务分为两类:
1.编程式事务(手动操作)
2.声明式事务(自动提交事务)
2.1.1 编程式事务
SpringBoot内置了两个对象,DataSourceTransactionManager用来获取事务(开启事务、提交事务、回滚事务)。TransactionDefinition是事务的属性,在获取事务时,需要将TransactionDefinition传递进去获取一个TransactionStatus
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@Autowired
private DataSourceTransactionManager transactionManager;
@Autowired
private TransactionDefinition transactionDefinition;
@RequestMapping("/del")
public int del(Integer id) {
if(id != null && id > 0) {
//开启事务
TransactionStatus transactionStatus =
transactionManager.getTransaction(transactionDefinition);
//删除用户业务操作
int result = userService.del(id);
System.out.println("删除了: " + result);
// 提交事务/回滚事务
// transactionManager.commit(transactionStatus); //提交事务
transactionManager.rollback(transactionStatus); //回滚事务
}
return 0;
}
}
2.1.2 声明式事务
声明式事务我们只需要在方法上@Transactional注解就可以实现,无需手动进行开启事务和提交事务,进入方法自动开启,执行完毕自动提交,发生异常后会自动回滚事务
@RestController
@RequestMapping("/user2")
public class UserController2 {
@Autowired
private UserService userService;
@Transactional
@RequestMapping("/del")
public int del(Integer id) {
if(id == null || id <= 0) {
return 0;
}
return userService.del(id);
}
}
这种是没有异常的异常,我们会进行commit,我们目前数据库数据
2.1.3 @Transactional的使用
@Transactional的作用范围:@Transactional既可以用来修饰方法也可以用来修饰类
1.修饰方法:只能应用到public方法上,否则不生效,推荐用法
2.修饰类:表明该注解对所有的public方法都生效
参数作用
我们可以通过设置@Transactional的一些参数来决定事务的一些具体的功能
2.1.4 Spring 五个事务的隔离级别
Spring的事务隔离级别是有以下5种的
1.DEFAULT:使用底层数据库默认的隔离级别。
2.READ_UNCOMMITTED:允许读取还未提交的数据,会出现脏读、不可重复读和幻读等问题。
3.READ_COMMITTED:只能读取已经提交的数据,避免了脏读的问题,但是可能出现不可重复读和幻读的问题。
4.REPEATABLE_READ:保证同一事务中多次读取同一记录结果是一致的,避免了脏读和不可重复读的问题,但是仍然可能出现幻读的问题。
5.SERIALIZABLE:最高的隔离级别,保证事务串行执行,避免了脏读、不可重复读和幻读等问题,但是影响系统性能。
默认情况下,Spring 会使用底层数据库的默认隔离级别,通常是READ_COMMITTED 级别。可以通过事务管理器的 setDefaultTransactionIsolation() 方法或在事务注解中使用 isolation 属性来设置隔离级别,例如:
@Transactional(isolation = Isolation.DEFAULT)
@RequestMapping("/del")
public int del(Integer id) {
}
2.2.spring 中事务的实现原理
- spring容器在启动的时候会判断是否含有@Transactional 注解,如果有则生基于该类bean的子类代理beanProxy。
- 在业务层调用该@Transactional 注解代理的方法,则被SpringAOP拦截处理,通过SpringAOP处理。
- 2.1 首先spring会调用代理对象,对于事务,代理对象会通过执行事务的切面逻辑,开启事务和数据库连接,设置conn.autocommit 设置为 false。(spring默认是开启了自动提交,当SQL执行结束之后就会提交,当遇到异常的时候,由于前面的事务都已经提升,因此就没法回滚了,所以需要把自动提交给关闭了) 。
- 2.2 子类代理beanProxy调用原来bean 执行业务代码 SQL。最后在通过第一次创建的对象(Spring个生命周期中通过构造方法创造的对象)去执行test方法。接着会去执行SQL语句,在此SQL执行完之后是不会进行提交的,在执行SQL语句之前,jdbcTemplate会去拿到事务管理器创建的这个数据库连接conn。
- 2.3 当执行完test方法后,Spring事务会去判断是否有异常,没有异常就会提交事务(conn.commit()),否者就会事务回滚(conn.rollback());
2.3 spring中事务失效的场景
2.3.1 被非public 修饰符修饰
java的访问权限有4种:private、default、protected、public,它们的权限从左到右,以此变大。如果在开发中,将事务方法定义了错误的访问权限,则事务功能会失效。
@Service
public class EmpService {
@Transactional
private void add(UserModel userModel){
saveData(userModel);
}
}
如上:add方法的权限被定义成了private,这样会导致事务失效,spring要求被代理方法必须是public的。、
在Spring源码中,如果目标方法不是public,则TransactionAttribute返回null,不支持事务。
2.3.2 方法用final修饰
@Service
public class EmpService {
@Transactional
public final void add(UserModel userModel){
saveData(userModel);
}
}
放方法被final修饰时,也会导致事务失效。
因为spring事务底层是用了aop,用了jdk的动态代理或者cglb的动态代理,会帮我们生成代理类,在代理类中实现事务功能。
如果某个方法被final修饰了,那么在代理类中,就无法重新该方法,而添加事务功能。
注意:如果某个方法被static修饰,同样也无法通过动态代理,变成事务方法。
2.3.3 直接调用内部方法
add 方法的事务生效,updateSataus 方法的事务不会生效
@Service
public class EmpService {
@Transactional
public void add(UserModel userModel){
saveData(userModel);
updateSataus(userModel);
}
@Transactional
public void updateSataus(UserModel userModel){
doSomething();
}
}
由上可知:在事务方法add可知,它直接调用了updateStatus方法,由2可知:方法拥有事务的能力是因为spring aop中生成了代理对象,但是直接调用updateStatus方法不会直接生成事务。但是可以直接将该类直接注入进来,比如:
@Service
public class EmpService {
private EmpService empService;
@Transactional
public void add(UserModel userModel){
saveData(userModel);
empService.updateSataus(userModel);
}
@Transactional(rollbackFor = Exception.class)
public void updateSataus(UserModel userModel){
doSomething();
}
}
这样updateSataus方法事务就生效了,add方法的事务也会生效,也不会穿生循环依赖的问题。
2.3.4 未被Spring管理
public class EmpService {
@Transactional
public void add(UserModel userModel){
saveData(userModel);
updateSataus(userModel);
}
}
如图:没有将该类交给spring管理,需要在配置声明类中
@EnableTransactionManagement // 开启事务注解,等同于配置文件tx:annotation-driven/
2.3.5 多线程调用
由以下代码可知:在add()事务方法里面,调用了updateStatus事务方法,但是updateStatus事务方法是在另外一个线程中调用的。这样就导致了两个方法不在同一个线程中,获取到了数据库连接不一样,从而是两个不同的事务,如果updateStatus方法中抛出了异常,add方法是不会回滚的
@Service
public class EmpService {
@Autowired
private OrderService orderService;
@Transactional
public void add(UserModel userModel){
new Thread(()->{
orderService.updateSataus();
}).start();
}
}
@Service
public class OrderService{
@Transactional
public void updateSataus(){
System.out.println("======================");
}
}
spring的事务是通过数据库的连接来实现的。当前线程中保存了一个map,key是数据源,value是数据库连接。同一个事务,指同一个数据库连接,只有拥有同一个事务连接才能保证同时提交和回滚。如果是不同的线程,拿到的数据库连接肯定是不同的。
解决办法,捕获子线程抛出的异常,然后手动回滚
2.3.6 表不支持事务
如果表的引擎是myisam,那么它是不支持事务的,要想支持事务,改成innodb引擎
2.3.7 事务没有开启
如果是springBoot项目,那么是事务默认是开启的,但如果是spring项目,需要xml配置
2.3.8 事务的传播特性
如果事务的传播特性设置错了,事务也会失效。如下:propagation = Propagation.NEVER这种类型的传播特性不支持事务,如果有事务会抛出异常。
目前只有这三种传播特性才会创建新事物:REQUIRED、REQUIRES_NEW、NESTED
@Service
public class EmpService {
@Transactional(propagation = Propagation.NEVER)
public void add(UserModel userModel){
saveData(userModel);
updateSataus(userModel);
}
}
2.3.9 自己吞了异常
事务不会回滚,最常见的问题是:开发者在代码中手动try…catch了异常。如下:
@Service
public class EmpService {
@Transactional
public void add(UserModel userModel){
try {
saveData(userModel);
updateSataus(userModel);
} catch (Exception e) {
e.printStackTrace();
}
}
}
这种情况下,因为开发者自己捕获了异常、又没有手动抛出
2.3.10 手动抛了别的异常
如果抛的异常不正确,事务也不会回滚
@Service
public class EmpService {
@Transactional
public void add(UserModel userModel) throws Exception {
try {
saveData(userModel);
updateSataus(userModel);
} catch (Exception e) {
throw new Exception(e);
}
}
}
因为Spring事务,默认情况下只会回滚RuntimeException(运行时异常)和Error(错误),对于普通的非运行时异常,它不会回滚。
2.4 事务回滚
2.4.1 自定义回滚异常
如果在使用@Transactional注解声明事务时,有时想自定义回滚异常,spring也是支持的。可以通过设置rollbackFor参数,来完成这个功能。如下:
@Service
public class EmpService {
@Transactional(rollbackFor = BusinessException.class)
public void add(UserModel userModel) {
saveData(userModel);
updateSataus(userModel);
}
}
但是如果在程序执行过程中,出现了sql异常,但是sql异常并不属于我们定义的BusinessException异常,所以事务也不会回滚
@Service
public class EmpService {
@Autowired
private OrderService orderService;
@Transactional
public void add(UserModel userModel) {
saveData(userModel);
orderService.updateSataus();
}
}
@Service
public class OrderService(){
@Transactional(propagation = Propagation.NESTED)
public void updateSataus(){
System.out.println("======================");
}
}
如果说对于这样的代码。如果saveData执行成功了,updataStatus执行失败了,那么整个add方法就会回滚。那么之前saveData执行成功的这个方法也会回滚。
那如何只让报错的方法进行回滚呢?只需要将报错的方法抓取异常就OK了。如下:
2.4.2 嵌套事务导致多回滚了怎么办
@Service
public class EmpService {
@Autowired
private OrderService orderService;
@Transactional
public void add(UserModel userModel) {
saveData(userModel);
try {
orderService.updateSataus();
} catch (Exception e) {
e.printStackTrace();
}
}
}
@Service
public class OrderService(){
@Transactional(propagation = Propagation.NESTED)
public void updateSataus(){
System.out.println("======================");
}
}
面试题:A 事务调用B事务方法,如何在B事务方法报错的情况下,A事务继续执行
答:A方法的事务隔离级别为 Propagation.Required ,B方法的事务隔离级别为Propagation.NESTED。A方法手动try catch B的方法,当B的方法报错的时候,不予处理,即可;
2.4.3 手动回滚事务的方法
在Spring中,你可以使用TransactionAspectSupport.currentTransactionStatus()方法来获取当前事务的状态,并手动回滚事务。以下是一个示例代码:
import org.springframework.transaction.interceptor.TransactionAspectSupport;
public void someMethod() {
try {
// 业务逻辑代码
} catch (Exception ex) {
// 手动回滚事务
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
}
}
确保你的方法someMethod是在一个事务的环境中执行的,例如通过@Transactional注解。如果没有当前的事务,currentTransactionStatus()将会抛出异常。
三、事务方法和非事务方法的调用
5.1 同一个类中,普通方法调用事务方法,事务失效
在同一个类中,一个普通方法调用另外一个有注解(比如@Async,@Transational)事务的方法,注解是不会生效的,事务失效;
例子中,有两方法,一个有@Transational注解,一个没有。如果调用了有注解的addPerson()方法,会启动一个Transaction;如果调用updatePersonByPhoneNo(),因为它内部调用了有注解的addPerson(),如果你以为系统也会为它启动一个Transaction,那就错了,实际上是没有的
@Service
public class PersonServiceImpl implements PersonService {
@Autowired
PersonDao personDao;
@Override
@Transactional
public boolean addPerson(Person person) {
boolean result = personDao.insertPerson(person)>0 ? true : false;
return result;
}
@Override
//@Transactional
public boolean updatePersonByPhoneNo(Person person) {
boolean result = personDao.updatePersonByPhoneNo(person)>0 ? true : false;
addPerson(person); //测试同一个类中@Transactional是否起作用
return result;
}
}
原因:
spring 在启动的时候会扫描bean的方法上是否包含@Transactional注解,如果包含,spring会为这个bean动态地生成一个子类(即代理类,proxy),代理类是继承原来那个bean的。此时,当这个有注解的方法被调用的时候,实际上是由代理类来调用的,代理类在调用之前就会启动transaction。然而,如果这个有注解的方法是被同一个类中的其他方法调用的,那么该方法的调用并没有通过代理类,而是直接通过原来的那个bean(即spring本身创建的bean,没有任何代理),所以就不会启动transaction,我们看到的现象就是@Transactional注解无效。
即:Spring的事务管理是通过AOP实现的,其AOP的实现对于非final类是通过cglib这种方式,即生成当前类的一个子类作为代理类,然后在调用其下的方法时,会判断这个方法有没有@Transactional注解,如果有的话,则通过动态代理实现事务管理(拦截方法调用,执行事务等切面)。当b()中调用a()时,发现b()上并没有@Transactional注解,所以整个AOP代理过程(事务管理)不会发生。
简化代码示例
为什么一个方法a()调用同一个类中另外一个方法b()的时候,b()不是通过代理类来调用的呢?可以看下面的例子(为了简化,用伪代码表示):
@Service
class A{
@Transactinal
method b(){...}
method a(){ //标记1
b();
}
}
//Spring扫描注解后,创建了另外一个代理类,并为有注解的方法插入一个startTransaction()方法:
class proxy$A{
A objectA = new A();
method b(){ //标记2
startTransaction();
objectA.b();
closeTransaction();
}
method a(){ //标记3
objectA.a(); //由于a()没有注解,所以不会启动transaction,而是直接调用A的实例的a()方法
}
}
当我们调用A的bean的a()方法的时候,也是被proxyA 拦截,执行 p r o x y A拦截,执行proxyA拦截,执行proxyA.a()(标记3),然而,由以上代码可知,这时候它调用的是objectA.a(),也就是由原来的bean来调用a()方法了,所以代码跑到了“标记1”。由此可见,“标记2”并没有被执行到,所以startTransaction()方法也没有运行。
解决的方法:
1.把这两个方法分开到不同的类中,在启动的时候,spring为两个方法都加载不同的事务代理类;
2.在启动类上加注解 @EnableAspectJAutoProxy(exposeProxy = true)
3.把注解加到类名上面,相当于每个方法都加了事务;
结论:
在一个Service内部,事务方法之间的嵌套调用,普通方法和事务方法之间的嵌套调用,都不会开启新的事务.
spring采用动态代理机制来实现事务控制,而动态代理最终都是要调用原始对象的,而原始对象在去调用方法时,是不会再触发代理了!
Spring的事务管理是通过AOP实现的,其AOP的实现对于非final类是通过cglib这种方式,即生成当前类的一个子类作为代理类,然后在调用其下的方法时,会判断这个方法有没有@Transactional注解,如果有的话,则通过动态代理实现事务管理(拦截方法调用,执行事务等切面)。当b()中调用a()时,发现b()上并没有@Transactional注解,所以整个AOP代理过程(事务管理)不会发生。
5.2 同一个类中,事务方法之间的嵌套调用不会开启新的事务
事务方法之间的嵌套调用,普通方法和事务方法之间的嵌套调用,都不会开启新的事务!
述的情况,说白了,就是在一个Service内部,事务方法之间的嵌套调用,普通方法和事务方法之间的嵌套调用,都不会开启新的事务!
其实通过上面的动态代理的代码,你应该可以发现:
动态代理最终都是要调用原始对象的,而原始对象在去调用方法时,是不会再触发代理了!
那么如何解决呢?
很简单,我们完全可以在抽出一个XxxService,在其内部调用UserService.txMethod()和UserService.txMethod2()方法即可。总而言之,避免在一个Service内部进行事务方法的嵌套调用!(因为动态代理导致这种场景事务失效了。)
四、事务传播实战
4.1 Propagation.REQUIRED
j结论:
- 对于正常的单个方法调用,更新、保存两张以上的表的时候如果不使用事务,会破坏插入数据的一致性;
- 对于嵌套的方法调用。第一个方法都是 Propagation.REQUIRED 事务传播机制,第二个方法没有事务,那么对于第一个方法执行成功后,在调用第二个方法报错的情况下。第一个方法的插入数据不会插入到数据库(异常回滚),第二个方法也不会插入执行成功的数据。只要有调用方法有事事务,被调用事务在没有事务Transactional修饰的情况下也会默认使用调用方法的事务
4.1.1 普通的方法事务插入数据
public String propagationRequired(String s) {
TGoods tGoods = new TGoods();
tGoods.setDescription("ropagationRequired");
tGoods.setCreateTime(new Date());
tGoods.setName("ropagationRequired");
tGoods.setPrice(1111);
tgoodsDao.insert(tGoods);
User user = new User();
userDao.insert(null);
return "ropagationRequired";
}
如果不加@Transactional 注解,在第二张表插入报错的时候会造成第一张表数据插入成功,第二章表数据插入失败
Creating a new SqlSession
SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@231b2d91] was not registered for synchronization because synchronization is not active
JDBC Connection [HikariProxyConnection@1519298501 wrapping com.mysql.cj.jdbc.ConnectionImpl@9ea0dad] will not be managed by Spring
==> Preparing: insert into user(username, mobile, create_by, create_time) values (?, ?, ?, ?)
==> Parameters: null, null, null, null
Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@231b2d91]
2024-05-24 22:51:50.547 ERROR 13284 --- [nio-9005-exec-1] o.a.c.c.C.[.[.[.[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [/amaster-work-server] threw exception [Request processing failed; nested exception is org.springframework.dao.DataIntegrityViolationException:
### Error updating database. Cause: java.sql.SQLIntegrityConstraintViolationException: Column 'username' cannot be null
### The error may exist in file [F:\private\dd\zhang-micro-services\business\amaster-work\amaster-work-server\target\classes\mapper\UserDao.xml]
### The error may involve defaultParameterMap
### The error occurred while setting parameters
### SQL: insert into user(username, mobile, create_by, create_time) values (?, ?, ?, ?)
### Cause: java.sql.SQLIntegrityConstraintViolationException: Column 'username' cannot be null
; Column 'username' cannot be null; nested exception is java.sql.SQLIntegrityConstraintViolationException: Column 'username' cannot be null] with root cause
java.sql.SQLIntegrityConstraintViolationException: Column 'username' cannot be null
at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:117) ~[mysql-connector-java-8.0.15.jar:8.0.15]
at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:97) ~[mysql-connector-java-8.0.15.jar:8.0.15]
at com.mysql.cj.jdbc.exceptions.SQLExceptionsMapping.translateException(SQLExceptionsMapping.java:122) ~[mysql-connector-java-8.0.15.jar:8.0.15]
at com.mysql.cj.jdbc.ClientPreparedStatement.executeInternal(ClientPreparedStatement.java:970) ~[mysql-connector-java-8.0.15.jar:8.0.15]
4.1.2 第一个方法都是 Propagation.REQUIRED 事务传播机制,第二个方法没有事务的插入
@Transactional
public String propagationRequiredNestedCall (String s) {
TGoods tGoods = new TGoods();
tGoods.setDescription("ropagationRequired");
tGoods.setCreateTime(new Date());
tGoods.setName("ropagationRequired");
tGoods.setPrice(66666);
tgoodsDao.insert(tGoods);
this.propagationRequiredNestedCallExcute();
return "propagationRequiredNestedCall";
}
public String propagationRequiredNestedCallExcute() {
TGoods tGoods = new TGoods();
tGoods.setDescription("propagationRequiredNestedCallExcute");
tGoods.setCreateTime(new Date());
tGoods.setName("propagationRequiredNestedCallExcute");
tGoods.setPrice(777777);
tgoodsDao.insert(tGoods);
User user = new User();
userDao.insert(null);
return "propagationRequiredNestedCallExcute";
}
执行结果
Creating a new SqlSession
Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@5aef5338]
JDBC Connection [HikariProxyConnection@1343699016 wrapping com.mysql.cj.jdbc.ConnectionImpl@34b3bf07] will be managed by Spring
==> Preparing: insert into t_goods(name, price, description, create_time, update_time) values (?, ?, ?, ?, ?)
==> Parameters: ropagationRequired(String), 66666(Integer), ropagationRequired(String), 2024-05-25 00:15:31.847(Timestamp), null
<== Updates: 1
Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@5aef5338]
Fetched SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@5aef5338] from current transaction
==> Preparing: insert into t_goods(name, price, description, create_time, update_time) values (?, ?, ?, ?, ?)
==> Parameters: propagationRequiredNestedCallExcute(String), 777777(Integer), propagationRequiredNestedCallExcute(String), 2024-05-25 00:15:34.178(Timestamp), null
<== Updates: 1
Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@5aef5338]
Fetched SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@5aef5338] from current transaction
==> Preparing: insert into user(username, mobile, create_by, create_time) values (?, ?, ?, ?)
==> Parameters: null, null, null, null
Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@5aef5338]
Transaction synchronization deregistering SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@5aef5338]
Transaction synchronization closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@5aef5338]
2024-05-25 00:15:42.573 ERROR 8144 --- [nio-9005-exec-2] o.a.c.c.C.[.[.[.[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [/amaster-work-server] threw exception [Request processing failed; nested exception is org.springframework.dao.DataIntegrityViolationException:
### Error updating database. Cause: java.sql.SQLIntegrityConstraintViolationException: Column 'username' cannot be null
### The error may exist in file [F:\private\dd\zhang-micro-services\business\amaster-work\amaster-work-server\target\classes\mapper\UserDao.xml]
### The error may involve defaultParameterMap
### The error occurred while setting parameters
### SQL: insert into user(username, mobile, create_by, create_time) values (?, ?, ?, ?)
### Cause: java.sql.SQLIntegrityConstraintViolationException: Column 'username' cannot be null
; Column 'username' cannot be null; nested exception is java.sql.SQLIntegrityConstraintViolationException: Column 'username' cannot be null] with root cause
java.sql.SQLIntegrityConstraintViolationException: Column 'username' cannot be null
at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:117) ~[mysql-connector-java-8.0.15.jar:8.0.15]
at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:97) ~[mysql-connector-java-8.0.15.jar:8.0.15]
数据库插入结果,可以看到没有插入数据
4.2 Propagation.SUPPORTS
结论:如果调用 Propagation.SUPPORTS 的方法有事务,那么Propagation.SUPPORTS 的方法就支持事务;如果调用 Propagation.SUPPORTS的方法没有事务,那么Propagation.SUPPORTS 的方法就不支持事务
4.2.1 没有事务的情况下
@Transactional(propagation =Propagation.SUPPORTS)
public String supportsNotTransactional(String s) {
TGoods tGoods = new TGoods();
tGoods.setDescription("propagationsupports");
tGoods.setCreateTime(new Date());
tGoods.setName("propagationsupports");
tGoods.setPrice(2222);
tgoodsDao.insert(tGoods);
User user = new User();
userDao.insert(null);
return "test/supports";
}
执行日志
Creating a new SqlSession
Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@e12250]
JDBC Connection [HikariProxyConnection@1936043538 wrapping com.mysql.cj.jdbc.ConnectionImpl@6342fd64] will be managed by Spring
==> Preparing: insert into t_goods(name, price, description, create_time, update_time) values (?, ?, ?, ?, ?)
==> Parameters: propagationsupports(String), 2222(Integer), propagationsupports(String), 2024-05-24 23:08:29.455(Timestamp), null
<== Updates: 1
Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@e12250]
Fetched SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@e12250] from current transaction
==> Preparing: insert into user(username, mobile, create_by, create_time) values (?, ?, ?, ?)
==> Parameters: null, null, null, null
Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@e12250]
Transaction synchronization deregistering SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@e12250]
Transaction synchronization closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@e12250]
2024-05-24 23:08:29.960 ERROR 14380 --- [nio-9005-exec-1] o.a.c.c.C.[.[.[.[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [/amaster-work-server] threw exception [Request processing failed; nested exception is org.springframework.dao.DataIntegrityViolationException:
### Error updating database. Cause: java.sql.SQLIntegrityConstraintViolationException: Column 'username' cannot be null
### The error may exist in file [F:\private\dd\zhang-micro-services\business\amaster-work\amaster-work-server\target\classes\mapper\UserDao.xml]
### The error may involve defaultParameterMap
### The error occurred while setting parameters
### SQL: insert into user(username, mobile, create_by, create_time) values (?, ?, ?, ?)
### Cause: java.sql.SQLIntegrityConstraintViolationException: Column 'username' cannot be null
; Column 'username' cannot be null; nested exception is java.sql.SQLIntegrityConstraintViolationException: Column 'username' cannot be null] with root cause
java.sql.SQLIntegrityConstraintViolationException: Column 'username' cannot be null
at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:117) ~[mysql-connector-java-8.0.15.jar:8.0.15]
at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:97) ~[mysql-connector-java-8.0.15.jar:8.0.15]
at com.mysql.cj.jdbc.exceptions.SQLExceptionsMapping.translateException(SQLExceptionsMapping.java:122) ~[mysql-connector-java-8.0.15.jar:8.0.15]
at com.mysql.cj.jdbc.ClientPreparedStatement.executeInternal(ClientPreparedStatement.java:970) ~[mysql-connector-java-8.0.15.jar:8.0.15]
插入结果
可以看到ropagation.SUPPORTS 直接使用的时候是没有事务的,所以在插入的时候没有支持事务,在User 插入报错的时候,造成TGoods 插入数据库成功了。
4.2.1 有事务的情况下
@Transactional
public String supportsHaveTransactional(String s) {
TGoods tGoods = new TGoods();
tGoods.setDescription("propagationsupportsHave");
tGoods.setCreateTime(new Date());
tGoods.setName("propagationsupportsHave");
tGoods.setPrice(3333);
tgoodsDao.insert(tGoods);
this.supportsHaveTransactionalExcute();
return "test/supports";
}
@Transactional(propagation =Propagation.SUPPORTS)
public void supportsHaveTransactionalExcute() {
TGoods tGoods = new TGoods();
tGoods.setDescription("supportsHaveTransactional---Excute");
tGoods.setCreateTime(new Date());
tGoods.setName("supportsHaveTransactional--Excute");
tGoods.setPrice(44444);
tgoodsDao.insert(tGoods);
User user = new User();
userDao.insert(null);
}
可以看到supportsHaveTransactional 有默认的事务,supportsHaveTransactionalExcute 为支持事务。当supportsHaveTransactional调用 supportsHaveTransactionalExcute方法的时候有事务,所以supportsHaveTransactionalExcute 执行的时候也是有事务的,所以supportsHaveTransactionalExcute 中插入第二张表的时候tGoods 值为 4444 的这条记录不会插入到数据库中。由于this.supportsHaveTransactionalExcute 插入的时候抛出异常所以supportsHaveTransactional方法在插入tGoods 为333的记录也不会插入成功;
执行日志
Creating a new SqlSession
Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@160467d6]
JDBC Connection [HikariProxyConnection@803423368 wrapping com.mysql.cj.jdbc.ConnectionImpl@55aa0209] will be managed by Spring
==> Preparing: insert into t_goods(name, price, description, create_time, update_time) values (?, ?, ?, ?, ?)
==> Parameters: propagationsupportsHave(String), 3333(Integer), propagationsupportsHave(String), 2024-05-24 23:35:49.214(Timestamp), null
<== Updates: 1
Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@160467d6]
Fetched SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@160467d6] from current transaction
==> Preparing: insert into t_goods(name, price, description, create_time, update_time) values (?, ?, ?, ?, ?)
==> Parameters: supportsHaveTransactional--Excute(String), 44444(Integer), supportsHaveTransactional---Excute(String), 2024-05-24 23:35:55.049(Timestamp), null
<== Updates: 1
Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@160467d6]
Fetched SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@160467d6] from current transaction
==> Preparing: insert into user(username, mobile, create_by, create_time) values (?, ?, ?, ?)
==> Parameters: null, null, null, null
Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@160467d6]
Transaction synchronization deregistering SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@160467d6]
Transaction synchronization closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@160467d6]
2024-05-24 23:35:59.796 ERROR 3280 --- [nio-9005-exec-1] o.a.c.c.C.[.[.[.[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [/amaster-work-server] threw exception [Request processing failed; nested exception is org.springframework.dao.DataIntegrityViolationException:
### Error updating database. Cause: java.sql.SQLIntegrityConstraintViolationException: Column 'username' cannot be null
### The error may exist in file [F:\private\dd\zhang-micro-services\business\amaster-work\amaster-work-server\target\classes\mapper\UserDao.xml]
### The error may involve defaultParameterMap
### The error occurred while setting parameters
### SQL: insert into user(username, mobile, create_by, create_time) values (?, ?, ?, ?)
### Cause: java.sql.SQLIntegrityConstraintViolationException: Column 'username' cannot be null
; Column 'username' cannot be null; nested exception is java.sql.SQLIntegrityConstraintViolationException: Column 'username' cannot be null] with root cause
java.sql.SQLIntegrityConstraintViolationException: Column 'username' cannot be null
at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:117) ~[mysql-connector-java-8.0.15.jar:8.0.15]
at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:97) ~[mysql-connector-java-8.0.15.jar:8.0.15]
at com.mysql.cj.jdbc.exceptions.SQLExceptionsMapping.translateException(SQLExceptionsMapping.java:122) ~[mysql-connector-java-8.0.15.jar:8.0.15]
at com.mysql.cj.jdbc.ClientPreparedStatement.executeInternal(ClientPreparedStatement.java:970) ~[mysql-connector-java-8.0.15.jar:8.0.15]
数据库执行结果
可以看到数据库supportsHaveTransactional 方法的插入和supportsHaveTransactionalExcute 方法在日志中插入成功的结果也没有插入到数据库中,印证了上面的猜想
4.3 Propagation.MANDATORY(强制使用事务)
结论:调用Propagation.MANDATORY 的方法必须要有事务,不然会报错
4.3.1
@Transactional(propagation=Propagation.MANDATORY)
public String mandatoryTransactional(String s) {
TGoods tGoods = new TGoods();
tGoods.setDescription("mandatoryTransactional");
tGoods.setCreateTime(new Date());
tGoods.setName("mandatoryTransactional");
tGoods.setPrice(5555);
tgoodsDao.insert(tGoods);
User user = new User();
user.setCreateTime(new Date());
user.setUsername("lisi");
user.setMobile("21312321321");
userDao.insert(user);
return "mandatoryTransactional";
}
日志
2024-05-24 23:51:35.112 ERROR 19140 --- [nio-9005-exec-1] o.a.c.c.C.[.[.[.[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [/amaster-work-server] threw exception [Request processing failed; nested exception is org.springframework.transaction.IllegalTransactionStateException: No existing transaction found for transaction marked with propagation 'mandatory'] with root cause
org.springframework.transaction.IllegalTransactionStateException: No existing transaction found for transaction marked with propagation 'mandatory'
at org.springframework.transaction.support.AbstractPlatformTransactionManager.getTransaction(AbstractPlatformTransactionManager.java:362) ~[spring-tx-5.2.8.RELEASE.jar:5.2.8.RELEASE]
at org.springframework.transaction.interceptor.TransactionAspectSupport.createTransactionIfNecessary(TransactionAspectSupport.java:574) ~[spring-tx-5.2.8.RELEASE.jar:5.2.8.RELEASE]
at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:361) ~[spring-tx-5.2.8.RELEASE.jar:5.2.8.RELEASE]
at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:118) ~[spring-tx-5.2.8.RELEASE.jar:5.2.8.RELEASE]
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186) ~[spring-aop-5.2.8.RELEASE.jar:5.2.8.RELEASE]
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:749) ~[spring-aop-5.2.8.RELEASE.jar:5.2.8.RELEASE]
at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:691) ~[spring-aop-5.2.8.RELEASE.jar:5.2.8.RELEASE]
at com.zhang.buiness.amaster.workserver.service.WorkTransactionalServiceImpl$$EnhancerBySpringCGLIB$$1dc41d04.mandatoryTransactional(<generated>) ~[classes/:na]
at com.zhang.buiness.amaster.workserver.service.WorkTransactionalServiceImpl$$FastClassBySpringCGLIB$$4c0f1875.invoke(<generated>) ~[classes/:na]
at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218) ~[spring-core-5.2.8.RELEASE.jar:5.2.8.RELEASE]
数据库也是没有插入记录的
4.3.2 @Transactional(propagation=Propagation.MANDATORY) 方法调用没有@Transactional事务的方法
可以看到没有@Transactional事务的方法 插入的使用也是默认使用事务的
@Transactional
public String mandatoryTransactionalNestedCall(String s) {
this.mandatoryTransactionalNestedCallB();
return "mandatoryTransactionalNestedCall";
}
@Transactional(propagation=Propagation.MANDATORY)
public void mandatoryTransactionalNestedCallB() {
this.mandatoryTransactionalNestedCallC();
}
public void mandatoryTransactionalNestedCallC() {
TGoods tGoods = new TGoods();
tGoods.setDescription("mandatoryTransactionalNestedCallC");
tGoods.setCreateTime(new Date());
tGoods.setName("mandatoryTransactionalNestedCallC");
tGoods.setPrice(888);
tgoodsDao.insert(tGoods);
User user = new User();
userDao.insert(null);
}
执行日志
Creating a new SqlSession
Registering transaction synchronization for SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@53407c99]
JDBC Connection [HikariProxyConnection@1704205209 wrapping com.mysql.cj.jdbc.ConnectionImpl@18b1f281] will be managed by Spring
==> Preparing: insert into t_goods(name, price, description, create_time, update_time) values (?, ?, ?, ?, ?)
==> Parameters: mandatoryTransactionalNestedCallC(String), 888(Integer), mandatoryTransactionalNestedCallC(String), 2024-05-25 00:30:32.542(Timestamp), null
<== Updates: 1
Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@53407c99]
Fetched SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@53407c99] from current transaction
==> Preparing: insert into user(username, mobile, create_by, create_time) values (?, ?, ?, ?)
==> Parameters: null, null, null, null
Releasing transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@53407c99]
Transaction synchronization deregistering SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@53407c99]
Transaction synchronization closing SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@53407c99]
2024-05-25 00:30:37.471 ERROR 24288 --- [nio-9005-exec-1] o.a.c.c.C.[.[.[.[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [/amaster-work-server] threw exception [Request processing failed; nested exception is org.springframework.dao.DataIntegrityViolationException:
### Error updating database. Cause: java.sql.SQLIntegrityConstraintViolationException: Column 'username' cannot be null
### The error may exist in file [F:\private\dd\zhang-micro-services\business\amaster-work\amaster-work-server\target\classes\mapper\UserDao.xml]
### The error may involve defaultParameterMap
### The error occurred while setting parameters
### SQL: insert into user(username, mobile, create_by, create_time) values (?, ?, ?, ?)
### Cause: java.sql.SQLIntegrityConstraintViolationException: Column 'username' cannot be null
; Column 'username' cannot be null; nested exception is java.sql.SQLIntegrityConstraintViolationException: Column 'username' cannot be null] with root cause
java.sql.SQLIntegrityConstraintViolationException: Column 'username' cannot be null
at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:117) ~[mysql-connector-java-8.0.15.jar:8.0.15]
at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:97) ~[mysql-connector-java-8.0.15.jar:8.0.15]
执行结果 可以看到没有插入结果