【工作中问题解决实践 九】Spring中事务传播的问题排查

news2024/10/7 14:28:31

最近在工作中遇到了两个关于事务操作的问题,顺便就着这两个问题又回顾了一遍Spring的事务相关的操作,想着一次性把这个问题研究明白了,后续使用事务的时候也能踏实点,让事务发挥真实的作用

什么是事务?什么是事务管理?什么是Spring事务

什么是事务?事务就是把一系列的动作当成一个独立的工作单元,这些动作要么全部完成,要么全部不起作用,关乎数据准确性的地方我们一定要用到事务,防止业务逻辑出错。

什么是事务管理,事务管理对于企业应用而言至关重要。它保证了用户的每一次操作都是可靠的,即便出现了异常的访问情况,也不至于破坏后台数据的完整性。就像银行的自助取款机,通常都能正常为客户服务,但是也难免遇到操作过程中机器突然出故障的情况,此时,事务就必须确保出故障前对账户的操作不生效,就像用户刚才完全没有使用过取款机一样,以保证用户和银行的利益都不受损失

关于事务的基本概念和定义可以参照我的另一篇Blog:【Spring学习笔记 九】Spring声明式事务管理实现机制。Sping事务简而言之就是一种JTA事务,这里不再详细展开。

一个用来演示的例子

我们还是沿用:【Spring学习笔记 九】Spring声明式事务管理实现机制这篇文章中的例子,只不过为了更贴近工作实战,这里我重构了一下代码实现。

单元测试入口

package com.example.springboot;

import com.example.springboot.model.Person;
import com.example.springboot.service.PersonAggService;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

import javax.annotation.Resource;

@SpringBootTest
class SpringbootApplicationTests {
    @Resource
    private PersonAggService personAggService;

    @Test
    public void springTransTest() {
        Person person = new Person();
        person.setUsername("wcong");
        person.setAge(30);
        person.setEmail("111111@qq.com");
        person.setPassword("111111");
        person.setPhone(11111111);
        person.setHobby("跳远");
        personAggService.addPerson(person, 100086L);
    }
}

聚合的Service方法

package com.example.springboot.service;

import com.example.springboot.model.Person;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;

/**
 * @author tianmaolin004
 * @date 2023/8/6
 */
@Service
public class PersonAggService {
   
    @Resource
    private PersonService personService;
    @Resource
    private PersonMaintainService personMaintainService;

   public void addPerson(Person person, Long creatorId) {
        //本地新增人员
        personService.insert(person);
        //保存人员创建者
        personMaintainService.savePersonCreator(creatorId);
    }
}

数据服务方法

package com.example.springboot.service;

import com.example.springboot.dao.PersonDao;
import com.example.springboot.model.Person;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.util.List;

@Service
public class PersonService {
    @Resource
    PersonDao personDao;

    public List<Person> getPersonList() {
        return personDao.getPersonList();
    }

    public Person getPersonById(Integer id) {
        return personDao.getPersonById(id);
    }

    public void insert(Person person) {
        personDao.insert(person);
    }

}

人员维护人添加方法

package com.example.springboot.service;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

/**
 * @author tianmaolin004
 * @date 2023/8/6
 */
@Service
public class PersonMaintainService {
    @Transactional(rollbackFor = Exception.class)
    public void savePersonCreator(Long userId) {
        System.out.println("保存人员创建者失败" + userId);
        throw new RuntimeException();
    }
}

数据表落库

在这里插入图片描述

不使用事务的情况

不使用事务的情况虽然单元测试报错了
在这里插入图片描述

但是数据库落库还是成功了:
在这里插入图片描述

遇到的两个事务问题

依据以上的基本case示例,模拟我遇到的两个问题和解决方案

Transaction rolled back because it has been marked as rollback-only

为了保证整体数据与预期一致可以回滚,我使用了事务,首先在外层加事务:

   @Transactional(rollbackFor = Exception.class)
    public void addPerson(Person person, Long creatorId) {
        //本地新增人员
        personService.insert(person);
        try {
            //发送人员同步到下游系统
            personMaintainService.savePersonCreator(creatorId);
        } catch (Exception e) {
            System.out.println("保存人员维护人异常但是被catch住了");
        }
    }

同时呢人员创建人这块我认为这里不需要报错阻塞整体操作,如果这里有问题只要有日志记录就行了,我通过巡检检查关注到即可,所以对这块代码加了try catch,但是呢因为内部代码不知道是谁写的也加了事务,

@Service
public class PersonMaintainService {
    @Transactional(rollbackFor = Exception.class)
    public void savePersonCreator(Long userId) {
        System.out.println("保存人员创建者失败" + userId);
        throw new RuntimeException();
    }
}

