Loading [MathJax]/jax/output/CommonHTML/config.js
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >Spring的事务那些场景会失效?

Spring的事务那些场景会失效?

作者头像
猫头虎
发布于 2024-04-08 03:22:40
发布于 2024-04-08 03:22:40
14000
代码可运行
举报
运行总次数:0
代码可运行

Spring的事务10种常见失效场景总结

对于从事java开发工作的同学来说,spring的事务肯定再熟悉不过了。在某些业务场景下,如果同时有多张表的写入操作,为了保证操作的原子性(要么同时成功,要么同时失败)避免数据不一致的情况,我们一般都会使用spring事务。

没错,spring事务大多数情况下,可以满足我们的业务需求。但是今天我要告诉大家的是,它有很多坑,稍不注意事务就会失效。

不信,我们一起看看。

1.错误的访问权限

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@Service
public class UserService {

    @Autowired
    private UserMapper userMapper;
    
    @Transactional
    private void add(UserModel userModel) {
        userMapper.insertUser(userModel);
    }
}

我们可以看到add方法的访问权限被定义成了private,这样会导致事务失效,spring要求被代理方法必须是public的。

AbstractFallbackTransactionAttributeSource类的computeTransactionAttribute方法中有个判断,如果目标方法不是public,则TransactionAttribute返回null,即不支持事务。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
protected TransactionAttribute computeTransactionAttribute(Method method, @Nullable Class<?> targetClass) {
    // Don't allow no-public methods as required.
    if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
      return null;
    }

    // The method may be on an interface, but we need attributes from the target class.
    // If the target class is null, the method will be unchanged.
    Method specificMethod = AopUtils.getMostSpecificMethod(method, targetClass);

    // First try is the method in the target class.
    TransactionAttribute txAttr = findTransactionAttribute(specificMethod);
    if (txAttr != null) {
      return txAttr;
    }

    // Second try is the transaction attribute on the target class.
    txAttr = findTransactionAttribute(specificMethod.getDeclaringClass());
    if (txAttr != null && ClassUtils.isUserLevelMethod(method)) {
      return txAttr;
    }

    if (specificMethod != method) {
      // Fallback is to look at the original method.
      txAttr = findTransactionAttribute(method);
      if (txAttr != null) {
        return txAttr;
      }
      // Last fallback is the class of the original method.
      txAttr = findTransactionAttribute(method.getDeclaringClass());
      if (txAttr != null && ClassUtils.isUserLevelMethod(method)) {
        return txAttr;
      }
    }

    return null;
  }

2.方法被定义成final的

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@Service
public class UserService {

    @Autowired
    private UserMapper userMapper;

    @Transactional
    public final void add(UserModel userModel) {
        userMapper.insertUser(userModel);
    }
}

我们可以看到add方法被定义成了final的,这样会导致spring aop生成的代理对象不能复写该方法,而让事务失效。

3.方法内部调用

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@Service
public class UserService {

    @Autowired
    private UserMapper userMapper;

    @Transactional
    public void add(UserModel userModel) {
        userMapper.insertUser(userModel);
        updateStatus(userModel);
    }

    @Transactional
    public void updateStatus(UserModel userModel) {
        // doSameThing();
    }
}

我们看到在事务方法add中,直接调用事务方法updateStatus。从前面介绍的内容可以知道,updateStatus方法拥有事务的能力是因为spring aop生成代理了对象,但是这种方法直接调用了this对象的方法,所以updateStatus方法不会生成事务。

4.当前实体没有被spring管理

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
//@Service
public class UserService {

    @Autowired
    private UserMapper userMapper;

    @Transactional
    public void add(UserModel userModel) {
        userMapper.insertUser(userModel);
    }
}

我们可以看到UserService类没有定义@Service注解,即没有交给spring管理bean实例,所以它的add方法也不会生成事务。

5.错误的spring事务传播特性

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@Service
public class UserService {

    @Autowired
    private UserMapper userMapper;

    @Transactional(propagation = Propagation.NEVER)
    public void add(UserModel userModel) {
        userMapper.insertUser(userModel);
    }

}

我们可以看到add方法的事务传播特性定义成了Propagation.NEVER,这种类型的传播特性不支持事务,如果有事务则会抛异常。只有这三种传播特性才会创建新事务:PROPAGATION_REQUIRED,PROPAGATION_REQUIRES_NEW,PROPAGATION_NESTED。

