Redis实现分布式锁容易漏考虑的3件事【Redis分布式锁】

今天我们来聊一聊分布式锁的那些事。

锁?分享式锁?

相信大家对锁已经不陌生了,我们在多线程环境中,如果需要对同一个资源进行操作,为了避免数据不一致,我们需要在操作共享资源之前进行加锁操作。

在计算机科学中,锁(lock)或互斥(mutex)是一种同步机制,用于在有许多执行线程的环境中强制对资源的访问限制。比如你去相亲,发现你和一大哥同时和一个女的相亲,那怎么行呢…,搞不好还要被揍一顿。

那什么是分布式锁呢。当多个客户端需要争抢锁时,我们就需要分布式锁。这把锁不能是某个客户端本地的锁,否则的话,其它客户端是无法访问的。所以分布式锁是需要存储在共享存储系统中的,比如Redis、Zookeeper等,可以被多个客户端共享访问和获取。

Redis 分布式锁的前提

今天我们就来看一下如何使用 Redis 来实现分布式锁。

在正式开始之前,我们先来了解两个 Redis 的命令:

SETNX key value

这个命名的含义是,当 key 存在时,不做任何赋值操作;当 key 不存在时,就创建 key,并赋值成 value,即(不存在即设置)。

SET key value [EX seconds | PX milliseconds] NX

SET 后加 NX 选项,就和 SETNX 命令类似了,也实现不存在即设置的功能。此外,这个命令在执行时,可以通过 EX 或者 PX 设置键值对的过期时间。

加锁原则

开始之前,先引入一个场景:

假设要给某个商品举行秒杀活动,事先把库存数据 100 已经存入到了 redis 中,现在需要来进行库存扣减。”
如图所示,假设有 1000 个客户端来进行库存扣减操作,那该如何做,才能保证库存扣减顺序一致且不会超扣呢。

在这里插入图片描述
首先想到的就是加锁,在进行库存扣减之前,只有先拿到锁,才能进行扣减,最后再释放锁,没拿到锁就不能扣减,这样保证了一致性。

加锁和释放锁过程

在 redis 中创建一个 lock_key 来代表一个锁变量,它的值表示锁变量的值。

客户端 2 的请求先来了,如果 lock_key 的值为 0,就设置成 1,表示加锁操作。再有新的客户端请求过来,发现值已经为 1 了,所以就返回加锁失败。
在这里插入图片描述
客户端 2 处理完共享资源后,要释放锁,将 lock_key 重新设置为 0。

但这种加锁和释放方式存在不小的问题。通过这种方式加锁包含了三个操作(读取锁变量、判断锁变量的值、把锁变量设为 1)在这个过程中,如果有两个服务同时取到锁值为0后,同时加锁,会存在抢占,不知道到底是哪个加锁的,很难保证并发状态下的原子性。

加锁原子性保证

正是由于 redis 单线程的特点,所以会串行执行请求。遵循先来先加锁的串行特点。所以单点redis本身就可以保证加锁操作的原子性。

有没有一种命令可以把这三步合为一步呢?有!

原生的SETNX命令会在 key 不存在时创建,key 存在时不做任何操作,返回设置失败。该命令本身保证了原子性,把读取和设置值两步过程合为一步,不需要再判断锁的值。

SETNX lock_key 1

正是因为SETNX命令在key 存在时不做任何操作,返回设置失败的特点,所以使用 DEL 命令来删除锁变量,便于下次执行时正常创建锁。

该方案就是最完美的了吗?

这里有两个问题,这也是我面试的时候遇到的灵魂拷问:

问题一、假如某客户端成功加锁后服务挂了,没能释放锁,这个锁就被一直占用着,其它客户端也拿不到锁了。
问题二、假如客户端1加锁,但是客户端2用DEL把锁释放了,又可以被其他客户端抢占导致数据不一致。

高频面试问题

为了问题一、需要给锁变量设置一个过期时间。这样一来,即使持有锁的客户端发生了异常,无法主动的释放锁,Redis 也会根据锁变量的过期时间把它删除。其它客户端在锁变量过期后,就可以重新进行加锁操作了。

对于问题二、需要能区分来自不同客户端的锁操作。给每个客户端生成一个唯一标记值,在进行加锁时,我们把锁变量赋值成这个唯一值。这样在释放锁的时候,客户端需要判断,当前锁变量的值是否和自己的唯一标识相等,在相等的情况下,才能释放锁。

下面来看一下如何在 Redis 中进行实现。我们可以使用 SET 加 EX/PX 和 NX 选项,来进行加锁操作。

SET lock_key uuid NX PX 100
其中lock_key是锁变量,uuid表示客户端的唯一标识,*PX 100表示 100ms 过期。由于我们在释放锁时需要对比客户端的标识和锁变量的值是否一致,这包含了多个操作,为了保证原子性,我们需要使用 lua 脚本,下面是 lua 脚本的实现。

if redis.call("get",KEYS[1]) == ARGV[1] then
   return redis.call("del",KEYS[1])
else  
   return 0
end

原文摘自:https://jishuin.proginn.com/p/763bfbd667d4

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

杵意

谢谢金主打赏呀!!

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

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

打赏作者

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

抵扣说明:

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

余额充值