隨想(二)-- 基於負載均衡和本地鎖的分佈式鎖

實現背景

  1. 業務需求
實現多人對同一片文檔的協同編輯,要求採用第一個發起請求時,當前文檔的最新版本作爲協同內容的基礎,多人同時請求時,要保證,所有請求獲得相同的文檔內容

  1. 現有架構

其中
  • 用戶請求到WebServer之間是給予WebServer負載的負載均衡算法
  • WebServer到存儲Server之間,是根據文檔Id Hash實現的定向算法,目的既有實現負載均衡,同時也可以利用本地的鎖,對同一個文件的讀寫進行控制(尤其是寫),實現文檔的版本和內容統一,例如如果兩個人在協同狀態下同時發出寫請求,我們會生成兩個版本,而不是後面覆蓋前面的。

分佈式鎖的引入
  1. 文檔同步問題
不同用戶協同的請求,很有可能會被不同的webServer所處理,所以協同請求同求在併發時就會出現問題。以下是分佈式鎖引入前的流程:


以上流程的問題在於如果對於同一篇文檔Step2和Step3,在不同的web server併發執行,也就是說兩個請求都會導致讀取最新版本文檔和寫入緩存,如果這個時候,兩個請求之間,又有協同的用戶,寫入了一個新的版本,就會導致兩個用戶獲得的協同內容不一致,從而導致協同失敗。

  1. 解決方案--分佈式鎖
基於上述Item 1的描述,需要在Step 2/3處實現一個分佈式鎖,保證同一時間只有一個請求讀取文件,並寫入Redish分佈式緩存。經過當前框架和業務需求的以下幾點分析,我們決定採用基於當前webserver到存儲server的定向Hash和存儲server上的本地鎖,來實現分佈式鎖
  • 從這個業務需求來看,分佈式鎖和文檔讀取,都是這個業務的關鍵點,也就是隻要一個失效,這個業務流程就需要終止,否則會產生意料不到的錯誤,例如,如果鎖有問題,導致兩個用在在不同版本上進行協同,產生的結果肯定是錯誤的。所以可以考慮將分佈式鎖和文檔讀取都分佈到存儲server上。
  • 從webserver到存儲server的RPC call,已經實現了基於文檔Id的Hash定向,也就是說,從webserver到存儲server的rpc call,可以根據文檔id, 定位到固定的存儲server上,所以可以用本地鎖來實現基於文檔Id的分佈式鎖
  • 存儲server已經實現了容錯機制,不會出現單點實現的問題,當前的分佈式鎖也就天然的繼承了這個能力

  1. 具體實現
首先,基於一樣的RPC定向機制,實現tryToRequestLock和releaseCollabLock方法,提供給webserver進行調用,參數中包含文檔id,以便進行store server的定向。
其次在store server上,實現一個本地鎖,最終基於文檔ID的定向+本地鎖,就可以實現這個分佈式鎖的需求了。本地鎖的實現可以用ReentrantLock來實現,一下是一個例子,供參考:


/**
這個鎖通過一個計數器,判斷請求的人數
同時加上鎖過期的概念,防止調用者沒有釋放鎖的情況
*/
public class CustomLock {

// the lock for counter
private final String lockName;
private final Lock lockInst = new ReentrantLock();
private final Condition waitForLock = lockInst.newCondition();
private final HashMap<String, Long> lockRequestList = new HashMap<String, Long>();
private final long requestExpireSecs = 5 * 60; // five minutes

public CustomLock(String lockName) {
// here the lockName can map to the document ID
this.lockName = lockName;
}

/**
* Request the lock
* the requester should be unique
* */
public void tryToRequestLock(String requester, long expireMilli) {
lockInst.lock();
try {

long requestCount = this.pushIntoRequestList(requester);
// for the first access, we don't need to wait
if (requestCount > 1) {
// if there are others to wait the lock,
// current one should also wait
try {
if(!waitForLock.await(expireMilli, TimeUnit.MILLISECONDS)){
// if timeout, we should decline the request count and
// throw timeout exception
this.releaseRequest(requester);
this.removeExpiredRequest();
throw new Exception();
}else{
// print some log
}
} catch (InterruptedException e) {
throw new Exception();
}
}else{
// print some log
}
}finally {
lockInst.unlock();
}
}
/*
釋放鎖
*/
public void tryToReleaseLock(String requester) throws YDriveException {
lockInst.lock();
try {
waitForLock.signal();
}finally {
this.releaseRequest(requester);
lockInst.unlock();
}
}

private long pushIntoRequestList(String requester) throws YDriveException{
long requestCount;
synchronized (this.lockRequestList) {
if(this.lockRequestList.containsKey(requester)){
throw new Exception();
}
this.lockRequestList.put(requester, Long.valueOf(System.currentTimeMillis()));
requestCount = this.lockRequestList.size();
}
return requestCount;
}

private void releaseRequest(String requester) {
synchronized (this.lockRequestList) {
this.lockRequestList.remove(requester);
}
}

/*
* remove the expired request
* */
private void removeExpiredRequest(){
Iterator<Map.Entry<String, Long>> itr = this.lockRequestList.entrySet().iterator();
if(itr != null){
while (itr.hasNext()){
Map.Entry<String, Long> request = itr.next();
long latency = (System.currentTimeMillis() - request.getValue().longValue()) / 1000;
if(latency >= this.requestExpireSecs){
itr.remove();
}
}
}
}
}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章