当前位置: 首页 > 科技观察

一目了然的“分布式锁”原理

时间:2023-03-13 16:52:10 科技观察

分布式锁和我们平时说的锁原理基本一样。目的是保证多个线程并发时,只有一个线程同时操作业务或方法、变量。在一个进程中,也就是一个JVM或者应用程序中,我们很容易去处理控制。在jdk的java.util并发包中,我们已经提供了这些方法来加锁,比如Synchronized关键字或者Lock锁。处理。但是如果我们现在的应用只部署一台服务器的话,并发性是很差的。如果同时有上万个请求,很有可能服务器会因为压力过大而瘫痪。想想双十一、除夕晚上10:00、瓜分支付宝红包等业务场景,自然需要多台服务器同时处理这些业务。可能有数百台服务器同时处理这些服务。但是大家想一想,如果有100台服务器来处理红包业务,现在假设有1亿个红包,1000万人分享,数量是随机的,那么在这个业务场景下,是否需要保证这1000万人的红包总量除以***就等于1亿?如果处理不好~~每人拿100万,那马云爸爸估计大年初一就要宣告破产了~~常规锁会怎样?首先我说一下我们为什么要搞集群。简单理解就是需求(请求的并发量)变大了,一个worker的处理能力有限,就招更多的worker一起处理。假设1000万个请求平均分配到100台服务器,每台服务器接收10w个请求。这10w个请求不是同一秒来的,可能是1-2小时之内,你可以想象我们30:20开红包,有的人马上打开,有的人等到12点我才记起来点钟。在这种情况下,每秒平均请求数小于1,000。这种压力对于压力一般的服务器来说还是可以承受的:第一个用户分,请求到来后,需要给他分1亿以内的一部分钱,金额是随机的,假设***个人得到100,再从1亿中减去100,剩下99999900元。第二个用户再瓜分,金额随机。这次要分200元,那么剩下的99999900元就要减去200元,剩下99999700元。当第10w个用户来了,看到还有1000w,那么这1000w就全部归他了。相当于每台服务器分1亿,也就是10万用户分1亿,一共100台服务器,分100亿。如果这样的话,虽然马云爸爸不会破产(据***统计,马云有2300亿),那发红包的开发项目组和产品经理可以GG~简化结构图如下:如何获取分布式锁Processing?那么为了解决这个问题,让1000万用户只分1亿,而不是100亿。这时候,分布式锁就派上用场了。分布式锁可以把整个集群看成一个应用,所以锁需要独立于各个服务,而不是在服务中。假设第一个服务器收到用户1的请求后,在自己的应用中不能只判断能分多少钱,而是需要到外面去请求负责管理的人(服务)一亿红包,问何:嘿嘿,我这里要分100块钱,给我100块钱。管理红包的妹子(服务)看到还剩1亿。好吧,我给你100元,然后就剩下99999900元了。第二个请求过来后,被服务器2获取到,我继续问,管理红包的妹子,我想在这里分享10块钱,管理红包的妹子先查看了还有99999900,然后说:好的,我给你10块,剩下99999890块。1000wth请求到达后,服务端100拿到请求,继续问,管理红包的妹子,我要100,妹子翻了个白眼,告诉你,只剩1块钱了,你要不要吧,那这时候我只能给你1块钱(1块钱也是钱,还是可以买辣条的)。这些请求编号1和2不代表执行顺序。正式场景下,应该有100台服务器,每台服务器持有一个请求,访问负责管理红包的妹子(服务)。那么负责红包的妹子会同时收到100个请求。这时候需要给负责红包(扔绣球花)的妹子加一把锁。你100台服务器谁拿到锁(抢绣球花),谁进来和我说话,我给你积分,其他人就等着吧。经过上面的分布式锁处理,马云爸爸终于松了口气,决定给红包队伍每人加一个鸡腿。简化结构图如下:分布式锁的实现有哪些?说到分布式锁的实现,还是有很多的,比如数据库方法,Redis分布式锁,Zookeeper分布式锁等等。如果我们用Redis作为分布式锁,那么负责“红包”的妹子(service)”可以用Redis代替,请自行斟酌。①为什么Redis可以实现分布式锁?首先,Redis是单线程的。这里的单线程是指网络请求模块使用一个线程(所以不需要考虑并发安全),即一个线程处理所有的网络请求,其他模块仍然使用多个线程。在实际操作中,流程大致是这样的:Server1要访问发红包的妹子,也就是Redis,然后会通过“setnxkeyvalue”操作在Redis中设置一个Key,这个value并不重要。重要的是要有Key,也就是token。并且你可以随意调用这个Key,只要所有服务器设置相同的Key即可。假设我们设置一个,如下图所示:那么我们可以看到会返回一个1,代表成功。如果有另一个请求设置相同的Key,如下图:此时会返回0,表示失败。那么我们可以通过这个操作来判断当前是否可以拿到锁,也可以访问“负责发红包的妹子”。如果它返回1,那么我就开始执行下面的逻辑。如果它返回0,那么说明已经有人占用了,那我就继续等。服务器1获得锁后,进行业务处理。完成后需要释放锁,如下图所示:如果删除成功返回1,那么其他服务器可以继续重复上面的步骤设置Key来达到锁的目的。当然,以上操作是直接在Redis客户端进行的。如果是通过程序调用的,一定不能这样写。比如Java需要通过Jedis来调用,但是整个处理逻辑基本是一样的。通过上面的方法,我们好像解决了分布式锁的问题,但是仔细想想,有什么问题吗?是的,还是有问题,可能会出现死锁问题。比如1号服务器搭建好后,get解锁后,突然宕机。那么后续删除Key的操作就无法进行了。这个Key会一直存在于Redis中。其他服务器每次检查都会返回0。他们会认为有人在使用锁,我需要等待。为了解决这个死锁问题,我们需要给Key设置一个有效期。有2种设置方式:第一种是在设置Key后直接设置Key的有效期“expirekeytimeout”,为Key设置一个超时时间,单位是Second,会加锁这段时间后自动释放Lock以避免死锁。这种方式相当于把锁的有效期交给了Redis来控制。如果时间到了,你还没有帮我删除Key,那么Redis会直接帮你删除,其他服务器可以继续去Setnx获取锁。第二种方式是将Key的删除权交给其他服务器。这时候就需要Value值了。比如Server1设置了Value,也就是Timeout为当前时间+1秒。此时服务器2通过Get发现时间已经超过了当前系统时间,说明服务器1还没有释放锁,服务器1可能有问题,服务器2开始删除Key操作,继续执行Setnx操作。但是这样做有个问题,就是不仅你的服务器2可能会发现服务器1超时了,服务器3也可能会发现如果对服务器2的setnx操作完成了,服务器3会继续删除,是服务器3也可以Setnx成功吗?这意味着服务器2和服务器3都获得了锁,这是一个大问题。这个时候怎么办?这时候我们就需要用到“GETSET键值”命令。这条命令的意思是获取当前Key的值,并设置一个新的值。假设服务器2发现Key已经过期,开始调用getset命令,然后根据获取到的时间判断是否过期。如果获取到的时间仍然过期,说明已经获取到了锁。如果不是,说明在服务2执行getset之前,服务器3也可能发现锁已经过期,先于服务器2执行getset操作,并重新设置过期时间。那么server2需要放弃后续操作,继续等待server3释放锁或者监听Key的有效期是否过期。其实这块有个小问题。服务器3已经修改了有效期。Server2获得锁后,也修改了有效期,但未能获得锁。不过在Server3的基础上增加了有效期,不过这个影响其实很小,几乎可以忽略不计。②为什么Zookeeper可以实现分布式锁?百度百科是这样介绍的:ZooKeeper是一个分布式的、开源的分布式应用协调服务,是Google的Chubby的开源实现,是Hadoop和Hbase的重要组件。对于我们这些初次见面的人来说,可以理解为ZooKeeper就像是我们的电脑文件系统。我们可以在d盘创建文件夹a,继续在a文件夹创建文件夹a1和a2。那么我们的文件系统有什么特点呢?即同一目录下的文件名不能重复,ZooKeeper也是如此。ZooKeeper中所有的节点,也就是文件夹,都叫做Znode,这个Znode节点可以存储数据。我们可以通过“create/zkjjjnice”来创建一个节点,这个命令的意思是在根目录下创建一个zkjjj节点,值为nice。同样,这里的值和我前面提到的Redis中的值是一样的,没有任何意义,你要给就给吧。另外,ZooKeeper可以创建4种类型的节点,分别是:持久节点、持久时序节点、临时节点、临时时序节点,无论你的ZooKeeper客户端是否断开连接,ZooKeeper服务端都会记录这个节点。临时节点正好相反。一旦你的ZooKeeper客户端断开连接,ZooKeeper服务器将不再保存这个节点。顺便说一下顺序节点。顺序节点是指ZooKeeper在创建节点时会自动给节点编号,比如0000001、0000002。Zookeeper有一个监控机制。客户端注册以监视它关心的目录节点。当目录节点发生变化(数据变化、被删除、子目录节点增加或删除)时,Zookeeper会通知客户端。如何锁定Zookeeper?下面结合我们上面的红包场景,继续介绍如何锁定Zookeeper。假设服务器1创建一个节点/zkjjj,如果成功则服务器1获取到锁,服务器2创建同样的锁失败。这个时候只能监听这个节点的变化。服务器1处理完业务删除节点后,会通知他,然后创建同一个节点,获取锁处理业务,然后删除节点。后续100台服务器类似。注意这里的100台服务器并不是一个一个执行上面创建节点的操作,而是并发的。当server1创建成功后,剩下的99台server会全部注册监听这个节点,等待通知等。但是大家有没有发现,这里还有一个问题,还是会出现死锁,对吧?当1号服务器创建节点挂掉,删除失败,其他99台服务器等待通知。然后就结束了。这时候就需要使用临时节点。之前我们说过,临时节点的特点是一旦客户端断开,就会丢失。即服务器1创建节点后,如果挂掉,该节点会自动删除,以便后续其他服务器继续创建节点并获取锁。但是我们可能还需要注意一点,那就是令人震惊的群体效应:举个很简单的例子,当你把一块食物扔到一群鸽子中间,虽然最后只有一只鸽子抢到了食物,所有的鸽子都会惊慌地参加比赛。Grabbing...意思是当服务器1节点发生变化时,会通知剩下的99台服务器,但是最终只有1台服务器创建成功,所以98还需要等待监听,所以为了应对这种情况,你需要使用临时序列节点。大致意思就是之前99台服务器都监听一个节点,现在每台服务器都监听自己前面的一个节点。假设有100台服务器同时发送请求,此时/zkjjj节点下会创建100个临时顺序节点/zkjjj/000000001,/zkjjj/000000002,直到/zkjjj/000000100。获取锁的顺序已经确定。当001节点处理完毕,删除节点后,002收到通知获取锁,开始执行。执行完成后删除该节点,并通知003~以此类推。