分布式鎖
分布式鎖
0 背景
最近在業務中出現用戶重復提交退款,因為重復提交時間差極端,在加上中間網絡延遲,導致請求到達服務端時,出現兩個請求的時間差在毫秒級,從而導致重復數據;后來在商戶端也出現類似的情況,因此開始在關鍵業務中使用分布式鎖來解決這類問題。
分布式鎖 是控制分布式系統之前訪問共享資源的一種方式。如果不同的系統或是同一個系統的不同主機之間共享了一個或一組資源,那么訪問這些資源的時候,往往需要互斥來防止彼此干擾來保證一致性,在這種情況下,便需要使用到分布式鎖。
實現分布式鎖,主要有以下三個方面為重點:
-
獲取鎖
在并發情況下,保證只有一個client能夠獲取到鎖。
-
釋放鎖
client在正常處理業務結束之后主動釋放鎖;client處理過程中出現異常未能主動釋放鎖,需要系統能夠主動釋放鎖,保證不會出現死鎖。
-
其他client獲知鎖被釋放
當鎖被釋放之后,其他client可以獲知到鎖已經被釋放,并可以重新競爭鎖。
1 數據庫實現
1.1 悲觀鎖實現
使用MySQL的InnoDB的排他鎖來實現加鎖,通過釋放鏈接的方式釋放鎖。使用select for update sql語句時,數據庫會給數據表增加排他鎖,當改跳記錄加上排他鎖之后,其他線程無法再對該記錄增加排他鎖。
``java public void lock(){ connection.setAutoCommit(false) try{ select * from lock where lock_name=xxx for update; if(結果不為空){ //代表獲取到鎖 return; } }catch(Exception e){} //為空或者拋異常的話都表示沒有獲取到鎖 sleep(1000); count++; } throw new LockException();
} public void release(){ connection.commit(); } ```</pre>
數據庫的實現方式,性能不高,在獲取鎖和釋放鎖時都可能因為數據庫異常而出現死鎖,為避免出現死鎖需要增加表設計的復雜度,如設置鎖的超時時間,并需要有job來保證鎖超時之后能夠正確釋放,實現成本相對較高。
1.2 唯一索引
將獲取鎖通過插入唯一索引來實現,釋放鎖則通過刪除改唯一索引記錄實現。
這種實現方式在實際業務中有變化的應用,一般實際業務會通過插入唯一索引獲取鎖,之后進行正常的業務處理,這個鎖記錄同時也是業務的一部分,因此不再執行釋放鎖的操作。比如在結算時,財務入賬時會采用這種方式。
java public void lock(){ int result =0 try{ result = execute("insert into lock values(uniqueid)") if(result >0){ //獲取到鎖 //開始業務處理 return; } }catch(Exception e){} // 未獲取到鎖 //業務決定是否需要等待以重新獲取鎖
} public void unlock(){ execute("delete from lock_table where uniqueid=id") } ```</pre>
2 分布式緩存實現
redis和memcached是目前應用最廣泛的分布式緩存,其中一些命令可用于實現分布式鎖。
-
memecached
add() —— 在新增緩存時,如果key已經存在則調用失敗
cas() —— 類似數據庫中的樂觀鎖,通過比較key對應value的變化來檢測是否獲取到鎖
-
redis
setnx() —— 設置key對應的value,如果該key已經存在,則設置失敗。expire() 緩存失效。
因為在我們的生產環境中主要使用redis,因此在這里只介紹redis的實現方式。常規使用方式:
public void lock(){ try{ int result = redis.setnx(lock_key, current_time+lock_timeout) if(result >0){ //獲取到鎖 //開始業務處理 return } }catch(Exception e){} //未獲取到鎖 //業務決定是否重復獲取
}
public void unlock(){ redis.del(lock_key) }</pre>
這種方式存在無法失效,但是當一個客戶端獲取到鎖之后掛掉了就無法即使釋放鎖,會導致死鎖的情況。因此現在主流的方式是為lock_key 設置一個過期時間,在讀取key的時實時判斷緩存是否過期。_
比如在文章 分布式鎖的實現 的實現方式:
``pythonget lock
lock = 0 while lock != 1: timestamp = current_unix_time + lock_timeout lock = SETNX lock.foo timestamp if lock == 1 or (now() > (GET lock.foo) and now() > (GETSET lock.foo timestamp)): break; else: sleep(10ms)
do your job
do_job()
release
if now() < GET lock.foo: DEL lock.foo
`</pre> <p>Redis從 2.6.12 版本開始,Redis中的SET命令可以通過EX second,PX millisecond,NX,XX進行修改,命令SET key value [EX seconds] [PX milliseconds] [NX|XX]</p> <p>EX second :設置鍵的過期時間為 second 秒。 SET key value EX second 效果等同于 SETEX key second value 。</p> <p>PX millisecond :設置鍵的過期時間為 millisecond 毫秒。 SET key value PX millisecond 效果等同于 PSETEX key millisecondvalue 。</p> <p>NX :只在鍵不存在時,才對鍵進行設置操作。 SET key value NX 效果等同于 SETNX key value 。</p> <p>XX :只在鍵已經存在時,才對鍵進行設置操作。</p> <p>因為 SET 命令可以通過參數來實現和 SETNX 、 SETEX 和 PSETEX 三個命令的效果,所以將來的 Redis 版本可能會廢棄并最終移除SETNX 、 SETEX 和 PSETEX</p> <p>這三個命令。</p> <p>因此上面的代碼可以簡單修改為如下模式:</p> <pre>
java public void lock(){ try{ int result = redis.set(lock_key, value,EX 1000,"NX") if(result >0){ //獲取到鎖 //開始業務處理 return } }catch(Exception e){} //未獲取到鎖 //業務決定是否重復獲取
}
public void unlock(){ redis.del(lock_key) } ```</pre>
redis官方也推薦了 Redlock 的分布式鎖實現方案,不過目前針對其中的算法還有爭論,在線上也沒有出現大規模使用,在這里不做過多討論;
2.1 優點
性能出色,實現相對簡單。
2.2 缺點
- redis是內存數據庫,雖然redis自身有AOF和RDB的數據恢復機制,并自帶復制功能,但在出現宕機的情況下,鎖數據很難保證。
- 通過鎖超時時間設置來保證鎖的最后釋放,這要求client在獲取鎖之后必須在超時時間內完成業務處理,否則超時之后會出現并發問題;且redis是分布式緩存,超時時間還需要考慮網絡時間消耗。
- redis單機情況下,存在redis單點故障的問題。如果為了解決單點故障而使用redis的sentinel或者cluster方案,則更加復雜,引入的問題更多。
3 zookeeper實現
zookeeper實現了類似paxos協議,是一個擁有多個節點分布式協調服務。對zookeeper寫入請求會轉發到leader,leader寫入完成,并同步到其他節點,直到所有節點都寫入完成,才返回客戶端寫入成功。
zookeeper一下特點使其非常適合用于實現分布式鎖:
- 支持watcher機制,通過watch鎖數據來實現鎖,采用刪除數據的方式來釋放鎖,刪除數據時可以通知到其他client;
- 支持臨時節點,如果客戶端獲取到鎖之后出現異常司機,臨時節點會被刪除,從而釋放鎖,無需通過設置超時時間的方式來避免死鎖。
zookeeper實現鎖的方式是客戶端一起競爭寫某條數據,比如/path/lock,只有第一個客戶端能寫入成功,其他的客戶端都會寫入失敗。寫入成功的客戶端就獲得了鎖,寫入失敗的客戶端,注冊watch事件,等待鎖的釋放,從而繼續競爭該鎖。
4 etcd 實現分布式鎖
etcd 是與zookeeper類似的高可用強一致性的服務發現倉庫,使用key-value的存儲方式。相對于zookeeper具有以下優點:
- 簡單:使用Golang編寫,部署更簡單;使用HTTP 作為接口使用簡單;使用Raft算法保證強一致性,便于理解。
- 數據持久化:默認數據一更新就進行持久化。
- 安全:支持SSL客戶端安全認證。
etcd作為后起之秀,處于告訴發展中,目前引用尚不及zookeeper廣泛。
References
來自:http://blog.brucefeng.info/post/distributed-locks