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

并发-分布式锁质量保证总结

时间:2023-04-02 01:55:36 Java

介绍:并发问题是电商系统中最常见的问题之一,例如库存超卖、多次抽奖、多次优惠券、积分多于少等。问题是有多台机器,多个请求同时修改同一个共享资源。如果没有限制,会导致数据混乱,数据不一致;解决并发问题的方法有很多种,比如:queue、asynchronous、responsive、lock都有;由于现在的互联网是分布式系统,本文只介绍目前广泛使用的分布式锁方式如何进行质量保证。作者|京北源|阿里科技公众号一、背景并发问题是电商系统中最常见的问题之一,比如库存超卖、多次开奖、多张优惠券、积分多积分少等问题。问题是有多台机器,多个请求同时修改同一个共享资源。如果没有限制,会导致数据混乱,数据不一致;解决并发问题的方法有很多种,比如:queue、asynchronous、responsive、lock都有;由于现在的互联网是分布式系统,本文只介绍目前广泛使用的分布式锁方式如何进行质量保证。2分布式锁简介1什么是分布式锁?我们先来了解一下什么是锁。在单机系统中,当多个线程同时修改一个变量时,需要对该变量或代码块进行同步,以保证该变量被串行修改。这个同步的本质就是上面是通过锁来实现的。为了让多个线程同时对同一块代码串行执行,需要在某个地方进行标记。该标记必须对每个线程可见。当标记不存在时,可以设置标记,后续线程发现已经有标记时,等待有标记的线程结束同步代码块,取消标记在尝试设置标记之前。这个标记可以理解为一把锁。分布式锁是多机系统下的标志。2分布式锁的主流实现方式目前分布式锁的主流实现方式有3种,即:基于数据库实现分布式锁。这里的数据库指的是MySQL关系型数据库。基于MySQL锁表数据库版本号的乐观锁实现基于缓存的分布式锁。这里的缓存指的是Redis基于zookeeper/etcd实现的分布式锁。关于锁的实现的文章已经太多了,本文不再赘述。三质量保证并发问题,一旦牵扯到钱,通常会导致不同程度的资金损失,而我们的功能测试很难发现,所以对于并发的质量保证尤为重要,可以抽象为三个层次保障:分为事前、事中、事后三大步骤;事前保障通过Review方式提前规避技术风险,事中保障验证技术实施过程是否存在漏洞,事后保障验证数据是否符合预期。对于有并发风险的项目,以上三步的保护必不可少。1、事前质量保证事前质量保证阶段发生在技术审评阶段。这个阶段需要评估当前业务场景是否存在并发风险;如果是这样,确定我们的技术选择。评估并发风险的关键点是是否有多个进程同时访问共享资源。简单的说,是否有多个进程同时更新同一个数据;例如:电商中的库存,多人同时购买同一个商品,意味着同一个商品的库存会同时更新,这里存在并发的风险。技术选型要实现正确的技术选型,我们需要了解以上三种方式实现的锁的优缺点和应用场景。MySQL数据库表乐观锁适用于读多写少,共享资源为数据库单行数据的场景;一般不推荐使用MySQL表锁实现的锁;ZooKeeper分布式锁适用于大部分分布式场景,但是由于其实现复杂度比较高,需要引入额外的中间件,所以在大部分业务场景中很少使用,而基于Redis的缓存分布式锁被广泛使用;但是具体的业务实现是用哪种类型的分布式锁呢?还是要结合当前的业务特点来做决定;在技??术审核阶段,一方面要评估是否存在并发风险;对于实现漏洞,请参考以下CodeReview关注点。2保证CodeReview1)Redis缓存分布式锁Redis通常可以使用setnx(key,value)函数来实现分布式锁。key和value是基于缓存的分布式锁的两个属性,其中key代表锁id。setnx函数返回1表示已经获得锁,返回0表示其他服务器已经获得锁;Redis缓存分布式锁CodeReview注意点1、RedisKey全面梳理业务场景,对于同一个公共资源,key必须保持一致;key是标识共享资源的唯一key,key的设计需要能够锁定当前共享资源,不影响其他资源;比如:商品库存,我们的key应该是具体到某一个商品,而不是所有商品,锁定商品A,不会影响到商品B。2.锁释放锁必须明确释放。try/finally结构加锁和解锁,最后释放锁;锁只能由被锁定的对象释放。这是经常出现问题的地方。如下图,A加锁,B释放锁,导致锁失效,锁被C抢占;针对以上问题,在释放锁时,需要先读取当前key的值,然后与传入的值进行比较;以上两步必须保证原子性,如果原生Redis可以使用lua脚本保证原子性;如果是Tair,可以使用TairString的cad方法;value必须是一个唯一的值,唯一的标记是加在当前对象上的锁。3、锁超时必须设置key的超时时间;例如:客户端A抢到锁后,系统突然出现异常,A无法释放锁,成为死锁;设置超时时间是为了防止这种情况发生。过期后自动删除key,间接释放锁;超时设置一般要大于服务的最大执行时间,但是服务的最大执行时间会受到很多因素的影响,是不可控的;例如:一个服务一般执行时间为30ms,锁超时时间设置为100ms,受网络影响服务执行时间变为200ms,100ms会释放锁;大多数场景下,开发不会处理这种情况,这里需要协商确定是否需要处理极端情况;有两种处理方式:可以另开一个线程继续当前的超时时间,但是这样会增加系统的复杂度;将过期时间设置成很长的时间,会保证逻辑到位,在锁释放之前可以执行;这个解决方案很简单但有缺陷。当系统遇到突发异常时,无法释放锁,只能等待rediskey超时,超时时间设置的比较长。因此,无论谁4.锁粒度如果对一个共享资源的写是根据另一个共享资源的值来计算的,那么加锁的范围一定是包括读共享资源的;范围不包括读共享资源的,这样会导致导致脏读,最终导致数据错误。如下图,ClientB最终计算出的B结果是错误的。5、获取锁失败由于其他线程已经获取了锁,所以当前线程获取锁失败后的处理方式有3种:抛出异常让用户重试;通过旋转再次抓住锁;发布订阅,订阅解锁消息;在低并发的场景下,抛异常和自旋抓取都是可以的,但是在高并发的场景下,抛异常和自旋抓取是不可取的。2)MySQL数据库锁CR点数据库版本号乐观锁需要在数据库表中包含一个数字字段版本,读取数据时读取版本字段,更新数据时判断当前版本是否等于读取的版本,并添加1到当前版本;如果相等,则更新成功,如果不相等,则表示数据已过期,更新失败。比如以积分系统为例,增加积分的场景有很多,使用乐观锁来保证数据的正确性。乐观锁CR注意点:where条件必须命中索引(最好是主键或唯一索引),否则会锁表;updatetableset必须包含version=version+1;当update返回0时,必须根据业务场景进行相应处理,独立重试或抛出异常;基于MySQL锁表,实现原理是:创建一个锁表,对关键资源做唯一约束,通过增加一条记录来锁定某个资源,释放锁时删除Logging;一般不推荐这种用法。并发测试并发测试一般可以分为三类复杂的并发场景。一次请求多个共享资源,前后有各种依赖关系。该场景适用于链路级压测,需要精心设计压测模型。.单个并发场景,一个共享资源,可以多次处理,比如:扣某款商品的库存可以重复调用。可以通过接口压测的方式进行测试,只需要检查最终数据是否与预期不一致即可;压测工具:可以使用jmeter进行压测(组内可以直接使用pas-server进行压测,方便快捷);单个并发场景,共享资源,只能处理一次,例如:用户只有一次抽奖机会,连续点击两次会不会抽两次;可以使用JVM的并发函数CountDownLatch、CyclicBarrier等,CountDownLatch片段代码:publicvoidinvokeAllTask??(ConcurrencyRequestrequest,Runnabletask){finalCountDownLatchstartCountDownLatch=newCountDownLatch(1);finalCountDownLatchendCountDownLatch=newCountDownLatch(request.getConcurrency());对于(inti=0;i{try{startCountDownLatch.await();try{task.run();}finally{endCountDownLatch.countDown();}}catch(Exceptionex){log.error("Exception",ex);}});t.开始();}startCountDownLatch。倒数();尝试{endCountDownLatch.await();}catch(InterruptedExceptionex){log.error("线程异常中断",ex);}}这个功能也可以通过jmeter的timer来实现SynchronizingTimer3GuaranteedataafterthetermsReconciliation数据对账(数据一致性验证)是我们在系统上线后对并发问题的最后一道防线。我们可以通过核对来识别数据中的不一致;压力测试是有成本的,受技能熟练度和压力测试设计的影响不一定能暴露问题;如果被测场景的评估并发问题出现的概率极低,即使发生影响,此时review+reconciliation的方式也是不错的选择;不同的业务场景有不同的对账方式,例如:每个用户在交互积分系统中的扣减和增加都会记录在水表中;每个用户的当前积分将放在积分表中;核对积分总和和积分表中的积分;交互式任务系统,一个订单只能推进一个任务,对账时只需要检查任务记录中一个订单是否有多个记录;selectcount(*)astask_count,scene_code,order_idfromtask_recordwhereunique_idisnotnullgroupbyscene_code,order_idhavingcount(*)>14.总结作为质量保证,同学们一定要时刻把握是否会并发当前场景存在的问题;并发问题的识别很简单,一句话就是是否存在同一个数据的同步更新。如果是的话,一定要注意开发同学有没有处理过并发。原则可以归纳为:1.梳理并发场景2.CR代码注意点3.并发测试(不是灵丹妙药,不是所有场景都可衡量)4.监控和调和彻底识别并发问题原文链接本文为阿里云原创内容,未经许可不得转载