Java架構筆記——分散式鎖
寫在前面
喜歡的朋友可以關注下專欄:Java架構技術進階。裡面有大量batj面試題集錦,還有各種技術分享,如有好文章也歡迎投稿哦。
分散式鎖
併發程式設計中的鎖併發程式設計的鎖機制: synchronized和lock 。在單程序的系統中,當存在多個執行緒可以同時改變某個變數時,就需要對變數或程式碼塊做同步,使其在修改這種變數時能夠線性執行消除併發修改變數。
而同步的本質是通過鎖來實現的。為了實現多個執行緒在一個時刻同一個程式碼塊只能有一個執行緒可執行,那麼需要在某個地方做個標記,這個標記必須每個執行緒都能看到,當標記不存在時可以設定該標記,其餘後續執行緒發現已經有標記了則等待擁有標記的執行緒結束同步程式碼塊取消標記後再去嘗試設定標記。
分散式環境下,資料一致性問題一直是一個比較重要的話題,而又不同於單程序的情況。分散式與單機情況下最大的不同在於其不是多執行緒而是多程序。多執行緒由於可以共享堆記憶體,因此可以簡單的採取記憶體作為標記儲存位置。而程序之間甚至可能都不在同一臺物理機上, 因此需要將標記儲存在一個所有程序都能看到的地方。 常見的是秒殺場景,訂單服務部署了多個例項 ,如
秒殺商品有4個,第一個使用者購買3個,第二個使用者購買2個,理想狀態下第一個使用者能購買成功,第二
個使用者提示購買失敗,反之亦然。而實際可能出現的情況是,兩個使用者都得到庫存為4,第一個使用者
買到了3個,更新庫存之前,第二個使用者下了2個商品的訂單,更新庫存為2,導致出錯。
在上面的場景中,商品的庫存是共享變數,面對高併發情形,需要保證對資源的訪問互斥。在單機環境中,java中其實提供了很多併發處理相關的API,但是這些API在分散式場景中就無能為力了。也就是說單純的java API 並不能提供分散式鎖的能力。分散式系統中,由於分散式系統的分佈性,即多執行緒和多程序並且分佈在不同機器中,synchronized和lock這兩種鎖將失去原有鎖的效果,需要我們自已實現分散式鎖。
常見的分散式鎖如下:
- 基於資料庫實現分散式鎖:有效能問題
- 基於快取實現分散式鎖,如redis
- 基於zookeeper實現分散式鎖
使用setnx實現分散式鎖
setnx key value
setnx是將key的值設為value,當且僅當key不存在。若給定的key已經存在,則setnx不做任何動作。
返回1,說明該程序獲得鎖,setnx將鍵(lock.id)的值設定為鎖的超時時間,當前時間+加上鎖的有效時間。
返回0,說明其他程序已經獲得了鎖,程序不能進入臨界區。程序可以在一個迴圈中不斷地嘗試setnx操作,以獲得鎖。
存在死鎖的問題
線上程釋放鎖,即執行del lock.id操作前,需要先判斷鎖是否已超時。如果鎖已超時,那麼鎖可能已由其他執行緒獲得,這時直接執行del lock.id操作會導致把其他執行緒已獲得的鎖釋放掉。
獲取分散式鎖
public boolean lock( long timeout, TimeUnit timeUnit ) throws InterruptedException { timeout = timeUnit.toMillis( timeout ); long time = timeout + System.currentTimeMillis(); lock.tryLock( timeout, timeUnit ); try{ while ( true ) { boolean hasLock = tryLock(); if ( hasLock ) { return(true); /* 獲得鎖 */ }else if ( timeout < System.currentTimeMillis() ) { break; } Thread.sleep( 1000 ); } } finally { if ( lock.isHeldByCurrentThread() ) { lock.unlock(); } } return(false); } public boolean tryLock() { longtime= System.currentTimeMillis(); longtimeout = 2000; Stringexpires = String.valueOf( timeout + time ); if ( redisService.setnx( "lock.id", expires ) > 0 ) { /* 獲取鎖,設定超時時間 */ setLockStatus( expires ); return(true); }else{ String locktime = redisService.get( "lock.id" ); /* 檢查鎖是否超時 */ if ( locktime != null && Long.parseLong( locktime ) < time ) { String oldlocktime = redis.getset( "lock.id", expires ); /* 舊值與當前時間比較 */ if ( oldlocktime != null && locktime.equals( oldlocktime ) ) { /* 獲取鎖,設定超時時間 */ setLockStatus( expires ); return(true); } } return(false); } }
釋放鎖
public boolean unlock() { if ( lockHolder == Thread.currentThread() ) { /* 判斷鎖是否超時,沒有超時才將互斥量刪除 */ if ( lockExpiresTime > System.currentTimeMillis() ) { redisService.del( "lock.id" ); } lockHolder = null; return(true); }else{ throws new IllegalMonitorStateException( "無法執行解鎖操作" ); } }
喜歡的朋友可以關注下專欄:Java架構技術進階
如果你是Java程式設計師,對技術提升很感興趣,歡迎 1~5 年的工程師可以加入我的Java進階之路來交流學習:878249276。裡面都是同行,有 資源共享 ,還有大量 面試題以及解析 。歡迎一到五年的工程師加入,合理利用自己每一分每一秒的時間來學習提升自己,不要再用"沒有時間“來掩飾自己思想上的懶惰!趁年輕,使勁拼,給未來的自己一個交代!

