SpringBoot(五)SpringBoot事务

news2024/9/25 11:16:45

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

目录

一、什么是事务

二、快速开启事务

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

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,通过模拟登录场景下更新两张数据表,测试了事务是否是符合预期的。最后呢,也简单的对事务的原理进行了介绍。如果有任何问题,欢迎留言或私聊交流。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/710057.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

单臂路由实现不同VLAN之间数据转发

实验环境&#xff1a; 思科模拟器&#xff0c;Cisco Packet Tracer 实验拓扑&#xff1a; 实验配置&#xff1a; &#xff08;1&#xff09;PC配置 IP地址子网掩码网关PC1192.168.10.1255.255.255.0192.168.10.254PC2192.168.10.2255.255.255.0192.168.10.254PC3192.168.20…

串口通讯监控方法

当我们调试硬件的时候&#xff0c;发现串口数据异常&#xff0c;用示波器和逻辑分析仪的话会比较麻烦&#xff0c;此时可以并一个监控串口&#xff0c;如下图所示 232串口&#xff0c;我们是不能直接并一个串口上去的&#xff1b;但是我们的监控串口&#xff0c;可以只接一根R…

【玩转循环】探索Python中的无限可能性

前言 循环可能是每个编程语言中使用比较多的语法了&#xff0c;如果能合理利用好循环&#xff0c;就会出现意想不到的结果&#xff0c;大大地减少代码量&#xff0c;让机器做那些简单枯燥的循环过程&#xff0c;今天我将为大家分享 python 中的循环语法使用。&#x1f697;&am…

数据结构--栈的链式存储

数据结构–栈的链式存储 推荐使用不带头结点的单链表 \color{green}推荐使用不带头结点的单链表 推荐使用不带头结点的单链表 typedef struct LNode {ElemType data;struct LNode* next; } LNode, *LinkList;bool InitList(LinkList &L) {L->next NULL; }后插操作&…

python网络编程(二)模拟ssh远程执行命令

1、项目需求&#xff1a; 要实现一个像ssh远程连接工具一样&#xff0c;在终端输入命令&#xff0c;返回对应的结果。 比如window的dos命令&#xff1a; dir &#xff1a;查看目录下的文件 ipconfig : 查看网卡信息 tasklist : 查看进程列表 linux的命令&#xff1a; ls : 查看…

Jenkins与CI/CD

简介 CI&#xff08;持续集成&#xff09; Continuous Integration是一种软件开发实践&#xff0c;即团队开发成员经常集成他们的工作&#xff0c;通常每个成员每天至少集成一次&#xff0c;也就意味着每天可能会发生多次集成。每次集成都通过自动化的构建&#xff08;包括编…

Debian 环境使用 docker compose 部署 sentry

Debian 环境使用 docker compose 部署 sentry Sentry 简介什么是 Sentry &#xff1f;Sentry 开发语言及支持的 SDKSentry 功能架构 前置准备条件规格配置说明Dcoker Desktop 安装WSL2/Debian11 环境准备 Sentry 安装步骤docker 部署 sentry 步骤演示过程说明 总结 Sentry 简介…

python机器学习在气象模式订正、短临预报、气候预测等场景的应用

基于机器学习的天河机场物流预测研究 全球经济快速增长的形势下,八大区域性枢纽之一的武汉天河机场的物流需求也在攀升。文章针对天河机场的货邮吞吐量,运用机器学习中的线性回归模型通过Python对其进行需求预测,并用二次指数平滑法与之对比,在平均绝对百分误差比较下得出机器…

需求分析引言:架构漫谈(四)性能专题

前文介绍了非功能性需求里的可靠性和可用性&#xff0c; 本文对非功能性需求里的性能&#xff0c;进行一些详细的说明&#xff0c;和如何度量系统的性能问题。 1、概念 性能通常是指一个软件系统的处理能力和速度&#xff0c;一般通过 延迟 和 吞吐量 这两个指标进行度量。 不…

分布式软件架构——域名解析系统

