请教大家一个问题,高并发的电商锁库存好呢?还是锁用户好呢?还是同时锁呢?thx
不会的,redis 做队列,保存到数据库的时机也不是说更改一次库存就存一次,持久化到数据库可以设定成了每 6 小时或每天等等。不过这么做就要配置 redis 的持久化方案为 AOF 就不会出现可能的库存数据丢失的问题。
其实就是个秒拍系统
秒拍系统的核心理念就是
能不动数据库就不动数据库
那么如何在不动数据库的情况下,解决用户高并发的问题呢? 高并发下,要守住两个底线:
将秒拍抢购进行进一步的分析:
其实并发压力就在 1 上,2 无所谓,毕竟付款成功后回调大多数系统都有 retry 机制,问题不大。 至于解决并发压力最好的方式就是让这些变成串行通过,
最简单方式自然是 利用 redis 的原子性来进行操作
方案 1:硬来
redis 记录库存数量,待秒杀开始时,进来的请求进行下单操作,DECR 后的读书如果>=0,则建立订单,注意,这里一定要利用 decr 的原子性,在此判断之前,不要有任何数据库操作,否则数据库会爆
if redis.decr(’SKU_STOCK_GATE‘) >= 0 # 在此之前不要有数据库读写操作
create_order(current_user, sku) #这里最好是个异步, 否则也可能造成数据库抖动
end
这种最基础的方式有一个麻烦之处,如果有一个买家想一次购买 N 个商品,就比较头疼了,因为我们很可能遇到这样的情况,库存只有 1, 但是此时用户要买 2 个或者更多
如果需求是:如果是多个购买,则允许最小购买数,那就无所谓,系统可以将其转化为单位为 1 的订单,但是大部分是:如果数量不够,就建立订单失败。
因为包含了多个购买,所以就不能用 decr 而是要用 decrby 2 了 此时 SKU_STOCK_GATE 为 1,redis 操作完毕后,GATE 值为 -1 不满足 GATE 条件,不能购买了。 库存不变
但是此时就有个麻烦的地方:
STOCK_GATE 已经小于 0 了,但是真实的库存是 1
此时,如果再有人购买一个库存,也会提示失败了
也就是说,这种方案的弊病就是:
有可能出现 库存还有,但是无法进行购买了
其实我们可以发现,大于 1 的购买,STOCK_GATE 有几种可能
在 STOCK_GATE 操作之前,STOCK_GATE 就已经为负数。这种分俩情况,第一种情况是,前置操作是 单一购买,以及前置操作是大于 1 的购买,用递归的思想,后者一定开始于前置是单一购买或 0 购买,而没人买的话,不存在库存不足的问题,所以 这种问题简化成:前置是单一购买,且操作前 STOCK_GATE 已经为负,这种情况很好理解,就是没库存了,购买失败
STOCK_GATE 在操作前后,都为正,所以没啥说的,正常购买
STOCK_GATE 在操作钱为 正,操作后为负,这种是库存不够,购买失败,理应回退库存
但是这里就有个问题,因为要拿到 decr 之前的数据,所以要先 get 一下数据,超过 1 次操作 redis 的话, 整个操作就不保证原子性了,当并发高的时候,就会出大问题,例如:
账户 A 和账户 B 请求同时 一起进来
有可能出现这样的情况,假设 key 的值是 0,当 k <0 的时候,跳出判断,否则执行减 1, A 和 B 一起发生 GET 请求,A 先返回 GET 请求,ruby 执行下一句 让 redis 执行 decr - 1,但是正当 ruby 要通知 redis,但 redis 还没执行的时候 B 的 ruby 的 GET 命令先到达了 redis , 此时 redis 也返回 0 于是根据 ruby 的逻辑,B 也会执行 -1
解决这类问题,自然是锁
改进后的方案 2:lock
使锁来保证判断的时候只有单线程,不会出现数据脏读
begin
block_result = lock_manager.lock!("SKU_LOCK_KEY", 2000) do
pre_gate_value = redis.get('SKU_STOCK_GATE')
return false if pre_gate_value <= 0 # 库存没有了
after_gate_value = redis.decrby(SKU_STOCK_GATE, num
if after_gate_value >=0
return true #可以购买
else
redis.incrby(SKU_STOCK_GATE, num) # 将扣除的加回来
return false #
end
end
rescue Redlock::LockError
# 锁超时, 理论上来说这里只有redis 和最简单的判断操作, 很难超时, 最好接入人工检查
end
通过锁可以解决问题
但是这种方案有没有啥其他的问题呢?
比较麻烦,自己维护锁,万一锁超时,检查起来也很烦躁
于是到了我觉得最简单的方案 3,排队。
方案 3:排队
即当发生抢购的时候,利用 redis,在一定时间内,给每个参加购买的人一个号码即可,之后入队。
因为 push 本身靠的是 redis 来保持原子性,所以无所谓。
之后拿一个进程单独处理这个队列,不符合的就直接 next,因为是独立处理,所以压根不存在并发问题,也就没有锁的概念。
但是这种的缺点也很明显,即订单生成时间与用户无关,只与 进程处理时间有关,但是其实这个时候秒杀用户基本也都在线,就算建立的订单最后他没支付,重新将库存加上即可。
秒杀,不用每人都来算一遍的,请求刚进 Controller 你就过滤一大半去,剩下的人,再真正让他们抢呗,反正都是卖,卖谁都一样,看运气了,而且服务器和数据库压力也小