Redis秒杀相关点
一、秒杀的特点
-
瞬时并发量很大
qps可能达到几十万,上百万。一般数据库只能支持千级别的并发量,而redis单节点数万的并发量支持,所以可以使用redis来处理大部分请求,最终少量请求进入DB,
-
读多写少
秒杀是大量用户针对少量的库存进行抢,所以最终的下单并不会特别多,所以读操作远远多于写操作。
秒杀场景下,一般是先读取产品的详情,剩余库存等,查询条件一般是一个简单的产品ID,典型的key-value查询,redis支持高效的key-value查询。
二、库存同步问题
商品的库存全部放入redis,库存的读取直接读取redis,到了下单环节,库存的扣除也直接在redis扣除,然后通过消息队列通知后端数据库,最终把库存的扣减异步同步到后台数据库,避免了对数据库的瞬时压力。
2.1 key-value设置
1. key:商品id
2. value:字符串-{total : x, ordered : y}。total代表总库存,ordered代表已经扣减的库存。
2.2 库存原子操作
使用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
客户端可以通过lua脚本的返回值来判断是否秒杀成功,返回k成功,返回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
2.3 扣库存的一致性
上面的设计中,扣减库存在先操作redis中,redis操作成功后发送消息队列,最终通过消息队列实现异步同步数据库库存,实现了最终一致性。 但是有几个异常情况:
-
redis扣减成功,消息队列发送失败。
解决方案:
- 回滚redis的库存扣减操作。返回下单失败
- 另外也可以直接返回扣库存失败,不再操作redis,出现少卖情况。
-
redis扣减成功,消息也发送成功,但是最终由于网络等原因奔溃
对于这种情况可以让调用方发起还库存操作。还库存api内部可以通过唯一序列号进行关联(扣和还都传入一样的序列号),来判断是否要真正的还库存。
2.4 还库存
还库存先操作数据库,后同步redis,因为秒杀场景还库存的case比较少,直接操作数据库第一时间保证库存的准确性。
还库存的核心在于保证数据库的库存是准确的。如果后续有部分没有同步到redis,也是部分少买问题,再加上秒杀场景中还库存的case比较少,少买问题可以忽略。
扣库存是异步传输到db的,所以会出现这种情况:还库存的请求先于扣库存的到达,这时候需要将这个还库存的请求暂时存储,后续不停尝试,比如结果1小时还是没有还成功,可以进行预警人工干预。
2.5 库存监控
可以在秒杀结束后,针对实际订单量和库存的扣除量做一个对比监控,用于第一时间发现库存不一致的问题,尤其超卖的case。