使用單個鍵插入單個表時出現死鎖。(Mysql)
我有一個簡單的遊戲伺服器。我收到的一項要求是一次處理單個玩家的單個請求。所以為了實現這一點,我決定創建一個鎖表:
CREATE TABLE `GameRoundLock` (`theKey` varchar(255) NOT NULL, PRIMARY KEY (`theKey`)) ENGINE=InnoDB DEFAULT CHARSET=utf8;
在我處理來自任何玩家的任何請求之前,我將在該表中插入一行,如下所示:
// save and flush == acquiring the lock GameRoundLock lock = this.gameRoundLockRepository.saveAndFlush(somePlayerKey); /** Play busniess logic here... **/ // deleting == releasing the lock this.gameRoundLockRepository.delete(lock);
這實際上有效。
在以下場景中,整個系統按預期工作:
1) PlayerA 播放,因此插入了一個新鎖。
2)處理 playerA 請求…(需要 30 秒)
3) 當 2) 仍在執行時,PlayerA 再次播放 - 這當然會導致嘗試為他創建鎖,並且執行緒現在等待直到 2) 完成。
4) 2) 執行緒完成
5) 3) 的執行緒現在繼續
6) 完成,playerA 總共玩了幾輪 == 2。
注意id(playerKey)是一樣的。
當 playerA 嘗試另一個播放請求而第一個執行緒的處理仍然發生時,就會出現問題。這是導致奇怪死鎖的場景:
1) PlayerA 播放,因此插入了一個新鎖。
2)處理 playerA 請求…(需要 30 秒)
3) 當 2) 仍在執行時,PlayerA 再次播放 - 這當然會導致嘗試為他創建鎖,並且執行緒現在等待直到 2) 完成。
4) 當 2) 仍在執行和 3) 仍在等待時,PlayerA 再次播放 - 這當然導致嘗試為他創建鎖,並且執行緒現在等待直到 2)/3) 完成。
5) 2) 執行緒完成
6) 錯誤 - 引發死鎖!!!!!!
這是死鎖資訊:
------------------------ LATEST DETECTED DEADLOCK ------------------------ 2016-11-23 17:05:26 0x1d98 *** (1) TRANSACTION: TRANSACTION 5493356, ACTIVE 8 sec inserting mysql tables in use 1, locked 1 LOCK WAIT 3 lock struct(s), heap size 1136, 2 row lock(s) MySQL thread id 5191, OS thread handle 5152, query id 15582091 localhost 127.0.0.1 root update insert into GameRoundLock (theKey) values ('1,1,1,64') *** (1) WAITING FOR THIS LOCK TO BE GRANTED: RECORD LOCKS space id 4865 page no 3 n bits 72 index PRIMARY of table `game_server`.`gameroundlock` trx id 5493356 lock_mode X locks rec but not gap waiting Record lock, heap no 2 PHYSICAL RECORD: n_fields 3; compact format; info bits 32 0: len 8; hex 312c312c312c3634; asc 1,1,1,64;; 1: len 6; hex 00000053d266; asc S f;; 2: len 7; hex 790000030903d1; asc y ;; *** (2) TRANSACTION: TRANSACTION 5493355, ACTIVE 12 sec inserting, thread declared inside InnoDB 1 mysql tables in use 1, locked 1 3 lock struct(s), heap size 1136, 2 row lock(s) MySQL thread id 5193, OS thread handle 7576, query id 15582083 localhost 127.0.0.1 root update insert into GameRoundLock (theKey) values ('1,1,1,64') *** (2) HOLDS THE LOCK(S): RECORD LOCKS space id 4865 page no 3 n bits 72 index PRIMARY of table `game_server`.`gameroundlock` trx id 5493355 lock mode S Record lock, heap no 2 PHYSICAL RECORD: n_fields 3; compact format; info bits 32 0: len 8; hex 312c312c312c3634; asc 1,1,1,64;; 1: len 6; hex 00000053d266; asc S f;; 2: len 7; hex 790000030903d1; asc y ;; *** (2) WAITING FOR THIS LOCK TO BE GRANTED: RECORD LOCKS space id 4865 page no 3 n bits 72 index PRIMARY of table `game_server`.`gameroundlock` trx id 5493355 lock_mode X locks rec but not gap waiting Record lock, heap no 2 PHYSICAL RECORD: n_fields 3; compact format; info bits 32 0: len 8; hex 312c312c312c3634; asc 1,1,1,64;; 1: len 6; hex 00000053d266; asc S f;; 2: len 7; hex 790000030903d1; asc y ;; *** WE ROLL BACK TRANSACTION (2)
似乎對於第二個和第三個播放請求的鎖的兩個“close\near”插入會導致這種情況。為什麼會出現死鎖以及如何緩解它?
我猜你有以下問題。
如果發生重複鍵錯誤,則會在重複索引記錄上設置共享鎖。如果另一個會話已經擁有排他鎖,那麼如果有多個會話嘗試插入同一行,則使用共享鎖可能會導致死鎖。如果另一個會話刪除了該行,就會發生這種情況。 http://dev.mysql.com/doc/refman/5.6/en/innodb-locks-set.html
使用鎖是錯誤的方法,因為這會為每個等待的玩家在伺服器上綁定一個執行緒。如果有很多玩家,您的伺服器將很快耗盡執行緒。它也是 DOS 攻擊的載體,因為該系統讓黑客可以輕鬆地在您的伺服器上創建足夠的執行緒,所有執行緒都在等待鎖定,以關閉服務。
一個更好的方法是:
- 插入行並立即送出事務,釋放 myslq 連接
- 當玩家完成他的回合時,刪除該行並送出
- 如果在前面的步驟之間,來自同一播放器的另一個客戶端嘗試插入同一行,則回复“稍後再試”,如果可能的話,給出等待延遲的估計值
這將等待執行緒從伺服器移動到客戶端,這是可擴展的。