最近在研究用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分佈式鎖的文章供你們參考