随想(二)-- 基于负载均衡和本地锁的分布式锁

实现背景

  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();
}
}
}
}
}

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