1.笔记图
2.秒杀活动三阶段
- 秒杀活动前
- 用户会不断刷新商品详情页,这会导致详情页的瞬时请求量剧增
- 把商品详情页的页面元素静态化,然后使用
CDN
或是浏览器把这些静态化的元素缓存起来
- 秒杀活动开始
- 大量用户点击商品详情页上的秒杀按钮,会产生大量的并发请求查询库存
- 为了支撑大量高并发的库存查验请求,我们需要在这个环节使用
Redis
保存库存量 - 订单处理可以在数据库中执行,但库存扣减不能交给数据库处理
- 订单处理会涉及支付、商品出库、物流等多个关联操作,这些操作本身涉及数据库中的多张数据表,要保证处理的事务性,需要在数据库中完成
- 需要直接在
Redis
中进行库存扣减,一旦库存有余量,我们就立即在Redis
中扣减库存
- 杀活动结束后
- 可能还会有部分用户刷新商品详情页,尝试等待有其他用户退单
- 已经成功下单的用户会刷新订单详情,跟踪订单的进展
- 这个阶段中的用户请求量已经下降很多了,服务器端一般都能支撑
3.秒杀对Redis的两个要求
- 支持高并发
Redis
本身高速处理请求的特性就可以支持高并发- 如有多个秒杀商品,可以使用切片集群,不同的实例保存不同商品的库存,避免所有的秒杀请求都集中在一个实例上
- 保证库存查验和库存扣减原子性执行:使用
Redis
的原子操作或是分布式锁这两个功能特性
4.高并发下Redis如何库存信息的正确
- 基于原子操作支撑秒杀场景
- 对应两个信息,分别是总库存量和已秒杀量,可以使用一个
Hash
类型的键值对来保存库存的这两个信息
key: itemID value: {total: N, ordered: M}
Tips:
itemID
是商品的编号,total
是总库存量,ordered
是已秒杀量
- 使用 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
5.和高并发相关的处理
- 前端静态页面的设计:秒杀页面上能静态化处理的页面元素,我们都要尽量静态化,这样可以充分利用
CDN
或浏览器缓存服务秒杀开始前的请求 - 请求拦截和流控:在秒杀系统的接入层,对恶意请求进行拦截,避免对系统的恶意攻击,例如使用黑名单禁止恶意
IP
进行访问。如果Redis
实例的访问压力过大,为了避免实例崩溃,我们也需要在接入层进行限流,控制进入秒杀系统的请求数量 - 库存信息过期时间处理:
Redis
中保存的库存信息其实是数据库的缓存,为了避免缓存击穿问题,我们不要给库存信息设置过期时间 - 数据库订单异常处理:如果数据库没能成功处理订单,可以增加订单重试功能,保证订单最终能被成功处理
Tips:小建议:秒杀活动带来的请求流量巨大,我们需要把秒杀商品的库存信息用单独的实例保存,而不要和日常业务系统的数据保存在同一个实例上,这样可以避免干扰业务系统的正常运行