【JavaEE】Spring 事务(1)
文章目录
- 【JavaEE】Spring 事务(1)
- 1. 为什么要使用事务
- 2. Spring中事务的实现
- 2.1 事务针对哪些操作
- 2.2 MySQL 事务使用
- 2.3 Spring 编程式事务(手动挡)
- 2.4 Spring 声明式事务(自动挡)
- 2.5 小疑问(@Transactional注解原理)
- 2.5.1 @Transactional注解原理
- 2.5.2 为什么必须被五大类注解修饰
- 2.5.3 为什么@Transactional不支持static方法
- 3. 实践
- 3.1 创建项目
- 3.2 编写代码
- 3.3 测试
- 3.4 注意事项
- 3.4.1 事务不会自动回滚解决方案1:重新抛出
- 3.4.2 事务不会自动回滚解决方案2:手动回滚
【JavaEE】Spring 事务(1)
1. 为什么要使用事务
比如跟钱相关的两个操作:
第一步操作:小马卡里 - 100元
第二步操作:老马卡里 + 100元
这就是一个事务,捆在一起的一组行为,就是事务
而它能保证的是,这个行为的原子性,一致性,隔离性,持久性:
- 两个操作都成功
- 两个操作都失败
要么一起成功,要么一起失败
但是,如果没有事务呢,则两个操作逻辑上是分开的:
- 第一个操作成功,第二个操作失败,则小马直接亏了100!
2. Spring中事务的实现
Spring中的事务操作主要分为两类:
- 编程式事务(原生方式去写代码操作事务)
- 声明式事务(利用注解,“约定规则”去自动开启和提交事务)
2.1 事务针对哪些操作
事务一般针对的是
- 持久化相关的操作,如数据库操作、文件系统操作等
正如刚才的那样,两个用户的转账操作
- 保证数据完整性的操作,如消息队列等
通过使用事务,可以在消息队列中提供可靠的消息传递机制,减少消息丢失或重复处理的可能性,同时确保系统在出现故障情况下能够正确恢复
事务的概念适用于需要保证一系列操作的原子性和一致性的任何场景
- 而其中,被持久化的数据,被传播的数据…等操作,都具有 “持久性影响” 的作用,所以要通过事务来控制其影响不要太糟糕
- 而一些操作,比如打印,都打印到控制台了,不会回滚的,也没有必要回滚,例如查看执行日志…
- 至于其他的不可见的操作,又没有持久化,是没有影响力的,程序出异常后,这些数据也销毁了~
2.2 MySQL 事务使用
--- 开启事务
start transaction;
--- transaction就是事务的意思
--- 提交事务
commit;
--- 回滚事务
rollback;
三个重要的操作:
- 开启事务
- 提交事务
- 回滚事务
2.3 Spring 编程式事务(手动挡)
与MySQL操作事务类似:
- 开启事务(获取一个事务/创建一个事务并获取)
- 提交事务
- 回滚事务
SpringBoot 内置了两个对象:
- DataSourceTransactionManager ⽤来获取事务(开启事务)、提交或 回滚事务的
- TransactionDefinition 是事务的属性,在获取事务的时候需要将 TransactionDefinition 传递进去从而获得⼀个事务 TransactionStatus
实现代码如下:
@RestController public class UserController { @Resource private UserService userService; // JDBC 事务管理器 @Resource private DataSourceTransactionManager dataSourceTransactionManager; // ------------定义事务属性------------ @Resource private TransactionDefinition transactionDefinition; @RequestMapping("/sava") public Object save(User user) { // ------------开启事务------------ TransactionStatus transactionStatus = dataSourceTransactionManager .getTransaction(transactionDefinition); // ------------插⼊数据库------------ int result = userService.save(user); // ------------提交事务------------ dataSourceTransactionManager.commit(transactionStatus); // // ------------回滚事务------------ // dataSourceTransactionManager.rollback(transactionStatus); return result; } }
反正就是,麻烦,难记,不简洁,容易出错(难写)
2.4 Spring 声明式事务(自动挡)
声明式事务的实现很简单
- 只需要在需要的类或者方法上添加 @Transactional 注解 就可以实现了
无需手动开启/提交/回滚事务:
- 进入方法,自动开启事务
- 方法执行完会,自动提交事务
- 如果中途发生了没有处理的异常,自动回滚事务
具体规则/作用范围是:
- 加在类上,内部的所有非静态public方法都相当于加了 @Transactional 注解
- 加在非静态public方法上,这个方法就是一个事务
- 所在的类,必须被五大类注解修饰,这跟其事务的实现有关
- 而且有了五大类注解,Spring开发才能进行呀~
代码实现:
@Service
@Transactional
public class Test {
@Autowired
private Mapper mapper;
public int save(User user) {
mapper.save(user);
}
}
@RequestMapping("/save")
@Transactional
public Object save(User user) {
int result = userService.save(user);
return result;
}
跟往常的注解版和不使用注解版的代码一样:
- 不使用注解版: 灵活,能实现很多功能,但是麻烦,使用困难,甚至正常人压根没法写,例如事务传播机制的代码实现起来就比较复杂
- 使用注解版: 使用规则约束,实现特定功能,但是方便,使用简单,且足以面对正常开发环境,不关心一些极端的不正常开发
- 对于注解的使用,就是:遵循约定,坐享其成,明白逻辑(作用),合理使用(逻辑分析合理)
编程式就相当于车的手动挡,声明式就相当于车的自动挡,那么现实咱们买不起偏贵的自动挡车,而我们现在可以无条件舒适地使用自动挡,那咋不用嘞🤣🤣🤣
2.5 小疑问(@Transactional注解原理)
2.5.1 @Transactional注解原理
- 这个行为,可能你也意识到了,其实就是AOP,对@Transactional注解下的代码,进行统一的处理
- 当然,对于不同的事务/复杂事务,处理可能不同~
- 这个在执行日志中也能看到,可以平时观察观察~
@Transactional 实现思路图:
@Transactionl执行思路图:
默认就是这么一个事务管理器执行这样的逻辑
- 而如果配置了多个事务管理器,则需要通过参数value/transactionManager去指定
2.5.2 为什么必须被五大类注解修饰
其实就是因为
@Transactional注解是基于Spring AOP的,而Spring AOP则通过JDK的或者CGLib的动态代理来实现AOP
对于使用
@Transactional
注解来实现事务管理,确实是通过动态代理来实现的
- 当你在一个类或方法上添加了
@Transactional
注解时,Spring会通过动态代理在运行时为该类或方法创建一个代理对象。这个代理对象会拦截调用,并在适当的时机开启、提交或回滚事务由于动态代理的实现方式,确实需要满足一些条件才能使
@Transactional
注解生效
- 具体来说,被注解的类或方法必须是Spring容器中的bean,而Spring容器会自动为标注了
@Service
、@Controller
、@Repository
、@Component
和@Configuration
等注解的类创建bean实例。这也是为什么我之前提到了五大类注解
2.5.3 为什么@Transactional不支持static方法
其实就是因为
无论JDK还是CGlib都无法对静态方法提供代理。原因在于静态方法是类级别的,调用需要知道类信息,而类信息在编译器就已经知道了,并 不支持在运行期的动态绑定
3. 实践
3.1 创建项目
为了方便,我就直接使用之前mybatis项目里写过的代码了
- 因为我们目前侧重学习的点是在事务的实现!
model.UserInfo:
@Component @Data public class UserInfo { private int id; private String username; private String password; private String photo; private LocalDateTime createtime; private LocalDateTime updatetime; private Integer state; public UserInfo(String username, String password, Integer state) { this.username = username; this.password = password; this.state = state; } public UserInfo() { } }
mapper.UserMapper:
@Mapper //跟五大类注解@Repository,say 拜拜 public interface UserMapper { List<UserInfo> getAll(); //获得所有用户信息 UserInfo getUserById(Integer id); //通过id查找用户 UserInfo getUserByUsername(@Param("username") String username); //通过username查找用户 List<UserInfo> getAll2(@Param("option") String option); UserInfo login(@Param("username") String username, @Param("password") String password); int update(UserInfo userInfo); //删除状态为state的用户 int delete(@Param("state") Integer state); //增加用户 int insert(UserInfo userInfo); List<UserInfo> getAllLikeSome(@Param("likeString") String likeString); //用户注册提交信息 // int add(UserInfo userInfo); int add(String username, String password, Integer state, Integer id); int add2(UserInfo userInfo); List<UserInfo> select1(UserInfo userInfo); int update2(UserInfo userInfo); int deleteByIDs(List<Integer> list); int insertByUsers(List<UserInfo> list, List<UserInfo> list2); }
mybatis.UserInfoMapper.xml:
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.example.demo.mapper.UserMapper"> <resultMap id="BaseMap" type="com.example.demo.model.UserInfo"> </resultMap> <select id="getAll" resultMap="BaseMap"> select * from userinfo </select> <!-- <select id="getAll" resultType="com.example.demo.model.UserInfo">--> <!-- select id, username as name, password, photo,--> <!-- createtime, updatetime, state from userinfo--> <!-- </select>--> <select id="getUserById" resultType="com.example.demo.model.UserInfo"> select * from userinfo where id = #{id} </select> <select id="getUserByUsername" resultType="com.example.demo.model.UserInfo"> select * from userinfo where username = ${username} </select> <select id="getAll2" resultType="com.example.demo.model.UserInfo"> select * from userinfo order by id ${option} </select> <select id="login" resultType="com.example.demo.model.UserInfo"> select * from userinfo where username = '${username}' and password = '${password}' </select> <update id="update"> update userinfo set state = #{state} where username = #{username} </update> <delete id="delete"> delete from userinfo where state = #{state} </delete> <insert id="insert" useGeneratedKeys="true" keyColumn="id" keyProperty="id"> <!-- 自增主键 id 不能为null也没有默认值,如果id不设置或者设置为null,都会导致自增 --> insert into userinfo (username, password) values (#{username}, #{password}); </insert> <select id="getAllLikeSome" resultType="com.example.demo.model.UserInfo"> select * from userinfo where username like concat('%', #{likeString}, '%') </select> <insert id="add"> insert into userinfo ( <if test="id != 0"> id, </if> <if test="username != null"> username, </if> <if test="password != null"> password, </if> <if test="state != null"> state </if> ) values ( <if test="id != 0"> #{id}, </if> <if test="username != null"> #{username}, </if> <if test="password != null"> #{password}, </if> <if test="state != null"> #{state} </if> ) </insert> <insert id="add2"> insert into userinfo <trim prefix="(" suffix=")" suffixOverrides=","> <if test="id != 0"> id, </if> <if test="username != null"> username, </if> <if test="password != null"> password, </if> <if test="state != null"> state </if> </trim> values <trim prefix="(" suffix=")" suffixOverrides=","> <if test="id != 0"> #{id}, </if> <if test="username != null"> #{username}, </if> <if test="password != null"> #{password}, </if> <if test="state != null"> #{state} </if> </trim> </insert> <select id="select1" resultType="com.example.demo.model.UserInfo"> select * from userinfo <where> <if test="id != 0"> id = #{id} </if> <if test="username != null"> or username = #{username} </if> <if test="password != null"> or password = #{password} </if> <if test="state != null"> or state = #{state} </if> </where> <!-- <trim prefix="where" prefixOverrides="and">--> <!-- <trim prefixOverrides="or">--> <!-- <if test="id != 0">--> <!-- id = #{id}--> <!-- </if>--> <!-- <if test="username != null">--> <!-- or username = #{username}--> <!-- </if>--> <!-- <if test="password != null">--> <!-- or password = #{password}--> <!-- </if>--> <!-- <if test="state != null">--> <!-- or state = #{state}--> <!-- </if>--> <!-- </trim>--> <!-- </trim>--> </select> <update id="update2"> update userinfo <set> <if test="username != null"> username = #{username}, </if> <if test="password != null"> password = #{password}, </if> <if test="state != null"> state = #{state} </if> </set> where id = #{id} </update> <delete id="deleteByIDs"> delete from userinfo where id in <foreach collection="list" open="(" close=")" item="x" separator=","> #{x} </foreach> </delete> <insert id="insertByUsers"> insert into userinfo(username, password, state) values <foreach collection="list" item="x" open="(" close=")" separator="),("> #{x.username}, #{x.password}, #{x.state} </foreach> , <foreach collection="list2" item="x" open="(" close=")" separator="),("> #{x.username}, #{x.password}, #{x.state} </foreach> </insert> </mapper>
application.properties:
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/test_db?characterEncoding=utf8 # MyBatis 基于jdbc实现~ 底层用的就是jdbc:mysql协议,这个地址是本地数据库的地址,test_db就是我们的那个数据库 spring.datasource.username=root # 用户名,默认固定是root spring.datasource.password=mmsszsd666 # 密码,是数据库的密码 spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver # MyBatis配置信息 mybatis.mapper-locations=classpath:mybatis/*Mapper.xml # 执行时打印SQL mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl #由于其默认情况下的日志类型为Debug,重要程度不高,所以我们需要设置我们对应的目录下的日志级别 logging.level.com.example.demo.controller=debug #将数据库中的下换线转换成驼峰,比如 user_name -> userName mybatis-plus.configuration.map-underscore-to-camel-case=true
目录结构:
3.2 编写代码
同样的controller接受请求,service调用方法~
加@Transactional:
3.3 测试
访问路由前:
delete from userinfo;
现在userinfo一条数据都没有了~
效果:
浏览器:
控制台:
数据库:
- 符合预期:还是空的
- 因为发生了因为@Transactional捕获到了异常,发生回滚
去这段代码后,效果:
3.4 注意事项
@Transactional 在异常被 try{}catch(){}
捕获的情况下,不会进行事务自动回滚,这也很好理解,因为 try{}catch(){}
后,后面的代码可以继续运行,这个异常是被我们写的 try{}catch(){}
抢走处理了,注解是捕获不到的~
代码:
效果:
浏览器:
控制台:
数据库:
说明没有回滚
3.4.1 事务不会自动回滚解决方案1:重新抛出
效果:
无新增数据,代表回滚成功
但是这不太美观,“优雅”,过于暴力
3.4.2 事务不会自动回滚解决方案2:手动回滚
TransactionAspectSupport.currentTransactionStatus()
可以得到当前的事务,然后设置回滚方法setRollbackOnly
就可以实现将当前事务的回滚了
- 跟切面有关=>aop
效果:
无新增数据,代表回滚成功
这种方式就比较“优雅”了~
文章到此结束!谢谢观看
可以叫我 小马,我可能写的不好或者有错误,但是一起加油鸭🦆!代码:事务/src/main · 游离态/马拉圈2023年8月 - 码云 - 开源中国 (gitee.com)