近期有一個業務需求,多臺機器須要同時從Mysql一個表裏查詢數據並作後續業務邏輯,爲了防止多臺機器同時拿到同樣的數據,每臺機器須要在獲取時鎖住獲取數據的數據段,保證多臺機器不拿到相同的數據。mysql
咱們Mysql的存儲引擎是innodb,支持行鎖。解決同時拿數據的方法有不少,爲了更加簡單,不增長其餘表和服務的狀況下,咱們考慮採用select... for update的方式,這樣X鎖鎖住查詢的數據段,表裏其餘數據沒有鎖,其餘業務邏輯仍是能夠操做。sql
這樣一臺服務器好比select .. for update limit 0,30時,其餘服務器執行一樣sql語句會自動等待釋放鎖,等待前一臺服務器鎖釋放後,該臺服務器就能查詢下一個30條數據。若是要求更智能,oracle支持for update skip locked跳過鎖區域,這樣能不等待立刻查詢沒有被鎖住的下一個30條記錄。服務器
下面說下mysql for update致使的死鎖。oracle
通過分析,mysql的innodb存儲引擎實務鎖雖然是鎖行,但它內部是鎖索引的,根據where條件和select的值是否只有主鍵或非主鍵索引來判斷怎麼鎖,好比只有主鍵,則鎖主鍵索引,若是隻有非主鍵,則鎖非主鍵索引,若是主鍵非主鍵都有,則內部會按照順序鎖。但一樣的select .. for update語句怎麼就死鎖了呢?一樣的sql語句查詢條件和結果順序都一致,按理不會致使一個鎖了主鍵索引,等待鎖非主鍵索引,另一個鎖了非主鍵索引,等待主鍵索引致使的死鎖。測試
最後通過分析,咱們項目裏發現是for update的sql語句,和另一個update非select數據的sql語句致使的死鎖。blog
好比有60條數據,select .. for update查詢第31-60條數據,update在更新1-10條數據,按照innodb存儲引擎的行鎖原理,應該不會致使不一樣行的鎖致使的互相等待。開始覺得是行鎖在數據量較大狀況下,會鎖數據塊。致使一個段的數據被鎖住,但通過大量數據測試,發現感受把整個表都鎖住了,但實際不是。索引
下面舉幾個例子說明:ip
數據從id =400000的數據開始,IsSuccess和GetTime字段都爲0,如今若是400000數據的IsSuccess爲1了。執行下面兩條sql.get
-- 1: set autocommit=0; begin; select * from table1 where getTime < 1 and IsSuccess=0 order by id asc limit 0,30 for update; commit; -- 2: update table1 a set IsSuccess=0 where id =400000;
第一條sql語句先不commit,則第二條sql語句將只能等待,所以第二條sql語句把IsSuccess修改成0,IsSuccess非主鍵索引鎖了值爲0的索引數據,第二條sql語句將沒法把數據更新到被鎖的行裏。it
再執行下面的sql語句
-- 1: set autocommit=0; begin; select * from table1 where getTime < 1 and IsSuccess=0 order by id asc limit 0,30 for update; commit; -- 2: update table1 a set IsSuccess=2 where id =400000;
這樣第二條sql語句將能夠執行。由於IsSuccess=2的索引段沒有被鎖。
上面的例子知道了鎖索引段後還比較容易看懂,下面就奇葩一點:
先把id =400000數據的GetTime修改成1,IsSuccess=0,而後一次執行sql:
-- 1: set autocommit=0; begin; update ctripticketchangeresultdata a set issuccess=1 where id =400000; commit; -- 2: select * from table1 where getTime < 1 and IsSuccess=0 order by id asc limit 0,30 for update;
第1個sql先不commit,按照道理只會鎖40000這行記錄,第二個sql執行,按照道理只能查詢從400001記錄的30條記錄,但第二個sql語句會阻塞等待。
緣由是第一個sql語句尚未commit也沒有rollback,所以它先鎖主鍵索引,再鎖IsSuccess的非主鍵索引,第二個sql語句因爲where裏要判斷IsSuccess字段的值,因爲400000這條數據之前的IsSuccess是0,如今更新爲1還不肯定,可能會回滾,所以sql2須要等待肯定400000這條數據的IsSuccess是否被修改。sql2的sql語句由於判斷了GetTime<1,實際400000這條記錄已經不知足了,但按照鎖索引的原理,因此sql2語句會被阻塞。
所以若是根據業務場景,能夠把sql2語句的IsSuccess條件取消掉,而且這裏GetTime查詢條件由GetTime<1修改成GetTime=0,這樣便可不阻塞直接查詢出來。
GetTime用範圍查詢致使的鎖影響通過分析,還不是間隙鎖的問題,感受應該是用範圍做爲條件,全部從第0行開始的全部查找範圍都會被鎖住。 好比這裏更新400000會被阻塞,但更新400031不會被阻塞。
咱們項目出現死鎖,就是這個原理,一條sql語句先鎖主鍵索引,再鎖非主鍵索引;另一條sql語句先鎖非主鍵索引,再鎖主鍵索引。雖然兩個sql語句指望鎖的數據行不同,但兩個sql語句查詢或更新的條件或結果字段若是有相同列,則可能會致使互相等待對方鎖,2個sql語句即引發了死鎖。
我的總結一下innodb存儲引擎下的鎖的分析,可能會有問題:
一、更新或查詢for update的時候,會在where條件中開始爲每一個字段判斷是否有鎖,若是有鎖就會等待,由於若是有鎖,那這個字段的值不肯定,只能等待鎖commit或rollback後數據肯定後再查詢。
二、另外還和order by有關係,由於可能前面數據有鎖,但從後面查詢一個範圍就能夠查詢。
三、另外limit也有關係,好比limit 20,30從第20條記錄取30行數據,但第一行數據若是被鎖,由於不肯定回滾仍是提交,也會鎖等待。
所以從篩選查詢條件通過的地方都會判斷鎖,若是有鎖,由於數據不肯定,都會等待鎖釋放。本文是我的測試結果,沒有深刻分析內部原理,可能有不許確的地方。留做本身之後參考。