SpringBoot(五)SpringBoot事务

2023-11-06

    在实际开发项目时,程序并不是总会按照正常的流程去执行,有时候线上可能出现一些无法预知的问题,任何一步操作都有可能发生异常,异常则会导致后续的操作无法完成。此时由于业务逻辑并未正确的完成,所以在之前操作过数据库的动作并不可靠,需要在这种情况下进行数据的回滚,SpringBoot提供了对这种数据回滚操作场景的支持,也就是事务。

 如果你是新手,且没看过我之前的一系列SpringBoot文章,建议至少看一下这一篇:

​​​​​​​SpringBoot(四)SpringBoot搭建简单服务端_springboot做成服务_heart荼毒的博客-CSDN博客​​​​​​​

    如果你想从头到尾系统地学习,欢迎关注我的专栏,持续更新:

https://blog.csdn.net/qq_21154101/category_12359403.html

目录

一、什么是事务

二、快速开启事务

三、用户登录场景模拟使用事务    

1、创建ActionDao

2、创建ActionRepository

3、action_info表设计

4、创建实现事务的Service类 

5、实现登录接口

 四、测试事务是否生效

1、正常测试

 2、在事务方法中打开异常代码注释

3、在第一个方法中打开异常注释

4、在第二个方法打开异常注释

5、反向测试

五、事务的坑

六、事务的原理 


一、什么是事务

    事务,就是一组操作数据库的动作集合。如果一组处理步骤由于其中的一步或多步执行失败,则事务必须回滚到最初的系统状态,也就是一步都不执行。

    举例来讲,在某些业务场景下,如果一个请求,需要同时写入多张表的数据。为了避免数据不一致的情况,我们一般都会用到事务。

    事务的最大特点就是原子性。整个事务是不可分割的最小工作单位,一个事务中的所有操作要么全部执行成功,要么全部都不执行。其中任何一条语句执行失败,都会导致事务回滚。

二、快速开启事务

    那么,如何在SpringBoot项目中开启事务呢?SpringBoot提供了多种开启事务的方式,我们在本篇中使用 @Transactional 注解的方式。至于基于xml配置的方式,我想说的是,这都2032年了,别再去使用一坨一坨的配置了。

    在这里顺嘴一提,如果java是你的日常工作语言,那么注解你一定要习惯去使用。因为不仅是java后端开发,Android开发也大量使用注解。 

三、用户登录场景模拟使用事务    

    上篇博客,我们实现了用户注册的场景,相信基本的步骤大家都掌握了,接下来的示例我不详细去解释了。本篇我们通过用户登录的场景,来模拟事务的使用。

场景比较简单,如下:

(1)用户登录后,我需要往user_info表更新该用户的登录时间。

(2)同时,用户登录是一种主动的行为,我需要往action_info表添加用户登录的打点。

    我的要求如下:

(1)如果更新user_info用户表出现了异常,那么就算是登录失败了,则不能继续执行用户登录的打点。

(2)更新user_info用户表成功执行,但是记录用户登录打点的action_info表执行出现了异常没有成功写入,那么需要回滚user_info表的操作。

1、创建ActionDao

    上面说到了,需要记录用户的登录行为。在这里因为是demo,我的数据库和字段设计没那么严密,只设计了简单的几个字段。

@Entity
@Table(name = "action_info")
public class ActionDao {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer actionId;
    @Column(name = "user_name")
    private String userName;
    @Column(name = "action_type")
    private String actionType;
    @Column(name = "action_time")
    private String actionTime;
    @Column(name = "action_ext")
    private String actionExt;

    public Integer getActionId() {
        return actionId;
    }

    public void setActionId(Integer actionId) {
        this.actionId = actionId;
    }

    public String getUserName() {
        return userName;
    }

    public void setUserName(String userName) {
        this.userName = userName;
    }

    public String getActionType() {
        return actionType;
    }

    public void setActionType(String actionType) {
        this.actionType = actionType;
    }

    public String getActionTime() {
        return actionTime;
    }

    public void setActionTime(String actionTime) {
        this.actionTime = actionTime;
    }

    public String getActionExt() {
        return actionExt;
    }

    public void setActionExt(String actionExt) {
        this.actionExt = actionExt;
    }
}

2、创建ActionRepository

    只自定义一个方法,findByUserName,也就是通过用户名查到Dao对象:    

@Repository
public interface ActionRepository extends JpaRepository<ActionDao,Integer> {

    public ActionDao findByUserName(String userName);
}

3、action_info表设计

    很简单,就跟ActionDao里面的字段一一对应即可,不过多介绍,上图: 

