事務與鎖完整版

事務

初學的時候,感覺事務的四大特性就那麼回事,不就是一堆事要麼完成,要麼全部失敗嗎。還有經常說的髒讀,幻讀,不可重複讀根本無法理解,就是那個存款取款的例子,我修改了數據,對方看到我修改的數據,這不很正常嗎。現在看來,當時根本就不知道併發是什麼鬼,更何談併發事物了。

然後給你來一堆名詞,共享鎖,排它鎖,悲觀鎖,樂觀鎖...... 想想就覺得那時候能記下來已經是奇蹟了。

Spring 還給事務弄了一個傳播機制的傢伙,Spring 事務傳播機制可以看這篇文章 。 本文應該來說是對初學者的福音,有一定經驗的人看的話應該也會有收穫。

事務的四大特性ACID

這個是剛入門面試的時候必問一個面試題,剛入行的時候我是硬生生背下來的。

  • 原子性(Atomicity) 一件事情的所有步驟要麼全部成功,要麼全部失敗,不存在中間狀態。
  • 一致性(Consistency) 事務執行的結果必須是使數據庫從一個一致性狀態變到另一個一致性狀態。一致性與原子性是密切相關的。
  • 隔離性(Isolation) 兩個事務之間是隔離程度,具體的隔離程度由隔離級別決定,隔離級別有

    • 讀未提交的 (read-uncommitted)
    • 讀提交的 (read-committed)
    • 可重複讀 (repeatable-read)
    • 串行 (serializable)
  • 持久性 (Durability) 一個事務提交後,數據庫狀態就永遠的發生改變,不會因爲數據庫宕機而讓提交不生效。

一個事務和併發事務

事務指的是從開始事務->執行操作->提交/回滾 整個過程,在程序中使用一個連接對應一個事務

-- 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);};
}

併發事務是指兩個事務一同開始執行,如果兩個事務操作的數據之間有交集,則很有可能產生衝突。這時怎麼辦呢,其實這也是 臨界資源 的一種,在應用程序中,我們解決這類問題的關鍵是加鎖,在數據庫的實現也是一樣,但在數據庫中需要考慮更多。常見的需要考慮的問題有(下面說的我和人都是指一個會話)

  • 對整張表數據加鎖還是對當前操作的數據行加鎖,這時有表鎖和行鎖,MyISAM 引擎只支持表鎖,而 innodb 支持行鎖和表鎖
  • 如果數據量龐大,比如選到了百萬數據,千萬數據,不可能一次性全部加鎖, 會很影響性能,innodb 是逐條加鎖的
  • 數據庫的操作其實有很大一部分是查詢操作,如果鎖住數據,任何人都不讓進的話,性能也會很低下,所以會有讀鎖和寫鎖,也叫共享鎖和排它鎖
  • 根據檢測衝突的時間不同,可以在一開始就把數據鎖住,直到我使用完,還有就是在真正操作數據的時候纔去鎖住,就是悲觀鎖和樂觀鎖
  • 就算是讓別人可以讀數據,在兩個事務也可能互相影響,比如髒讀。

事務的隔離級別及會帶來的問題

看過網上的大部分文章,基本都是一個表格來演示兩個事務的併發,有的根本就是直接抄的,不知道那作者真的懂了沒,其實我們是可以用客戶端來模擬兩個事務併發的情況的,打開兩個 session ,讓兩個事務互相穿插。

下面的演示都是基於 mysql5.7 版本,查詢事務隔離級別和修改隔離級別語句

-- 查看事務隔離級別
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 隔離級別下防止了幻讀,也沒有鎖整張表,它取了一個平衡值,鎖定索引間的間隙。具體查看這篇文章或查看官網說明

https://blog.csdn.net/aaa821/article/details/81017704

隔離級別 髒讀 不可重複讀 幻讀
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 和 Innodb 及行級鎖的條件

都知道 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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章