秒杀,这个源于淘宝的名称,现在已经为大家所熟知,本质是用户对于稀缺资源的竞购,整个过程以秒级为单位的时间内瞬时完成。而在分布式环境下,这个简单的名词背后对技术的要求却是极高的,架构师需要从整体平衡角度出发,做好设计才能从容应对。
在对稀缺资源的竞购过程中,应充分考虑从打开页面、不断刷新、下单、支付、扣库存、发货这整个过程进行优化提升,包括静态资源的CDN缓存,AJAX异步数据加载,数据缓存,IP限流,库存控制优化等等。
大致的部署结构如下:
其他关于高并发的优化文章很多,这里不再赘述,本文着重介绍WeX云金融系统的库存控制的演化历程,大致经历了三个阶段。
一、简单数据库锁
在业务量起步阶段,并发并不高,普通的数据库行级锁就可以完成库存的控制。
分为两种方式,伪代码作一下示例:
方式1:
select for update;
if(stock - delta >= 0){
update set stock = stock - delta;
insert order;
}
方式2:
result = update set stock = stock – delta where stock – delta >= 0;
if(result >= 0){
insert order;
}
随着业务量的增长以及品牌的推广,优质商品很快成为稀缺资源,秒杀逐渐上演。用户经常出现购买等待超过十几秒,甚至几十秒的情况,用户体验急剧下降。确认瓶颈就在数据库行级锁上,为了让业务尽快恢复正常,决定采用临时方案,即利用Memcache的Incr原子性操作将性能快速提高。
二、Memcache自增数+数据库锁
思路是减少行级锁竞争,在前期库存足够时候通过memcache完成计数,直接完成订单记录不做库存消耗;当计数结果大于库存后,则清空缓存,再统计已发生订单总消耗量一次性更新库存。
伪代码如下:
// 计数缓存不为空情况
if(memcacheClient.get(key) != null){
result = memcacheClient.incr(key, delta);
if(result >= stock){
// 1、锁定库存
select for update;
// 2、统计已发生订单消耗
sum = select sum(delta) from order;
// 3、一次性更新库存
update set stock = stock - sum;
// 4、清空计数
memcacheClient.set(key, null);
}else{
insert order;
}
}
// 计数超过库存情况
else{
result = update set stock = stock - delta where stock - delta;
if(result > 0){
insert order;
}
}
经过此次改造,性能大幅提升,但细心的读者会发现这样的模式是存在安全隐患的,缓存和数据库就是跨网络的独立服务,本身就无法保证强一致性,缓存服务的网络抖动或者缓存服务宕机都可能出现超卖的情况。不过这样的临时改造代价较小,而且快速解决了眼前的问题,保证业务能顺畅,为后续做进一步评估和整体改造换来了时间。
三、基于数据库的库存分块
在对高并发、强一致性场景下,解决的思路最终会围绕着减少热点竞争来做根本的改变,而方式基本上还是两种:
1、队列模式,将瞬时的高峰转变为相对低频的流式竞争;
2、分散热点模式,将原有的一个区块切割为多个区块,分块竞争。
对于队列模式,往往要求延时响应,即用户完成付款后等待申请被队列消耗结果完成确认,而相比较分散热点模式可继续保持同步响应,并且在后续叠加队列模式也不冲突,所以最终选择分散热点的方案作为升级方案。
运作模式如下:
当所有的从库存消耗完毕后进入主库存消耗,整个过程关于每个库存消耗逻辑可重用、代码简洁、性能大幅提升。在此基础上可结合自身运营的特点进行分块切割策略控制,畅销商品分块数量可相对多点,但也不是越多越好,合并库存会带来额外的开销。
最后,业务运营也可将畅销商品在时间上作分散发布。