spring boot redis分布式锁

随着现在分布式架构越来越盛行,在很多场景下需要使用到分布式锁。分布式锁的实现有很多种,比如基于数据库、zookeeper等,本文主要介绍使用Redis做分布式锁的方式,并封装成spring boot starter,方便使用

一.Redis分布式锁的实现以及存在的问题

锁是针对某个资源,保证其访问的互斥性,在实际使用当中,这个资源一般是一个字符串。使用Redis实现锁,主要是将资源放到Redis当中,利用其原子性,当其他线程访问时,如果Redis中已经存在这个资源,就不允许之后的一些操作。spring boot使用Redis的操作主要是通过RedisTemplate来实现,一般步骤如下:

将锁资源放入Redis(注意是当key不存在时才能放成功,所以使用setIfAbsent方法):

设置过期时间

释放锁

一般情况下,这样的实现就能够满足锁的需求了,但是如果在调用setIfAbsent方法之后线程挂掉了,即没有给锁定的资源设置过期时间,默认是永不过期,那么这个锁就会一直存在。所以需要保证设置锁及其过期时间两个操作的原子性,spring data的RedisTemplate当中并没有这样的方法。但是在jedis当中是有这种原子操作的方法的,需要通过RedisTemplate的execute方法获取到jedis里操作命令的对象,代码如下:

注意:Redis从2.6.12版本开始set命令支持NX、PX这些参数来达到setnx、setex、psetex命令的效果,文档参见:http://doc.redisfans.com/string/set.html

NX:表示只有当锁定资源不存在的时候才能SET成功。利用Redis的原子性,保证了只有第一个请求的线程才能获得锁,而之后的所有线程在锁定资源被释放之前都不能获得锁。

PX:expire表示锁定的资源的自动过期时间,单位是毫秒。具体过期时间根据实际场景而定

这样在获取锁的时候就能够保证设置Redis值和过期时间的原子性,避免前面提到的两次Redis操作期间出现意外而导致的锁不能释放的问题。但是这样还是可能会存在一个问题,考虑如下的场景顺序:

线程T1获取锁

线程T1执行业务操作,由于某些原因阻塞了较长时间

锁自动过期,即锁自动释放了

线程T2获取锁

线程T1业务操作完毕,释放锁(其实是释放的线程T2的锁)

按照这样的场景顺序,线程T2的业务操作实际上就没有锁提供保护机制了。所以,每个线程释放锁的时候只能释放自己的锁,即锁必须要有一个拥有者的标记,并且也需要保证释放锁的原子性操作。

因此在获取锁的时候,可以生成一个随机不唯一的串放入当前线程中,然后再放入Redis。释放锁的时候先判断锁对应的值是否与线程中的值相同,相同时才做删除操作。

Redis从2.6.0开始通过内置的Lua解释器,可以使用EVAL命令对Lua脚本进行求值,文档参见:http://doc.redisfans.com/script/eval.html

因此我们可以通过Lua脚本来达到释放锁的原子操作,定义Lua脚本如下:

具体意思可以参考上面提供的文档地址

使用RedisTemplate执行的代码如下:

代码中分为集群模式和单机模式,并且两者的方法、参数都一样,原因是spring封装的执行脚本的方法中(RedisConnection接口继承于RedisScriptingCommands接口的eval方法),集群模式的方法直接抛出了不支持执行脚本的异常(虽然实际是支持的),所以只能拿到Redis的connection来执行脚本,而JedisCluster和Jedis中的方法又没有实现共同的接口,所以只能分开调用。

spring封装的集群模式执行脚本方法源码:

至此,我们就完成了一个相对可靠的Redis分布式锁,但是,在集群模式的极端情况下,还是可能会存在一些问题

线程T1获取锁成功

Redis的master节点挂掉,slave自动顶上

线程T2获取锁,会从slave节点上去判断锁是否存在,由于Redis的master slave复制是异步的,所以此时线程T2可能成功获取到锁

为了可以以后扩展为使用其他方式来实现分布式锁,定义了接口和抽象类,所有的源码如下:

二. 基于AOP的Redis分布式锁

在实际的使用过程中,分布式锁可以封装好后使用在方法级别,这样就不用每个地方都去获取锁和释放锁,使用起来更加方便。

首先定义个注解:

装配分布式锁的bean

定义切面(spring boot配置方式)

spring boot starter还需要在resources/META-INF中添加spring.factories文件

这样封装之后,使用spring boot开发的项目,直接依赖这个starter,就可以在方法上加RedisLock注解来实现分布式锁的功能了,当然如果需要自己控制,直接注入分布式锁的bean即可

如果需要使用其他的分布式锁实现,继承AbstractDistributedLock后实现获取锁和释放锁的方法即可

你可能感兴趣的:(java,redis,开发语言)