【JavaEE31-后端部分】Spring事务入门:从编程式到@Transactional,带你轻松搞定数据一致性

老铁们,你有没有遇到过这种情况:给朋友转账,你这边扣了钱,对方却没收到。你打电话问银行,银行说"系统出了点小问题"。你心里一万只羊驼跑过:我的钱呢?飞了?

这就是没有事务的后果。

事务就是用来解决这种问题的:一组操作,要么全部成功,要么全部失败,绝对不会出现"一半成功一半失败"的情况。

今天我们就来学习 Spring 中的事务,看看怎么用最简单的方式保证数据的安全。尤其是 @Transactional 这个注解

一、什么是事务

我们先回顾一下事务的概念,我们的事务它不仅仅是mysql中的事物,那事务它是一个概念,它并不是属于某个数据库的。

事务是一组操作的集合,是一个不可分割的操作,事务会把所有的操作作为一个整体,一起向数据库提交或者撤销操作请求。所以这种操作,要么同时成功,要么同时失败。

二、为什么需要事务

我们在进行程序开发的时候也会有事务的需求,比如转账操作

第1步:a账户扣了100块钱

第2步:b账户增了100块钱,那么如果没有事务,此时第1步执行成功了,而第2步执行失败了,那么a账户的100块钱就平白无故的消失了,所以呢,使用事务就可以解决这个问题,让这一组操作要么一起成功,要么一起失败。

再比如我们的秒杀系统

第1步:下单成功

第2步:扣减库存,下单成功之后,库存也需要同步减少,那如果你下单成功而库存扣减失败的话,那么就会造成下单超出的情况,所以就需要把这两个步操作呢,放在同一个事务中,要么一起成功,要么一起失败。

三、我们 MYSQL中事务的操作

回顾一下我们mysql中事务的操作主要有三步:

  1. 开启事务start transition/begin(一组操作前开启事务)
  2. 提交事务commit(这组操作全部成功提交事务)
  3. 回滚事务roll back(这组操作中任何一个操作出现异常,那么就回归事务)

而我们做项目的时候也是会操作多条数据的,或者我用一个接口来减100一个接口来加100我要两个接口同时成功或同时失败。所以呢我们本期就学习spring中事务是如何实现的。


四、Spring中事务的实现

在我们前面的课程讲了mysqll的事务操作,Spring对事务也进行了实现,我们Spring中的事务操作分为两大类:

  • 编程式事务:就是说你手动写代码操作事务
  • 声明式事务:就是说利用注解自动开启和提交事务

那在学习事务之前,我们先准备数据和数据的访问代码

需求:

我们这里有一个需求,就是用户注册,注册的时候在日志中插入一条操作记录。

用户注册时,要干两件事:

  1. 向 user_info 表插入用户数据
  2. 向 log_info 表插入一条操作日志

这两步必须在同一个事务中:要么都成功,要么都失败。

1. 数据准备

sql 复制代码
-- 删除用户表(如果存在)
drop table if exists user_info;

-- 创建用户表
create table user_info (
  `id` int not null auto_increment comment '用户主键ID',
  `user_name` varchar(128) not null comment '用户名',
  `password` varchar(128) not null comment '用户密码',
  `create_time` datetime default current_timestamp comment '创建时间',
  `update_time` datetime default current_timestamp on update current_timestamp comment '更新时间',
  primary key (`id`)
) engine = innodb default character set = utf8mb4 comment = '用户表';

-- 删除操作日志表(如果存在)
drop table if exists log_info;

-- 创建操作日志表
create table log_info (
  `id` int primary key auto_increment comment '日志主键ID',
  `user_name` varchar(128) not null comment '操作用户名',
  `op` varchar(256) not null comment '操作内容描述',
  `create_time` datetime default current_timestamp comment '创建时间',
  `update_time` datetime default current_timestamp on update current_timestamp comment '更新时间'
) engine = innodb default charset = utf8mb4 comment = '操作日志表';

2. 项目搭建

2.1 配置数据库的连接

yml 复制代码
spring:
  application:
    name: spring-trans
  datasource:
    url: jdbc:mysql://localhost:3306/javaee_test?useSSL=false&characterEncoding=utf8
    username: root       # 你的数据库用户名
    password: 123456     # 你的数据库密码
    driver-class-name: com.mysql.cj.jdbc.Driver

mybatis:
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl   # 打印SQL日志
    map-underscore-to-camel-case: true                     # 开启驼峰命名自动映射

注意:


2.2 实体类

java 复制代码
// UserInfo.java
import lombok.Data;
import java.util.Date;

