基于Redis分布式锁(一)
仅适用于单机版Redis(非集群)
1. Redis 指令
1.1 永久锁
SETNX key value
KEY不存在返回1,KEY已经存在返回0
1.2 限时锁
SET key value [EX seconds][PX milliseconds] [NX|XX]
- EX seconds 设置指定的到期时间(单位:秒)
- PX milliseconds 设置指定的到期时间(单位:毫秒)
- NX 仅在键不存在时设置
- XX 仅在键存在时才设置
1.3 解锁
DEL key
1.4 设置过期时间
# 1.面向剩余时间
# 单位秒
EXPIRE key seconds
# 单位毫秒
PEXPIRE key milliseconds
# 2.面向时间戳
# 时间戳(秒)
EXPIREAT key seconds
# 时间戳(毫秒)
PEXPIREAT key milliseconds
2. 代码实现
使用RedisTemplate<String, Object>
2.1 基础代码
- 1、使用SETNX key value 上锁
- 2、使用EXPIRE key seconds 设置锁过期时间
- 3、使用DEL key 解锁
public void redisTest() throws Exception {
String key = "Lock";
// 1.上锁
Boolean isLock = redisTemplate.opsForValue().setIfAbsent(key, "Lock");
// 2.设置过期时间
redisTemplate.expire(key, 2, TimeUnit.SECONDS);
while (isLock == null || !isLock) {
TimeUnit.MILLISECONDS.sleep(100);
redisTest();
return;
}
try {
// 业务代码 ...
} finally {
// 2.解锁
redisTemplate.delete(key);
}
}
存在问题:
- 1、非原子性:上锁-设置过期时间期间服务器宕机会导致锁无法释放,发生死锁
- 2、会存在误删:线程A业务未执行完成锁过期了,线程B获取锁,线程A执行完后把锁释放了
2.2 优化上述问题
- 1、使用SET key value [EX seconds][PX milliseconds] [NX|XX]指令
- 2、释放锁前,先判断是否是自己的锁
public void redisTest() throws Exception {
String key = "Lock";
String uuid = UUID.randomUUID().toString();
// 1.上锁+设置过期时间
Boolean isLock = redisTemplate.opsForValue().setIfAbsent(key, uuid, 2, TimeUnit.SECONDS);
while (isLock == null || !isLock) {
TimeUnit.MILLISECONDS.sleep(100);
redisTest();
return;
}
try {
// 业务代码 ...
} finally {
// 2.解锁(先判断,再解锁)
if (StringUtils.equals(uuid, redisTemplate.opsForValue().get(key))){
redisTemplate.delete(key);
}
}
}
存在问题:
- 1、解锁非原子性:线程A在获取锁内容之后锁过期了,线程B获取锁成功,线程A将B上的锁释放了
2.3 使用Lua脚本解锁
1、EVAL 指令
Redis 提供 EVAL 指令执行 Lua 脚本
- 一次性可以发送多个指令给redis执行,并且是有序的,遵循one-by-one规则
- EVAL执行的脚本需要有返回值
EVAL script numkeys key [key...] arg [arg...]
# EVAL "if KEYS[1]>KEYS[2] then return ARGV[1] else return ARGV[2] end" 2 10 20 true false
# EVAL "return {KEYS[1], KEYS[2], ARGV[1],ARGV[2]}" 2 10 20 30 40
- script:执行的脚本(必须)
- numkeys:key的个数(必须)
- key:键列表,下标从1开始:KEYS[1]
- arg:值列表,下标从1开始:ARGV[1]
2、使用Lua
- 使用Lua脚本的类库:redis.call() 执行redis指令
# 获取lock值
EVAL "return redis.call('get', 'lock')" 0
# 设置lock值为111
EVAL "return redis.call('set', 'lock', '111')" 0
# 传参设置lock值为111
EVAL "return redis.call('set', KEYS[1], ARGV[1])" 1 lock 111
# 删除lock值
EVAL "return redis.call('del', 'lock')" 0
3、Java代码使用
- lua脚本
if redis.call('get', 'KEYS[1]')==ARGV[1]
then
return redis.call('del', 'KEYS[1]')
else
return 0
end
- Java代码
public void redisTest() throws Exception {
String key = "Lock";
String uuid = UUID.randomUUID().toString();
// 1.上锁+设置过期时间
Boolean isLock = redisTemplate.opsForValue().setIfAbsent(key, uuid, 2, TimeUnit.SECONDS);
while (isLock == null || !isLock) {
TimeUnit.MILLISECONDS.sleep(100);
redisTest();
return;
}
try {
// 业务代码 ...
} finally {
String script = "if redis.call('get', 'KEYS[1]')==ARGV[1] " +
" then return redis.call('del', 'KEYS[1]') else return 0 end";
// 2.原子性判断解锁
redisTemplate.execute(new DefaultRedisScript<>(script, Integer.class)
, Collections.singletonList(key)
, Collections.singletonList(uuid));
}
}
共同存在的问题:
1、A线程未执行完、锁过期失效了,B线程重新上锁成功(锁不会自动续期)
2、不可重入锁(当前线程只可获取锁·一次)
3. 失败重试机制
- 高并发情况下可能存在获取不到锁情况,可以尝试等待-重试机制
3.1 循环模式
public Object tautology(int time) throws Exception {
Boolean isLock;
for (int i = 0; i <= time; i++) {
// 获取锁
isLock = true;
if (isLock) {
// 成功获取锁就跳出循环
break;
}
TimeUnit.MILLISECONDS.sleep(300);
}
try {
// 业务代码 ...
return null;
} finally {
// 释放锁
}
}
3.2 递归模式
// 最大重试次数
private final Integer MAX_TIME = 3;
public Object tautology(int time) throws Exception {
// 超过最大重试次数
if (time > MAX_TIME) {
// 打印日志
return null;
}
// 获取锁并判断
Boolean isLock = true;
if (isLock) {
try {
// 业务代码 ...
return null;
} finally {
// 释放锁
}
} else {
TimeUnit.MILLISECONDS.sleep(300);
// 重试
return tautology(time + 1);
}
}
4.Redis序列化配置
- 将对象类型存入redis,以便能够正确反序列化
- 对NULL属性、集合、数组不进行序列化
- 解决jackson2无法反序列化LocalDateTime
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(LettuceConnectionFactory redisConnectionFactory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
// 解决value的序列化方式
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
// 首先解决key的序列化方式
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
redisTemplate.setKeySerializer(stringRedisSerializer);
// Hash序列方式
redisTemplate.setHashKeySerializer(stringRedisSerializer);
redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
// 序列化时将类的数据类型存入json,以便反序列化的时候转换成正确的类型
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance
, ObjectMapper.DefaultTyping.NON_FINAL);
// 只针对非空的属性、集合、数组进行序列化
objectMapper.setSerializationInclusion(JsonInclude.Include.NON_EMPTY);
// 解决jackson2无法反序列化LocalDateTime的问题
objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
objectMapper.registerModule(new JavaTimeModule());
jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
return redisTemplate;
}
}