先了解一下什么是锁,在单机系统中,多个线程同时改变一个变量时,需要对变量或者代码块做同步从而保证串行修改变量,该同步实质上就是通过锁来实现。为了实现多个线程在同一个时刻针对同一块代码串行执行,就需要在某个地方做个标记,该标记必须每个线程都能看到,当标记不存在时可以设置该标记,其余后续线程发现已经有标记了则等待拥有标记的线程结束同步代码块取消标记后再去尝试设置标记,此标记可以理解为锁。分布式锁就是在多机系统下的该标记。
目前分布式锁的实现方式有3种主流方法,即:
(1) 基于数据库实现分布式锁,此处的数据库指的是MySQL关系型数据库
(2) 基于缓存实现分布式锁,此处的缓存指的是Redis
(3) 基于zookeeper/etcd实现分布式锁
具体的关于锁的实现方式,已经有太多的文章进行介绍,本文就不再赘述。
并发问题一旦涉及到钱,通常都会导致不同程度的资损,而且在我们的功能测试中是很难发现,因此对于并发的质量保障显得尤为的重要,可以抽象为3层来保障:事前、事中、事后三大步骤;事前保障通过Review 方式提前规避技术上的风险,事中保障验证在技术实现过程中是否存在漏洞,事后保障校验数据是否符合预期,对于有并发风险的项目上述三个步骤的保障缺一不可。
事前保障的阶段发生在技术评审阶段,在此阶段,我们需要评估出当前业务场景下是否存在并发风险;如果存在,确定我们的技术选型。
评估并发风险的关键点在于是否存在多个进程同时访问共享资源,简单来说是否存在多个进程在同一时间对同一个数据进行更新的操作;例如:电商中的库存,多人同时购买同一个商品,也就是会存在同一时间对同一个商品的库存进行更新,此处就存在并发风险。
要做到正确的技术选型,我们就需要对上述种方式实现的锁的优缺点以及应用场景需要进行了解。
MySQL数据库表的乐观锁适用于读多写少的场景且共享资源为数据库的单行数据;MySQL表锁实现的锁一般都不推荐使用;ZooKeeper分布式锁虽然适用于大部分分布式场景,但是由于其实现复杂度相对较高以及需要额外引入中间件,在大部分业务场景中的应用比较少,而基于Redis的缓存分布式锁应用较为广泛;但是具体业务实现采用哪种类型的分布式锁,还是需要基于当前的业务特性来进行决定;
在技术评审阶段,一方面我们要评估出是否存在并发风险,另外一方面,我们需要识别开发同学在技术的实现上可能存在的漏洞,针对分布式锁的实现漏洞可参考下文的CodeReview的关注点。
(1) Redis缓存分布式锁
Redis通常可以使用setnx(key,value)函数来实现分布式锁。key和value就是基于缓存的分布式锁的两个属性,其中key表示锁id。setnx函数返回1表示获得锁,返回0表示其他服务器已经获得了锁;
Redis缓存分布式锁CodeReview注意点:
1)Redis Key
例如:商品库存,我们的key应该是具体到某个商品,而不是所有商品,锁住A商品,不会影响B商品。
2)锁释放
针对上述问题,释放锁时需要先读取当前key的value,再和传入的value进行比较;上述是两个步骤一定要保证原子性,如果原生Redis可采用lua脚本保证原子性;如果tair,可采取TairString的cad方法;value必须是一个唯一值,唯一标记是当前对象加的锁。
3)锁超时
a. 一定要设置key的超时时间;例如:客户端A 抢到锁后,系统突然异常,A就无法释放锁,变成死锁;设置超时时间就是为了防止此种情况发生,在时间到期后,自动删除key,间接释放锁;
b. 超时时间的设置一般来讲大于服务的最大执行时间即可,但是服务最大的执行时间会受很多因素影响,是不可控的;例如:A服务一般执行时间是30ms,设置的锁超时时间为100ms,受网络影响服务执行时间变成了200ms,在100ms的时候锁就会被释放了;在大部分场景下,开发不会处理此种情况,此种极端情况是否需要处理,需要进行协商;处理方式如下两种:
4)锁粒度
如果针对某个共享资源的写是基于另外一个共享资源的值计算而来,那么锁的范围必须包含读共享资源;范围不包含读共享资源会导致脏读,最终导致数据的错误,如下图所示,Client B最终计算的B的结果就是错误的。
5)获取锁失败
由于其他线程已经获取到了锁,当前线程获取锁失败后有3种处理方式:异常抛出让用户重试;通过自旋再次进行抢锁;发布订阅,订阅锁释放消息;在并发度低的场景下异常抛出以及自旋抢锁都可以,在高并发场景下异常抛出和自旋抢锁都不可取。
(2) MySQL数据库锁CR点
1)数据库版本号乐观锁
在数据库的表中需要包含一个数字类型的字段version,读取数据时把version字段读出来,更新数据时判断当前version是否等于读取出来的version,并对当前version+1;如果等于就更新成功,不等于表示数据已过期更新失败。例如以积分体系为例,存在多种场景增加积分,通过乐观锁来保证数据的正确性。
乐观锁CR注意点:
2)基于MySQL锁表
其实现原理是:创建一张锁表,对临界资源做唯一性约束,通过增加一条记录对某一资源上锁,释放锁时删除记录;一般不推荐此种用法。
并发测试总体上可以分为三大类:
(1) 复杂的并发场景,一次请求共享资源存在多个,且前后存在各种依赖关系,此种场景适合于链路级别压测,压测模型需要精心设计。
(2) 单一并发场景,一个共享资源,可以处理多次,例如:扣除某个商品的库存,可以反复调用。
(3) 单一并发场景,一个共享资源,且只能处理1次,例如:用户只有一次抽奖机会,连续点2次会不会抽2次;
public void invokeAllTask(ConcurrencyRequest request, Runnable task) {
final CountDownLatch startCountDownLatch = new CountDownLatch(1);
final CountDownLatch endCountDownLatch = new CountDownLatch(request.getConcurrency());
for (int i = 0; i < request.getConcurrency(); i++) {
Thread t = new Thread(() -> {
try {
startCountDownLatch.await();
try {
task.run();
} finally {
endCountDownLatch.countDown();
}
} catch (Exception ex) {
log.error("异常", ex);
}
});
t.start();
}
startCountDownLatch.countDown();
try {
endCountDownLatch.await();
} catch (InterruptedException ex) {
log.error("线程异常中断", ex);
}
}
数据对账(数据一致性校验)是我们在系统上线后对并发问题的最后一道防线,通过对账来识别我们的数据的不一致性问题;压测有成本,且受技巧熟练度和压测设计的影响,不一定能暴露问题;如果被测场景评估并发问题的发生概率极低,即使发生了影响也比较小,此时review+对账方式也不失为一种好的选择;
如何进行对账,不同的业务场景有不同的对账方法,例如:
select count(*) as task_count,
scene_code,
order_id
from task_record
where unique_id is not null
group by scene_code,
order_id
having count(*)> 1
作为质量保障同学一定要时刻绷着一根弦,当前场景下是否会存在并发问题;并发问题的识别简单而言就是是否存在同时更新同一个数据,如果是就一定要注意开发同学是否处理了并发,并发的实现主要是上面阐述的几种,然后按照场景进行分析即可;关于并发场景的质量保障,大体原则可以概括为如下:
文章名称:并发-分布式锁质量保障总结
浏览地址:http://www.shufengxianlan.com/qtweb/news11/440611.html
网站建设、网络推广公司-创新互联,是专注品牌与效果的网站制作,网络营销seo公司;服务项目有等
声明:本网站发布的内容(图片、视频和文字)以用户投稿、用户转载内容为主,如果涉及侵权请尽快告知,我们将会在第一时间删除。文章观点不代表本网站立场,如需处理请联系客服。电话:028-86922220;邮箱:631063699@qq.com。内容未经允许不得转载,或转载时需注明来源: 创新互联