因为它们用的都是默认的传播机制,所以可以看做一个事务,使用REQUIRED传播模式,addAndSendPerson和savePersonCreator在同一个事务里面,savePersonCreator抛出异常要回滚,addAndSendPerson try Catch了异常正常执行commit,同一个事务一个要回滚,一个要提交,会报read-only异常,结果就是全部回滚,而外层所以这里就会出现rollback-only
在这里插入图片描述
解决方法有两种,一种是

干掉内层事务

内层的savePersonCreator事务干掉,这时数据也能落库成功了,事实上因为JTA的事务是有非常强的业务含义的,所以对于DAO层或简单的数据操作指令,不要加事务,否则对于较长的外部调用链路,会在传播过程中导致意外情况发生
在这里插入图片描述

内层声明为新事务

还有一种解决思路就是内层的事务声明为新事务

package com.example.springboot.service;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

/**
 * @author tianmaolin004
 * @date 2023/8/6
 */
@Service
public class PersonMaintainService {
    @Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRES_NEW)
    public void savePersonCreator(Long userId) {
        System.out.println("保存人员创建者失败" + userId);
        throw new RuntimeException();
    }
}

声明后再跑单测:
在这里插入图片描述
数据也落库成功了,因为是两个独立事务,所以内层事务遇到异常回滚,外层事务捕获到了异常catch住了,没有继续回滚

事务设置为什么不生效?

还有个例子是方法设置了事务但是不生效,我们再调整下以上的代码,模拟一种场景:savePerson要执行很多事项,但是不希望saveDate的执行异常回滚影响整体回滚,所以saveDate中的核心数据操作被try catch,并且声明内部的savePersonCreator方法为新事务,符合上边我们提到的那种场景,这种情况下理论上savePersonCreator抛出异常后会使 personDao.insert(person);回滚,数据不能写入

@SpringBootTest
class SpringbootApplicationTests {
    @Resource
    private PersonAggService personAggService;

    @Test
    public void springTransTest() {
        Person person = new Person();
        person.setUsername("wcong");
        person.setAge(30);
        person.setEmail("111111@qq.com");
        person.setPassword("111111");
        person.setPhone(11111111);
        person.setHobby("跳远");
        personAggService.savePerson(person, 100086L);
    }

}

@Service
public class PersonAggService {
    @Resource
    PersonDao personDao;

    @Transactional(rollbackFor = Exception.class)
    public void savePerson(Person person, Long creatorId) {
        System.out.println("执行其它事项");
        saveDate(person, creatorId);
    }

    @Transactional(rollbackFor = Exception.class)
    public void saveDate(Person person, Long creatorId) {
        //本地新增人员
        try {
            savePersonCreator(person, creatorId);
        } catch (Exception e) {
            System.out.println("捕获到创建人员异常");
        }
    }

    @Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRES_NEW)
    public void savePersonCreator(Person person, Long userId) {
        personDao.insert(person);
        System.out.println("保存人员创建者失败" + userId);
        throw new RuntimeException();
    }
}


但事实上,数据库写入数据能成功:
在这里插入图片描述
数据库数据写入成功了
在这里插入图片描述
这是因为:Spring中事务的默认实现使用的是AOP,也就是代理的方式,如果大家在使用代码测试时,同一个Service类中的方法相互调用需要使用注入的对象来调用,不要直接使用this.方法名来调用,this.方法名调用是对象内部方法调用,不会通过Spring代理,也就是事务不会起作用,所以实际上saveDate和savePersonCreator的事务都没有生效

把需要成为事务的方法单独抽出来

上述代码我们把需要有事务机制的savePersonCreator单独抽到一个方法中

@SpringBootTest
class SpringbootApplicationTests {
    @Resource
    private PersonAggService personAggService;

    @Test
    public void springTransTest() {
        Person person = new Person();
        person.setUsername("wcong");
        person.setAge(30);
        person.setEmail("111111@qq.com");
        person.setPassword("111111");
        person.setPhone(11111111);
        person.setHobby("跳远");
        personAggService.savePerson(person, 100086L);
    }

}

@Service
public class PersonAggService {
    @Resource
    PersonService personService;

    @Transactional(rollbackFor = Exception.class)
    public void savePerson(Person person, Long creatorId) {
        System.out.println("执行其它事项");
        saveDate(person, creatorId);
    }

    @Transactional(rollbackFor = Exception.class)
    public void saveDate(Person person, Long creatorId) {
        //本地新增人员
        try {
            personService.savePersonCreator(person, creatorId);
        } catch (Exception e) {
            System.out.println("捕获到创建人员异常");
        }
    }
}

@Service
public class PersonService {
    @Resource
    PersonDao personDao;

