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 ========== \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

 */@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 屬性執行


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM