惱騷
最近在搞並發的問題,訂單的異步通知和主動查詢會存在並發的問題,用到了Mysql數據庫的 for update 鎖
在TP5直接通過lock(true),用於數據庫的鎖機制
Db::name('pay_order')->where('order_no',‘S1807081342018949’)->lock(true)->find();
打印生成的SQL語句
SELECT * FROM `pay_order` WHERE `order_no` = 'S1807081342018949' LIMIT 1 FOR UPDATE
上面的查詢語句中,我們使用了 select…for update 的方式,這樣就通過開啟排他鎖的方式實現了悲觀鎖。此時在 pay_order 表中,order_no 為 S1807081342018949 的那條數據就被我們鎖定了,其它的事務必須等本次事務提交之后才能執行。這樣我們可以保證當前的數據不會被其它事務修改。
上面我們提到,使用 select…for update 會把數據給鎖住,不過我們需要注意一些鎖的級別,MySQL InnoDB默認行級鎖。行級鎖都是基於索引的,如果一條SQL語句用不到索引是不會使用行級鎖的,會使用表級鎖把整張表鎖住,這點需要注意。
理解悲觀鎖與樂觀鎖
在數據庫的鎖機制中介紹過,數據庫管理系統(DBMS)中的並發控制的任務是確保在多個事務同時存取數據庫中同一數據時不破壞事務的隔離性和一致性以及數據庫的一致性。
樂觀並發控制(樂觀鎖)和悲觀並發控制(悲觀鎖)是並發控制主要采用的技術手段。無論是悲觀鎖還是樂觀鎖,都是人們定義出來的概念,可以認為是一種思想。其實不僅僅是數據庫系統中有樂觀鎖和悲觀鎖的概念,像memcache、hibernate、tair等都有類似的概念。
針對於不同的業務場景,應該選用不同的並發控制方式。所以,不要把樂觀並發控制和悲觀並發控制狹義的理解為DBMS中的概念,更不要把他們和數據中提供的鎖機制(行鎖、表鎖、排他鎖、共享鎖)混為一談。其實,在DBMS中,悲觀鎖正是利用數據庫本身提供的鎖機制來實現的。
在數據庫中,悲觀鎖的流程如下:
在對任意記錄進行修改前,先嘗試為該記錄加上排他鎖(exclusive locking)。
如果加鎖失敗,說明該記錄正在被修改,那么當前查詢可能要等待或者拋出異常。 具體響應方式由開發者根據實際需要決定。
如果成功加鎖,那么就可以對記錄做修改,事務完成后就會解鎖了。
其間如果有其他對該記錄做修改或加排他鎖的操作,都會等待我們解鎖或直接拋出異常。
以下這句話應用來自:http://www.cnblogs.com/bigfish--/archive/2012/02/18/2356886.html
在oracle中,利用 select * for update 可以鎖表。假設有個表單products ,里面有id跟name二個欄位,id是主鍵。
例1: (明確指定主鍵,並且有此筆資料,row lock)
SELECT * FROM products WHERE id='3' FOR UPDATE;
例2: (明確指定主鍵,若查無此筆資料,無lock)
SELECT * FROM products WHERE id='-1' FOR UPDATE;
例3: (無主鍵,table lock)
SELECT * FROM products WHERE name='Mouse' FOR UPDATE;
例4: (主鍵不明確,table lock)
SELECT * FROM products WHERE id<>'3' FOR UPDATE;
例5: (主鍵不明確,table lock)
SELECT * FROM products WHERE id LIKE '3' FOR UPDATE;
注1: FOR UPDATE僅適用於InnoDB,且必須在交易區塊(BEGIN/COMMIT)中才能生效。
注2: 要測試鎖定的狀況,可以利用MySQL的Command Mode ,開二個視窗來做測試。(點開鏈接,這里已經有人做個測試了)
先開始一把
使用悲觀鎖的原理就是,當我們在查詢出 pay_order 信息后就把當前的數據鎖定,直到我們修改完畢后再解鎖。那么在這個過程中,因為 pay_order 被鎖定了,就不會出現其他操作者來對其進行修改了。
第一次,開啟事務,但是不提交事務
異步通知
-- 開啟事務
START TRANSACTION;
-- 查詢訂單
SELECT id,order_no,`status` FROM `pay_order` WHERE `order_no` = 'S1807081342018949' LIMIT 1 FOR UPDATE;
-- 修改訂單
UPDATE `pay_order` SET `status` = 11 WHERE id = 347;
COMMIT;
-- 查詢數據是否修改成功
SELECT id,order_no,`status` FROM `pay_order` WHERE `order_no` = 'S1807081342018949' LIMIT 1 FOR UPDATE;
執行結果:很快就執行完畢了,但是數據並沒有修改成功(注意:但是重復執行一次,則數據又修改成功了)
主動查詢
1、加鎖
SELECT id,order_no,`status` FROM `pay_order` WHERE `order_no` = 'S1807081342018949' LIMIT 1 FOR UPDATE;
執行結果,一直在阻塞中
過一會,會自動取消鎖機制
[Err] 1205 - Lock wait timeout exceeded; try restarting transaction
2、不加鎖
SELECT id,order_no,`status` FROM `pay_order` WHERE `order_no` = 'S1807081342018949';
執行結果,沒有阻塞,則能正常查詢出數據,不會受第一個事務的影響
第二次,開啟事務,提交事務
異步查詢開啟事務,提交事務
主動查詢加鎖則不受影響
總結:鎖如果是回滾或者提交事務,會自動釋放掉鎖的。
下面研究以下行鎖和表鎖
例1: 明確指定主鍵,並且有此數據,row lock
說明:通過上面的演示,可以清楚的看到,鎖的是同一個記錄(id = 347),記錄(id = 348)並沒有受到上一條記錄的影響。
例2: 明確指定主鍵,若查無此數據,無lock
說明:窗口1 查詢結果為空。窗口2 查詢結果也為空,查詢無阻塞,說明 窗口1 沒有對數據執行鎖定。
例3:無主鍵,table lock
說明:
窗口1 開啟了事務,查詢訂單號 : order_no = "S1807081342018949",查詢數據正常。
窗口2 也開啟了事務,查詢訂單號 : order_no = "S1807081342018949",查詢阻塞,說明 窗口1 把該記錄給鎖住了(其實這里表已經被鎖定, 而不是該記錄了)。
窗口3 開啟了事務,查詢訂單號 : order_no = "S1807171712053133",查詢阻塞,說明 窗口1 把該表給鎖住了,不是同一條記錄都不給查啊,阻塞的不要不要的。
只有 窗口1 的記錄回滾或者提交了,窗口2 的查詢阻塞立刻釋放掉了,但是 窗口3 依然在阻塞中(由於 窗口2 開啟了事務導致的)。同理,回滾或者提交 窗口2 的事務后,窗口3 的記錄也可以正常查詢了。
例4: 主鍵不明確,table lock
說明:
窗口1 開啟了事務,查詢主鍵 : id > 375 的記錄,查詢數據正常(3條記錄)。
窗口2 也開啟了事務,查詢訂單號 : id > 375 的記錄,查詢阻塞,說明 窗口1 把該記錄給鎖住了(其實這里表已經被鎖定, 而不是該記錄了)。
窗口3 開啟了事務,查詢訂單號 : id > 376 的記錄,查詢阻塞,說明 窗口1 把該表給鎖住了,不是同一條記錄都不給查啊,阻塞的不要不要的。
只有 窗口1 的記錄回滾或者提交了,窗口2 的查詢阻塞立刻釋放掉了,但是 窗口3 依然在阻塞中(由於 窗口2 開啟了事務導致的)。同理,回滾或者提交 窗口2 的事務后,窗口3 的記錄也可以正常查詢了。
例5: 主鍵不明確,table lock
select * from pay_order where id<>1 for update;
索引對數據庫的鎖定級別
例6: 明確指定索引,並且有此數據,row lock
mysql> select id,status,order_no from pay_order where status=1 for update;
+------+----------+-------------------+
| id | status | order_no |
|------+----------+-------------------|
| 348 | 1 | S1807081353042055 |
| 349 | 1 | S1807081356043257 |
+------+----------+-------------------+
13 rows in set
Time: 0.003s
注意:上面的字段 status 是建立過索引的
例7: 明確指定索引,若查無此數據,無lock
mysql> select id,status,order_no from pay_order where status=11 for update;
+------+----------+------------+
| id | status | order_no |
|------+----------+------------|
+------+----------+------------+
0 rows in set
Time: 0.001s
演示操作
1、定義索引字段開啟事務加悲觀鎖
pay_order 表結構,order_no 定義索引。
CREATE TABLE `pay_order` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '#', `order_no` varchar(255) NOT NULL COMMENT '訂單號', PRIMARY KEY (`id`) USING BTREE, UNIQUE KEY `idx_order_no` (`order_no`) USING BTREE ) ENGINE=InnoDB AUTO_INCREMENT=514 DEFAULT CHARSET=utf8mb4 COMMENT='支付訂單';
相同訂單號第一次查詢
mysql@:Tinywan> START TRANSACTION; Query OK, 0 rows affected Time: 0.000s mysql@:Tinywan> SELECT id,order_no,`status` FROM `pay_order` WHERE order_no = 'S64064191 -> 1161202555241' LIMIT 1 FOR UPDATE; +------+------------------------+----------+ | id | order_no | status | |------+------------------------+----------| | 11 | S640641911161202555241 | 1 | +------+------------------------+----------+ 1 row in set Time: 0.001s mysql@:Tinywan>
相同訂單號第二次查詢
mysql@:Tinywan> START TRANSACTION; Query OK, 0 rows affected Time: 0.001s mysql@:Tinywan> SELECT id,order_no,`status` FROM `pay_order` WHERE order_no = 'S640641911161 -> 202555241' LIMIT 1 FOR UPDATE;
被阻塞掉了
不同訂單號第一次查詢
mysql@:Tinywan> START TRANSACTION; Query OK, 0 rows affected Time: 0.000s mysql@:Tinywan> SELECT id,order_no,`status` FROM `pay_order` WHERE -> order_no = 'T705961911161238428844' LIMIT 1 FOR UP -> DATE; +------+------------------------+----------+ | id | order_no | status | |------+------------------------+----------| | 25 | T705961911161238428844 | 0 | +------+------------------------+----------+ 1 row in set Time: 0.002s mysql@:Tinywan>
2、未定義索引
表結構
CREATE TABLE `pay_order` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '#', `order_no` varchar(255) NOT NULL COMMENT '訂單號', PRIMARY KEY (`id`) USING BTREE ) ENGINE=InnoDB AUTO_INCREMENT=514 DEFAULT CHARSET=utf8mb4 COMMENT='支付訂單';
相同訂單號第一次查詢
相同訂單號第二次查詢
不同訂單號第一次查詢
第一個提交事務
第二個提交事務
參考: