大家好,今天我們來看下如何使用本地MySql實現一把分散式鎖,以及Mysql實現分散式鎖的原理是怎麼樣的
MySql實現分散式鎖有三種方式
1:基於行鎖實現分散式鎖
實現原理
首先我們的表lock要提前存好相對應的lockName,這時候多個客戶端來執行
select lock_name from lock where lock_name = #{lockName} for update
由於第一個客戶端來執行這條sql語句,給這行記錄加了行鎖,在這個客戶端沒有提交事務之前,其它客戶端就會被阻塞住。所以這時候就只能有一個客戶端去執行我們自己的業務了,其它客戶端就只能阻塞等待,那麼這個過程就是加鎖
那麼釋放鎖該怎麼操作呢?
其實釋放鎖就很簡單了,也就是將獲取到鎖的這個客戶端的事務提交,這樣其它客戶端就可以來獲取到這把行鎖了,所以這時候就需要我們手動的提交事務了
程式碼實現
首先就是編寫我們的加鎖SQL語句了
@Select("select lock_name from lock where lock_name = #{lockName} for update") List<String> queryLockNameForUpdate(@Param("lockName") String lockName);
然後我們需要實現我們的加鎖 和 解鎖
public class MySqlDistributeLock { //加鎖的KEY,也就是我們提前存到表lock的值 private String lockName; //手動提交事務需要的事務管理器,由外部傳入 private DataSourceTransactionManager dataSourceTransactionManager; //自定義編寫的mybatis的mapper檔案 private MySqlLockMapper mySqlLockMapper; private TransactionStatus status; public MySqlDistributeLock(String lockName,DataSourceTransactionManager dataSourceTransactionManager,MySqlLockMapper mySqlLockMapper) { this.lockName = lockName; this.dataSourceTransactionManager = dataSourceTransactionManager; this.mySqlLockMapper = mySqlLockMapper; } public void lock() { TransactionDefinition transactionDefinition = new DefaultTransactionDefinition(); status = dataSourceTransactionManager.getTransaction(transactionDefinition); while (true) { try{ mySqlLockMapper.queryLockNameForUpdate(this.lockName); //如果加鎖成功,就退出該迴圈 break; }catch (Exception e) { //說明丟擲異常了,讓執行緒重試 try { //讓執行緒休眠一會 Thread.sleep(100); } catch (InterruptedException ignored) { } } } } public void unLock() { //手動提交事務,也就是釋放鎖 dataSourceTransactionManager.commit(this.status); } }
最後看下業務方如何使用
@Service public class LockService { @Resource private DataSourceTransactionManager dataSourceTransactionManager; @Resource private MySqlDistributeLock.MySqlLockMapper mySqlLockMapper; public String deductStockMysqlLock(String productId,Integer count) { MySqlDistributeLock lock = null; try{ lock = new MySqlDistributeLock(productId,dataSourceTransactionManager,mySqlLockMapper); //加鎖 lock.lock(); //加鎖成功,開始執行我們自己的業務邏輯 }finally { if(lock != null) { lock.unLock(); } } return "success"; } }
2:基於唯一索引實現分散式鎖
實現原理
首先我們的lock表要給lock_name欄位建立一個唯一索引,這時候有多個客戶端來加鎖,本質上也就是新增一條記錄,只不過lockName的值都是一樣的
這時候客戶端A成功的把lockName儲存到lock表中了,那麼其它客戶端要儲存這個lockName的時候(也就是執行加鎖),由於唯一索引的緣故,就會插入失敗。也就保證了同一個時間只能有一個客戶端儲存成功,也就是加鎖成功了
那麼如何釋放鎖呢?
在這個客戶端業務執行完之後,手動的把這條記錄刪除掉,那麼其它客戶端就可以來繼續加鎖了
程式碼實現
首先我們在mapper檔案中編寫 加鎖 和 解鎖 的SQL,這裏為什麼還要儲存個uuid,後續會講到(主要是防止鎖被誤刪)
//加鎖語句 @Insert("insert into record_lock (lock_name, uuid) values (#{lockName}, #{uuid})") Integer insert(@Param("lockName") String lockName, @Param("uuid") String uuid); //解鎖語句 @Delete("delete from record_lock where lock_name = #{lockName} and uuid = #{uuid}") Integer delete(@Param("lockName") String lockName, @Param("uuid") String uuid);
然後我們需要實現我們的加鎖 和 解鎖
public class MySqlDistributeLock { private String lockName; //自定義編寫的mybatis的mapper檔案 private MySqlLockMapper mySqlLockMapper; private String uuid; public MySqlDistributeLock(String lockName,MySqlLockMapper mySqlLockMapper,String uuid) { this.lockName = lockName; this.mySqlLockMapper = mySqlLockMapper; this.uuid = uuid; } public void lock() { while (true) { try{ int result = mySqlLockMapper.insert(this.lockName, this.uuid); if(result > 0) { //代表加鎖成功 break; } } catch (Exception e) { } //唯一索引加鎖失敗 try { Thread.sleep(100); } catch (InterruptedException interruptedException) { throw new RuntimeException(); } } } public void unLock() { mySqlLockMapper.delete(this.lockName,this.uuid); } }
最後看下業務方如何使用
@Service public class LockService { @Resource private MySqlDistributeLock.MySqlLockMapper mySqlLockMapper; public String deductStockMysqlLock(String productId,Integer count) { MySqlDistributeLock lock = null; try{ lock = new MySqlDistributeLock(productId, mySqlLockMapper,UUID.randomUUID().toString()); //加鎖 lock.lock(); //加鎖成功,開始執行我們自己的業務邏輯 }finally { if(lock != null) { lock.unLock(); } } return "success"; } }
基於唯一索引實現的分散式鎖有沒有什麼問題呢??
死鎖問題
我們試想一下,如果客戶端A來加鎖成功了,業務也執行完了,但是這時候釋放鎖的時候,也就是執行刪除語句的時候因為一些原因導致刪除失敗了,那麼這條記錄一直存在,後續的執行緒就沒辦法再獲取到鎖了,這就是所謂的死鎖
所以這時候我們還需要另外一個服務來定時掃描這些記錄,如果這個記錄超過了10分鐘,或者20分鐘還沒有被刪除掉,那麼大機率是釋放鎖的時候失敗了,所以需要再次刪除這條記錄
鎖誤刪
為什麼鎖會誤刪呢? 爲了防止死鎖,我們會有一個單獨的定時任務來掃描,假設我們判斷一把鎖超過10分鐘就認為是釋放鎖失敗了,這時候定時任務就會把這條記錄刪除掉,但是這時候就會有問題了,舉個例子
客戶端A首先獲取到鎖了,然後開始執行業務,但是因為業務比較複雜,執行完業務可能需要15分鐘,這時候到第10分鐘的時候,定時任務就會把這條記錄給刪除掉了
這時候因為記錄沒有了,客戶端B來獲取鎖是能成功獲取到的,所以這時候這把鎖的持有者應該是客戶端B的
到第15分鐘的時候,客戶端A業務執行完了,就是執行釋放鎖的邏輯,那麼客戶端A就會把這條記錄給刪除掉了,也就導致客戶端A把客戶端B的鎖給釋放掉了
所以在開頭的時候,我們加鎖除了要儲存lockName,還要儲存一個uuid,在釋放鎖的時候,判斷一下uuid是否相等,如果不相等,那就不能刪除這條記錄了,因為這時候這把鎖已經不是當前客戶端持有的了
鎖續期
大家可以想一下,分散式鎖的主要目的就是同一個時間點只能有一個執行緒去執行業務,但是在上面我們可以看到,即使加了uuid來保證了鎖誤刪,但是在同一個時間點可能是有多個執行緒在一起執行業務的,爲了避免這種情況,就需要保證一個客戶端在沒有執行完業務以前,是不允許其它客戶端執行業務的
但是定時任務判斷的時間我們沒辦法預估,可能業務需要10分鐘,也有可能是20分鐘,我們沒辦法準確預估這個時間
所以我們在一個客戶端加鎖成功之後,可以起一個額外的執行緒,時時的更新加鎖的時間,這就類似Redisson的看門狗機制了,那麼如何去做呢??
1:加鎖的時候,除了儲存lockName,uuid,額外儲存一個加鎖時間lockTime
2:加鎖成功之後,額外開啟一個執行緒,每過10秒就更新lockTime為當前時間
3:定時任務掃描到lcokTime距離當前時間超過10分鐘或者5分鐘的記錄就刪除掉這條記錄
3:基於樂觀鎖實現分散式鎖
基於樂觀鎖機制就是依靠版本機制來實現,我們一般在資料庫會儲存version,或者是時間戳,至於實現方式大家可以自己實現一下,這裏就不做贅述了
4:最後
至此我們就完成了基於MySql實現的分散式鎖了