隨着業務越來越負責,現在的業務,能夠支持分佈式和高併發是基本的要求,涉及到高併發和分佈式就一定會涉及到分佈式鎖機制,分佈式鎖就是爲了保證分佈式環境下,只有一個機器能夠拿到鎖對象,其餘的都需等待該鎖釋放,再進行申請鎖資源!
分佈式鎖必須遵循以下原則:
- 同一時刻只能有一個
機器(進程或線程)
能夠拿到鎖對象! - 擁有過期機制,防止機器宕機沒有釋放鎖的情況下造成死鎖!
- 加鎖和解鎖的必須是一個機器(線程、進程)!
- 集羣環境下,存活機器依舊何以做完整的加解鎖操作!
一、思路圖
二、思路圖實現
1.正確實現
該實現由
Jedis
實現,模擬多線程環境下使用分佈式鎖
<!-- https://mvnrepository.com/artifact/redis.clients/jedis -->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.3.0</version>
</dependency>
定義接口
package com.lock.jedis;
import java.util.concurrent.TimeUnit;
/**
* 分佈式鎖
* @author huangfu
*/
public interface DistributedLock {
/**
* 嘗試給線程加鎖
* @param lockName 鎖名稱
* @param lockValue 鎖值
* @param timeUnit 時間單位
* @param time 時間
* @return 是否加鎖成功
*/
boolean tryLock(String lockName, String lockValue, TimeUnit timeUnit, long time);
/**
* 上鎖
* @param lockName 鎖名稱
* @param lockValue 鎖值
* @param timeUnit 時間單位
* @param time 時間
*/
void lock(String lockName, String lockValue, TimeUnit timeUnit, long time);
/**
* 線程解鎖
* @param lockName 即將解鎖的鎖名稱
*/
void unLock(String lockName);
}
Jedis實現分佈式鎖
package com.lock.jedis.impl;
import com.lock.jedis.DistributedLock;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
import redis.clients.jedis.params.SetParams;
import java.util.Collections;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
/**
* jedis實現分佈式鎖
* @author huangfu
*/
public class JedisDistributedLock implements DistributedLock {
private final static String SCRIPT_UNLOCK_LUA = "if redis.call(\"get\",KEYS[1]) == ARGV[1] then\n" +
"return redis.call(\"del\",KEYS[1])\n" +
"else\n" +
"return 0\n" +
"end";
private final static ThreadLocal<String> THREAD_LOCAL = new ThreadLocal<String>();
public static final String OK = "OK";
JedisPool pool;
public JedisDistributedLock() {
JedisPoolConfig config = new JedisPoolConfig();
config.setMaxIdle(8);
config.setMaxTotal(18);
pool = new JedisPool(config, "192.168.1.4", 6379, 2000);
}
@Override
public boolean tryLock(String lockName, String lockValue, TimeUnit timeUnit,
long time) {
Jedis jedis = pool.getResource();
try {
long timeout = timeUnit.toSeconds(time);
SetParams setParams = new SetParams();
setParams.nx();
setParams.ex((int)timeout);
String result = jedis.set(lockName, lockValue, setParams);
if (OK.equals(result)) {
THREAD_LOCAL.set(lockValue);
return true;
}else{
return false;
}
}finally {
jedis.close();
}
}
@Override
public void lock(String lockName, String lockValue, TimeUnit timeUnit, long time) {
if (tryLock(lockName,lockValue,timeUnit,time)) {
return;
} else {
try {
Thread.sleep(ThreadLocalRandom.current().nextInt(2000,3000));
} catch (InterruptedException e) {
e.printStackTrace();
}
lock(lockName,lockValue,timeUnit,time);
}
}
@Override
public void unLock(String lockName) {
Jedis jedis = pool.getResource();
Object eval = jedis.eval(SCRIPT_UNLOCK_LUA, Collections.singletonList(lockName), Collections.singletonList(THREAD_LOCAL.get()));
if(Integer.parseInt(eval.toString()) == 0){
jedis.close();
throw new RuntimeException("解鎖失敗!");
}
THREAD_LOCAL.remove();
}
}
2. 代碼註釋
-
爲什麼要設置過期時間
- 保證再服務宕機時,鎖依舊能夠被正常釋放!
-
爲什麼刪除鎖的時候會進行判斷值操作?
- 多線程環境下,對於鎖而言,本線程只能刪除本線程加的鎖,無法刪除別的線程加的鎖!這裏舉例用UUID來實現值的唯一性
-
tryLock
方法中爲什麼要使用SetParams
承載 setnx參數?爲很麼刪除鎖時要使用lua
腳本來實現?- 因爲無論時加鎖(setnx)操作,還是解鎖(del)操作,都必須保證其代碼的原子性
SetParams setParams = new SetParams();
setParams.nx();
setParams.ex((int)timeout);
String result = jedis.set(lockName, lockValue, setParams);
這段文中的代碼等價與
jedis.setnx(lockName,lockValue);
jedis.expire(lockName,(int)timeout);
但是爲什麼不這樣寫呢?因爲redis時單線程,同一時間只能執行一個命令,而這種寫法再redis認爲是兩個命令,那麼在多機或者多線程環境下執行時就可能出現問題!
我們看個圖,正常情況:
但是,非正常情況下:
爲很麼刪除鎖時要使用
lua
腳本來實現?
文中代碼
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
這段lua腳本等同於代碼
String s = jedis.get(lockName);
if(THREAD_LOCAL.get().equals(s)){
jedis.del(lockName);
}
這段代碼和上面的一樣,都保證不了原子性!一起看個圖!
正常情況
異常情況
好了,本期就是使用lua腳本來實現分佈式鎖的內容了,這個是分佈式鎖的基本實現的原理,但是再生產環境上使用會有很嚴重的問題!
- 現代生產環境都是追求高可用,那麼redis在集羣環境和哨兵集羣環境下如何保證分佈式鎖的高可用呢?
- 當前問題並沒有保證業務時間絕對小於鎖的過期時間,但不確定的業務場景下,依舊會出現分佈式鎖失效的情況!
以上問題如何解決呢?這個就是明天的內容了!怕文章太長,你們看不下去!哈哈哈!
才疏學淺,如果文章中理解有誤,歡迎大佬們私聊指正!歡迎關注作者的公衆號,一起進步,一起學習!