Redis在秒杀场景下应用(九)

1,183 阅读5分钟

这是我参与11月更文挑战的第9天,活动详情查看:2021最后一次更文挑战

秒杀场景下,需要注意几个问题:

  • Redis在秒杀场景中需要缓存哪些数据?
  • 缓存的数据做更新时,如何保证原子性?

秒杀场景下不同阶段下的分析

在秒杀场景中,有着高并发,特点是读多写少。面对瞬间的读请求流量,单数据库层是顶不住压力的,从磁盘上拿数据比较慢,连接数很快就满了,这很快就可以把数据库给搞崩了。

秒杀场景的过程可以分为秒杀前、秒杀开始、秒杀后。在每一个阶段,Redis所发挥的作用也不一样。

秒杀活动前

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

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

秒杀活动开始

秒杀开始时,大量用户点击秒杀按钮,会产生大量的并发请求查询库存。一旦某个请求查询到有库存,系统就会进行库存扣减,然后系统会生成实际订单,并进行后续处理,例如订单支付、物流服务等。

这个阶段的操作共分为三个部分:库存查验库存扣减订单处理

因为只有当库存查验发现有库存后,才会进行库存扣减和订单处理的流程,所以最大的并发压力都在库存查验操作上。

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

库存扣减这个操作也需要在缓存中处理。如果将该操作放到数据库中执行,会带来两个问题:

  • 额外的开销。数据库层的数据更新后,还需要和Redis进行同步,带来了额外的操作逻辑;
  • 下单量超过实际库存量,出现超售。由于数据库的处理速度较慢,不能及时更新库存余量,有可能在更新完数据库层后再更新Redis缓存时,其它并发请求就会读取到旧的库存值,就会出现下单数量大于实际的库存量,导致出现超售。

所以需要在Redis中进行库存扣减。当库存查验完成后,一旦库存有余量,就立即在Redis中进行库存扣减。而且为了避免请求查询到旧的库存值,库存查验和库存扣减这两个操作需要保证原子性。

秒杀活动后

在秒杀活动后,请求会大大降低。可能会有用户尝试退单,但量不会很多了。这时候不需要使用Redis即可完成请求。

Redis 支撑秒杀场景的方法

在对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只是实现了后端对库存查验和库存扣减的支撑。而其它环节是需要别的支撑的。主要目的是为了减少大量请求进入后端系统,降低后端系统服务的压力,常见的手段有页面静态化(CDN)、请求拦截和流控、缓存校验和扣减库存、消息队列处理订单。

另外,为了不影响其他业务系统,秒杀系统最好和业务系统隔离,主要包括应用隔离、部署隔离、数据存储隔离。