Redis分布式锁:保障微服务架构下的并发控制与数据一致性实战指南

目录

    

一、分布式锁简介   

二、Redis 实现的分布式锁

        2.1 单机 Redis 加锁流程

三、多 Redis 实例实现高可靠的分布式锁

        3.1 Redlock

        3.2 Redission​​​​​​​    


一、分布式锁简介   

        分布式锁是一种在分布式系统中实现锁机制的技术,用于控制多节点对共享资源的访问,确保在并发环境下能够正确地实现互斥访问。在传统的单机环境下,我们可以使用线程锁来解决并发控制问题,但在分布式环境下,由于多个节点间物理分离,无法直接使用线程锁,因此需要设计一套能够在分布式系统中工作的锁服务。

        分布式锁需要一个单独的分布式系统来实现,可以实现分布式锁的主要方式有:Redis、Zookeeper、基于数据库实现等。本文主要介绍基于 Redis 实现的分布式锁。

        分布式锁应该具有以下特性:

  • 互斥性:确保同一时刻只有一个客户端持有锁
  • 避免死锁:锁在客户端异常或网络中断后能自动释放
  • 容错性:在部分节点失效的情况下,锁依然可以正常工作
  • 可重入性:支持一个客户端再持有锁的情况下能再次获取锁
  • 公平性:按申请顺序依次分配锁资源

二、Redis 实现的分布式锁

        对于在单机上运行的多线程程序来说,锁本身其实就是一个变量,比如用变量 0 表示没有现成获取锁,此时可以获取锁;变量值为 1 时,表示已经有线程持有了锁。

        和单机上多线程加锁的方式类似,Redis 分布式锁同样可以用一个变量来实现。但是,和线程在单机上操作锁不同的是,在分布式场景下,锁变量需要由一个共享存储系统来维护,只有这样,多个客户端才可以通过访问共享存储系统来访问锁变量。相应的,加锁和释放锁的操作就变成了读取、判断和设置共享存储系统中的锁变量值。

        这样一来,实现分布式的要求如下:

  • 要求一:分布式锁的加锁和释放锁过程涉及多个步骤。读取、判断、设置这三个步骤要保证原子操作,不能被打断。
  • 要求二:Redis 中保存了锁变量,如果 Redis 挂了,那么客户端无法操作分布式锁,这时要保证其可靠性。

        2.1 单机 Redis 加锁流程

        作为分布式锁实现过程中的共享存储系统,Redis 可以使用键值对来保存锁变量,再接收和处理不同客户端发送的加锁和释放锁的操作请求。

        客户端 A 和 C 同时请求加锁。因为 Redis 使用单线程处理请求,所以,即使客户端 A 和 C 同时把加锁请求发给了 Redis,Redis 也会串行处理它们的请求。假设 Redis 先处理客户端 A 的请求,读取 lock_key 的值,发现 lock_key 为 0,所以,Redis 就把 lock_key 的 value 置为 1,表示已经加锁了。紧接着,Redis 处理客户端 C 的请求,此时,Redis 会发现 lock_key 的值已经为 1 了,所以就返回加锁失败的信息。

        加锁的流程包含三个步骤,分别是:读取、判断、设置。Redis 中要想保证操作的原子性,有两种通用的方法,分别是使用 Redis 的单命令操作和使用 Lua 脚本。那么,在分布式加锁场景下,该怎么应用这两个方法呢?

        先来看下,Redis 可以用哪些单命令操作实现加锁操作。

        首先是 SETNX 命令,它用于设置键值对的值。具体来说,就是这个命令在执行时会判断键值对是否存在,如果不存在,就设置键值对的值,如果存在,就不做任何设置。

