一.秒杀场景的负载特征对支撑系统的要求
- 秒杀活动售卖的商品通常非常优惠,会吸引大量用户进行抢购。
- 商家库存量远小于购买商品的用户数,会限定用户只能在一定时间段内购买,秒杀系统带来明显的负载特性,对支持系统提出要求。
二.瞬时并发访问量非常高
- 一般数据库每秒只能支持千级别的并发请求,而Redis的并发处理能力(每秒处理请求数据达到十万级)
- 大量并发请求涌入秒杀系统,我们需要使用Redis拦截大部分请求,避免大量请求发送至数据库,把数据库压垮。
三.读多写少,而且读操作是简单的查询操作
秒杀场景:
- 用户需要先查询商品是否还有库存(根据商品ID查询该商品的库存还有多少)。
- 只有库存还有余量,秒杀系统才能进行扣存和下单操作。
- 库存查询操作是典型的键值对查询,而Redis对键值对查询的高效支持,正好和这个操作要求匹配。
- 秒杀操作只有少部分用户才能成功下单,所以,商品库存查询操作(读操作)要远多于库存扣减和下单操作(写操作)。
四.Redis可以在哪些秒杀场景的环节发挥作用
- 秒杀活动前
- 用户会不断刷新商品详情页,会导致详情页的瞬时请求剧增
- 将商品详情页的页面元素静态化,然后使用CDN或者浏览器将这些静态化元素缓存起来。
- 大量请求直接由CDN或者浏览器缓存服务,不会到达服务端减轻服务端压力。
CDN和浏览器缓存服务请求,不需要使用Redis
2.秒杀活动开始
- 大量用户点击商品详情页上的秒杀按钮,会产生大量的并发请求查询库存
- 一旦请求查询到库存,系统会自动库存扣减,然后系统生成实际订单,并进行后续处理。比如订单支付和物流服务
- 用户通常继续点击秒杀按钮,继续查询库存。
秒杀活动分析:
- 库存查验,库存扣减和订单处理。
- 这个压力是在库存查验操作上。
- 订单处理可以在数据库中执行,但库存扣减操作不能交给后端数据库处理。
数据库中处理订单的原理:
- 订单处理设计支付,商品出库,物流等多个关联操作,这个操作本身设计数据库多张表操作,需要保证处理的事务性,需要在数据库中完成。
- 订单数据量比较小,数据可以支撑订单处理请求。
库存扣减操作不能在数据库中执行:
- 额外的开销 数据库和Redis同时维护
- 下单量超过实际库存量,产生超卖 数据库的处理速度慢,不能及时更新库存余量,导致线程问题
我们需要直接在Redis中进行库存扣减。
具体操作:
- 当库存查验完成后,一旦库存由余量,我们需要在Redis中扣减库存
- 避免请求查询到旧库存值,库存查验和库存扣减这两个操作需要原子性
秒杀活动结束
五.Redis哪些方法可以支持秒杀场景:
支持高并发
- Redis本身高速处理请求特性就可以支持高并发。
- 如果有个秒杀商品,我们也可以使用切片集群,不同实例保存不同商品库存,避免使用单个实例导致所有秒杀请求都集中在一个实例上的问题。
- 当使用切片集群,我们要先用CRC算法计算不同秒杀商品key对应的slot
- 我们在分配slot和实例对应关系,才能不同秒杀商品对应的slot分配到不同实例上保存。
保证库存查验和库存扣减原子性执行
Redis 的原子操作或是分布式锁这两个功能特性来支撑了。
六.基于原子操作支撑秒杀场景
使用:
- 一个商品库存对应两个信息:总库存量和已秒杀量
- key(商品ID)对应两个属性(总库存量和已秒杀量)
- 我们使用HASH类型和键值对保存库存:
-
key:商品ID value:{total:N,ordered:M}
保证Redis的原子性:
-
因为库存查验和库存扣减是两个操作,我们无法使用一条命令来完成,我们需要使用lua脚本原子性的执行这两个操作。
#获取商品库存信息
local counts = redis.call("HMGET", KEYS[1], "total", "ordered");
#将总库存转换为数值
local total = tonumber(counts[1])
#将已被秒杀的库存转换为数值
local ordered = tonumber(counts[2])
#如果当前请求的库存量加上已被秒杀的库存量仍然小于总库存量,就可以更新库存
if ordered + k <= total then
#更新已秒杀的库存量
redis.call("HINCRBY",KEYS[1],"ordered",k)
return k;
end
return 0
七.基于分布式锁来支撑秒杀场景
使用分布式锁来支撑秒杀场景:让客户都安向Redis申请分布式锁,只有拿到锁的客户端才能执行库存查验和库存扣减。
//使用商品IO作为key
key=itemID
//使用客户端唯一标识作为value
value=clientUniqueID
//申请分布式锁,timeout是超时时间
lock=acqurieLock(key,val,Timeout)
//拿到锁后,才能进行库存查验和扣减
if(lock==true){
//库存查验和扣减
availStock=DECR(key,k)
//库存扣减完,释放锁,返回秒杀失败
if(availStock <0){
releaseLock(key,val)
return error;
}
//库存扣减成功,释放锁
releaseLock(key, val)
}esle{
//没有拿到锁,直接返回
return ;
}
- 我们可以使用切片集群中的不同实例来分别保存分布式锁的商品库存信息
- 使用这种保存方式后,秒杀请求会首先访问保存分布式锁的实例
- 如果客户端没有拿到锁,这些客户端不会查询商品库存,可以减轻库存信息的实例压力。
八.秒杀系统除了Redis之外的4个环节:
- 前端静态页面的设计。秒杀页面上能静态化处理的页面元素,我们都要尽量静态化,这样可以充分利用 CDN 或浏览器缓存服务秒杀开始前的请求。
- 请求拦截和流控。在秒杀系统的接入层,对恶意请求进行拦截,避免对系统的恶意攻击,例如使用黑名单禁止恶意 IP 进行访问。如果 Redis 实例的访问压力过大,为了避免实例崩溃,我们也需要在接入层进行限流,控制进入秒杀系统的请求数量。
- 库存信息过期时间处理。Redis 中保存的库存信息其实是数据库的缓存,为了避免缓存击穿问题,我们不要给库存信息设置过期时间。
- 数据库订单异常处理。如果数据库没能成功处理订单,可以增加订单重试功能,保证订单最终能被成功处理。
九.使用多个实例的切片集群来分担秒杀请求,是否是一个好方法?
假设一个商品的库存量是 800,我们使用一个包含了 4 个实例的切片集群来服务秒杀请求。我们让每个实例各自维护库存量 200,然后,客户端的秒杀请求可以分发到不同的实例上进行处理,你觉得这是一个好方法吗?
Redis的 800个库存,分布到4个server上,打到哪个server上,取决于key,如果key分布不均匀,会导致 一定的不公平,就像高考一样,有的地方考生多,有的地方考生少,虽然在每个省中录取名额一样,但是也不公平