当前位置: 首页 > 后端技术 > PHP

分布式锁机制原理与实现

时间:2023-03-30 04:04:08 PHP

前言分布式锁是一种控制分布式系统间共享资源同步访问的方式。在分布式系统中,经常需要协调它们的动作。如果不同系统或同一系统的不同主机共享一个或一组资源,在访问这些资源时往往需要互斥,以防止相互干扰,保证一致性。在这种情况下,你需要使用分布式锁。这里简单介绍三种方式:基于数据库的实现,基于redis的实现,基于ZooKeeper的实现。场景示例假设有一个进程A,每隔一小时准时向用户发送一条短信“Helloworld”。为了高可用性,必须在多台机器上部署多个进程以避免停机;假设部署在两台机器上,那么问题来了,用户每小时都会收到两个“Helloworld”,信息会重复;我们希望只发送一个“Helloworld”,那么可以引入分布式锁的概念;进程A和进程B在发送短信前注册了一个锁。假设进程A抢到了锁,进程B会等待结果。如果发送成功,那么B就会放弃任务,等待下一个小时。问题的核心是如何注册锁。检查锁是否存在并注册锁是一个原子操作。类似于mysql的主键,存在则不能插入。也就是说,你不能覆盖我的锁。你得等一等;我们有很多方法可以实现分布式锁。最简单的就是以小时时间为主键,写一条数据到mysql,用数据库来保持一致性。为什么要使用分布式锁在我们开发应用程序的时候,如果需要对一个共享变量进行多线程同步访问,可以使用我们学过的java多线程方案。注意这是一个单机应用,即所有的请求都会分配到当前服务器的jvm中,然后映射到操作系统的线程中去处理,而这个共享变量只是里面的一块内存空间虚拟机。后来业务发展起来,需要集群。一个应用需要部署在多台机器上,然后进行负载均衡。主要表现是类中的成员变量是有状态对象),如果不加控制,变量A会同时在JVM1、JVM2、JVM3中分配一块内存;(2)三个请求同时对变量进行操作,结果明显不同。(3)即使不是同时发送,三个请求分别操作三个不同JVM内存区域的数据,变量A之间没有共享,没有可见性,处理结果也是错误的。(4)如果我们的业务中存在这样的场景,我们需要一种方法来解决这个问题。为了保证在高并发的情况下,一个方法或者属性在同一时间只能被同一个线程执行,在传统单机应用的单机部署情况下,可以使用javaconcurrent的相关API互斥控制的处理(如ReentrantLock或Synchronized)。在单机环境下,java提供了很多与并发处理相关的API。但是随着业务发展的需要,原来的单机部署系统演变为分布式集群系统后,由于分布式系统是多线程、多进程、分布在不同的机器上,这就使得并发控制原有的单机部署锁策略失效,纯javaAPI无法提供分发锁的能力。为了解决这个问题,需要一种跨JVM的互斥机制来控制对共享资源的访问,这就是分布式锁要解决的问题。分布式锁应该具备的条件在分布式系统环境下,一个方法一次只能被一台机器的一个线程执行;高可用的锁获取和释放锁;高性能锁的获取和释放锁;可重入功能;具有锁失效机制,防止死锁;具有非阻塞锁特性,即如果没有获取到锁,则直接返回获取锁失败。分布式锁的实现方法——前言目前,几乎很多大型网站和应用都是采用分布式方式部署的。分布式场景下的数据一致性一直是一个比较重要的话题。分布式CAP理论告诉我们,任何分布式系统都不可能同时满足一致性、可用性和分区容错,只能同时满足两者。因此,很多系统在设计之初就对这三项进行了权衡。在互联网领域的大部分场景下,都需要牺牲强一致性来换取系统的高可用性。系统往往只需要保证最终的一致性,只要最终的时间在用户可接受的范围内即可。在很多场景下,我们以保证数据的最终一致性为例,这需要很多技术方案来支持,比如分布式事务、分布式锁等,有时候我们需要保证一个方法在同一个线程中执行。基于数据库实现分布式锁;基于缓存redis实现分布式锁;基于Zookeeper实现分布式锁。基于数据库的实现方法的核心思想是在数据库中创建一个表,表中包含方法名等字段,并在方法名字段上创建唯一索引。如果要执行某个方法,使用这个方法名向表中插入数据,插入成功获取锁,执行完成后删除对应行数据释放锁。创建表:DROPTABLEIFEXISTS`method_lock`;CREATETABLE`method_lock`(`id`int(11)unsignedNOTNULLAUTO_INCREMENTCOMMENT'主键',`method_name`varchar(64)NOTNULLCOMMENT'锁定的方法名',`desc`varchar(255)NOTNULLCOMMENT'备注',`update_time`timestampNOTNULLDEFAULTCURRENT_TIMESTAMPONUPDATECURRENT_TIMESTAMP,PRIMARYKEY(`id`),UNIQUEKEY`uidx_method_name`(`method_name`)USINGBTREE)ENGI=InnoDBAUTO_INCREMENT=3DEFAULTCHARSET=utf8COMMENT='A锁中的方法';如果要执行一个方法,使用这个方法名向表中插入数据:INSERTINTOmethod_lock(method_name,desc)VALUES('methodName','methodNameofthetest');因为我们对method_name做了唯一约束,如果有多个请求同时提交给数据库,数据库会保证只有一个操作成功,那么我们就可以认为操作成功的线程获取到了方法的锁可以执行方法体的内容。如果插入成功,则获取锁,执行完成后,删除对应行数据释放锁:deletefrommethod_lockwheremethod_name='methodName';使用这种基于数据库的实现非常简单,但是对于分布式锁应该具备的条件,它有一些需要解决和优化的问题:(1)由于是基于数据库实现的,所以分布式锁的可用性和性能数据库会直接影响分布式锁的可用性和性能。所以数据库需要双机部署,数据同步,主备切换。(2)不具备可重入性,因为在同一个线程释放锁之前行数据仍然存在,无法再次成功插入数据。因此需要在表中增加一列记录当前获取锁的机器和线程信息,再次获取锁时先检查表中的机器和线程信息是否与当前机器相同并且线程信息,相同则直接获取锁;(3)没有锁失效机制,因为有可能插入数据成功之后,服务器宕机了,相应的数据还没有被删除,服务恢复后无法获取锁,所以需要一个新的列添加到锁中记录过期时间,需要定时任务清除这些无效数据;(4)不具备阻塞锁的特性,获取不到锁直接返回失败,所以需要优化采集逻辑,多次循环获取。在实施过程中会遇到各种各样的问题。为了解决这些问题,实现方法会越来越复杂;依赖数据库需要一定的资源开销,需要考虑性能问题。基于redis的实现选择redis分布式锁的原因:(1)redis性能高;(2)redis对此支持的命令比较好,实现起来也比较方便。使用分布式锁时使用的主要命令简介:(1)SETNXSETNXkeyval:当且仅当key不存在时,设置一个key为val的字符串,返回1;如果key存在,什么也不做,返回0。(2)expireexpirekey超时:key设置超时时间,单位秒,超时后自动释放锁,避免死锁。(3)deletedeletekey:deletekey实现思路:(1)获取锁时,使用setnx加锁,使用expire命令给锁加超时时间。过了这个时间,锁就会自动释放。锁的值是一个随机生成的UUID,用于释放锁时的判断。(2)在获取锁的时候,也设置了一个获取超时时间。如果超过这个时间,将放弃获取锁。(3)释放锁时,通过UUID判断是否是锁。如果是锁,则执行delete释放锁。基于ZooKeeper的实现ZooKeeper是一个开源组件,为分布式应用程序提供一致的服务。它内部有一个层次化的文件系统目录树结构,规定同一个目录下只能有一个唯一的文件名。基于ZooKeeper实现分布式锁的步骤如下:(1)创建目录mylock;(2)线程A要获取锁,在mylock目录下创建一个临时的顺序节点;(3)获取mylock目录下的所有子节点,然后获取如果没有比自己小的兄弟节点,则说明当前线程序号最小,获取锁;(4)线程B获取所有节点,判断自己不是最小节点,设置监听比自己小的节点;(5)线程A处理完,删除自己的结点,线程B监听change事件,判断是否为最小结点,如果是则获取锁。这里推荐Curator,一个Apache开源库,它是一个ZooKeeper的客户端。Curator提供的InterProcessMutex是分布式锁的一种实现。acquire方法用于获取锁,release方法用于释放锁。优点:具有高可用、可重入、阻塞锁等特点,可以解决无效死锁问题。缺点:由于需要频繁创建和删除节点,性能不如redis方式。总结以上三种方式,都不是在所有场合都是完美的,所以要根据不同的应用场景选择最合适的实现方式。在分布式环境中,有时候对资源进行锁定是非常重要的,比如抢购某个资源。这时候使用分布式锁可以很好的控制资源。参考链接https://blog.csdn.net/xlgen15...