深入理解分布式锁——以Redis为例

一、分布式锁简介

1. 什么是分布式锁

分布式锁是一种在分布式系统环境下,通过多个节点对共享资源进行访问控制的一种同步机制。它的主要目的是防止多个节点同时操作同一份数据,从而避免数据的不一致性。

  • 线程锁 : 也被称为互斥锁(Mutex),主要用于控制同一进程中的多个线程对共享资源的访问。
  • 进程锁 进程锁是用于控制同一台机器上的多个进程对共享资源的访问。进程锁可以是系统级的,如文件锁,也可以是用户级的,如信号量(Semaphore)。
  • 分布式锁 分布式锁是用于控制分布式系统中的多个节点对共享资源的访问。由于分布式系统中的节点可能位于不同的机器甚至不同的地理位置,因此分布式锁的实现比线程锁和进程锁要复杂得多。分布式锁需要在网络中的多个节点之间进行协调,以保证锁的唯一性和一致性。
2. 分布式锁的特性

分布式锁主要有以下几个特性:

  • 互斥性:在任何时刻,只有一个节点可以持有锁。
  • 不会发生死锁:如果一个节点崩溃,锁可以被其他节点获取。
  • 公平性:如果多个节点同时申请锁,系统应该保证每个节点都有获取锁的机会。
  • 可重入性:同一个节点可以多次获取同一个锁,而不会被阻塞。
  • 高可用:锁服务应该是高可用的,不能因为锁服务的故障而影响整个系统的运行。

二、分布式锁的基本原理

1. 分布式锁的基本步骤

分布式锁的基本原理可以分为以下几个步骤:

一般,在实现Redis分布式锁时,不分开使用SETNX和EXPIRE命令,而是使用SETNX的拓展命令 SET NX EX

示例:

  1. 请求锁:当一个实例需要访问共享资源时,它会向分布式锁系统发送一个请求,试图获取一个锁。

  2. 锁定资源:分布式锁系统会检查是否有其他实例已经持有这个锁。如果没有,那么这个实例就会获得锁,并且有权访问共享资源。如果有,那么这个实例就必须等待,直到锁被释放。

  3. 访问资源:一旦实例获取了锁,它就可以安全地访问共享资源,而不用担心其他实例会同时访问这个资源。

  4. 释放锁 :当实例完成对共享资源的访问后,它需要通知分布式锁系统释放锁。这样,其他正在等待的实例就可以获取锁,访问共享资源。

    2. 分布式锁实现的关键点

    在实现分布式锁时,通常会有一个中心节点(或者称为锁服务),所有需要获取锁的节点都需要向这个中心节点申请。

    当一个节点申请锁时,中心节点会检查当前是否有其他节点持有锁,如果没有,则将锁分配给申请的节点;如果有,则拒绝申请。当持有锁的节点完成操作后,会向中心节点归还锁,此时其他的节点可以再次申请锁。

    三、基于Redis的分布式锁

    1. Redis的基本介绍

    Redis是一个开源的,内存中的数据结构存储系统,它可以用作数据库、缓存和消息代理。

    Redis 提供了多种命令和能力来支持实现分布式锁

  5. SETNX 命令:SETNX(Set if Not Exists)命令用于在 key 不存在时设置值。这是实现分布式锁的关键命令,因为它能确保在同一时间只有一个客户端能够获得锁。

  6. EXPIRE 命令:EXPIRE 命令用于为 key 设置过期时间。这对于避免死锁非常重要,因为即使某个客户端崩溃,锁也会在一定时间后自动释放。

  7. DEL 命令:DEL 命令用于删除 key。在释放锁时,需要使用此命令删除对应的 key。

  8. Lua 脚本:Redis 支持使用 Lua 脚本来执行一系列原子操作。这对于实现安全的分布式锁非常有用,因为它可以确保在释放锁时检查锁的持有者。

  9. RedLock 算法:Redis 官方推荐了一种名为 RedLock 的分布式锁算法。RedLock 是一种基于多个 Redis 实例的分布式锁算法,旨在提供更高的安全性和容错能力。

    SET my_key my_value NX EX 10 # 设置键值对, 超时时间为10s。 如果my_key存在,则不进行任何操作

