當前位置:
首頁 > 最新 > 資金系統中資料庫鎖的應用實踐

資金系統中資料庫鎖的應用實踐

最近因為工作原因,開發了一個資金系統。在設計的過程中,因為充分考慮到了公司的長遠發展和我們自身對性能的要求,所以開發之初,就對資料庫進行了水平拆分,這樣就導致不同的用戶,通常都是位於不同的資料庫中的,在資金操作時,就會涉及到分散式事務。

熟悉CAP分散式理論的同學都知道,我們在解決這類問題的時候,對於可用性和分區容錯性是必須要保留的,通常都是放棄強一致性,通過最終一致性解決此類分散式事務問題。

分散式事務較為複雜,在實際的開發過程中也有不少需要注意的點,以後會再起篇幅進行介紹。

資金系統因為要保證資金安全,所以對數據的一致性要求非常高,所以在開發過程中需要用到資料庫鎖,接下去博主會對資料庫鎖進行詳細的介紹,此文章中將會主要介紹兩塊內容:

1、MYSQL中的鎖和它們的應用場景

2、死鎖、鎖阻塞的排查方式

接下去的講解過程,會一步步結合實踐,所以我們需要先創建一張資料庫表,並插入幾條測試數據:

CREATE TABLE `my_test` (

`id` bigint(20) DEFAULT NULL,

`name` varchar(50) CHARACTER SET latin1 DEFAULT NULL,

`amount` int(5) DEFAULT NULL,

`version` bigint(20) DEFAULT NULL,

UNIQUE KEY `id` (`id`)

) ENGINE=InnoDB DEFAULT CHARSET=utf8;

INSERT INTO `my_test` VALUES ("1", "zhangsan", "10", "1");

INSERT INTO `my_test` VALUES ("2", "lisi", "15", "1");

INSERT INTO `my_test` VALUES ("8", "wangwu", "16", "1");

INSERT INTO `my_test` VALUES ("26", "xiaoming", "18", "1");

INSERT INTO `my_test` VALUES ("28", "xiaohong", "19", "1");

樂觀鎖

樂觀鎖是基於一種「樂觀」的思想,假設資料庫操作的並發非常少,多數情況下是不會發生並發的,更新是按照順序執行的,通過版本號控制來防止臟數據的產生。

具體過程為,在操作資料庫數據的時候,對數據不加顯式的鎖,而是通過資料庫的版本號(version)或者時間戳的對比來保證的一致性。一般在操作數據之前,需要先獲取到這條記錄的版本號,在更新數據的時候,通過WHERE條件對比記錄的版本號,如果版本號一致,則繼續更新,否則停止更新。

代碼如下:

SELECT * FROM my_test WHERE id=1

UPDATE my_test SET age=11 WHERE id=1 AND version=1

樂觀鎖在同一時刻,只有一個更新請求能夠成功,其他的都會失敗。

關於樂觀鎖的使用,阿里巴巴Java開發手冊裡面有下面這麼一句話:

如果每次訪問衝突概率小於 20%,推薦使用樂觀鎖,否則使用悲觀鎖。樂觀鎖的重試次數不得小於3次。

從上面這句話中,我們可以得到如下幾個信息:

1、樂觀鎖適用於並發不高的場景,防止並發修改一份數據。

2、使用樂觀鎖在失敗的時候會再嘗試多次更新,這會導致並發量始終上不去,是一個反模式。

3、這種模式是應用層實現的,阻止不了其他程序對資料庫數據的直接更新。

悲觀鎖

悲觀鎖與樂觀鎖相反,基於一種具有「悲觀」的思想,假設資料庫操作的並發很多,多數情況下是有並發的,在更新數據之前對數據上鎖,更新過程中防止任何其他的請求更新數據而產生臟數據,更新完成之後,再釋放鎖,這裡的鎖是資料庫級別的鎖。

代碼如下:

SELECT * FROM my_test WHERE id=1 FOR UPDATE

如果查詢的數據不為空,則可以繼續進行接下去的業務邏輯。

有一點需要注意的是,WHERE後面的條件,一定要是主鍵或者是唯一性索引,原因是,只有通過索引條件檢索數據,InnoDB才使用行級鎖,否則,InnoDB將使用表鎖。

驗證測試過程如下:

session1:

set session autocommit=0;

SELECT * FROM my_test WHERE name="zhangsan" FOR UPDATE;

session2:

set session autocommit=0;

