Redis实现分布式锁的核心便在于SETNX命令,它是SET if Not eXists的缩写,如果键不存在,则将键设置为给定值,在这种情况下,它等于SET;当键已存在时,不执行任何操作;成功时返回1,失败返回0
根据这个特性,我们依据是否设置成功来实现分布式锁。
加上过期时间,保证不会死锁。
LUA脚本,保证释放时是自己的锁
@Slf4j
public class RedisLock implements AutoCloseable {
private RedisTemplate redisTemplate;
private String key;
private String value;
//单位:秒
private int expireTime;
/**
* 没有传递 value,因为直接使用的是随机值
*/
public RedisLock(RedisTemplate redisTemplate,String key,int expireTime){
this.redisTemplate = redisTemplate;
this.key = key;
this.expireTime=expireTime;
this.value = UUID.randomUUID().toString();
}
/**
* JDK 1.7 之后的自动关闭的功能
*/
@Override
public void close() throws Exception {
unLock();
}
/**
* 获取分布式锁
* SET resource_name my_random_value NX PX 30000
* 每一个线程对应的随机值 my_random_value 不一样,用于释放锁的时候校验
* NX 表示 key 不存在的时候成功,key 存在的时候设置不成功,Redis 自己是单线程,串行执行的,第一个执行的才可以设置成功
* PX 表示过期时间,没有设置的话,忘记删除,就会永远不过期
*/
public boolean getLock(){
RedisCallback<Boolean> redisCallback = connection -> {
//设置NX
RedisStringCommands.SetOption setOption = RedisStringCommands.SetOption.ifAbsent();
//设置过期时间
Expiration expiration = Expiration.seconds(expireTime);
//序列化key
byte[] redisKey = redisTemplate.getKeySerializer().serialize(key);
//序列化value
byte[] redisValue = redisTemplate.getValueSerializer().serialize(value);
//执行setnx操作
Boolean result = connection.set(redisKey, redisValue, expiration, setOption);
return result;
};
//获取分布式锁
Boolean lock = (Boolean)redisTemplate.execute(redisCallback);
return lock;
}
/**
* 释放锁的时候随机数相同的时候才可以释放,避免释放了别人设置的锁(自己的已经过期了所以别人才可以设置成功)
* 释放的时候采用 LUA 脚本,因为 delete 没有原生支持删除的时候校验值,证明是当前线程设置进去的值
* 脚本是在官方文档里面有的
*/
public boolean unLock() {
// key 是自己才可以释放,不是就不能释放别人的锁
String script = "if redis.call(\"get\",KEYS[1]) == ARGV[1] then\n" +
" return redis.call(\"del\",KEYS[1])\n" +
"else\n" +
" return 0\n" +
"end";
log.info("执行的LUA脚本:"+script);
RedisScript<Boolean> redisScript = RedisScript.of(script,Boolean.class);
List<String> keys = Arrays.asList(key);
// 执行脚本的时候传递的 value 就是对应的值
Boolean result = (Boolean)redisTemplate.execute(redisScript, keys, value);
log.info("释放锁的结果:"+result);
return result;
}
}
使用锁时,每次使用都new一个新的RedisLock
public String redisLock(){
log.info("我进入了方法!");
try (RedisLock redisLock = new RedisLock(redisTemplate,"redisKey",30)){
if (redisLock.getLock()) {
log.info("我进入了锁!!");
Thread.sleep(15000);
}else {
log.info("获取锁失败!!");
}
} catch (InterruptedException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
log.info("方法执行完成");
return "方法执行完成";
}
Redis setNx 命令用于实现分布式锁,通过它可以保证同一时间只能有一个客户端访问共享资源从而避免了数据竞争的问题。下面是 Redis setNx 实现分布式锁的优缺点:
优点:
-
简单:使用 Redis setNx 命令实现分布式锁非常简单,只需要在 Redis 中创建一个 key-value 即可。
-
高效:Redis 的 setNx 命令是原子性的操作,不会导致死锁问题,并且支持设置过期时间,能够防止死锁。
-
高可用:Redis 是一个高可用的 NoSQL 数据库,可以实现主从复制、哨兵集群、分片等多种方式的高可用方案。
-
易于扩展:Redis 支持读写分离和分片技术,可以轻松地实现横向扩展和纵向扩展。
缺点:
-
网络延迟:在使用 Redis setNx 命令时,由于需要与 Redis 服务器进行通信,如果网络延迟较高,则可能会导致线程等待的时间增加,从而影响系统的性能。
-
锁过期问题:因为 Redis setNx 命令只有在 key 不存在时才会生效,所以如果在获取锁后业务逻辑执行时间太长,超过了锁的过期时间,则可能会导致锁被其他客户端获取,这个问题可以通过设置锁的超时时间和续约机制解决。
-
误删锁问题:在释放锁时,如果多个客户端同时执行删除操作,则可能会误删掉其他客户端的锁,这个问题可以通过为每个锁分配一个唯一的 ID 来避免。
综上所述,Redis setNx 实现分布式锁是一种简单、高效和可靠的方式,但也有一些需要注意的缺点。当需要实现分布式锁时,需要根据具体场景选择适合的方式,并且在使用 Redis setNx 命令时需要针对缺点进行相应的优化和处理。