【SpringBoot】Spring中事务的实现:声明式事务@Transactional、编程式事务
1. 准备工作
1.1 在MySQL数据库中创建相应的表
用户注册的例子进行演示事务操作,索引需要一个用户信息表
(1)创建数据库
-- 创建数据库
DROP DATABASE IF EXISTS trans_test;
CREATE DATABASE trans_test DEFAULT CHARACTER SET utf8mb4;
(2)创建用户表
-- ⽤⼾表
DROP TABLE IF EXISTS user_info; CREATE TABLE user_info (`id` INT NOT NULL AUTO_INCREMENT,`user_name` VARCHAR (128) NOT NULL,`count` int NOT NULL,`create_time` DATETIME DEFAULT now(),`update_time` DATETIME DEFAULT now() ON UPDATE now(), PRIMARY KEY (`id`)) ENGINE = INNODB DEFAULT CHARACTER
SET = utf8mb4 COMMENT = '用户表';
(3)操作日志表
-- 操作⽇志表
DROP TABLE IF EXISTS log_info;
CREATE TABLE log_info ( `id` INT PRIMARY KEY auto_increment, `from` VARCHAR ( 128 ) NOT NULL, `to` VARCHAR ( 256 ) NOT NULL, `num` int not null,`create_time` DATETIME DEFAULT now(), `update_time` DATETIME DEFAULT now() ON UPDATE now()) DEFAULT charset 'utf8mb4';
1.2 在Java项目中创建相应的实体类
(1)UserInfo类:
@Data
public class UserInfo {private Integer id;private String userName;private Integer count;private Date createTime;private Date updateTime;
}
(2)用户日志表
@Data
public class LogInfo {private Integer id;private String from;private String to;private Integer num;private Date createTime;private Date updateTime;
}
2. Spring编程式事务
2.1 简单介绍
Spring ⼿动操作事务和上⾯ MySQL 操作事务类似, 有 3 个重要操作步骤:
(1)开启事务(获取事务)
(2)提交事务
(3)回滚事务
SpringBoot 内置了两个对象:
- DataSourceTransactionManager 事务管理器.:⽤来获取事务(开启事务), 提交或回滚事务
- TransactionDefinition 是事务的属性, 在获取事务的时候需要将
TransactionDefinition 传递进去从而获得⼀个事务 TransactionStatus
2.2 使用事务
2.2.1 创建Mapper接口
(1)创建UserInfoMapper接口
@Mapper
@Mapper
public interface UserInfoMapper {@Insert("insert into user_info(user_name, `count`) values(#{userName},#{count})")Integer insert(UserInfo userInfo);@Update("update user_info set count = count +#{countAdd} where user_name=#{userName}")Integer updateAdd(@Param("userName") String userName, @Param("countAdd") int countAdd);@Update("update user_info set count = count - #{countDelete} where user_name=#{userName}")Integer updateDelete(@Param("userName") String userName, @Param("countDelete") int countDelete);
}
使用测试类向该表转中插入数据:
@SpringBootTest
class UserInfoMapperTest {@Autowiredprivate UserInfoMapper userInfoMapper;@Testvoid insert() {UserInfo userInfo1 = new UserInfo();userInfo1.setUserName("zangsan");userInfo1.setCount(100);UserInfo userInfo2 = new UserInfo();userInfo2.setUserName("lisi");userInfo2.setCount(100);userInfoMapper.insert(userInfo1);userInfoMapper.insert(userInfo2);}
}
(2)创建LogInfoMapper接口
@Mapper
public interface LogInfoMapper {@Insert("insert into log_info(`from` ,`to` , `num`) values(#{from},#{to},#{num})")Integer insert(LogInfo logInfo);
}
2.3.2 创建Controller接口
需求:A 向 B 转 10
@RequestMapping("/user")
@RestController
public class UserInfoController {@Autowiredprivate UserInfoService userInfoService;@RequestMapping("/update")public boolean updateUserInfo(@RequestBody LogInfo logInfo) {boolean flag = userInfoService.updateUserInfo(logInfo);return flag;}
}
2.3.3 创建Service接口
事务通常放在Service接口中,事务的逻辑也在Service接口中
@Service
public class UserInfoService {@Autowiredprivate LogInfoMapper logInfoMapper;@Autowiredprivate UserInfoMapper userInfoMapper;//JDBC事务管理器@Autowiredprivate DataSourceTransactionManager dataSourceTransactionManager;//定义事务属性private TransactionDefinition transactionDefinition;public boolean updateUserInfo(LogInfo logInfo) {// 处理 from 向 to 转账 10//开启事务TransactionStatus transactionStatus = dataSourceTransactionManager.getTransaction(transactionDefinition);// 事务逻辑// 1.from 的账户 -10userInfoMapper.updateDelete(logInfo.getFrom(),logInfo.getNum());// 2.to 的账户 +10userInfoMapper.updateAdd(logInfo.getTo(),logInfo.getNum());// 3. 记录日志logInfoMapper.insert(logInfo);//提交事务dataSourceTransactionManager.commit(transactionStatus);return true;}
}
2.3.4 测试
使用PostMan测试:
运行结果:
在MySQL中查询是否成功:
成功–数据已更新和记录
2.3.5 演示事务出错
在Service的事务中设置一个出错点:
@Service
public class UserInfoService {@Autowiredprivate LogInfoMapper logInfoMapper;@Autowiredprivate UserInfoMapper userInfoMapper;//JDBC事务管理器@Autowiredprivate DataSourceTransactionManager dataSourceTransactionManager;//定义事务属性private TransactionDefinition transactionDefinition;public boolean updateUserInfo(LogInfo logInfo) {// 处理 from 向 to 转账 10//开启事务TransactionStatus transactionStatus = dataSourceTransactionManager.getTransaction(transactionDefinition);// 事务逻辑// 1.from 的账户 -10userInfoMapper.updateDelete(logInfo.getFrom(),logInfo.getNum());// 2.to 的账户 +10userInfoMapper.updateAdd(logInfo.getTo(),logInfo.getNum());// 3. 记录日志logInfoMapper.insert(logInfo);// 故意设置的出错点int n = 10/0;//提交事务dataSourceTransactionManager.commit(transactionStatus);return true;}
}
使用Postman测试:
运行结果:
可以从运行结果中看到,只是释放了会话,没有提交事务,在java语句int n = 10/0;
之前的语句会执行成功吗?
看一下MySQL中表的变化:
没有任何的变化。
所以,在事务中如果有异常会自动回滚事务。
2.3.6 演示事务回滚
只需要把Service接口中的提交事务改为回滚事务即可:
@Service
public class UserInfoService {@Autowiredprivate LogInfoMapper logInfoMapper;@Autowiredprivate UserInfoMapper userInfoMapper;//JDBC事务管理器@Autowiredprivate DataSourceTransactionManager dataSourceTransactionManager;//定义事务属性private TransactionDefinition transactionDefinition;public boolean updateUserInfo(LogInfo logInfo) {// 处理 from 向 to 转账 10//开启事务TransactionStatus transactionStatus = dataSourceTransactionManager.getTransaction(transactionDefinition);// 事务逻辑// 1.from 的账户 -10userInfoMapper.updateDelete(logInfo.getFrom(),logInfo.getNum());// 2.to 的账户 +10userInfoMapper.updateAdd(logInfo.getTo(),logInfo.getNum());// 3. 记录日志logInfoMapper.insert(logInfo);// //提交事务
// dataSourceTransactionManager.commit(transactionStatus);//事务回滚dataSourceTransactionManager.rollback(transactionStatus);return true;}
}
当再次请求的时候会发现程序运行成功但是数据库中的数据没有修改。
3. Spring声明式事务 @Transactional
3.1 简介
Spring声明式事务很简单,只需要添加注解@Transactional
(1)相关的依赖:
<dependency><groupId>org.springframework</groupId><artifactId>spring-tx</artifactId></dependency>
(2)在需要事务的⽅法上添加 注解就可以实现了. ⽆需⼿动开启事务和提交事务, 进⼊⽅法时⾃动开启事务, ⽅法执⾏完会⾃动提交事务, 如果中途发生了没有处理的异常会⾃动回滚事务.
3.2 使用@Transactional
3.2.1 创建Mapper接口
(1)创建UserInfoMapper接口
@Mapper
@Mapper
public interface UserInfoMapper {@Insert("insert into user_info(user_name, `count`) values(#{userName},#{count})")Integer insert(UserInfo userInfo);@Update("update user_info set count = count +#{countAdd} where user_name=#{userName}")Integer updateAdd(@Param("userName") String userName, @Param("countAdd") int countAdd);@Update("update user_info set count = count - #{countDelete} where user_name=#{userName}")Integer updateDelete(@Param("userName") String userName, @Param("countDelete") int countDelete);
}
使用测试类向该表转中插入数据:
@SpringBootTest
class UserInfoMapperTest {@Autowiredprivate UserInfoMapper userInfoMapper;@Testvoid insert() {UserInfo userInfo1 = new UserInfo();userInfo1.setUserName("zangsan");userInfo1.setCount(100);UserInfo userInfo2 = new UserInfo();userInfo2.setUserName("lisi");userInfo2.setCount(100);userInfoMapper.insert(userInfo1);userInfoMapper.insert(userInfo2);}
}
(2)创建LogInfoMapper接口
@Mapper
public interface LogInfoMapper {@Insert("insert into log_info(`from` ,`to` , `num`) values(#{from},#{to},#{num})")Integer insert(LogInfo logInfo);
}
3.3.2 创建Controller接口
需求:A 向 B 转 10
@RequestMapping("/user")
@RestController
public class UserInfoController {@Autowiredprivate UserInfoService userInfoService;@RequestMapping("/update")public boolean updateUserInfo(@RequestBody LogInfo logInfo) {boolean flag = userInfoService.updateUserInfo(logInfo);return flag;}
}
3.3.3 创建Service接口
事务通常放在Service接口中,在方法上使用注解@Transactional
,事务的逻辑写在方法中
@Service
public class UserInfoService {@Autowiredprivate LogInfoMapper logInfoMapper;@Autowiredprivate UserInfoMapper userInfoMapper;public boolean updateUserInfo(LogInfo logInfo) {// 处理 from 向 to 转账 10// 事务逻辑// 1.from 的账户 -10userInfoMapper.updateDelete(logInfo.getFrom(),logInfo.getNum());// 2.to 的账户 +10userInfoMapper.updateAdd(logInfo.getTo(),logInfo.getNum());// 3. 记录日志logInfoMapper.insert(logInfo);return true;}
}
3.3.4 测试
使用PostMan测试:
注:
如果发生报错:Public Key Retrieval is not allowed
在配置文件中修改如下:
spring.datasource.url=jdbc:mysql://x.x.x.x:3306/trans_test?characterEncoding=utf8&useSSL=false&&allowPublicKeyRetrieval=true&useSSL=false
关键参数说明:
allowPublicKeyRetrieval=true:允许驱动从服务器获取公钥。
useSSL=false:禁用 SSL(开发环境可用,生产环境建议启用)。
运行结果:
MySQL中的结果:
数据已更新和记录
3.3.4 演示事务出错
在Service中设置一个出错点:
@Service
public class UserInfoService {@Autowiredprivate LogInfoMapper logInfoMapper;@Autowiredprivate UserInfoMapper userInfoMapper;@Transactionalpublic boolean updateUserInfo(LogInfo logInfo) {// 处理 from 向 to 转账 10// 事务逻辑// 1.from 的账户 -10userInfoMapper.updateDelete(logInfo.getFrom(),logInfo.getNum());// 2.to 的账户 +10userInfoMapper.updateAdd(logInfo.getTo(),logInfo.getNum());// 3. 记录日志logInfoMapper.insert(logInfo);// 故意设置出错点int n = 10/0;return true;}
}
postman测试:
运行结果:
可以看到只是释放了会话,没有提交事务
MySQL中的数据没有修改成功(数据没有改动)。在java语句int n = 10/0;
之前的语句不会执行成功。
3.3 @Transactional 注解
3.3.1 介绍
@Transactional 可以⽤来修饰⽅法或类:
(1)修饰⽅法时: 只有修饰public 方法时才生效(修饰其他⽅法时不会报错, 也不⽣效)[推荐]
(2) 修饰类时: 对 @Transactional 修饰的类中所有的 public ⽅法都⽣效.
方法/类被 @Transactional 注解修饰时, 在⽬标⽅法执⾏开始之前, 会⾃动开启事务, ⽅法执⾏结束之后, ⾃动提交事务.
如果在⽅法执⾏过程中, 出现异常, 且异常未被捕获, 就进⾏事务回滚操作.
如果异常被程序捕获, ⽅法就被认为是成功执⾏, 依然会提交事务.
3.3.2 异常捕获–事务提交
修改Service接口:
@Slf4j
@Service
public class UserInfoService {@Autowiredprivate LogInfoMapper logInfoMapper;@Autowiredprivate UserInfoMapper userInfoMapper;@Transactionalpublic boolean updateUserInfo(LogInfo logInfo) {// 处理 from 向 to 转账 10// 事务逻辑// 1.from 的账户 -10userInfoMapper.updateDelete(logInfo.getFrom(),logInfo.getNum());// 2.to 的账户 +10userInfoMapper.updateAdd(logInfo.getTo(),logInfo.getNum());// 3. 记录日志logInfoMapper.insert(logInfo);try {// 故意设置出错点int n = 10 / 0;}catch (Exception e) {log.info("捕获 事务中的异常:"+ e);}return true;}
}
使用postman测试:
运行结果:
MySQL中的数据:
数据已更新和记录
3.3.3 异常捕获后再抛出–事务回滚
修改Service中的代码:
@Slf4j
@Service
public class UserInfoService {@Autowiredprivate LogInfoMapper logInfoMapper;@Autowiredprivate UserInfoMapper userInfoMapper;@Transactionalpublic boolean updateUserInfo(LogInfo logInfo) {// 处理 from 向 to 转账 10// 事务逻辑// 1.from 的账户 -10userInfoMapper.updateDelete(logInfo.getFrom(),logInfo.getNum());// 2.to 的账户 +10userInfoMapper.updateAdd(logInfo.getTo(),logInfo.getNum());// 3. 记录日志logInfoMapper.insert(logInfo);try {// 故意设置出错点int n = 10 / 0;}catch (Exception e) {log.info("捕获事务中的异常后 再抛出:"+ e);//抛出异常throw e;}return true;}
}
使用postman测试:
可以看到发生报错
运行结果:
可以看到只是释放了会话,没有提交事务
MySQL:
MySQL中的数据没有发生改变
3.3.4 异常捕获后手动抛出–事务回滚
使⽤ TransactionAspectSupport.currentTransactionStatus() 得到当前的事务, 并使⽤ setRollbackOnly 设置 setRollbackOnly
Service接口:
@Slf4j
@Service
public class UserInfoService {@Autowiredprivate LogInfoMapper logInfoMapper;@Autowiredprivate UserInfoMapper userInfoMapper;@Transactionalpublic boolean updateUserInfo(LogInfo logInfo) {// 处理 from 向 to 转账 10// 事务逻辑// 1.from 的账户 -10userInfoMapper.updateDelete(logInfo.getFrom(),logInfo.getNum());// 2.to 的账户 +10userInfoMapper.updateAdd(logInfo.getTo(),logInfo.getNum());// 3. 记录日志logInfoMapper.insert(logInfo);try {// 故意设置出错点int n = 10 / 0;}catch (Exception e) {log.info("捕获事务中的异常后 手动回滚事务:"+ e);TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();}return true;}
}
使用postman测试:
运行结果:
前端收到的访问结果是成功的,但是运行过程中还是没有提交事务
MySQL:
MySQL中的数据没有发生改变