成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

高并發場景下的MySQL幾類死鎖事故案例分析

數據庫 MySQL
通俗一些來說,A線程持有B鎖,然后想要訪問A鎖,此時B線程持有A鎖,想要訪問B鎖,這種情況下就容易出現死鎖。

本文主要講解MySQL中出現死鎖的應用案例,以及相關的業務場景,不會純講理論,希望對這塊感興趣的朋友可以有所幫助。

什么是死鎖

多個線程在訪問某些資源的時候,需要等待對方釋放彼此所需資源,而進入了等待互斥的狀態。

通俗一些來說,A線程持有B鎖,然后想要訪問A鎖,此時B線程持有A鎖,想要訪問B鎖,這種情況下就容易出現死鎖。

MySQL中鎖的類型有哪些?

下邊我們以用戶消息表案例來進行說明:

CREATE TABLE `t_user_message` (
  `id` bigint unsigned NOT NULL AUTO_INCREMENT,
  `user_id` int unsigned NOT NULL DEFAULT '0' COMMENT '發信方id',
  `object_id` int unsigned NOT NULL DEFAULT '0' COMMENT '收信方id',
  `relation_id` int unsigned NOT NULL DEFAULT '0' COMMENT '關聯id',
  `is_read` tinyint unsigned NOT NULL DEFAULT '0' COMMENT '是否已讀(0未讀,1已讀)',
  `sid` int unsigned NOT NULL DEFAULT '0' COMMENT '消息條數',
  `status` tinyint unsigned NOT NULL DEFAULT '1' COMMENT '狀態(0無效 1有效)',
  `content` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT '消息內容',
  `type` tinyint unsigned NOT NULL DEFAULT '0' COMMENT '類型(0文本,1語音,2圖片,3視頻,4表情,5分享鏈接)',
  `ext_json` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT '擴展字段',
  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '創建時間',
  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新時間',
  PRIMARY KEY (`id`),
  KEY `idx_user_id` (`user_id`) USING BTREE COMMENT '發信方id索引',
  KEY `idx_object_id` (`object_id`) USING BTREE COMMENT '收信方id索引',
  KEY `idx_relation_id` (`relation_id`) USING BTREE COMMENT '關聯id索引'
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb3 COLLATE=utf8_bin COMMENT='用戶消息表';

按照鎖的粒度來區分,可以分為以下兩種:

行鎖

只會鎖住特定的行記錄,例如下邊這條sql:

select * from t_user_message where user_id=1001 for update;

表鎖

會把整個表的數據給鎖住,性能較差,例如下邊這條sql:

select * from t_user_message  for update;

排它鎖和共享鎖的區別

共享鎖

在多個事務里面都可以讀取共享鎖所鎖住的內容。

排它鎖

只能在一個事務里對同樣的數據進行加鎖,假設A事務對某行數據加入了排它鎖之后,其他事務就無法再對該行記錄加入排它鎖。

關于排它鎖和共享鎖的使用

看到這里,你可能對共享鎖和排它鎖并不是理解得很徹底,那么先別著急,我們先從實戰來加深下你對它的理解。

排它鎖

在Innodb存儲引擎中,常見的update,insert,delete這些sql都會默認加入上排他鎖,而我們的select語句如果沒有加入特殊關鍵字(下邊會講是什么樣的特殊關鍵字) ,是不會加入排他鎖的。

如果select語句希望加入排它鎖,那么可以嘗試以下方式:

使用 for update 關鍵字

select * from t_user_message  for update;

共享鎖

在正常的select語句中,是不會有加鎖的,例如下邊這條sql:

select * from t_user_message;

這條sql在innodb中,默認是不會鎖表,也不會鎖行記錄。如果你希望加上一把共享鎖,那么可以嘗試以下的這種寫法:

使用 lock in share mode 關鍵字

select * from t_user_message lock in share mode;

lock in share mode 和 for update使用起來有什么區別?

來看看這個案例,我們準備了兩個MySQL的會話窗口。

lock in share mode 測試

先來看會話A:會話A中,關閉了自動提交功能,然后執行這個lock in share mode的鎖,此時它使用了共享鎖鎖住了全表的內容。

圖片圖片

再來看會話B:會話B中也是相同的,關閉自動提交后,執行lock in share mode的共享鎖,發現依然可以正常查詢,沒有堵塞行為。

圖片圖片

這時候我們將會話B的當前事務先提交,然后在會話B中繼續執行一條update語句(非事務狀態下) ,要知道update是默認帶了拍它鎖的,此時因為我們的會話A沒有commit,所以會話B的這條update操作會進入堵塞的狀態,如下圖:

圖片圖片

只有當會話A的事務執行完畢了,將lock in share mode的鎖給釋放掉,會話B才會繼續執行。

for update測試

下邊讓我們來看看 for update 加鎖的影響,會話A關閉了自動提交,然后執行了一條for update的sql,但是沒有commit;此時我們的會話B也開始了同樣的步驟,但是卻卡住了。

圖片圖片

假如事務A一直都不提交的話,那么事務B最終會報出以下異常:

圖片圖片

[查詢2中發生錯誤] Lock wait timeout exceeded; try restarting transaction

再來看看for update鎖住的數據,對于其他會話的寫操作有何影響。

如下圖所示,我們的會話A依舊沒有commit,但是此時會話B中嘗試執行一次update操作,由于update默認帶了排他鎖,這條sql會鎖表,所以和會話A中的for update鎖出現了沖突,導致會話B一直處于堵塞狀態。

圖片圖片

小總結

通過上述的幾個測試,大家應該也有深刻的體會了,那么我們就來進行下總結,加深下印象。


lock in share mode 鎖

for update 鎖

多session讀

不會堵塞,多個session可以讀共同鎖住的記錄。

會堵塞,只能有一個session讀取到鎖住的記錄,其他session的訪問得等待。

多session寫

會堵塞,任何寫相關的操作都不行

會堵塞,任何寫相關的操作都不行


看到這里,你應該對lock in share mode 和 for update 有一定了解了吧,但是這兩種鎖,光了解理論,其實還是不夠的,需要有實戰才能讓你對它理解更加深刻,來看下邊的案例。

lock in share mode使用不當,導致死鎖

來看下邊的這個業務場景:

假設我們有一個賬戶表,表結構如下:

CREATE TABLE `t_account` (
  `id` int unsigned NOT NULL AUTO_INCREMENT,
  `user_id` int DEFAULT NULL,
  `coin` int DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb3 COLLATE=utf8_bin;

然后在業務操作上,我們的賬戶扣款和增款邏輯上的設計如下:

//開啟一個事務操作
set autocommit=0;
begin;
//如果賬戶存在,才進行update,如果賬戶不存在,就得先insert
select * from t_account where user_id=111 lock in share mode;


//這里我們假設賬戶是存在的,那么就直接選擇打款入賬
update t_account set coin=coin+100 where user_id=111;


//記錄到賬戶流水表中
INSERT INTO `transaction_log` (`id`, `business`, `foreign_key`)
VALUES (1, X'6F726465722D62697A', X'3234303938413535333031324444443044444137363036333744434233373834333643313138423441414332323236454644463430303034');


commit;

這里為了保證將賬戶流水記錄和打款兩個操作保證一致性,得加入一個本地事務去修飾。但是這段代碼中使用了一個lock in share mode的關鍵字,這個關鍵字是為了避免在并發的情況下,對賬戶記錄進行讀的過程中,有其他地方對賬戶的coin值進行寫的修改。

之所以可能會有其他地方對coin值進行額外的寫操作,主要原因是因為系統業務中的老舊代碼存在,重復造輪子,本來A服務中只有一處地方對賬戶進行修改操作,結果在B服務里,也有一段類似的代碼修改,直接操作了數據庫表,但是由于不好去調整那個服務的代碼,所以暫時只能用 lock in share mode 操作去加鎖。相比于for update鎖來說,使用lock in share mode加鎖,對于讀的影響不大,所以早期設計的時候,沒有考慮那么多,就直接用了它上線。并且上線之后并發度不高,暫時就沒有發現什么問題。

看到這里,你可能感覺似乎這種設計沒有什么問題,那么我們來看看下邊的這個場景:

隨著并發度的增加,我們將修改余額的這個操作,在A服務里面封裝成為了一個方法,并且供各個地方進行調用。但是有一天,出現了這么一個業務場景:

在RocketMQ的消費方,會對用戶的賬戶進行打款操作。在這個消費方的代碼中,同一個userId的消息會有許多條,而且是同一時刻的大量并發消費,這就意味著,同一時刻會有大量的請求調用這個打款的操作,而且是并發,同一個userId。那么這種情況下,我們的 lock in share mode會發生什么樣的情況呢 -- 死鎖

來看下圖:

圖片圖片

由于我們的線程A持有了鎖,線程B也持有了鎖,但是它們接下來的update操作,都是得等對方將共享鎖釋放后才可以繼續執行,所以就發生了死鎖的場景。

圖片圖片

如何解決上述的lock in share mode死鎖

那么我們如何卻避免上邊的場景發生呢,這里我給出以下兩種思路。

不用鎖,提升事務隔離級別為讀已提交

//開啟一個事務操作
set autocommit=0;


//如果賬戶存在,才進行update,如果賬戶不存在,就得先insert
select * from t_account where user_id=111;


//這里我們假設賬戶是存在的,那么就直接選擇打款入賬
update t_account set coin=coin+100,version=version+1 where user_id=111;


//記錄到賬戶流水表中
INSERT INTO `transaction_log` (`id`, `business`, `foreign_key`)
VALUES (1, X'6F726465722D62697A', X'3234303938413535333031324444443044444137363036333744434233373834333643313138423441414332323236454644463430303034');


commit;


去掉使用lock in share mode,使用樂觀鎖。

例如加入一個version字段,那么我們在執行賬戶扣款的時候,加入version的判斷。例如:

//開啟一個事務操作
set autocommit=0;


//如果賬戶存在,才進行update,如果賬戶不存在,就得先insert
select * from t_account where user_id=111 and version=#{version};


//這里我們假設賬戶是存在的,那么就直接選擇打款入賬
update t_account set coin=coin+100,version=version+1 where user_id=111 and version=#{version};


//記錄到賬戶流水表中
INSERT INTO `transaction_log` (`id`, `business`, `foreign_key`)
VALUES (1, X'6F726465722D62697A', X'3234303938413535333031324444443044444137363036333744434233373834333643313138423441414332323236454644463430303034');


commit;

這里要注意,當同時兩個會話針對同一行數據執行上述更新操作的時候,可能會導致同一行的記錄被鎖,所以我們在進行update的時候,可以用一個version字段去管理。但是這種設計,可能會導致一次更新失敗,需要進行重試,因此并發量高的情況下,容易對MySQL造成較大的壓力。

引入分布式鎖

直接在業務層引入一把分布式鎖,這種思路比較暴力,但是確實有效。

其實只要我們的select類型的sql中進行顯示加鎖,就有可能會有死鎖情況發生,所以建議大家使用的時候謹慎。

行鎖的幾種類型

  • Record Lock(記錄鎖):單個行記錄上的鎖。這個也是我們日常認為的行鎖。
  • Gap Lock(間隙鎖):間隙鎖,鎖定一個范圍,但不包括記錄本身(只不過它的鎖粒度比記錄鎖的鎖整行更大一些,他是鎖住了某個范圍內的多個行,包括根本不存在的數據)。GAP鎖的目的,是為了防止同一事務的兩次當前讀,出現幻讀的情況。該鎖只會在隔離級別是RR或者以上的級別內存在。間隙鎖的目的是為了讓其他事務無法在間隙中新增數據。
  • Next-Key Lock(臨鍵鎖):它是記錄鎖和間隙鎖的結合,鎖定一個范圍,并且鎖定記錄本身。對于行的查詢,都是采用該方法,主要目的是解決幻讀的問題。next-key鎖是InnoDB默認的鎖,該鎖也只會在隔離級別是RR或者以上的級別內存在。

行鎖的事故案例

消息數據更新設計不當,導致出現Record Lock死鎖

這里我們需要先了解下消息記錄表的結構;

CREATE TABLE `t_user_message` (
  `id` bigint unsigned NOT NULL AUTO_INCREMENT,
  `user_id` int unsigned NOT NULL DEFAULT '0' COMMENT '發信方id',
  `object_id` int unsigned NOT NULL DEFAULT '0' COMMENT '收信方id',
  `relation_id` int unsigned NOT NULL DEFAULT '0' COMMENT '關聯id',
  `is_read` tinyint unsigned NOT NULL DEFAULT '0' COMMENT '是否已讀(0未讀,1已讀)',
  `sid` int unsigned NOT NULL DEFAULT '0' COMMENT '消息條數',
  `status` tinyint unsigned NOT NULL DEFAULT '1' COMMENT '狀態(0未審核 1審核失敗 2審核通過)',
  `content` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT '消息內容',
  `type` tinyint unsigned NOT NULL DEFAULT '0' COMMENT '類型(0文本,1語音,2圖片,3視頻,4表情,5分享鏈接)',
  `ext_json` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT '擴展字段',
  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '創建時間',
  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新時間',
  PRIMARY KEY (`id`),
  KEY `idx_user_id` (`user_id`) USING BTREE COMMENT '發信方id索引',
  KEY `idx_object_id` (`object_id`) USING BTREE COMMENT '收信方id索引',
  KEY `idx_relation_id` (`relation_id`) USING BTREE COMMENT '關聯id索引'
) ENGINE=InnoDB AUTO_INCREMENT=100015 DEFAULT CHARSET=utf8mb3 COLLATE=utf8_bin COMMENT='用戶消息表';

假設我們的會話A執行了以下事務操作:

START TRANSACTION;


//更新用戶的消息狀態,從未讀變為已讀
update t_user_message set is_read=1 where user_id=1003 and object_id=1004;
//...中間有些別的業務操作
update t_user_message set is_read=1 where user_id=1001 and object_id=1002;


commit;

而此時我們的會話B在執行一個異步的消息是否合法的檢測工作,具體操作如下:

set autocommit=0;
START TRANSACTION;


//定時任務更新用戶的消息審核狀態,從未審核變為審核通過
update t_user_message set status=2 where user_id=1001 and object_id=1002;
//...中間有些別的業務操作
update t_user_message set status=2 where user_id=1003 and object_id=1004;


commit;

這兩個事務如果并發執行,并發度高的情況下,可能會出現死鎖情況,死鎖產生的步驟如下圖所示:

圖片圖片

一般遇到這類情況,我們都會推薦在進行更新的時候,盡可能的避免死鎖條件發生,例如調整sql的執行順序。例如變更為如下操作:

圖片圖片

另外,調整順序后,盡量將本地事務的顆粒度控制到最小,減少因為加鎖堵塞帶來的性能問題。

間隙鎖堵塞案例分析

首先我們要將當前會話的事務隔離級別設置為可重復讀:

set SESSION transaction ISOLATION LEVEL REPEATABLE READ;

如果你想確認當前的會話的事務隔離級別,那么可以使用以下命令去查詢:

SELECT @@transaction_isolation; (mysql8.0語法)
 SELECT @@tx_isolation; (mysql5.7語法)

還是針對我們的消息表t_user_message,在某些高并發場景下,如果使用可重復讀的話,尤其是事務場景中,出現死鎖的概率會加大。例如下邊這個場景:

事務1中,對消息表的可讀狀態進行修改,修改的是記錄表中的前3條數據,由于是可重復讀,以及非唯一索引user_id和object_id所以這里會鎖住的是(0,100011]這個區間的id記錄,也就是說只要我們更新的行是超過了100011 id的都沒問題。

圖片圖片

但是假設此時有個插入請求,打算往100009之前寫入一條記錄的話,就會出現間隙鎖堵塞的問題,例如下圖所示:

圖片圖片

產生間隙鎖的原因

1.使用了update,delete,selecct... for update相關操作

2.使用了可重復讀的隔離級別

3.在執行update/delete/select ... for update操作之后,在對應的間隙中插入了新的數據(注意是insert了新的數據才會有間隙鎖問題產生)。

MySQL中的死鎖檢測

在mysql5.7、mysql5.8等5系版本中

查看死鎖代碼是

select * from information_schema.innodb_locks;

查看等待鎖的代碼

select * from information_schema.innodb_lock_waits

但是要注意,在mysql 8.0中查看死鎖代碼變了,如果繼續用5.7的代碼會提示報錯

Unknown table ‘INNODB_LOCKS’ in information_schema

所以在8.0使用以下代碼

查看死鎖

select * from performance_schema.data_locks;

查看死鎖等待時間

select * from performance_schema.data_lock_waits;


責任編輯:武曉燕 來源: Idea的技術分享
相關推薦

2020-07-16 21:20:08

數據庫MySQL死鎖

2025-02-26 03:00:00

2025-02-28 00:03:22

高并發TPS系統

2025-02-26 08:10:40

2018-07-27 10:56:10

2025-06-05 01:22:00

SpringGateway高并發

2021-01-13 05:27:02

服務器性能高并發

2016-11-09 21:09:54

mysqlmysql優化

2025-05-26 02:11:00

2017-06-07 16:10:24

Mysql死鎖死鎖日志

2025-03-31 10:42:31

2024-08-29 09:32:36

2025-01-27 00:40:41

2023-10-07 08:54:28

項目httpPost對象

2017-05-03 16:26:24

MySQL并發死鎖

2018-05-04 15:15:37

數據庫MySQL并發場景

2019-07-05 17:40:24

MySQL并發數據庫

2025-02-14 03:00:00

2025-01-03 09:56:09

2023-08-16 11:39:19

高并發調優
點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 日韩视频在线一区 | 中文字幕在线一区 | 在线观看中文字幕 | 国产一区二区三区高清 | 激情一区二区三区 | 久久亚洲一区二区三区四区 | 欧美日韩国产精品一区 | 欧美精品一区免费 | 欧美一级黄色免费看 | av免费电影在线 | 国产精品永久免费观看 | 鲁一鲁资源影视 | 免费观看www7722午夜电影 | 国产精品一区2区 | 国产在线精品一区二区三区 | 激情五月婷婷综合 | 久久久成 | 亚洲精品电影在线观看 | 欧美日韩在线看 | 99精品99 | 99精品免费久久久久久久久日本 | 特级丰满少妇一级aaaa爱毛片 | 色一级| 国产精品视频久久 | 欧美日韩福利视频 | 久久久久久国模大尺度人体 | 日韩av一区二区在线观看 | 欧美激情综合色综合啪啪五月 | 亚洲精品久久久久中文字幕欢迎你 | 91麻豆精品国产91久久久久久 | 午夜一级做a爰片久久毛片 精品综合 | 久久久久免费精品国产小说色大师 | 中文字幕乱码一区二区三区 | 中文字幕在线观看国产 | 一本色道久久综合亚洲精品高清 | 一级a性色生活片久久毛片 午夜精品在线观看 | 午夜激情影院 | 免费一区 | 欧美高清成人 | 粉色午夜视频 | 色婷婷亚洲一区二区三区 |