SELECT * FROM my_test WHERE name="lisi" FOR UPDATE;

會發現session2執行的時候,會造成阻塞,因為name不是主鍵,也不是唯一性索引,所以當session1執行的時候會使用表鎖,因為表鎖的緣故,session2執行的時候,會造成阻塞。

悲觀鎖是資料庫引擎層面實現的,因此能阻止所有的並發操作。但是為了更新一條數據,需要先把這條數據上鎖,直到業務處理完成,事務提交,別的請求才能進來,所以悲觀鎖性能較為低下。

但是在資金領域,由於它能保證數據更新操作的強一致性,是最安全的處理方式,所以在很多資金系統中,依然使用這種方式,雖然降低了性能,但是規避了資金風險。

記錄鎖(行鎖)

資料庫引擎本身有行級別的鎖,在更新一條數據的時候本身就會對記錄上鎖,這個時候即使有多個請求,也不會產生臟數據,我們可以利用這個行級別的鎖,控制鎖的時間窗口最小化,一次來保證高並發的場景下更新數據的有效性。

例如我們可以採用如下方式扣減金額:

UPDATE my_test SET amount=amount-1 WHERE id=1 AND amount>1

在實際使用場景中,我們可以對記錄表的狀態轉換、對賬戶或者訂單的狀態控制,都可以使用這種方式。

死鎖

在詳細講解死鎖之前,我們先通過一個演示來說明死鎖,然後通過這個場景說明我們日常開發中可能常出現的問題。

session1:

SET autocommit=0;

DELETE FROM my_test WHERE id=29;

session2:

SET autocommit=0;

DELETE FROM my_test WHERE id=30;

session1:

INSERT INTO my_test VALUES(29, "zhangsan", 10, 1);

session2:

INSERT INTO my_test VALUES(30, "zhangsan", 10, 1);

順序執行以上代碼,會產生如下情況:

1、當我們執行 session1 中 DELETE FROM my_test WHERE id=29 語句的時候,會發生阻塞。

2、當我們執行到 session2 中 INSERT INTO my_test VALUES(30, "zhangsan", 10, 1) 這條sql語句的會出現死鎖。

我們來分析為什麼會產生阻塞和死鎖。

當session1和session2執行DELETE的語句的時候,在嘗試刪除一條不存在的數據,都會產生一個從29到正無窮的區間鎖,區間鎖是一種索引記錄之間或者某索引記錄之前或者某索引記錄之後的鎖。UPDATE和DELETE時,除了對唯一索引的唯一搜索外都會獲取gap區間鎖或next-key鎖,即鎖住其掃描的範圍。

我們將上面的sql語句修改一下:

session1:

SET autocommit=0;

DELETE FROM my_test WHERE id=27;

session2:

SET autocommit=0;

DELETE FROM my_test WHERE id=30;

session1:

INSERT INTO my_test VALUES(27, "zhangsan", 10, 1);

session2:

INSERT INTO my_test VALUES(30, "zhangsan", 10, 1);

順序執行以上代碼,發現順利插入進入了,沒有發生阻塞和死鎖。原因是session1鎖住了27這個數據,而session2鎖住了從30到正無窮的數據,他們互相併沒有鎖住對方的數據,所以也不會產生死鎖。

為了驗證上面的一句話 「UPDATE和DELETE時,除了對唯一索引的唯一搜索外都會獲取gap區間鎖或next-key鎖,即鎖住其掃描的範圍」 我們繼續做如下驗證:

將 my_test 表中的id從唯一索引改為普通索引,順序執行如下sql:

session1:

SET autocommit=0;

DELETE FROM my_test WHERE id=8;

session2:

SET autocommit=0;

INSERT INTO my_test VALUES(11, "zhangsan", 10, 1);

INSERT INTO my_test VALUES(100, "zhangsan", 10, 1);

順序執行以上sql,會發現如下情況:

1、session2 中 INSERT INTO my_test VALUES(11, "zhangsan", 10, 1) 這條sql語句發生了阻塞

2、session2 中 INSERT INTO my_test VALUES(100, "zhangsan", 10, 1) 這條sql語句沒有發生阻塞,順利插入

原因是session1的DELETE鎖住了從1到28之間的數據,在這個區間內,任何數據都不能插入,而28以上的數據是沒有鎖住的,所以session2能順利插入數據。

如何發現死鎖?

