關於redis分佈式鎖, 查了不少資料, 發現不少只是實現了最基礎的功能, 可是, 並無解決當鎖已超時而業務邏輯還未執行完的問題, 這樣會致使: A線程超時時間設爲10s(爲了解決死鎖問題), 但代碼執行時間可能須要30s, 而後redis服務端10s後將鎖刪除, 此時, B線程剛好申請鎖, redis服務端不存在該鎖, 能夠申請, 也執行了代碼, 那麼問題來了, A、B線程都同時獲取到鎖並執行業務邏輯, 這與分佈式鎖最基本的性質相違背: 在任意一個時刻, 只有一個客戶端持有鎖, 即獨享java
爲了解決這個問題, 本文將用完整的代碼和測試用例進行驗證, 但願能給小夥伴帶來一點幫助git
壓測工具jmeter
下載連接
提取碼: 8f2agithub
redis-desktop-manager客戶端
下載連接
提取碼: 9bhfredis
postman
下載連接
提取碼: vfu7算法
也能夠直接官網下載, 我這邊都整理到網盤了spring
須要postman是由於我還沒找到jmeter多開窗口的辦法, 哈哈緩存
springmvc項目安全
maven依賴mvc
<!--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>
分佈式鎖工具類: DistributedLockdom
測試接口類: PcInformationServiceImpl
鎖延時守護線程類: PostponeTask
先測試在不開啓鎖延時線程的狀況下, A線程超時時間設爲10s, 執行業務邏輯時間設爲30s, 10s後, 調用接口, 查看是否可以獲取到鎖, 若是獲取到, 說明存在線程安全性問題
同上, 在加鎖的同時, 開啓鎖延時線程, 調用接口, 查看是否可以獲取到鎖, 若是獲取不到, 說明延時成功, 安全性問題解決
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同時執行了這段本應該獨享的代碼, 須要優化
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, 看一下大神是怎麼實現的
若是有什麼不對的或者能夠優化的但願小夥伴多多指教, 留言評論什麼的, 謝謝