请教大家一个问题,高并发的电商锁库存好呢?还是锁用户好呢?还是同时锁呢?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 你就过滤一大半去,剩下的人,再真正让他们抢呗,反正都是卖,卖谁都一样,看运气了,而且服务器和数据库压力也小