🚆实现 Redis 单机分布式锁
00 min
2024-11-9
2024-11-9
type
status
date
slug
summary
tags
category
icon
password

什么是锁?锁是为了做什么?

锁(Lock)是一种用于控制对共享资源访问的机制。在计算机科学和信息技术领域,锁通常用于多线程编程和并发控制中,以确保在同一时刻只有一个线程可以访问共享资源,防止多个线程同时修改共享数据而导致数据不一致或错误。
锁的主要目的是解决竞态条件(Race Condition)问题,竞态条件发生在多个线程同时访问共享资源时,最终的结果依赖于线程执行的顺序。通过使用锁,可以限制只有一个线程能够进入临界区(Critical Section),即访问共享资源的部分,从而避免竞态条件的发生。
锁可以分为多种类型,包括互斥锁(Mutex Lock)、读写锁(Read-Write Lock)、自旋锁(Spin Lock)等。不同类型的锁适用于不同的并发场景。例如,互斥锁用于保护临界区,确保同一时刻只有一个线程可以进入,而读写锁则允许多个线程同时读取共享资源,但只有一个线程可以写入共享资源。 总之,锁的主要目的是在多线程环境下确保共享资源的安全访问,防止数据不一致和错误的发生。

为什么需要分布式锁?

保持进程处理的幂等性

notion image
当某一订阅服务消费上游发送的消息进而创建对应的资源,为了保证资源不会被创建多次所以下游只能有一个节点去消费。这里就需要在节点A拿到资源后先对本次操作加锁,当节点B操作时候检查到当前资源已经在处理那么就可以直接将本条消息舍弃,这样不仅避免了资源的多次创建还能及时的舍弃不需要处理的消息进而减少了下游服务无用的消耗。

保持数据的一致性

notion image
基于 redis 分布式锁实现 “秒杀"就是为了保持数据的一致性的一个非常好的例子。当用户1在抢的时候给当前操作进行加锁操作,当用户1操作完毕后再进行解锁。这样就很好地避免了商品超卖的问题。

逐步实现分布式锁

通过上面的内容你应该知道分布式锁的重要性,接下来我将一步一步的带你实现它。

简单的实现

notion image
分布式锁的原理简单的来说就是在操作共享资源前加锁,操作完共享资源后对其进行解锁。所以分布式锁的主要就是加锁&&解锁,我们接下来就看下怎么使用 redis 完成这两种操作。

加锁

为什么使用 SETNX 这个命令来实现加锁操作?
SETNX key value 功能是 如果 key 不存在则设置 value
因为当客户端1在操作共享资源时候,客户端2再来操作我们是不希望他可以操作的这也是分布式锁最重要的属性,为了实现这种互斥性所以使用 SETNX

解锁

基于以上的操作我们就能够简单的实现分布式锁了
notion image
分布式锁就这样地实现了,是不是非常简单。但是,他现在存在很严重的问题,那就是死锁问题。
  1. 程序逻辑异常,服务出错没有释放锁
  1. 进程挂了,未释放锁
以上两种情况都会造成死锁的问题,这样就会导致其他客户端无法成功的获取到锁也就无法成功的操作共享资源。面对这种情况我们要怎么处理呢?

死锁

面对造成死锁的第一种情况我们只需要在代码层面加上异常捕获然后在最后进行解锁即可。但是面对第二种情况我们要怎么处理呢?我们一定会想到给这操作定个时,就像我们早上起不来定个闹钟一样。那我们怎么去定时的删除这个 key 呢?巧了不是,redis 正好就有这样一个命令来给 key 设置过去时间,当达到设置时间会自动删除 key。他就是 EXPIRE
给这个解锁操作加上过期时间,就让分布式锁更加的完美了。但是这就没有其他的问题了吗?答案一定不是的,那还会有什么问题呢?我们来看接下来这个案例:
notion image
上面案例可以看到如果在执行 EXPIRE 命令过程中出现了错误也会导致死锁的问题题,造成这个问题主要原因是因为加锁和设置过期时间两个操作不是原子性操作,如果是原子性操作就一定会一起成功或者失败就不会存在上面这个问题了,我们怎么能保证这两个操作是原子,有两个方式:
  1. redis 2.6.12 之后,redis 扩展了 SET 命令的参数,把 NX/EX 集成到了 SET 命令中,用这一条命令就可以了:
  1. 使用 Lua 脚本执行这两条命令,因为 redis 是单线程的在执行一个 Lua 脚本时,其它请求必须等待,直到这个 Lua 脚本处理完成。
到这里我们就解决了 redis 分布式锁由于各种原因造成死锁的问题了,但是想实现一个健壮的分布式目前还是不够,目前存在一个比较严重的问题就是当客户端1未操作完共享资源但是锁过期了 key 被删除了,此时客户端2再来操作这时他就可以加锁成功并成功操作共享资源。这就导致了最开始我们说的那两种无分布式锁的问题了。那我们应该怎么解决呢?

锁过期时间问题

产生锁过期时间问题主要的问题在于我们无法精准的设置锁的过期时间,我们可以设计这样一个方案:
首先根据操作共享资源的执行平均时间来设置过期时间,然后我们开启一个守护线程定时的去检测锁的过期时间,如果发现锁即将过期并且还未操作完共享资源,那么就对锁进行续期,重新设置锁过期时间。

锁被别人释放

到这里我们已经将分布式锁实现的七七八八了,但是还有一个问题需要我们去解决。上面讲述的加锁解锁的操作都直接去操作并没有检查当前的锁是不是自己的,这就会导致有可能将其他人的锁解锁问题。我们要怎么去解决呢?
很简单是不是,只需要在加锁的时候随机生成一个 uuid 然后设置到 value 中
然后在解锁之前获取 value 比对是否和加锁前是否相同就可以了。
但是,上面解锁过程中又是两个操作,很有可能还会造成原子性的问题。所以我们还需要使用 Lua 脚本执行这两条命令。

总结

到这里我们就完美的构建了 redis 分布式锁,但是我们上面构建是基于 redis 单机的。如果是 redis 集群的话还会存在很多问题,例如:主从同步对 redis 分布式锁的影响, 等等。如果想使用 redis 集群的话可以参考下 Redlock 的实现方式,这里就不详细的展开了,感兴趣的同学可以自己去了解下。
上一篇
HTTPS 的前世今生
下一篇
MySQL 事务隔离级别和MVCC