• Spring 事务编程实践


    Spring 事务实战


    Spring 事务使用非常简便,我们在日常使用 Spring事务时,如果不了解其底层实现原理,经常会踩到一些坑,本文会介绍 Spring 事务的两种使用方式:声明式事务和编程式事务,以及其使用误区和最佳实践,最后会从源码角度简要分析其实现原理。

    什么是 Spring 事务?

    Spring的事务框架将开发过程中事务管理相关的关注点进行适当的分离,通过Spring的事务框架,我们可以按照统一的编程模型来进行事务编程,却不用关心所使用的数据访问技术以及具体要访问什么 类型的事务资源。

    Spring 事务的设计理念的基本原则是:让 事务管理 与 数据访问,相分离。

    • 当在业务层使用事务的抽象API进行事务管理的时候,不需要关心事务将要管理的资 源是什么,对不同的事务资源的管理将由相应的框架实现类来完成。

    • 当在数据访问层对可能参与事务的数据资源进行访问的时候,只需要使用相应的数据访问API 进行数据访问,不需要关心当前的事务资源如何参与事务或者是否需要参与事务。这同样将 由事务框架类来打理。

    对于我们开发人员来说,唯一需要关系的,就是通过抽象后的事务管理API(PlatformTransactionManager)对当前的事务进行管理而已。通常我们有两种使用方式,一种是声明式事务也就是 AOP 代理的方式,一种是使用 声明式事务也就是 PlatformTransactionManager API 的方式。

    声明式事务使用举例

    Spring 声明式事务非常简单,只需要一个注解 @Transaction 即可完成事务工作,如下:

        @Transactional(rollbackFor = Exception.class)
        public void insert(User User) {
            // 保存数据库
        }
    
    • 1
    • 2
    • 3
    • 4

    这个也是我们日常开发中经常会使用到的一种方式,但是如果是个新手或者对 Spring 声明式事务原理不了解的话经常会踩到一些坑,后面我们会介绍。下面我们再来看一下另外一种不常用的 Spring 事务使用方式:编程式事务 API

    编程式事务使用举例

    什么是编程式事务?

    通过硬编码的方式使用 Spring 中提供的抽象事务API PlatformTransactionManager 来控制事务。

    Spring 使用模板方法对其封装为我们提供了事务模板类:TransactionTemplate 方便我们使用。

    @Rescorce
    private TransactionTemplate transactionTemplate
     public void test2(){
    
            // TransactionCallback 有返回值
            transactionTemplate.execute(status -> {
               return doDaoSomthing();
               
            });
    
            // TransactionCallbackWithoutResult 无返回值
            transactionTemplate.executeWithoutResult(transactionStatus -> {
                doDaoSomthing();
            });
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    其中 TransactionTemplate 在 SpringBoot 中,Spring 已经为我们初始化好放在 IoC 容器中,我们可以直接注入使用。

    编程式事务的好处是可以通过更细力度更个性化的事务控制流程。如对于事务操作中可能抛出的 checked exception,如果既想回滚事务,又不想让它以 unchecked exception 的形式向上层传播的话,我们当然不能通过将其转译成 unchecked exception 的方式来处理。可以通过TransactionStatus设置 rollBack0nly 来达到以上“一箭双雕”的目的。

    声明式事务常见问题

    我们上面说过,Spring 事务使用不当会造成一些严重的问题,下面我们就 Spring事务 经常遇到的一些问题做一起讨论下。

    事务不生效

    事务不生效问题经常出现在我们使用 Spring 声明式事务中,主要原因是因为对 Spring 事务管理机制和 声明式事务实现原理不够了解所致,下面我们一起来看下常见误区:

    Case1:类内部访问
    • 场景描述

    A 类的 a1 方法没有标注 @Transactional,a2 方法标注 @Transactional,在 a1里面调用 a2;事务不生效。

    • 举例说明:
    @Service
    public class TestService {
    
        public void test(){
            doSomething1();
        }
    
        @Transactional
        public void doSomething1(){
            // 数据库操作
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    test() 方法中调用被 @Transaction 注解标注的 doSomething1() 方法

    @RestController
    public class TestController {
    
        @Resource
        private TestService testService;
    
        @GetMapping("/test")
        public void test() {
            testService.test();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    当进行调用 testService.test() 时,doSomething1() 方法的事务不会生效。

    • 原因解释:

    因为 @Transactional 的工作机制是基于 AOP 实现,AOP 是使用动态代理实现的。如果通过代理调用doSomething1(),通过 AOP 会前后进行增强,增强的逻辑其实就是在 doSomething1() 的前后别加上开启、提交事务的逻辑。

    而现在是在 test() 方法中直接调用 this.doSomething1(),this 指向 TestService 的非代理对象,不会经过 AOP 的事务增强操作。也就是类内部调用事务方法,不会通过代理方式访问。

    解决方法也很简单,这里介绍一个通用的处理方式,那就是在 本类中获取 Spring IoC 容器,使用依赖查找的方式从容器中直接获取 当前代理对象。如下:

    @Service
    public class TestService implements ApplicationContextAware {
    
        private ApplicationContext applicationContext;
        
        @Override
        public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
            this.applicationContext = applicationContext;
        }
        
        public void test() {
            // 从Spring IoC 容器中获取代理类
            TestService testService = applicationContext.getBean(TestService.class);
            // 调用代理类目标方法
            testService.doSomething1();
        }
    
        @Transactional
        public void doSomething1() {
            // 数据库操作
            System.out.println("doSomething1 数据库操作。。。");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    除了上述方法,我们还可以 通过 AopContext 获取代理对象来调用:

    @Service
    public class TestService {
        
        public void test() {        
           // 获取当前类的代理对象
           TestService testServiceProxy = ((TestService)AopContext.currentProxy())
            // 调用代理类目标方法
           testServiceProxy.doSomething1();
        }
    
        @Transactional
        public void doSomething1() {
            // 数据库操作
            System.out.println("doSomething1 数据库操作。。。");
        }
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    Case2:非 public 可重载方法
    • 场景描述

    在 Spring Bean 的非 public 方法上,添加 @Transactional 注解不会生效。

    • 举例说明:
    @Service
    public class TestService {
        
        @Transactional
        protected void doSomething2(){
            // 数据库操作
        }
    
        @Transactional
        public final void doSomething3(){
            // 数据库操作
        }
    
        @Transactional
        public static void doSomething4(){
            // 数据库操作
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    doSomething2() 被 protected 修饰符修饰,导致事务不会生效;
    doSomething3() 被 final 修饰,导致事务不生效;
    doSomething4() 被 static 修饰,导致事务不会生效。

    @RestController
    public class TestController {
    
        @Resource
        private TestService testService;
    
        @GetMapping("/test")
        public void test() {
            testService.doSomething2();
            testService.doSomething3();
            testService.doSomething4();
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    实际使用时,这三种场景不太容易出现,因为 IDEA 会有提醒: 被 @Transactional 标注的方法必须可被重载。

    • 原因解释:

    Spring 要求被代理方法必须是 public 的并且是可被重载的。至于深层次的原理,源码部分会给你解读。

    Case3:异常不匹配(重点)
    • 场景描述

    @Transactional 没有设置 rollbackFor = Exception.class 属性或者指定异常类型和方法抛出的异常类型不匹配。

    • 举例说明:
    @Service
    public class TestService {
    
        @Transactional
        public void doSomething5() throws Exception {
            // 数据库操作
            throw new Exception("发生异常");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 原因解释:

    Spring 事务默认只捕获 uncheck exception,即只会回滚 RuntimeException(运行时异常)和Error(错误),对于普通的 Exception(非运行时异常),它不会回滚。

    异常体系

    Case4:多线程
    • 场景描述

    在 Spring 事务方法中开启线程操作数据库,不受主线程事务控制。

    • 举例说明:

    下面给出两个不同的Case,一个是子线程 ok,父线程抛异常;一个是子线程抛异常,主线程 ok。

        @Transactional
        public  void doSomething6() {
            // 数据库操作
    
            //  开启子线程
            new Thread(() -> {
               doDb();
            }).start();
    
            // 主线程异常
            throw new RuntimeException("父线程异常");
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    父线程抛出线程,事务回滚。因为子线程是独立存在,和父线程不在同一个事务中,所以子线程的修改并不会被回滚。

        @Transactional
        public  void doSomething7() {
            // 数据库操作
    
            //  开启子线程
            new Thread(() -> {
                doDb();
                throw new RuntimeException("子线程异常");
            }).start();
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    由于子线程的异常不会被外部的线程捕获,所以父线程不抛异常,事务回滚没有生效。

    • 原因解释:

    Spring事务控制是通过 AOP 动态代理,在方法开始是开启一个事务,并把该事务放在当前前程上下文中进行传递,开启子线程事务无法传递。导致子线程事务失效。至于深层次的原理,源码部分会给你解读。

    Case5:嵌套事务未捕获异常

    Spring 传播属性中的嵌套事务是通过 JDBC 提供 SavePoint 来实现的。PROPAGATION_REQUIRES_NEW 和 PROPAGATION_NESTED 比较容易混淆:

    PROPAGATION_REQUIRES_NEW:启动一个新的, 不依赖于环境的 “内部” 事务. 这个事务将被完全 commited 或 rolled back 而不依赖于外部事务, 它拥有自己的隔离范围, 自己的锁, 等等. 当内部事务开始执行时, 外部事务将被挂起, 内务事务结束时, 外部事务将继续执行。

    PROPAGATION_NESTED: 开始一个 “嵌套的” 事务, 它是已经存在事务的一个真正的子事务.潜套事务开始执行时, 它将取得一个 savepoint. 如果这个嵌套事务失败, 我们将回滚到此savepoint. 嵌套事务是外部事务的一部分, 只有外部事务结束后它才会被提交。如果外部事务 commit, 嵌套事务也会被 commit, 这个规则同样适用于 rollback

    • 场景描述

    一个事务方法A调用另一个嵌套事务方法B,嵌套事务方法A 异常,不希望影响 事务方法B

    • 举例说明:
    public class TestService {
    
        @Resource
        private TagRepository tagRepository;
    
           // 默认隔离级别
        @Transactional(propagation = Propagation.REQUIRED)
        public void doSomething8() {
            tagRepository.upsert("doSomething8", TagType.UNKNOWN);
            ((TestService) AopContext.currentProxy()).doSomething9();
        }
    
        @Transactional(propagation = Propagation.NESTED, rollbackFor = RuntimeException.class)
        public void doSomething9() {
            tagRepository.upsert("doSomething9", TagType.UNKNOWN);
              throw new RuntimeException("doSomething9 exception");
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

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

    • 原因解释:

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

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

        // 默认隔离级别
        @Transactional(propagation = Propagation.REQUIRED)
        public void doSomething8() {
            tagRepository.upsert("doSomething8", TagType.UNKNOWN);
            try {
                ((TestService) AopContext.currentProxy()).doSomething9();
            } catch (Exception e) {
                System.out.println("捕获 doSomething9 异常");
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    这种方式也是嵌套事务最有价值的地方, 它起到了分支执行的效果。嵌套事务执行异常,自动回滚到保存点,在外部事务捕获异常,做其他逻辑处理。

    Case6:事务传播属性使用理解不当
    • 场景描述

    事务方法A 调用事务方法B,期望事务B回滚,事务A也回滚。

    • 举例说明:
        // 默认隔离级别
        @Transactional(propagation = Propagation.REQUIRED)
        public void doSomething8() {
            tagRepository.upsert("doSomething8", TagType.UNKNOWN);
    
            applicationContext.getBean(TestService.class).doSomething9();
            throw new RuntimeException("doSomething9 exception");
        }
    
        @Transactional(propagation = Propagation.REQUIRES_NEW, rollbackFor = RuntimeException.class)
        public void doSomething9() {
            tagRepository.upsert("doSomething9", TagType.UNKNOWN);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    这种情况下,doSomething9 事务为 PROPAGATION_REQUIRES_NEW ,所以与PROPAGATION_REQUIRED 不会有任何关系。不会因为对方的执行情况影响事务的结果,因为他们根本就是两个事务。

    • 原因解释:

    PROPAGATION_REQUIRES_NEW 配置下,当内部事务开始执行时, 外部事务将被挂起, 内务事务结束时, 外部事务将继续执行。外部事务异常不会影响内部事务。

    Case7:自己吞掉了异常
    • 场景描述

    在事务方法中 try…catch 住异常没有向外抛出,导致事务不生效。

    • 举例说明:
        @Transactional
        protected void test(){
            // 数据库操作
            try {
                doOther();
            } catch (Exception e) {
                log.error("xxx");
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 原因解释:

    Spring 事物处理是通过 AOP try…catch 代理方法,根据异常信息来回滚事物的,如果没检测到异常,也便不会回滚事物了。

    大事务问题

    @Transactional注解一般加在某个业务方法上,会导致整个业务方法都在同一个事务中,粒度太粗,不好控制事务范围,是出现大事务问题的最常见的原因。

    大事务常见问题?

    所谓大事务就是事务流程较长的事务,大事务会造成许多性能问题:

    • 死锁
    • 锁定太多的数据,造成大量的阻塞和锁超时
    • 执行时间长,容易造成主从延迟
    • 回滚时间长
    • 并发情况下,数据库连接池容易被占满
    • undo log日志膨胀,不仅增加了存储的空间,而且可能降低查询的性能

    最主要的影响数据库连接池容易被撑爆,导致大量线程等待,造成请求无响应或请求超时。

    如何查询大事务?

    select * from information_schema.innodb_trx where TIME_TO_SEC(timediff(now(),trx_started))>10
    
    • 1
    如何避免大事务问题?

    这里我总结了如下几点:

    1.少用 @Transactional 注解

    @Transactional注解一般加在某个业务方法上,会导致整个业务方法都在同一个事务中,粒度太粗,不好控制事务范围,是出现大事务问题的最常见的原因。可以使用我们上面介绍到的编程式事务,在spring项目中使用 TransactionTemplate 手动执行事务。

    2. 将查询方法放到事务外

    如果出现大事务,可以将查询(select)方法放到事务外,也是比较常用的做法,因为一般情况下这类方法是不需要事务的。如下面代码:

    @Transactional(rollbackFor=Exception.class) 
       public void save(User user) { 
             queryData1(); 
             queryData2(); 
             addData1(); 
             updateData2(); 
       } 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    可以将queryData1和queryData2两个查询方法放在事务外执行,将真正需要事务执行的代码才放到事务中,比如:addData1和updateData2方法,这样就能有效的减少事务的粒度。

    该怎么拆分呢? 多数人可能会想到下面这样:

    public void save(User user) { 
             queryData1(); 
             queryData2(); 
             doSave(); 
        } 
        
        @Transactional(rollbackFor=Exception.class) 
        public void doSave(User user) { 
           addData1(); 
           updateData2(); 
        } 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    这个例子是非常经典的错误,这种直接方法调用的做法事务不会生效,这和我们前面在 Spring 事务不生效一章中介绍的 Case1 一模一样,这种错误经常会在日常开发中出现。即使有经验的开发人员稍不注意,就会采坑。

    因此,我们更推荐使用下面方法重构实现:

    @Autowired 
       private TransactionTemplate transactionTemplate; 
        
       public void save(final User user) { 
             queryData1(); 
             queryData2(); 
             transactionTemplate.execute((status) => { 
                addData1(); 
                updateData2(); 
                return Boolean.TRUE; 
             }) 
       } 
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    3. 事务中避免远程调用

    我们在接口中调用其他系统的接口是不能避免的,由于网络不稳定,这种远程调的响应时间可能比较长,如果远程调用的代码放在某个事务中,这个事务就可能是大事务。当然,远程调用不仅仅是指调用接口,还有包括:发MQ消息,或者连接redis、mongodb保存数据等。

    @Transactional(rollbackFor=Exception.class) 
       public void save(User user) { 
             callRemoteApi(); 
             addData1();
             sendMq(); 
       } 
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    远程调用的代码可能耗时较长,切记一定要放在事务之外。

       @Resource
       private TransactionTemplate transactionTemplate; 
        
       public void save(User user) { 
             callRemoteApi(); 
             transactionTemplate.execute((status) => { 
                addData1(); 
                return Boolean.TRUE; 
             }) ;
             sendMq();
       } 
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    有些朋友可能会问,远程调用的代码不放在事务中如何保证数据一致性呢?这就需要建立:重试+补偿机制,达到数据最终一致性了。

    4. 事务中避免一次性处理太多数据

    如果一个事务中需要处理的数据太多,也会造成大事务问题。比如为了操作方便,你可能会一次批量更新1000条数据,这样会导致大量数据锁等待,特别在高并发的系统中问题尤为明显。

        @Transactional(rollbackFor = Exception.class)
        public void saveList(List<User> userList) {
            insert(userList);
        }
    
    • 1
    • 2
    • 3
    • 4

    解决办法是分页处理,1000条数据,分50页,一次只处理20条数据,这样可以大大减少大事务的出现。

        public void saveList(List<User> userList) {
            Lists.partition(userList, 20)
                    .forEach(users -> {
                        transactionTemplate.execute(transactionStatus -> {
                            return insert(users);
                        });
                    });
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    Spring 事务使用最佳实践

    1. Spring 声明式事务适合简单的事务场景,使用方便,但是容易采坑,当使用 Spring 声明式事务时,请仔细检查确认一下几个方面:
    • @Transactional 注解必须明确指定回滚异常 rollbackFor = XXX.class
    • 该事物方法必须是通过代理类访问
    • 确保异常被抛出
    1. 对于一些不便于使用 声明式事务的场景,我们推荐使用 编程式事物管理来控制事务。 使用编程式事务管理几乎不需要注意什么,编程上事务可以让我们更加细粒度的控制事务范围,并且可以很好的避免声明式事物带来的一些坑。

    编程式事务使用最佳实践

    编程式事务虽然使用起来相比声明式事务有些繁琐,但是借助于 TransactionTemplate 我们也能很容易的实现事务控制逻辑。这里我们建议,尽量使用 编程式事务 TransactionTemplate 替代声明式事务使用。

    原理分析

    源码分析

    Spring的事务框架将开发过程中事务管理相关的关注点进行适当的分离,通过Spring的事务框架,我们可以按照统一的编程模型来进行事务编程。
    下面我们简要分析下其中关键源码,想要系统了解 Spring 抽象事务管理器可以参考这篇文章:
    Spring 三部曲(三):Spring 的 数据访问

    Spring 事务的设计理念的基本原则是:让 事务管理数据访问,相分离。Spring 把事务管理当做一种横切关注点,基于 Spring AOP 提供了一个事务拦截器 TransactionInterceptor ,在业务方法执行开始之前开启一个事务,当方法执行完成或者异常退出的时候就提交事务或者回滚事务。在拦截器方法: org.springframework.transaction.interceptor.TransactionAspectSupport#invokeWithinTransaction 中处理事务逻辑。

    第二步是判断是声明式事务还是编程式事务,else 逻辑走编程式事务处理:

    非 public 方法导致事务不生效原因

    上面分析到,拦截器调用了方法 getTransactionAttribute(),主要是为了获取 txAttr 变量,它是用于读取 @Transactional 的配置,如果这个 txAttr = null,后面就不会走事务逻辑,我们看一下这个变量的含义:

    直接进入 getTransactionAttribute(),重点关注获取事务配置的方法。

    异常不匹配原因

    我们继续回到事务的核心逻辑,因为主方法抛出 Exception() 异常,进入事务回滚的逻辑:



    当没有设置 rollbackFor 属性时,默认只对 RuntimeException 和 Error 的异常执行回滚。

    这里只对关键源码进行简要分析,如果想了解更多关于 Spring 事务处理的细节,可以自行 debug,走一遍 Spring 事务处理流程,将会更加印象深刻。

    总结

    建议在项目中少使用 @Transactional注解开启事务。如果项目中有些业务逻辑比较简单,而且不经常变动,使用@Transactional注解开启事务开启事务也无妨,因为它更简单,开发效率更高,但是千万要小心事务失效的问题。

    本文我们介绍了 Spring 声明式事务使用的常见误区,并结合源码做了一些答疑。希望对大家有所帮助。

  • 相关阅读:
    kubernetes Pod
    下载遥感数据慢的原因
    C# 使用DocX生成word文档
    免费开源的积分商城系统_积分商城的功能逻辑_OctShop
    前端代码规范
    简析Cookie、Session、Token
    百度地图JS在线开发_获取经纬度_存储_读取显示轨迹
    IPv6改造方案:协议转换技术
    蜘蛛蜂优化算法SWO求解不闭合MD-MTSP,可以修改旅行商个数及起点(提供MATLAB代码)
    计算机毕业设计php+vue基于微信小程序的在线挂号预约小程序
  • 原文地址:https://blog.csdn.net/itguangit/article/details/127687796