一、前言
關於redis分布式鎖, 查了很多資料, 發現很多只是實現了最基礎的功能, 但是, 並沒有解決當鎖已超時而業務邏輯還未執行完的問題, 這樣會導致: A線程超時時間設為10s(為了解決死鎖問題), 但代碼執行時間可能需要30s, 然后redis服務端10s后將鎖刪除, 此時, B線程恰好申請鎖, redis服務端不存在該鎖, 可以申請, 也執行了代碼, 那么問題來了, A、B線程都同時獲取到鎖並執行業務邏輯, 這與分布式鎖最基本的性質相違背: 在任意一個時刻, 只有一個客戶端持有鎖, 即獨享
為了解決這個問題, 本文將用完整的代碼和測試用例進行驗證, 希望能給小伙伴帶來一點幫助
二、准備工作
也可以直接官網下載, 我這邊都整理到網盤了
需要postman是因為我還沒找到jmeter多開窗口的辦法, 哈哈
三、說明
-
springmvc項目
-
maven依賴
<!--redis-->
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-redis</artifactId>
<version>1.6.5.RELEASE</version>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.7.3</version>
</dependency>
- 核心類
-
分布式鎖工具類: DistributedLock
-
測試接口類: PcInformationServiceImpl
-
鎖延時守護線程類: PostponeTask
四、實現思路
-
先測試在不開啟鎖延時線程的情況下, A線程超時時間設為10s, 執行業務邏輯時間設為30s, 10s后, 調用接口, 查看是否能夠獲取到鎖, 如果獲取到, 說明存在線程安全性問題
-
同上, 在加鎖的同時, 開啟鎖延時線程, 調用接口, 查看是否能夠獲取到鎖, 如果獲取不到, 說明延時成功, 安全性問題解決
五、實現
- 版本01代碼
1)、DistributedLock
package com.cn.pinliang.common.util;
import com.cn.pinliang.common.thread.PostponeTask;
import com.google.common.collect.Lists;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import redis.clients.jedis.Jedis;
import java.io.Serializable;
import java.util.Collections;
@Component
public class DistributedLock {
@Autowired
private RedisTemplate<Serializable, Object> redisTemplate;
private static final Long RELEASE_SUCCESS = 1L;
private static final String LOCK_SUCCESS = "OK";
private static final String SET_IF_NOT_EXIST = "NX";
private static final String SET_WITH_EXPIRE_TIME = "EX";
// 解鎖腳本(lua)
private static final String RELEASE_LOCK_SCRIPT = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
/**
* 分布式鎖
* @param key
* @param value
* @param expireTime 單位: 秒
* @return
*/
public boolean lock(String key, String value, long expireTime) {
return redisTemplate.execute((RedisCallback<Boolean>) redisConnection -> {
Jedis jedis = (Jedis) redisConnection.getNativeConnection();
String result = jedis.set(key, value, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
if (LOCK_SUCCESS.equals(result)) {
return Boolean.TRUE;
}
return Boolean.FALSE;
});
}
/**
* 解鎖
* @param key
* @param value
* @return
*/
public Boolean unLock(String key, String value) {
return redisTemplate.execute((RedisCallback<Boolean>) redisConnection -> {
Jedis jedis = (Jedis) redisConnection.getNativeConnection();
Object result = jedis.eval(RELEASE_LOCK_SCRIPT, Collections.singletonList(key), Collections.singletonList(value));
if (RELEASE_SUCCESS.equals(result)) {
return Boolean.TRUE;
}
return Boolean.FALSE;
});
}
}
說明: 就2個方法, 加鎖解鎖, 加鎖使用jedis setnx方法, 解鎖執行lua腳本, 都是原子性操作
2)、PcInformationServiceImpl
public JsonResult add() throws Exception {
String key = "add_information_lock";
String value = RandomUtil.produceStringAndNumber(10);
long expireTime = 10L;
boolean lock = distributedLock.lock(key, value, expireTime);
String threadName = Thread.currentThread().getName();
if (lock) {
System.out.println(threadName + " 獲得鎖...............................");
Thread.sleep(30000);
distributedLock.unLock(key, value);
System.out.println(threadName + " 解鎖了...............................");
} else {
System.out.println(threadName + " 未獲取到鎖...............................");
return JsonResult.fail("未獲取到鎖");
}
return JsonResult.succeed();
}
說明: 測試類很簡單, value隨機生成, 保證唯一, 不會在超時情況下解鎖其他客戶端持有的鎖
3)、打開redis-desktop-manager客戶端, 刷新緩存, 可以看到, 此時是沒有add_information_lock
的key的
4)、啟動jmeter, 調用接口測試
設置5個線程同時訪問, 在10s的超時時間內查看redis, add_information_lock
存在, 多次調接口, 只有一個線程能夠獲取到鎖
redis
1-4個請求, 都未獲取到鎖
第5個請求, 獲取到鎖
OK, 目前為止, 一切正常, 接下來測試10s之后, A仍在執行業務邏輯, 看別的線程是否能獲取到鎖
可以看到, 操作成功, 說明A和B同時執行了這段本應該獨享的代碼, 需要優化
- 版本02代碼
1)、DistributedLock
package com.cn.pinliang.common.util;
import com.cn.pinliang.common.thread.PostponeTask;
import com.google.common.collect.Lists;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import redis.clients.jedis.Jedis;
import java.io.Serializable;
import java.util.Collections;
@Component
public class DistributedLock {
@Autowired
private RedisTemplate<Serializable, Object> redisTemplate;
private static final Long RELEASE_SUCCESS = 1L;
private static final Long POSTPONE_SUCCESS = 1L;
private static final String LOCK_SUCCESS = "OK";
private static final String SET_IF_NOT_EXIST = "NX";
private static final String SET_WITH_EXPIRE_TIME = "EX";
// 解鎖腳本(lua)
private static final String RELEASE_LOCK_SCRIPT = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
// 延時腳本
private static final String POSTPONE_LOCK_SCRIPT = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('expire', KEYS[1], ARGV[2]) else return '0' end";
/**
* 分布式鎖
* @param key
* @param value
* @param expireTime 單位: 秒
* @return
*/
public boolean lock(String key, String value, long expireTime) {
// 加鎖
Boolean locked = redisTemplate.execute((RedisCallback<Boolean>) redisConnection -> {
Jedis jedis = (Jedis) redisConnection.getNativeConnection();
String result = jedis.set(key, value, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
if (LOCK_SUCCESS.equals(result)) {
return Boolean.TRUE;
}
return Boolean.FALSE;
});
if (locked) {
// 加鎖成功, 啟動一個延時線程, 防止業務邏輯未執行完畢就因鎖超時而使鎖釋放
PostponeTask postponeTask = new PostponeTask(key, value, expireTime, this);
Thread thread = new Thread(postponeTask);
thread.setDaemon(Boolean.TRUE);
thread.start();
}
return locked;
}
/**
* 解鎖
* @param key
* @param value
* @return
*/
public Boolean unLock(String key, String value) {
return redisTemplate.execute((RedisCallback<Boolean>) redisConnection -> {
Jedis jedis = (Jedis) redisConnection.getNativeConnection();
Object result = jedis.eval(RELEASE_LOCK_SCRIPT, Collections.singletonList(key), Collections.singletonList(value));
if (RELEASE_SUCCESS.equals(result)) {
return Boolean.TRUE;
}
return Boolean.FALSE;
});
}
/**
* 鎖延時
* @param key
* @param value
* @param expireTime
* @return
*/
public Boolean postpone(String key, String value, long expireTime) {
return redisTemplate.execute((RedisCallback<Boolean>) redisConnection -> {
Jedis jedis = (Jedis) redisConnection.getNativeConnection();
Object result = jedis.eval(POSTPONE_LOCK_SCRIPT, Lists.newArrayList(key), Lists.newArrayList(value, String.valueOf(expireTime)));
if (POSTPONE_SUCCESS.equals(result)) {
return Boolean.TRUE;
}
return Boolean.FALSE;
});
}
}
說明: 新增了鎖延時方法, lua腳本, 自行腦補相關語法
2)、PcInformationServiceImpl不需要改動
3)、PostponeTask
package com.cn.pinliang.common.thread;
import com.cn.pinliang.common.util.DistributedLock;
public class PostponeTask implements Runnable {
private String key;
private String value;
private long expireTime;
private boolean isRunning;
private DistributedLock distributedLock;
public PostponeTask() {
}
public PostponeTask(String key, String value, long expireTime, DistributedLock distributedLock) {
this.key = key;
this.value = value;
this.expireTime = expireTime;
this.isRunning = Boolean.TRUE;
this.distributedLock = distributedLock;
}
@Override
public void run() {
long waitTime = expireTime * 1000 * 2 / 3;// 線程等待多長時間后執行
while (isRunning) {
try {
Thread.sleep(waitTime);
if (distributedLock.postpone(key, value, expireTime)) {
System.out.println("延時成功...........................................................");
} else {
this.stop();
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
private void stop() {
this.isRunning = Boolean.FALSE;
}
}
說明: 調用lock同時, 立即開啟PostponeTask線程, 線程等待超時時間的2/3時間后, 開始執行鎖延時代碼, 如果延時成功, add_information_lock
這個key會一直存在於redis服務端, 直到業務邏輯執行完畢, 因此在此過程中, 其他線程無法獲取到鎖, 也即保證了線程安全性
下面是測試結果
10s后, 查看redis服務端, add_information_lock
仍存在, 說明延時成功
此時用postman再次請求, 發現獲取不到鎖
看一下控制台打印
A線程在19:09:11獲取到鎖, 在10 * 2 / 3 = 6s后進行延時, 成功, 保證了業務邏輯未執行完畢的情況下不會釋放鎖
A線程執行完畢, 鎖釋放, 其他線程又可以競爭鎖
OK, 目前為止, 解決了鎖超時而業務邏輯仍在執行的鎖沖突問題, 還很簡陋, 而最嚴謹的方式還是使用官方的 Redlock 算法實現, 其中 Java 包推薦使用 redisson, 思路差不多其實, 都是在快要超時時續期, 以保證業務邏輯未執行完畢不會有其他客戶端持有鎖
后面學習redisson, 看一下大神是怎么實現的
如果有什么不對的或者可以優化的希望小伙伴多多指教, 留言評論什么的, 謝謝
參考文章: https://blog.csdn.net/weixin_33943347/article/details/88009397