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

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

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

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

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

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

一个用来演示的例子

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

单元测试入口

java 复制代码
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方法

java 复制代码
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);
    }
}

数据服务方法

java 复制代码
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);
    }

}

人员维护人添加方法

java 复制代码
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

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

java 复制代码
   @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,但是呢因为内部代码不知道是谁写的也加了事务,

java 复制代码
@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层或简单的数据操作指令,不要加事务,否则对于较长的外部调用链路,会在传播过程中导致意外情况发生

内层声明为新事务

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

java 复制代码
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);回滚,数据不能写入

java 复制代码
@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单独抽到一个方法中

java 复制代码
@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代理,也就是事务不会起作用,这点非常重要。

相关推荐
小电玩31 分钟前
JAVA SE8
java·开发语言
程序员大金1 小时前
基于SSM+Vue+MySQL的酒店管理系统
前端·vue.js·后端·mysql·spring·tomcat·mybatis
努力的布布1 小时前
Spring源码-从源码层面讲解声明式事务的运行流程
java·spring
程序员大金1 小时前
基于SpringBoot的旅游管理系统
java·vue.js·spring boot·后端·mysql·spring·旅游
小丁爱养花1 小时前
记忆化搜索专题——算法简介&力扣实战应用
java·开发语言·算法·leetcode·深度优先
大汉堡~1 小时前
代理模式-动态代理
java·代理模式
爱上语文1 小时前
Springboot三层架构
java·开发语言·spring boot·spring·架构
loveLifeLoveCoding1 小时前
Java List sort() 排序
java·开发语言
草履虫·2 小时前
【Java集合】LinkedList
java
AngeliaXue2 小时前
Java集合(List篇)
java·开发语言·list·集合