? ? ? ? 上一篇文章我們介紹了什么是分布式鎖和分布式鎖的一些基本概念。這篇文章我們來講解一下基于數據庫如何實現分布式鎖。
基于數據庫實現分布式鎖
? ? ? ? 基于數據庫實現分布式鎖可以分為兩種方式,分別是基于數據庫表和基于數據庫排他鎖。
基于數據庫表
? ? ? ? 要實現分布式鎖,最簡單的方式可能就是直接創建一張鎖表,然后通過操作該表中的數據來實現。當我們要鎖住某個方法或者資源時,我們就在這張表中增加一條記錄,想要釋放鎖的時候就刪除這條記錄。
? ? ? ? 當我們想要鎖住某個方法時,執行以下SQL語句:
insert into methodLock(method_name,desc) values ('method_name','desc');
? ? ? ? 因為我們對method_name做了唯一性約束,這里如果有多個請求同時提交到數據庫的話,數據庫會保證只有一個操作可以成功,那么我們就可以認為操作成功的那個線程獲得了該方法的鎖,可以執行方法的具體內容。
? ? ? ? 當方法執行完成后,想要釋放鎖時,可以執行以下SQL語句:
delete from methodLock where method_name = 'method_name';
? ? ? ? 但是上面這種簡單的實現存在以下四個問題:
? ? ? ? (1)這把鎖強依賴數據庫的可用性,數據庫是一個單點,一旦數據庫掛掉,會導致業務系統不可用;
? ? ? ? (2)這把鎖沒有失效時間,一旦解鎖操作失敗,就會導致鎖記錄一直在數據庫中,其他線程無法再獲得到鎖;
? ? ? ? (3)這把鎖只能是非阻塞的,因為數據的insert操作,一旦插入失敗就會直接報錯。沒有獲得鎖的線程并不會進入排隊隊列,要想再次獲得鎖就要再次觸發獲得鎖的動作;
? ? ? ? (4)這把鎖是非重入的,同一個線程在沒有釋放鎖之前無法再次獲得該鎖。因為數據中數據已經存在了。
? ? ? ? 針對以上的問題,我們可以通過以下方式來進行解決:
? ? ? ? (1)可以使用數據庫集群來代替單點數據庫;
? ? ? ? (2)可以定義一個定時任務,每隔一定時間把數據庫中的超時數據清理一遍來解決鎖沒有超時時間的問題;
? ? ? ? (3)可以使用while循環進行不斷的insert嘗試,直到成功,解決鎖是非阻塞的問題;
? ? ? ? (4)可以在數據庫中加入一個字段,記錄當前獲得鎖的機器的主機信息和線程信息,那么下次再獲取鎖時先查詢數據庫,看這個字段記錄的是否是當前獲取鎖的主機信息和線程信息,如果是,那就直接進行鎖的分配即可。
基于數據庫排他鎖
? ? ? ? 除了可以通過增刪操作數據表中的記錄以外,還可以借助數據庫自帶的鎖來實現分布式鎖。
? ? ? ? 基于MySQL的InnoDB引擎,可以使用以下方式來進行加鎖操作:
public boolean lock(){connection.setAutoCommit(false);while(true){try{result = select * from methodLock where method_name = ... for update;if(result == null){return true;}}catch(Exception e){}sleep(1000);}return false;
}
? ? ? ? 在查詢語句后面增加for update,數據庫會在查詢過程中給數據庫表增加排他鎖(InnoDB引擎在加鎖的時候,只有通過索引進行檢索的時候才會使用行鎖,否則會使用表鎖。這里我們希望使用行鎖,就要給method_name添加索引。這個索引一定要創建成唯一索引,否則會出現多個重載方法之間無法同時被訪問的問題。重載方法的話建議把參數類型也加上)。當某條記錄被加上排他鎖之后,其他線程無法再在該行記錄上增加排他鎖。
? ? ? ? 我們可以認為獲得排他鎖的線程即可獲得分布式鎖,當獲取到鎖之后,可以執行方法的業務邏輯,執行完方法之后,再通過以下方式進行解鎖:
public void unlock(){connection.commit();
}
? ? ? ? 通過connection.commit()操作來釋放鎖,可以有效的解決上面提到的無法釋放鎖和非阻塞鎖問題。for update語句會在執行成功之后立即返回,在執行失敗時會一直處于阻塞狀態,直到成功;使用這種方式,出現意外時數據庫會自動將鎖釋放掉。但是這種方法還是無法直接解決掉數據庫單點和鎖不可重入問題。
? ? ? ?使用這種方法還會帶來新的問題,雖然我們對method_name使用了唯一索引,并且顯示使用for update來使用行鎖。但是,MySQL會對查詢進行優化,即使在條件中使用了索引字段,但是否使用索引來進行檢索還是由MySQL通過判斷不同執行計劃的代價來決定。如果最終MySQL走了表鎖而沒有走行鎖,那么這種方式就失效了;另一個問題是,如果我們使用排他鎖來實現分布式鎖,那么一個排他鎖長時間不提交,就會占用數據庫連接。一旦類似的連接變得多了,就可能把數據庫連接池撐爆。
總結
? ? ? ? 依賴數據庫實現分布式鎖有兩種方法,一種是基于數據庫表,一種是基于數據庫的排他鎖。直接借助數據庫實現分布式鎖容易理解,但是其實現上會有各種各樣的問題,解決這些問題會使得方案變得復雜,并且操作數據庫會帶來一定的性能開銷,行鎖的使用也不想我們認為的那樣可靠,尤其是表比較小的時候很可能不走索引導致使用表鎖。
? ? ? ? 這篇文章我們講解了如何基于數據庫實現分布式鎖,大家有什么問題或者勘誤可以在評論區留言,筆者看到都會回復的。