热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

分布式锁_对比各类分布式锁缺陷,抓住Redis分布式锁实现命门

篇首语:本文由编程笔记#小编为大家整理,主要介绍了对比各类分布式锁缺陷,抓住Redis分布式锁实现命门相关的知识,希望对你有一定的参考价值。

篇首语:本文由编程笔记#小编为大家整理,主要介绍了对比各类分布式锁缺陷,抓住Redis分布式锁实现命门相关的知识,希望对你有一定的参考价值。







近两年来微服务变得越来越热门,越来越多的应用部署在分布式环境中,在分布式环境中,数据一致性是一直以来需要关注并且去解决的问题,分布式锁也就成为了一种广泛使用的技术。




常用的分布式实现方式为Redis,Zookeeper,其中基于Redis的分布式锁的使用更加广泛。




但是在工作和网络上看到过各个版本的Redis分布式锁实现,每种实现都有一些不严谨的地方,甚至有可能是错误的实现,包括在代码中,如果不能正确的使用分布式锁,可能造成严重的生产环境故障。




本文主要对目前遇到的各种分布式锁以及其缺陷做了一个整理,并对如何选择合适的Redis分布式锁给出建议。








一、各个版本的Redis分布式锁












1、V1.0











tryLock(){  


    SETNX Key 1


    EXPIRE Key Seconds


}


release(){  


  DELETE Key


}








这个版本应该是最简单的版本,也是出现频率很高的一个版本,首先给锁加一个过期时间操作是为了避免应用在服务重启或者异常导致锁无法释放后,不会出现锁一直无法被释放的情况。





  • 这个方案的一个问题在于每次提交一个Redis请求,如果执行完第一条命令后应用异常或者重启,锁将无法过期,一种改善方案就是使用Lua脚本(包含SETNX和EXPIRE两条命令),但是如果Redis仅执行了一条命令后crash或者发生主从切换,依然会出现锁没有过期时间,最终导致无法释放。


  • 另外一个问题在于,很多同学在释放分布式锁的过程中,无论锁是否获取成功,都在finally中释放锁,这样是一个锁的错误使用,这个问题将在后续的V3.0版本中解决。


  • 针对锁无法释放问题的一个解决方案基于GETSET命令来实现。









2、V1.1 基于GETSET











tryLock(){  


    NewExpireTime=CurrentTimestamp+ExpireSeconds


    if(! SET Key NewExpireTime Seconds NX){


         oldExpireTime = GET(Key)


          if( oldExpireTime


              NewExpireTime=CurrentTimestamp+ExpireSeconds


              CurrentExpireTime=GETSET(Key,NewExpireTime)


              if(CurrentExpireTime == oldExpireTime){


                return 1;


              }else{


                return 0;


              }


          }


    }


}


release(){  


        DELETE key


    }








思路:





  • SETNX(Key,ExpireTime)获取锁。


  • 如果获取锁失败,通过GET(Key)返回的时间戳检查锁是否已经过期。


  • GETSET(Key,ExpireTime)修改Value为NewExpireTime。


  • 检查GETSET返回的旧值,如果等于GET返回的值,则认为获取锁成功。




注意:这个版本去掉了EXPIRE命令,改为通过Value时间戳值来判断过期。




问题:





  • 在锁竞争较高的情况下,会出现Value不断被覆盖,但是没有一个Client获取到锁。


  • 在获取锁的过程中不断的修改原有锁的数据,设想一种场景C1,C2竞争锁,C1获取到了锁,C2锁执行了GETSET操作修改了C1锁的过期时间,如果C1没有正确释放锁,锁的过期时间被延长,其它Client需要等待更久的时间。









3、V2.0 基于SETNX











tryLock(){  


    SET Key 1 Seconds NX


}


release(){  


  DELETE Key


}








Redis2.6.12版本后SETNX增加过期时间参数,这样就解决了两条命令无法保证原子性的问题。但是设想下面一个场景: 





  • C1成功获取到了锁,之后C1因为GC进入等待或者未知原因导致任务执行过长,最后在锁失效前C1没有主动释放锁。


  • C2在C1的锁超时后获取到锁,并且开始执行,这个时候C1和C2都同时在执行,会因重复执行造成数据不一致等未知情况。


  • C1如果先执行完毕,则会释放C2的锁,此时可能导致另外一个C3进程获取到了锁。





大致的流程图:




对比各类分布式锁缺陷,抓住Redis分布式锁实现命门




存在问题:





  • 由于C1的停顿导致C1 和C2同都获得了锁并且同时在执行,在业务实现间接要求必须保证幂等性。


  • C1释放了不属于C1的锁。









4、V3.0











tryLock(){  


    SET Key UnixTimestamp Seconds NX


}


release(){  


    EVAL(


      //LuaScript


      if redis.call("get",KEYS[1]) == ARGV[1] then


          return redis.call("del",KEYS[1])


      else


          return 0


      end


    )


}








