文章目录
- 前言
- 一、事务是用来干什么的?事务是用来解决什么问题的?
- 二、事务执行的流程
- 三、事务的ACID四大特性
- 四、事务的四种隔离级别
- 五、声明式事务@Transactional
- 六、web层实战
- 七、service层实战
- 7.1 BookBorrowService
- 7.2 细数BookBorrowServiceImpl实战细节
- 7.2.1 rollbackFor
- 7.2.2 @Transactional不生效的场景
- 八、dal层实战
- 九、测试和Git提交
- 最后
前言
通过上文我们实现了图书借阅审核列表,本文实战的场景是:审核图书借阅!因为审核后最终会执行2条更新SQL,所以就派上了事务
。
事务是用来干什么的?事务是用来解决什么问题的? 什么情况下用到事务?
对于SpringBoot,我们主要使用的是声明式事务@Transactional,不仅有实战的用法,我还会用白话讲解事务的执行流程、ACID四大特性、MySQL四种隔离级别,以及 @Transactional不生效场景 坑点 (也适用于其它AOP注解)!
掌握这些应对大部分项目绰绰有余,面对一般面试也会尽在掌握中,OK,话不多说,嗨一波,Let’s Go!
一、事务是用来干什么的?事务是用来解决什么问题的?
- 事务是用来干什么的?干嘛的?
- 事务是用来解决什么问题的?
- 事务是用来保证什么的?
- 事务一般用在什么地方?
- 什么情况下用到事务?
如果你有上面这些疑问,我想通过本文的审核场景 来回答你的疑问!
为了方便你理解需求,我画了一个流程图,如下:
审核通过或驳回后主要干两件事:
-
修改 图书借阅表
book_borrowing
的状态status
字段(1-通过,2-驳回) -
审核通过:修改 图书表
book
的借阅数量borrow_count
+1驳回:修改 图书表
book
的status
字段(0-空闲)
那么这时问题就来了,不管是通过还是驳回,一个方法内都需要更新2条SQL,那么这时如果出现一个sql成功,一个sql失败,数据不就不一致了吗?
那怎么保证2个SQL要么全成功,要么全失败,保证数据的一致性呢?
那么在这种情况下就需要SQL事务
了,简单来说:用它来保证多条SQL的数据一致!!!
怎么保证的呢?具体看下面的执行流程~
二、事务执行的流程
对于事务动作行为有3种:
- 开始 begin
- 提交 commit
- 回滚 rollback
那么串起来看一下它的执行流程,如下图:
从流程可以看出来,不管执行多少条SQL,最终都会一起【提交】或【回滚】!也就能保证多条SQL要么全成功,要么全失败,保证数据的一致!!!
三、事务的ACID四大特性
谈到事务,不得不说事务的四大特性,尽管是老生常谈,也是面试常见问题,但依然有很多人说不明白,因为确实太抽象,这里我想用白话的方式助你打通你的任督二脉:
- 原子性(Atomicity):就像化学中学过的原子,最小单位不可拆分,是一个整体,要么全成功,要么全失败;
- 一致性(Consistency):事务不管怎么搞,最终的数据都是一致的;
- 隔离性(Isolation):不同事务之间是隔离的,具体隔离的度取决于对应数据库的事务隔离级别;
- 持久性(Durability):事务结束以后数据会持久化
四、事务的四种隔离级别
上面提到的数据库的事务隔离级别,每个数据库的实现不尽相同,例如MySQL实现了4种隔离级别:
- 读未提交(Read uncommitted):A事务能读到B事务执行过但
未提交
的SQL的结果 - 读已提交(Read committed):A事务能读到B事务执行过也
已提交
的SQL的结果 - 可重复读(Repeatable read):A事务对于同一个查询SQL,每次读取的结果也是
相同的
,尽管B事务已经修改了结果并提交了事务 - 串行化(Serializable):A事务与B事务按顺序执行
因为【读未提交】会产生脏读,【串行化】性能低,所以常用的是【读已提交】和【可重复读】,因为【可重复读】在MySQL会有【间隙锁】,所以更多会使用【读已提交】这个事务隔离级别。
如果你想了解更多,像脏读、幻读、间隙锁、MVCC机制,推荐阅读我之前写的MySQL相关文章,例如:
【MySQL】事务隔离机制 – 必须说透
【MySQL】MVCC原理分析 + 源码解读 – 必须说透
五、声明式事务@Transactional
在Spring中,事务的实现有声明式事务
和编程式事务
。
-
声明式事务
就是利用AOP切面实现,就像 【7.6 SpringBoot AOP实战 统一角色权限校验】,定义注解实现切面。声明式事务正是通过
@Transactional
注解 控制事务的提交和回滚!对代码无侵入性! -
编程式事务
就是编程直接手写,手动控制事务的提交和回滚!对代码有侵入性,好处是灵活,可以做到代码块级别!
在SpringBoot中,当然推荐使用【声明式事务】AOP的实现
@Transactional
注解。具体使用实战代码继续向下看~
六、web层实战
对于借阅审核API,依然定义在BookAdminController中,post请求(@PostMapping
) + body传参(@RequestBody
),并支持管理员角色校验@Role
,最后返回通用泛型结果TgResult<String>
,代码如下:
@Role
@PostMapping("/book/borrow/examine")
public TgResult<String> examineBookBorrow(@RequestBody ExamineBookBorrowParamVO paramVO) {
return bookBorrowService.examineBookBorrow(paramVO.getBorrowId()
, paramVO.getApproved(), paramVO.getRejectReason());
}
其中的VO入参对象定义如下:
@Data
public class ExamineBookBorrowParamVO implements Serializable {
private Integer borrowId;
// 是否审核通过
private Boolean approved;
// 驳回原因
private String rejectReason;
}
七、service层实战
7.1 BookBorrowService
在BookBorrowService中,新增 审核图书借阅 examineBookBorrow
方法定义
public interface BookBorrowService {
/**
* 审核图书借阅
**/
TgResult<String> examineBookBorrow(Integer borrowId, Boolean approved, String rejectReason);
}
7.2 细数BookBorrowServiceImpl实战细节
写的比较细节,实战就更不能忽略细节!分3块解释如下:
-
- 校验
-
- 更新 图书借阅表
-
- 更新 图书表
每块的代码,我相信你都能看的懂,如果跟着学到这还看不懂的,需要补补前面的知识啦~~~
@Autowired
private BookMapperExt bookMapperExt;
@Transactional(rollbackFor = Exception.class)
@Override
public TgResult<String> examineBookBorrow(Integer borrowId, Boolean approved, String rejectReason) {
// 1. 校验
BookBorrowing existsPo = bookBorrowingMapper.selectByPrimaryKey(borrowId);
if (existsPo == null) {
// 不存在
return TgResult.fail("400", "图书借阅记录不存在! id=" + borrowId);
}
if (!BookBorrowStatusEnum.TO_BE_EXAMINE.getCode().equals(existsPo.getStatus())) {
// 借阅状态不是:待审核
return TgResult.fail("400", "图书借阅状态不是待审核状态!");
}
// 2.更新 图书借阅表
BookBorrowing po = new BookBorrowing();
po.setId(borrowId);
po.setStatus(BookBorrowStatusEnum.getStatusCode(approved));
po.setRejectReason(rejectReason);
po.setVerifyTime(new Date());
po.setVerifyUserId(AuthContextInfo.getAuthInfo().loginUserId());
int rows = bookBorrowingMapper.updateByPrimaryKeySelective(po);
if (rows > 0) {
// 3.更新图书表
Integer bookId = existsPo.getBookId();
if (Boolean.TRUE.equals(approved)) {
// 审核通过, 图书借阅次数+1。
// 这里之所以不用updateByPrimaryKeySelective, 是因为如果不查一次mysql不知道现在的borrowCount
bookMapperExt.increaseBorrowCount(bookId);
} else {
// 审核驳回, 图书状态修改为:空闲
Book book = new Book();
book.setId(bookId);
book.setStatus(BookStatusEnum.FREE.getCode());
bookMapper.updateByPrimaryKeySelective(book);
}
}
return TgResult.ok();
}
7.2.1 rollbackFor
除了方法内的代码,主要说一下为什么是@Transactional(rollbackFor = Exception.class)
?而不是直接加的@Transactional
?
这里的rollbackFor属性,从命名上看肯定是与回滚有关,没错,是指遇到什么异常时才回滚!
默认是在抛出运行时异常(RuntimeException及其子类)时才回滚该事务,但是非RuntimeException的异常抛出时,是不会回滚事务的,所以我们指定Exception就可以将异常一网打尽!
指定Exception.class,凡是抛出异常就回滚!
7.2.2 @Transactional不生效的场景
最后说一下@Transactional不生效的场景
,也就是AOP切面注解不起作用的场景,也是我们开发时注意的点,常见的坑点如下:
- @Transactional注解加在了private方法上
- @Transactional注解加在了final方法上
- @Transactional注解虽然加在了public方法上,但是被同一个类中的方法直接内部调用(而调用方法未加@Transactional)
- 所在方法内 try catch 吃掉了异常,或者抛的异常rollbackFor没有覆盖,造成没有回滚
- 所在类本身未被spring管理
- 未开启事务等等。。。
其实造成@Transactional 不生效,都与AOP的实现原理相关,也适用于其它AOP注解失效!
这里很重要,但由于篇幅原因,并未展开深入讲解,看大家的需要我再安排吧,如果还想要讲的更深入,请投票或评论反馈!
八、dal层实战
新建BookMapperExt接口,实现审核通过后,该图书的借阅数量+1
package org.tg.book.dal.mapper.ext;
public interface BookMapperExt {
int increaseBorrowCount(Integer id);
}
自动生成BookMapperExt.xml,并增加<update>
标签
<?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="org.tg.book.dal.mapper.ext.BookMapperExt">
<update id="increaseBorrowCount">
update book set borrow_count=borrow_count+1
where id=#{id}
</update>
</mapper>
如果Mybatis Update标签生疏了,复习入口:5.5 Mybatis Update标签实战
九、测试和Git提交
留给大家测试:
- 在SQL1更新成功后,方法内抛出异常,查看SQL1是否更新到了数据库!
- 在SQL1和SQL2更新成功后,方法内抛出异常,查看SQL1和SQL2是否更新到了数据库!
养成好习惯,每个小功能,一步一提交!
最后
想要看更多实战好文章,还是给大家推荐我的实战专栏–>《基于SpringBoot+SpringCloud+Vue前后端分离项目实战》,由我和 前端狗哥 合力打造的一款专栏,可以让你从0到1快速拥有企业级规范的项目实战经验!
具体的优势、规划、技术选型都可以在《开篇》试读!
订阅专栏后可以添加我的微信,我会为每一位用户进行针对性指导!
另外,别忘了关注我:天罡gg ,发布新文不容易错过: https://blog.csdn.net/scm_2008