redis分布式锁实现秒杀活动

        最近,参与和负责公司的一次秒杀活动的设计开发,收获颇多,与大家分享。其实大家在生活中或见过或参见过秒杀活动,用户以极低的成本获得高价值的商品,所以也导致活动期间出现拥挤现象,进而导致一些高并发问题,所谓每一次的秒杀活动都是考研公司技术架构的时刻,可真是一点不假。下面就以公司的此次活动为例,介绍下整个秒杀活动从无到有的过程。

秒杀活动期间原价为三位数的商品,只需一元,且该热点商品有以下限制:

1、库存限制,秒杀商品有N件,不可以超卖

2、每个用户仅可以购买一次,即对用户限购

3、限定地区为A城市,即活动只允许A城市用户参加

4、活动有时间限制

5、未抢购到的用户,给予优惠券补偿,且只允许一张

6、用户可能在多个端操作(APP、小程序等)

整个活动的流程如下图:


       经过上述描述后,秒杀活动对象、要求限制已经很详细,接下来开始分析,如何设计我们的系统来满足活动要求。活动时间限制非常简单,不再赘述。其中地域要求可以通过地图服务商(高德、百度、搜狗等)提供的经纬度实现限制,当然也可以通过IP,但是准确性不高,常用到的包括API如下:

淘宝API:http://ip.taobao.com/service/getIpInfo.php?ip=127.0.0.1
新浪API:http://int.dpool.sina.com.cn/iplookup/iplookup.php?format=json&ip=218.192.3.42
pconline API:http://whois.pconline.com.cn/ 
百度API:http://api.map.baidu.com/location/ip?ip=218.192.3.42

       接下来是整个设计的重头戏:库存,活动库存要求N件,卖少了,不讨论,而多卖了,公司会损失。库存限制的重要性不言而喻,接下来重点分析。现在很多公司的支付都会使用支付宝、微信以及银联,支付过程分为发起和三方回调通知两大步骤。用户每买一件,就会做减库存操作,重复上述过程直至库存为零。

       库存的基本操作逻辑已经明确,那我们在什么时候减,怎么减,库存是否有时效性、如果用户退款怎么办,退款后库存怎么操作?一系列问题等待被解决。那么我们将不同的减库存操作一一列出讨论。

       1、用户下单减库存:首先判断用户是否具有购买资格(新用户、活动时间、活动地区、重复购买等检查),用户有了购买资格后检查是否有库存,无库存提示退出,有库存,用户便会占用这个库存资格,同时减库存。首先抢到的用户,占用一个库存资格,用户可以选择继续付款或放弃。正常情况下用户付款走完全流程,用户等待商家发货即可。可是也会有用户占有资格,却不付款或是付款后退款。假如有恶意刷库存的用户,他们通过一些手段抢占不付款或退款,那么会导致正常用户抢不到,影响用户体验,同时给公司带来不利影响,原本希望通过秒杀抢购回馈老用户或扩大影响力目的无法实现。

        2、支付回调后减库存:资格检查同上不再赘述。用户发起支付前有库存,那么用户可以正常购买支付,只要等待回调修改该库存(减库存)即可。可是从发起请求到真正减库存,中间会有时间差。那么当请求量很大时,会出现用户A即将准备减库存前,B用户请求进来发现还有库存,等B真正到支付回调减库存前发现库存早已经被抢光。此时B虽然付款却已经没有库存,导致订单失败,或者超卖。用户体验差,还会给公司带来影响。

        3、预扣库存:用户下单后,库存为其保留N分钟(N由具体业务定义)。等用户发起付款前检查库存库存有效性,有效则继续,若无效,则重复库存扣减逻辑。

      以上3个方案已经将扣减库存各个环境都试了一遍,看似第三个不会出现前两个的问题,可是观察发现恶意用户仍然可以等待N分钟刷单。三种方案好想都不够完美。那么我们继续分析,是否有一个最优解。我们总结上述方案的可能发生的问题反向思考,秒杀活动的本质是低价获取高价值商品,那么正常用户一般只要抢到即会去付款,所以我们只要通过一定的手段控制恶意刷库存即可。那么我们选择怎么选择方案呐?首先方案一,我们只需要保证减库存线程安全,同时结合防作弊手段(给用户打标签、风控、黑名单、重复下单不付款次数限制等)即可。方案二在并发量很大的时候是无法控制库存,极大程度会出现超卖现象,放弃。方案三同方案一相似仍需要反作弊手段,减库存则先预扣,付款前检查,相比方案一复杂一些。因此,相较于方案三我们选择了性能更优的方案一。

有了技术方案,那开始我们的开发工作,我们需要保证下单减库存的数据一致性。可采用的方案包括:

