分散式鎖的基本原理和實現
歡迎關注作者簡書
文章目錄
轉發:原文來自 傳送門
一、什麼是分散式鎖?
要介紹分散式鎖,首先要提到與分散式鎖相對應的是執行緒鎖、程序鎖。
執行緒鎖:主要用來給方法、程式碼塊加鎖。當某個方法或程式碼使用鎖,在同一時刻僅有一個執行緒執行該方法或該程式碼段。執行緒鎖只在同一JVM中有效果,因為執行緒鎖的實現在根本上是依靠執行緒之間共享記憶體實現的,比如synchronized是共享物件頭,顯示鎖Lock是共享某個變數(state)。
程序鎖:為了控制同一作業系統中多個程序訪問某個共享資源,因為程序具有獨立性,各個程序無法訪問其他程序的資源,因此無法通過synchronized等執行緒鎖實現程序鎖。
分散式鎖:當多個程序不在同一個系統中,用分散式鎖控制多個程序對資源的訪問。
二、分散式鎖的使用場景。
執行緒間併發問題和程序間併發問題都是可以通過分散式鎖解決的,但是強烈不建議這樣做!因為採用分散式鎖解決這些小問題是非常消耗資源的!分散式鎖應該用來解決分散式情況下的多程序併發問題才是最合適的。
有這樣一個情境,執行緒A和執行緒B都共享某個變數X。
如果是單機情況下(單JVM),執行緒之間共享記憶體,只要使用執行緒鎖就可以解決併發問題。
如果是分散式情況下(多JVM),執行緒A和執行緒B很可能不是在同一JVM中,這樣執行緒鎖就無法起到作用了,這時候就要用到分散式鎖來解決。
三、分散式鎖的實現(Redis)
分散式鎖實現的關鍵是在分散式的應用伺服器外,搭建一個儲存伺服器,儲存鎖資訊,這時候我們很容易就想到了Redis。首先我們要搭建一個Redis伺服器,用Redis伺服器來儲存鎖資訊。
在實現的時候要注意的幾個關鍵點:
-
鎖資訊必須是會過期超時的,不能讓一個執行緒長期佔有一個鎖而導致死鎖;
-
同一時刻只能有一個執行緒獲取到鎖。
幾個要用到的redis命令:
-
setnx(key, value):“set if not exits”,若該key-value不存在,則成功加入快取並且返回1,否則返回0。
-
get(key):獲得key對應的value值,若不存在則返回nil。
-
getset(key, value):先獲取key對應的value值,若不存在則返回nil,然後將舊的value更新為新的value。
-
expire(key, seconds):設定key-value的有效期為seconds秒。
看一下流程圖:
在這個流程下,不會導致死鎖。
我採用Jedis作為Redis客戶端的api,下面來看一下具體實現的程式碼。
(1)首先要建立一個Redis連線池。
public class RedisPool {
private static JedisPool pool;//jedis連線池
private static int maxTotal = 20;//最大連線數
private static int maxIdle = 10;//最大空閒連線數
private static int minIdle = 5;//最小空閒連線數
private static boolean testOnBorrow = true;//在取連線時測試連線的可用性
private static boolean testOnReturn = false;//再還連線時不測試連線的可用性
static {
initPool();//初始化連線池
}
public static Jedis getJedis(){
return pool.getResource();
}
public static void close(Jedis jedis){
jedis.close();
}
private static void initPool(){
JedisPoolConfig config = new JedisPoolConfig();
config.setMaxTotal(maxTotal);
config.setMaxIdle(maxIdle);
config.setMinIdle(minIdle);
config.setTestOnBorrow(testOnBorrow);
config.setTestOnReturn(testOnReturn);
config.setBlockWhenExhausted(true);
pool = new JedisPool(config, "127.0.0.1", 6379, 5000, "liqiyao");
}
}
(2)對Jedis的api進行封裝,封裝一些實現分散式鎖需要用到的操作。
public class RedisPoolUtil {
private RedisPoolUtil(){}
private static RedisPool redisPool;
public static String get(String key){
Jedis jedis = null;
String result = null;
try {
jedis = RedisPool.getJedis();
result = jedis.get(key);
} catch (Exception e){
e.printStackTrace();
} finally {
if (jedis != null) {
jedis.close();
}
return result;
}
}
public static Long setnx(String key, String value){
Jedis jedis = null;
Long result = null;
try {
jedis = RedisPool.getJedis();
result = jedis.setnx(key, value);
} catch (Exception e){
e.printStackTrace();
} finally {
if (jedis != null) {
jedis.close();
}
return result;
}
}
public static String getSet(String key, String value){
Jedis jedis = null;
String result = null;
try {
jedis = RedisPool.getJedis();
result = jedis.getSet(key, value);
} catch (Exception e){
e.printStackTrace();
} finally {
if (jedis != null) {
jedis.close();
}
return result;
}
}
public static Long expire(String key, int seconds){
Jedis jedis = null;
Long result = null;
try {
jedis = RedisPool.getJedis();
result = jedis.expire(key, seconds);
} catch (Exception e){
e.printStackTrace();
} finally {
if (jedis != null) {
jedis.close();
}
return result;
}
}
public static Long del(String key){
Jedis jedis = null;
Long result = null;
try {
jedis = RedisPool.getJedis();
result = jedis.del(key);
} catch (Exception e){
e.printStackTrace();
} finally {
if (jedis != null) {
jedis.close();
}
return result;
}
}
}
(3)分散式鎖工具類
public class DistributedLockUtil {
private DistributedLockUtil(){
}
public static boolean lock(String lockName){//lockName可以為共享變數名,也可以為方法名,主要是用於模擬鎖資訊
System.out.println(Thread.currentThread() + "開始嘗試加鎖!");
Long result = RedisPoolUtil.setnx(lockName, String.valueOf(System.currentTimeMillis() + 5000));
if (result != null && result.intValue() == 1){
System.out.println(Thread.currentThread() + "加鎖成功!");
RedisPoolUtil.expire(lockName, 5);
System.out.println(Thread.currentThread() + "執行業務邏輯!");
RedisPoolUtil.del(lockName);
return true;
} else {
String lockValueA = RedisPoolUtil.get(lockName);
if (lockValueA != null && Long.parseLong(lockValueA) >= System.currentTimeMillis()){
String lockValueB = RedisPoolUtil.getSet(lockName, String.valueOf(System.currentTimeMillis() + 5000));
if (lockValueB == null || lockValueB.equals(lockValueA)){
System.out.println(Thread.currentThread() + "加鎖成功!");
RedisPoolUtil.expire(lockName, 5);
System.out.println(Thread.currentThread() + "執行業務邏輯!");
RedisPoolUtil.del(lockName);
return true;
} else {
return false;
}
} else {
return false;
}
}
}
}
歡迎加入Java猿社群!
免費領取我歷年收集的所有學習資料哦!