热门标签 | 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全球敏捷运维峰会-北京站





推荐阅读
  • 本文探讨了如何通过优化SOAP服务调用和多线程处理来减少生成的事件数量,并提高加载大量实体的效率。 ... [详细]
  • importjava.io.*;importjava.util.*;publicclass五子棋游戏{staticintm1;staticintn1;staticfinalintS ... [详细]
  • 本文详细介绍了 Java 中 org.w3c.dom.Node 类的 isEqualNode() 方法的功能、参数及返回值,并通过多个实际代码示例来展示其具体应用。此方法用于检测两个节点是否相等,而不仅仅是判断它们是否为同一个对象。 ... [详细]
  • JUC并发编程——线程的基本方法使用
    目录一、线程名称设置和获取二、线程的sleep()三、线程的interrupt四、join()五、yield()六、wait(),notify(),notifyAll( ... [详细]
  • 阿里面试题解析:分库分表后的无限扩容瓶颈与解决方案
    本文探讨了在分布式系统中,分库分表后的无限扩容问题及其解决方案。通过分析不同阶段的服务架构演变,提出了单元化作为解决数据库连接数过多的有效方法。 ... [详细]
  • 在使用 Cacti 进行监控时,发现已运行的转码机未产生流量,导致 Cacti 监控界面显示该转码机处于宕机状态。进一步检查 Cacti 日志,发现数据库中存在 SQL 查询失败的问题,错误代码为 145。此问题可能是由于数据库表损坏或索引失效所致,建议对相关表进行修复操作以恢复监控功能。 ... [详细]
  • 微信小程序实现类似微博的无限回复功能,内置云开发数据库支持
    本文详细介绍了如何利用微信小程序实现类似于微博的无限回复功能,并充分利用了微信云开发的数据库支持。文中不仅提供了关键代码片段,还包含了完整的页面代码,方便开发者按需使用。此外,HTML页面中包含了一些示例图片,开发者可以根据个人喜好进行替换。文章还将展示详细的数据库结构设计,帮助读者更好地理解和实现这一功能。 ... [详细]
  • 尽管我们尽最大努力,任何软件开发过程中都难免会出现缺陷。为了更有效地提升对支持部门的协助与支撑,本文探讨了多种策略和最佳实践,旨在通过改进沟通、增强培训和支持流程来减少这些缺陷的影响,并提高整体服务质量和客户满意度。 ... [详细]
  • 深入解析Postman内置变量的实用技巧与示例代码
    本文详细探讨了Postman内置变量的实用技巧和应用案例,通过具体的示例代码,全面解析了这些变量在实际开发和测试中的使用方法,为读者提供了宝贵的学习和参考资源。 ... [详细]
  • 本文介绍了UUID(通用唯一标识符)的概念及其在JavaScript中生成Java兼容UUID的代码实现与优化技巧。UUID是一个128位的唯一标识符,广泛应用于分布式系统中以确保唯一性。文章详细探讨了如何利用JavaScript生成符合Java标准的UUID,并提供了多种优化方法,以提高生成效率和兼容性。 ... [详细]
  • HBase Java API 进阶:过滤器详解与应用实例
    本文详细探讨了HBase 1.2.6版本中Java API的高级应用,重点介绍了过滤器的使用方法和实际案例。首先,文章对几种常见的HBase过滤器进行了概述,包括列前缀过滤器(ColumnPrefixFilter)和时间戳过滤器(TimestampsFilter)。此外,还详细讲解了分页过滤器(PageFilter)的实现原理及其在大数据查询中的应用场景。通过具体的代码示例,读者可以更好地理解和掌握这些过滤器的使用技巧,从而提高数据处理的效率和灵活性。 ... [详细]
  • 揭秘腾讯云CynosDB计算层设计优化背后的不为人知的故事与技术细节
    揭秘腾讯云CynosDB计算层设计优化背后的不为人知的故事与技术细节 ... [详细]
  • 近年来,BPM(业务流程管理)系统在国内市场逐渐普及,多家厂商在这一领域崭露头角。本文将对当前主要的BPM厂商进行概述,并分析其各自的优势。目前,市场上较为成熟的BPM产品主要分为两类:一类是综合型厂商,如IBM和SAP,这些企业在整体解决方案方面具有明显优势;另一类则是专注于BPM领域的专业厂商,它们在特定行业或应用场景中表现出色。通过对比分析,本文旨在为企业选择合适的BPM系统提供参考。 ... [详细]
  • 本文推荐了六款高效的Java Web应用开发工具,并详细介绍了它们的实用功能。其中,分布式敏捷开发系统架构“zheng”项目,基于Spring、Spring MVC和MyBatis技术栈,提供了完整的分布式敏捷开发解决方案,支持快速构建高性能的企业级应用。此外,该工具还集成了多种中间件和服务,进一步提升了开发效率和系统的可维护性。 ... [详细]
  • 本文详细解析了 MySQL 5.7.20 版本中二进制日志(binlog)崩溃恢复机制的工作流程。假设使用 InnoDB 存储引擎,并且启用了 `sync_binlog=1` 配置,文章深入探讨了在系统崩溃后如何通过 binlog 进行数据恢复,确保数据的一致性和完整性。 ... [详细]
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社区 版权所有