0.前言
在多線程併發的情況下,我們可以使用鎖來保證代碼在同一時間只能一個線程訪問,比如synchronize或者lock。但在分佈式的集羣環境,就需要使用分佈式鎖。
分佈式:一個業務拆分爲多個子業務,部署在多個服務器上 。
集羣:同一個業務,部署在多個服務器上 。
1.場景:
a).避免不同節點(業務)重複相同工作,
b).避免破壞數據的正確性
比如,同一服務部署在不同的服務器上,同時接受到請求,同時對數據進行修改(如i- -),則可能出現重複- -的情況。
2.Redis分佈式鎖原理:
Set命令附加NX和PX屬性
主要依託了redis的 Key 不存在時才能 Set 成功的特性
A服務set成功(獲取鎖成功),那麼B服務就無法獲取改Key的鎖,除非A服務釋放了鎖,B服務才能獲取到。,
3.Redis分佈式鎖主要關注的問題:
a.鎖超時:
A服務set成功(獲取了鎖),之後A服務突然掛掉了,那麼其它服務就無法獲取到鎖了,爲了避免這種問題,所以需要添加鎖過期時間,像上述圖片t2在30s後會自動失效,即釋放鎖。
b.原子性問題:
加鎖的時候,如果先set t2 666,然後再expire t2 30 ;這樣的操作不是原子性的,也就是說,如果set t2 666後突然服務掛了,又還沒來得及設置過期時間,則其他服務永遠也獲取不到鎖,但是set t2 666 PX 30000 NX這樣就是原子性的。
解鎖的時候,如果直接jedis.del(key);可能會刪除不是自己加的鎖。,
是否會想到下述的操作,先get判斷再del,但這並不是原子性的操作,比如A服務加鎖後,到了過期時間自動釋放了鎖,之後B服務獲取了鎖,此時A服務業務邏輯執行完後,直接就把鎖給刪了,服務就亂套了。
if (value.equals(jedis.get(key))) {
// 如果這時鎖過期自動釋放,又被其他線程加鎖,該線程就會釋放不屬於自己的鎖
jedis.del(key);
}
正確釋放鎖,需要執行lua腳本來進行。
c.業務執行超過自動過期時間:
A服務獲得了鎖,業務執行時間超過了過期時間,A服務還未手動釋放鎖就自動釋放了鎖,然後B服務獲得了鎖,也有可能出現別的問題,所以爲了避免這個問題, Redis 分佈式鎖不要用於較長時間的任務。
或者對獲得鎖的線程啓動一個守護線程,用來給鎖續期(執行expire命令),執行時間就變長了這樣。
d.請求是否不可丟失問題:
如果請求不允許丟失,即每個服務都要獲取鎖來執行業務邏輯,則不斷重試獲取鎖,直到獲取鎖成功。
如果允許丟失,則嘗試重試到設置的重試時間閾值,無法獲取鎖則不管了,也就不執行業務邏輯了。
4.Redis分佈式鎖代碼實現:
加鎖與釋放鎖的類:
package com.zwh.JedisDistributed;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.params.SetParams;
import java.util.Collections;
public class RedisLock {
public String LOCK_KEY = "reids_lock";
private long AUTO_EXPIRE_TIME; //鎖自動釋放閾值
private long reTryTime; //重試時間閾值
private SetParams params ;
private static GenericObjectPoolConfig poolConfig = new GenericObjectPoolConfig();
public static JedisPool jedisPool = null;
public RedisLock(Integer inventory, long AUTO_EXPIRE_TIME,long reTryTime) {
this.AUTO_EXPIRE_TIME = AUTO_EXPIRE_TIME;
this.params = SetParams.setParams().nx().px(AUTO_EXPIRE_TIME);
this.reTryTime=reTryTime;
poolConfig.setMaxIdle(inventory);
poolConfig.setMaxTotal(inventory);
poolConfig.setMaxWaitMillis(2000);//2s後報錯
jedisPool = new JedisPool(poolConfig, "192.168.199.188", 6379,4000);//4s後報連接超時
}
/**
* 嘗試獲取鎖,到了重試時間閾值退出,即可能請求丟失
* */
public String lock(String id) {
Long start = System.currentTimeMillis();
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
for (; ; ) {
//SET命令返回OK ,則證明獲取鎖成功
String lock = jedis.set(LOCK_KEY, id, params);
if ("OK".equals(lock)) {
return "OK";
}
//否則循環等待,在timeout時間內仍未獲取到鎖,則獲取失敗
long l = System.currentTimeMillis() - start;
if (l >= reTryTime) {
return "TimeOut";
}
Thread.sleep(200);
}
} catch (Exception e) {
System.out.println("******異常*******:"+e.toString());
} finally {
try {
if (jedis != null) {
// System.out.println(jedisPool.getNumWaiters()+"----鏈接活躍數:"+jedisPool.getNumActive()+"----空閒連接數:"+jedisPool.getNumIdle());
jedis.close();
// System.out.println(jedisPool.getNumWaiters()+"----Close後活躍數:"+jedisPool.getNumActive()+"----空閒連接數:"+jedisPool.getNumIdle());
}
} catch (Exception e) {
e.printStackTrace();
}
}
return "";
}
/**
* 釋放鎖方法
* */
public boolean unlock(String id) {
Jedis jedis = null;
String script =
"if redis.call('get',KEYS[1]) == ARGV[1] then" +
" return redis.call('del',KEYS[1]) " +
"else" +
" return 0 " +
"end";
try {
jedis = jedisPool.getResource();
String result = jedis.eval(script, Collections.singletonList(LOCK_KEY), Collections.singletonList(id)).toString();
return "1".equals(result) ? true : false;
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
jedis.close();
} catch (Exception e) {
e.printStackTrace();
}
}
return false;
}
}
測試類:
import com.zwh.JedisDistributed.RedisLock;
import redis.clients.jedis.Jedis;
import java.util.UUID;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class Test {
private static Integer POOL_SIZE=1001;
private static long AUTO_EXPIRE_TIME = 3000;
private static int NUM =1000;
private static long reTryTime = 50000;
private static LinkedBlockingQueue linkedBlockingQueue = new LinkedBlockingQueue();
static RedisLock redisLock = new RedisLock(POOL_SIZE,AUTO_EXPIRE_TIME,reTryTime);
public static void getDeamon() {
Thread deamonThread = new Thread(new Runnable() {
@Override
public void run() {
try (Jedis jedis = redisLock.jedisPool.getResource()) {
while (true){
Thread.sleep(2000);
System.out.println("ttl:"+jedis.ttl(redisLock.LOCK_KEY));
jedis.expire(redisLock.LOCK_KEY, 3);
// System.out.println("----------續期了----------"+Thread.currentThread().getName());
}
} catch (Exception e) {
System.out.println("守護線程異常:" + e.toString());
}
}
});
deamonThread.setDaemon(true);
deamonThread.start();
}
/**
* 業務處理超過自動過期時間,守護線程爲獲取鎖續期。
* */
public static void main(String[] args) {
ThreadPoolExecutor executor = new ThreadPoolExecutor(POOL_SIZE, POOL_SIZE, 10L, TimeUnit.SECONDS, linkedBlockingQueue);
for (int i = 0; i <50; i++) {
final int finalI = i;
executor.execute(new Runnable() {
@Override
public void run() {
String uuid =UUID.randomUUID().toString();
String lockResult = redisLock.lock(uuid); //重試獲取鎖失敗時,請求丟失
if ("OK".equals(lockResult)){
getDeamon(); //啓動守護線程,每2s續期3s
try {
Thread.sleep(2000); //業務邏輯處理耗時
NUM--;
System.out.println("-------庫存NUM:"+NUM);
} catch (InterruptedException e) {
e.printStackTrace();
}
boolean unlock = redisLock.unlock(uuid);
// System.out.println("************unlock:"+unlock+"******"+Thread.currentThread().getName());
}else {
System.out.println("重試超時:"+lockResult);
}
}
});
}
executor.shutdown();
}
/**
* 業務處理超過自動過期時間,不做處理
* */
public static void main1(String[] args) {
ThreadPoolExecutor executor = new ThreadPoolExecutor(POOL_SIZE, POOL_SIZE, 10L, TimeUnit.SECONDS, linkedBlockingQueue);
for (int i = 0; i <100; i++) {
// final int finalI = i;
executor.execute(new Runnable() {
@Override
public void run() {
String uuid =UUID.randomUUID().toString();
String lockResult = redisLock.lock(uuid);//重試獲取鎖失敗時,請求丟失
if ("OK".equals(lockResult)){
long start = System.currentTimeMillis();
try {
Thread.sleep(100); //業務邏輯處理耗時
NUM--;
System.out.println("-------庫存NUM:"+NUM);
} catch (InterruptedException e) {
e.printStackTrace();
}
long end = System.currentTimeMillis();
if ((end-start)<=AUTO_EXPIRE_TIME){
boolean unlock = redisLock.unlock(uuid); //成功獲得了鎖並且業務處理爲超時
}else{
System.out.println("------------------業務超時---------------------");
}
}else {
System.out.println("重試超時:"+lockResult);
}
}
});
}
executor.shutdown();
}
}
參考資料:
漫畫:什麼是分佈式鎖?
Redis—分佈式鎖深入探究
姍姍來遲的Redis分佈式鎖
代碼地址:
https://github.com/OooooOz/Redis
//-------------------------------------------------------相互交流--------------------------------------------------//