【Redis】Redis分布式锁

【Redis】Redis分布式锁

分布式应用进行逻辑处理时经常会遇到并发问题。如果一个操作要修改用户的状态。修改状态需要先读出用户的状态,在内存里进行修改,改完了再存回去。

如果这样的操作同时进行,就会出现并发问题,因为"读取"和"保存状态"这两个操作不是原子操作。(原子操作是指不会被线程调度机制打断的操作。这种操作一旦开始,就会一直运行到结束,中间不会有任何线程切换)

这个时候就要使用到分布式锁来限制程序的并发执行。Redis 分布式锁使用得非常广泛,它是面试的重要考点之一,很多同学都知道这个知识,也大致知道分布式锁的原理,但是具体到细节的掌握上,往往并不完全正确。

分布式锁是什么?

分布式锁本质上要实现的目标就是在 Redis 里面占一个"坑",当别的进程也要来占坑时,发现那里已经有一根"大萝卜"了,就只好放弃或者稍后再试。

占坑一般使用setnx(set if not exists)指令,只允许被一个客户端占坑。先来先占用完了,再调用del指令释放"坑"

shell 复制代码
> setnx lock:codehole true
OK
... do something critical ...
> del lock:codehole
(integer) 1

但是有个问题,如果逻辑执行到中间出现异常了,可能会导致 del 指令没有被调用,这样就会陷入死锁,锁永远得不到释放。

于是我们在拿到锁之后,再给锁加上一个过期时间,比如 5s,这样即使中间出现异常也可以保证5s之后锁会自动释放。

shell 复制代码
> setnx lock:codehole true
OK
> expire lock:codehole 5
... do something critical ...
> del lock:codehole
(integer) 1

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

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

为了解决这个疑难,Redis 开源社区涌现了许多分布式锁的 library,专门用来解决这个问题,实现方法极为复杂,小白用户一般要费很大的精力才可以弄懂。如果你需要使用分布式锁,意味着你不能仅仅使用 Jedis 或者 redis-py,还得引入分布式锁的 library。

为了治理这个乱象,在 Redis 2.8 版本中,作者加入了 set 指令的扩展参数,使得 setnx 和expire 指令可以一起执行,彻底解决了分布式锁的乱象。从此以后所有的第三方分布式锁 library 都可以休息了。

shell 复制代码
> set lock:codehole true ex 5 nx
OK
... do something critical ...
> del lock:codehole

上面这个指令就是setnxexpire组合在一起的原子指令,它就是分布式锁的奥义所在。

超时问题

Redis 的分布式锁不能解决超时问题,如果在加锁和释放锁之间的逻辑执行得长,以至于超出了锁的超时限制,就会出现问题。因为这时候第一个线程持有的锁过期了,临界区的逻辑还没有执行完,而同时第二个线程就提前重新持有了这把锁,导致临界区代码不能得到严格串行执行。

为了避免这个问题,Redis 分布式锁不要用于较长时间的任务。如果真的偶尔出现了问题,造成的数据小错乱可能需要人工介入解决。

有一个稍微安全一点的方案是将 set 指令的 value 参数设置为一个随机数,释放锁时先匹配随机数是否一致,然后再删除 key,这是为了确保当前线程占有的锁不会被其他线程释放,除非这个锁是因为过期了而被服务器自动释放的。

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

可重入性

可重入性是指线程在持有锁的情况下再次请求加锁,如果一个锁支持同一个线程的多次加锁,那么这个锁就是可重入的。比如Java语言里有个 ReentrantLock就是可重入锁。Redis 分布式锁如果要支持可重入,需要对客户端的 set 方法进行包装,使用线程的Threadlocal 变量存储当前持有锁的计数。

相关推荐
ccddsdsdfsdf6 小时前
DBeaver怎么链接mongoDB
数据库·mongodb
程序员老邢6 小时前
《技术底稿 43》今日踩坑复盘:Redis 乱码 + MySQL 配置注入失败
redis·技术底稿·redisson 序列化·mysql 配置·项目踩坑·微服务问题排查
丷丩7 小时前
Postgresql基础实践教程(十一)各种Join
数据库·postgresql·join
星夜夏空997 小时前
FreeRTOS学习(4)——内存映射
数据库·学习·mongodb
TheRouter7 小时前
AI Agent 记忆体系建设实战:短期、长期与工作记忆的工程实现
数据库·人工智能·oracle
Omics Pro8 小时前
首个!外源天然产物综合性代谢图谱
数据库·人工智能·算法·机器学习·r语言
phltxy8 小时前
RabbitMQ集群搭——多机多节点与单机多节点
分布式·rabbitmq·ruby
JAVA面经实录9179 小时前
Hibernate面试题库
数据库·oracle·hibernate
Mr. zhihao9 小时前
Redis五大高级数据结构:原理-场景-底层-横向对比
数据结构·redis
迷枫7129 小时前
DM8 目录结构与常用排查入口梳理
服务器·数据库