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