Redis学习笔记36——Redis支撑秒杀场景的关键技术和实践都有哪些

Posted qq_34132502

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Redis学习笔记36——Redis支撑秒杀场景的关键技术和实践都有哪些相关的知识,希望对你有一定的参考价值。

秒杀场景包含了多个环节,可以分成秒杀前、秒杀中和秒杀后三个阶段,每个阶段的请求处理需求并不相同,Redis 并不能支撑秒杀场景的每一个环节。

秒杀场景的负载特征对支撑系统的要求

第一个特征是瞬时并发访问量非常高

一般数据库每秒只能支撑千级别的并发请求,而 Redis 的并发处理能力(每秒处理请求数)能达到万级别,甚至更高。所以,当有大量并发请求涌入秒杀系统时,我们就需要使用 Redis 先拦截大部分请求,避免大量请求直接发送给数据库,把数据库压垮

第二个特征是读多写少,而且读操作是简单的查询操作

在秒杀场景下,用户需要先查验商品是否还有库存(也就是根据商品 ID 查询该商品的库存还有多少),只有库存有余量时,秒杀系统才能进行库存扣减和下单操作。

库存查验操作是典型的键值对查询,而 Redis 对键值对查询的高效支持,正好和这个操作的要求相匹配。

不过,秒杀活动中只有少部分用户能成功下单,所以,商品库存查询操作(读操作)要远多于库存扣减和下单操作(写操作)。

Redis可以在秒杀场景的哪些环节发挥作用

秒杀前

在这个阶段,用户会不断刷新商品详情页,这会导致详情页的瞬时请求量剧增。这个阶段的应对方案,一般是尽量把商品详情页的页面元素静态化,然后使用 CDN 或是浏览器把这些静态化的元素缓存起来。这样一来,秒杀前的大量请求可以直接由 CDN 或是浏览器缓存服务,不会到达服务器端了,这就减轻了服务器端的压力。

在这个阶段,有 CDN 和浏览器缓存服务请求就足够了,我们还不需要使用 Redis。

秒杀中

此时,大量用户点击商品详情页上的秒杀按钮,会产生大量的并发请求查询库存。一旦某个请求查询到有库存,紧接着系统就会进行库存扣减。然后,系统会生成实际订单,并进行后续处理,例如订单支付和物流服务。如果请求查不到库存,就会返回。用户通常会继续点击秒杀按钮,继续查询库存。

简单来说,这个阶段的操作就是三个:库存查验、库存扣减和订单处理。因为之后查询到有库存余量之后,后续的操作才会执行,所以这个阶段最大的并发压力都在库存查验操作上。

为了支撑大量高并发的库存查验请求,我们需要在这个环节使用 Redis 保存库存量,这样一来,请求可以直接从 Redis 中读取库存并进行查验。

那么,库存扣减和订单处理是否都可以交给后端的数据库来执行呢?

订单处理可以,但库存扣减不行。

因为,订单处理会涉及支付、商品出库、物流等多个关联操作,这些操作本身涉及数据库中的多张数据表,要保证处理的事务性,需要在数据库中完成。而且,订单处理时的请求压力已经不大了,数据库可以支撑这些订单处理请求。

那,库存扣减为什么不行?

  1. 额外的开销。Redis 中保存了库存量,而库存量的最新值又是数据库在维护,所以数据库更新后,还需要和 Redis 进行同步,这个过程增加了额外的操作逻辑,也带来了额外的开销。
  2. 下单量超过实际库存量,出现超售。由于数据库的处理速度较慢,不能及时更新库存余量,这就会导致大量库存查验的请求读取到旧的库存值,并进行下单。此时,就会出现下单数量大于实际的库存量,导致出现超售。

最后就是秒杀活动结束后。这时用户请求量已经下降很多了,一般服务器都可以支撑。

Redis 的哪些方法可以支撑秒杀场景

秒杀场景对 Redis 操作的根本要求有两个。

支持高并发

