Redis基础知识(学习笔记2--分布式锁)

1.并发问题

例如,一个操作是修改用户的账户的状态。修改前需要先读取,在内存里修改,修改完了,再存进去。

如果这样的操作同时进行,就会出现并发问题,因为“读取”和“保持”(设置)这两个操作不是原子操作。

原子操作是指不会被线程调度机制打断的操作。这种操作一旦开始,就会一直运行到结束,中间不会有任何线程切换。

2.分布式锁的奥义

分布式锁要实现的最终目标就是在redis里面占一个“坑”,当别的进程也要来占“坑”时,发现那里已经有一个“大萝卜”了,就智能放弃或者稍后再试。

占坑一般使用setnx指令,指允许被一个客户端占坑。先来先占,用完了,再调用del指令释放“坑”。

3.占坑指令--SETNX

setnx key value

将key的值设为value,当且仅当key不存在。

若给定的key已经存在,则SETNX不做任何动作。

SETNX是【SET if NOT eXists】(如果不存在,则SET)的缩写。

 4. 示例

//这里的冒号“:”就是一个普通的字符,没特殊含义,它可以是其它任意字符。
> setnx lock:codehole true
OK
... do something critical ...
> del lock:codehole
(integer) 1

5.优化1--添加过期时间

待优化的地方:如果逻辑执行到中间出现了问题,可能会导致del指令没有被调用,这样就会陷入死锁,锁永远等不到释放。

优化思路; 在拿到锁之后,再给锁加上一个过期时间,比如2s,这样即使中间出现了异常,也可以保证2s之后,锁会自动释放。

> setnx lock:codehole true
OK
> expire lock:codehole 2
... do something critical ...
> del lock:codehole
(integer) 1

6. 继续优化2--过期时间的设置

上面的code,逻辑上还有问题:如果在setnx 和 expire 之间服务器进程突然挂掉了,可能是因为机器掉电或者是人为造成的,这就会导致expire得不到执行,也会造成死锁。

这种问题的根源在于setnx和expire是两条指令而不是原子指令。

如果这两条指令可以一起执行就不会出现问题,也许你会想到用redis事务来解决,但在这里不行,因为expire是依赖setnx的执行结果,如果setnx没抢到锁,expire是不应该执行的。事务里没有if-else分支逻辑,事务的特点是一口气执行,要么全部执行,要么一个都不执行。

Redis 2.8 版本之后,set指令进行了参数扩展,使setnx 和 expire指令可以一起执行。

> setnx lock:codehole true ex 5 nx
OK
... do something critical ...
> del lock:codehole

7. 继续优化3--锁名称的设置

Redis的分布式锁不能解决超时问题,如果在加锁和释放锁之间的逻辑执行得太长,以至于超出了锁的限时限制,就会出现问题。

因为这时候第一个线程持有的锁过期了,但临界区的逻辑(脚本)还没执行完,而同时第二个线程就提前重新持有了这把锁,导致临界区代码不能得到严格的串行执行。

出问题的关键点:在于释放锁/删除锁时,容易把别人添加的锁释放掉。优化的思路,就是在释放锁的时候,判断下,是不是自己之前添加的锁。

例如,针对每一个请求,生成一个基于clientid(或者UUID.randomUUID().toString())的value。

即将set指令的value参数设置为一个随机数,释放时,先匹配随机数释放一致,然后再删除。

这是为了确保当前线程占有的锁不会被其它线程释放,除非这个锁是因为过期了而被服务器自动释放。

tag = random.nextint()   ##随机数
if redis.set(key, tag, nx=True, ex=5):
   do_something()
   redis.delifequals(key,tag)  ##抽象的功能代码

但是匹配value和删除key不是一个原子操作,redis也没有提供类似的delifequals这样的指令,需要使用Lua脚本处理(Lua脚本可以保证连续多个指令的原子性执行)

# delifequals
if redis.call("get",KEYS[1]) == ARGV[1] then
   return redis.call("del",KEYS[1])
else
   return 0
end

但这也不是一个完美的方案,它只是相对安全一点,因为如果真的超时了,当前线程的逻辑没有执行完,其它线程的逻辑也会乘虚而入。

8. 继续优化4--锁续期

redis锁的过期时间能够自动续期。

使用redis客户端redisson。redisson在加锁成功后,会注册一个定时任务监听这个锁,每隔10秒就去查看这个锁,如果还持有锁,就对过期时间进行续期。默认过期时间30秒。

举例子:假如加锁的时间是30秒,过10秒检查一次,一旦加锁的业务没有执行完,就会进行一次续期,把锁的过期时间再次重置成30秒。

1. 加锁成功后,启动一个定时任务,每个一段时间检测是否执行完成业务代码,依据是锁是否还存在。

2. 这个定时任务每隔三分之一时间检查一次,比如锁过期时间是30秒,就每隔10秒检查一次。

3. 如果锁还存在,就把锁过期时间继续设置成30秒。

Redisson框架已经实现了这个流程,名叫看门狗机制(Watch Dog),底层使用 Lua 脚本实现。

// 获取 Redisson 锁
RLock lock = redissonClient.getLock(lock_key);
try {
    // 加锁,并设置3秒后过期
    lock.lock(3, TimeUnit.SECONDS);
    // 执行业务代码
    doBusiness();
} catch (Exception e) {
    System.out.println("加锁超时");
} finally {
    // 释放锁
    lock.unlock();
}

学习参阅声明

1.《Redis深度历险--核心原理与应用实践》

2. 《Redis分布式锁如何实现续期》 https://www.jb51.net/article/233992.htm

3. 《高并发必备,使用Redis分布式锁必须注意的10个细节》https://zhuanlan.zhihu.com/p/647085067

 

热门相关:兵王传说   穿越王妃有点野   首席天价逼婚:老婆不准逃   萌妻鲜嫩:神秘老公晚上见   重生娘子在种田