如何解决高并发秒杀的超卖问题
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了如何解决高并发秒杀的超卖问题相关的知识,希望对你有一定的参考价值。
参考技术A由秒杀引发的一个问题
我们假设现在商品只剩下一件了,此时数据库中 num = 1;
但有100个线程同时读取到了这个消息 num = 1 ,所以100个线程都开始减库存了。
但你最终会发觉, 其实只有一个线程减库存成功,其他99个线程全部失败。
为何?
这就是MySQL中的排他锁起了作用。
排他锁又称为写锁,简称X锁,顾名思义,排他锁就是不能与其他所并存, 如一个事务获取了一个数据行的排他锁,其他事务就不能再获取该行的其他锁 ,包括共享锁和排他锁,但是获取排他锁的事务是可以对数据就行读取和修改。
就是类似于我在执行update操作的时候,这一行是一个事务 (默认加了排他锁 )。 这一行不能被任何其他线程修改和读写
这种方式采用了 版本号 的方式,其实也就是 CAS 的原理。
假设此时version = 100, num = 1; 100个线程进入到了这里,同时他们select出来版本号都是version = 100。
然后直接update的时候,只有其中一个先update了,同时更新了版本号。
那么其他99个在更新的时候,会发觉version并不等于上次select的version,就说明version被其他线程修改过了。那么我就放弃这次update
利用redis的单线程预减库存。比如商品有100件。那么我在redis存储一个k,v。例如
每一个用户线程进来,key值就减1,等减到0的时候,全部拒绝剩下的请求。
那么也就是说只有100个线程会进入到后续操作。所以一定不会出现超卖的现象
可见第二种CAS是失败重试,并无加锁。应该比第一种加锁效率要高很多。 类似于Java中的Synchronize和CAS 。
关于秒杀的系统架构优化思路
一、问题的提出
秒杀或抢购活动一般会经过 预约,下单,支付 ,扛不住的地方在于下单,一般会带来2个问题:
1、高并发
比较火热的秒杀在线人数都是10w起的,如此之高的在线人数对于网站架构从前到后都是一种考验。
2、超卖
任何商品都会有数量上限,如何避免成功下订单买到商品的人数不超过商品数量的上限,这是每个抢购活动都要面临的难题。
秒杀系统难做的原因:库存只有一份,瞬间大量用户读和写这些数据。
例如小米手机每周二的秒杀,可能手机只有1万部,但瞬时进入的流量可能是几百几千万
二、架构
常见站点架构如下
1)浏览器端,最上层,会执行到一些JS代码
2)站点层,这一层会访问后端数据,返回数据给浏览器
3)服务层,向上游屏蔽底层数据细节
4)数据层,最终的库存是存在这里的
三、优化思路
1、将请求尽量拦截在上游:传统秒杀系统之所以挂,请求都压倒了后端数据层,数据库读写锁冲突严重,导致响应慢,下单基本不能成功
2、利用缓存:这是一个典型的读多些少的应用场景,非常适合使用缓存
四、优化细节
1 、浏览器层请求拦截
a)产品层面,用户点击“查询”或者“购票”后,按钮置灰,禁止用户重复提交请求
b)js层面,限制用户在x秒之内只能提交一次请求
可以拦截很多无效请求
2、站点层请求拦截与页面缓存
防止像服务器直接发送过多的恶意http请求
a)同一个uid,限制访问频度,做页面缓存,x秒内到达站点层的请求,均返回同一页面
b)同一个item的查询,例如手机车次,做页面缓存,x秒内到达站点层的请求,均返回同一页面
可以拦截很多无效请求
3、服务层请求拦截与数据缓存
a)给过多的请求去数据库有什么意义呢?对于写请求,做请求队列,每次只透过有限的写请求去数据层,如果均成功再放下一批,如果库存不够则队列里的写请求全部返回“已售完
b)对于读请求,cache来抗,用memcached or redis(10wqps)
如此限流,只有非常少的写请求,和非常少的读缓存mis的请求会透到数据层去
4、数据层闲庭信步
到了数据这一层,几乎就没有什么请求了,库存是有限的,透过过多请求来数据库没有意义
五、解决方案
关于超卖,首先设定一个前提,为了防止超卖现象,所有减库存操作都需要进行一次减后检查,保证减完不能等于负数。(由于MySQL事务的特性,这种方法只能降低超卖的数量,但是不可能完全避免超卖)
update number set x=x-1 where (x -1 ) >= 0;
解决方案1:
将存库从MySQL前移到Redis中,所有的写操作放到内存中,由于Redis中不存在锁故不会出现互相等待,并且由于Redis的写性能和读性能都远高于MySQL,这就解决了高并发下的性能问题。然后通过队列等异步手段,将变化的数据异步写入到DB中。
优点:解决性能问题
缺点:没有解决超卖问题,同时由于异步写入DB,存在某一时刻DB和Redis中数据不一致的风险。
解决方案2:
引入队列,然后将所有写DB操作在单队列中排队,完全串行处理。当达到库存阀值的时候就不在消费队列,并关闭购买功能。这就解决了超卖问题。
优点:解决超卖问题,略微提升性能。
缺点:性能受限于队列处理机处理性能和DB的写入性能中最短的那个,另外多商品同时抢购的时候需要准备多条队列。
解决方案3:
将写操作前移到MC中,同时利用MC的轻量级的锁机制CAS来实现减库存操作。
优点:读写在内存中,操作性能快,引入轻量级锁之后可以保证同一时刻只有一个写入成功,解决减库存问题。
缺点:没有实测,基于CAS的特性不知道高并发下是否会出现大量更新失败?不过加锁之后肯定对并发性能会有影响。
解决方案4:
将提交操作变成两段式,先申请后确认。然后利用Redis的原子自增操作(相比较MySQL的自增来说没有空洞),同时利用Redis的事务特性来发号,保证拿到小于等于库存阀值的号的人都可以成功提交订单。然后数据异步更新到DB中。
优点:解决超卖问题,库存读写都在内存中,故同时解决性能问题。
缺点:由于异步写入DB,可能存在数据不一致。另可能存在少买,也就是如果拿到号的人不真正下订单,可能库存减为0,但是订单数并没有达到库存阀值。
总结
扩容+限流+内存缓存+排队
以上是关于如何解决高并发秒杀的超卖问题的主要内容,如果未能解决你的问题,请参考以下文章