2. Redis实现分布式锁的基本实现

请求锁

假设我们有一个 Redis 键 my_lock,用于表示锁的状态。当一个客户端想要获取锁时,它会尝试使用 SETNX 命令来设置这个键。

复制代码
SET my_lock<unique_value> NX EX <lock_timeout>

如果命令返回 OK,则表示客户端成功获取了锁。如果返回 nil,则表示锁已被其他客户端持有。

  • <unique_value>: 一个唯一的值,比如 UUID,用于标识锁的持有者。
  • NX: 只有当 my_lock 不存在时,才会设置该键。这确保了同一时间只有一个客户端能获得锁。
  • EX <lock_timeout>: 设置锁的过期时间,防止因客户端崩溃而导致的死锁。

锁续期

为了防止锁过早地因为过期而被释放,可以在锁快到期时进行续期操作。这可以通过定期检查锁的剩余时间,并在必要时使用 EXPIRE 命令来更新过期时间来实现。

复制代码
# 检查锁是否仍由当前客户端持有
if redis.call("get", "my_lock") ==<unique_value>" then
    # 续期锁
    redis.call("EXPIRE", "my_lock", <new_lock_timeout>)
end

注意:上述代码是一个简化的 Lua 脚本示例,实际应用中可能需要更复杂的逻辑来处理续期操作。
  • 释放锁 当客户端完成需要加锁保护的操作后,应该释放锁。为了确保只有锁的持有者才能释放锁,可以使用 Lua 脚本来执行释放操作。

    if redis.call("get", "my_lock") ==<unique_value>" then
    return redis.call("del", "my_lock")
    else
    return 0 -- 锁未被当前客户端持有,无法释放
    end

    这个 Lua 脚本首先检查锁是否仍由当前客户端持有,如果是,则删除 my_lock 键以释放锁。

3. Redis分布式锁的使用场景

Redis分布式锁可以用于所有需要在分布式环境中同步访问共享资源的场景。例如,电商秒杀活动中,为了防止超卖,可以使用Redis分布式锁来保证同一时刻只有一个请求可以操作库存。又如,在分布式计算中,为了防止重复计算,可以使用Redis分布式锁来保证同一时刻只有一个节点可以进行计算。

4. Redis分布式锁的优点和缺点

优点:

  • 性能高:由于Redis是基于内存的,因此Redis分布式锁的性能非常高。
  • 实现简单:Redis提供的命令可以很容易地实现分布式锁。

缺点:

  • 不可重入:Redis分布式锁默认是不可重入的,如果需要可重入,需要额外的逻辑来实现。
  • 非阻塞:Redis分布式锁是非阻塞的,如果获取锁失败,需要自己进行重试。
  • 安全性:如果Redis服务器出现故障,可能会导致锁无法正常工作。

四、其他分布式锁的实现方式

1. 基于数据库的分布式锁

数据库分布式锁是通过在数据库中创建一个锁表,表中包含锁的名称和锁的状态等信息。

当一个节点需要获取锁时,它会在这个表中插入一条记录,如果插入成功,那么这个节点就获取到了锁。当节点使用完锁后,会删除这条记录,从而释放锁。

这种方式的优点是实现简单,缺点是性能较低,且如果数据库出现故障,可能会影响到锁的功能。

2. 基于Zookeeper的分布式锁

Zookeeper是一个开源的分布式协调服务,它提供了一种高效且可靠的分布式锁实现方式。

在Zookeeper中,可以创建一个临时节点作为锁,当一个节点需要获取锁时,它会尝试创建这个临时节点,如果创建成功,那么这个节点就获取到了锁。

当节点使用完锁后,会删除这个临时节点,从而释放锁。如果节点崩溃,Zookeeper会自动删除这个临时节点,从而避免了死锁的问题。

3. 基于Etcd的分布式锁

Etcd是一个开源的分布式键值存储系统,它也提供了一种分布式锁的实现方式。

Etcd的分布式锁是通过创建一个带有TTL(Time To Live)的键值对来实现的,当一个节点需要获取锁时,它会尝试创建这个键值对,如果创建成功,那么这个节点就获取到了锁。

当节点使用完锁后,会删除这个键值对,从而释放锁。如果节点崩溃,Etcd会自动删除这个键值对,从而避免了死锁的问题。

