SpringBoot 系列教程之事務不生效的幾種 case

SpringBoot 系列教程之事務不生效的幾種 casejava

前面幾篇博文介紹了聲明式事務@Transactional的使用姿式,只知道正確的使用姿式可能還不夠,還得知道什麼場景下不生效,避免採坑。本文將主要介紹讓事務不生效的幾種 casemysql

<!-- more -->git

I. 配置

本文的 case,將使用聲明式事務,首先咱們建立一個 SpringBoot 項目,版本爲2.2.1.RELEASE,使用 mysql 做爲目標數據庫,存儲引擎選擇Innodb,事務隔離級別爲 RRgithub

1. 項目配置

在項目pom.xml文件中,加上spring-boot-starter-jdbc,會注入一個DataSourceTransactionManager的 bean,提供了事務支持spring

<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 相關的信息sql

## 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 支持事務app

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

爲後續的演示 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

相關文章
相關標籤/搜索