Redis:代码实战之锁和原子操作

【关于作者】

关于作者,目前在蚂蚁金服搬砖任职,在支付宝营销投放领域工作了多年,目前在专注于内存数据库相关的应用学习,如果你有任何技术交流或大厂内推及面试咨询,都可以从我的个人博客(https://0522-isniceday.top/)联系我

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Lckr7xiE-1681039834044)(https://zhangyuxiangplus.oss-cn-hangzhou.aliyuncs.com/boke/锁和原子操作.png)]

使用redis时,碰到并发有两种处理方式

  • 第一种:看是否能够使用原子操作
  • 第二种:分布式锁

为什么需要优先第一种呢,因为我们需要考虑到分布式锁会降低并发度,并且分布式锁需要使用到其余的系统(共享存储系统)来实现

而原子操作则无需加锁,既能控制并发度,也能够减少对系统并发性的影响

1.Redis的两种原子操作方法

  • (1)把多个操作放在redis中实现成一个操作,也就是单命令操作

    例如:INCR/DECRSETNX命令,其将读取-修改-写入合并为了一个操作,而redis本身就是单线程的,命令与命令之间是互斥的,所以该操作符合原子性

  • (2)把多个命令写到lua脚本中,以原子性的方式执行单个脚本

    Redis会把整个Lua脚本作为一个整体执行,在执行的过程中不会被其他命令打断,从而保证了Lua脚本中操作的原子性

    另外使用lua脚本时,还有一些注意点:

    1、lua 脚本尽量只编写通用的逻辑代码,避免直接写死变量。变量通过外部调用方传递进来,这样 lua 脚本的可复用度更高。

    2、建议先使用SCRIPT LOAD命令把 lua 脚本加载到 Redis 中,然后得到一个脚本唯一摘要值,再通过EVALSHA命令 + 脚本摘要值来执行脚本,这样可以避免每次发送脚本内容到 Redis,减少网络开销。

2.Redis实现分布式锁

2.1.分布式锁的概念

分布式锁类似于单机上的锁,通过一个变量来维护,加锁操作实际上就是判断该变量是否为0,释放锁操作实际上就是将该变量置为0。

只是分布式锁的变量变为了一个共享变量,关于分布式锁的要求如下:

  • 互斥性:加锁和释放锁过程涉及多个操作,需要保证操作的原子性,只允许一个请求拿到锁

  • 可靠性:需要保证依赖的共享系统的可靠性,进而保证锁的可靠性

  • 释放锁只能够释放自己加的锁

2.2.Redis分布式锁的实现

2.2.1.基于单个redis节点实现分布式锁

  • 加锁操作:redis通过键值对lock_key:0保存变量,后续其他加锁请求发现该键值对存在,则返回失败标识

加锁过程包含读取、判断变量值、修改锁变量值为1三个过程,redis是如何保证原子性的:

SETNX命令:redis先判断当前key是否存在,如果不存就创建该键值,存在就直接返回,redis的具体实现如下

SET key value [EX seconds | PX milliseconds]  [NX]
  • 释放锁操作:执行完业务流程后,使用DEL删除锁变量,释放锁过程包含了读取锁变量、判断值、删除锁变量多个操作,因此使用Lua脚本,原子性的方式执行保证释放锁操作的原子性

  • 使用SETNX+DEL实现分布式锁存在的问题

    • (1)可能申请了锁但是没有正常的释放,就会导致其他请求无法申请锁

      解决办法:给key设置过期时间,将影响降低

    • (2)删除锁操作没有区分谁申请的锁,就是说A可以释放B的锁

      解决办法:区分当前锁是那个客户端申请的,其实就是给锁的变量设置一个唯一id进行客户端的区分,删除的时候也比较这个值是不是和申请锁的客户端的唯一id相等,相等才能执行删除锁操作

知道了解决方案后,我们可以来看下redis的实现

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

//释放锁 比较unique_value是否相等,避免误释放,这里是使用lua实现
if redis.call("get",KEYS[1]) == ARGV[1] then
    return redis.call("del",KEYS[1])
else
    return 0
end
redis-cli  --eval  unlock.script lock_key , unique_value 

2.2.2.基于多个redis节点实现高可靠的分布式锁

由于一个redis实例可能出现宕机的风险,因此为了保证可靠性,我们可以采取多个实例实现redis的分布式锁

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

Redlock

基本思路:让客户端请求多个redis实例依次请求加锁,能够获得大多数(2n+1)实例的锁,则申请锁成功。这样能够有效避免某个实例宕机导致锁操作异常

基本流程:

  • 第一步:客户端获取当前时间

  • 第二步:客户端按顺序向N个实例请求加锁操作。加锁方法仍是使用SETNX

    这里redis实例给每个客户端进行加锁时,会设置客户端的超时时间,如果在超时时间内没有加锁成功则会请求下一个实例,一般的超时时间为几十ms

  • 第三步:完成了与所有实例的加锁操作之后需要计算整个过程的耗时,用于比较锁的过期时间,如果请求锁的时间大于了过期时间,则将该锁释放,如果客户端请求成功的实例不占大多数,也会释放锁

RedLock算法中,只需要保证N个redis实例的半数以上可用就能保证锁操作的可靠性

2.3.redis加锁过程中的错误使用

  • 加锁操作

    错误一:

    	Long result = jedis.setnx(lockKey, requestId);
        if (result == 1) {
            // 若在这里程序突然崩溃,则无法设置过期时间,将发生死锁
            jedis.expire(lockKey, expireTime);
        }
    
  • 解锁操作

     	// 判断加锁与解锁是不是同一个客户端
        if (requestId.equals(jedis.get(lockKey))) {
            //删除点: 若在此时,这把锁突然不是这个客户端的,则会误解锁
            jedis.del(lockKey);
        }
    

    可能会存在当前代码执行到删除点时,锁正好过期然后A又申请到了锁,此时删除会释放A的锁导致错误

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

哈哈哈张大侠

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

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

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

打赏作者

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

抵扣说明:

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

余额充值