基于Redis的分布式锁实现思路
- 利用set nx ex获取锁,并设置过期时间,保存线程标识
- 锁释放时先判断线程标识是否与自己的一致,一致则删除 避免误删
特性
- 利用set nx 满足锁的互斥性
- 利用set ex 保证故障时锁依然释放,避免死锁,提高安全性
- 利用Redis集群保证高可用和高并发特性
简单的Redis分布式锁示例代码
获取锁
@Override
public boolean tryLock(Long timeoutSec) {
System.out.println("tryLock:" + ID_PREFIX);
// 获取当前线程的id
String id = ID_PREFIX + "-" + Thread.currentThread().getId();
Boolean aBoolean = redisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, id, timeoutSec, TimeUnit.SECONDS);
return Boolean.TRUE.equals(aBoolean);
}
释放锁
@Override
public void unlock() {
redisTemplate.delete(KEY_PREFIX + name);
}
整体示例代码
package com.hmdp.utils;
import cn.hutool.core.lang.UUID;
import org.springframework.data.redis.core.StringRedisTemplate;
import java.util.concurrent.TimeUnit;
public class SimpleRedisLock implements ILock{
private StringRedisTemplate redisTemplate;
private String name;
public SimpleRedisLock(String name, StringRedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
}
private final String KEY_PREFIX = "lock:";
private final String ID_PREFIX = UUID.randomUUID().toString(true);
@Override
public boolean tryLock(Long timeoutSec) {
// 获取当前线程的id
String id = ID_PREFIX + "-" + Thread.currentThread().getId();
Boolean aBoolean = redisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, id, timeoutSec, TimeUnit.SECONDS);
return Boolean.TRUE.equals(aBoolean);
}
@Override
public void unlock() {
redisTemplate.delete(KEY_PREFIX + name);
}
}
上述简易式分布式锁存在的问题
问题
当业务代码执行时间超过锁的过去时间,导致锁被超时释放,另外的线程此时进入代码块获取锁后,第一个线程业务代码执行完成,开始执行释放锁代码,由于删除同名key,导致第二个线程尚未执行完成,锁已被释放,导致后续线程可以一直获取锁,造成并发安全问题
解决方案
为每个锁的内容设置唯一标识,例如 设置UUID+当前线程ID,删除时对内容进行判断,若为内容相同则删除锁,否则不删除
@Override
public void unlock() {
System.out.println("unLock:" + ID_PREFIX);
// ID_PREFIX为UUID 凭借线程号
String id = ID_PREFIX + "-" + Thread.currentThread().getId();
// 获取当前redis中锁的值
String value = redisTemplate.opsForValue().get(KEY_PREFIX + name);
// 判断是否相等
if (id.equals(value)){
redisTemplate.delete(KEY_PREFIX + name);
}
}
上述问题依旧存在问题,当代码执行到redisTemplate.delete(KEY_PREFIX + name);前出现了阻塞,则同样会出现误删除的情况,此时必须保证redis分布式锁判断标识与删除的原子性,此处可引入LUA脚本,实现原子性操作,避免锁被误删除的情况
LUA脚本示例如下:
if (redis.call('get', KEYS[1]) == ARGV[1]) then
-- 释放锁
return redis.call('del', KEYS[1])
end
return 0
释放锁相关java代码
@Override
public void unlock() {
String id = ID_PREFIX + "-" + Thread.currentThread().getId();
// 调用lua脚本
redisTemplate.execute(redisScript, Collections.singletonList(KEY_PREFIX + name), id);
}
最终代码
package com.hmdp.utils;
import cn.hutool.core.lang.UUID;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.RedisScript;
import java.util.Collections;
import java.util.concurrent.TimeUnit;
public class SimpleRedisLock implements ILock{
private StringRedisTemplate redisTemplate;
private String name;
public SimpleRedisLock(String name, StringRedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
this.name = name;
}
private final String KEY_PREFIX = "lock:";
private final String ID_PREFIX = UUID.randomUUID().toString(true);
private static DefaultRedisScript<Long> redisScript;
static {
// 加载Lua脚本
redisScript = new DefaultRedisScript<>();
redisScript.setLocation(new ClassPathResource("unLock.lua"));
redisScript.setResultType(Long.class);
}
@Override
public boolean tryLock(Long timeoutSec) {
// 获取当前线程的id
String id = ID_PREFIX + "-" + Thread.currentThread().getId();
Boolean aBoolean = redisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, id, timeoutSec, TimeUnit.SECONDS);
return Boolean.TRUE.equals(aBoolean);
}
@Override
public void unlock() {
String id = ID_PREFIX + "-" + Thread.currentThread().getId();
// 调用lua脚本
redisTemplate.execute(redisScript, Collections.singletonList(KEY_PREFIX + name), id);
}
}
评论区