    public List<Person> getPersonList() {
        return personDao.getPersonList();
    }

    public Person getPersonById(Integer id) {
        return personDao.getPersonById(id);
    }

    @Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRES_NEW)
    public void savePersonCreator(Person person, Long userId) {
        personDao.insert(person);
        System.out.println("保存人员创建者失败" + userId);
        throw new RuntimeException();
    }
}

这样savePersonCreator的事务就生效了,数据没有插入成功

Spring事务的更多传播机制

以上两个示例是真实工作中遇到的,基于安全原则模拟了两个类似的case,其实spring还有更多的花式的事务使用机制,可以参照带你读懂Spring 事务——事务的传播机制

总结一下

照例总结一下,在单一的数据操作方法不要加事务,事务应该是一系列操作指令的聚合,添加了细粒度的事务可能会导致上层使用者在方法添加事务时产生了非预期的传播机制。当然如果内外层的方法调用都很复杂,则基于自己的预期进行考虑,如果不希望内层方法影响外层方法,可以使用外层方法异常捕获加内层事务的REQUIRES_NEW传播机制解决。需要注意的是Spring的事务是基于AOP实现的,所以对象内部方法调用,不会通过Spring代理,也就是事务不会起作用,这点非常重要。

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

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

相关文章

【探索Linux】—— 强大的命令行工具 P.2(Linux下基本指令)

前言 前面我们讲了C语言的基础知识&#xff0c;也了解了一些数据结构&#xff0c;并且讲了有关C的一些知识&#xff0c;也相信大家都掌握的不错&#xff0c;今天博主将会新开一个Linux专题&#xff0c;带领大家继续学习有关Linux的内容。今天第一篇文章博主首先带领大家了解一下…

客服型电话呼叫中心系统,助力企业提升客户服务质量

客服型电话呼叫中心系统是企业客户服务的重要工具之一&#xff0c;它通过电话和网络等方式&#xff0c;为客户提供快速、便捷、高效的服务。客服型电话呼叫中心系统具备自动接听来电、自动路由、管理知识库、录音和监控、生成报表分析等多种功能&#xff0c;有利于企业提高客户…

IP提取器对比器

需求&#xff1a; 一个html 页面 &#xff0c;有两个输入框 第一个输入框输入文本中包含多个ip&#xff0c;输入的ip是不规则的&#xff0c;需要使用正则表达式提取出 输入文本的ip地址 &#xff0c; 然后在第二个输入框中输入内容&#xff0c;并提取出内容的ip &#xff0c;如…

实时渲染与传统渲染有啥区别?实时渲染器有哪些

您是否曾经玩过 3D 视频游戏&#xff0c;或观看过让您感觉身处真实的建筑环境&#xff1f;如果是&#xff0c;那么您已经体验过实时渲染。和传统的渲染有什么不同吗&#xff1f;在本文中了解有关实时渲染的所有信息。 什么是实时渲染&#xff1f; 为了更容易理解什么是实时渲…

jupyter文档转换成markdown

背景 上一篇文章**《如何优雅地用python生成模拟数据》**我就使用jupyter写的&#xff0c;这个真的是万能的&#xff0c;可以插入markdown格式的内容&#xff0c;也可写代码&#xff0c;关键是像ipython一样&#xff0c;可以分步执行。 我可以这样自由的写我的博客内容&#x…

Docker入门——保姆级

Docker概述 ​ —— Notes from WAX through KuangShen 准确来说&#xff0c;这是一篇学习笔记&#xff01;&#xff01;&#xff01; Docker为什么出现 一款产品&#xff1a;开发—上线 两套环境&#xff01;应用环境如何铜鼓&#xff1f; 开发 – 运维。避免“在我的电脑…

【Groups】50 Matplotlib Visualizations, Python实现,源码可复现

详情请参考博客: Top 50 matplotlib Visualizations 因编译更新问题&#xff0c;本文将稍作更改&#xff0c;以便能够顺利运行。 1 Dendrogram 树状图根据给定的距离度量将相似的点组合在一起&#xff0c;并根据点的相似性将它们组织成树状的链接。 新建文件Dendrogram.py: …

怎样在pdf上直接修改?看看这几种修改方法

怎样在pdf上直接修改&#xff1f;PDF是一种非常流行的文件格式&#xff0c;它在保持文档格式不变的同时也可以压缩文件大小&#xff0c;便于分享。尽管 PDF 文件很便捷&#xff0c;但是在 PDF 上进行修改却是一件比较困难的事情。幸运的是&#xff0c;有很多工具可以帮助你在 P…

AUTOSAR笔记2:AP主要模块

