7.8 SpringBoot事务@Transactional实战 管理员借阅审核

CSDN成就一亿技术人


前言

通过上文我们实现了图书借阅审核列表,本文实战的场景是:审核图书借阅!因为审核后最终会执行2条更新SQL,所以就派上了事务
事务是用来干什么的?事务是用来解决什么问题的? 什么情况下用到事务?
对于SpringBoot,我们主要使用的是声明式事务@Transactional,不仅有实战的用法,我还会用白话讲解事务的执行流程ACID四大特性MySQL四种隔离级别,以及 @Transactional不生效场景 坑点 (也适用于其它AOP注解)!

掌握这些应对大部分项目绰绰有余,面对一般面试也会尽在掌握中,OK,话不多说,嗨一波,Let’s Go!


一、事务是用来干什么的?事务是用来解决什么问题的?

  • 事务是用来干什么的?干嘛的?
  • 事务是用来解决什么问题的?
  • 事务是用来保证什么的?
  • 事务一般用在什么地方?
  • 什么情况下用到事务?

如果你有上面这些疑问,我想通过本文的审核场景 来回答你的疑问!

为了方便你理解需求,我画了一个流程图,如下:

在这里插入图片描述

审核通过或驳回后主要干两件事:

  1. 修改 图书借阅表book_borrowing的状态status字段(1-通过,2-驳回)

  2. 审核通过:修改 图书表book的借阅数量borrow_count+1

    驳回:修改 图书表bookstatus字段(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块解释如下:

    1. 校验
    1. 更新 图书借阅表
    1. 更新 图书表

每块的代码,我相信你都能看的懂,如果跟着学到这还看不懂的,需要补补前面的知识啦~~~

@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

猜你喜欢

转载自blog.csdn.net/scm_2008/article/details/132009967