老铁们你有没有遇到过这种情况给朋友转账你这边扣了钱对方却没收到。你打电话问银行银行说“系统出了点小问题”。你心里一万只羊驼跑过我的钱呢飞了这就是没有事务的后果。事务就是用来解决这种问题的一组操作要么全部成功要么全部失败绝对不会出现“一半成功一半失败”的情况。今天我们就来学习 Spring 中的事务看看怎么用最简单的方式保证数据的安全。尤其是 Transactional 这个注解一、什么是事务我们先回顾一下事务的概念我们的事务它不仅仅是mysql中的事物那事务它是一个概念它并不是属于某个数据库的。事务是一组操作的集合是一个不可分割的操作事务会把所有的操作作为一个整体一起向数据库提交或者撤销操作请求。所以这种操作要么同时成功要么同时失败。二、为什么需要事务我们在进行程序开发的时候也会有事务的需求比如转账操作第1步a账户扣了100块钱第2步b账户增了100块钱那么如果没有事务此时第1步执行成功了而第2步执行失败了那么a账户的100块钱就平白无故的消失了所以呢使用事务就可以解决这个问题让这一组操作要么一起成功要么一起失败。再比如我们的秒杀系统第1步下单成功第2步扣减库存下单成功之后库存也需要同步减少那如果你下单成功而库存扣减失败的话那么就会造成下单超出的情况所以就需要把这两个步操作呢放在同一个事务中要么一起成功要么一起失败。三、我们 MYSQL中事务的操作回顾一下我们mysql中事务的操作主要有三步:开启事务start transition/begin(一组操作前开启事务)提交事务commit(这组操作全部成功提交事务)回滚事务roll back(这组操作中任何一个操作出现异常那么就回归事务)而我们做项目的时候也是会操作多条数据的或者我用一个接口来减100一个接口来加100我要两个接口同时成功或同时失败。所以呢我们本期就学习spring中事务是如何实现的。四、Spring中事务的实现在我们前面的课程讲了mysqll的事务操作Spring对事务也进行了实现我们Spring中的事务操作分为两大类编程式事务就是说你手动写代码操作事务声明式事务就是说利用注解自动开启和提交事务那在学习事务之前我们先准备数据和数据的访问代码需求我们这里有一个需求就是用户注册注册的时候在日志中插入一条操作记录。即用户注册时要干两件事向 user_info 表插入用户数据向 log_info 表插入一条操作日志这两步必须在同一个事务中要么都成功要么都失败。1. 数据准备-- 删除用户表如果存在droptableifexistsuser_info;-- 创建用户表createtableuser_info(idintnotnullauto_incrementcomment用户主键ID,user_namevarchar(128)notnullcomment用户名,passwordvarchar(128)notnullcomment用户密码,create_timedatetimedefaultcurrent_timestampcomment创建时间,update_timedatetimedefaultcurrent_timestamponupdatecurrent_timestampcomment更新时间,primarykey(id))engineinnodbdefaultcharactersetutf8mb4comment用户表;-- 删除操作日志表如果存在droptableifexistslog_info;-- 创建操作日志表createtablelog_info(idintprimarykeyauto_incrementcomment日志主键ID,user_namevarchar(128)notnullcomment操作用户名,opvarchar(256)notnullcomment操作内容描述,create_timedatetimedefaultcurrent_timestampcomment创建时间,update_timedatetimedefaultcurrent_timestamponupdatecurrent_timestampcomment更新时间)engineinnodbdefaultcharsetutf8mb4comment操作日志表;2. 项目搭建2.1 配置数据库的连接spring:application:name:spring-transdatasource:url:jdbc:mysql://localhost:3306/javaee_test?useSSLfalsecharacterEncodingutf8username:root# 你的数据库用户名password:123456# 你的数据库密码driver-class-name:com.mysql.cj.jdbc.Drivermybatis:configuration:log-impl:org.apache.ibatis.logging.stdout.StdOutImpl# 打印SQL日志map-underscore-to-camel-case:true# 开启驼峰命名自动映射注意2.2 实体类// UserInfo.javaimportlombok.Data;importjava.util.Date;DatapublicclassUserInfo{privateIntegerid;privateStringuserName;privateStringpassword;privateDatecreateTime;privateDateupdateTime;}// LogInfo.javaimportlombok.Data;importjava.util.Date;DatapublicclassLogInfo{privateIntegerid;privateStringuserName;privateStringop;privateDatecreateTime;privateDateupdateTime;}2.3 创建 Mapper 接口packagecom.zhongge.mapper;importorg.apache.ibatis.annotations.Insert;importorg.apache.ibatis.annotations.Mapper;/** * ClassName LogInfoMapper * Description TODO 日志持久层 * Author 笨忠 * Date 2026-04-03 22:00 * Version 1.0 */MapperpublicinterfaceLogInfoMapper{Insert(INSERT INTO log_info(user_name, op) VALUES(#{name}, #{op}))IntegerinsertLog(Stringname,Stringop);}packagecom.zhongge.mapper;importorg.apache.ibatis.annotations.Insert;importorg.apache.ibatis.annotations.Mapper;/** * ClassName UserInfoMapper * Description TODO 用户持久层 * Author 笨忠 * Date 2026-04-03 21:59 * Version 1.0 */MapperpublicinterfaceUserInfoMapper{Insert(INSERT INTO user_info(user_name, password) VALUES(#{name}, #{password}))Integerinsert(Stringname,Stringpassword);}2.4 创建 Service 层packagecom.zhongge.service;importcom.zhongge.mapper.LogInfoMapper;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.stereotype.Service;/** * ClassName LogService * Description TODO 日志服务层 * Author 笨忠 * Date 2026-04-03 22:02 * Version 1.0 */ServicepublicclassLogService{AutowiredprivateLogInfoMapperlogInfoMapper;publicvoidinsertLog(Stringname,Stringop){logInfoMapper.insertLog(name,op);}}packagecom.zhongge.service;importcom.zhongge.mapper.UserInfoMapper;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.stereotype.Service;/** * ClassName UserService * Description TODO 用户服务层 * Author 笨忠 * Date 2026-04-03 22:01 * Version 1.0 */ServicepublicclassUserService{AutowiredprivateUserInfoMapperuserInfoMapper;publicvoidregistryUser(Stringname,Stringpassword){userInfoMapper.insert(name,password);}}2.5创建 Controllerpackagecom.zhongge.controller;importcom.zhongge.service.UserService;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.web.bind.annotation.RequestMapping;importorg.springframework.web.bind.annotation.RestController;/** * ClassName UserController * Description TODO 用户控制层 * Author 笨忠 * Date 2026-04-03 22:03 * Version 1.0 */RestControllerRequestMapping(/user)publicclassUserController{AutowiredprivateUserServiceuserService;RequestMapping(/registry)publicStringregistry(Stringname,Stringpassword){userService.registryUser(name,password);return注册成功;}}3. Spring编程式事务我们首先来看这个编程是事务也就是说我们通过自己代码的手写如何实现事务的提交和回滚。他的话需要借助一个类我们的spring给我们提供了一个类叫做DataSourceTransactionManager 它翻译为数据源事务管理器那这个类呢我们直接用就可以了直接用Autowired的注解把它给注入进来我们对于数据的操作的话那第1步应该是先开启事务第2步开启完事务部之后呢我们要进行数据的操作然后呢我们的第3个步是事务的提交或者回滚操作成功你就提交失败你就回滚。3.1 开启事务那问题来了我们该如何开启一个事务呢这就要用到DataSourceTransactionManager数据源事务管理器它里面有一个关键方法getTransaction()。这个方法的作用是获取一个TransactionStatus事务状态同时完成事务的初始化准备。我们用“装修房子 施工许可证”的例子来理解你想装修一套房子不能直接抡起锤子就砸墙。你得先向物业申请一张《施工许可证》。这张许可证上会写清楚施工时间、施工范围、施工标准——这些就是TransactionDefinition事务定义里面包含了隔离级别、超时时间、是否只读等规则。物业审核通过后会给你发一张《施工许可证》。但在发证的同时物业还会悄悄做两件重要的事确认你有施工所需的工具和场地——对应 Spring 从数据源获取数据库连接登记这套房子的装修归属——对应 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回滚日志撤销本次事务内的所有修改让数据回到事务开启前的状态。真正实现“房子恢复原样”的是数据库自己的日志不是那张许可证。这里的关键是《施工许可证》只用来告诉物业“请针对我这一单施工进行提交或回滚”它不备份数据也不恢复数据。数据怎么恢复是数据库自己内部的事。具体过程如下图所示提交事务此时我们就启动服务器看一下我们的结果提交成功回滚事务结果重启服务器运行你会发现我们刚才已经插入一条张三现在我们插入一条李四那么按理来说的话数据库中应该会有两条数据张三和李四而我们执行了回滚操作所以导致李四这条数据是没有被插入到数据库中的所以数据库中仍然只有张三这条数据此时就代表我们回滚成功数据表中只有一条数据完整代码packagecom.zhongge.controller;importcom.zhongge.service.UserService;importlombok.extern.slf4j.Slf4j;importorg.springframework.beans.factory.annotation.Autowired;importorg.springframework.jdbc.datasource.DataSourceTransactionManager;importorg.springframework.transaction.TransactionDefinition;importorg.springframework.transaction.TransactionStatus;importorg.springframework.web.bind.annotation.RequestMapping;importorg.springframework.web.bind.annotation.RestController;/** * ClassName UserController * Description TODO 用户控制层 * Author 笨忠 * Date 2026-04-03 22:03 * Version 1.0 */RestControllerRequestMapping(/user)Slf4j//使用日志方便我们观察publicclassUserController{AutowiredprivateUserServiceuserService;//编程时事务需要有的一个类AutowiredprivateDataSourceTransactionManagerdataSourceTransactionManager;AutowiredprivateTransactionDefinitiondefinition;RequestMapping(/registry)publicStringregistry(Stringname,Stringpassword){/** * 事务相关的操作 我们分三步 * 1. 开启事务 * 2. 操作数据 * 3. 提交事务/回滚 成功提交事务 失败回滚事务 *///1.开启事务TransactionStatustransactiondataSourceTransactionManager.getTransaction(definition);//2. 操作数据userService.registryUser(name,password);log.info(用户注册成功);//3. 事务的提交//dataSourceTransactionManager.commit(transaction);//3. 事务的回滚dataSourceTransactionManager.rollback(transaction);return注册成功;}}那上述就是我们的第1种方式就是通过手动的开启事务和手动的提交和回滚事务。那接下来我们就说第2种方式就是通过注解来帮我们自动完成事务的开启和提交事务。4. Spring声明式事务TransactionalTransactional最常用译法事务性的那么在讲注解实现事务之前呢我们先思考一个问题我们上述手动编写代码去完成事务的操作也就是编程事务我们呢有没有发现一个特点答也就是说它本质就是一个aop它呢在我们的目标方法执行之前去开启事务在我们目标方法执行之后去提交或者是回滚事务这个不就是我们的aop思想吗那你再思考一个问题我们什么时候去回滚事务呢也不可能说我们写着代码写完代码你就直接回滚嘛其实这样的场景是不存在的我们一般是在我们的sql发生异常之后才会回滚事务你明明写的好好的你为什么去回滚事务呢对吧只有发生异常我们才会去回滚事务。那既然声明式事务是基于 AOP 思想实现的Spring 就会自动帮我们生成一个切面。这个切面会在目标方法执行之前去开启事务在目标方法执行之后去提交或回滚事务——注意只有发生异常才会回滚。那我们开发者需要做什么呢只需要写中间操作数据的那部分业务代码就行了。其他的你只需要打上一个标签也就是加上 Transactional 注解Spring 就会自动帮你操作这个方法在执行目标方法之前开启事务执行之后根据情况提交或回滚。简单说你只管写业务事务的事交给 Spring4.1 事务提交此时我们就启动服务器然后插入李四这条数据成功提交 因为没有异常4.2 事务回滚那么上述提交成功的话那什么时候回滚呢我们说了只有 after throwing的时候才会回滚。我们说过它是aop的思想那么aop中有一个通知类型叫做 after throwing也就是说只有触发异常的时候他才会回滚。所以我们现在人为的去造一个异常看他会发生什么然后呢我们启动服务器去测试一下将王五这个数据插入进去看能否插入成功前端的话是返回了一个500的错误然后你看后面我们的后端打印出来影响行数是一这样的一个注册成功的数据说明目标方法已经执行了也就代表你这个数据难道已经插入到数据库里面去了吗此时我们赶紧去查数据库看数据库中有几条数据查询之后你会发现王五这个数据并没有插入到我们的数据表中因为发生了异常所以呢我们加了Transactional这个注解之后呢Spring就会为我们在异常的时候将数据给回滚。完整代码RestControllerRequestMapping(/user2)Slf4j//使用日志方便我们观察publicclassUserController2{AutowiredprivateUserServiceuserService;TransactionalRequestMapping(/registry)publicStringregistry(Stringname,Stringpassword){//用户注册IntegerresultuserService.registryUser(name,password);log.info(用户注册成功, 影响行数result);//人为的造一个异常inta10/0;return注册成功;}}4.3 手动捕获异常并处理掉这个异常会怎么样那如果我们去手动捕获并处理这个异常会发生什么呢如下所示:然后我们接下来就继续启动服务器继续插入王五这条数据看看会发生什么1、前端显示注册成功2、后端显示的是操作异常的一个日志。3、我们的这个数据库中成功把王五给插进去了。所以我们虽然会发生异常但是当你把这个异常进行捕获的时候此时我们的事务就正常提交也就是说你虽然出现了一些困难但是你自己把它处理了你自己把它给处理掉的话唉我们aop就感知不到它的存在此时就不用我们的spring给我们兜底了。4.4 我捕获到异常但我不处理 我将异常抛出去会怎么样也就是说虽然我们去捕获这个异常但是我不去处理它我把它给抛出去那此时会发生什么此时我们重启服务器然后运行结果看它会发生什么这次我们将赵六给插入进去看数据表中是否会出现赵六这行数据1、前端返回错误结果2、后端日志发生异常3、而此时我们数据库中数据表里面没有将赵六这行数据给插入进去。说明我们的数据进行了回归也就是说如果你虽然捕获了异常但是你不处理它你继续把它给抛出去的话那么我们的事务还是会进行回滚。完整代码RestControllerRequestMapping(/user2)Slf4j//使用日志方便我们观察publicclassUserController2{AutowiredprivateUserServiceuserService;TransactionalRequestMapping(/registry)publicStringregistry(Stringname,Stringpassword){//用户注册IntegerresultuserService.registryUser(name,password);log.info(用户注册成功, 影响行数result);try{inta10/0;}catch(Exceptione){log.error(查询发生异常);thrownewRuntimeException(e);}return注册成功;}}那我如果说出现了异常然后你去捕获了异常但是我又不希望你去抛出异常(只要不抛出事务就不会帮我们回滚)但是此时我偏要事务去给我们进行回滚。怎么做 请继续往下看4.5 希望自己手动去处理异常还希望事务帮我们回滚此时我们就自己手动去回滚事务就行了。也就是说当发生异常时我们去捕获这个异常然后手动处理使用事务切面支持类 TransactionAspectSupport调用它里面的 currentTransactionStatus() 方法获取当前事务的状态对象——这和上面编程式事务中拿到的事务状态是一样的。拿到这个事务状态对象之后我们再通过它调用 setRollbackOnly() 方法就能手动回滚事务了。也就是说呀从这个图中我们是要明白如果你发生异常然后你自己去try-catch了自己手动去处理这个异常然后只要你不去重新抛出这个异常的话那么我们的spring是不会去帮我们回滚的而我们现在的需求是我既想要去手动处理异常不重新抛出异常然后我也想去让你帮我去回滚此时我们需要实现这个需求就得自己去手动回滚事务。那么重启服务器我们继续插入赵六这条数据然后看结果首先目前数据库中有三条数据然后我们继续前端传递赵六这条数据看能否插进去然后再观察它的主键此时前端显示注册成功没有报错然后我们的数据库数据表中仍然只有三条数据他没有将那条赵六数据给插入进去完整代码RestControllerRequestMapping(/user2)Slf4j//使用日志方便我们观察publicclassUserController2{AutowiredprivateUserServiceuserService;TransactionalRequestMapping(/registry)publicStringregistry(Stringname,Stringpassword){//用户注册IntegerresultuserService.registryUser(name,password);log.info(用户注册成功, 影响行数result);try{inta10/0;}catch(Exceptione){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 种行为附代码实战内容更干、更实用如果这篇对你有帮助别忘了点赞、收藏、关注下期见