※ Redis分布式鎖的實作,主要就是基于“加鎖、釋放鎖、防止出現并發”三點來展開的。網上也有很多例子,但都沒有實作守護線程,我在這裡将守護線程的實作整合進去,供大家參考。
在《Redis實作分布式鎖的思路》中我們講了Redis分布式鎖實作的思路,沒看過的童鞋可以先去看一下,因為這裡的實作就是基于設計思路展開的。
實作Redis分布式鎖,我們需要考慮
- 加鎖,使用Redis 2.6.12以上版本提供的多參數set指令。
- 釋放鎖,使用del指令,但需要注意在釋放鎖之前判斷是否是自己加的鎖,不要把其他線程加的鎖給删除了,具體原因《Redis實作分布式鎖的思路》中有詳細說明。另外為了保證判斷和釋放操作保證原子性,此處我們使用Lua腳本實作。
- 防止出現并發,具體原因也在《Redis實作分布式鎖的思路》中有詳細說明,我們讓獲得鎖的線程開啟一個守護線程,給快要逾時的線程續命。
代碼
> pom檔案
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.felix</groupId>
<artifactId>demo</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.3.RELEASE</version>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.2.0</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.12</version>
<scope>provided</scope>
</dependency>
</dependencies>
</project>
> Jedis初始化類
package com.felix.util;
import org.springframework.stereotype.Service;
import redis.clients.jedis.Jedis;
/**
* @Copyright (C), 2019-2020, Felix
* @ClassName: JedisUtil
* @Author: long.yuan
* @E-mail: [email protected]
* @Date: 2020/2/15 15:10
* @Version: V1.0
* @Description: Jedis初始化類
*/
@Service
public class JedisUtil {
/**
* 初始化Jedsi執行個體
*/
public static final Jedis jedis;
static{
jedis = new Jedis("Redis主機IP",6379);
jedis.auth("Redis連接配接密碼");
}
}
> Redis分布式鎖工具類
package com.felix.util;
import org.springframework.stereotype.Service;
import redis.clients.jedis.params.SetParams;
import java.util.Collections;
import java.util.concurrent.TimeUnit;
/**
* @Copyright (C), 2019-2020, Felix
* @ClassName: RedisDistributedLockTool
* @Author: long.yuan
* @E-mail: [email protected]
* @Date: 2020/2/15 14:02
* @Version: V1.0
* @Description: Redis分布式鎖工具類
*/
@Service
public class RedisDistributedLockTool {
/**
* 加鎖成功
*/
private static final String LOCK_SUCCESS = "OK";
/**
* 解鎖成功
*/
private static final Long RELEASE_SUCCESS = 1L;
/**
* 如果沒有擷取到鎖,每10ms重試一次
*/
private static final int DEFAULT_ACQUIRY_RETRY_MILLIS = 10;
/**
* @Title 加鎖
* @Description 如果擷取鎖失敗,每10ms重試一次,重試最大次數為3,否則傳回false
* @Author long.yuan
* @Date 2020/2/15 14:49
* @Param [key, value, expireTime]
* @return boolean
**/
public static boolean lock(String key, String value, long expireTime) throws InterruptedException {
int count = 0;
int countMax = 3;
while (count < countMax){
SetParams params = new SetParams();
// NX表示僅在key不存在時才能設定成功
params.nx();
// EX表示機關是秒,PX表示機關是毫秒
params.px(expireTime);
if (LOCK_SUCCESS.equals(JedisUtil.jedis.set(key, value, params))) {
return true;
}else{
// 如果沒有拿到鎖,每10ms重試一次,最多重試3次
TimeUnit.MILLISECONDS.sleep(DEFAULT_ACQUIRY_RETRY_MILLIS);
count++;
}
}
return false;
}
/**
* @Title 解鎖
* @Description 解鎖時與傳進來的value作對比,看是否是自己加的鎖,如果是自己加的鎖再删除
* @Author long.yuan
* @Date 2020/2/15 14:55
* @Param [key, value]
* @return boolean
**/
public static boolean unLock(String key, String value) {
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
Object result = JedisUtil.jedis.eval(script, Collections.singletonList(key), Collections.singletonList(value));
if (RELEASE_SUCCESS.equals(result)) {
return true;
}
return false;
}
}
> 守護線程類
package com.felix.util;
import lombok.Getter;
import lombok.Setter;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
/**
* @Copyright (C), 2019-2020, Felix
* @ClassName: DaemonThread
* @Author: long.yuan
* @E-mail: [email protected]
* @Date: 2020/2/15 15:12
* @Version: V1.0
* @Description: 守護線程類
*/
@Service
@Getter
@Setter
public class DaemonThread implements Runnable{
/**
* 續命鎖的key
*/
private String key;
/**
* 每隔多久續命一次(機關:ms)
*/
private long aTime;
/**
* 每次續命多長時間(機關:ms)
*/
private long bTime;
public void run() {
while (true){
try {
// aTime時間後再續命
TimeUnit.MILLISECONDS.sleep(aTime);
// 每次續命bTime時間
System.out.println("守護線程續命"+bTime+"毫秒");
JedisUtil.jedis.expire(this.key, (int)bTime);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
> Redis分布式鎖測試類
package com.felix.service;
import com.felix.util.DaemonThread;
import com.felix.util.RedisDistributedLockTool;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
/**
* @Copyright (C), 2019-2020, Felix
* @ClassName: RedisDistributedLockTest
* @Author: long.yuan
* @E-mail: [email protected]
* @Date: 2020/2/15 15:35
* @Version: V1.0
* @Description: Redis分布式鎖測試類
*/
public class RedisDistributedLockTest {
public static void main(String[] args) {
try {
// 鎖key
String key = "REDIS_KEY_ORDER_ID_014";
// UUID作value,在删除的時候用來判斷是不是目前線程加的鎖
String value = UUID.randomUUID().toString();
// 逾時時間(機關:ms)
long expireTime = 10000;
if (RedisDistributedLockTool.lock(key,value,expireTime)){
// 1.加鎖完成後開啟一個守護線程給自己續命
DaemonThread daemonThread = new DaemonThread();
// 續命key
daemonThread.setKey(key);
// 過期前5s續命
daemonThread.setATime(expireTime-5000);
// 每次續命時長
daemonThread.setBTime(expireTime);
Thread thread = new Thread(daemonThread);
thread.setDaemon(true);
thread.start();
//2.執行業務邏輯(為了看到效果,我們這裡用睡眠60s來模拟)
System.out.println("執行業務邏輯start...");
TimeUnit.MILLISECONDS.sleep(60000);
System.out.println("執行業務邏輯end...");
//3.解鎖 & 關閉守護線程
RedisDistributedLockTool.unLock(key,value);
thread.interrupt();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
分析:運作main方法結合代碼我們可以看出,剛開始加鎖時設定的逾時時間是10s,為了看的明顯,我們模拟執行業務邏輯sleep 60s,在設定的逾時時間10s過期前5s第一次給鎖續命,續命時重新設定逾時時間為10s,以後每隔(10s-5s)續命一次,直到業務邏輯執行完守護線程被中斷。
感興趣的小夥伴可以關注一下部落客的公衆号,1W+技術人的選擇,緻力于原創技術幹貨,包含Redis、RabbitMQ、Kafka、SpringBoot、SpringCloud、ELK等熱門技術的學習&資料。