经常见到的一个简单的秒杀下单场景:
商品详细页-->创建订单-->修改库存--->支付中心
其中最重要的就是修改库存的时候,防止超卖。今天就主要说一下防止超卖的实现方式。(当然一个秒杀系统涉及的东西非常多,后面会有一个国美在线的秒杀案例)
1:通过数据库的乐观锁方式
利用mysql的行锁特性:
update countTab set num = num - #{buy} where sku = #{sku} and num- #{buy} > 0
大型互联网公司,秒杀峰值很高,用这种方式会对数据库造成很大压力,所以一般不会采用此方式。
2:通过缓存(redis)的方式
先将库存设置到redis缓存中,每次通过redis的原子操作DECR进行库存的修改。
伪代码:
//将库存num设置到redis缓存中
init() {
jedis=jedisPool.getResource();
jedis.set(sku,num);
}
//修改库存
update(){
jedis=jedisPool.getResource();
int num = jedis.decrBy(sku,buy);
if(num >=0 ) {
//订单处理
};
}
3:总结
做一个真正的秒杀系统,涉及的东西不只是库存的超卖控制,还有以下几点需要考虑:
1)页面优化
防止按钮重复点击,cdn,动静分离
2)代理层
拦截流量,防止重复刷新
3)应用层
业务上核心代码cas,限流,缓存
4)数据库
读写分离
国美在线的一个秒杀设计流程:
1:一键秒杀引导
一键秒杀引导设置后无需在结算页面填写/修改收货地址,优惠,发票等元素,减少结算再次计算和调用操作。
2:验证码输入
该验证码为图形验证码,是用户在点击“一键秒杀”后进行的一次交互操作。因为无业务牵连,该验证码秒杀系统自动生成。
3:下单页面防止恶意请求
为了防止获取秒杀下单地址进行恶意请求,将秒杀商品的下单地址动态化,动态地址在商品秒杀开始时返回给页面。
4:秒杀商品库存预冻结
为了避免同步调用库存中心带来的预估之外的故障,秒杀活动创建生效时会将该商品的库存冻结,库存中心增加一种冻结类型。秒杀成功会直接使用该库存生成订单,订单中心增加一种秒杀订单类型。
5:库存释放
因为客户秒杀后可能不付款或者取消订单。订单中心将秒杀成功不支付或者取消订的订单通知秒杀系统进行本地系统和库存中心的库存释放,让客户可以再次抢购。
6:数据交互安全内存操作
为了提高执行效率,程序会提前将数据预加载到redis缓存中,比如库存计数器,用户违规操作(同ip时间内访问频率过高,同用户时间内访问频率高,黑名单等),redis表的键值设置会尽量遵从最短执行/扫描路径设计
7:内存操作的数据对象
库存计数器,服务器节点接受的请求计数器,用户参与秒杀记录(针对商品,活动,时间段)预生成订单,秒杀商品信息,访问控制数据。
8:数据的预加载到内存时机
库存计数器----秒杀活动创建,递减
服务器节点接受的请求计数器----递减
用户参与秒杀记录-----秒杀成功
预生成订单------秒杀活动创建后
9:秒杀库存计数器
通过redis的原子自增锁实现,关键字:DECR,INCR
10:请求的四次拦截
1)请求错流:验证码输入,将流量错开,减少并发;
2)恶意流量拦截:访问控制将恶意流量拦截;
3)服务节点拦截:每个服务节点允许下单的流量等于商品的库存数,其他请求提示秒杀已抢完。
4)持久层拦截:真正到达持久层的请求=服务器节点*库存数,如果到达持久层的请求大于库存数,其他请求提示秒杀已完毕。
11:秒杀页面独立
增加秒杀详情页面,该页面只调用秒杀系统,减小营销接口的压力
12:秒杀成功后的点单持久化操作
因为4次请求拦截,真正到达持久层的请求=秒杀商品数量*秒杀商品库存数,所以目前的实际是整个秒杀流程从前端提交到持久化为一步操作,如果后续持久化的量非常高再考虑消息队列的引入。