初學的時候,感受事務的四大特性就那麼回事,不就是一堆事要麼完成,要麼所有失敗嗎。還有常常說的髒讀,幻讀,不可重複讀根本沒法理解,就是那個存款取款的例子,我修改了數據,對方看到我修改的數據,這不很正常嗎。如今看來,當時根本就不知道併發是什麼鬼,更何談併發事物了。java
而後給你來一堆名詞,共享鎖,排它鎖,悲觀鎖,樂觀鎖...... 想一想就以爲那時候能記下來已是奇蹟了。mysql
Spring 還給事務弄了一個傳播機制的傢伙,Spring 事務傳播機制能夠看這篇文章 。 本文應該來講是對初學者的福音,有必定經驗的人看的話應該也會有收穫。git
這個是剛入門面試的時候必問一個面試題,剛入行的時候我是硬生生背下來的。程序員
隔離性(Isolation) 兩個事務之間是隔離程度,具體的隔離程度由隔離級別決定,隔離級別有面試
事務指的是從開始事務->執行操做->提交/回滾 整個過程,在程序中使用一個鏈接對應一個事務spring
-- sql 中的事務 START TRANSACTION; select * from question; commit ;
// 最原始的 jdbc 事務 Connection connection = 獲取數據庫鏈接; try{ connection.setAutoCommit(false); // todo something connection.commit(); }catch(Exception e){log(e); connection.rollback(); }finally{ try{connection.close()}catch(Exception e){log(e);}; }
併發事務是指兩個事務一同開始執行,若是兩個事務操做的數據之間有交集,則頗有可能產生衝突。這時怎麼辦呢,其實這也是 臨界資源 的一種,在應用程序中,咱們解決這類問題的關鍵是加鎖,在數據庫的實現也是同樣,但在數據庫中須要考慮更多。常見的須要考慮的問題有(下面說的我和人都是指一個會話)sql
看過網上的大部分文章,基本都是一個表格來演示兩個事務的併發,有的根本就是直接抄的,不知道那做者真的懂了沒,其實咱們是能夠用客戶端來模擬兩個事務併發的狀況的,打開兩個 session ,讓兩個事務互相穿插。數據庫
下面的演示都是基於 mysql5.7
版本,查詢事務隔離級別和修改隔離級別語句session
-- 查看事務隔離級別 select @@tx_isolation; -- 修改當前 session 事務隔離級別 set session transaction isolation level read uncommitted; set session transaction isolation level read committed ; set session transaction isolation level repeatable read ; set session transaction isolation level serializable; -- 開啓事務提交和回滾 START TRANSACTION; select * from question; commit ;rollback;
準備數據表,暫時先使用 InnoDB 引擎併發
CREATE TABLE `account` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(64) DEFAULT NULL, `balance` decimal(10,2) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; INSERT INTO `test`.`account` (`id`, `name`, `balance`) VALUES ('1', 'sanri', '100.00'); INSERT INTO `test`.`account` (`id`, `name`, `balance`) VALUES ('2', '9420', '100.00');
打開兩個 session ,設置隔離級別爲 read uncommitted
時間(相對時間) | 事務A | 事務B |
---|---|---|
1 | start TRANSACTION | |
2 | start TRANSACTION | |
3 | update account set balance = balance - 20 where id = 1; | |
4 | select * from account where id = 1 -- 80 | |
5 | rollback | |
6 | commit |
這個會有什麼問題呢,網上說可能事務 B 可能會去存款,但我試過了,事務B 在這時候存款會被阻塞,由於事務A 在更新的時候已經加了排它鎖,只有等事務A 提交或回滾事務B 才能執行。
它真正的問題出在,若是程序來讀到了這個 80 塊錢返回到了第三方的系統,而事務A 回滾了,這時候問題就大了,它主要體如今讀不一致。或者用戶看到我本身取款失敗了錢沒取到但爲何我賬戶餘額少了的不一致問題。
解決髒讀是設置隔離級別爲讀提交的數據 read committed
打開兩個 session 設置隔離級別爲 read committed
時間(相對時間) | 事務A | 事務B |
---|---|---|
1 | start TRANSACTION | |
2 | start TRANSACTION | |
3 | select * from account where id = 1 -- 100 | |
4 | update account set balance = balance - 20 where id = 1; | |
5 | commit; | |
6 | select * from account where id = 1 -- 80 | |
7 | commit; |
兩次一樣條件的查詢,結果確不一致。剛開始的時候必定會以爲,這沒問題啊,事務B 作了更新操做,我這少 20 塊錢變 80 有問題嗎?
其實仍是有問題的,主要出如今複雜的業務邏輯查了兩次相同的數據集(在程序員看來是相同數據集),又好比 mapper 中有兩個方法名不同,但作了一樣功能的 sql 語句 (這個在代碼屢次接手後會出現),再或者在一個 sql 塊中有兩個更新語句使用了同一個查詢,恰好數據被改了
begin update xxx inner join (select balance from account where id = 1) set xxx = xxoo; update xoxo inner join (select balance from account where id = 1) set xxbb = mmcc; end
解決辦法是設置隔離級別爲可重複讀 repeatable read
或者顯示的加上共享鎖 (select * from account where id = 1 lock in share mode;
),但這會阻塞事務B,由於共享鎖是一種悲觀鎖
mysql 的多事務併發版本控制
使用可重複讀以後會發現,發現查詢和更新並無互相阻塞,推測 mysql 應該不是簡單的使用共享鎖來實現可重複讀, 使用共享鎖會使性能特別低下,由於一個查詢也要加鎖。
Mysql 的可重複讀使用的是 MVCC 機制,當一個事務開始後,select 查詢屢次都會和第一次查詢的結果一致,這種查詢稱爲快照讀,與之相對的是當前讀,對於加鎖語句,或更新語句都是使用當前讀 ,好比
-- 這裏的更新會使用最新的 balance 來更新,同時會加上排它鎖,不用擔憂最終結果是錯的 update account set balance = balance - 20 where id = 1
幻讀相比較於不可重複讀來講有點相似,都是同一個查詢條件查到了不一致的結果,但幻讀更注重於添加或刪除數據,而不可重複讀注重於修改數據,產生的影響也是和不可重複讀相似的。
More Actions時間(相對時間) | 事務A | 事務B |
---|---|---|
1 | start TRANSACTION | |
2 | start TRANSACTION | |
3 | select * from account | |
4 | delete from account where id = 1 | |
5 | commit; | |
6 | select * from account -- 少了一行 |
幻讀的解決辦法一種就是修改隔離級別爲 serializable
,或者鎖定整張表,但不論是串行化執行事務或鎖定整張表,都是同一時刻只有一個事務在執行的意思,也即沒有併發事務了,性能會特別低下。
mysql 有一個 gap 鎖的機制,它在 repeatable read
隔離級別下防止了幻讀,也沒有鎖整張表,它取了一個平衡值,鎖定索引間的間隙。具體查看這篇文章或查看官網說明
http://www.javashuo.com/article/p-mxfgnwhq-z.html
隔離級別 | 髒讀 | 不可重複讀 | 幻讀 |
---|---|---|---|
read uncommitted | 容許 | 容許 | 容許 |
read committed | 不容許 | 容許 | 容許 |
repeatable read | 不容許 | 不容許 | 容許 |
serializable | 不容許 | 不容許 | 不容許 |
這個問題是我在工做中遇到的,先來看一段代碼
@Transactional public synchronized void insertXX(xx){ long maxNo = xxMapper.selectMaxNo(); return maxNo + 1; XXEntity xx = new XXEntity(maxNo,'x','xx'); xxMapper.insert(xx); }
初一看這個方法,沒啥問題,獲取最大編號並添加進數據庫,爲防止併發致使編號重複加了同步鎖。
但在實際生產環境中這個方法出問題了,出現了相同的編號致使程序出錯。
其實這裏的緣由是由於鎖並無完整的包含事務,事務是 spring 用 aop 實現的,在代理方法中去調用了目標方法,可是鎖是加在了目標方法上,事務在鎖釋放後才提交,又由於隔離級別使用的是可重複讀,讀不到未提交的數據,因此若是在事務提交的過程當中,有線程執行此方法,是沒有上鎖的,進來查到的編號仍是原來的編號,解決辦法有兩種 ,一種是把鎖上移,使用 aop 來實現鎖,一種是再加一個方法不加事務,幷包裹本方法。
方法一:
@Autowized private XXService xxService; @Transactional(propagation = Propagation.NOT_SUPPORTED) public synchronized void proxyXX(){ xxService.insertXX(); } @Transactional public void insertXX(xx){ long maxNo = xxMapper.selectMaxNo(); return maxNo + 1; XXEntity xx = new XXEntity(maxNo,'x','xx'); xxMapper.insert(xx); }
這裏必須另啓一個類,由於 spring aop 是對類生效的
方法二:
定義一個切面,好比用註解來實現切點,而後加鎖
@Lock @Transactional public void insertXX(xx){ long maxNo = xxMapper.selectMaxNo(); return maxNo + 1; XXEntity xx = new XXEntity(maxNo,'x','xx'); xxMapper.insert(xx); }
都知道 MyISAM 只支持表鎖,MyISAM 能支持行鎖和表鎖,但 Innodb 使用行鎖也是有條件的,就是查詢列必須是索引的,不然將使用表鎖
還有一個特色就是 Innodb 是支持事務的,但 Myisam 不支持事務
對於 MyISAM來講更加適合那種不常常作更新操做只提供查詢和 統計操做的數據,好比
統計表,配置表,冷數據表...
對於 Innodb 來講適合的主要對象就是常常作更新操做的表,好比
業務表,熱數據表
創做不易,但願能夠支持下個人開源軟件,及個人小工具,歡迎來 gitee 點星,fork ,提 bug 。
Excel 通用導入導出,支持 Excel 公式
博客地址:https://blog.csdn.net/sanri1993/article/details/100601578
gitee:https://gitee.com/sanri/sanri-excel-poi
使用模板代碼 ,從數據庫生成代碼 ,及一些項目中常常能夠用到的小工具
博客地址:https://blog.csdn.net/sanri1993/article/details/98664034
gitee:https://gitee.com/sanri/sanri-tools-maven