SETNX key value

        对于释放锁操作来说,我们可以在执行完业务逻辑后,使用 DEL 命令删除锁变量。不过,你不用担心锁变量被删除后,其他客户端无法请求加锁了。因为 SETNX 命令在执行时,如果要设置的键值对(也就是锁变量)不存在,SETNX 命令会先创建键值对,然后设置它的值。所以,释放锁之后,再有客户端请求加锁时,SETNX 命令会创建保存锁变量的键值对,并设置锁变量的值,完成加锁。

        不过,使用 SETNX 和 DEL 命令组合实现分布锁,存在两个潜在的风险。

  1. 假如某个客户端在执行了 SETNX 命令、加锁之后,紧接着却在操作共享数据时发生了异常,结果一直没有执行最后的 DEL 命令释放锁。因此,锁就一直被这个客户端持有,其它客户端无法拿到锁,也无法访问共享数据和执行后续操作,这会给业务应用带来影响。针对这个问题,一个有效的解决方法是,给锁变量设置一个过期时间。这里留一道思考题:锁设定的时间多少才算合适呢?
  2. 如果客户端 A 执行了 SETNX 命令加锁后,假设客户端 B 执行了 DEL 命令释放锁,此时,客户端 A 的锁就被误释放了。如果客户端 C 正好也在申请加锁,就可以成功获得锁,进而开始操作共享数据。这样一来,客户端 A 和 C 同时在对共享数据进行操作,数据就会被修改错误,这也是业务层不能接受的。针对这个问题,我们可以再设置 value 值时,设置上客户端的唯一标识,释放锁时,只能释放自己加的锁。

        为了能达到和 SETNX 命令一样的效果,Redis 给 SET 命令提供了类似的选项 NX,用来实现“不存在即设置”。如果使用了 NX 选项,SET 命令只有在键值对不存在时,才会进行设置,否则不做赋值操作。此外,SET 命令在执行时还可以带上 EX 或 PX 选项,用来设置键值对的过期时间。

        举个例子,执行下面的命令时,只有 key 不存在时,SET 才会创建 key,并对 key 进行赋值。另外,key 的存活时间由 seconds 或者 milliseconds 选项值来决定。

SET key value [EX seconds | PX milliseconds]  [NX]

        有了 SET 命令的 NX 和 EX/PX 选项后,我们就可以用下面的命令来实现加锁操作了。

// 加锁, unique_value作为客户端唯一性的标识
SET lock_key unique_value NX PX 10000

        其中,unique_value 是客户端的唯一标识,可以用一个随机生成的字符串来表示,PX 10000 则表示 lock_key 会在 10s 后过期,以免客户端在这期间发生异常而无法释放锁。

        因为在加锁操作中,每个客户端都使用了一个唯一标识,所以在释放锁操作时,我们需要判断锁变量的值,是否等于执行释放锁操作的客户端的唯一标识,如下所示:

//释放锁 比较unique_value是否相等,避免误释放
if redis.call("get",KEYS[1]) == ARGV[1] then
    return redis.call("del",KEYS[1])
else
    return 0
end

        在释放锁操作中,我们使用了 Lua 脚本,这是因为,释放锁操作的逻辑也包含了读取锁变量、判断值、删除锁变量的多个操作,而 Redis 在执行 Lua 脚本时,可以以原子性的方式执行,从而保证了锁释放操作的原子性。