透明多级分流系统的设计原则 用户在使用信息系统的过程中&#xff0c;请求首先是从浏览器出发&#xff0c;在DNS的指引下找到系统的入口&#xff0c;然后经过了网关、负载均衡器、缓存、服务集群等一系列设施&#xff0c;最后接触到了系统末端存储于数据库服务器中的信息&…

云计算——容器

作者简介&#xff1a;一名云计算网络运维人员、每天分享网络与运维的技术与干货。 座右铭&#xff1a;低头赶路&#xff0c;敬事如仪 个人主页&#xff1a;网络豆的主页​​​​​ 目录 前言 一.容器简介 二.主流容器技术 1.docker &#xff08;1&#xff09;容器的组…

HTML5+ Runtime提示

使用的环境 vue-cli框架&#xff0c;Andriod调试、云打包都会出现该弹框 1.我遇到的问题 上述弹框提示&#xff0c;HBuilderX3.8.2 &#xff0c; 手机SDK版本是3.8.4&#xff0c;不匹配 解决目的&#xff1a;需要让两个版本匹配 2. 点击“查看详情”&#xff0c;查看原因 …

JS文件UTF8格式乱码问题

UTF8格式的JS文件在IE中显示乱码问题的解决 这种情况通常是由于JS文件头缺少BOM标志引起的,解决方式: 方法1:用系统自带记事本,另存为 UTF-8,覆盖原文件,会自动加上BOM标志(就是文件开头的EF BB BF 三个字节) 方法2: 用notepad 打开,编码菜单,由UTF8编码改为 UTF8-BOM编码

10-Vue从入门到手撕

什么时候可以开始学习Vue? 学习路线&#xff1a;H5 CSS3 ---> ES6 ---> 网络 ---> 第三方库 ---> 工程化 ---> Vue 不经过前面的铺垫是无法学习vue的&#xff0c;就算学了还得倒回去补知识点 展现Vue Vue源码分析&#xff0c;走进作者的内心世界 …

记录一次对STM32G4串口硬件FIFO的调试

记录一次对STM32G4串口硬件FIFO的调试 前言&#xff1a;通常我们使用串口接收多字节数据会使用中断和DMA两种方式。使用中断方式&#xff0c;每接收到一个字节就会触发一次中断&#xff0c;我们可以在中断函数里将接收到的这一字节保存在内存中然后等待其他程序处理&#xff0c…

麦语言是什么东东?怎么学?

麦语言&#xff08;M Language&#xff09;是一种用于处理数据的编程语言&#xff0c;最初由微软公司开发。它是Power Query&#xff08;数据提取和转换工具&#xff09;和Power BI&#xff08;商业智能工具&#xff09;中的一部分。麦语言支持对各种数据源进行查询、转换和清理…

农业温室大棚数据监控系统的设计与实现

1.引言 农业温室大棚作为现在农业发展的必要条件&#xff0c;将高新技术融入农业温室大棚也愈发的重要&#xff0c;对农业温室大棚数据的监控&#xff0c;将温室大棚智能化。本设计对温室大棚实现远程数据监控&#xff0c;自动化控制&#xff0c;对温室内的环境数据进行巡回检…

解决Springboot在启动时报错:不支持发行版本17

今天在创建新项目时控制台出现如下错误&#xff1a; 最后经过排查发现问题出现如下几点。将以下几点进行修改问题得以解决。 1.将红色箭头地方由17改为11 2.将maven的pom文件中 的javaversion由17改为113.将spingboot的版本调为2.7.5 如果以上还没有解决问题&#xff0c;可以尝…

机器视觉(图像处理)入门金典之图像数字化及处理方法

图像的数字化 一般的图像(模拟图像)不能直接用计算机来处理,必须首先转化为数字图像 把模拟图像分割成一个个称为像素的小区域,每个像素的亮度或灰度值用一个整数表示 数字化的含义: 使模拟图像的灰度、亮度和色彩数据化 图像数字化的步骤: 两个步骤: 1、在空间坐标…

时间序列分解 | Matlab改进的自适应噪声完备集合经验模态分解ICEEMDAN

文章目录 效果一览文章概述部分源码参考资料效果一览 文章概述 时间序列分解 | Matlab改进的自适应噪声完备集合经验模态分解ICEEMDAN 部分源码 %--------------------