6.数据库不支持事务

msql8以前的版本数据库引擎是支持myslam和innerdb的。我以前也用过,对应查多写少的单表操作,可能会把表的数据库引擎定义成myslam,这样可以提升查询效率。但是,要千万记得一件事情,myslam只支持表锁,并且不支持事务。所以,对这类表的写入操作事务会失效。

7.自己吞掉了异常

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@Slf4j
@Service
public class UserService {

    @Autowired
    private UserMapper userMapper;
    
    @Transactional
    public void add(UserModel userModel) {
        try {
            userMapper.insertUser(userModel);
        } catch (Exception e) {
            log.error(e.getMessage(), e);
        }
    }
}

这种情况下事务不会回滚,因为开发者自己捕获了异常,又没有抛出。事务的AOP无法捕获异常,导致即使出现了异常,事务也不会回滚。

8.抛出的异常不正确

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@Slf4j
@Service
public class UserService {

    @Autowired
    private UserMapper userMapper;
    
    @Transactional
    public void add(UserModel userModel) throws Exception {
        try {
            userMapper.insertUser(userModel);
        } catch (Exception e) {
            log.error(e.getMessage(), e);
            throw new Exception(e);
        }
    }

}

这种情况下,开发人员自己捕获了异常,又抛出了异常:Exception,事务也不会回滚。因为spring事务,默认情况下只会回滚RuntimeException(运行时异常)和Error(错误),不会回滚Exception。

9.多线程调用

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@Slf4j
@Service
public class UserService {

    @Autowired
    private UserMapper userMapper;
    @Autowired
    private RoleService roleService;

    @Transactional
    public void add(UserModel userModel) throws Exception {
        userMapper.insertUser(userModel);
        new Thread(() -> {
            roleService.doOtherThing();
        }).start();
    }
}

@Service
public class RoleService {

    @Transactional
    public void doOtherThing() {
        System.out.println("保存role表数据");
    }
}

我们可以看到事务方法add中,调用了事务方法doOtherThing,但是事务方法doOtherThing是在另外一个线程中调用的,这样会导致两个事务方法不在同一个线程中,获取到的数据库连接不一样,从而是两个不同的事务。如果想doOtherThing方法中抛了异常,add方法也回滚是不可能的。

如果看过spring事务源码的朋友,可能会知道spring的事务是通过数据库连接来实现的。当前线程中保存了一个map,key是数据源,value是数据库连接。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
private static final ThreadLocal<Map<Object, Object>> resources =
      new NamedThreadLocal<>("Transactional resources");

我们说的同一个事务,其实是指同一个数据库连接,只有拥有同一个数据库连接才能同时提交和回滚。如果在不同的线程,拿到的数据库连接肯定是不一样的,所以是不同的事务。

10.嵌套事务多回滚了

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class UserService {

    @Autowired
    private UserMapper userMapper;

    @Autowired
    private RoleService roleService;

    @Transactional
    public void add(UserModel userModel) throws Exception {
        userMapper.insertUser(userModel);
        roleService.doOtherThing();
    }
}

@Service
public class RoleService {

    @Transactional(propagation = Propagation.NESTED)
    public void doOtherThing() {
        System.out.println("保存role表数据");
    }
}

这种情况使用了嵌套的内部事务,原本是希望调用roleService.doOtherThing方法时,如果出现了异常,只回滚doOtherThing方法里的内容,不回滚 userMapper.insertUser里的内容,即回滚保存点。。但事实是,insertUser也回滚了。

why?

因为doOtherThing方法出现了异常,没有手动捕获,会继续往上抛,到外层add方法的代理方法中捕获了异常。所以,这种情况是直接回滚了整个事务,不只回滚单个保存点。

怎么样才能只回滚保存点呢?

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@Slf4j
@Service
public class UserService {

    @Autowired
    private UserMapper userMapper;

    @Autowired
    private RoleService roleService;

    @Transactional
    public void add(UserModel userModel) throws Exception {

        userMapper.insertUser(userModel);
        try {
            roleService.doOtherThing();
        } catch (Exception e) {
            log.error(e.getMessage(), e);
        }
    }

}

在代码中手动把内部嵌套事务放在try/catch中,并且不继续往抛异常。

