javascript
jdbctemplate 开启事务_SpringBoot 系列教程之事务隔离级别知识点小结
上一篇博文介紹了聲明式事務@Transactional的簡單使用姿勢,最文章的最后給出了這個注解的多個屬性,本文將著重放在事務隔離級別的知識點上,并通過實例演示不同的事務隔離級別下,臟讀、不可重復讀、幻讀的具體場景
I. 基礎知識
在進入正文之前,先介紹一下事務隔離級別的一些基礎知識點,詳細內容,推薦參考博文
mysql 之鎖與事務[1]
1. 基本概念
以下基本概念源于個人理解之后,通過簡單的 case 進行描述,如有問題,歡迎拍磚
更新丟失
簡單來講,兩個事務 A,B 分別更新一條記錄的 filedA, filedB 字段,其中事務 B 異常,導致回滾,將這條記錄的恢復為修改之前的狀態,導致事務 A 的修改丟失了,這就是更新丟失
臟讀
讀取到另外一個事務未提交的修改,所以當另外一個事務是失敗導致回滾的時候,這個讀取的數據其實是不準確的,這就是臟讀
不可重復讀
簡單來講,就是一個事務內,多次查詢同一個數據,返回的結果居然不一樣,這就是不可重復度(重復讀取的結果不一樣)
幻讀
同樣是多次查詢,但是后面查詢時,發現多了或者少了一些記錄
比如:查詢 id 在[1,10]之間的記錄,第一次返回了 1,2,3 三條記錄;但是另外一個事務新增了一個 id 為 4 的記錄,導致再次查詢時,返回了 1,2,3,4 四條記錄,第二次查詢時多了一條記錄,這就是幻讀
幻讀和不可重復讀的主要區別在于:
- 幻讀針對的是查詢結果為多個的場景,出現了數據的增加 or 減少
- 不可重復讀對的是某些特定的記錄,這些記錄的數據與之前不一致
2. 隔離級別
后面測試的數據庫為 mysql,引擎為 innodb,對應有四個隔離級別
隔離級別說明fixnot fixRU(read uncommitted)未授權讀,讀事務允許其他讀寫事務;未提交寫事務禁止其他寫事務(讀事務 ok)更新丟失臟讀,不可重復讀,幻讀RC(read committed)授權讀,讀事務允許其他讀寫事務;未提交寫事務,禁止其他讀寫事務更新丟失,臟讀不可重復讀,幻讀RR(repeatable read)可重復度,讀事務禁止其他寫事務;未提交寫事務,禁止其他讀寫事務更新丟失,臟讀,不可重復度幻讀serializable序列化讀,所有事務依次執行更新丟失,臟讀,不可重復度,幻讀-
說明,下面純為個人觀點,不代表權威,謹慎理解和引用
- 我個人的觀點,rr 級別在 mysql 的 innodb 引擎上,配合 mvvc + gap 鎖,已經解決了幻讀問題
- 下面這個 case 是幻讀問題么?
- 從鎖的角度來看,步驟 1、2 雖然開啟事務,但是屬于快照讀;而 9 屬于當前讀;他們讀取的源不同,應該不算在幻讀定義中的同一查詢條件中
II. 配置
接下來進入實例演示環節,首先需要準備環境,創建測試項目
創建一個 SpringBoot 項目,版本為2.2.1.RELEASE,使用 mysql 作為目標數據庫,存儲引擎選擇Innodb,事務隔離級別為 RR
1. 項目配置
在項目pom.xml文件中,加上spring-boot-starter-jdbc,會注入一個DataSourceTransactionManager的 bean,提供了事務支持
mysql mysql-connector-javaorg.springframework.boot spring-boot-starter-jdbc2. 數據庫配置
進入 spring 配置文件application.properties,設置一下 db 相關的信息
## DataSourcespring.datasource.url=jdbc:mysql://127.0.0.1:3306/story?useUnicode=true&characterEncoding=UTF-8&useSSL=falsespring.datasource.username=rootspring.datasource.password=3. 數據庫
新建一個簡單的表結構,用于測試
CREATETABLE`money` ( `id`int(11) unsignedNOTNULL AUTO_INCREMENT, `name`varchar(20) NOTNULLDEFAULT''COMMENT'用戶名', `money`int(26) NOTNULLDEFAULT'0'COMMENT'錢', `is_deleted`tinyint(1) NOTNULLDEFAULT'0', `create_at`timestampNOTNULLDEFAULTCURRENT_TIMESTAMPCOMMENT'創建時間', `update_at`timestampNOTNULLDEFAULTCURRENT_TIMESTAMPONUPDATECURRENT_TIMESTAMPCOMMENT'更新時間', PRIMARY KEY (`id`), KEY`name` (`name`)) ENGINE=InnoDB AUTO_INCREMENT=1DEFAULTCHARSET=utf8mb4;III. 實例演示
1. 初始化數據
準備一些用于后續操作的數據
@Componentpublicclass 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); returntrue;}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); returnfalse;}2. RU 隔離級別
我們先來測試 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)) { returntrue; } } this.query("ru: after updateMoney money", id); returnfalse;}只讀事務二(設置 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); returntrue;}接下來屬于測試的 case,用兩個線程來調用只讀事務,和讀寫事務
@Componentpublicclass 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 func) { System.out.println("============ " + tag + " start ========== "); try { func.apply(id); } catch (Exception e) { } System.out.println("============ " + tag + " end ========== ");}@FunctionalInterfacepublicinterface CallFunc { 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 >>>>開頭的記錄,首先兩次輸出結果不一致,所以不可重復讀問題是存在的
其次,第二次讀取的數據與讀寫事務中的中間結果一致,即讀取到了未提交的結果,即為臟讀
3. RC 事務隔離級別
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); returntrue;}/** * 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)) { returntrue; } } returnfalse;}測試用例
/** * 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 ==========從上面的輸出中,在只讀事務,前面兩次查詢,結果一致,雖然第二次查詢時,讀寫事務修改了這個記錄,但是并沒有讀取到這個中間記錄狀態,所以這里沒有臟讀問題;
當讀寫事務完畢之后,只讀事務的第三次查詢中,返回的是讀寫事務提交之后的結果,導致了不可重復讀
4. RR 事務隔離級別
針對 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); returntrue;}/** * 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)) { returntrue; } } returnfalse;}我們希望讀寫事務的執行周期在只讀事務的兩次查詢之內,所有測試代碼如下
/** * 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)
5. SERIALIZABLE 事務隔離級別
串行事務隔離級別,所有的事務串行執行,實際的業務場景中,我沒用過... 也不太能想像,什么場景下需要這種
@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); returntrue;}/** * 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)) { returntrue; } } returnfalse;}測試 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 了
6. 小結
本文主要介紹了事務的幾種隔離級別,已經不同干的隔離級別對應的場景,可能出現的問題;
隔離級別說明
級別fixnot fixRU更新丟失臟讀,不可重復讀,幻讀RC更新丟失 臟讀不可重復讀,幻讀RR更新丟、臟讀,不可重復讀,幻讀-serialze更新丟失、 臟讀,不可重復讀,幻讀-
使用說明
- mysql innodb 引擎默認為 RR 隔離級別;@Transactinoal注解使用數據庫的隔離級別,即 RR
- 通過指定Transactional#isolation來設置事務的事務級別
IV. 其他
0. 系列博文&源碼
源碼
- 工程:https://github.com/liuyueyi/spring-boot-demo[8]
- 實例源碼: https://github.com/liuyueyi/spring-boot-demo/blob/master/spring-boot/101-jdbctemplate-transaction[9]
總結
以上是生活随笔為你收集整理的jdbctemplate 开启事务_SpringBoot 系列教程之事务隔离级别知识点小结的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: matlab除与左除,Matlab左除和
- 下一篇: redistemplate使用_如何使用