这个方案通过指定Value为时间戳,并在释放锁的时候检查锁的Value是否为获取锁的Value,避免了V2.0版本中提到的C1释放了C2持有的锁的问题。




另外在释放锁的时候因为涉及到多个Redis操作,并且考虑到Check And Set模型的并发问题,所以使用Lua脚本来避免并发问题。




存在问题:




如果在并发极高的场景下,比如抢红包场景,可能存在Unix Timestamp重复问题,另外由于不能保证分布式环境下的物理时钟一致性,也可能存在Unix Timestamp重复问题,只不过极少情况下会遇到。








5、V3.1











tryLock(){  


    SET Key UniqId Seconds


}


release(){  


    EVAL(


      //LuaScript


      if redis.call("get",KEYS[1]) == ARGV[1] then


          return redis.call("del",KEYS[1])


      else


          return 0


      end


    )


}








Redis2.6.12后SET同样提供了一个NX参数,等同于SETNX命令,官方文档上提醒后面的版本有可能去掉SETNX、SETEX、PSETE、SET命令代替,另外一个优化是使用一个自增的唯一UniqId代替时间戳来规避V3.0提到的时钟问题。




这个方案是目前最优的分布式锁方案,但是如果在Redis集群环境下依然存在问题:




由于Redis集群数据同步为异步,假设在Master节点获取到锁后未完成数据同步情况下Master节点crash,此时在新的Master节点依然可以获取锁,所以多个Client同时获取到了锁。








二、分布式Redis锁:Redlock








V3.1的版本仅在单实例的场景下是安全的,针对如何实现分布式Redis的锁,国外的分布式专家有过激烈的讨论,Antirez提出了分布式锁算法Redlock,在distlock话题下可以看到对Redlock的详细说明,下面是Redlock算法的一个中文说明(引用)。




假设有N个独立的Redis节点:





  • 获取当前时间(毫秒数)。






  • 按顺序依次向N个Redis节点执行获取锁的操作。这个获取操作跟前面基于单Redis节点的获取锁的过程相同,包含随机字符串my_random_value,也包含过期时间(比如PX 30000,即锁的有效时间)。





为了保证在某个Redis节点不可用的时候算法能够继续运行,这个获取锁的操作还有一个超时时间(time out),它要远小于锁的有效时间(几十毫秒量级)。客户端在向某个Redis节点获取锁失败以后,应该立即尝试下一个Redis节点。




这里的失败,应该包含任何类型的失败,比如该Redis节点不可用,或者该Redis节点上的锁已经被其它客户端持有(注:Redlock原文中这里只提到了Redis节点不可用的情况,但也应该包含其它的失败情况)。





  • 计算整个获取锁的过程总共消耗了多长时间,计算方法是用当前时间减去第1步记录的时间。如果客户端从大多数Redis节点(>=N/2+1)成功获取到了锁,并且获取锁总共消耗的时间没有超过锁的有效时间(lock validity time),那么这时客户端才认为最终获取锁成功;否则,认为最终获取锁失败。








  • 如果最终获取锁成功了,那么这个锁的有效时间应该重新计算,它等于最初的锁的有效时间减去第3步计算出来的获取锁消耗的时间。






  • 如果最终获取锁失败了(可能由于获取到锁的Redis节点个数少于N/2+1,或者整个获取锁的过程消耗的时间超过了锁的最初有效时间),那么客户端应该立即向所有Redis节点发起释放锁的操作(即前面介绍的Redis Lua脚本)。







  • 释放锁:对所有的Redis节点发起释放锁操作。





然而Martin Kleppmann针对这个算法提出了质疑,提出应该基于fencing token机制(每次对资源进行操作都需要进行token验证):





  • Redlock在系统模型上尤其是在分布式时钟一致性问题上提出了假设,实际场景下存在时钟不一致和时钟跳跃问题,而Redlock恰恰是基于timing的分布式锁。


  • 另外Redlock由于是基于自动过期机制,依然没有解决长时间的gc pause等问题带来的锁自动失效,从而带来的安全性问题。





接着Antirez又回复了Martin Kleppmann的质疑,给出了过期机制的合理性,以及实际场景中如果出现停顿问题导致多个Client同时访问资源的情况下如何处理。




针对Redlock的问题,基于“Redis的分布式锁到底安全吗”给出了详细的中文说明,并对Redlock算法存在的问题提出了分析。








三、总结








不论是基于SETNX版本的Redis单实例分布式锁,还是Redlock分布式锁,都是为了保证以下特性:





  • 安全性:在同一时间不允许多个Client同时持有锁。


  • 活性

    死锁:锁最终应该能够被释放,即使Client端crash或者出现网络分区(通常基于超时机制)。



      容错性:只要超过半数Redis节点可用,锁都能被正确获取和释放。




