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

说说分布式锁的多种实现!

时间:2023-03-20 12:56:31 科技观察

1。分布式锁概述我们的系统是分布式部署的。在日常开发中,为了防止在秒下单、抢购等业务场景下库存超卖,我们需要使用分布式锁。分布式锁实际上是一种控制分布式系统中不同进程访问共享资源的锁的实现。如果不同的系统或者同一系统的不同主机共享某个关键资源,往往需要互斥来防止相互干扰,保证一致性。业界流行的分布式锁实现方式一般有以下三种:基于数据库实现的分布式锁。基于Redis实现的分布式锁。基于Zookeeper实现的分布式锁。2.基于数据库的分布式锁2.1数据库悲观锁实现的分布式锁可以使用select...forupdate来实现分布式锁。我们自己的项目,分布式定时任务,采用了类似的实现方案。让我给你看一个简单的版本!表结构如下:CREATETABLE`t_resource_lock`(`key_resource`varchar(45)COLLATEutf8_binNOTNULLDEFAULT'resourceprimarykey',`status`char(1)COLLATEutf8_binNOTNULLDEFAULT''COMMENT'S,F,P',`lock_flag`int(10)unsignedNOTNULLDEFAULT'0'COMMENT'1锁定,0解锁',`begin_time`datetimeDEFAULTNULLCOMMENT'开始时间',`end_time`datetimeDEFAULTNULLCOMMENT'结束time',`client_ip`varchar(45)COLLATEutf8_binNOTNULLDEFAULT'获得锁的IP',`time`int(10)unsignedNOTNULLDEFAULT'60'COMMENT'只允许一个节点获取一次锁在方法lifetime,unit:minute',PRIMARYKEY(`key_resource`)USINGBTREE)ENGINE=InnoDBDEFAULTCHARSET=utf8COLLATE=utf8_bin加锁方法的伪代码如下:@Transcational//mustaddtransactionpublicbooleanlock(StringkeyResource,inttime){resourceLock='select*fromt_resource_lockwherekey_resource='#{keySource}'forupdate';try{if(resourceLock==null){//插入锁数据resourceLock=newResourceLock();resourceLock.setTime(时间);资源ourceLock.setLockFlag(1);//锁定resourceLock.setStatus(P);//处理resourceLock.setBeginTime(newDate());intcount="插入资源锁";if(count==1){/}返回false;}}catch(Exceptionx){返回false;}//如果锁没有上锁且锁超时,则可以成功获取锁if(resourceLock.getLockFlag=='0'&&'S'.equals(resourceLock.getstatus)&&newDate()>=resourceLock.addDateTime(resourceLock.getBeginTime(,time)){resourceLock.setLockFlag(1);//锁定resourceLock.setStatus(P);//处理resourceLock.setBeginTime(newDate());//更新resourceLock;returntrue;}elseif(newDate()>=resourceLock.addDateTime(resourceLock.getBeginTime(,time)){//超时未正常完成,获取锁失败returnfalse;}else{returnfalse;}}伪代码解锁方法如下:publicvoidunlock(Stringv,status){resourceLock.setLockFlag(0);//解锁resourceLock.setStatus(status);S:表示成功,F表示失败//更新资源锁;return;}整体流程:try{if(lock(keyResource,time)){//lockstatus=process();//你的业务逻辑处理}}finally{unlock(keyResource,status);//释放锁}其实悲观锁实现的分布式锁的整体流程已经比较清楚了。就是select...forupdate锁定主键key_resource的记录。如果为空,则可以插入一条记录。如果存在记录,判断状态和时间,是否超时。这里需要注意,必须要添加事务。2.2数据库乐观锁实现的分布式锁除了悲观锁,还可以使用乐观锁来实现分布式锁。乐观锁,顾名思义,就是非常乐观。每次更新操作,感觉不会有并发冲突。只有更新失败后,才会重试。它是基于CAS的思想实现的。我之前的公司就是用这个方法扣余额的。做一个版本字段,每更新修改一次,就加一,然后更新余额的时候,把查到的版本号带条件更新。如果是上次的版本号,更新一下。如果没有,说明如果其他人并发修改了,继续重试。大致流程如下:查询版本号和余额。选择版本,账户余额whereuser_id='666';假设找到的版本号是oldVersion=1。判断余额的逻辑处理。if(余额<扣除金额){return;}left_balance=balance-扣除金额;扣除余额。更新账户设置balance=#{left_balance},version=version+1whereversion=#{oldVersion}andbalance>=#{left_balance}anduser_id='666';可以看看这个流程图:这种方法适用于并发量不高的场景,一般需要设置重试次数。3、基于Redis实现的分布式锁Redis分布式锁一般有以下几种实现方式:setnx+expire。setnx+value是过期时间。set的扩展命令(setexpxnx)。setexpxnx+校验唯一随机值,然后删除。雷迪森。雷迪森+红锁。3.1setnx+expire说到Redis分布式锁,很多朋友反手就是setnx+expire,如下:if(jedis.setnx(key,lock_value)==1){//setnxlockexpire(key,100);//设置过期时间try{dosomething//业务处理}catch(){}finally{jedis.del(key);//释放锁}}这段代码可以成功加锁,但是如果发现有问题,加锁操作和设置超时时间是分开的。假设执行完setnx锁后,即将执行expire设置过期时间时,进程崩溃或者需要重启维护,那么锁将是永生的,其他线程将永远无法获取到锁,所以分布式锁不能这样实现!3.2setnx+value为过期时间longexpires=System.currentTimeMillis()+expireTime;//系统时间+设置过期时间StringexpiresStr=String.valueOf(expires);//如果当前锁不存在,则返回Lock成功if(jedis.setnx(key,expiresStr)==1){returntrue;}//如果锁已经存在,则获取锁的过期时间StringcurrentValueStr=jedis.get(key);//如果获取到过期时间,小于系统当前时间,表示已经过期if(currentValueStr!=null&&Long.parseLong(currentValueStr)30ms+40ms+50ms+4m0s+50ms)。如果获得了锁,则key的真实有效时间会发生变化,需要减去获得锁所用的时间。如果获取锁失败(至少有N/2+1个master实例没有获取到锁,或者获取锁时间已经超过有效时间),client必须解锁所有master节点(即使部分master节点没有有锁成功的还需要解锁,防止有人漏网)。接下来要简化的步骤是:依次向5个主节点请求锁。根据设置的超时时间判断是否跳过主节点。如果成功加锁的节点大于等于3个,且使用时间小于锁的有效期,则可以判定加锁成功。如果获取锁失败,请解锁!Redisson已经实现了redLock版本的锁。有兴趣的可以去了解一下~4.Zookeeper分布式锁在学习Zookeeper分布式锁之前,我们先来回顾一下Zookeeper的节点。Zookeeper的节点Znode有四种类型:持久节点:默认节点类型。创建该节点的客户端与zookeeper断开连接后,该节点仍然存在。持久节点时序节点:所谓时序节点是指在创建节点时,Zookeeper按照创建的时间顺序对节点名称进行编号,持久节点时序节点即为时序持久节点。临时节点:与持久节点相反,当创建该节点的客户端与zookeeper断开连接时,临时节点将被删除。临时时序节点:顺序中的临时节点。Zookeeper分布式锁实现应用临时顺序节点。代码这里就不贴了,说说zk分布式锁的实现原理。4.1zk获取锁的过程当客户端第一次请求时,Zookeeper客户端会创建一个持久化的节点锁。如果它(Client1)想要获取锁,需要在locks节点下创建一个时序节点lock1。如图:接下来,客户端Client1会搜索locks下的所有临时顺序子节点,判断自己的节点lock1是否是序号最小的那个,如果是,则成功获取到锁。这时候如果有另一个客户端client2过来尝试获取锁,就会在locks下创建另一个临时节点lock2。客户端client2也会查找locks下的所有临时顺序子节点,判断自己的节点lock2是否最小。这时发现lock1最小,所以获取锁失败。如果获取锁失败,则不会进行协调。client2向其排名靠前的节点lock1注册一个Watcher事件,以监视lock1是否存在。也就是说client2抢锁失败,进入等待状态。此时如果另一个客户端Client3试图获取锁,它会在locks下创建另一个临时节点lock3。同样,client3也会查找locks下的所有临时顺序子节点,判断自己的节点lock3是否是最小的。如果发现它不是最小的,则无法获取锁。它不会和解,它会向前面的节点lock2注册Watcher事件,监听lock2节点是否存在。4.2释放锁我们再来看看释放锁的过程。当Zookeeper的客户端业务完成或失败时,临时节点会被删除,锁也会被释放。如果任务完成,Client1会显式调用删除lock1的命令。如果客户端出现故障,根据临时节点的特点,lock1会自动删除。lock1节点删除后,Client2很高兴,因为它一直在监听lock1。当lock1节点被删除时,Client2会立即收到通知,同时也会搜索所有处于锁定状态的临时顺序子节点,将lock2最小的发送出去,然后获取锁。同理,Client2拿到锁后,Client3也看了,呵呵~Zookeeper的设计定位是分布式协调,好用。如果拿不到锁,加个监听就可以了,很适合分布式锁。Zookeeper作为分布式锁也有缺点:如果很多客户端频繁申请锁和释放锁,Zookeeper集群的压力会更大。5、三种分布式锁的比较5.1数据库分布式锁实现的优点:简单、易用,无需引入Redis、zookeeper等中间件。缺点:不适合高并发场景。db操作性能差,存在锁表风险。5.2Redis分布式锁实现优点:性能好,适合高并发场景。轻的。有更好的框架支持,比如Redisson。缺点:过期时间不易控制。需要考虑锁被其他线程误删的场景。5.3Zookeeper分布式锁实现的缺点:性能不如redis实现的分布式锁。比较重的分布式锁。优点:更好的性能和可靠性。有封装更好的框架,比如Curator。5.4对比总结从性能上看(从高到低)Redis>Zookeeper>=database;从易于理解的角度(从低到高)数据库>Redis>Zookeeper;从实现复杂度来看(从低到高)Zookeeper>Redis>Database;从可靠性角度(从高到低)Zookeeper>Redis>Database。