介绍到这里,你会发现spring事务的坑还是挺多的~

结语

如果这篇文章对您有所帮助,或者有所启发的话,求一键三连:点赞、评论、收藏➕关注,您的支持是我坚持写作最大的动力。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2024-04-07,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
C++:43---派生类向基类转换、静态/动态的类变量
一、继承中类的类型转换规则 我们普通的编程规则规定,如果我们想把引用或指针绑定到一个对象上,则引用或指针的类型必须与所绑定的对象的类型一致或者对象的类型含有一种可接受的const类型转换规则。但是继承关系中的类比较例外,其规则如下: ①我们可以将基类的指针或引用绑定到派生对象上 #include <iostream>class A {};class B:public A{};int main(){ A *a; B b; a = &b; return 0;} ②即使不是指针/引用类型,我们也可以将派生类转换为
用户3479834
2021/02/03
2.1K0
C++:43---派生类向基类转换、静态/动态的类变量
C++的函数隐藏、覆盖和重载
http://blog.csdn.net/lin49940/article/details/5553664
bear_fish
2018/09/20
1.5K0
C++:45---多态
一、多态介绍 面向对象的核心思想是多态性,其含义是“多种形式” 概念:在子类覆盖了父类函数的情况下,用父类的指针(或引用)调用子类对象,或者通过父类指针调用覆盖函数的时候(动态绑定),实际上调用的是子类的覆盖版本,这种现象叫做多态 注意事项: 只有用父类的指针(或引用)调用子类对象多态才会产生,非指针/引用不会产生多态 且只有用父类的指针(或引用)调用虚函数才会产生多态,调用非虚函数不会产生多态效果 运行时解析: 当我们使用基类的引用或指针调用基类中定义的一个虚函数时,我们并不知道该函数真正作用的对象是什么
用户3479834
2021/02/03
3730
C++:45---多态
[C++] 剖析多态的原理及实现
多态(Polymorphism)是面向对象编程中的一个重要概念,它使得同一个行为可以针对不同类型的对象表现出不同的形态。通俗来讲,多态就是“多种形态”的实现。
DevKevin
2024/09/18
3220
[C++] 剖析多态的原理及实现
【C++】多态
多态的概念:通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态。
zxctscl
2024/04/25
1560
【C++】多态
C++中函数重载、隐藏、覆盖和重写的区别
C++规定在同一作用域中,同名函数的形式参数(指参数的个数、类型或者顺序)不同时,构成函数重载。
恋喵大鲤鱼
2018/08/03
8.8K1
【c++】全面理解C++多态:虚函数表深度剖析与实践应用
多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。比如Student继承了Person。Person对象买票全价,Student对象买票半价
用户11029103
2024/05/24
4330
基类派生类多态虚函数?
通常在层次关系的根部有一个基类,其他类则直接或间接的从基类继承而来,这些继承得到的类称为派生类。基类负责定义在层次关系中所有类共同拥有的成员,而每个派生类定义各自特有的成员。
洁洁
2023/10/10
4040
C++多态
在 C++ 程序设计中,多态性是指具有不同功能的函数可以用同一个函数名,这样就可以用一个函数名调用不同内容的函数。在面向对象方法中,一般是这样表述多态性的:向不同的对象发送同一个消息,不同的对象在接收时会产生不同的行为(即方法);也就是说,每个对象可以用自己的方式去响应共同的消息所谓消息,就是调用函数,不同的行为就是指不同的实现,即执行不同的函数。换言之,可以用同样的接口访问功能不同的函数,从而实现“一个接口,多种方法”。在C++中主要分为静态多态和动态多态两种,在程序运行前就完成联编的称为静态多态,主要通过函数重载和模板实现,动态多态在程序运行时才完成联编,主要通过虚函数实现。
范中豪
2021/05/19
2K0
C++多态
解锁C++继承的奥秘:从基础到精妙实践(上)
继承是C++面向对象编程的核心特性之一,它允许程序员创建层次化的类结构,从而实现代码的重用和扩展。在这篇文章中,我们将深入探讨C++继承的基础概念,包括基类与派生类的关系、多重继承的处理、虚函数与多态的应用,以及如何在复杂系统中有效利用继承来构建可维护且扩展性强的代码架构。通过系统的学习,你将对C++继承有更深入的理解,并能够在实际开发中灵活应用这些知识。
suye
2024/10/16
3750
今天你学C++了吗?——C++中的多态
具体来说,运行时多态允许我们在完成某个行为(函数)时,传入不同的对象就会完成不同的行为,从而达到多种形态。假设我们正在开发一个图形绘制应用程序,该程序需要支持多种图形对象(如圆形、矩形和三角形)的绘制。我们可以设计一个基类Shape,并在其中定义一个虚函数draw()用于绘制图形。不同的图形对象(圆形、矩形和三角形)将作为Shape的派生类,并在这些派生类中重写draw()函数以实现各自的绘制逻辑。同样地,在动物叫声的模拟中,传入猫对象时发出“喵”的叫声,传入狗对象时发出“汪汪”的叫声,这也是通过多态性来实现的。
用户11352420
2025/03/16
3110
今天你学C++了吗?——C++中的多态
【C++】多态——实现、重写、抽象类、多态原理
多态的概念:通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态。
平凡的人1
2023/10/15
6910
【C++】多态——实现、重写、抽象类、多态原理
C++多态
多态的概念:通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态。
南桥
2024/08/07
1720
C++多态
C++程序诗篇的灵动赋形:多态
本篇将开启 C++ 三大特性中的多态篇章,多态允许你以统一的方式处理不同类型的对象,通过相同的接口来调用不同的实现方法。这意味着你可以编写通用的代码,而这些代码可以在运行时根据对象的实际类型来执行特定的操作
DARLING Zero two
2025/04/11
1110
C++程序诗篇的灵动赋形:多态
【C++】结构体、类和引用
注:最后有面试挑战,看看自己掌握了吗 文章目录 结构体和类 构造函数 析构函数 this指针 类的继承 虚函数与多态性、纯虚函数 虚函数与多态 纯虚函数 覆盖和隐藏 引用 C++类的设计习惯及头文件包含问题 结构体和类 C++结构体中可以有函数。 称为成员函数 #include <iostream> struct point { int x; int y; void output() { std::cout<<x<<std::endl<<y; } }; void main() { poin
20岁爱吃必胜客
2022/11/13
1K0
【C++】结构体、类和引用
封装、继承、多态、重载:C++中的强大特性与代码设计
C++中的封装是一种面向对象编程的概念,它将数据(成员变量)和操作(成员函数)封装在一个类中,通过访问控制来限制对类内部实现的访问。封装提供了类与外部世界之间的接口,隐藏了类的内部实现细节,提高了代码的可维护性和安全性。
洁洁
2024/01/03
7540
十一、多态
多态是面向对象编程中的一个核心概念,它指的是允许不同类的对象对同一消息作出响应,即同一操作作用于不同的对象,可以有不同的行为。多态问题的引入,可以从以下几个方面进行阐述:
用户11332765
2024/10/28
3750
【C++】多态(定义、虚函数、重写、隐藏)
多态的概念:通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会 产生出不同的状态。
秦jh
2024/07/03
3020
【C++】多态(定义、虚函数、重写、隐藏)
C++虚函数知识点总结
对应虚函数的类,该类的对象所占内存大小为,数据成员的大小+一个指向虚函数表指针 (4字节)。
半生瓜的blog
2023/05/12
2530
C++虚函数知识点总结
C++:44---关键字virtual、override、final
一、虚函数 概念:在函数前面加virtual,就是虚函数 虚函数的一些概念: 只有成员函数才可定义为虚函数,友元/全局/static/构造函数都不可以 虚函数需要在函数名前加上关键字virtual 成员函数如果不是虚函数,其解析过程发生在编译时而非运行时 派生类可以不覆盖(重写)它继承的虚函数 重写(覆盖)的概念与规则 派生类重写(覆盖)基类中的函数,其中函数名,参数列表,返回值类型都必须一致,并且重写(覆盖)的函数是virtual函数 虚函数在子类和父类中的访问权限可以不同 相关规则: ①如果虚函数的返回
用户3479834
2021/02/03
4.6K0
C++:44---关键字virtual、override、final
相关推荐
C++:43---派生类向基类转换、静态/动态的类变量
更多 >
LV.3
公众号:猫头虎技术团队职业: 全栈软件工程师
交个朋友
加入腾讯云官网粉丝站
蹲全网底价单品 享第一手活动信息
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验