mongo分佈式鎖的bug

最近在研究用mongo做爲載體, 來實現分佈式鎖. 網上查了一下, 相關資料並很少, 討論得最多的一種實現方式思路以下: 得到鎖的步驟:java

* 一、首先判斷鎖是否被其餘請求得到;若是沒被其餘請求得到則往下進行redis

* 二、判斷鎖資源是否過時,若是過時則釋放鎖資源;spring

* 3.一、嘗試得到鎖資源,若是value=1,那麼得到鎖資源正常;mongodb

       (在當前請求已經得到鎖的前提下,還可能有其餘請求嘗試去得到鎖,此時會致使當前鎖的過時時間被延長,因爲延長時間在毫秒級, 能夠忽略。)安全

* 3.二、value>1,則表示當前請求在嘗試獲取鎖資源過程當中,其餘請求已經獲取了鎖資源,即當前請求沒有得到鎖;分佈式

* !!!注意,不須要鎖資源時,及時釋放鎖資源!!!。測試

我將他的代碼稍做了修改,看起來更清爽一些,具體代碼以下:this

##MongoLockHandler類代碼:
package com.example.demo.common.mongo;

import com.mongodb.client.result.DeleteResult;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.mongodb.core.FindAndModifyOptions;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.core.query.Update;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.util.List;

@Component
public class MongoLockHandler {

private static final Logger logger = LoggerFactory.getLogger(MongoLockHandler.class);

@Resource
private MongoTemplate mongoTemplate;

public boolean lock(String key, long expire) {
List<MongoLock> locks = this.getLockByKey(key);
if (locks.size() > 0) {
if (locks.get(0).getExpire() >= System.currentTimeMillis()) {//鎖已經被別人獲取
return false;
} else {//釋放過時的鎖,以便進行新一輪競爭
this.releaseExpiredLock(key, System.currentTimeMillis());
}
}

//開始新一輪競爭
int value = this.upsertLock(key, 1, System.currentTimeMillis() + expire);
// logger.info("鎖爲:{}", value);
return value == 1 ? true : false;

}

public boolean unLock(String key) {
Query query = new Query();
query.addCriteria(Criteria.where("key").is(key));
DeleteResult result = mongoTemplate.remove(query, MongoLock.class);
return result.getDeletedCount() > 0;
}

private Integer upsertLock(String key, int value, long expireTime) {
Query query = new Query();
query.addCriteria(Criteria.where("key").is(key));

Update update = new Update();
update.inc("value", value);
update.set("expire", expireTime);

FindAndModifyOptions options = new FindAndModifyOptions();
options.upsert(true); //存在則更新,不然插入
options.returnNew(true); //返回更新後的值

//此處貌似不是線程安全的,這種實現方式不能做爲分佈式鎖使用??????????
MongoLock mongoLock = mongoTemplate.findAndModify(query, update, options, MongoLock.class);
return mongoLock.getValue();
}

private void releaseExpiredLock(String key, long expireTime) {
Query query = new Query();
query.addCriteria(Criteria.where("key").is(key));
query.addCriteria(Criteria.where("expire").lt(expireTime));
mongoTemplate.remove(query, MongoLock.class);
}

private List<MongoLock> getLockByKey(String key) {
Query query = new Query();
query.addCriteria(Criteria.where("key").is(key));
return mongoTemplate.find(query, MongoLock.class);
}

}
##MongoLock實體類代碼:
package com.example.demo.common.mongo;

public class MongoLock {

private String key;

private Integer value;

private Long expire;

public String getKey() {
return key;
}

public void setKey(String key) {
this.key = key;
}

public Integer getValue() {
return value;
}

public void setValue(Integer value) {
this.value = value;
}

public Long getExpire() {
return expire;
}

public void setExpire(Long expire) {
this.expire = expire;
}

}

原做者想利用mongo實現id自增,且自增過程爲原子操做,即線程安全,這個特性來實現鎖機制,spa

可是忽略了,mongo的findAndModify方法不是線程安全的,以致於整個鎖機制失效.線程

有疑問的童鞋能夠去官網瞭解一下findAndModify方法,網上有些資料說這個方法是同步的.....

 

我一開始也沒把注意力放在這個上面,後來好奇,寫了段測試代碼,結果就發現問題了

測試很簡單, 啓動兩個線程(你也能夠啓動更多,但目前兩個都暴露了問題), 去競爭鎖 ,

代碼以下:

public void init() {

Runnable r1 = () -> {
while (true) {
logger.info("aaaaa:{}", mongoLockHandler.lock(LOCK_KEY, 3000));
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};

Runnable r2 = () -> {
while (true) {
logger.info("bbbbb:{}", mongoLockHandler.lock(LOCK_KEY, 3000));
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};

new Thread(r1).start();
new Thread(r2).start();
}

結果是,這兩個線程同時獲取到了鎖......

碰到問題後,一直在懷疑這段代碼時間判斷問題,致使提早釋放了鎖:

if (locks.size() > 0) {
if (locks.get(0).getExpire() >= System.currentTimeMillis()) {//鎖已經被別人獲取
return false;
} else {//釋放過時的鎖,以便進行新一輪競爭
this.releaseExpiredLock(key, System.currentTimeMillis());
}
}

最終發現這裏並無問題,問題仍是findAndModify方法並非同步的,兩個線程同時往mongo裏寫了數據,而且value都是1

至此纔算發現問題根源,那有沒有解決方案呢,嘗試過一些方法,無果......若是哪位大神有可靠的解決方案,記得留言哦,也不知道有多少網友在本身的系統用這種坑爹的代碼...仍是趕快偷偷摸摸改掉呀.目前有redis實現的分佈式鎖是比較可靠的,經驗證暫無bug.後續將寫一篇關於redis分佈式鎖的文章供你們參考
相關文章
相關標籤/搜索