1 CM CM&#xff08;Communication Management&#xff09;组件提供独立于网络和协议的应用间通信服 务&#xff0c;支持如下功能&#xff1a; 服务发现&#xff0c;包括服务注册、服务查找等&#xff1b;应用间通信&#xff0c;支持单向数据收发&#xff08;Event&#xff0…

STM32入门——定时器

内容为江科大STM32标准库学习记录 TIM简介 TIM&#xff08;Timer&#xff09;定时器定时器可以对输入的时钟进行计数&#xff0c;并在计数值达到设定值时触发中断16位计数器、预分频器、自动重装寄存器的时基单元&#xff0c;在72MHz计数时钟下可以实现最大59.65s的定时&…

TFTP 的使用操作指南(轻松入门版)

(꒪ꇴ꒪ ),hello我是祐言博客主页&#xff1a;C语言基础,Linux基础,软件配置领域博主&#x1f30d;快上&#x1f698;&#xff0c;一起学习&#xff01;送给读者的一句鸡汤&#x1f914;&#xff1a;集中起来的意志可以击穿顽石!作者水平很有限&#xff0c;如果发现错误&#x…

springCache-缓存

SpringCache 简介&#xff1a;是一个框架&#xff0c;实现了基于注解的缓存功能&#xff0c;底层可以切换不同的cache的实现&#xff0c;具体是通过CacheManager接口实现 使用springcache,根据实现的缓存技术&#xff0c;如使用的redis,需要导入redis的依赖包 基于map缓存 …

一招让你的Python爬虫事半功倍

在Python爬虫的世界里&#xff0c;你是否也被网站的IP封锁问题困扰过&#xff1f;别担心&#xff0c;我来教你一个简单而又有效的爬虫ip设置方法&#xff0c;让你的爬虫畅行无阻&#xff01;快来跟我学&#xff0c;让你的Python爬虫事半功倍&#xff0c;轻松搞定IP封锁问题&…

【室内定位】UWB TDOA定位,PDOA定位介绍

当前室内应用场景&#xff0c;最大的难点是没有基础设施&#xff0c;目前应用的场景中&#xff0c;都是基于用户的需求&#xff0c;或采用 UWB 技术&#xff0c;或采用蓝牙技术&#xff0c;并根据不同的室内环境来定制化的定制化的布设定位网络&#xff0c;并借助同技术的UWB定…

[C++项目] Boost文档 站内搜索引擎(4): 搜索的相关接口的实现、线程安全的单例index接口、cppjieba分词库的使用、综合调试...

有关Boost文档搜索引擎的项目的前三篇文章, 已经分别介绍分析了: 项目背景: &#x1fae6;[C项目] Boost文档 站内搜索引擎(1): 项目背景介绍、相关技术栈、相关概念介绍…文档解析、处理模块parser的实现: &#x1fae6;[C项目] Boost文档 站内搜索引擎(2): 文档文本解析模块…

百模大战,谁是赢家?文心3.5稳坐国内第一,综合评分超ChatGPT!

近日&#xff0c;清华大学新闻与传播学院沈阳团队发布《大语言模型综合性能评估报告》&#xff08;下文简称“报告”&#xff09;&#xff0c;报告显示百度文心一言在三大维度20项指标中综合评分国内第一&#xff0c;超越ChatGPT&#xff0c;其中中文语义理解排名第一&#xff…

取多个元素的整数部分 numpy.fix()

【小白从小学Python、C、Java】 【计算机等级考试500强双证书】 【Python-数据分析】 取多个元素的整数部分 numpy.fix() [太阳]选择题 请问关于以下代码最后的输出结果的是&#xff1f; import numpy as np a [1.6, 2.3, -3.8, -4.2] print("【显示】a",a) print(&…

【MySQL】对表中数据的操作

本期给大家带来的是MySQL下对表中数据的增删查改操作 目录 一、对表插入数据 1.1 单行数据插入 1.2 多行数据插入 1.3 插入冲突时更新数据 1.4 替换式插入 1.5 插入查询结果 二、对表中数据进行查询 2.1 基本select 2.1.1 使用select查询表中数据 2.1.2 使用select…

innovus gui界面文字大小和对话框大小调整

我正在「拾陆楼」和朋友们讨论有趣的话题&#xff0c;你⼀起来吧&#xff1f;拾陆楼知识星球入口 uiApp::setfont -dialog

【Java从0到1学习】06 Java 面向对象

1. 面向对象思想 面向对象是一种符合人类思维习惯的编程思想。现实生活中存在各种形态不同的事物&#xff0c;这些事物之间存在着各种各样的联系。在程序中使用对象来映射现实中的事物&#xff0c;使用对象的关系来描述事物之间的联系&#xff0c;这种思想就是面向对象。 提到…