@Data
public class UserInfo {
    private Integer id;
    private String userName;
    private String password;
    private Date createTime;
    private Date updateTime;
}

// LogInfo.java
import lombok.Data;
import java.util.Date;

@Data
public class LogInfo {
    private Integer id;
    private String userName;
    private String op;
    private Date createTime;
    private Date updateTime;
}

2.3 创建 Mapper 接口

java 复制代码
package com.zhongge.mapper;

import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;

/**
 * @ClassName LogInfoMapper
 * @Description TODO 日志持久层
 * @Author 笨忠
 * @Date 2026-04-03 22:00
 * @Version 1.0
 */
@Mapper
public interface LogInfoMapper {
    @Insert("INSERT INTO log_info(user_name, op) VALUES(#{name}, #{op})")
    Integer insertLog(String name, String op);
}

package com.zhongge.mapper;

import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;

/**
 * @ClassName UserInfoMapper
 * @Description TODO 用户持久层
 * @Author 笨忠
 * @Date 2026-04-03 21:59
 * @Version 1.0
 */
@Mapper
public interface UserInfoMapper {
    @Insert("INSERT INTO user_info(user_name, password) VALUES(#{name}, #{password})")
    Integer insert(String name, String password);
}

2.4 创建 Service 层

java 复制代码
package com.zhongge.service;

import com.zhongge.mapper.LogInfoMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

/**
 * @ClassName LogService
 * @Description TODO 日志服务层
 * @Author 笨忠
 * @Date 2026-04-03 22:02
 * @Version 1.0
 */
@Service
public class LogService {
    @Autowired
    private LogInfoMapper logInfoMapper;

    public void insertLog(String name, String op) {
        logInfoMapper.insertLog(name, op);
    }
}

package com.zhongge.service;

import com.zhongge.mapper.UserInfoMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

/**
 * @ClassName UserService
 * @Description TODO 用户服务层
 * @Author 笨忠
 * @Date 2026-04-03 22:01
 * @Version 1.0
 */
@Service
public class UserService {
    @Autowired
    private UserInfoMapper userInfoMapper;

    public void registryUser(String name, String password) {
        userInfoMapper.insert(name, password);
    }
}

2.5创建 Controller

java 复制代码
package com.zhongge.controller;

import com.zhongge.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @ClassName UserController
 * @Description TODO 用户控制层
 * @Author 笨忠
 * @Date 2026-04-03 22:03
 * @Version 1.0
 */
@RestController
@RequestMapping("/user")
public class UserController {
    @Autowired
    private UserService userService;

    @RequestMapping("/registry")
    public String registry(String name, String password) {
        userService.registryUser(name, password);
        return "注册成功";
    }
}

3. Spring编程式事务

我们首先来看这个编程是事务,也就是说我们通过自己代码的手写如何实现事务的提交和回滚。

他的话,需要借助一个类,我们的spring给我们提供了一个类叫做DataSourceTransactionManager 它翻译为数据源事务管理器

那这个类呢,我们直接用就可以了:直接用@Autowired的注解把它给注入进来


我们对于数据的操作的话,那:

  • 第1步应该是先开启事务;
  • 第2步,开启完事务部之后呢,我们要进行数据的操作;
  • 然后呢,我们的第3个步:是事务的提交或者回滚,操作成功你就提交,失败你就回滚。

3.1 开启事务

那问题来了,我们该如何开启一个事务呢?

这就要用到 DataSourceTransactionManager (数据源事务管理器),它里面有一个关键方法:getTransaction()。这个方法的作用是获取一个 TransactionStatus(事务状态),同时完成事务的初始化准备。

我们用"装修房子 + 施工许可证"的例子来理解:

你想装修一套房子,不能直接抡起锤子就砸墙。你得先向物业申请一张 《施工许可证》 。这张许可证上会写清楚施工时间、施工范围、施工标准------这些就是 TransactionDefinition(事务定义),里面包含了隔离级别、超时时间、是否只读等规则。

物业审核通过后,会给你发一张 《施工许可证》。但在发证的同时,物业还会悄悄做两件重要的事:

  1. 确认你有施工所需的工具和场地------对应 Spring 从数据源获取数据库连接;
  2. 登记这套房子的装修归属------对应 Spring 把数据库连接绑定到当前线程,确保你后面所有的砸墙、布线、刷漆都用同一套工具,针对同一套房子。