4、创建实现事务的Service类 

    特别需要注意的是,这个类的实现有以下特点:

(1)新起的一个类。

(2)提供了一个开启事务(@Transactional)的public方法updateData。

(3)在这个支持事务的方法里有两个串行的数据库操作的方法。

(4)三个方法的最后我手动抛了一个异常,临时注释掉,打开的话模拟实际的登录异常。

@Service
public class LoginService {

    @Transactional
    public void updateData(UserRepository userRepository, ActionRepository actionRepository, UserDao userDao, String name) {
        updateUser(userRepository, userDao);
        addAction(actionRepository, name);
        // throw new RuntimeException("更新用户SQL执行异常");
    }

    /**
     * 更新用户信息
     *
     * @param userDao UserDao
     */
    public void updateUser(UserRepository userRepository, UserDao userDao) {
        Timestamp timestamp = new Timestamp(new java.util.Date().getTime());
        userDao.setUserLastLoginTime(timestamp + "");
        userRepository.save(userDao);
        // throw new RuntimeException("更新用户登录时间SQL执行异常");
    }

    /**
     * 添加用户行为
     *
     * @param name 用户名
     */
    private void addAction(ActionRepository actionRepository, String name) {
        ActionDao actionDao = new ActionDao();
        actionDao.setUserName(name);
        Timestamp timestamp = new Timestamp(new java.util.Date().getTime());
        actionDao.setActionTime(timestamp + "");
        actionDao.setActionType("login");
        actionRepository.save(actionDao);
        // throw new RuntimeException("添加用户行为SQL执行异常");
    }
}

5、实现登录接口

    通过@RestController注解,实现一个登录接口。我直接把我的代码全部贴上,注意里面的TextIUtils是我自己写的工具类,就是比较两个字符串是否一致,你也可以str1.equals(str2),但是需要注意str1判空:

@RestController
public class LoginController {
    @Autowired
    private UserRepository userRepository;
    @Autowired
    private ActionRepository actionRepository;
    @Autowired
    private LoginService loginService;

    @GetMapping("login")
    public LoginResult login(String name, String password) {
        int code = 0;
        String status = "未知状态";
        String msg = "未知信息";
        LoginResult loginResult = new LoginResult();

        UserDao userDao = userRepository.findByUserName(name);
        if (userDao == null) {
            status = "fail";
            msg = "用户名不存在";
        } else {
            String localPassword = userDao.getUserPassword();
            if (TextUtils.equals(password, localPassword)) {
                try {
                    loginService.updateData(userRepository, actionRepository, userDao, name);
                    status = "success";
                    msg = "登录成功";
                } catch (Exception e) {
                    status = "fail";
                    msg = e.getMessage();
                }
            } else {
                status = "fail";
                msg = "密码错误";
            }
        }

        loginResult.setCode(code);
        loginResult.setStatus(status);
        loginResult.setMsg(msg);
        return loginResult;
    }
}

 四、测试事务是否生效

     接下来,我们通过上面的代码来测试下,程序正常执行和分别在三个地方发生异常时,事务能否回滚来规避数据的不一致性。

1、正常测试

    首先,直接运行上述程序,不产生任何异常。如下是我之前的博客中注册成功的用户,我还是使用这个用户的用户名密码来模拟登录:http://localhost:8080/login?name=zj&password=123456

    可以看到,登录成功:

 我们看下user_info和action_info两个表是否都更新,时间刚好跟我的测试时间一致:

 

 2、在事务方法中打开异常代码注释

    如下所示,直接把支持事务的方法打开抛出异常代码的注释,手动抛一个异常:

    @Transactional
    public void updateData(UserRepository userRepository, ActionRepository actionRepository, UserDao userDao, String name) {
        updateUser(userRepository, userDao);
        addAction(actionRepository, name);
        throw new RuntimeException("更新用户SQL执行异常");
    }

    模拟调用登录接口后,登录失败:

     看下数据库两个表格是否回滚了,在这里不截图了,跟上面的截图一致,没有任何更新,事务回滚成功。

3、在第一个方法中打开异常注释

    也就是,在这样的场景下:

    /**
     * 更新用户信息
     *
     * @param userDao UserDao
     */
    public void updateUser(UserRepository userRepository, UserDao userDao) {
        Timestamp timestamp = new Timestamp(new java.util.Date().getTime());
        userDao.setUserLastLoginTime(timestamp + "");
        userRepository.save(userDao);
        throw new RuntimeException("更新用户登录时间SQL执行异常");
    }

    可以看到,登录失败:

     查看数据库的两张表,确实也没有更新,事务回滚成功。