所以在开发或者使用分布式锁的过程中要保证安全性和活性,避免出现不可预测的结果。




另外每个版本的分布式锁都存在一些问题,在锁的使用上要针对锁的实用场景选择合适的锁,通常情况下锁的使用场景包括:





  • Efficiency(效率):只需要一个Client来完成操作,不需要重复执行,这是一个对宽松的分布式锁,只需要保证锁的活性即可。


  • Correctness(正确性):多个Client保证严格的互斥性,不允许出现同时持有锁或者对同时操作同一资源,这种场景下需要在锁的选择和使用上更加严格,同时在业务代码上尽量做到幂等。





在Redis分布式锁的实现上还有很多问题等待解决,我们需要认识到这些问题并清楚如何正确实现一个Redis分布式锁,然后在工作中合理的选择和正确的使用分布式锁。








来源


http://tech.dianwoda.com/2018/04/11/redisfen-bu-shi-suo-jin-hua-shi/


dbaplus社群欢迎广大技术人员投稿,投稿邮箱:editor@dbaplus.cn















近期热文














近期活动


2019Gdevops全球敏捷运维峰会-北京站





推荐阅读
  • 本文继续探讨 Redis 分布式锁的高级特性,重点分析超时问题和可重入性的实现,以及如何通过不同的策略处理锁冲突。 ... [详细]
  • 本文深入探讨了MySQL中常见的面试问题,包括事务隔离级别、存储引擎选择、索引结构及优化等关键知识点。通过详细解析,帮助读者在面对BAT等大厂面试时更加从容。 ... [详细]
  • 深入解析Spring Cloud微服务架构与分布式系统实战
    本文详细介绍了Spring Cloud在微服务架构和分布式系统中的应用,结合实际案例和最新技术,帮助读者全面掌握微服务的实现与优化。 ... [详细]
  • 前言无论是对于刚入行工作还是已经工作几年的java开发者来说,面试求职始终是你需要直面的一件事情。首先梳理自己的知识体系,针对性准备,会有事半功倍的效果。我们往往会把重点放在技术上 ... [详细]
  • 字节跳动夏季招聘面试经验分享
    本文详细记录了字节跳动夏季招聘的面试经历,涵盖了一、二、三轮面试的技术问题及项目讨论,旨在为准备类似面试的求职者提供参考。 ... [详细]
  • java程序员_Java程序员最新职业规划,逆袭面经分享
    java程序员_Java程序员最新职业规划,逆袭面经分享 ... [详细]
  • 本文探讨了随着并发需求的增长,MySQL数据库架构如何从简单的单一实例发展到复杂的分布式系统,以及每一步演进背后的原理和技术解决方案。 ... [详细]
  • 实用正则表达式有哪些
    小编给大家分享一下实用正则表达式有哪些,相信大部分人都还不怎么了解,因此分享这篇文章给大家参考一下,希望大家阅读完这篇文章后大有收获,下 ... [详细]
  • 历经三十年的开发,Mathematica 已成为技术计算领域的标杆,为全球的技术创新者、教育工作者、学生及其他用户提供了一个领先的计算平台。最新版本 Mathematica 12.3.1 增加了多项核心语言、数学计算、可视化和图形处理的新功能。 ... [详细]
  • 深入解析Spring启动过程
    本文详细介绍了Spring框架的启动流程,帮助开发者理解其内部机制。通过具体示例和代码片段,解释了Bean定义、工厂类、读取器以及条件评估等关键概念,使读者能够更全面地掌握Spring的初始化过程。 ... [详细]
  • 本文探讨了Web开发与游戏开发之间的主要区别,旨在帮助开发者更好地理解两种开发领域的特性和需求。文章基于作者的实际经验和网络资料整理而成。 ... [详细]
  • Redis安全防护深入解析
    本文详细探讨了如何通过指令安全、端口管理和SSL代理等措施有效保护Redis服务的安全性。 ... [详细]
  • Spring Cloud因其强大的功能和灵活性,被誉为开发分布式系统的‘一站式’解决方案。它不仅简化了分布式系统中的常见模式实现,还被广泛应用于企业级生产环境中。本书内容详实,覆盖了从微服务基础到Spring Cloud的高级应用,适合各层次的开发者。 ... [详细]
  • 利用RabbitMQ实现高效延迟任务处理
    本文详细探讨了如何利用RabbitMQ实现延迟任务,包括其应用场景、实现原理、系统设计以及具体的Spring Boot实现方式。 ... [详细]
  • 深入解析 Golang 中的 Cache::remember 方法实现
    本文详细探讨了如何在 Golang 中实现类似于 Laravel 的 Cache::remember 方法,通过具体的代码示例和深入的分析,帮助读者更好地理解和应用这一技术。 ... [详细]
author-avatar
nuabolalalala6_535
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有