这张 《施工许可证》 就是 Spring 中的 TransactionStatus 。它不是什么数据副本,也不是快照,它只是一个 "控制把手",用来标识"你现在干的这一堆活属于同一个装修项目"。它不存储房子的任何原始数据,也不备份房子的状态。

简单说:拿到 TransactionStatus,不仅代表"你可以开始施工了"(执行数据库操作),更意味着 Spring 已经为你准备好了专属的数据库连接,并且你后面的所有操作都会自动关联到这个事务。


具体过程:如下图所示


如此一来,我们就完成了事物的开启。


3.2 操作数据


3.3 事务的提交或者回滚

那事务的提交或回滚,我们又该怎么操作呢?

还是一样,用 DataSourceTransactionManager 这个事务管理器。它里面提供了两个方法:commit()rollback(),分别对应"提交"和"回滚"。调用这两个方法的时候,都需要传入一个参数------就是我们在开启事务时拿到的那张 《施工许可证》 (也就是 TransactionStatus)。

1. 提交事务:commit(status)

如果你装修完了,所有施工都符合预期,没有砸错墙、没有装歪柜子。你就把 《施工许可证》 交还给物业,同时说:"没问题,我干完了,请确认生效"。物业收到你的指令后,就会把你这段时间的所有施工记录正式备案,你的装修成果永久保留,以后再也不能反悔了。

对应到 Spring 事务机制:Spring 会通过 TransactionStatus 找到绑定的数据库连接,向数据库发送"提交"指令;数据库收到指令后,会将本次事务内的所有操作永久写入磁盘。Spring 只负责传话,真正把数据存下来的,是数据库自己。

2. 回滚事务:rollback(status)

如果你装修到一半,发现砸错了墙、装错了家具(比如数据库操作出错、程序抛异常了)。你也把 《施工许可证》 交还给物业,说:"干砸了,我要取消这次施工"。物业就会根据施工前留下的记录,把房子恢复到装修开始前的样子,就好像你从来没动过一样。

对应到 Spring 事务机制:Spring 同样通过 TransactionStatus 找到绑定的数据库连接,向数据库发送"回滚"指令。数据库收到指令后,会利用底层的 undo log(回滚日志) 撤销本次事务内的所有修改,让数据回到事务开启前的状态。真正实现"房子恢复原样"的,是数据库自己的日志,不是那张许可证。

这里的关键是:《施工许可证》只用来告诉物业"请针对我这一单施工进行提交或回滚",它不备份数据,也不恢复数据。数据怎么恢复,是数据库自己内部的事。

具体过程:如下图所示

提交事务


此时我们就启动服务器看一下我们的结果:

提交成功


回滚事务

结果:

重启服务器运行,你会发现,我们刚才已经插入一条张三,现在我们插入一条李四:

那么按理来说的话,数据库中应该会有两条数据,张三和李四,而我们执行了回滚操作,所以导致李四这条数据是没有被插入到数据库中的,所以数据库中仍然只有张三这条数据,此时就代表我们回滚成功:

数据表中只有一条数据


完整代码

java 复制代码
package com.zhongge.controller;

import com.zhongge.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionStatus;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @ClassName UserController
 * @Description TODO 用户控制层
 * @Author 笨忠
 * @Date 2026-04-03 22:03
 * @Version 1.0
 */
@RestController
@RequestMapping("/user")
@Slf4j//使用日志方便我们观察
public class UserController {
    @Autowired
    private UserService userService;

    //编程时事务需要有的一个类
    @Autowired
    private DataSourceTransactionManager dataSourceTransactionManager;

    @Autowired
    private TransactionDefinition definition;

    @RequestMapping("/registry")
    public String registry(String name, String password) {
        /**
         * 事务相关的操作 我们分三步
         * 1. 开启事务
         * 2. 操作数据
         * 3. 提交事务/回滚 成功:提交事务  失败:回滚事务
         */

        //1.开启事务
        TransactionStatus transaction = dataSourceTransactionManager.getTransaction(definition);
        //2. 操作数据
        userService.registryUser(name, password);
        log.info("用户注册成功");
        //3. 事务的提交
        //dataSourceTransactionManager.commit(transaction);
        //3. 事务的回滚
        dataSourceTransactionManager.rollback(transaction);
        return "注册成功";
    }
}

那上述就是我们的第1种方式,就是通过手动的开启事务和手动的提交和回滚事务。那接下来我们就说第2种方式,就是通过注解来帮我们自动完成事务的开启和提交事务。


4. Spring声明式事务@Transactional

Transactional最常用译法:事务性的

那么在讲注解实现事务之前呢,我们先思考一个问题,我们上述手动编写代码去完成事务的操作,也就是编程事务,我们呢有没有发现一个特点?

