上一篇博文介紹了聲明式事務@Transactional
的簡單使用姿式,最文章的最後給出了這個註解的多個屬性,本文將着重放在事務隔離級別的知識點上,並經過實例演示不一樣的事務隔離級別下,髒讀、不可重複讀、幻讀的具體場景mysql
<!-- more -->git
在進入正文以前,先介紹一下事務隔離級別的一些基礎知識點,詳細內容,推薦參考博文github
mysql 之鎖與事務spring
如下基本概念源於我的理解以後,經過簡單的 case 進行描述,若有問題,歡迎拍磚sql
更新丟失數據庫
簡單來說,兩個事務 A,B 分別更新一條記錄的 filedA, filedB 字段,其中事務 B 異常,致使回滾,將這條記錄的恢復爲修改以前的狀態,致使事務 A 的修改丟失了,這就是更新丟失app
髒讀ide
讀取到另一個事務未提交的修改,因此當另一個事務是失敗致使回滾的時候,這個讀取的數據實際上是不許確的,這就是髒讀spring-boot
不可重複讀
簡單來說,就是一個事務內,屢次查詢同一個數據,返回的結果竟然不同,這就是不可重複度(重複讀取的結果不同)
幻讀
一樣是屢次查詢,可是後面查詢時,發現多了或者少了一些記錄
好比:查詢 id 在[1,10]之間的記錄,第一次返回了 1,2,3 三條記錄;可是另一個事務新增了一個 id 爲 4 的記錄,致使再次查詢時,返回了 1,2,3,4 四條記錄,第二次查詢時多了一條記錄,這就是幻讀
幻讀和不可重複讀的主要區別在於:
後面測試的數據庫爲 mysql,引擎爲 innodb,對應有四個隔離級別
隔離級別 | 說明 | fix | not fix |
---|---|---|---|
RU(read uncommitted) | 未受權讀,讀事務容許其餘讀寫事務;未提交寫事務禁止其餘寫事務(讀事務 ok) | 更新丟失 | 髒讀,不可重複讀,幻讀 |
RC(read committed) | 受權讀,讀事務容許其餘讀寫事務;未提交寫事務,禁止其餘讀寫事務 | 更新丟失,髒讀 | 不可重複讀,幻讀 |
RR(repeatable read) | 可重複度,讀事務禁止其餘寫事務;未提交寫事務,禁止其餘讀寫事務 | 更新丟失,髒讀,不可重複度 | <del>幻讀</del> |
serializable | 序列化讀,全部事務依次執行 | 更新丟失,髒讀,不可重複度,幻讀 | - |
說明,下面存爲我的觀點,不表明權威,謹慎理解和引用
接下來進入實例演示環節,首先須要準備環境,建立測試項目
建立一個 SpringBoot 項目,版本爲2.2.1.RELEASE
,使用 mysql 做爲目標數據庫,存儲引擎選擇Innodb
,事務隔離級別爲 RR
在項目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>
進入 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=
新建一個簡單的表結構,用於測試
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=1 DEFAULT CHARSET=utf8mb4;
準備一些用於後續操做的數據
@Component public class DetailDemo { @Autowired private JdbcTemplate jdbcTemplate; @PostConstruct public void init() { String sql = "replace into money (id, name, money) values (320, '初始化', 200)," + "(330, '初始化', 200)," + "(340, '初始化', 200)," + "(350, '初始化', 200)"; jdbcTemplate.execute(sql); } }
提供一些基本的查詢和修改方法
private boolean updateName(int id) { String sql = "update money set `name`='更新' where id=" + id; jdbcTemplate.execute(sql); return true; } public void query(String tag, int id) { String sql = "select * from money where id=" + id; Map map = jdbcTemplate.queryForMap(sql); System.out.println(tag + " >>>> " + map); } private boolean updateMoney(int id) { String sql = "update money set `money`= `money` + 10 where id=" + id; jdbcTemplate.execute(sql); return false; }
咱們先來測試 RU 隔離級別,經過指定@Transactional
註解的isolation
屬性來設置事務的隔離級別
經過前面的描述,咱們知道 RU 會有髒讀問題,接下來設計一個 case,進行演示
事務一,修改數據
/** * ru隔離級別的事務,可能出現髒讀,不可避免不可重複讀,幻讀 * * @param id */ @Transactional(isolation = Isolation.READ_UNCOMMITTED, rollbackFor = Exception.class) public boolean ruTransaction(int id) throws InterruptedException { if (this.updateName(id)) { this.query("ru: after updateMoney name", id); Thread.sleep(2000); if (this.updateMoney(id)) { return true; } } this.query("ru: after updateMoney money", id); return false; }
只讀事務二(設置 readOnly 爲 true,則事務爲只讀)屢次讀取相同的數據,咱們但願在事務二的第一次讀取中,能獲取到事務一的中間修改結果(因此請注意兩個方法中的 sleep 使用)
@Transactional(readOnly = true, isolation = Isolation.READ_UNCOMMITTED, rollbackFor = Exception.class) public boolean readRuTransaction(int id) throws InterruptedException { this.query("ru read only", id); Thread.sleep(1000); this.query("ru read only", id); return true; }
接下來屬於測試的 case,用兩個線程來調用只讀事務,和讀寫事務
@Component public class DetailTransactionalSample { @Autowired private DetailDemo detailDemo; /** * ru 隔離級別 */ public void testRuIsolation() throws InterruptedException { int id = 330; new Thread(new Runnable() { @Override public void run() { call("ru: 只讀事務 - read", id, detailDemo::readRuTransaction); } }).start(); call("ru 讀寫事務", id, detailDemo::ruTransaction); } } private void call(String tag, int id, CallFunc<Integer, Boolean> func) { System.out.println("============ " + tag + " start ========== "); try { func.apply(id); } catch (Exception e) { } System.out.println("============ " + tag + " end ========== \n"); } @FunctionalInterface public interface CallFunc<T, R> { R apply(T t) throws Exception; }
輸出結果以下
============ ru 讀寫事務 start ========== ============ ru: 只讀事務 - read start ========== ru read only >>>> {id=330, name=初始化, money=200, is_deleted=false, create_at=2020-01-20 11:37:51.0, update_at=2020-01-20 11:37:51.0} ru: after updateMoney name >>>> {id=330, name=更新, money=200, is_deleted=false, create_at=2020-01-20 11:37:51.0, update_at=2020-01-20 11:37:52.0} ru read only >>>> {id=330, name=更新, money=200, is_deleted=false, create_at=2020-01-20 11:37:51.0, update_at=2020-01-20 11:37:52.0} ============ ru: 只讀事務 - read end ========== ru: after updateMoney money >>>> {id=330, name=更新, money=210, is_deleted=false, create_at=2020-01-20 11:37:51.0, update_at=2020-01-20 11:37:54.0} ============ ru 讀寫事務 end ==========
關注一下上面結果中ru read only >>>>
開頭的記錄,首先兩次輸出結果不一致,因此不可重複讀問題是存在的
其次,第二次讀取的數據與讀寫事務中的中間結果一致,即讀取到了未提交的結果,即爲髒讀
rc 隔離級別,能夠解決髒讀,可是不可重複讀問題沒法避免,因此咱們須要設計一個 case,看一下是否能夠讀取另一個事務提交後的結果
在前面的測試 case 上,稍微改一改
// ---------- rc 事物隔離級別 // 測試不可重複讀,一個事務內,兩次讀取的結果不同 @Transactional(readOnly = true, isolation = Isolation.READ_COMMITTED, rollbackFor = Exception.class) public boolean readRcTransaction(int id) throws InterruptedException { this.query("rc read only", id); Thread.sleep(1000); this.query("rc read only", id); Thread.sleep(3000); this.query("rc read only", id); return true; } /** * rc隔離級別事務,未提交的寫事務,會掛起其餘的讀寫事務;可避免髒讀,更新丟失;但不能防止不可重複讀、幻讀 * * @param id * @return */ @Transactional(isolation = Isolation.READ_COMMITTED, rollbackFor = Exception.class) public boolean rcTranaction(int id) throws InterruptedException { if (this.updateName(id)) { this.query("rc: after updateMoney name", id); Thread.sleep(2000); if (this.updateMoney(id)) { return true; } } return false; }
測試用例
/** * rc 隔離級別 */ private void testRcIsolation() throws InterruptedException { int id = 340; new Thread(new Runnable() { @Override public void run() { call("rc: 只讀事務 - read", id, detailDemo::readRcTransaction); } }).start(); Thread.sleep(1000); call("rc 讀寫事務 - read", id, detailDemo::rcTranaction); }
輸出結果以下
============ rc: 只讀事務 - read start ========== rc read only >>>> {id=340, name=初始化, money=200, is_deleted=false, create_at=2020-01-20 11:46:17.0, update_at=2020-01-20 11:46:17.0} ============ rc 讀寫事務 - read start ========== rc: after updateMoney name >>>> {id=340, name=更新, money=200, is_deleted=false, create_at=2020-01-20 11:46:17.0, update_at=2020-01-20 11:46:23.0} rc read only >>>> {id=340, name=初始化, money=200, is_deleted=false, create_at=2020-01-20 11:46:17.0, update_at=2020-01-20 11:46:17.0} ============ rc 讀寫事務 - read end ========== rc read only >>>> {id=340, name=更新, money=210, is_deleted=false, create_at=2020-01-20 11:46:17.0, update_at=2020-01-20 11:46:25.0} ============ rc: 只讀事務 - read end ==========
從上面的輸出中,在只讀事務,前面兩次查詢,結果一致,雖然第二次查詢時,讀寫事務修改了這個記錄,可是並無讀取到這個中間記錄狀態,因此這裏沒有髒讀問題;
當讀寫事務完畢以後,只讀事務的第三次查詢中,返回的是讀寫事務提交以後的結果,致使了不可重複讀
針對 rr,咱們主要測試一下不可重複讀的解決狀況,設計 case 相對簡單
/** * 只讀事務,主要目的是爲了隔離其餘事務的修改,對本次操做的影響; * * 好比在某些耗時的涉及屢次表的讀取操做中,爲了保證數據一致性,這個就有用了; 開啓只讀事務以後,不支持修改數據 */ @Transactional(readOnly = true, isolation = Isolation.REPEATABLE_READ, rollbackFor = Exception.class) public boolean readRrTransaction(int id) throws InterruptedException { this.query("rr read only", id); Thread.sleep(3000); this.query("rr read only", id); return true; } /** * rr隔離級別事務,讀事務禁止其餘的寫事務,未提交寫事務,會掛起其餘讀寫事務;可避免髒讀,不可重複讀,(我我的認爲,innodb引擎可經過mvvc+gap鎖避免幻讀) * * @param id * @return */ @Transactional(isolation = Isolation.REPEATABLE_READ, rollbackFor = Exception.class) public boolean rrTransaction(int id) { if (this.updateName(id)) { this.query("rr: after updateMoney name", id); if (this.updateMoney(id)) { return true; } } return false; }
咱們但願讀寫事務的執行週期在只讀事務的兩次查詢以內,全部測試代碼以下
/** * rr * 測試只讀事務 */ private void testReadOnlyCase() throws InterruptedException { // 子線程開啓只讀事務,主線程執行修改 int id = 320; new Thread(new Runnable() { @Override public void run() { call("rr 只讀事務 - read", id, detailDemo::readRrTransaction); } }).start(); Thread.sleep(1000); call("rr 讀寫事務", id, detailDemo::rrTransaction); }
輸出結果
============ rr 只讀事務 - read start ========== rr read only >>>> {id=320, name=初始化, money=200, is_deleted=false, create_at=2020-01-20 11:46:17.0, update_at=2020-01-20 11:46:17.0} ============ rr 讀寫事務 start ========== rr: after updateMoney name >>>> {id=320, name=更新, money=200, is_deleted=false, create_at=2020-01-20 11:46:17.0, update_at=2020-01-20 11:46:28.0} ============ rr 讀寫事務 end ========== rr read only >>>> {id=320, name=初始化, money=200, is_deleted=false, create_at=2020-01-20 11:46:17.0, update_at=2020-01-20 11:46:17.0} ============ rr 只讀事務 - read end ==========
兩次只讀事務的輸出一致,並無出現上面的不可重複讀問題
說明
@Transactional
註解的默認隔離級別爲Isolation#DEFAULT
,也就是採用數據源的隔離級別,mysql innodb 引擎默認隔離級別爲 RR(全部不額外指定時,至關於 RR)串行事務隔離級別,全部的事務串行執行,實際的業務場景中,我沒用過... 也不太能想像,什麼場景下須要這種
@Transactional(readOnly = true, isolation = Isolation.SERIALIZABLE, rollbackFor = Exception.class) public boolean readSerializeTransaction(int id) throws InterruptedException { this.query("serialize read only", id); Thread.sleep(3000); this.query("serialize read only", id); return true; } /** * serialize,事務串行執行,fix全部問題,可是性能低 * * @param id * @return */ @Transactional(isolation = Isolation.SERIALIZABLE, rollbackFor = Exception.class) public boolean serializeTransaction(int id) { if (this.updateName(id)) { this.query("serialize: after updateMoney name", id); if (this.updateMoney(id)) { return true; } } return false; }
測試 case
/** * Serialize 隔離級別 */ private void testSerializeIsolation() throws InterruptedException { int id = 350; new Thread(new Runnable() { @Override public void run() { call("Serialize: 只讀事務 - read", id, detailDemo::readSerializeTransaction); } }).start(); Thread.sleep(1000); call("Serialize 讀寫事務 - read", id, detailDemo::serializeTransaction); }
輸出結果以下
============ Serialize: 只讀事務 - read start ========== serialize read only >>>> {id=350, name=初始化, money=200, is_deleted=false, create_at=2020-01-20 12:10:23.0, update_at=2020-01-20 12:10:23.0} ============ Serialize 讀寫事務 - read start ========== serialize read only >>>> {id=350, name=初始化, money=200, is_deleted=false, create_at=2020-01-20 12:10:23.0, update_at=2020-01-20 12:10:23.0} ============ Serialize: 只讀事務 - read end ========== serialize: after updateMoney name >>>> {id=350, name=更新, money=200, is_deleted=false, create_at=2020-01-20 12:10:23.0, update_at=2020-01-20 12:10:39.0} ============ Serialize 讀寫事務 - read end ==========
只讀事務的查詢輸出以後,才輸出讀寫事務的日誌,簡單來說就是讀寫事務中的操做被 delay 了
本文主要介紹了事務的幾種隔離級別,已經不一樣乾的隔離級別對應的場景,可能出現的問題;
隔離級別說明
級別 | fix | not fix |
---|---|---|
RU | 更新丟失 | 髒讀,不可重複讀,幻讀 |
RC | 更新丟失 髒讀 | 不可重複讀,幻讀 |
RR | 更新丟、髒讀,不可重複讀,幻讀 | - |
serialze | 更新丟失、 髒讀,不可重複讀,幻讀 | - |
使用說明
@Transactinoal
註解使用數據庫的隔離級別,即 RRTransactional#isolation
來設置事務的事務級別系列博文
源碼
盡信書則不如,以上內容,純屬一家之言,因我的能力有限,不免有疏漏和錯誤之處,如發現 bug 或者有更好的建議,歡迎批評指正,不吝感激
下面一灰灰的我的博客,記錄全部學習和工做中的博文,歡迎你們前去逛逛