三、多 Redis 实例实现高可靠的分布式锁

        当要实现高可靠的分布式锁时,就不能只依赖单个的命令操作了,需要按照一定的步骤和规则进行加解锁操作,否则,就可能会出现锁无法工作的情况。“一定的步骤和规则”是指啥呢?其实就是分布式锁的算法。

        为了避免 Redis 实例故障而导致的锁无法工作的问题,Redis 的开发者 Antirez 提出了分布式锁算法 Redlock。

        3.1 Redlock

        首先要明确,Redlock 并不是一个直接的具体软件库,而是一种理论上的分布式锁涉及策略。

        Redlock 算法的基本思路,是让客户端和多个独立的 Redis 实例依次请求加锁,如果客户端能够和半数以上的实例成功地完成加锁操作,那么就认为,客户端成功地获得分布式锁了,否则加锁失败。这样一来,即使有单个 Redis 实例发生故障,因为锁变量在其它实例上也有保存,所以,客户端仍然可以正常地进行锁操作,锁变量并不会丢失。

        我们来具体看下 Redlock 算法的执行步骤。Redlock 算法的实现需要有 N 个独立的 Redis 实例。接下来,我们可以分成 3 步来完成加锁操作。

  1. 客户端获取当前时间。
  2. 客户端按顺序依次向N个 Redis 实例执行加锁操作。这里的加锁操作和在单实例上执行的加锁操作一样,使用set命令,带上NX、EX/PX选项,以及带上客户端的唯一标识。如果客户端在和一个 Redis 实例请求加锁时,一直到超时都没有成功,那么此时,客户端会和下一个 Redis 实例继续请求加锁。加锁操作的超时时间需要远远地小于锁的有效时间,一般也就是设置为几十毫秒。
  3. 一旦客户端完成了和所有 Redis 实例的加锁操作,客户端就要计算整个加锁过程总耗时。

        客户端只有在满足下面两个条件时,才能认为加锁成功:

  • 客户端从超过半数的 Redis 实例上成功获取到锁;
  • 客户端获取锁的总耗时没有超过锁的有效时间。

        在满足了这两个条件后,我们需要重新计算这把锁的有效时间,计算的结果是锁的最初有效时间减去客户端为获取锁的总耗时。如果锁的有效时间已经来不及完成共享数据的操作了,我们可以释放锁,以免出现还没完成数据操作,锁就过期了的情况。  

        当然,如果客户端在和所有实例执行完加锁操作后,没能同时满足这两个条件,那么,客户端向所有 Redis 节点发起释放锁的操作。   

        这种方案需要多个 Redis 集群,成本比较高。

        3.2 Redission

        Redission 是一个在 Java 中广泛使用的 Redis 客户端库,它是 Redlock 的具体实现。它不仅提供了一系列的分布式的java常用对象,还实现了可重入锁(Reentrant Lock)、公平锁(Fair Lock)、联锁(MultiLock)、红锁(RedLock)、读写锁(ReadWriteLock)等,还提供了许多分布式服务。

        Redisson支持单点模式、主从模式、哨兵模式、集群模式,这里以单点模式为例看下具体实现:

// 1.构造redisson实现分布式锁必要的Config
Config config = new Config();
config.useSingleServer().setAddress("redis://127.0.0.1:6379").setPassword("123456").setDatabase(0);
// 2.构造RedissonClient
RedissonClient redissonClient = Redisson.create(config);
// 3.获取锁对象实例(无法保证是按线程的顺序获取到)
RLock rLock = redissonClient.getLock(lockKey);
try {
    /**
     * 4.尝试获取锁
     * waitTimeout 尝试获取锁的最大等待时间,超过这个值,则认为获取锁失败
     * leaseTime   锁的持有时间,超过这个时间锁会自动失效(值应设置为大于业务处理的时间,确保在锁有效期内业务能处理完)
     */
    boolean res = rLock.tryLock((long)waitTimeout, (long)leaseTime, TimeUnit.SECONDS);
    // 设置锁的过期时间时可以设置自动续期,通过以下方式设置
    // boolean res = lock.lock(10, TimeUnit.SECONDS, 5, TimeUnit.SECONDS); // 第一个参数是锁的初始过期时间,第三个参数是自动续期间隔
    if (res) {
        //成功获得锁,在这里处理业务
    }
} catch (Exception e) {
    throw new RuntimeException("aquire lock fail");
}finally{
    //无论如何, 最后都要解锁
    rLock.unlock();
}

        在使用分布式锁时,锁设置多长时间,要经过严格的评估,如果业务流程还没执行完,锁就过期了,那肯定会影响正常的业务,一般情况下,设置的过期时间都要略长于代码执行的时间,不过 Redission 为我们提供了一种更友好的方式,在执行代码的过程中你可以对锁进行自动续期,以延长锁的持有时间。

        总之,在使用分布式锁的过程中要注意锁超时、死锁、误删等问题。使用分布式锁时要规避各种问题才能提高可靠性。