答:也就是说它本质就是一个aop,它呢在我们的目标方法执行之前去开启事务,在我们目标方法执行之后去提交或者是回滚事务,这个不就是我们的aop思想吗?
那你再思考一个问题,我们什么时候去回滚事务呢?也不可能说我们写着代码,写完代码你就直接回滚嘛,其实这样的场景是不存在的,我们一般是在我们的sql发生异常之后才会回滚事务,你明明写的好好的,你为什么去回滚事务呢?对吧?只有发生异常我们才会去回滚事务。


那既然声明式事务是基于 AOP 思想实现的,Spring 就会自动帮我们生成一个切面。这个切面会在目标方法执行之前去开启事务,在目标方法执行之后去提交或回滚事务------注意,只有发生异常才会回滚。

那我们开发者需要做什么呢?只需要写中间操作数据的那部分业务代码就行了。其他的,你只需要打上一个标签,也就是加上 @Transactional 注解,Spring 就会自动帮你操作这个方法:在执行目标方法之前开启事务,执行之后根据情况提交或回滚。

简单说:你只管写业务,事务的事交给 Spring

4.1 事务提交




此时我们就启动服务器,然后插入李四这条数据,成功提交 因为没有异常:



4.2 事务回滚

那么上述提交成功的话,那什么时候回滚呢?我们说了,只有 after throwing的时候才会回滚。我们说过它是aop的思想,那么aop中有一个通知类型叫做 after throwing,也就是说只有触发异常的时候,他才会回滚。

所以我们现在人为的去造一个异常,看他会发生什么?

然后呢,我们启动服务器去测试一下,将王五这个数据插入进去,看能否插入成功:

前端的话是返回了一个500的错误

然后你看后面我们的后端打印出来影响行数是一这样的一个注册成功的数据,说明目标方法已经执行了,也就代表你这个数据难道已经插入到数据库里面去了吗?

此时我们赶紧去查数据库,看数据库中有几条数据:

查询之后,你会发现王五这个数据并没有插入到我们的数据表中,因为发生了异常,所以呢,我们加了@Transactional这个注解之后呢,Spring就会为我们在异常的时候将数据给回滚。


完整代码

java 复制代码
@RestController
@RequestMapping("/user2")
@Slf4j//使用日志方便我们观察
public class UserController2 {
    @Autowired
    private UserService userService;

    @Transactional
    @RequestMapping("/registry")
    public String registry(String name, String password) {

        //用户注册
        Integer result = userService.registryUser(name, password);
        log.info("用户注册成功, 影响行数:" + result);

        //人为的造一个异常
        int a = 10/0;
        return "注册成功";
    }
}

4.3 手动捕获异常并处理掉这个异常会怎么样?

那如果我们去手动捕获并处理这个异常会发生什么呢?如下所示:

然后我们接下来就继续启动服务器,继续插入王五这条数据,看看会发生什么?

1、前端显示注册成功

2、后端显示的是操作异常的一个日志。

3、我们的这个数据库中,成功把王五给插进去了。


所以我们虽然会发生异常,但是当你把这个异常进行捕获的时候,此时我们的事务就正常提交,也就是说你虽然出现了一些困难,但是你自己把它处理了,你自己把它给处理掉的话,唉,我们aop就感知不到它的存在,此时就不用我们的spring给我们兜底了。


4.4 我捕获到异常,但我不处理 我将异常抛出去,会怎么样?

也就是说虽然我们去捕获这个异常,但是我不去处理它,我把它给抛出去,那此时会发生什么?

此时我们重启服务器,然后运行结果,看它会发生什么,这次我们将赵六给插入进去,看数据表中是否会出现赵六这行数据:

1、前端返回错误结果

2、后端日志发生异常

3、而此时我们数据库中数据表里面没有将赵六这行数据给插入进去。

说明我们的数据进行了回归,也就是说,如果你虽然捕获了异常,但是你不处理它,你继续把它给抛出去的话,那么我们的事务还是会进行回滚。

完整代码:

java 复制代码
@RestController
@RequestMapping("/user2")
@Slf4j//使用日志方便我们观察
public class UserController2 {
    @Autowired
    private UserService userService;

    @Transactional
    @RequestMapping("/registry")
    public String registry(String name, String password) {

        //用户注册
        Integer result = userService.registryUser(name, password);
        log.info("用户注册成功, 影响行数:" + result);


        try {
            int a = 10/0;
        }catch (Exception e) {
            log.error("查询发生异常!");

            throw new RuntimeException(e);
        }

        return "注册成功";
    }
}

