1.前言
Redis實(shí)現(xiàn)分布式鎖,本身比較簡(jiǎn)單弄息,就是Redis中一個(gè)簡(jiǎn)單的KEY痊班。一般都利用setnx(set if not exists)指令可以非常簡(jiǎn)單的實(shí)現(xiàn)加鎖,鎖用完后疑枯,再調(diào)用del指令釋放鎖辩块。要確保鎖可用,一般需要解決幾個(gè)問題:
- 不能出現(xiàn)死鎖情況荆永,一個(gè)獲得鎖的客戶端宕機(jī)或者異常后废亭,要保障其他客戶端也能獲得鎖。
- 應(yīng)用程序通過網(wǎng)絡(luò)與Redis交互具钥,為避免網(wǎng)絡(luò)延遲以及獲取鎖線程與其他線程不沖突豆村,需要保障鎖操作的原子性,既同一時(shí)間只有一個(gè)客戶端可用獲取到鎖骂删。
- 加鎖和解鎖的客戶端必須是同一個(gè)掌动,不能把其他客戶端加的鎖給解了。
- 考慮容錯(cuò)性宁玫,如果一個(gè)客戶端加鎖成功后粗恢,Redis集群Master宕掉并沒有及時(shí)同步,另外一個(gè)客戶端加鎖會(huì)立即成功欧瘪,避免同一把鎖被兩個(gè)客戶端持有眷射。
2.解決思路
- 死鎖問題,通常是在拿到鎖后給鎖設(shè)置一個(gè)過期時(shí)間(expire指令)佛掖,即使出現(xiàn)異常妖碉,在過期時(shí)間后,鎖也會(huì)自動(dòng)釋放
- 原子性問題通常的兩個(gè)解決方式:
- 通過redis2.8版本后加入的set擴(kuò)展參數(shù)芥被,將加鎖和設(shè)置過期時(shí)間作為一個(gè)原子操作欧宜,目前發(fā)現(xiàn)不是所有Java的Redis客戶端都支持這樣的set指令
set lock:test true ex 5 nx
- LUA腳本,Redis Lua腳本可以保證多條指令的原子性執(zhí)行
- 釋放其他客戶端鎖拴魄,通過在加鎖的時(shí)候指定隨機(jī)值冗茸,在解鎖的時(shí)候用這個(gè)隨機(jī)值去匹配,匹配成功則解鎖匹中,匹配失敗就不能解鎖夏漱,因?yàn)殒i可能已經(jīng)過期或者已經(jīng)被其他客戶端占用
- Redis集群宕掉的極端情況下,可以考慮redlock算法职员,但是算法本身復(fù)雜,而且?guī)硪恍┬阅軗p耗跛溉,可以根據(jù)實(shí)際場(chǎng)景判斷焊切,是否非常在乎這樣的高可用
3.SpringBoot實(shí)現(xiàn)
3.1 LUA腳本
本實(shí)現(xiàn)基于SpringBoot2x扮授,考慮SpringBoot2x中Redis的默認(rèn)連接是由lettuce提供,不是常用的Jedis专肪,同時(shí)考慮不同版本的Redis刹勃,加鎖和解鎖都采用LUA腳本。
-- 加鎖腳本嚎尤,其中KEYS[]為外部傳入?yún)?shù)
-- KEYS[1]表示key
-- KEYS[2]表示value
-- KEYS[3]表示過期時(shí)間
if redis.call("setnx", KEYS[1], KEYS[2]) == 1 then
return redis.call("pexpire", KEYS[1], KEYS[3])
else
return 0
-- 解鎖腳本
-- KEYS[1]表示key
-- KEYS[2]表示value
-- return -1 表示未能獲取到key或者key的值與傳入的值不相等
if redis.call("get",KEYS[1]) == KEYS[2] then
return redis.call("del",KEYS[1])
else
return -1
3.2 加鎖代碼
依賴SpringBoot的RedisTemplate執(zhí)行LUA腳本荔仁,同時(shí)考慮重試機(jī)制
/**
* 加鎖
* @param key Key
* @param timeout 過期時(shí)間
* @param retryTimes 重試次數(shù)
* @return
*/
public boolean lock(String key, long timeout, int retryTimes) {
try {
final String redisKey = this.getRedisKey(key);
final String requestId = this.getRequestId();
logger.debug("lock :::: redisKey = " + redisKey + " requestid = " + requestId);
//組裝lua腳本參數(shù)
List<String> keys = Arrays.asList(redisKey, requestId, String.valueOf(timeout));
//執(zhí)行腳本
Long result = redisTemplate.execute(LOCK_LUA_SCRIPT, keys);
//存儲(chǔ)本地變量
if(!StringUtils.isEmpty(result) && result == LOCK_SUCCESS) {
localRequestIds.set(requestId);
localKeys.set(redisKey);
logger.info("success to acquire lock:" + Thread.currentThread().getName() + ", Status code reply:" + result);
return true;
} else if (retryTimes == 0) {
//重試次數(shù)為0直接返回失敗
return false;
} else {
//重試獲取鎖
logger.info("retry to acquire lock:" + Thread.currentThread().getName() + ", Status code reply:" + result);
int count = 0;
while(true) {
try {
//休眠一定時(shí)間后再獲取鎖,這里時(shí)間可以通過外部設(shè)置
Thread.sleep(100);
result = redisTemplate.execute(LOCK_LUA_SCRIPT, keys);
if(!StringUtils.isEmpty(result) && result == LOCK_SUCCESS) {
localRequestIds.set(requestId);
localKeys.set(redisKey);
logger.info("success to acquire lock:" + Thread.currentThread().getName() + ", Status code reply:" + result);
return true;
} else {
count++;
if (retryTimes == count) {
logger.info("fail to acquire lock for " + Thread.currentThread().getName() + ", Status code reply:" + result);
return false;
} else {
logger.warn(count + " times try to acquire lock for " + Thread.currentThread().getName() + ", Status code reply:" + result);
continue;
}
}
} catch (Exception e) {
logger.error("acquire redis occured an exception:" + Thread.currentThread().getName(), e);
break;
}
}
}
} catch (Exception e1) {
logger.error("acquire redis occured an exception:" + Thread.currentThread().getName(), e1);
}
return false;
}
- getRedisKey根據(jù)傳入的key加上一個(gè)前綴生成鎖的key
/** * 獲取RedisKey * @param key 原始KEY芽死,如果為空乏梁,自動(dòng)生成隨機(jī)KEY * @return */ private String getRedisKey(String key) { //如果Key為空且線程已經(jīng)保存,直接用关贵,異常保護(hù) if (StringUtils.isEmpty(key) && !StringUtils.isEmpty(localKeys.get())) { return localKeys.get(); } //如果都是空那就拋出異常 if (StringUtils.isEmpty(key) && StringUtils.isEmpty(localKeys.get())) { throw new RuntimeException("key is null"); } return LOCK_PREFIX + key; }
- getRequestId用于為每一個(gè)加鎖請(qǐng)求生成請(qǐng)求ID遇骑,內(nèi)部方法
/** * 獲取隨機(jī)請(qǐng)求ID * @return */ private String getRequestId() { return UUID.randomUUID().toString(); }
- redisTemplate.execute(LOCK_LUA_SCRIPT, keys),execute最終調(diào)用的RedisConnection的eval方法將LUA腳本交給Redis服務(wù)端執(zhí)行揖曾,可兼容springboot中不同redis客戶端實(shí)現(xiàn)(Jedis落萎、Lettuce等)。這個(gè)操作通過setnx設(shè)置鎖key炭剪,成功后設(shè)置鎖的有效期练链,成功返回1,失敗返回0奴拦,其中LOCK_LUA_SCRIPT為常量定義
//定義獲取鎖的lua腳本 private final static DefaultRedisScript<Long> LOCK_LUA_SCRIPT = new DefaultRedisScript<>( "if redis.call(\"setnx\", KEYS[1], KEYS[2]) == 1 then return redis.call(\"pexpire\", KEYS[1], KEYS[3]) else return 0 end" , Long.class );
- 根據(jù)腳本執(zhí)行情況媒鼓,將鎖的key和requestId分別存入線程本地變量localKeys和localRequestIds中,兩個(gè)都是ThreadLocal變量粱坤,通過兩個(gè)變量在釋放鎖的時(shí)候避免釋放其他客戶端占用的鎖隶糕。
- 根據(jù)重試次數(shù)retryTimes值進(jìn)行重試判斷,如果為0則不重試站玄,否則進(jìn)入重試邏輯枚驻。
3.3 解鎖代碼
/**
* 釋放KEY
* @param key
* @return
*/
public boolean unlock(String key) {
try {
String localKey = localKeys.get();
//如果本地線程沒有KEY,說明還沒加鎖株旷,不能釋放
if(StringUtils.isEmpty(localKey)) {
logger.error("release lock occured an error: lock key not found");
return false;
}
String redisKey = getRedisKey(key);
//判斷KEY是否正確再登,不能釋放其他線程的KEY
if(!StringUtils.isEmpty(localKey) && !localKey.equals(redisKey)) {
logger.error("release lock occured an error: illegal key:" + key);
return false;
}
//組裝lua腳本參數(shù)
List<String> keys = Arrays.asList(redisKey, localRequestIds.get());
logger.debug("unlock :::: redisKey = " + redisKey + " requestid = " + localRequestIds.get());
// 使用lua腳本刪除redis中匹配value的key,可以避免由于方法執(zhí)行時(shí)間過長而redis鎖自動(dòng)過期失效的時(shí)候誤刪其他線程的鎖
Long result = redisTemplate.execute(UNLOCK_LUA_SCRIPT, keys);
//如果這里拋異常晾剖,后續(xù)鎖無法釋放
if (!StringUtils.isEmpty(result) && result == RELEASE_SUCCESS) {
logger.info("release lock success:" + Thread.currentThread().getName() + ", Status code reply=" + result);
return true;
} else if (!StringUtils.isEmpty(result) && result == LOCK_EXPIRED) {
//返回-1說明獲取到的KEY值與requestId不一致或者KEY不存在锉矢,可能已經(jīng)過期或被其他線程加鎖
// 一般發(fā)生在key的過期時(shí)間短于業(yè)務(wù)處理時(shí)間,屬于正吵菥。可接受情況
logger.warn("release lock exception:" + Thread.currentThread().getName() + ", key has expired or released. Status code reply=" + result);
} else {
//其他情況沽损,一般是刪除KEY失敗,返回0
logger.error("release lock failed:" + Thread.currentThread().getName() + ", del key failed. Status code reply=" + result);
}
} catch (Exception e) {
logger.error("release lock occured an exception", e);
} finally {
//清除本地變量
this.clean();
}
return false;
}
- 如果本地線程localKeys中無法獲取到key循头,或者獲取到的key與傳入的不一致绵估,解鎖失敗
- redisTemplate.execute(UNLOCK_LUA_SCRIPT, keys) 將LUA腳本交給Redis服務(wù)端執(zhí)行炎疆。UNLOCK_LUA_SCRIPT常量定義,先判斷key值是否與傳入的requestId一致国裳,如果一致則刪除key形入,如果不一致返回-1表示key可能已經(jīng)過期或被其他客戶端占用,避免誤刪缝左。
//定義釋放鎖的lua腳本 private final static DefaultRedisScript<Long> UNLOCK_LUA_SCRIPT = new DefaultRedisScript<>( "if redis.call(\"get\",KEYS[1]) == KEYS[2] then return redis.call(\"del\",KEYS[1]) else return -1 end" , Long.class );
- 最后通過clean做清理工作
/** * 清除本地線程變量亿遂,防止內(nèi)存泄露 */ private void clean() { localRequestIds.remove(); localKeys.remove(); }
4.后記
- 可將鎖改成注解方式,通過AOP降低鎖使用的復(fù)雜度
- 重試機(jī)制可以根據(jù)業(yè)務(wù)情況進(jìn)行優(yōu)化
- 可以更進(jìn)一步借助ThreadLocal保存鎖計(jì)數(shù)器可實(shí)現(xiàn)類似ReentrantLock可重入鎖機(jī)制
- 釋放鎖失敗后可以加入回調(diào)方法進(jìn)行一些業(yè)務(wù)處理
- 如果業(yè)務(wù)掛起或者執(zhí)行時(shí)間過長渺杉,超過了鎖的超時(shí)時(shí)間蛇数,另外的客戶端可能提前獲取到鎖,導(dǎo)致臨界區(qū)代碼不能嚴(yán)格的串行執(zhí)行少办。除了合理設(shè)置鎖超時(shí)時(shí)間外苞慢,盡量不要把分布式鎖用于執(zhí)行時(shí)間長的任務(wù)
5.補(bǔ)充
5.1 RedisTemplate加載
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.io.Serializable;
/**
* @Description Redis配置類,替代SpringBoot自動(dòng)配置的RedisTemplate英妓,參加RedisAutoConfiguration
* 這個(gè)類沒有設(shè)置序列化方式等
* @Author Gazza Jiang
* @Date 2018/11/12 9:30
* @Version 1.0
*/
@Configuration
@AutoConfigureAfter(RedisAutoConfiguration.class)
public class RedisConfig {
@Bean
public RedisTemplate<String, Serializable> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, Serializable> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory);
//Jackson序列化器
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
//字符串序列化器
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
//普通Key設(shè)置為字符串序列化器
template.setKeySerializer(stringRedisSerializer);
//Hash結(jié)構(gòu)的key設(shè)置為字符串序列化器
template.setHashKeySerializer(stringRedisSerializer);
//普通值和hash的值都設(shè)置為jackson序列化器
template.setValueSerializer(jackson2JsonRedisSerializer);
template.setHashValueSerializer(jackson2JsonRedisSerializer);
template.afterPropertiesSet();
return template;
}
}
5.2 簡(jiǎn)單測(cè)試類
import org.junit.Test;
import org.junit.runner.RunWith;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import xyz.gazza.demo.redis.Application;
import xyz.gazza.demo.redis.lock.RedisLock;
import java.util.ArrayList;
/**
* @Description 測(cè)試類
* @Author Gazza Jiang
* @Date 2018/11/12 13:29
* @Version 1.0
*/
@RunWith(SpringRunner.class)
@SpringBootTest(classes = {Application.class})
public class ApplicationTest {
private final Logger logger = LoggerFactory.getLogger(ApplicationTest.class);
@Autowired
RedisLock redisLock;
@Test
public void testRedisLock() throws InterruptedException {
ArrayList<Thread> list = new ArrayList<>();
for(int i =0; i<10; i++) {
//logger.info("線程開始");
Thread t = new Thread() {
@Override
public void run() {
if (redisLock.lock("suaner")) {
try {
//成功獲取鎖
logger.info("獲取鎖成功挽放,繼續(xù)執(zhí)行任務(wù)" + Thread.currentThread().getName());
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}catch (Exception e) {
logger.error("excepiotn ", e);
} finally {
redisLock.unlock("suaner");
}
}
}
};
list.add(t);
t.start();
}
for(Thread t : list) {
t.join();
}
Thread.sleep(10000);
}
}
5.3 pom依賴
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>