这个很简单,Redis 本身高速处理请求的特性就可以支持高并发。而且,如果有多个秒杀商品,我们也可以使用切片集群,用不同的实例保存不同商品的库存,这样就避免,使用单个实例导致所有的秒杀请求都集中在一个实例上的问题了。不过,需要注意的是,当使用切片集群时,我们要先用 CRC 算法计算不同秒杀商品 key 对应的 Slot,然后,我们在分配 Slot 和实例对应关系时,才能把不同秒杀商品对应的 Slot 分配到不同实例上保存。

保证库存插眼和库存扣减的原子性执行

针对这条要求,我们就可以使用 Redis 的原子操作或是分布式锁这两个功能特性来支撑了。

基于原子操作支撑秒杀场景

原子操作可以是 Redis 自身提供的原子命令,也可以是 Lua 脚本。因为库存查验和库存扣减是两个操作,无法用一条命令来完成,所以,我们就需要使用 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 申请分布式锁,只有拿到锁的客户端才能执行库存查验和库存扣减。这样一来,大量的秒杀请求就会在争夺分布式锁时被过滤掉。而且,库存查验和扣减也不用使用原子操作了,因为多个并发客户端只有一个客户端能够拿到锁,已经保证了客户端并发访问的互斥性。

//使用商品ID作为key
key = itemID
//使用客户端唯一标识作为value
val = clientUniqueID
//申请分布式锁,Timeout是超时时间
lock =acquireLock(key, val, Timeout)
//当拿到锁后,才能进行库存查验和扣减
if(lock == True) 
   //库存查验和扣减
   availStock = DECR(key, k)
   //库存已经扣减完了,释放锁,返回秒杀失败
   if (availStock < 0) 
      releaseLock(key, val)
      return error
   
   //库存扣减成功,释放锁
   else
     releaseLock(key, val)
     //订单处理
   

//没有拿到锁,直接返回
else
   return

我们可以使用切片集群中的不同实例来分别保存分布式锁和商品库存信息。使用这种保存方式后,秒杀请求会首先访问保存分布式锁的实例。如果客户端没有拿到锁,这些客户端就不会查询商品库存,这就可以减轻保存库存信息的实例的压力了。

小结

对于秒杀场景来说,只用 Redis 是不够的。秒杀系统是一个系统性工程,Redis 实现了对库存查验和扣减这个环节的支撑,除此之外,还有 4 个环节需要我们处理好。

  1. 前端静态页面的设计。秒杀页面上能静态化处理的页面元素,我们都要尽量静态化,这样可以充分利用 CDN 或浏览器缓存服务秒杀开始前的请求。
  2. 请求拦截和流控。在秒杀系统的接入层,对恶意请求进行拦截,避免对系统的恶意攻击,例如使用黑名单禁止恶意 IP 进行访问。如果 Redis 实例的访问压力过大,为了避免实例崩溃,我们也需要在接入层进行限流,控制进入秒杀系统的请求数量。
  3. 库存信息过期时间处理。Redis 中保存的库存信息其实是数据库的缓存,为了避免缓存击穿问题,我们不要给库存信息设置过期时间。
  4. 数据库订单异常处理。如果数据库没能成功处理订单,可以增加订单重试功能,保证订单最终能被成功处理。

以上是关于Redis学习笔记36——Redis支撑秒杀场景的关键技术和实践都有哪些的主要内容,如果未能解决你的问题,请参考以下文章

Redis学习笔记36——Redis支撑秒杀场景的关键技术和实践都有哪些

Redis学习笔记36——Redis支撑秒杀场景的关键技术和实践都有哪些

Day762.Redis秒杀场景 -Redis 核心技术与实战

Day762.Redis秒杀场景 -Redis 核心技术与实战

Redis个人笔记:Redis应用场景,Redis常见命令,Reids缓存击穿穿透,Redis分布式锁实现方案,秒杀设计思路,Redis消息队列,Reids持久化,Redis主从哨兵分片集群

SpringBoot + Redis搭建支撑10w 人的秒杀抢单系统!