Redis分布式锁使用总结
前言
最近因为项目需要进行多实例的协调,使用到了分布式锁,所以对分布式锁的原理、使用等做了一番调查、学习,顺便将其记录下来,供需要的同学学习交流。
项目中使用的是基于Redis的分布式锁,所以这篇文件的内容都是是基于Redis分布式锁。
分布式锁简介
谈起编程语言中的锁,开发者应该是相当熟悉的,当系统中存在多线程并且多线程之间存在竞态条件或者需要协作的时候,我们就会使用到锁,如Java中的Lock
、Synchronized
等,但是编程语言中提供的锁,基本上都只适用于在同一个机器上运行的情况,在分布式环境下并不适用。
而在某些情况下,我们是需要在多个机器实例/节点之间进行协作的,这个时候,就需要使用到分布式锁了。
顾名思义,分布式锁就是应用于在分布式环境下多个节点之间进行同步或者协作的锁
分布式锁同普通的锁一样,具有以下几个重要特性
- 互斥性,保证只有持有锁的实例中的某个线程才能进行操作
- 可重入性,同一个实例的同一个线程可以多次获取锁
- 锁超时,支持超时自动释放锁,避免死锁的产生
- 谁加的锁只能由谁释放
Redis分布式锁原理
由于Redis的命令本身是原子性的,所以,非常适合于作为分布式锁的协调者。
一般情况下,为了保证锁的释放只能由加锁者或者超时释放,一般我们会将对应键的值设置为一个线程唯一标志,如为每个线程生成一个UUID,只有当线程的UUID与锁的值一致时,才能释放锁。
利用Redis来实现分布式的原理非常简单,加锁的时候为某个键设置值,释放的时候将对应的键删除即可。
不过在使用的时候,有一些需要注意的地方,下面我们详细看下基于Redis不同命令来实现分布式锁的操作
setnx命令
在Redis2.6之前,常用于分布式锁的命令是:setnx key val
,该命令在对应的键没有值的时候设置成功,存在值的时候设置失败,保证了同时只会有一个连接者设置成功,也即保证同时只会有一个实例的一个线程获取成功。
但是该命令存在一个缺陷,不支持超时机制,所以需要额外的命令来保证能够在超时的情况下释放锁,也就是删除键,可以配合expire
命令来实现。
由于上述操作涉及到两个命令,所以最好的方式是通过lua脚本来实现加锁的操作,如下所示
# KEYS[1]是锁的名称,KEYS[2]是锁的值,KEYS[3]是锁的超时时间
local c = redis.call('setnx', KEYS[1], KEYS[2])
if(c == 1) then
redis.call('expire', KEYS[1], KEYS[3])
end
return c
释放锁的时候,需要验证释放锁的是不是锁的持有者,具体代码如下
# KEYS[1]是锁的名称,KEYS[2]是锁的值
if redis.call('get', KEYS[1]) == KEYS[2] then
return redis.call('del', KEYS[1])
else return 0
end
set命令
从上面的setnx命令可以看到,加锁的操作还是比较麻烦的,所以,在Redis2.6之后,redis的set命令进行了增强,设置值的时候,同时支持设置过期时间
# nx表示不存在的时候设置,ex表示设置过期时间,单位是秒
set LOCK VAL nx ex 15
可以看到,通过该命令,进行加锁就方便很多了
释放锁的操作同setnx里提到的释放操作
Redis分布式锁实现
上面我们提到的是Redis分布式锁的实现原理,不过,每次需要用到锁的时候都需要自己手动实现一次,虽然代码本身没有多少,其实也不是很方便。
正因为如此,有挺多的项目都实现了分布式,并且提供了更加丰富的功能,如下面讨论到的RedisLockRegistry
RedisLockRegistry
Spring-integration项目是Spring官方提供了集成各种工具的项目,通过integration-redis子项目,提供了非常丰富的功能,关于该项目,后面有时间再写篇文章具体分析一下,这里我们用到其中的一个组件RedisLockRegistry
导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-integration</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.integration</groupId>
<artifactId>spring-integration-redis</artifactId>
</dependency>
配置RedisLockRegistry
@Configuration
public class RedisLockConfiguration {
@Bean
public RedisLockRegistry redisLockRegistry(
RedisConnectionFactory redisConnectionFactory) {
// 注意这里的时间单位是毫秒
return new RedisLockRegistry(redisConnectionFactory, "registryKey", TIME);
}
}
RedisLockRegistry相当于一个锁的管理仓库,所有的锁都可以从该仓库获取,所有锁的键名为:registryKey:LOCK_NAME
,默认时间为60s
配置完锁的仓库之后,只需要注入仓库,当需要使用到锁的时候,从仓库中获取一个锁就可以了,如下所示
Lock lock = redisLockRegistry.obtain("redis-lock");
该操作返回一个Lock对象,该对象其实是Spring实现的基于Redis的锁,该锁支持了丰富的功能,如tryLock
等
但使用的时候,只需要跟普通的锁一样操作即可
// lock.tryLock(10, TimeUnit.SECONDS);
lock.lock();
try {
// ops
}catch(Exception e) {
}finally {
// 释放锁
lock.unlock();
}
可以看到,通过RedisLockRegistry,我们可以更加方便地使用Redis分布式锁了
RedisLockRegistry源码分析
上面学习了RedisLockRegistry的使用之后,接下来我们来具体看下RedisLockRegistry的具体实现
从上面的继承结构可以清晰地看出RedisLockRegistry的继承情况,而上面的几个接口基本上都只提供了基本的定义,这里就不展开分析了。直接看RedisLockRegistry的实现
构造函数
首先是构造函数,有两个构造函数,如下
private static final long DEFAULT_EXPIRE_AFTER = 60000L;
// 提供了默认的的过期时间,默认过期时间为60s
public RedisLockRegistry(RedisConnectionFactory connectionFactory, String registryKey) {
this(connectionFactory, registryKey, DEFAULT_EXPIRE_AFTER);
}
public RedisLockRegistry(RedisConnectionFactory connectionFactory,
String registryKey,
long expireAfter) {
Assert.notNull(connectionFactory, "'connectionFactory' cannot be null");
Assert.notNull