Redis分散式鎖
阿新 • • 發佈:2018-12-09
前言
1、Jedis分散式鎖
1.1、鎖的工具類
package com.hlj.redis.lock.utils;
import redis.clients.jedis.Jedis;
import java.util.Collections;
/**
* @Desc:
* @Author HealerJean
* @Date 2018/9/13 上午11:31.
*/
public class RedisTool {
private static final String LOCK_SUCCESS = "OK";
private static final String SET_IF_NOT_EXIST = "NX" ;
private static final String SET_WITH_EXPIRE_TIME = "PX";
private static final Long RELEASE_SUCCESS = 1L;
/**
* 嘗試獲取分散式鎖
*
* @param jedis Redis客戶端
* @param lockKey 鎖
* @param requestId 請求標識
* @param expireTime 超期時間
* @return 是否獲取成功
* 第一個為key,我們使用key來當鎖,因為key是唯一的。
第二個為value,我們傳的是requestId,
很多童鞋可能不明白,有key作為鎖不就夠了嗎,為什麼還要用到value?原因就是我們在上面講到可靠性時,分散式鎖要滿足第四個條件解鈴還須繫鈴人,通過給value賦值為requestId,我們就知道這把鎖是哪個請求加的了,在解鎖的時候就可以有依據。requestId可以使用UUID.randomUUID().toString()方法生成。
第三個為nxxx,這個引數我們填的是NX,意思是SET IF NOT EXIST,即當key不存在時,我們進行set操作;若key已經存在,則不做任何操作;
第四個為expx,這個引數我們傳的是PX,意思是我們要給這個key加一個過期的設定,具體時間由第五個引數決定。
第五個為time,與第四個引數相呼應,代表key的過期時間。
*/
public static boolean tryGetDistributedLock(Jedis jedis, String lockKey, String requestId, int expireTime) {
String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
if (LOCK_SUCCESS.equals(result)) {
return true;
}
return false;
}
/**
* 釋放分散式鎖
*
* @param jedis Redis客戶端
* @param lockKey 鎖
* @param requestId 請求標識
* @return 是否釋放成功
* 那麼這段Lua程式碼的功能是什麼呢?其實很簡單,首先獲取鎖對應的value值,檢查是否與requestId相等,如果相等則刪除鎖(解鎖)。那麼為什麼要使用Lua語言來實現呢?因為要確保上述操作是原子性的。關於非原子性會帶來什麼問題,可以閱讀【解鎖程式碼-錯誤示例2】 。那麼為什麼執行eval()方法可以確保原子性,源於Redis的特性,下面是官網對eval命令的部分解釋:
*/
public static boolean releaseDistributedLock(Jedis jedis, String lockKey, String requestId) {
// 第一行程式碼,我們寫了一個簡單的Lua指令碼程式碼
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
// 引數KEYS[1]賦值為lockKey,ARGV[1]賦值為requestId。eval()方法是將Lua程式碼交給Redis服務端執行。
Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));
if (RELEASE_SUCCESS.equals(result)) {
return true;
}
return false;
}
}
1.2、進行測試
package com.hlj.redis.lock.service;
import com.hlj.redis.lock.utils.RedisTool;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
/**
* @Desc:
* @Author HealerJean
* @Date 2018/9/13 上午11:32.
*/
public class RedisLockConsumerService {
//庫存個數
static int goodsCount = 10;
//賣出個數
static int saleCount = 0;
public static void main(String[] args) {
JedisPool jedisPool = new JedisPool(new JedisPoolConfig(), "127.0.0.1", 6379, 1000);
for (int i = 0; i < 1000; i++) {
new Thread(() -> {
try {Thread.sleep(2);} catch (InterruptedException e) {}
Jedis jedis = jedisPool.getResource();
boolean lock = false;
while (!lock) {
lock = RedisTool.tryGetDistributedLock(jedis, "goodsCount", Thread.currentThread().getName(), 10);
}
if (lock) {
if (goodsCount > 0) {
goodsCount--;
System.out.println("剩餘庫存:" + goodsCount + " 賣出個數" + ++saleCount);
}
}
RedisTool.releaseDistributedLock(jedis, "goodsCount", Thread.currentThread().getName());
jedis.close();
}).start();
}
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
jedisPool.close();
}
}
2、redisTemplate鎖
2.1、定義鎖的介面
package com.hlj.redis.lock.utils;
/**
* redis鎖,原地址https://gitee.com/itopener/springboot/tree/master
*/
public interface DistributedLock {
public static final long TIMEOUT_MILLIS = 30000; //超時時間
public static final int RETRY_TIMES = Integer.MAX_VALUE; //重試次數
public static final long SLEEP_MILLIS = 500; //重試時 執行緒休眠次數
public boolean lock(String key);
public boolean lock(String key, int retryTimes);
public boolean lock(String key, int retryTimes, long sleepMillis);
public boolean lock(String key, long expire);
public boolean lock(String key, long expire, int retryTimes);
public boolean lock(String key, long expire, int retryTimes, long sleepMillis);
public boolean releaseLock(String key);
}
2、2、實現上述介面的抽象類
package com.hlj.redis.lock.utils;
/**
* redis鎖,原地址https://gitee.com/itopener/springboot/tree/master
*/
public abstract class AbstractDistributedLock implements DistributedLock {
@Override
public boolean lock(String key) {
return lock(key, TIMEOUT_MILLIS, RETRY_TIMES, SLEEP_MILLIS);
}
@Override
public boolean lock(String key, int retryTimes) {
return lock(key, TIMEOUT_MILLIS, retryTimes, SLEEP_MILLIS);
}
@Override
public boolean lock(String key, int retryTimes, long sleepMillis) {
return lock(key, TIMEOUT_MILLIS, retryTimes, sleepMillis);
}
@Override
public boolean lock(String key, long expire) {
return lock(key, expire, RETRY_TIMES, SLEEP_MILLIS);
}
@Override
public boolean lock(String key, long expire, int retryTimes) {
return lock(key, expire, retryTimes, SLEEP_MILLIS);
}
}
2.3、具體服務層實現分散式鎖
package com.hlj.redis.lock.utils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisCluster;
import redis.clients.jedis.JedisCommands;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
/**
* redis鎖,原地址https://gitee.com/itopener/springboot/tree/master
*/
@Service("redisDistributedLock")
public class RedisDistributedLock extends AbstractDistributedLock {
private final Logger logger = LoggerFactory.getLogger(RedisDistributedLock.class);
private RedisTemplate<Object, Object> redisTemplate;
private ThreadLocal<String> lockFlag = new ThreadLocal<String>();
public static final String UNLOCK_LUA;
static {
StringBuilder sb = new StringBuilder();
sb.append("if redis.call(\"get\",KEYS[1]) == ARGV[1] ");
sb.append("then ");
sb.append(" return redis.call(\"del\",KEYS[1]) ");
sb.append("else ");
sb.append(" return 0 ");
sb.append("end ");
UNLOCK_LUA = sb.toString();
}
public RedisDistributedLock(RedisTemplate<Object, Object> redisTemplate) {
super();
this.redisTemplate = redisTemplate;
}
@Override
public boolean lock(String key, long expire, int retryTimes, long sleepMillis) {
boolean result = setRedis(key, expire);
// 如果獲取鎖失敗,按照傳入的重試次數進行重試
while ((!result) && retryTimes-- > 0) {
try {
logger.debug("lock failed, retrying..." + retryTimes);
Thread.sleep(sleepMillis);
} catch (InterruptedException e) {
return false;
}
result = setRedis(key, expire);
}
return result;
}
private boolean setRedis(String key, long expire) {
try {
String result = redisTemplate.execute(new RedisCallback<String>() {
@Override
public String doInRedis(RedisConnection connection) throws DataAccessException {
JedisCommands commands = (JedisCommands) connection.getNativeConnection();
String uuid = UUID.randomUUID().toString();
lockFlag.set(uuid);
//PX millionSecond
return commands.set(key, uuid, "NX", "PX", expire);
}
});
return !StringUtils.isEmpty(result);
} catch (Exception e) {
logger.error("set redis occured an exception", e);
}
return false;
}
@Override
public boolean releaseLock(String key) {
// 釋放鎖的時候,有可能因為持鎖之後方法執行時間大於鎖的有效期,此時有可能已經被另外一個執行緒持有鎖,所以不能直接刪除
try {
List<String> keys = new ArrayList<String>();
keys.add(key);
List<String> args = new ArrayList<String>();
args.add(lockFlag.get());
// 使用lua指令碼刪除redis中匹配value的key,可以避免由於方法執行時間過長而redis鎖自動過期失效的時候誤刪其他執行緒的鎖
// spring自帶的執行指令碼方法中,叢集模式直接丟擲不支援執行指令碼的異常,所以只能拿到原redis的connection來執行指令碼
Long result = redisTemplate.execute(new RedisCallback<Long>() {
public Long doInRedis(RedisConnection connection) throws DataAccessException {
Object nativeConnection = connection.getNativeConnection();
// 叢集模式和單機模式雖然執行指令碼的方法一樣,但是沒有共同的介面,所以只能分開執行
// 叢集模式
if (nativeConnection instanceof JedisCluster) {
return (Long) ((JedisCluster) nativeConnection).eval(UNLOCK_LUA, keys, args);
}
// 單機模式
else if (nativeConnection instanceof Jedis) {
return (Long) ((Jedis) nativeConnection).eval(UNLOCK_LUA, keys, args);
}
return 0L;
}
});
return result != null && result > 0;
} catch (Exception e) {
logger.error("release lock occured an exception : key = {}", key, e);
} finally {
// 清除掉ThreadLocal中的資料,避免記憶體溢位
lockFlag.remove();
}
return false;
}
}
2.4、進行測試
package com.hlj.redis.lock;
import com.hlj.redis.lock.utils.DistributedLock;
import com.hlj.redis.lock.utils.RedisTool;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.annotation.Resource;
import java.util.Date;
/**
* @Desc:
* @Author HealerJean
* @Date 2018/9/13 下午12:04.
*/
@RequestMapping("redis/lock")
@Controller
public class LockController {
//庫存個數
int goodsCount = 10;
//賣出個數
int saleCount = 0;
/**
* 快取key-使用者體力鎖
*/
public static final String TEST_LOCK = "test_lock:";
@Resource
private DistributedLock lock;
@GetMapping("test")
@ResponseBody
public void lockRedis(){
for (int i = 0; i < 1000; i++) {
new Thread(() -> {
try {Thread.sleep(2);} catch (InterruptedException e) {}
if (lock.lock(TEST_LOCK , 3000l, 5, 500)) {
if (goodsCount > 0) {
goodsCount--;
System.out.println("剩餘庫存:" + goodsCount + " 賣出個數" + ++saleCount);
}
}
lock.releaseLock(TEST_LOCK);
}).start();
}
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
2.5、測試結果
剩餘庫存:9 賣出個數1
剩餘庫存:8 賣出個數2
剩餘庫存:7 賣出個數3
剩餘庫存:6 賣出個數4
剩餘庫存:5 賣出個數5
剩餘庫存:4 賣出個數6
剩餘庫存:3 賣出個數7
剩餘庫存:2 賣出個數8
剩餘庫存:1 賣出個數9
剩餘庫存:0 賣出個數10
如果滿意,請打賞博主任意金額,感興趣的在微信轉賬的時候,新增博主微信哦, 請下方留言吧。可與博主自由討論哦
支付包 | 微信 | 微信公眾號 |
---|---|---|