美文网首页JAVA随笔码农日历
SpringBoot 系列教程之事务不生效的几种 case

SpringBoot 系列教程之事务不生效的几种 case

作者: 一灰灰blog | 来源:发表于2020-02-05 08:33 被阅读0次
    image

    SpringBoot 系列教程之事务不生效的几种 case

    前面几篇博文介绍了声明式事务@Transactional的使用姿势,只知道正确的使用姿势可能还不够,还得知道什么场景下不生效,避免采坑。本文将主要介绍让事务不生效的几种 case

    I. 配置

    本文的 case,将使用声明式事务,首先我们创建一个 SpringBoot 项目,版本为2.2.1.RELEASE,使用 mysql 作为目标数据库,存储引擎选择Innodb,事务隔离级别为 RR

    1. 项目配置

    在项目pom.xml文件中,加上spring-boot-starter-jdbc,会注入一个DataSourceTransactionManager的 bean,提供了事务支持

    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-jdbc</artifactId>
    </dependency>
    

    2. 数据库配置

    进入 spring 配置文件application.properties,设置一下 db 相关的信息

    ## DataSource
    spring.datasource.url=jdbc:mysql://127.0.0.1:3306/story?useUnicode=true&characterEncoding=UTF-8&useSSL=false
    spring.datasource.username=root
    spring.datasource.password=
    

    3. 数据库

    新建一个简单的表结构,用于测试

    CREATE TABLE `money` (
      `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
      `name` varchar(20) NOT NULL DEFAULT '' COMMENT '用户名',
      `money` int(26) NOT NULL DEFAULT '0' COMMENT '钱',
      `is_deleted` tinyint(1) NOT NULL DEFAULT '0',
      `create_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
      `update_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
      PRIMARY KEY (`id`),
      KEY `name` (`name`)
    ) ENGINE=InnoDB AUTO_INCREMENT=551 DEFAULT CHARSET=utf8mb4;
    

    II. 不生效 case

    在声明式事务的使用教程200119-SpringBoot 系列教程之声明式事务 Transactional 中,也提到了一些事务不生效的方式,比如声明式事务注解@Transactional主要是结合代理实现,结合 AOP 的知识点,至少可以得出放在私有方法上,类内部调用都不会生效,下面进入详细说明

    1. 数据库

    事务生效的前提是你的数据源得支持事务,比如 mysql 的 MyISAM 引擎就不支持事务,而 Innodb 支持事务

    下面的 case 都是基于 mysql + Innodb 引擎

    为后续的演示 case,我们准备一些数据如下

    @Service
    public class NotEffectDemo {
        @Autowired
        private JdbcTemplate jdbcTemplate;
    
        @PostConstruct
        public void init() {
            String sql = "replace into money (id, name, money) values" + " (520, '初始化', 200)," + "(530, '初始化', 200)," +
                    "(540, '初始化', 200)," + "(550, '初始化', 200)";
            jdbcTemplate.execute(sql);
        }
    }
    

    2. 类内部访问

    简单来讲就是指非直接访问带注解标记的方法 B,而是通过类普通方法 A,然后由 A 访问 B

    下面是一个简单的 case

    /**
     * 非直接调用,不生效
     *
     * @param id
     * @return
     * @throws Exception
     */
    @Transactional(rollbackFor = Exception.class)
    public boolean testCompileException2(int id) throws Exception {
        if (this.updateName(id)) {
            this.query("after update name", id);
            if (this.update(id)) {
                return true;
            }
        }
    
        throw new Exception("参数异常");
    }
    
    public boolean testCall(int id) throws Exception {
        return testCompileException2(id);
    }
    

    上面两个方法,直接调用testCompleException方法,事务正常操作;通过调用testCall间接访问,在不生效

    测试 case 如下:

    @Component
    public class NotEffectSample {
        @Autowired
        private NotEffectDemo notEffectDemo;
    
        public void testNotEffect() {
            testCall(530, (id) -> notEffectDemo.testCall(530));
        }
    
        private void testCall(int id, CallFunc<Integer, Boolean> func) {
            System.out.println("============ 事务不生效case start ========== ");
            notEffectDemo.query("transaction before", id);
            try {
                // 事务可以正常工作
                func.apply(id);
            } catch (Exception e) {
            }
            notEffectDemo.query("transaction end", id);
            System.out.println("============ 事务不生效case end ========== \n");
        }
    
        @FunctionalInterface
        public interface CallFunc<T, R> {
            R apply(T t) throws Exception;
        }
    }
    

    输出结果如下:

    ============ 事务不生效case start ==========
    transaction before >>>> {id=530, name=初始化, money=200, is_deleted=false, create_at=2020-02-03 13:44:11.0, update_at=2020-02-03 13:44:11.0}
    after update name >>>> {id=530, name=更新, money=200, is_deleted=false, create_at=2020-02-03 13:44:11.0, update_at=2020-02-03 13:44:11.0}
    transaction end >>>> {id=530, name=更新, money=210, is_deleted=false, create_at=2020-02-03 13:44:11.0, update_at=2020-02-03 13:44:11.0}
    ============ 事务不生效case end ==========
    

    从上面的输出可以看到,事务并没有回滚,主要是因为类内部调用,不会通过代理方式访问

    3. 私有方法

    在私有方法上,添加@Transactional注解也不会生效,私有方法外部不能访问,所以只能内部访问,上面的 case 不生效,这个当然也不生效了

    /**
     * 私有方法上的注解,不生效
     *
     * @param id
     * @return
     * @throws Exception
     */
    @Transactional
    private boolean testSpecialException(int id) throws Exception {
        if (this.updateName(id)) {
            this.query("after update name", id);
            if (this.update(id)) {
                return true;
            }
        }
    
        throw new Exception("参数异常");
    }
    

    直接使用时,下面这种场景不太容易出现,因为 IDEA 会有提醒,文案为: Methods annotated with '@Transactional' must be overridable

    4. 异常不匹配

    @Transactional注解默认处理运行时异常,即只有抛出运行时异常时,才会触发事务回滚,否则并不会如

    /**
     * 非运行异常,且没有通过 rollbackFor 指定抛出的异常,不生效
     *
     * @param id
     * @return
     * @throws Exception
     */
    @Transactional
    public boolean testCompleException(int id) throws Exception {
        if (this.updateName(id)) {
            this.query("after update name", id);
            if (this.update(id)) {
                return true;
            }
        }
    
        throw new Exception("参数异常");
    }
    

    测试 case 如下

    public void testNotEffect() {
        testCall(520, (id) -> notEffectDemo.testCompleException(520));
    }
    

    输出结果如下,事务并未回滚(如果需要解决这个问题,通过设置@Transactional的 rollbackFor 属性即可)

    ============ 事务不生效case start ==========
    transaction before >>>> {id=520, name=初始化, money=200, is_deleted=false, create_at=2020-02-03 13:44:11.0, update_at=2020-02-03 13:44:11.0}
    after update name >>>> {id=520, name=更新, money=200, is_deleted=false, create_at=2020-02-03 13:44:11.0, update_at=2020-02-03 13:44:11.0}
    transaction end >>>> {id=520, name=更新, money=210, is_deleted=false, create_at=2020-02-03 13:44:11.0, update_at=2020-02-03 13:44:11.0}
    ============ 事务不生效case end ==========
    

    5. 多线程

    这个场景可能并不多见,在标记事务的方法内部,另起子线程执行 db 操作,此时事务同样不会生效

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

    a. case1

    /**
     * 子线程抛异常,主线程无法捕获,导致事务不生效
     *
     * @param id
     * @return
     */
    @Transactional(rollbackFor = Exception.class)
    public boolean testMultThread(int id) throws InterruptedException {
        new Thread(new Runnable() {
            @Override
            public void run() {
                updateName(id);
                query("after update name", id);
            }
        }).start();
    
        new Thread(new Runnable() {
            @Override
            public void run() {
                boolean ans = update(id);
                query("after update id", id);
                if (!ans) {
                    throw new RuntimeException("failed to update ans");
                }
            }
        }).start();
    
        Thread.sleep(1000);
        System.out.println("------- 子线程 --------");
    
        return true;
    }
    

    上面这种场景不生效很好理解,子线程的异常不会被外部的线程捕获,testMultThread这个方法的调用不抛异常,因此不会触发事务回滚

    public void testNotEffect() {
        testCall(540, (id) -> notEffectDemo.testMultThread(540));
    }
    

    输出结果如下

    ============ 事务不生效case start ==========
    transaction before >>>> {id=540, name=初始化, money=200, is_deleted=false, create_at=2020-02-03 13:44:11.0, update_at=2020-02-03 13:44:11.0}
    after update name >>>> {id=540, name=更新, money=200, is_deleted=false, create_at=2020-02-03 13:44:11.0, update_at=2020-02-03 13:44:11.0}
    Exception in thread "Thread-3" java.lang.RuntimeException: failed to update ans
        at com.git.hui.boot.jdbc.demo.NotEffectDemo$2.run(NotEffectDemo.java:112)
        at java.lang.Thread.run(Thread.java:748)
    after update id >>>> {id=540, name=更新, money=210, is_deleted=false, create_at=2020-02-03 13:44:11.0, update_at=2020-02-03 13:44:11.0}
    ------- 子线程 --------
    transaction end >>>> {id=540, name=更新, money=210, is_deleted=false, create_at=2020-02-03 13:44:11.0, update_at=2020-02-03 13:44:11.0}
    ============ 事务不生效case end ==========
    

    b. case2

    /**
     * 子线程抛异常,主线程无法捕获,导致事务不生效
     *
     * @param id
     * @return
     */
    @Transactional(rollbackFor = Exception.class)
    public boolean testMultThread2(int id) throws InterruptedException {
        new Thread(new Runnable() {
            @Override
            public void run() {
                updateName(id);
                query("after update name", id);
            }
        }).start();
    
        new Thread(new Runnable() {
            @Override
            public void run() {
                boolean ans = update(id);
                query("after update id", id);
            }
        }).start();
    
        Thread.sleep(1000);
        System.out.println("------- 子线程 --------");
    
        update(id);
        query("after outer update id", id);
    
        throw new RuntimeException("failed to update ans");
    }
    

    上面这个看着好像没有毛病,抛出线程,事务回滚,可惜两个子线程的修改并不会被回滚

    测试代码

    public void testNotEffect() {
        testCall(550, (id) -> notEffectDemo.testMultThread2(550));
    }
    

    从下面的输出也可以知道,子线程的修改并不在同一个事务内,不会被回滚

    ============ 事务不生效case start ==========
    transaction before >>>> {id=550, name=初始化, money=200, is_deleted=false, create_at=2020-02-03 13:52:38.0, update_at=2020-02-03 13:52:38.0}
    after update name >>>> {id=550, name=更新, money=200, is_deleted=false, create_at=2020-02-03 13:52:38.0, update_at=2020-02-03 13:52:40.0}
    after update id >>>> {id=550, name=更新, money=210, is_deleted=false, create_at=2020-02-03 13:52:38.0, update_at=2020-02-03 13:52:40.0}
    ------- 子线程 --------
    after outer update id >>>> {id=550, name=更新, money=220, is_deleted=false, create_at=2020-02-03 13:52:38.0, update_at=2020-02-03 13:52:41.0}
    transaction end >>>> {id=550, name=更新, money=210, is_deleted=false, create_at=2020-02-03 13:52:38.0, update_at=2020-02-03 13:52:40.0}
    ============ 事务不生效case end ==========
    

    6. 传播属性

    上一篇关于传播属性的博文中,介绍了其中有几种是不走事务执行的,所以也需要额外注意下,详情可以参考博文 200202-SpringBoot 系列教程之事务传递属性

    7. 小结

    下面小结几种@Transactional注解事务不生效的 case

    • 数据库不支持事务
    • 注解放在了私有方法上
    • 类内部调用
    • 未捕获异常
    • 多线程场景
    • 传播属性设置问题

    III. 其他

    0. 系列博文&源码

    系列博文

    源码

    1. 一灰灰 Blog

    尽信书则不如,以上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现 bug 或者有更好的建议,欢迎批评指正,不吝感激

    下面一灰灰的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛

    一灰灰blog

    相关文章

      网友评论

        本文标题:SpringBoot 系列教程之事务不生效的几种 case

        本文链接:https://www.haomeiwen.com/subject/qvujxhtx.html