往期经典推荐

探析Drools规则引擎的工作机制-CSDN博客

Redis使用规范的最佳实践:打造高性能与稳定性的关键法则-CSDN博客

Spring循环依赖的成因与破局-CSDN博客

决胜微服务架构:OpenFeign轻量级REST客户端的魅力解析_openfeign rest-CSDN博客

Redis高性能的秘密原来在这,超越想象-CSDN博客

  • 13
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
### 回答1: 在高并发下,可以使用 Redis 的 SETNX 命令来实现分布式锁。SETNX 指令是将 key 的值设为 value,当且仅当 key 不存在。 步骤如下: 1. 使用 SETNX 命令尝试获取锁,如果返回值为 1,则说明获取锁成功。 2. 使用 EXPIRE 命令为锁设置过期时间,防止死锁。 3. 在业务代码执行完成后,使用 DEL 命令释放锁。 为了防止网络延迟等问题,可以在获取锁时设置一个随机值,并在释放锁时判断该值是否与当前锁对应的值相同,以确保只有持有锁的客户端才能释放锁。 ### 回答2: 高并发下Redis分布式锁实现是通过Redis的setnx命令和expire命令来实现的。 首先,通过setnx命令尝试设置一个带有过期时间的key,如果成功设置,则表示获取到了分布式锁。如果设置失败,说明锁已经被其他客户端占用,需要等待或进行重试。 为了防止因为某个客户端处理时间过长而导致锁过期的情况,可以为锁的过期时间设置一个合理的值。在获取到锁后,可以使用expire命令为锁的key设置过期时间,确保在一定时间内释放锁。 为了提高锁的安全性,可以为每个客户端设置一个唯一的ID作为锁的值,并将锁名称与该ID进行绑定。这样可以确保只有获取锁的客户端才能释放锁,防止其他客户端误释放锁。 另外,考虑到高并发情况下的锁竞争,可以在获取锁失败后进行等待一段时间再进行重试,避免频繁的锁竞争对系统性能造成负面影响。 需要注意的是,Redis分布式锁实现并不能解决所有并发问题,仅适用于单个业务场景下的加锁和释放锁操作。在设计和使用时需要考虑到具体业务需求和场景,并进行适当的优化和调整。 ### 回答3: 在高并发场景下,为了保证数据一致性和并发执行的正确性,常常需要使用分布式锁来控制对共享资源的访问。Redis作为一个高性能的内存键值存储系统,也可以用来实现分布式锁Redis实现分布式锁的一种常见方式是使用SETNX命令。当多个客户端同时尝试获取锁时,只有一个客户端能成功执行SETNX操作,即将对应的key设置为1,表示获取了锁。其他客户端获取锁失败,需要等待锁被释放后重新尝试。 为了保证锁的正确性和防止死锁,还需要为锁设置一个合理的过期时间,以防止获取锁的客户端因为异常情况导致无法及时释放锁。 在高并发环境下,为了提高锁的性能,可以考虑使用红锁机制。红锁是将分布式锁Redis的复制功能相结合,确保在大部分节点上都获取到锁后,才认为锁已经获得。这样可以避免某个节点出现故障或网络异常导致锁丢失的情况。 另外,为了避免因为程序异常导致锁无法释放的情况,可以在获取到锁之后,使用Lua脚本来保证在一个原子操作中判断锁的状态并释放锁。 在实现分布式锁时,还需要考虑高并发下锁的性能问题。可以通过优化Redis的部署结构、增加Redis的内存和CPU资源,以及使用连接池等方法来提高Redis的性能和并发能力,从而提高分布式锁的性能。 综上所述,高并发下的Redis分布式锁实现主要包括使用SETNX命令获取锁、设置合理的超时时间、使用红锁机制增强锁的可靠性、使用Lua脚本保证原子操作、优化Redis的性能和并发能力等方面。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

超越不平凡

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值