4、在第二个方法打开异常注释

    模拟第一个数据操作已经完成,第二个数据操作出现了异常:

    /**
     * 添加用户行为
     *
     * @param name 用户名
     */
    private void addAction(ActionRepository actionRepository, String name) {
        ActionDao actionDao = new ActionDao();
        actionDao.setUserName(name);
        Timestamp timestamp = new Timestamp(new java.util.Date().getTime());
        actionDao.setActionTime(timestamp + "");
        actionDao.setActionType("login");
        actionRepository.save(actionDao);
        throw new RuntimeException("添加用户行为SQL执行异常");
    }

    毫无疑问,登录失败:

    

    同样的,数据库的两张表没有任何更新。 

5、反向测试

    那么,我们把支持事务的方法的注解去掉会发生什么事情呢?去掉注解,再次运行程序,模拟登录行为:

    //@Transactional
    public void updateData(UserRepository userRepository, ActionRepository actionRepository, UserDao userDao, String name) {
        updateUser(userRepository, userDao);
        addAction(actionRepository, name);
        // throw new RuntimeException("更新用户SQL执行异常");
    }

    毫无疑问,现象还是跟上面一致,登录失败: 

    但是,我们看下数据库呢。可以看到,数据库也变了:

    这就前后不一致了,程序发生异常了,告诉用户的其实是登录失败了这没问题,但是数据库更新了用户的登录时间,并且增加了用户登录的打点。但实际上,用户本次登录确实失败了。

    通过这几个例子可以看出,事务确实很好地帮我们避免了数据不一致的情况或数据库跟前端表现不一致的情况。

五、事务的坑

    事务的使用,其实是有很多坑的。在一开始使用事务的时候,我就说明了我处理事务的类的特点,其实那也是使用事务的几个基本的规范。在这里,我直接引用知乎一个大佬总结的事务使用的坑,我就不一一给大家详解了。

 图片出处:聊聊Spring事务失效的12种场景,太坑人了 - 知乎

六、事务的原理 

    简单说下事务的原理。如果你对java常用的设计模式有过了解,那么通过其特点,其实可以想到,在这里使用了拦截器模式。

拦截器模式:每个任务都对应一个拦截器,共同组成了一个拦截器链,每个拦截器都是拦截器链的一环。下一个拦截器是否执行,取决于上一个拦截器执行的结果。

    在这里只说其实现原理,不去深扒源码看其实现,感兴趣的同学自己去查阅。添加事务注解的方法,其内部的每一个方法都是一个拦截器,多个方法共同组成了拦截器链。当上一个任务执行成功时,下一个任务才会执行。反之,如果上一个任务执行失败了,那么会回滚该方法的执行,并且下一个方法不再执行。以此类推,只有拦截器链的每一个拦截器均执行成功了,那么才算执行成功。

    最后,简单的总结下。本篇介绍了SpringBoot事务是什么以及事务的特点,并且通过注解的方式实现了事务的demo,通过模拟登录场景下更新两张数据表,测试了事务是否是符合预期的。最后呢,也简单的对事务的原理进行了介绍。如果有任何问题,欢迎留言或私聊交流。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

SpringBoot(五)SpringBoot事务 的相关文章

