非SpringBoot項目
基於jedis
package com.blog.www.util.lock;
import lombok.extern.slf4j.Slf4j;
import redis.clients.jedis.Jedis;
import java.util.Collections;
import java.util.UUID;
/**
* Redis實現分佈式鎖,基於 jedis
* <br/>
* 請使用最新實現 {@link RedisLock}
* <br/>
* 分佈式鎖一般有三種實現方式:1. 數據庫樂觀鎖;2. 基於Redis的分佈式鎖;3. 基於ZooKeeper的分佈式鎖。<br/>
* 可靠性:<br/>
* 爲了確保分佈式鎖可用,我們至少要確保鎖的實現同時滿足以下四個條件:<br/>
* <ul>
* <li>
* 1. 互斥性。在任意時刻,只有一個客戶端能持有鎖。
* </li>
* <li>
* 2. 不會發生死鎖。即使有一個客戶端在持有鎖的期間崩潰而沒有主動解鎖,也能保證後續其他客戶端能加鎖。
* </li>
* <li>
* 3. 具有容錯性。只要大部分的Redis節點正常運行,客戶端就可以加鎖和解鎖。
* </li>
* <li>
* 4. 解鈴還須繫鈴人。加鎖和解鎖必須是同一個客戶端,客戶端自己不能把別人加的鎖給解了。
* </li>
* </ul>
* 參考:<br/>
* <ul>
* <li>
* <a href='https://www.cnblogs.com/linjiqin/p/8003838.html'>Redis分佈式鎖的正確實現方式</a>
* </li>
* <li>
* <a href='https://www.cnblogs.com/cmyxn/p/9047848.html'>什麼是分佈式鎖及正確使用redis實現分佈式鎖</a>
* </li>
* <li>
* <a href='https://blog.csdn.net/crystalqy/article/details/89024653'>基於Spring boot 2.1 使用redisson實現分佈式鎖</a>
* </li>
* </ul>
*
* @author :leigq
* @date :2019/7/2 11:22
*/
@Slf4j
@Deprecated
public class RedisLockForJedis {
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 請求標識 可以使用UUID.randomUUID().toString()方法生成
* @param expireTime 超期時間 單位毫秒
* @return 是否獲取成功
*/
public static boolean lock(Jedis jedis, String lockKey, String requestId, int expireTime) {
String result = jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);
return LOCK_SUCCESS.equals(result);
}
/**
* 釋放分佈式鎖
*
* @param jedis Redis客戶端
* @param lockKey 鎖
* @param requestId 請求標識 可以使用UUID.randomUUID().toString()方法生成
* @return 是否釋放成功
*/
public static boolean unLock(Jedis jedis, String lockKey, String requestId) {
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));
return RELEASE_SUCCESS.equals(result);
}
/**
* 測試加鎖解鎖(測試通過)
*/
public static void main(String[] args) {
String requestId = UUID.randomUUID().toString();
/* 單機 jedis連接使用參考:https://blog.csdn.net/qianqian666888/article/details/79087930*/
Jedis jedis = new Jedis("127.0.0.1", 6379);
// 設置密碼
jedis.auth("111111");
// 加鎖
boolean locked = lock(jedis, "lockKey", requestId, 60 * 60 * 1000);
log.warn("locked result is : [{}]", locked);
// 解鎖
boolean unLocked = unLock(jedis, "lockKey", requestId);
log.warn("unLocked result is : [{}]", unLocked);
}
}
SpringBoot項目
客戶端選用 jedis
或 Lettuce
均可
package com.blog.www.util.lock;
import com.blog.www.config.RedisConfig;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.stereotype.Service;
import java.util.Collections;
import java.util.Objects;
/**
* Redis實現分佈式鎖,基於 RedisTemplate
* <br/>
* jedis 實現請看:<a href='https://blog.csdn.net/qq_28397259/article/details/80839072'>基於redisTemplate的redis的分佈式鎖正確打開方式</a>
* <br/>
* 分佈式鎖一般有三種實現方式:1. 數據庫樂觀鎖;2. 基於Redis的分佈式鎖;3. 基於ZooKeeper的分佈式鎖。<br/>
* 可靠性:<br/>
* 爲了確保分佈式鎖可用,我們至少要確保鎖的實現同時滿足以下四個條件:<br/>
* <ul>
* <li>
* 1. 互斥性。在任意時刻,只有一個客戶端能持有鎖。
* </li>
* <li>
* 2. 不會發生死鎖。即使有一個客戶端在持有鎖的期間崩潰而沒有主動解鎖,也能保證後續其他客戶端能加鎖。
* </li>
* <li>
* 3. 具有容錯性。只要大部分的Redis節點正常運行,客戶端就可以加鎖和解鎖。
* </li>
* <li>
* 4. 解鈴還須繫鈴人。加鎖和解鎖必須是同一個客戶端,客戶端自己不能把別人加的鎖給解了。
* </li>
* </ul>
* 參考:<br/>
* <ul>
* <li>
* <a href='https://www.cnblogs.com/linjiqin/p/8003838.html'>Redis分佈式鎖的正確實現方式</a>
* </li>
* <li>
* <a href='https://blog.csdn.net/long2010110/article/details/82911168'>springboot的RedisTemplate實現分佈式鎖</a>
* </li>
* </ul>
* <a href='https://blog.csdn.net/weixin_38399962/article/details/82753763'>使用示例參考</a>:
* <pre>
* {@link @Autowired}
* private {@link RedisLock} redisLock;
*
* boolean locked = redisLock.lock(lockKey, requestId, expireTime);
* if (locked) {
* // 執行邏輯操作
* ......
* ......
* redisLock.unLock(lockKey, requestId);
* } else {
* // 設置失敗次數計數器, 當到達5次時, 返回失敗
* int failCount = 1;
* while(failCount <= 5){
* // 等待100ms重試
* try {
* Thread.sleep(100l);
* } catch (InterruptedException e) {
* e.printStackTrace();
* }
* if (redisLock.lock(lockKey, requestId, expireTime)){
* // 執行邏輯操作
* ......
* ......
* redisLock.unLock(lockKey, requestId);
* }else{
* failCount ++;
* }
* }
* throw new RuntimeException("現在創建的人太多了, 請稍等再試");
* }
* </pre>
*
* @author :leigq
* @date :2019/7/2 11:22
*/
@Slf4j
@Service
@SuppressWarnings(value = "unchecked")
public final class RedisLock {
private final RedisTemplate redisTemp;
/**
* 使用 RedisConfig 中的 redisTemp,自定義序列化 及 兼容 java8 時間
* @see RedisConfig#getRedisTemplate(RedisConnectionFactory)
*/
public RedisLock(@Qualifier("redisTemp") RedisTemplate redisTemp) {
this.redisTemp = redisTemp;
}
/**
* 嘗試獲取分佈式鎖
*
* @param lockKey 鎖key
* @param requestId 請求標識 可以使用UUID.randomUUID().toString()方法生成
* @param expireTime 超期時間 單位秒
* @return 是否獲取成功
*/
public boolean lock(String lockKey, String requestId, int expireTime) {
// 使用腳本,保證原子性
RedisScript redisScript = RedisScript.of(LOCK_LUA, Long.class);
Object lockResult = redisTemp.execute(redisScript, Collections.singletonList(lockKey), requestId, expireTime);
log.warn("lock executeResult is [{}]", lockResult);
return Objects.equals(SUCCESS, lockResult);
// 不符合原子性
// Boolean setIfAbsentResult = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, requestId);
// Boolean setExpireResult = stringRedisTemplate.expire(lockKey, expireTime, TimeUnit.SECONDS);
// return setIfAbsentResult && setExpireResult;
}
/**
* 釋放分佈式鎖
*
* @param lockKey 鎖key
* @param requestId 請求標識 可以使用UUID.randomUUID().toString()方法生成
* @return 是否釋放成功
*/
public boolean unLock(String lockKey, String requestId) {
RedisScript redisScript = RedisScript.of(UNLOCK_LUA, Long.class);
Object unLockResult = redisTemp.execute(redisScript, Collections.singletonList(lockKey), requestId);
log.warn("unLock executeResult is [{}]", unLockResult);
return Objects.equals(SUCCESS, unLockResult);
}
private static final Long SUCCESS = 1L;
// 加鎖 Lua 腳本
private static final String LOCK_LUA;
// 解鎖 Lua 腳本
private static final String UNLOCK_LUA;
static {
// if redis.call('setNx', KEYS[1], ARGV[1]) then if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('expire', KEYS[1], ARGV[2]) else return 0 end end
LOCK_LUA = "if redis.call('setNx', KEYS[1], ARGV[1]) " +
"then " +
" if redis.call('get', KEYS[1]) == ARGV[1] " +
" then " +
" return redis.call('expire', KEYS[1], ARGV[2]) " +
" else " +
" return 0 " +
" end " +
"end ";
// if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end
UNLOCK_LUA = "if redis.call('get', KEYS[1]) == ARGV[1] " +
"then " +
" return redis.call('del', KEYS[1]) " +
"else " +
" return 0 " +
"end ";
}
}
redisTemp
請在這獲取:SpringBoot2.0.X配置Redis
測試
package com.blog.www.util.lock;
import com.blog.www.base.BaseApplicationTests;
import org.junit.Test;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.UUID;
/**
* RedisLock Tester.
*
* @author leigq
* @version 1.0
* @since <pre>10/17/2019</pre>
*/
public class RedisLockTest extends BaseApplicationTests {
@Autowired
private RedisLock redisLock;
/**
* Redis分佈式鎖測試,基於RedisTemplate,測試通過
*/
@Test
public void testLockAndUnLock() throws Exception {
String requestId = UUID.randomUUID().toString();
// 加鎖
boolean locked = redisLock.lock("lockKey", requestId, 40);
log.warn("locked result is : [{}]", locked);
// 解鎖
boolean unLocked = redisLock.unLock("lockKey", requestId);
log.warn("unLocked result is : [{}]", unLocked);
}
}
BaseApplicationTests.java
package com.blog.www.base;
import org.junit.After;
import org.junit.Before;
import org.junit.runner.RunWith;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
/**
* 測試基類,其他類繼承此類
* <br/>
* @author :leigq
* @date :2019/8/13 17:17
*/
@RunWith(SpringRunner.class)
@SpringBootTest
public abstract class BaseApplicationTests {
protected Logger log = LoggerFactory.getLogger(this.getClass());
private Long time;
@Before
public void setUp() {
this.time = System.currentTimeMillis();
log.info("==> 測試開始執行 <==");
}
@After
public void tearDown() {
log.info("==> 測試執行完成,耗時:{} ms <==", System.currentTimeMillis() - this.time);
}
}
測試結果如下: