【SpringBoot】Spring中事务的实现:声明式事务@Transactional、编程式事务

1. 准备工作

1.1 在MySQL数据库中创建相应的表

用户注册的例子进行演示事务操作,索引需要一个用户信息表

(1)创建数据库

sql 复制代码
-- 创建数据库
DROP DATABASE IF EXISTS trans_test;
CREATE DATABASE trans_test DEFAULT CHARACTER SET utf8mb4;

(2)创建用户表

sql 复制代码
-- ⽤⼾表
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)操作日志表

sql 复制代码
-- 操作⽇志表
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类:

java 复制代码
@Data
public class UserInfo {

    private Integer id;
    private String userName;
    private Integer count;
    private Date createTime;
    private Date updateTime;
}

(2)用户日志表

java 复制代码
@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 内置了两个对象:

  1. DataSourceTransactionManager 事务管理器.:⽤来获取事务(开启事务), 提交或回滚事务
  2. TransactionDefinition 是事务的属性, 在获取事务的时候需要将
    TransactionDefinition 传递进去从而获得⼀个事务 TransactionStatus

2.2 使用事务

2.2.1 创建Mapper接口

(1)创建UserInfoMapper接口

java 复制代码
@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);
}

使用测试类向该表转中插入数据:

java 复制代码
@SpringBootTest
class UserInfoMapperTest {

    @Autowired
    private UserInfoMapper userInfoMapper;

    @Test
    void 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接口

java 复制代码
@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

java 复制代码
@RequestMapping("/user")
@RestController
public class UserInfoController {
    @Autowired
    private UserInfoService userInfoService;

    @RequestMapping("/update")
    public boolean  updateUserInfo(@RequestBody LogInfo logInfo) {
        boolean flag =  userInfoService.updateUserInfo(logInfo);

        return flag;
    }
}

2.3.3 创建Service接口

事务通常放在Service接口中,事务的逻辑也在Service接口中

java 复制代码
@Service
public class UserInfoService {

    @Autowired
    private LogInfoMapper logInfoMapper;

    @Autowired
    private UserInfoMapper userInfoMapper;

    //JDBC事务管理器
    @Autowired
    private DataSourceTransactionManager dataSourceTransactionManager;

    //定义事务属性
    private TransactionDefinition transactionDefinition;



    public boolean updateUserInfo(LogInfo logInfo) {

        // 处理 from 向 to 转账 10

        //开启事务
        TransactionStatus transactionStatus = dataSourceTransactionManager.getTransaction(transactionDefinition);

        // 事务逻辑

        // 1.from 的账户 -10
        userInfoMapper.updateDelete(logInfo.getFrom(),logInfo.getNum());

        // 2.to 的账户 +10
        userInfoMapper.updateAdd(logInfo.getTo(),logInfo.getNum());

        // 3. 记录日志
        logInfoMapper.insert(logInfo);

        //提交事务
        dataSourceTransactionManager.commit(transactionStatus);
        
        return true;
    }
}

2.3.4 测试

使用PostMan测试:

运行结果:

在MySQL中查询是否成功:

成功--数据已更新和记录

2.3.5 演示事务出错

在Service的事务中设置一个出错点:

java 复制代码
@Service
public class UserInfoService {

    @Autowired
    private LogInfoMapper logInfoMapper;

    @Autowired
    private UserInfoMapper userInfoMapper;

    //JDBC事务管理器
    @Autowired
    private DataSourceTransactionManager dataSourceTransactionManager;

    //定义事务属性
    private TransactionDefinition transactionDefinition;



    public boolean updateUserInfo(LogInfo logInfo) {

        // 处理 from 向 to 转账 10

        //开启事务
        TransactionStatus transactionStatus = dataSourceTransactionManager.getTransaction(transactionDefinition);

        // 事务逻辑

        // 1.from 的账户 -10
        userInfoMapper.updateDelete(logInfo.getFrom(),logInfo.getNum());

        // 2.to 的账户 +10
        userInfoMapper.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接口中的提交事务改为回滚事务即可:

java 复制代码
@Service
public class UserInfoService {

    @Autowired
    private LogInfoMapper logInfoMapper;

    @Autowired
    private UserInfoMapper userInfoMapper;

    //JDBC事务管理器
    @Autowired
    private DataSourceTransactionManager dataSourceTransactionManager;

    //定义事务属性
    private TransactionDefinition transactionDefinition;



    public boolean updateUserInfo(LogInfo logInfo) {

        // 处理 from 向 to 转账 10

        //开启事务
        TransactionStatus transactionStatus = dataSourceTransactionManager.getTransaction(transactionDefinition);

        // 事务逻辑

        // 1.from 的账户 -10
        userInfoMapper.updateDelete(logInfo.getFrom(),logInfo.getNum());

        // 2.to 的账户 +10
        userInfoMapper.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)相关的依赖:

xml 复制代码
        <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-tx</artifactId>
        </dependency>

(2)在需要事务的⽅法上添加 注解就可以实现了. ⽆需⼿动开启事务和提交事务, 进⼊⽅法时⾃动开启事务, ⽅法执⾏完会⾃动提交事务, 如果中途发生了没有处理的异常会⾃动回滚事务.

3.2 使用@Transactional

3.2.1 创建Mapper接口

(1)创建UserInfoMapper接口

java 复制代码
@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);
}

使用测试类向该表转中插入数据:

java 复制代码
@SpringBootTest
class UserInfoMapperTest {