随机推荐

  • packages are looking for funding

    npm fund 此命令检索 如何为项目的依赖项提供资金支持的信息 如果没有提供软件包名称 将用树状结构中列出所有正在寻找资金支持的依赖项 其中有资金的类型和要访问的URL 如果提供了软件包名称 可以将尝试使用 browser config
  • 在win10系统安装mysql 8.0.21

    首先第一步进入 mysql下载 然后选择操作系统为 windows 其他下载 选择第一个下载 如图 等待下在完成后 解压到自己想要安装的文件夹 然后打开 mysql 8 0 21 winx64 文件夹 在此文件夹下建立配置文件 my ini
  • 基于红外人脸识别的汽车防盗系统

    基于红外人脸识别的汽车防盗系统总结 人脸识别技术的发展可谓是比较成熟了 目前 大部分的技术都是关于可见光人脸识别 该项技术容易受到光照强度和角度的影响 发生假冒现象 近红外人脸识别克服了可见光的影响 将其与汽车防盗进行结合 解决了大量的传统
  • Springboot整合activiti(最详细版)

    写在最前 flowable和activiti本是一家 所以有很多api和设计是一样的 这里用的api是flowable的 流程设计器war包也可以共用 建议搜不到activiti某些资料的搜flowable的试试 1 导包
  • Capability CAP_DAC_OVERRIDE

    linux CAP DAC OVERRIDE and other permisions Unix Linux Stack Exchange
  • 复制列表—赋值、深浅拷贝与切片拷贝

    最近看了本书 你也能看得懂的Python算法书 觉得不错 分享下学习心得 这里是第一章 Python中列表存储的方法和其他语言中的不太一样 列表中的元素在计算机的存储空间中占据一定的内存 而列表本身存储的是这些元素的存储地址 在调用列表元素
  • 10_Introduction to Artificial Neural_4_Regression MLP_Sequential_Subclassing_saveMode_Callback_board

    10 Introduction to Artificial Neural Networks with Keras HuberLoss astype dtype DNN MLP G gv pdf mnist https blog csdn n
  • 火狐网页访问https提示安全连接失败

    使用https访问网站提示错误 在服务器密钥交换握手信息中 SSL 收到了一个弱临时 Diffie Hellman 密钥 错误码 ssl error weak server ephemeral dh key 解决方法 安装插件 https
  • JAVA实现本地上传阿里云OSS云存储(超详细)

    JAVA实现阿里云的oss的云存储 一 阿里云操作步骤 开通oss云存储 1 找到对象存储OSS 2 进入控制台 3 创建Bucket 都默认即可 4 获取accessKeyId和accessKeySecret 二 文件上传 简单文件上传
  • mysql 安装了最新版本8.x版本后的报错: the server requested authentication method unknown to the client

    一 在MySQL 8 0 11中 caching sha2 password是默认的身份验证插件 而不是以往的mysql native password 有关此更改对服务器操作的影响以及服务器与客户端和连接器的兼容性的信息 请参阅cachi
  • Fabric 1.4和BCOS 2.0对比

    Fabric 1 4和BCOS 2 0对比 实现方式 架构分析 一 节点分类 二 交易流程 三 灵活性 核心技术组件 一 通信 二 存储 三 安全机制 四 共识机制 应用功能 一 身份认证 二 账户设计 三 支持智能合约 四 监管功能 五
  • python自动发送qq消息_使用python发送QQ消息,QQ消息自动发送

    源代码 from tkinter import import win32gui import win32con import win32clipboard as w LOG LINE NUM 0 class Play def init se
  • 3D游戏与计算机图形学中的数学方法-四元数

    3D游戏与计算机图形学中的数学方法 四元数 说实话关于四元数这一节真的是不好懂 因为里面涉及到好多数学知识 单说推出来的公式就有很多 不怕大家笑话 对于四元数的学习我足足花了两天的时间 包括整理出这篇文章 在前面一章我写到了 变换 这也是总
  • 通过Power Platform自定义D365 CE 业务需求 - 4. 使用Power Automate

    Microsoft Power Automate 以前称为Flow 可帮助您在喜爱的云和本地应用程序之间创建自动化工作流 它提供同步文件 获取通知 收集数据等服务 Power Automate是Microsoft Power Platfor
  • adbd cannot run as root in production builds 的解决方法

    超级root的adb下载地址 http download csdn net download anthony 3 9633800这里有一个 今天用adb root命令时候 报了错误 adbd cannot run as root in pr
  • 开炮,开炮

    哈喽大家好 我是安德酱 我做了一款意大利炮的游戏 我用到了这种代码 int shanben rand 20 这句的意思是从0到20之间取随机数 作为山本 位置 游戏玩法 我们有100发炮弹 要有五次炸到山本 山本会随机躲闪 楚云飞牵制住了他
  • Failed building wheel for xxx 解决办法

    在下面两个链接之一下载相应whl 下载whl链接 UCI 下载whl链接 清华大学 安装 pip install 刚刚下载whl文件绝对路径
  • Source Insight 3.5和Source Insight 4.0的安装

    系列文章目录 Source Insight是一个功能非常强大的C C 的代码阅读器 通过工程的管理 Source Insight可实现多文件代码中 C工程或C和ASM的混合工程 的变量 函数的快速定位和搜索 并且对每个打开的源程序 C或C
  • MySQL-Centos下MySQL5.7安装教程

    MySQL安装教程 一 卸载MySQL 二 安装MySQL 三 mysql登录 四 修改配置文件 一 卸载MySQL 1 如果你的机器上mysqld服务器还在运行 那么第一步就是要停掉服务 systemctl stop mysqld 2 查
  • SpringBoot(五)SpringBoot事务

    在实际开发项目时 程序并不是总会按照正常的流程去执行 有时候线上可能出现一些无法预知的问题 任何一步操作都有可能发生异常 异常则会导致后续的操作无法完成 此时由于业务逻辑并未正确的完成 所以在之前操作过数据库的动作并不可靠 需要在这种情况下