Redis怎样实现分布式锁?
前言
我们知道,在Java单进程中,多线程的环境下,如果我们要操作一个共享变量,需要使用synchronized或者是JUC同步工具类才能保证线程安全。那么,多进程环境下,我们要怎样保证线程安全?
为什么需要分布式锁?
我们知道,synchronized或者是JUC同步工具类只能在同一进程中保证线程安全,他们的影响范围没办法超出本Java进程。但是随着分布式成为主流,多进程共享数据的情况越来越常见。
如上图,两个进程同时对存储在MySQL、Redis或者是zookeeper中的共享数据进行读写,即有可能出现线程安全问题,这种情况下,我们就需要一个可以在分布式环境下也可以使用的锁,来保证线程安全,这即是分布式锁。
分布式锁可以使用什么组件实现?
因为要在分布式环境下生效,因此实现分布式锁使用的组件也必须是每个进程都可以连接到的,目前比较常见的是使用Redis和Zookeeper来实现。
Redis分布式锁实现逻辑
Redis中,我们使用String数据结构来实现分布式锁。
加锁
Redis中,set的语法如下: SET key value [EX seconds] [PX milliseconds] [NX|XX]
EX second
:设置键的过期时间为second
秒。SET key value EX second
效果等同于SETEX key second value
。PX millisecond
:设置键的过期时间为millisecond
毫秒。SET key value PX millisecond
效果等同于PSETEX key millisecond value
。NX
:只在键不存在时,才对键进行设置操作。SET key value NX
效果等同于SETNX key value
。XX
:只在键已经存在时,才对键进行设置操作。
上述参数中,我们可以将key
作为锁标识,然后设置NX
参数。如果key
写入成功,表示当前Redis中不存在这个key
,可以加锁;如果key
写入失败,表示当前Redis中已存在这个key
,已经有其它线程获取到锁了。命令如下:
SET lockKey requestId NX
lockKey
为锁标识,最好带上使用共享变量唯一标识,可以使用订单编号、用户编号等。requestId
为本次锁请求编号,释放锁时使用。
上述命令中,可以实现加锁,但是如果在加锁后,应用挂了,或者出现了其它问题,导致没有及时解锁,就有可能出现死锁,因此,需要再给加上一个过期时间,让锁可以自动消失。命令如下:
SET lockKey requestId EX seconds NX
释放锁
释放锁的时候,我们不能直接使用del
命令去删除Redis键值,否则会出现A获取的锁,B也可以释放的情况。因此,我们在释放锁的时候,需要判断当前锁的请求ID是否是加锁时是否一致,如果一致,才能释放锁。
由于没有现成的命令可以实现上述的释放锁的逻辑,所以我们需要使用Redis的script
来实现,script
脚本如下:
if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end
KEYS[1]
为锁标识,即加锁时的lockKey
ARGV[1]
为锁请求编号,即加锁时的requestId
Redis分布式锁代码
public class RedisDistributedLock implements Lock {
// 锁键值
private String lockKey;
// 锁请求编号
private String requestId;
// redis集群客户端
private JedisCluster jedisCluster;
public RedisDistributedLock(String lockKey, JedisCluster jedisCluster){
this.lockKey = lockKey;
// 使用UUID作为锁唯一标识
requestId = UUID.randomUUID().toString();
this.jedisCluster = jedisCluster;
}
/**
* 尝试获取锁
*/
@Override
public boolean tryLock() {
// 获取锁
String result = jedisCluster.set(lockKey, requestId, "NX", "EX", 2);
return StringUtils.isNotBlank(result) && LOCK_SUCCESS.equals(result);
}
/**
* 释放锁
*/
@Override
public void unlock() {
// 若redis中存在lockKey,则删除lockKey,否则返回0
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
Object result = jedisCluster.eval(script, Collections.singletonList(lockKey),
Collections.singletonList(requestId));
// 成功
if (result != null && "OK".equals(result.toString())) {
logger.debug("释放锁成功,lockKey:{}, requestId:{}", lockKey, requestId);
} else {// 失败
logger.debug("释放锁失败,lockKey:{}, requestId:{}", lockKey, requestId);
}
}
}
后言
既然看到这里了,感觉有所收获的朋友,不妨来个大大的点赞吧~~~
转载自:https://juejin.cn/post/7159556729439518727