    @Autowired
    private UserInfoMapper userInfoMapper;

    @Test
    void 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接口

java 复制代码
@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

java 复制代码
@RequestMapping("/user")
@RestController
public class UserInfoController {
    @Autowired
    private UserInfoService userInfoService;

    @RequestMapping("/update")
    public boolean  updateUserInfo(@RequestBody LogInfo logInfo) {
        boolean flag =  userInfoService.updateUserInfo(logInfo);

        return flag;
    }
}

3.3.3 创建Service接口

事务通常放在Service接口中,在方法上使用注解@Transactional,事务的逻辑写在方法中

java 复制代码
@Service
public class UserInfoService {

    @Autowired
    private LogInfoMapper logInfoMapper;

    @Autowired
    private UserInfoMapper userInfoMapper;
    
    public boolean updateUserInfo(LogInfo logInfo) {

        // 处理 from 向 to 转账 10
        
        // 事务逻辑

        // 1.from 的账户 -10
        userInfoMapper.updateDelete(logInfo.getFrom(),logInfo.getNum());

        // 2.to 的账户 +10
        userInfoMapper.updateAdd(logInfo.getTo(),logInfo.getNum());

        // 3. 记录日志
        logInfoMapper.insert(logInfo);
        
        return true;

    }
}

3.3.4 测试

使用PostMan测试:

注:

如果发生报错:Public Key Retrieval is not allowed

在配置文件中修改如下:

xml 复制代码
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中设置一个出错点:

java 复制代码
@Service
public class UserInfoService {

    @Autowired
    private LogInfoMapper logInfoMapper;

    @Autowired
    private UserInfoMapper userInfoMapper;

    @Transactional
    public boolean updateUserInfo(LogInfo logInfo) {

        // 处理 from 向 to 转账 10

        // 事务逻辑

        // 1.from 的账户 -10
        userInfoMapper.updateDelete(logInfo.getFrom(),logInfo.getNum());

        // 2.to 的账户 +10
        userInfoMapper.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接口:

java 复制代码
@Slf4j
@Service
public class UserInfoService {

    @Autowired
    private LogInfoMapper logInfoMapper;

    @Autowired
    private UserInfoMapper userInfoMapper;

    @Transactional
    public boolean updateUserInfo(LogInfo logInfo) {

        // 处理 from 向 to 转账 10

        // 事务逻辑

        // 1.from 的账户 -10
        userInfoMapper.updateDelete(logInfo.getFrom(),logInfo.getNum());

        // 2.to 的账户 +10
        userInfoMapper.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中的代码:

java 复制代码
@Slf4j
@Service
public class UserInfoService {

    @Autowired
    private LogInfoMapper logInfoMapper;

    @Autowired
    private UserInfoMapper userInfoMapper;

    @Transactional
    public boolean updateUserInfo(LogInfo logInfo) {

        // 处理 from 向 to 转账 10

        // 事务逻辑

        // 1.from 的账户 -10
        userInfoMapper.updateDelete(logInfo.getFrom(),logInfo.getNum());

        // 2.to 的账户 +10
        userInfoMapper.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接口:

java 复制代码
@Slf4j
@Service
public class UserInfoService {

    @Autowired
    private LogInfoMapper logInfoMapper;

    @Autowired
    private UserInfoMapper userInfoMapper;

    @Transactional
    public boolean updateUserInfo(LogInfo logInfo) {

        // 处理 from 向 to 转账 10

        // 事务逻辑

        // 1.from 的账户 -10
        userInfoMapper.updateDelete(logInfo.getFrom(),logInfo.getNum());

        // 2.to 的账户 +10
        userInfoMapper.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中的数据没有发生改变

相关推荐
hello_ejb311 分钟前
聊聊Spring AI Alibaba的MermaidGenerator
人工智能·python·spring
-曾牛15 分钟前
开启 Spring AI 之旅:从入门到实战
java·人工智能·spring·指南·教学·大模型应用·springai
Catfood_Eason1 小时前
XML简介
xml·java·前端
钢铁男儿2 小时前
C#编程精要:局部变量、类型推断与常量深度解析
java·开发语言·c#
JQLvopkk2 小时前
c#读取txt指定行
java·前端·c#
汐栊2 小时前
Redis总结及设置营业状态案例
java·redis·spring
hac13222 小时前
SpringBoot多环境配置
java·spring boot·后端
lllsure3 小时前
SpringCloud组件——Gateway
spring·spring cloud·gateway
猿来入此小猿4 小时前
基于SpringBoot+Vue实现的电影推荐平台功能一
vue.js·spring boot·毕业设计·毕业源码·免费学习·猿来入此·电影推荐平台
bingbingyihao4 小时前
ES集群搭建及工具类
java·elasticsearch