文章目录
- Spring事务和事务的传播机制
- 1. 为什么需要事务?
- 2. Spring中事务的实现
- 3. Spring声明事务(自动事务)
- 1)@Transactional作用范围和参数说明
- 2)事务失效场景
- 对异常进行捕获
- 特定异常
- aop切面顺序导致事务不能正确回滚
- 方法不是public
- 没有开启事务
- 方法被final修饰
- 直接调用本类方法导致传播行为失效
- @Transactional 没有保证原子行为
- @Transactional ⼯作原理
- 4. 事务的隔离级别
- 1) 事务特性
- 2) Spring中设置事务隔离级别
- 3) Spring中5种事务隔离级别
- 5. 事务的传播机制
- 1) 为啥需要传播机制?
- 2) Spring中的事务传播机制
Spring事务和事务的传播机制
1. 为什么需要事务?
事务:将一组操作捆绑在一起,封装成一个执行单元,这一组操作要么全部成功,要么全部失败。
假设有一个转账程序,A用户给B用户转了1000块钱,当A账户扣了1000块时,服务器突然断电了,于是A账户余额减少了1000块,当B账户余额并没有增加,那么这1000块钱不就平白无故消失了?所有这个转账中余额减少和余额增加这一组操作,要么全部成功,要么全部失败。
2. Spring中事务的实现
回顾一下MySQL中的事务(在MySQL5.7中)
-- 开启事务
start transaction;
-- 业务执行
......
-- 提交事务
commit;
-- 回滚事务
rollback;
Spring手动操作事务
Spring手动操作事务和MySQL中手动操作事务类似,它也是有3个重要操作步骤:
- 开启事务(获取事务)
- 提交事务
- 回滚事务
SpringBoot内置了两个对象,DataSourceTransactionManager用来获取事务(开启事务)、提交或者是回滚事务。而TransactionDefinition是事务的属性,在获取事务的时候需要将TransactionDefinition传递进去从而获得事务TransactionStatus.
代码示例:
只要在新增用户的过程中发生异常就会进行回滚
@Service
public class UserServiceImpl implements UserService {@Autowiredprivate UserMapper userMapper;// JDBC 事务管理器@Resourceprivate DataSourceTransactionManager dataSourceTransactionManager;// 定义事务属性@Resourceprivate TransactionDefinition transactionDefinition;/*** 注册功能* @param username* @param password* @return*/public ResponseState reg(String username,String password) {ResponseState responseState = new ResponseState();responseState.setSuccess(200);// 开启事务TransactionStatus transactionStatus = dataSourceTransactionManager.getTransaction(transactionDefinition);// 插入数据库int ret = userMapper.reg(username,password);// 提交事务dataSourceTransactionManager.commit(transactionStatus);// 回滚事务dataSourceTransactionManager.rollback(transactionStatus);return responseState;}}
但是上面这种方式实现事务是比较麻烦的,可以使用更简单的声明式事务。
3. Spring声明事务(自动事务)
声明式事务的实现很简单,只需要再需要的方法上添加@Transactional
注解就可以实现了,无需手动的开启事务和提交事务,进入方法时自动开启事务,方法执行完后自动提交事务,如果中途发生了没有处理的异常就会自动回滚
代码示例:
@Transactional
public ResponseState reg(String username,String password) {ResponseState responseState = new ResponseState();responseState.setSuccess(200);int ret = userMapper.reg(username,password);if (ret == 1) {responseState.setState(1);responseState.setMessage("注册成功");} else {responseState.setState(-1);responseState.setMessage("注册失败");}}// 此处发生算数异常int a = 10/0;return responseState;
}
当方法内发送异常且不处理就发生回滚
1)@Transactional作用范围和参数说明
@Transactional
可以用来修饰方法或类:
- 修饰方法时:需要注意只能修饰在public方法上,否则不起作用
- 修饰类时:表明注解对该类中所有的public方法都生效
@Transactional参数说明
参数 | 作用 |
---|---|
value | 当配置了多个事务管理器时,可以使用该属性指定选择哪个事务管理器 |
transactionManager | 当配置了多个事务管理器时,可以使用该属性指定选择哪个事务管理器 |
propagation | 事务的传播行为,默认值为Propagation.REQUIRED |
isolation | 事务隔离级别,默认值我Isolation.DEFAULT |
timeout | 事务超时时间,默认值为-1,如果超过该时间限制但事务还没有完成,则自动回滚事务 |
readOnly | 指定事务是否为只读事务,默认值为false,为了忽略那些不需要事务的方法,比如读取数据,可以设置read-only为true |
rollbackFor | 用于指定能够触发事务回滚的异常类型,可以指定多个异常类型 |
rollbackForClassName | 用于指定能够触发事务回滚的异常类型,可以指定多个异常类型 |
noRollbackFor | 抛出指定的异常类型,不回滚事务,也可以指定多个异常类型 |
noRollbackForClassName | 抛出指定的异常类型,不回滚事务,也可以指定多个异常类型 |
- readOnly:true只读事务,false读写事务,增删改要设为false,查询设为true。
- timeout:设置超时时间单位秒,在多长时间之内事务没有提交成功就自动回滚,-1表示不设置超时时间。
- rollbackFor:当出现指定异常进行事务回滚
- noRollbackFor:当出现指定异常不进行事务回滚
- rollbackForClassName等同于rollbackFor,只不过属性为异常的类全名字符串
- noRollbackForClassName等同于noRollbackFor,只不过属性为异常的类全名字符串
2)事务失效场景
对异常进行捕获
@Transactional
注解在异常被捕获的情况下,是不会进行回滚操作的。
因为声明式事务@Transactional
是通过动态代理实现的,所以在容器中获取到的Bean不是原始的对象,而是Spring生成的代理对象,这个代理对象是一个环绕通知,环绕通知里包裹的就是真正要执行的业务逻辑,只有把异常抛出去,外层的事务通知才会知道需要回滚。
比如下面的代码就不会进行回滚,
@Transactional
public ResponseState reg(String username,String password) {ResponseState responseState = new ResponseState();responseState.setSuccess(200);// 业务逻辑......try {// 对异常进行捕获int a = 10/0;} catch (ArithmeticException e) {e.printStackTrace();}return responseState;
}
但可以手动把异常再抛出事务就会回滚了,又或者使用事务状态对象告诉调用者需要回滚事务
@Transactional
public ResponseState reg(String username,String password) {ResponseState responseState = new ResponseState();responseState.setSuccess(200);// 业务逻辑......try {// 对异常进行捕获int a = 10/0;} catch (ArithmeticException e) {e.printStackTrace();// 手动抛出异常throw new RuntimeException();// 或者使用事务状态对象告诉调用者需要回滚事务//TransactionInterceptor.currentTransactionStatus().setRollbackOnly();}return responseState;
}
特定异常
需要注意的是Spring的事务只是对Error
和RuntimeException
异常生效,对非运行时异常也就是受查异常时不生效的,比如说IOException
,注意Exception
也是会导致事务不生效的
@Transactional
public ResponseState reg(String username,String password) {ResponseState responseState = new ResponseState();responseState.setSuccess(200);int ret = userMapper.reg(username,password);if(true){throw new IOException(); //这个异常事务就不会回滚}return responseState;
}
为了解决这个问题,此时就可以使用rollbackFor属性来设置出现IOException异常不回滚
@Transactional(rollbackFor = {IOException.class})
public ResponseState reg(String username,String password) {ResponseState responseState = new ResponseState();responseState.setSuccess(200);int ret = userMapper.reg(username,password);if(true){throw new IOException(); //这个异常事务就不会回滚}return responseState;
}
aop切面顺序导致事务不能正确回滚
我们经常使用切面来做一些统一功能的处理,但是如果使用不当也会导致事务失效。切面是由优先级的,而事务的优先级默认是最低的,但如果我们自己自定义一个切面,它的优先级也是最低的,两个都是最低的Spring的做法是让自定义切面先执行,所以当我们的自定义切面把异常给捕获后,事务切面就无法知道业务层抛出了异常,从而导致事务失效
@Aspect
@Component
public class Test {@Pointcut("execution(* com.example.my_ssm.service.impl.UserServiceImpl.*(..))")public void test(){System.out.println("test");}@Around("test()")public void doAround(ProceedingJoinPoint joinPoint){System.out.println("环绕通知前置方法");try {joinPoint.proceed();} catch (Throwable e) {// 直接捕获异常e.printStackTrace();}// 执行后置通知...//....}
}
业务层代码
@Override
@Transactional
public Result reg(String username, String password) {if (userRepository.findByUsername(username) != null) {return Result.error("用户名已经存在");}User user = new User();user.setUsername(username);user.setPassword(password);user = userRepository.save(user);if (user == null) {return Result.error("注册失败");}int tmp = 10 / 0;return Result.success("注册成功");
}
解决次问题的方法有三种:
-
在自定义切面内将异常向外抛出
-
使用事务状态对象告诉调用者需要回滚事务
@Around("test()") public void doAround(ProceedingJoinPoint joinPoint){System.out.println("环绕通知前置方法");try {joinPoint.proceed();} catch (Throwable e) {// 手动抛出异常throw new RuntimeException();// 或者使用事务状态对象告诉调用者需要回滚事务//TransactionInterceptor.currentTransactionStatus().setRollbackOnly();}// 执行后置通知...//.... }
-
使用
@Order
注解调整切面的执行顺序,@Order
注解的数字越小优先级越高@Around("test()") @Order(Ordered.LOWEST_PRECEDENCE - 1) public void doAround(ProceedingJoinPoint joinPoint){System.out.println("环绕通知前置方法");try {joinPoint.proceed();} catch (Throwable e) {e.printStackTrace();} }
方法不是public
如果被@Transactional
修饰的方法不是public的事务就会失效,因为Spring是基于动态代理实现的,而Spring要求被代理的方法是public的
没有开启事务
没有在启动类上用@EnableTransactionManagement
开启事务
方法被final修饰
如果一个方法被final
修饰,也会导致Spring的事务失效
直接调用本类方法导致传播行为失效
但我么在业务方法直接调用同一个类的另外一个方法的时候,因为直接通过方法调用本类方法并不会使用代理类的,所以无法使该方法增强,就会导致事务失效。
@Service
public class UserServiceImpl implements UserService {@Autowiredprivate UserRepository userRepository;@Override@Transactionalpublic void delete(Integer id) {userRepository.delete(id);}@Override@Transactionalpublic Result getUser(String username) {User user = userRepository.byNameUser(username);if (user != null) {// 直接调用导致事务失效delete(id);}}}
-
解决方法1,依赖注入自己(代理)来调用
@Service public class UserServiceImpl implements UserService {@Autowiredprivate UserRepository userRepository;@Autowiredprivate UserService userService;@Override@Transactionalpublic void delete(Integer id) {userRepository.delete(id);}@Override@Transactionalpublic Result getUser(String username) {User user = userRepository.byNameUser(username);if (user != null) {// 注入接口userService.delete(id);}}}
-
通过 AopContext 拿到代理对象,来调用,还需要在 AppConfig 上添加
@EnableAspectJAutoProxy(exposeProxy = true)
@Service public class UserServiceImpl implements UserService {@Autowiredprivate UserRepository userRepository;@Override@Transactionalpublic void delete(Integer id) {userRepository.delete(id);}@Override@Transactionalpublic Result getUser(String username) {User user = userRepository.byNameUser(username);if (user != null) {((UserService) AopContext.currentProxy()).delete(id);}}}
@Transactional 没有保证原子行为
下面是一个转账代码并在转账方法上加上了事务,看上去好像并没有什么问题,但如果在多线程场景下是会出现问题的。
@Service
public class Service7 {private static final Logger logger = LoggerFactory.getLogger(Service7.class);@Autowiredprivate AccountMapper accountMapper;// from转账用户id,to转账对象id,转账金额@Transactional(rollbackFor = Exception.class)public void transfer(int from, int to, int amount) {int fromBalance = accountMapper.findBalanceBy(from);logger.debug("更新前查询余额为: {}", fromBalance);if (fromBalance - amount >= 0) {accountMapper.update(from, -1 * amount);accountMapper.update(to, amount);}}public int findBalance(int accountNo) {return accountMapper.findBalanceBy(accountNo);}
}
- 上述代码如果有两个线程同时执行的时候,就可能出现问题
- 假设两个线程同时进行转账操作,并同时执行了查询操作,两个线程查到的余额是一样的就会进行转账操作
- 所以多线程场景下就可能出现余额为负数问题
直接加上synchronized
可行吗?
下面代码好像是没有问题,但是需要注意的是这里保证的只是当前方法的原子性,也就是说只是保证了目标方法的原子性,而Spring事务是通过代理类的环绕通知来实现的,并没有保证事务的原子性,也就是commit
操作并不是原子的。
// from转账用户id,to转账对象id,转账金额
@Transactional(rollbackFor = Exception.class)
public synchronized void transfer(int from, int to, int amount) {int fromBalance = accountMapper.findBalanceBy(from);logger.debug("更新前查询余额为: {}", fromBalance);if (fromBalance - amount >= 0) {accountMapper.update(from, -1 * amount);accountMapper.update(to, amount);}
}
针对上诉问题有两种解决办法:
-
使用 select … for update 替换 select,借助MySQL数据库的排他锁
select balance from account where accountNo=? for update;
-
或者可以扩大
synchronized
的范围到代理方法上
事务不会自动回滚解决方案
解决方案1:简单粗暴,将异常直接抛出去
try {int tmp = 10/0;
} catch (ArithmeticException e) {e.printStackTrace();throw new ArithmeticException(); // 再把异常抛出去
}
解决方法2:⼿动回滚事务,在⽅法中使⽤ TransactionAspectSupport.currentTransactionStatus() 可
以得到当前的事务,然后设置回滚⽅法 setRollbackOnly 就可以实现回滚了
try {int tmp = 10/0;
} catch (ArithmeticException e) {e.printStackTrace();// 手动回滚TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
}
@Transactional ⼯作原理
@Transactional
是基于AOP实现的,AOP又是使用动态代理实现的,如果目标对象实现了接口,默认情况下会采用JDK的动态代理,如果目标对象没有实现了接口,会使用CGLIB动态代理。
@Transactional
在开始执行业务之前,通过代理先开启事务,在执行成功之后再提交事务,如果中途遇到异常,则会回滚事务。
4. 事务的隔离级别
1) 事务特性
事务有4大特性(ACID,原子性、持久性、一致性和隔离
- 原子性(Atomicity ):一个事务中的所有操作,要么全部完成,要么全部不完成,不会结束在中间某个环节,只要事务在执行过程中发生错误,会被回滚到事务开始执行前的状态,就像这个事务从来没执行过。
- 一致性(Consistency):在事务开始之前和事务结束以后,数据库的完整性没有被破坏,这表示写入的资料必须要完全符合所有的预设规则,这包含数据的精确度、串联以及后续数据库可以自发性地顺利完成预定的工作
- 持久性(Isolation):事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失
- 隔离性(Durability):数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。事务隔离分为不同级别,包括读未提交,读提交、可重复度和串行化。
但是这4种特性中,只有隔离性(隔离级别)是可以设置的
为什么要设置事务的隔离级别?
设置事务的隔离级别是用例保障多个并发事务执行更可控,更符合操作者预期的。而事务的隔离级别就是为了防止,其他的事务影响当前事务执行的一种策略
2) Spring中设置事务隔离级别
Spring中事务隔离级别可以通过@Transactional
中的isolation属性进行设置
MySQL的4种隔离级别:
- READ UNCOMMITTED:读未提交,也叫未提交读,该隔离级别的事务可以看到其他事务中未提交的数据,该隔离级别因为可以读取到其它事务中未提交的数据,而未提交的数据可能会发生回滚,因此我们把该级别读取到的数据称之为脏数据,把这个问题称为脏读
- READ COMMITTED:读已提交,也叫提交读,该隔离级别的事务能读取到已经提交事务的数据,因此它不会有脏读问题,但由于在事务的执行中可以读取到其它事务提交的结果,所以在不同时间的相同SQL查询中,可能会得到不同的结果,这种现象叫做不可重复读。
- REPEATABLE READ:可重复读,这是MySQL的默认事务隔离级别,它能确保同一事务多次查询的结果一致,但也会有新的问题,比如此级别的事务正在执行时,另一个事务成功的插入了某条数据,但因为它每次查询的结果都是一样的,所有会导致查询不到这条数据,自己重复插入时又失败(唯一约束的原因)。明明在事务中查询不到这条信息,但自己就是插入不进去,这就叫幻读
- SERIALIZABLE:序列化,事务最高隔离级别,它会强制事务排序,使之不会发生冲突,从而解决了脏读,不可重复读和幻读问题,但因为执行效率低,所以真正使用的场景并不多。
事务隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
读未提交(READ UNCOMMITTED) | 存在 | 存在 | 存在 |
读已提交(READ COMMITTED) | 不存在 | 存在 | 存在 |
可重复读(REPEATABLE READ) | 不存在 | 不存在 | 存在 |
串行化(SERIALIZABLE) | 不存在 | 不存在 | 不存在 |
- 脏读:一个事务读取到了另一个事务修改的数据之后,后一个事务又进行了回滚操作,从而
导致第一个事务读取的数据是错误的。
- 不可重复读:一个事务两次查询得到的结果不同,因为在两次查询中间,有另一个事务把数据修改了
- 幻读:一个事务两次查询中得到的结果集不同,因为在两次查询中另一个事务又新增了一部分数据
通过下面这一条命令可以查询,当前数据库的事务隔离级别和当前连接事务隔离级别
mysql> select @@global.tx_isolation,@@tx_isolation;
+-----------------------+-----------------+
| @@global.tx_isolation | @@tx_isolation |
+-----------------------+-----------------+
| REPEATABLE-READ | REPEATABLE-READ |
+-----------------------+-----------------+
3) Spring中5种事务隔离级别
- Isolation.DEFAULT:以连接的数据库的事务隔离级别为主。
- Isolation.READ_UNCOMMITTED:读未提交,可以读取到未提交的事务,存在脏读。
- Isolation.READ_COMMITTED:读已提交,只能读取到已经提交的事务,解决了脏读,存在不可重
复读。 - Isolation.REPEATABLE_READ:可重复读,解决了不可重复读,但存在幻读(MySQL默认级
别)。 - Isolation.SERIALIZABLE:串⾏化,可以解决所有并发问题,但性能太低。
从上述介绍可以看出,相⽐于 MySQL 的事务隔离级别,Spring 的事务隔离级别只是多了⼀个
Isolation.DEFAULT(以数据库的全局事务隔离级别为主)
相比于MySQL的事务隔离级别,Spring的事务隔离级别只是多了一个Isolation.DEFAULT(以数据库的全局事务隔离级别为主)
Spring中事务隔离级别只需要设置@Transactional
注解里的isolation属性即可,具体实现代码如下:
@PostMapping("/reg")
@Transactional(isolation = Isolation.DEFAULT)
public ResponseState reg(String username,String password) {}
5. 事务的传播机制
1) 为啥需要传播机制?
事务的传播机制的定义:它指的是Spring中多个事务在相互调用时,他们之间的行为方式是如何执行的
事务隔离级别是保证多个并发事务执行的可控性的(稳定性),而事务传播机制是保证一个事务在多个调用方法间的可控性的(稳定性)
事务隔离级别:解决的是多个事务同时调用数据库的问题,如下图所示:
而事务传播机制解决的是一个事务在多个方法中传递的问题,如下图所示:
假设有这么一段代码,添加用户后记录日志。
那么这种service和service之间的调用就是事务的传播,其实这里一共有三个事务
- 第一就是Spring的事务也就是
@Transactional
声明的事务 - 第二个事添加用户的事务
- 第三个事记录日志的事务
把@Transactional
叫做事务的管理员,把添加用户和记录日志的事务叫做事务的协调员,那么此时这两个事物就会加入到事务管理员的事务也就是@Transactional
,那么此时就只有一个事务了
@Service
public class UserServiceImpl implements UserService {@Resourceprivate UserMapper userMapper;@Autowiredprivate UserLogService userLogService;@Override@Transactionalpublic void saveUser(User user) {userMapper.save(user);userLogService.saveUserLog("添加了用户:"+user.getUsername());}
}
@Service
public class UserLogServiceImpl implements UserLogService {@Resourceprivate UserInfoMapper userInfoMapper;@Override@Transactionalpublic void saveUserLog(String info) {userInfoMapper.saveUserLog(info,new Timestamp(System.currentTimeMillis()));}
}
利用事务的传播机制就可以让事务协调员对于事务管理者,设置不同的处理态度
2) Spring中的事务传播机制
在Spring中包含7种事务传播机制,事务可选的传播行为如下:
REQUIRED
:默认的传播行为- 如果事务管理员开启了事务,事务协调员就会加入该事务
- 如果事务管理员没有开启事务,那么事务协调员就会自己创建事务
REQUIRES_NEW
- 如果事务管理员开启了事务,事务协调员创建自己的新事务执行,两个事物互不影响
- 如果事务管理员没有开启事务,事务协调员也是自己创建新事务
SUPPORTS
- 如果事务管理员开启了事务,事务协调员会直接加入事务
- 如果事务管理员没有开启事务,那么事务协调员也不会开启事务
NOT_SUPPORTED
- 如果事务管理员开启了事务,事务协调员不会开启事务
- 如果事务管理员没有开启事务,那么事务协调员也不会开启事务
MANDATORY
- 如果事务管理员开启了事务,那么事务协调员会加入该事务
- 如果事务管理员没有开启事务,那么事务协调员会直接报错
NEVER
- 如果事务管理员开启了事务,那么事务协调员就会报错
- 如果事务管理员没有开启事务,那么事务协调员就什么也不会开启事务
NESTED
- 如果事务管理员开启了事务,那么事务协调员会创建⼀个事务作为当前事务的嵌套事务,类似于一个子事务
- 如果事务管理员没有开启事务,那么事务协调源码的事务该取值等价于
REQUIRED
假设有方法userAdd去调用添加日志的add方法
-
Propagation.REQUIRED:默认的事务传播级别,它表示如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务
在saveUser方法或者saveUserLog方法中发生异常,异常都会回滚
@Service public class UserServiceImpl implements UserService {@Resourceprivate UserMapper userMapper;@Autowiredprivate UserLogService userLogService;@Override@Transactionalpublic void saveUser(User user) {// 添加用户记录userMapper.save(user);// 记录添加用户日志userLogService.saveUserLog("添加了用户:"+user.getUsername());} }
添加日志的方法
@Service public class UserLogServiceImpl implements UserLogService {@Resourceprivate UserInfoMapper userInfoMapper;@Override@Transactional(propagation = Propagation.REQUIRES_NEW)public void saveUserLog(String info) {userInfoMapper.saveUserLog(info,new Timestamp(System.currentTimeMillis()));} }
如果此时要求添加用户发送异常,也需要记录日志该如何实现?
-
REQUIRES_NEW:使用该隔离级别就能实现,因为该机制是创建一个新事务,且两个事务互不影响
新增用户业务层
@Service public class UserServiceImpl implements UserService {@Resourceprivate UserMapper userMapper;@Autowiredprivate UserLogService userLogService;@Override@Transactionalpublic void saveUser(User user) {try {// 添加用户记录userMapper.save(user);// 模拟出现异常int row = 1/0;} finally {// 记录添加用户日志userLogService.saveUserLog("添加了用户:"+user.getUsername());}} }
@Service public class UserLogServiceImpl implements UserLogService {@Resourceprivate UserInfoMapper userInfoMapper;@Override@Transactional(propagation = Propagation.REQUIRES_NEW)public void saveUserLog(String info) {userInfoMapper.saveUserLog(info,new Timestamp(System.currentTimeMillis()));} }
通过以上代码就能保证,无论插入用户是否成功都会记录日志。
-
NESTED:可以实现事务部分回滚。
假设此时的要求是新增用户失败了全部回滚,如果是记录日志失败了并不影响新增用户。
通过
NESTED
就可以实现,因为该传播机制是创建一个子事务,回滚的时候是部分回滚的。@Service public class UserServiceImpl implements UserService {@Resourceprivate UserMapper userMapper;@Autowiredprivate UserLogService userLogService;@Override@Transactionalpublic void saveUser(User user) {// 添加用户记录userMapper.save(user);// 记录添加用户日志userLogService.saveUserLog("添加了用户:"+user.getUsername());} }
添加日志业务层
@Service public class UserLogServiceImpl implements UserLogService {@Resourceprivate UserInfoMapper userInfoMapper;@Override@Transactional(propagation = Propagation.NESTED)public void saveUserLog(String info) {try {userInfoMapper.saveUserLog(info, new Timestamp(System.currentTimeMillis()));// 模拟出现异常int row = 1/0;} catch (Exception e){// 手动回滚当前事务TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();}} }
该代码运行后,插入用户成功,但并没有记录日志