那我如果说出现了异常,然后你去捕获了异常,但是我又不希望你去抛出异常(只要不抛出事务就不会帮我们回滚),但是此时我偏要事务去给我们进行回滚。怎么做? 请继续往下看

4.5 希望自己手动去处理异常,还希望事务帮我们回滚

此时我们就自己手动去回滚事务就行了。也就是说,当发生异常时,我们去捕获这个异常,然后手动处理:使用事务切面支持类 TransactionAspectSupport,调用它里面的 currentTransactionStatus() 方法,获取当前事务的状态对象------这和上面编程式事务中拿到的事务状态是一样的。拿到这个事务状态对象之后,我们再通过它调用 setRollbackOnly() 方法,就能手动回滚事务了。

也就是说呀,从这个图中我们是要明白:如果你发生异常,然后你自己去try-catch了,自己手动去处理这个异常,然后只要你不去重新抛出这个异常的话,那么我们的spring是不会去帮我们回滚的,而我们现在的需求是,我既想要去手动处理异常,不重新抛出异常,然后我也想去让你帮我去回滚,此时我们需要实现这个需求,就得自己去手动回滚事务。

那么重启服务器,我们继续插入赵六这条数据,然后看结果:

首先,目前数据库中有三条数据

然后我们继续前端传递赵六这条数据,看能否插进去,然后再观察它的主键

此时前端显示注册成功,没有报错:

然后我们的数据库数据表中,仍然只有三条数据,他没有将那条赵六数据给插入进去


完整代码

java 复制代码
@RestController
@RequestMapping("/user2")
@Slf4j//使用日志方便我们观察
public class UserController2 {
    @Autowired
    private UserService userService;

    @Transactional
    @RequestMapping("/registry")
    public String registry(String name, String password) {

        //用户注册
        Integer result = userService.registryUser(name, password);
        log.info("用户注册成功, 影响行数:" + result);


        try {
            int a = 10/0;
        }catch (Exception e) {
            log.error("查询发生异常!");
            
            //手动回滚事务
            TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
        }

        return "注册成功";
    }
}

4.6 @Transactional的作⽤(留意

@Transactional 既可以修饰方法,也可以修饰类,但是有其他条件:

  • 修饰方法时:只有 public 方法才会生效(修饰 private 或 protected 方法不会报错,但也不会生效)。推荐加在 public 方法上。

  • 修饰类时:对该类中所有的 public 方法都会生效。

我们推荐你加在方法上,就别加在类上了。

为什么必须是 public?

因为 Spring 事务是基于 AOP(动态代理) 实现的,而动态代理只能拦截 public 方法。非 public 方法无法被代理,所以事务不会生效。

4.7 @Transactional默认回滚规则

@Transactional 默认只在遇到 运行时异常(RuntimeException 及其子类)和 Error 时才会回滚。

如果遇到 非运行时异常(比如 IOException、SQLException),事务不会回滚。

如果需要让非运行时异常也回滚,可以通过 @Transactional(rollbackFor = Exception.class) 来指定。


本篇我们学会了 Spring 事务的基本用法,但 @Transactional 的威力远不止这些。

下一期,我们将深入它的三个核心属性:

🔹 rollbackFor ------ 指定哪些异常触发回滚(默认只回滚运行时异常)

🔹 isolation ------ 事务隔离级别(脏读、不可重复读、幻读,一次讲透)

🔹 propagation ------ 事务传播机制(REQUIRED、REQUIRES_NEW 等 7 种行为,附代码实战)

内容更干、更实用!

如果这篇对你有帮助,别忘了点赞、收藏、关注,下期见!🚀

相关推荐
程序员榴莲7 小时前
Java(八):方法覆盖
java
J2虾虾8 小时前
Java使用jcifs读取Windows的共享文件
java·开发语言·windows
cheoyeon8 小时前
ruoyi-cloud项目开发
spring·spring cloud·maven
Java成神之路-8 小时前
Spring IOC 注解开发实战:从环境搭建到纯注解配置详解(Spring系列3)
java·后端·spring
凌波粒8 小时前
LeetCode--383.赎金信(哈希表)
java·算法·leetcode·散列表
Rsun045518 小时前
Cursor 快捷键 + 提示词速查卡片
spring
贺小涛8 小时前
VictoriaMetrics深度解析
java·网络·数据库
疯狂打码的少年9 小时前
【Day02 Java转Python】Python的ArrayList: list与tuple的“双面人生
java·python·list