(1) 同步关键字Synchronized,获取减库存方法或代码块使用同步关键字,保证访问是串行

(2) 通过数据库的事务,事务中库存为负数,回滚退出

(3) 使用redis,redis做分布式锁,同时库存操作也在缓存中操作

上述三种方案我们对比发现,1和2的在并发量很大时因为存在锁竞争,会导致吞吐量减小和响应时间变长,同时事务因为使用数据库锁,可能会影响其他业务对表的读写。综合分析,我们选择延迟小,吞吐量高的方案3。

       此处,我们使用了高性能分布式缓存方案:redisson。相较于jedis,redisson的所提供的功能更加丰富,如可重入锁、连锁、红锁等,同时还有原子类AtomicLong、AtomicDouble,github地址:https://github.com/redisson/redisson,大家可以阅读使用。我们并没有直接使用lock。大家都知道原子类也是提供了线程安全的读写操作,我们完全可以使用该特性。以下为下单减存库的实现逻辑: 

... 初始化库存
... 前置检查

// 库存数
RAtomicLong cardStock = redissonClient.getAtomicLong("YOU_CUSTOMIZE_STOCK_KEY");
boolean isSuccess = true;
// 判断库存缓存是否存在且库存数>0
if (!cardStock.isExists() || cardStock.get() <= 0) {
    // 库存为空,提示抢购失败,进入其他逻辑
    isSuccess = false;
} else if (cardStock.decrementAndGet() < 0) {
    // 如果库存大于0,则进行-1操作,当有多个线程同时判断库存>0,那么在-1操作时则会排队等待,如果            
    // 首先抢到锁的线程-1后库存数为0,那之后的线程-1后均为负数,则直接进入其他处理逻辑
    isSuccess = false;
}

// 判断是否抢购成功
if (isSuccess) {
   // 抢购成功,建立用户和库存关系,在支付前再做一次判断,支付回调后将库存关系解除
   RBucket<Integer> userStock = redissonClient.getBucket("YOU_CUSTOMIZE_USER_STOCK_RELATION_KEY:" + uid);
    // 设置用户库存关系失效时间
   userStock.set(1, 60, TimeUnit.DAYS);

   // 订单抢购成功发短信通知
   threadPoolTaskExecutor.submit(() -> mqService.sendRpcMessageService(uid, MessageActionEnum.PROMOTION, cardServiceConfig.getJulyCampaignSms2(), Maps.newHashMap()));
}

... 后置处理

支付发起前的检查:

... 其他检查

// 再次检查在下单时建立的用户库存关系是否存在,此操作也能防止用户多段操作带来的风险
RBucket<Integer> userStock = redissonClient.getBucket("YOU_CUSTOMIZE_USER_STOCK_RELATION_KEY:" + userDO.getUid());
if (!userStock.isExists() || userStock.get() == 0) {
    // 没有或已经购买过,则进行友好提示
    Yi23ParamAssert.isTrue(false, "活动太过火爆,活动商品已被抢光~");
}

... 后置操作

 

支付回调后的解除操作:

// 支付回调检查用户库存关系缓存
RBucket<Integer> userStock = redissonClient.getBucket("YOU_CUSTOMIZE_USER_STOCK_RELATION_KEY:" + userDO.getUid());
// 有购买资格,消耗个人占有库存
if (userStock.isExists()) {
    // 将库存置为0
    userStock.set(0);
}

经过上述三个步骤,基本的库存操作逻辑开发完毕,当中还有其他很多小的细节再次没有赘述,需要根据各自的业务分析。

那么,我们采用的方案是不是已经很完美了?其实,仍然有许多的问题会随着并发量的进一步增大而显现,此时我们可能需要考虑更多的技术手段,,如:将秒杀热点数据隔离、熔断降级、设计兜底方案、流量削峰等等。篇幅有限,以后会将更过的技术方案写出,供大家参考。

        开发结束了,那么我们接下来需要去验证我们的技术方案是否可靠,也即是我们需要模拟大并发场景下,库存数据操作是否一致,其他逻辑是否如期按照我们的设计进行。此处,给大家推荐两个性能压测工具:jmeter、阿里的PTS。jmeter可以在本机测试,但是因为机型性能会有一些影响。PTS性能更好,但是需要一定的费用。各位可以根据自身情况选择。

jmeter : https://jmeter.apache.org/

PTS    : https://www.aliyun.com/product/pts

       经过这次的秒杀活动,收获颇丰。之前都只是停留在理论层面,真正到实践时发现有很多的细节需要考虑。并发这个话题的讨论从来都没停过,希望大家有机会多多总结实践。

文章中的有不少细节未体现,后期会不断完善,供大家启发参考。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值