简介:并发问题是电商零碎最常见的问题之一,例如库存超卖、抽奖多发、券多发放、积分多发少发等场景;之所以会呈现上述问题,是因为存在多机器多申请同时对同一个共享资源进行批改,如果不加以限度,将导致数据错乱和数据不一致性;解决并发问题的形式有很多,例如:队列、异步、响应式、锁都能够;因为以后互联网都是分布式系统,因而本文只针对应用较为宽泛的分布式锁的形式来进行叙述如何进行品质保障。
作者 | 靖北
起源 | 阿里技术公众号
一 背景
并发问题是电商零碎最常见的问题之一,例如库存超卖、抽奖多发、券多发放、积分多发少发等场景;之所以会呈现上述问题,是因为存在多机器多申请同时对同一个共享资源进行批改,如果不加以限度,将导致数据错乱和数据不一致性;解决并发问题的形式有很多,例如:队列、异步、响应式、锁都能够;因为以后互联网都是分布式系统,因而本文只针对应用较为宽泛的分布式锁的形式来进行叙述如何进行品质保障。
二 分布式锁介绍
1 什么是分布式锁
先理解一下什么是锁,在单机零碎中,多个线程同时扭转一个变量时,须要对变量或者代码块做同步从而保障串行批改变量,该同步本质上就是通过锁来实现。为了实现多个线程在同一个时刻针对同一块代码串行执行,就须要在某个中央做个标记,该标记必须每个线程都能看到,当标记不存在时能够设置该标记,其余后续线程发现曾经有标记了则期待领有标记的线程完结同步代码块勾销标记后再去尝试设置标记,此标记能够了解为锁。分布式锁就是在多机系统下的该标记。
2 实现分布式锁的支流形式
目前分布式锁的实现形式有 3 种支流办法,即:
基于数据库实现分布式锁,此处的数据库指的是 MySQL 关系型数据库
- 基于 MySQL 锁表
- 数据库版本号乐观锁
基于缓存实现分布式锁,此处的缓存指的是 Redis
基于 zookeeper/etcd 实现分布式锁
具体的对于锁的实现形式,曾经有太多的文章进行介绍,本文就不再赘述。
三 品质保障
并发问题一旦波及到钱,通常都会导致不同水平的资损,而且在咱们的功能测试中是很难发现,因而对于并发的品质保障显得尤为的重要,能够形象为 3 层来保障:事先、事中、预先三大步骤;事先保障通过 Review 形式提前躲避技术上的危险,事中保障验证在技术实现过程中是否存在破绽,预先保障校验数据是否合乎预期,对于有并发危险的我的项目上述三个步骤的保障缺一不可。
1 事先品质保障
事先保障的阶段产生在技术评审阶段,在此阶段,咱们须要评估出以后业务场景下是否存在并发危险;如果存在,确定咱们的技术选型。
评估并发危险
评估并发危险的关键点在于是否存在多个过程同时访问共享资源,简略来说是否存在多个过程在同一时间对同一个数据进行更新的操作;例如:电商中的库存,多人同时购买同一个商品,也就是会存在同一时间对同一个商品的库存进行更新,此处就存在并发危险。
技术选型
要做到正确的技术选型,咱们就须要对上述 3 种形式实现的锁的优缺点以及利用场景须要进行理解。
MySQL 数据库表的乐观锁实用于读多写少的场景且共享资源为数据库的单行数据;MySQL 表锁实现的锁个别都不举荐应用;ZooKeeper 分布式锁尽管实用于大部分分布式场景,然而因为其实现复杂度绝对较高以及须要额定引入中间件,在大部分业务场景中的利用比拟少,而基于 Redis 的缓存分布式锁利用较为宽泛;然而具体业务实现采纳哪种类型的分布式锁,还是须要基于以后的业务个性来进行决定;
在技术评审阶段,一方面咱们要评估出是否存在并发危险,另外一方面,咱们须要辨认开发同学在技术的实现上可能存在的破绽,针对分布式锁的实现破绽可参考下文的 CodeReview 的关注点。
2 事中保障
CodeReview
1)Redis 缓存分布式锁
Redis 通常能够应用 setnx(key,value)函数来实现分布式锁。key 和 value 就是基于缓存的分布式锁的两个属性,其中 key 示意锁 id。setnx 函数返回 1 示意取得锁,返回 0 示意其余服务器曾经取得了锁;
Redis 缓存分布式锁 CodeReview 留神点
1、Redis Key
- 全面梳理业务场景,对于同一独特资源,key 要保持一致;
- key 是辨认共享资源的惟一键,key 的设计既须要可能锁住以后共享资源又不能影响到其余资源;
- 例如:商品库存,咱们的 key 应该是具体到某个商品,而不是所有商品,锁住 A 商品,不会影响 B 商品。
2、锁开释
锁肯定需明确开释,try/finally 构造加锁解锁,finally 内开释锁;
锁只能被加锁的对象开释,此处是常常出问题的点,如下图所示,A 加锁被 B 开释锁,导致锁生效,锁被 C 抢占到;
针对上述问题,开释锁时须要先读取以后 key 的 value, 再和传入的 value 进行比拟;上述是两个步骤肯定要保障原子性, 如果原生 Redis 可采纳 lua 脚本保障原子性;如果 tair,可采取 TairString 的 cad 办法;value 必须是一个惟一值,惟一标记是以后对象加的锁。
3、锁超时
- 肯定要设置 key 的超时工夫;例如: 客户端 A 抢到锁后,零碎忽然异样,A 就无奈开释锁,变成死锁;设置超时工夫就是为了避免此种状况产生,在工夫到期后,主动删除 key,间接开释锁;
- 超时工夫的设置一般来讲大于服务的最大执行工夫即可,然而服务最大的执行工夫会受很多因素影响,是不可控的;例如:A 服务个别执行工夫是 30ms,设置的锁超时工夫为 100ms,受网络影响服务执行工夫变成了 200ms,在 100ms 的时候锁就会被开释了;在大部分场景下,开发不会解决此种状况,此种极其状况是否须要解决,须要进行协商;解决形式如下 2 种:
- 能够再开启一个线程,为以后超时工夫续时,但减少了零碎的复杂度;
- 将过期工夫设置十分长,肯定能保障逻辑在锁开释之前可能执行实现;此计划简略然而有缺点,当遇到零碎突发异样时,锁无奈被开释,只能期待 redis key 超时,而超时工夫又设置的较长,因而在以后工夫内谁都无奈获取到锁,阻断业务执行,很有可能造成故障;
4、锁粒度
如果针对某个共享资源的写是基于另外一个共享资源的值计算而来,那么锁的范畴必须蕴含读共享资源;范畴不蕴含读共享资源会导致脏读,最终导致数据的谬误,如下图所示,Client B 最终计算的 B 的后果就是谬误的。
5、获取锁失败
因为其余线程曾经获取到了锁,以后线程获取锁失败后有 3 种解决形式:异样抛出让用户重试;通过自旋再次进行抢锁;公布订阅,订阅锁开释音讯;在并发度低的场景下异样抛出以及自旋抢锁都能够,在高并发场景下异样抛出和自旋抢锁都不可取。
2)MySQL 数据库锁 CR 点
- 数据库版本号乐观锁
在数据库的表中须要蕴含一个数字类型的字段 version,读取数据时把 version 字段读出来,更新数据时判断以后 version 是否等于读取进去的 version,并对以后 version+1;如果等于就更新胜利,不等于示意数据已过期更新失败。例如以积分体系为例,存在多种场景减少积分,通过乐观锁来保证数据的正确性。
乐观锁 CR 留神点:
- where 条件肯定要命中索引(最好是主键或者惟一索引),否则会锁表;
- update table set 中必须要蕴含 version = version + 1;
- update 返回后果为 0 时,肯定要依据业务场景进行相应的解决,自主重试或者抛异样;
- 基于 MySQL 锁表
其实现原理是:创立一张锁表,对临界资源做唯一性束缚,通过减少一条记录对某一资源上锁,开释锁时删除记录;个别不举荐此种用法。
并发测试
并发测试总体上能够分为 3 大类
- 简单的并发场景,一次申请共享资源存在多个,且前后存在各种依赖关系,此种场景适宜于链路级别压测,压测模型须要精心设计。
- 繁多并发场景,一个共享资源,能够解决屡次,例如:扣除某个商品的库存,能够重复调用。
能够通过接口压测的形式进行测试,通过查看最终数据是否会存在与预期不统一状况即可;
压测工具:jmeter 即可进行压测(团体可间接采纳 pas-server 进行压测,方便快捷);
- 繁多并发场景,一个共享资源,且只能解决 1 次,例如:用户只有一次抽奖机会,间断点 2 次会不会抽 2 次;
能够利用 JVM 的并发函数 CountDownLatch,CyclicBarrier 等,
CountDownLatch 片段代码: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);
}
}
利用 jmeter 的定时器 Synchronizing Timer 也能够实现此性能
3 预先保障
数据对账
数据对账 (数据一致性校验) 是咱们在零碎上线后对并发问题的最初一道防线,通过对账来辨认咱们的数据的不一致性问题;压测有老本,且受技巧熟练度和压测设计的影响,不肯定能裸露问题;如果被测场景评估并发问题的产生概率极低,即便产生了影响也比拟小,此时 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
四 总结
作为品质保障同学肯定要时刻绷着一根弦,以后场景下是否会存在并发问题;并发问题的辨认简略而言就是是否存在同时更新同一个数据,如果是就肯定要留神开发同学是否解决了并发,并发的实现次要是下面论述的几种,而后依照场景进行剖析即可;对于并发场景的品质保障,大体准则能够概括为如下:
1、梳理并发场景
2、带着留神点 CR 代码
3、并发测试(非银弹,不是所有场景都具备可测性)
4、监控对账进行兜底辨认并发问题
原文链接
本文为阿里云原创内容,未经容许不得转载。