分布式锁
锁是什么?
在讲解分布式锁之前,我们首先得了解什么是“锁”。你可以把“锁”想象成一种控制机制,它用来保护共享资源,确保在同一时刻只有一个人能够访问它。
假设你和朋友都想借图书馆里的唯一一本热门书。这本书只能借给一个人,如果你们两个人同时去借,可能会出错。为了避免这种情况,图书馆管理员会给这本书上锁,只有第一个去借的人可以拿走书,其他人必须等这本书归还后才能借到。
同样,在计算机程序中,多个线程或进程可能需要同时访问共享资源(比如一个数据库、一台打印机等)。为了避免多个进程同时操作同一个资源,程序就需要使用“锁”来保证同一时刻只有一个进程可以访问。
什么是分布式锁?
“分布式”指的是系统中的资源分布在不同的机器或节点上。也就是说,分布式锁是在多个不同计算机上使用的一种锁。当一个进程想要访问共享资源时,它必须先“获取”锁,而这个锁并不是单纯地在同一台机器上进行管理,而是跨越了多个服务器或者节点。
想象一下,你和朋友不仅仅在同一个图书馆,而是每个人都在不同的城市有各自的图书馆分馆。你们都想借到这本热门书,而这本书只存在其中一座分馆。如果你们不使用分布式锁,就有可能发生两个人同时借到这本书的错误。这时候,分布式锁就帮助你确保无论你们在哪里,只有一个人能借到书,其他人必须等待。
为什么需要分布式锁?
在单机系统中,资源都在同一台机器上,锁的管理也比较简单,操作系统可以很容易地控制哪些进程可以访问共享资源。但是,当系统变得更大,资源分布在不同的机器上时,情况就复杂了。这时就需要一种能够跨多个机器协调访问共享资源的机制——分布式锁。
为什么不能只用单机锁?
- 分布式系统:在多个服务器中运行的应用程序通常会分配到不同的机器上,资源也可能分散在不同的地方。单机锁无法跨机器控制对资源的访问,因此需要分布式锁。
- 高并发:随着应用的规模和用户的增多,访问共享资源的请求变得非常频繁。分布式锁能够有效地控制资源的访问,避免数据的冲突和不一致。
为什么用 redis 来实现分布式锁?
那么,为什么 Redis 会成为实现分布式锁的优选方案呢?答案在于 Redis 的高性能和原子性操作。
首先,Redis 是一个内存数据库,读写速度非常快,这使得它可以作为锁的存储介质。通过 Redis 存储锁信息,可以确保加锁和释放锁操作的高效性。
而且 Redis 提供了原子性的命令,如 SETNX
(只有键不存在时才会设置成功)。通过这个命令,我们可以很容易地实现加锁操作。比如,当某个进程试图获取锁时,它会使用 SETNX
命令在 Redis 中设置一个标识,如果该标识已经存在,说明锁已经被占用,当前进程无法获取锁。
与此同时,Redis 提供了主从复制、持久化和分布式集群等特性,可以确保即使某个 Redis 实例出现故障,整个系统依然可以稳定运行。这样,Redis 在分布式环境中的可靠性得到了保证。
redis 是如何实现分布式锁的?
要实现 Redis 分布式锁,基本的操作流程包括加锁、使用锁、以及释放锁。
- 加锁:进程尝试通过 Redis 的
SETNX
命令在 Redis 中设置一个键(例如lock:key
)。SETNX
命令只有在该键不存在时才会设置成功。如果 Redis 中已经存在该键,表示锁已经被占用,当前进程就无法获取锁。 - 使用锁:一旦锁被获取,进程就可以开始执行需要保护的操作了。比如,某个进程可以安全地修改数据库中的数据,其他进程无法在此时修改相同的数据。
- 释放锁:当进程完成操作后,它会从 Redis 中删除锁(即删除
lock:key
)。这样,其他进程就可以再次尝试获取锁,执行自己的操作。
了解完操作流程之后,我们来分析一下这个具体的命令:
SET lock_key unique_value NX EX 30
- lock_key:锁的键,可以使用资源的标识符(比如文章 ID、商品 ID)作为键的一部分。
- unique_value:锁的值,一般可以是一个唯一的值,比如客户端的标识(例如 UUID)。确保同一个客户端不会在同一时刻获取多个锁。
- NX:仅在键不存在时设置锁(即只有没有获取锁的其他客户端时才可以获取锁)。
- EX 30:设置锁的过期时间为 30 秒,防止因某些原因客户端崩溃或者无法释放锁而导致死锁。
上述的这条命令是用来加锁的,那么如何实现解锁呢?
实际上,解锁的过程就是将 lock_key 这个键删除的过程。但是在删除的时候,需要先判断 unique_value 是否为客户端,如果是,则可以删除,否则就无法删除。也就是说,这个锁是谁加的,谁才有权利解锁。
解锁的过程是先判断,再删除,所以需要用到 Lua 脚本来实现原子性:
//释放锁时,先比较 unique_value 是否相等,避免锁的误释放
if redis.call("get",KEYS[1])== ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
通过使用 String 的 SET 命令和 Lua 脚本,就实现了分布式锁。
虽然 Redis 提供了一个简单的方式来实现分布式锁,但当我们在实际应用的时候,我们需要注意一些细节。
- 锁的超时设置: 如果进程在获得锁后崩溃,锁将永远不会被释放。为了解决这个问题,我们可以在加锁时设置一个过期时间(例如 30 秒)。这样,即使进程崩溃,锁也会在一定时间后自动失效,其他进程就可以获得锁。
- 避免死锁: 死锁问题发生在进程持有锁的同时又发生了其他问题(比如没有及时释放锁),导致锁无法释放,其他进程永远无法获取锁。为了避免死锁,我们可以使用带有过期时间的锁,这样即使进程发生故障,锁也会被自动清除。
- 锁的唯一性: 分布式系统中的锁需要确保是唯一的。通常,我们会使用一个唯一的标识符作为锁的名称(例如
lock_key
),确保每个资源的锁是唯一的。如果每个进程都使用相同的键去加锁,就能确保资源不会被多个进程同时访问。
redis 分布式锁有什么优缺点?
优点我们可以结合 Redis 的特性来考虑,因为Redis 的操作非常快速,能够承载大量并发请求,所以适合在高并发环境下使用。而且我们通过使用 SETNX 命令,实现分布式锁比较简单。另外,Redis 支持主从复制和分布式集群,还可以保证锁服务的高可用性。
但是,如果 Redis 宕机或者出现故障,可能会导致锁丢失。为了保证锁的可靠性,可以使用 Redis 的持久化机制或其他高可用策略。而且,如果一个进程在持有锁的过程中出现故障或没有及时释放锁,可能会导致其他进程永远无法获取锁。