4. 各种实现方式的比较
分布式锁类型 优点 缺点
基于Redis 性能高、实现简单 不可重入、非阻塞
基于数据库 易于实现,无需额外依赖 性能较差,数据库瓶颈
基于Zookeeper 高性能,高可靠性,有顺序一致性保证 需要额外安装和维护Zookeeper集群
基于Etcd 简单易用,高可用性,弹性扩展,有顺序一致性保证 需要额外安装和维护etcd集群

在选择分布式锁的实现方式时,需要根据具体的应用场景和需求来决定。

五、分布式锁的常见问题和解决方案

1. 死锁问题
  • 问题: 当一个客户端获取了锁,但由于某些原因(如程序崩溃、异常等)无法释放锁时,会导致其他客户端永远无法获取锁。
  • 解决方案: 设置锁的过期时间。当锁的持有者未能在过期时间内执行完毕并释放锁时,锁将自动过期,从而允许其他客户端获取锁。
2. 锁续命问题
  • 问题: 如果一个操作需要的时间可能超过锁的过期时间,那么在操作执行过程中锁过期会导致其他客户端获取到锁,从而产生并发问题。 解决方案: 使用锁续命机制。在锁持有者执行操作期间,可以定期检查锁是否即将过期,并在适当的时候对锁进行续命,即重新设置锁的过期时间。
3. 锁释放问题
  • 问题: 为确保数据的一致性,只有锁的持有者才能释放锁。但在实际应用中,可能会出现误解锁的情况。
  • 解决方案: 在设置锁时,为锁关联一个唯一的值(如UUID)。在释放锁时,先检查锁的值是否与当前客户端的值匹配,如果匹配则释放锁,否则不做任何操作。注意,锁持有人的判断和锁的释放应该在一个原子操作内完成。
4. 锁的公平性问题
  • 问题: 在高并发环境中,如果多个节点同时请求获取锁,可能会出现"饥饿"现象,即某些节点长时间无法获取到锁。
  • 解决方案: 引入队列,将请求锁的节点按照顺序排队。例如,在Zookeeper中,可以使用顺序节点来实现公平锁。
5. 锁的可重入性问题
  • 问题: 在某些场景中,一个节点可能需要多次获取同一个锁,如果锁不支持重入,可能会导致死锁。
  • 解决方案: 为锁添加一个拥有者的概念,只有锁的拥有者才能再次获取到锁。例如,在Redis中,可以将锁的值设置为节点的唯一标识,获取锁时检查锁的值是否为自己的标识。
6. 锁的安全性问题
  • 问题: 如果分布式锁的存储系统(如Redis、Zookeeper等)出现故障,可能会导致锁无法正常工作。
  • 解决方案: 使用高可用的存储系统,如使用Redis集群或Zookeeper集群。另外,可以使用心跳机制来检测存储系统的状态,如果检测到故障,可以及时进行切换。

六、参考文件

  1. Distributed Locks with Redis
相关推荐
倔强的石头_7 小时前
kingbase备份与恢复实战(二)—— sys_dump库级逻辑备份与恢复(Windows详细步骤)
数据库
jiayou642 天前
KingbaseES 实战:深度解析数据库对象访问权限管理
数据库
李广坤2 天前
MySQL 大表字段变更实践(改名 + 改类型 + 改长度)
数据库
初次攀爬者3 天前
ZooKeeper 实现分布式锁的两种方式
分布式·后端·zookeeper
爱可生开源社区3 天前
2026 年,优秀的 DBA 需要具备哪些素质?
数据库·人工智能·dba
随逸1774 天前
《从零搭建NestJS项目》
数据库·typescript
加号34 天前
windows系统下mysql多源数据库同步部署
数据库·windows·mysql
シ風箏4 天前
MySQL【部署 04】Docker部署 MySQL8.0.32 版本(网盘镜像及启动命令分享)
数据库·mysql·docker
李慕婉学姐4 天前
Springboot智慧社区系统设计与开发6n99s526(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面。
数据库·spring boot·后端
百锦再4 天前
Django实现接口token检测的实现方案
数据库·python·django·sqlite·flask·fastapi·pip