程序员子龙(Java面试 + Java学习) 程序员子龙(Java面试 + Java学习)
首页
学习指南
工具
开源项目
技术书籍

程序员子龙

Java 开发从业者
首页
学习指南
工具
开源项目
技术书籍
  • 基础

  • JVM

  • Spring

    • 动态代理-CGLIB
    • Hibernate Validator 参数校验优雅实战
    • Jackson序列化json时null转成0或空串
    • 别自己瞎写工具类了!SpringBoot中自带工具类,开发效率增加一倍
    • Spring @Autowired Map
    • SpringBoot 缓存之 @Cacheable 详细介绍与失效时间TTL
    • Spring Security 入门
    • Spring Security原理
    • Spring项目整合MybatisPlus出现org.mybatis.logging.LoggerFactory Not Found 异常
    • Spring在代码中获取bean
    • 别再乱写了,Controller 层代码这样写才足够规范!
    • 非静态变量给静态变量赋值
    • 过滤器与拦截器区别、使用场景
    • 接口重试机制 Spring-Retry
    • 利用cglib动态创建对象或在原对象新增属性
    • 聊聊spring事务失效的场景
      • Spring Event 事件解耦
      • 最全的Spring依赖注入方式
      • Spring初始化之ApplicationRunner、InitializingBean、@PostConstruct 使用详解
      • 为啥不建议用 BeanUtils.copyProperties 拷贝数据
    • 并发编程

    • Mybatis

    • 网络编程

    • 数据库

    • 缓存

    • 设计模式

    • 分布式

    • 高并发

    • SpringBoot

    • SpringCloudAlibaba

    • Nginx

    • 面试

    • 生产问题

    • 系统设计

    • 消息中间件

    • Java
    • Spring
    程序员子龙
    2024-01-29
    目录

    聊聊spring事务失效的场景

    对于从事java开发工作的同学来说,spring的事务肯定再熟悉不过了。

    在某些业务场景下,如果一个请求中,需要同时写入多张表的数据。为了保证操作的原子性(要么同时成功,要么同时失败),避免数据不一致的情况,我们一般都会用到spring事务。

    确实,spring事务用起来贼爽,就用一个简单的注解:@Transactional,就能轻松搞定事务。我猜大部分小伙伴也是这样用的,而且一直用一直爽。

    但如果你使用不当,事务不会不生效。

    今天我们就一起聊聊,事务失效的一些场景,说不定你已经中招了。

    spring事务的原理是AOP,进行了切面增强,那么失效的根本原因是这个AOP不起作用了!

    img

    # 数据库引擎不支持事务

    这里以 MySQL 为例,其 MyISAM 引擎是不支持事务操作的,InnoDB 才是支持事务的引擎,一般要支持事务都会使用 InnoDB。从 MySQL 5.5.5 开始的默认存储引擎是:InnoDB,之前默认的都是:MyISAM,所以这点要值得注意,底层引擎不支持事务再怎么搞都不能支持事务。

    # 没有被 Spring 管理

    // @Service
    public class OrderServiceImpl implements OrderService {
    
        @Transactional
        public void updateOrder(Order order) {
            // update order
        }
    
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9

    如果此时把 @Service 注解注释掉,这个类就不会被加载成一个 Bean,那这个类就不会被 Spring 管理了,事务自然就失效了。

    # 方法不是public的

    @Service
    public class UserService {
    
        @Transactional 
        private void add(UserModel userModel) {
             saveData(userModel);
             updateData(userModel);
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9

    spring事务也是通过动态代理来实现的,在对一个bean进行初始化的过程中,在执行到第八个后置处理器方法,org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator#postProcessAfterInitialization

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

    // Don't allow no-public methods as required.
    if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
      return null;
    }
    
    1
    2
    3
    4

    如果我们自定义的事务方法(即目标方法),它的访问权限不是public,而是private、default或protected的话,spring则不会提供事务功能。可以开启 AspectJ 代理模式。

    # 方法内部调用

    对同一个类里面的方法调用,比如有一个类Test,它的一个方法A,A再调用本类的方法B(不论方法B是用public还是private修饰),但方法A没有声明注解事务,而B方法有。则外部调用方法A之后,方法B的事务是不会起作用的。

    @Service
    public class UserService {
    
        @Autowired
        private UserMapper userMapper;
    
    
        public void add(UserModel userModel) {
            userMapper.insertUser(userModel);
            this.updateStatus(userModel);
        }
    
        @Transactional
        public void updateStatus(UserModel userModel) {
            doSameThing();
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17

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

    由此可见,在同一个类中的方法直接内部调用,会导致事务失效。

    如果有些场景,确实想在同一个类的某个方法中,调用它自己的另外一个方法,该怎么办呢?

    如果不想再新加一个Service类,在该Service类中注入自己也是一种选择。具体代码如下:

    @Servcie
    public class ServiceA {
       @Autowired
       prvate ServiceA serviceA;
    
       public void save(User user) {
             queryData1();
             queryData2();
             serviceA.doSave(user);
       }
    
       @Transactional(rollbackFor=Exception.class)
       public void doSave(User user) {
           addData1();
           updateData2();
        }
     } 
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17

    # 方法用final修饰

    @Service
    public class UserService {
    
        @Transactional
        // 编译时直接报错 Methods annotated with '@Transactional' must be overridable 
        public final void add(UserModel userModel){
            saveData(userModel);
            updateData(userModel);
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10

    spring事务底层使用了aop,也就是通过jdk动态代理或者cglib,帮我们生成了代理类,在代理类中实现的事务功能。

    但如果某个方法用final修饰了,那么在它的代理类中,就无法重写该方法,从而实现事务功能。

    # 异常被吃了

    事务不会回滚,最常见的问题是:开发者在代码中手动try...catch了异常。

    @Service
    public class UserService {
    
        @Transactional
        public void add(UserModel userModel) {
            try {
                saveData(userModel);
                updateData(userModel);
            } catch (Exception e) {
                log.error(e.getMessage(), e);
            }
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13

    这种情况下spring事务当然不会回滚,因为开发者自己捕获了异常,又没有手动抛出,换句话说就是把异常吞掉了。

    如果想要spring事务能够正常回滚,必须抛出它能够处理的异常。如果没有抛异常,则spring认为程序是正常的。

    # 手动抛了别的异常

    即使没有手动捕获异常,但如果抛的异常不正确,spring事务也不会回滚。

    public class UserService {
    
        @Transactional
        public void add(UserModel userModel) throws Exception {
            try {
                 saveData(userModel);
                 updateData(userModel);
                 int i = 1/0
            } catch (Exception e) {
                log.error(e.getMessage(), e);
                throw new Exception(e);
            }
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14

    因为spring事务,默认情况下只会回滚RuntimeException(运行时异常)和Error(错误),对于普通的Exception(非运行时异常),它不会回滚。

    # 抛出自定义异常

    在使用@Transactional注解声明事务时,有时我们想自定义回滚的异常,spring也是支持的。可以通过设置rollbackFor参数,来完成这个功能。

    @Service
    public class UserService {
    
        @Transactional(rollbackFor = BusinessException.class)
        public void add(UserModel userModel) throws Exception {
           saveData(userModel);
           updateData(userModel);
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9

    这种情况事务也不会回滚。

    如果在执行上面这段代码,保存和更新数据时,程序报错了,抛了SqlException、DuplicateKeyException等异常。而BusinessException是我们自定义的异常,报错的异常不属于BusinessException,所以事务也不会回滚。

    # 多线程

    @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(() -> {
                 try {
                     test();
                 } catch (Exception e) {
    	            roleService.doOtherThing();
                 }
            }).start();
        }
    }
    
    @Service
    public class RoleService {
    
        @Transactional
        public void doOtherThing() {
             try {
                 int i = 1/0;
           		 System.out.println("保存role表数据");
             }catch (Exception e) {
                throw new RuntimeException();
            }
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35

    从上面的例子中,我们可以看到事务方法add中,调用了事务方法doOtherThing,但是事务方法doOtherThing是在另外一个线程中调用的。

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

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

    # 错误的传播属性

    spring目前支持7种传播特性:

    • REQUIRED 如果当前上下文中存在事务,那么加入该事务,如果不存在事务,创建一个事务,这是默认的传播属性值。
    • SUPPORTS 如果当前上下文存在事务,则支持事务加入事务,如果不存在事务,则使用非事务的方式执行。
    • MANDATORY 如果当前上下文中存在事务,否则抛出异常。
    • REQUIRES_NEW 每次都会新建一个事务,并且同时将上下文中的事务挂起,执行当前新建事务完成以后,上下文事务恢复再执行。
    • NOT_SUPPORTED 如果当前上下文中存在事务,则挂起当前事务,然后新的方法在没有事务的环境中执行。
    • NEVER 如果当前上下文中存在事务,则抛出异常,否则在无事务环境上执行代码。
    • NESTED 如果当前上下文中存在事务,则嵌套事务执行,如果不存在事务,则新建事务。

    如果我们在手动设置propagation参数的时候,把传播特性设置错了,比如:

    @Service
    public class UserService {
    
        @Transactional(propagation = Propagation.NEVER)
        public void add(UserModel userModel) {
            saveData(userModel);
            updateData(userModel);
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9

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

    目前只有这三种传播特性才会创建新事务:REQUIRED,REQUIRES_NEW,NESTED。

    # 嵌套事务回滚多了

    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表数据");
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23

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

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

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

    @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);
            }
        }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21

    可以将内部嵌套事务放在try/catch中,并且不继续往上抛异常。这样就能保证,如果内部嵌套事务中出现异常,只回滚内部事务,而不影响外部事务。

    # 总结

    本文总结了事务失效的场景,其实发生最多就是自身调用、异常被吃、异常抛出类型不对这三个。平常使用的时候一定要注意下。

    上次更新: 2024/01/30, 15:08:57
    利用cglib动态创建对象或在原对象新增属性
    Spring Event 事件解耦

    ← 利用cglib动态创建对象或在原对象新增属性 Spring Event 事件解耦→

    最近更新
    01
    一个注解,优雅的实现接口幂等性
    11-17
    02
    MySQL事务(超详细!!!)
    10-14
    03
    阿里二面:Kafka中如何保证消息的顺序性?这周被问到两次了
    10-09
    更多文章>
    Theme by Vdoing | Copyright © 2024-2024

        辽ICP备2023001503号-2

    • 跟随系统
    • 浅色模式
    • 深色模式
    • 阅读模式