我們基於上面的測試,執行 SHOW ENGINE INNODB STATUS 可以看到類似於如下信息,博主已將關鍵信息,通過注釋的方式標記出來了

------------------------

LATEST DETECTED DEADLOCK

------------------------

# 死鎖發生的時間

2018-06-19 20:49:54 0x7f526468d700

# 發生死鎖時的第一個事務

*** (1) TRANSACTION:

mysql tables in use 1, locked 1

LOCK WAIT 3 lock struct(s), heap size 1136, 2 row lock(s), undo log entries 1

# 具體執行的SQL語句

INSERT INTO my_test VALUES(29, "zhangsan", 10, 1)

# 第一個事務正在等待的鎖

*** (1) WAITING FOR THIS LOCK TO BE GRANTED:

RECORD LOCKS space id 2793 page no 4 n bits 80 index id of table `tcc_red`.`my_test` trx id 144836650 lock_mode X locks gap before rec insert intention waiting

Record lock, heap no 7 PHYSICAL RECORD: n_fields 2; compact format; info bits 0

# 發生死鎖時的第二個事務

*** (2) TRANSACTION:

mysql tables in use 1, locked 1

3 lock struct(s), heap size 1136, 2 row lock(s), undo log entries 1

INSERT INTO my_test VALUES(30, "zhangsan", 10, 1)

# 第二個事務獲得的鎖

*** (2) HOLDS THE LOCK(S):

RECORD LOCKS space id 2793 page no 4 n bits 80 index id of table `tcc_red`.`my_test` trx id 144836651 lock_mode X locks gap before rec

Record lock, heap no 7 PHYSICAL RECORD: n_fields 2; compact format; info bits 0

# 第二個事務正在等待的鎖

*** (2) WAITING FOR THIS LOCK TO BE GRANTED:

RECORD LOCKS space id 2793 page no 4 n bits 80 index id of table `tcc_red`.`my_test` trx id 144836651 lock_mode X locks gap before rec insert intention waiting

Record lock, heap no 7 PHYSICAL RECORD: n_fields 2; compact format; info bits 0

# 顯示的mysql選擇了哪一個事務回滾,避免無期限的死鎖等待,innodb_lock_wait_timeout參數可配置死鎖的等待超時時間

*** WE ROLL BACK TRANSACTION (2)

如何發現阻塞?

繼續以上面的例子為演示。

當發生阻塞的情況下,我們可以通過如下sql查詢正在阻塞的sql:

SELECT

b.trx_mysql_thread_id AS "blocked_thread_id",

b.trx_query AS "blocked_sql_text",

c.trx_mysql_thread_id AS "blocker_thread_id",

c.trx_query AS "blocker_sql_text",

(

Unix_timestamp() - Unix_timestamp(c.trx_started)

) AS "blocked_time"

FROM

information_schema.innodb_lock_waits a

INNER JOIN information_schema.innodb_trx b ON a.requesting_trx_id = b.trx_id

INNER JOIN information_schema.innodb_trx c ON a.blocking_trx_id = c.trx_id

WHERE

(

Unix_timestamp() - Unix_timestamp(c.trx_started)

查詢結果如下:

通過如下sql查詢阻塞的源sql:

將上面的查詢結果226作為查詢條件,執行如下sql:

SELECT

a.sql_text,

c.id,

d.trx_started

FROM

PERFORMANCE_SCHEMA.events_statements_current a

JOIN PERFORMANCE_SCHEMA.threads b ON a.thread_id = b.thread_id

JOIN information_schema.PROCESSLIST c ON b.processlist_id = c.id

JOIN information_schema.innodb_trx d ON c.id = d.trx_mysql_thread_id

WHERE

c.id=226

ORDER BY

d.trx_started;

查詢結果如下:

需要注意的是,查詢阻塞的源sql可能是不準確的,因為在正常的程序執行過程中,執行完上面的DELETE語句後,很可能立刻就會執行程序中接下去的sql,並不像我們演示的過程這樣,故意停在這裡,上面的查詢結果是,阻塞源事務中執行的最後一條sql。我們在分析源sql時,應該結合實際代碼,分析上下文來找到真正阻塞的源sql


喜歡這篇文章嗎?立刻分享出去讓更多人知道吧!

本站內容充實豐富,博大精深,小編精選每日熱門資訊,隨時更新,點擊「搶先收到最新資訊」瀏覽吧!


請您繼續閱讀更多來自 hi很高興見到你 的精彩文章:

TAG:hi很高興見到你 |