• SpringBoot应用之事务不生效的几种情况


    一. 配置

    将使用声明式事务,首先我们创建一个 SpringBoot 项目,版本为2.3.1.RELEASE,使用 mysql 作为目标数据库,存储引擎选择Innodb,事务隔离级别为 RR,springboot主方法入口开启@EnableTransactionManagement

    二. 不生效

    比如声明式事务注解@Transactional主要是结合代理实现,结合 AOP 的知识点,至少可以得出放在私有方法上,类内部调用都不会生效,下面进入详细说明

    1. 数据库

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

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

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

    @Servicepublic 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 如下:

    @Componentpublic 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 ========== ");

        }

        @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

     */@Transactionalprivate 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

     */@Transactionalpublic 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. 传播属性

       

    事务的几种传播特性

    1. PROPAGATION_REQUIRED: 如果存在一个事务,则支持当前事务。如果没有事务则开启

    2. PROPAGATION_SUPPORTS: 如果存在一个事务,支持当前事务。如果没有事务,则非事务的执行

    3. PROPAGATION_MANDATORY: 如果已经存在一个事务,支持当前事务。如果没有一个活动的事务,则抛出异常。

    4. PROPAGATION_REQUIRES_NEW: 总是开启一个新的事务。如果一个事务已经存在,则将这个存在的事务挂起。

    5. PROPAGATION_NOT_SUPPORTED: 总是非事务地执行,并挂起任何存在的事务。

    6. PROPAGATION_NEVER: 总是非事务地执行,如果存在一个活动事务,则抛出异常

    7. PROPAGATION_NESTED:如果一个活动的事务存在,则运行在一个嵌套的事务中. 如果没有活动事务,

          则按TransactionDefinition.PROPAGATION_REQUIRED 属性执行

  • 相关阅读:
    python之并发编程
    python之并发编程(理论部分)
    python之网络部分
    python之异常处理
    py之包和日志
    mysql之general log 日志
    python之代码规范
    文件权限管理命令chmod,chown与文本搜索命令grep
    Linux用户与组管理命令
    Linux常见文件管理命令
  • 原文地址:https://www.cnblogs.com/mscm/p/13138997.html
Copyright © 2020-2023  润新知