文章目录
- 1.Redis的两种原子操作方法
- 2.Redis实现分布式锁
- 2.1.分布式锁的概念
- 2.2.Redis分布式锁的实现
- 2.2.1.基于单个redis节点实现分布式锁
- 2.2.2.基于多个redis节点实现高可靠的分布式锁
- 2.3.redis加锁过程中的错误使用
使用redis时,碰到并发有两种处理方式
- 第一种:看是否能够使用原子操作
- 第二种:分布式锁
为什么需要优先第一种呢,因为我们需要考虑到分布式锁会降低并发度,并且分布式锁需要使用到其余的系统(共享存储系统)来实现
而原子操作则无需加锁,既能控制并发度,也能够减少对系统并发性的影响
1.Redis的两种原子操作方法
- (1)把多个操作放在redis中实现成一个操作,也就是单命令操作
例如:INCR/DECR
、SETNX
命令,其将读取-修改-写入合并为了一个操作,而redis本身就是单线程的,命令与命令之间是互斥的,所以该操作符合原子性 - (2)把多个命令写到lua脚本中,以原子性的方式执行单个脚本
Redis会把整个Lua脚本作为一个整体执行,在执行的过程中不会被其他命令打断,从而保证了Lua脚本中操作的原子性
另外使用lua脚本时,还有一些注意点:
1、lua 脚本尽量只编写通用的逻辑代码,避免直接写死变量。变量通过外部调用方传递进来,这样 lua 脚本的可复用度更高。
2、建议先使用SCRIPT LOAD命令把 lua 脚本加载到 Redis 中,然后得到一个脚本唯一摘要值,再通过EVALSHA命令 + 脚本摘要值来执行脚本,这样可以避免每次发送脚本内容到 Redis,减少网络开销。
2.Redis实现分布式锁
2.1.分布式锁的概念
分布式锁类似于单机上的锁,通过一个变量来维护,加锁操作实际上就是判断该变量是否为0,释放锁操作实际上就是将该变量置为0。
只是分布式锁的变量变为了一个共享变量,关于分布式锁的要求如下:
- 互斥性:加锁和释放锁过程涉及多个操作,需要保证操作的原子性,只允许一个请求拿到锁
- 可靠性:需要保证依赖的共享系统的可靠性,进而保证锁的可靠性
- 释放锁只能够释放自己加的锁
2.2.Redis分布式锁的实现
2.2.1.基于单个redis节点实现分布式锁
- 加锁操作:redis通过键值对
lock_key:0
保存变量,后续其他加锁请求发现该键值对存在,则返回失败标识
加锁过程包含读取、判断变量值、修改锁变量值为1三个过程,redis是如何保证原子性的:
SETNX
命令:redis先判断当前key是否存在,如果不存就创建该键值,存在就直接返回,redis的具体实现如下
SET key value [EX seconds | PX milliseconds] [NX]
- 释放锁操作:执行完业务流程后,使用
DEL
删除锁变量,释放锁过程包含了读取锁变量、判断值、删除锁变量多个操作,因此使用Lua脚本,原子性的方式执行保证释放锁操作的原子性 - 使用SETNX+DEL实现分布式锁存在的问题
- (1)可能申请了锁但是没有正常的释放,就会导致其他请求无法申请锁
解决办法:给key设置过期时间,将影响降低 - (2)删除锁操作没有区分谁申请的锁,就是说A可以释放B的锁
解决办法:区分当前锁是那个客户端申请的,其实就是给锁的变量设置一个唯一id进行客户端的区分,删除的时候也比较这个值是不是和申请锁的客户端的唯一id相等,相等才能执行删除锁操作
知道了解决方案后,我们可以来看下redis的实现
// 加锁, unique_value作为客户端唯一性的标识
SET lock_key unique_value NX PX 10000
//释放锁 比较unique_value是否相等,避免误释放,这里是使用lua实现
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
redis-cli --eval unlock.script lock_key , unique_value
2.2.2.基于多个redis节点实现高可靠的分布式锁
由于一个redis实例可能出现宕机的风险,因此为了保证可靠性,我们可以采取多个实例实现redis的分布式锁
为了避免Redis实例故障而导致的锁无法工作的问题,Redis的开发者Antirez提出了分布式锁算法Redlock
Redlock:
基本思路:让客户端请求多个redis实例依次请求加锁,能够获得大多数(2n+1)实例的锁,则申请锁成功。这样能够有效避免某个实例宕机导致锁操作异常
基本流程:
- 第一步:客户端获取当前时间
- 第二步:客户端按顺序向N个实例请求加锁操作。加锁方法仍是使用
SETNX
这里redis实例给每个客户端进行加锁时,会设置客户端的超时时间,如果在超时时间内没有加锁成功则会请求下一个实例,一般的超时时间为几十ms - 第三步:完成了与所有实例的加锁操作之后需要计算整个过程的耗时,用于比较锁的过期时间,如果请求锁的时间大于了过期时间,则将该锁释放,如果客户端请求成功的实例不占大多数,也会释放锁
RedLock算法中,只需要保证N个redis实例的半数以上可用就能保证锁操作的可靠性
2.3.redis加锁过程中的错误使用
- 加锁操作:
错误一:
Long result = jedis.setnx(lockKey, requestId);
if (result == 1) {
// 若在这里程序突然崩溃,则无法设置过期时间,将发生死锁
jedis.expire(lockKey, expireTime);
}
- 解锁操作:
// 判断加锁与解锁是不是同一个客户端
if (requestId.equals(jedis.get(lockKey))) {
//删除点: 若在此时,这把锁突然不是这个客户端的,则会误解锁
jedis.del(lockKey);
}
可能会存在当前代码执行到删除点时,锁正好过期然后A又申请到了锁,此时删除会释放A的锁导致错误