這一篇文章拖了有點久,雖然在項目中使用分散式鎖的頻率比較高,但整理成文章發佈出來還是花了一點時間。在一些移動端、用戶量大的互聯網項目中,經常會使用到 Redis 分散式鎖作為控制訪問高併發的工具。 ...
目錄
前言
這一篇文章拖了有點久,雖然在項目中使用分散式鎖的頻率比較高,但整理成文章發佈出來還是花了一點時間。在一些移動端、用戶量大的互聯網項目中,經常會使用到 Redis 分散式鎖作為控制訪問高併發的工具。
一、關於分散式鎖
總結:分散式鎖是一種在分散式系統中用於控制併發訪問的機制。
在分散式系統中,多個客戶端同時對一個資源進行操作時,容易影響數據的一致性。分散式鎖的主要作用就是確保同一時刻只有一個客戶端能夠對某個資源進行操作,以避免數據不一致的問題。
主要應用場景:
- 資料庫併發控制:在分散式資料庫中,多個線程同時對某張表進行操作時,可能會出現併發衝突問題,使用分散式鎖可以確保同一時刻只有一個線程能夠對該表進行操作,避免併發衝突。
- 分散式緩存:在分散式緩存中,如果多個線程同時對某個緩存進行操作,可能會出現緩存數據不一致的問題。使用分散式鎖可以確保同一時刻只有一個線程能夠對該緩存進行操作,保證緩存數據的一致性。
- 分散式任務調度:在分散式任務調度中,多個線程同時執行某個任務,可能出現任務被重覆執行的問題,使用分散式鎖可以確保同一時刻只有一個線程能夠執行該任務,避免任務被重覆執行。
目前主流的分散式鎖實現方案是基於 Redis 來實現的,今天要分享的有 2 種實現: 基於 RedLock 紅鎖和基於 setIfAbsent() 方法。
二、RedLock 紅鎖(不推薦)
RedLock 對於多節點(集群)的分散式鎖演算法使用了多個實例來存儲鎖信息,這種方式可以提高獲取鎖的速度和成功率,從而可以有效地防止單點故障;
但由於 RedLock 的實現比較複雜,且容易因為配置不正確而導致鎖無法獲取。此外,如果 Redis 服務宕機,也會導致鎖無法正常使用。
RedLock 會對集群的每個節點進行加鎖,如果大多數(N/2+1)加鎖成功了,則認為獲取鎖成功。這個過程中可能會因為網路問題,或節點超時的問題,影響加鎖的性能,故而在最新的 Redisson 版本中中已經正式宣佈廢棄 RedLock。
以下是一個簡易的 demo 實現:
包括兩部分:暴露給業務系統邏輯層使用的靜態方法、鎖的底層實現。思路用代碼和註釋說得比較清楚了,大家可以看一下:
/**
* 嘗試獲取鎖,業務系統用
* @param key key
* @param requestId 唯一請求標識,用於解鎖
* @param expireTime 過期時間
* @param timeUnit 過期時間單位
* @return
*/
public static boolean getLock(String key, String requestId, long expireTime, TimeUnit timeUnit) {
RedisSetArgs redisSetArgs = RedisSetArgs.instance().nx().px((int) timeUnit.toMillis(expireTime));
//CacheFactory 為緩存的抽象類,set() 為具體實現
String result = CacheFactory.getCache().set(key, requestId, redisSetArgs);
if (LOCK_SUCCESS.equals(result)) {
return true;
}
return false;
}
/**
* 具體的底層實現
* @param key key
* @param value value
* @param redisSetArgs set 參數對象
* @return 返回值
*/
@Override
public String set(String key, String value, RedisSetArgs redisSetArgs) {
//這裡引入的是 Jedis 的客戶端,後來被拋棄了,Redis 推薦的是 Redission
try (Jedis jedis = getJedis()) {
SetParams setParams = new SetParams();
if (redisSetArgs.isNx()) {
setParams.nx();
} else if (redisSetArgs.isXx()) {
setParams.xx();
}
if (Objects.nonNull(redisSetArgs.getEx())) {
setParams.ex(redisSetArgs.getEx());
} else if (Objects.nonNull(redisSetArgs.getPx())) {
setParams.px(redisSetArgs.getPx());
}
return jedis.set(SafeEncoder.encode(buildKey(key)), SafeEncoder.encode(value), setParams);
}
}
/**
* 解鎖,業務系統用
* @param key key
* @param requestId 唯一請求標識
* @return
*/
public static boolean unlock(String key, String requestId) {
//使用 Lua 腳本保證原子性:RELEASE_LOCK_LUA_SCRIPT = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
Object result = CacheFactory.getCache().loose(RELEASE_LOCK_LUA_SCRIPT, Collections.singletonList(key), Collections.singletonList(requestId));
if (NumberUtils.LONG_ONE.equals(result)) {
return true;
}
return false;
}
/**
* 執行解鎖腳本(底層實現)
* @param script 腳本
* @param keys keys
* @param args 參數
* @return 返回對象
*/
@Override
public Object loose(String script, List<String> keys, List<String> args) {
try (Jedis jedis = getJedis()) {
return jedis.eval(SafeEncoder.encode(script), keys.stream().map(this::buildKey).map(SafeEncoder::encode).collect(Collectors.toList()),
args.stream().map(SafeEncoder::encode).collect(Collectors.toList()));
}
}
三、基於 setIfAbsent() 方法
以下推薦的是在分散式集群環境中的最佳實踐,其實無論是單機還是集群,保證原子性都是第一位的,如果能同時保證性能和高可用,那麼就是一個可靠的分散式鎖解決方案。
主要思路是:設置鎖時,使用 setIfAbsent() 方法,因為其底層實際包含了 setnx 、expire 的功能,起到了原子操作的效果。給 key 設置隨機且唯一的值,並且只有在 key 不存在時才設置成功返回 True,並且設置 key 的過期時間(最好是毫秒級別)。
以下同樣給出一個簡單的示例,包括兩部分:暴露給業務系統邏輯層使用的靜態方法、鎖的底層實現。註釋寫得比較清楚了:
/**
* 獲取鎖,業務系統用
* @return 解鎖唯一標識
*/
public String getLock() {
try {
// 獲取鎖的超時時間,超過這個時間則取鎖失敗
long end = System.currentTimeMillis() + acquireTimeout;
// 隨機生成一個 value 作為解鎖的唯一標識
this.unLockIdentify = UUID.randomUUID().toString();
while (System.currentTimeMillis() < end) {
Boolean result = iCache.setIfAbsent(lockKey, this.unLockIdentify, Duration.ofMillis(expireTime));
if (result) {
return this.unLockIdentify;
}
try {
//再休眠 100 微秒
TimeUnit.MICROSECONDS.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
} catch (Exception e) {
logger.error("error info", e);
}
return null;
}
/**
* 具體的 setIfAbsent() 底層實現
* @param key 鍵
* @param value 值
* @param timeout 超時時間
* @return 是否設置成功
*/
public Boolean setIfAbsent(String key, String value, Duration timeout) {
return this.redisTemplate.opsForValue().setIfAbsent(key, value, timeout);
}
/**
* 釋放鎖,業務系統用
* @param unLockIdentify 解鎖唯一標識
* @return 是否解鎖成功
*/
public Boolean loose(String unLockIdentify) {
if (unLockIdentify == null) {
return Boiolfalse;
}
try {
if (iCache.deleteIfEquals(lockKey, unLockIdentify)) {
return Boolean.TRUE;
}
} catch (Exception e) {
logger.error("error info", e);
}
return Boolean.FALSE;
}
/**
* 具體判斷方法實現(底層實現)
* @param key 鍵
* @param expectedValue 期望的值
* @return 是否相等
*/
public Boolean deleteIfEquals(String key, String expectedValue) {
//Lua 腳本保證原子性
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
RedisScript<Long> redisScript = new DefaultRedisScript<>(script, Long.class);
Long result = redisTemplate.execute(redisScript, Collections.singletonList(key), expectedValue);
return result != null && result == 1;
}
四、使用示例
下麵分別給出兩個使用示例分別來介紹怎麼在具體的業務場景中去使用的 demo,一般來說針對資料庫的併發操作和多線程的併發任務操作,會使用得比較多。
至於為什麼不使用分散式鎖去保證緩存數據的一致性,其實是有專門的分散式緩存方案的:https://www.cnblogs.com/CodeBlogMan/p/18022719
4.1RedLock 使用
@Test
public void testRedLock(){
final String requestId = UUIDUtils.generateUUID();
if (RedisRedLock.attemptLock("xxxSys.insert.xxxId(唯一)", requestId, 3L, TimeUnit.SECONDS)) {
try {
//todo: 資料庫併發插入操作
log.info("併發插入成功!");
} catch (Throwable e) {
//底層沒有加 try catch,所以這裡加一下
log.error("併發插入失敗! error", e);
} finally {
RedisRedLock.unlock("xxxSys.insert.xxxId(唯一)", requestId);
}
}
}
4.2setIfAbsent() 方法使用
@Test
public void testDistributedLock(){
//這裡是抽像類和介面,具體使用可以更加靈活
DistributedLock distributedLock = CacheFactory.getDistributedLock("xxxSys.insert.xxxId(唯一)" ,3,1);
Assert.hasText(distributedLock.getLock(), "操作頻繁,請稍後重試");
//todo: 多線程的併發任務操作
if (distributedLock.loose("xxxSys.insert.xxxId(唯一)")){
//這裡是為了演示才加的日誌,其實底層已經加過了
log.info("釋放鎖成功!");
}
}
五、文章小結
到這裡基於 Redis 實現分散式鎖的全過程就分享完了,其實基於 Redis 實現分散式鎖還有許多底層和實際應用的情況沒有展開來說。目前筆者雖然在日常項目里有較多使用,但還是感到技術的海洋深不見底:學到的越多就感覺到自己的不足越多。
最後,如果文章有不足和錯誤,還請大家指正。或者你有其它想說的,也歡迎大家在評論區里交流!