图解秒杀问题和数据一致性问题

库存超卖问题

临近秋招,最近在面试过程中,遇到面试官问我这个问题,结果没答上来,很不应该,故写一篇博客总结该问题。

在秒杀抢购过程中(一定是高并发的情况),例如,一件商品库存如果剩余一个,但是这个时候有两个用户来抢购,并且它们是并行存在的,这个时候如果我们不做处理,会导致,库存变为负数,导致库存超卖!

我们可以从下面这几个方面入手该问题:

  • 数据库层面
  • 分布式锁
  • 使用Redis预减库存 + 异步队列
  1. 数据库层面

    在数据库层面,我们可以在进行减库存的时候,进行一个判断,也就是设置一个条件库存 > 0,也就是说,只有在库存大于0的时候才能进行一个减一

    update 秒杀商品表 set 秒杀商品库存 = 秒杀商品库存 - 1 where 秒杀商品id = id and 秒杀商品库存 > 0
    

    其实这样的操作相当于一种乐观锁问题。。。。

    这样做的话有一个弊端,我们这样做会导致,我们的性能比较差,因为我们要不停的去打数据库

    在数据库层面我们还有一个操作能防止库存超卖

    如果我们没有使用Redis我们可以使用Mysql的排他锁,我们可以在select语句之后加上for update这样加上排他锁,这样做的话,其他线程可以读取这个字段,但是不能给这个字段加锁,例如for updateupdatedelete这样的语句都会将其阻塞,直到拿到锁的线程释放锁

    同样的,这样做也会有弊端,就是Mysql的性能问题。

  2. 分布式锁

    关于分布式锁的内容,这里就不再详细阐述,我的其他文章已经讲解过了哈

    同一个锁key,再同一时间内,只有一个客户端能拿到该锁,这个原理类似于数据库层面的加锁问题,不过这个过程是在服务端完成的,只有拿到该锁的客户端才能真正执行下单操作。

    这个方案也有缺陷,缺陷是如果一个商品没办法同时完成大批量下单请求,因为这个锁在同一时间内只能有一个客户端能拿到。

  3. Redis预减库存 + 异步队列

redis+异步队列解决超卖问题.png

这里其实还隐藏了一个问题,我们将库存提前放入Redis当中,在Redis当中去维护这个库存,这样会出现一个数据不一致问题。

接下里我来讲述当缓存和数据库数据不一致的时候该怎么解决。

数据不一致问题

在上面我们进行秒杀的时候,我们将库存数量提前放入Redis中,进行Redis预减操作,而如果我们这样做,会导致缓存中的数据和数据库中的数据,数据不一致问题。

缓存确实可以提高我们的性能,毕竟Redis一秒钟能查8w多次,不像数据库那样。

对于缓存和数据库中数据不一致问题,首先我们想到的话,就是当他们数据不一致的时候,更新即可,但是具体要怎么更新呢?到底先更新哪个里面的数据呢?先更新缓存?还是先更新数据库?

我们先不考虑高并发情况,我们将更新数据分为:

  • 先更新数据库,再更新缓存

  • 先更新缓存,再更新数据库

很明显,在不考虑高并发的情况下,我们无论怎样做都可以保证数据一致性,不过需要考虑失败问题,也就是,在操作过程中,第一步失败,第二步失败这样子。

我们一个一个分析

我们假设第一步成功,第二步失败这样

  • 先更新缓存,再更新数据库

    如果缓存更新成功,而数据库更新失败,此时缓存中是最新的数据,而数据库中不是旧的数据,然后如果此时Redis当中最新的数据key失效了,此时又从数据库中拿到数据,这个数据是是老的数据,是错误的。

  • 先更新数据库,再更新缓存

    如果数据库更新成功,而缓存更新失败,此时数据库中的数据是最新的,而缓存中的数据是旧的数据,然后如果此时Redis当中的数据key失效了,此时从数据库中拿到数据,这个数据是最新的数据,是正确的,但是如果缓存的key没有失效呢?这个时候,将不会打到数据库,我们也不会拿到最新的数据,操作的仍然是旧数据。

    然后我们再来考虑并发问题

在并发情况下,这样操作很明显是不恰当的,如果我们使用到了缓存,明显是一个读多写少的场景,但是同样也可能是一个写多读少的环境,在这样的一个环境下,我们可以发现,我们频繁的更新缓存中的数据并没有任何用处,同样的这样的方案也可能将脏数据写回到缓存中

image.png

很明显最后缓存数据应该是count = 1,可是缓存中的数据确实count = 3,所以我们完全可以将缓存删除掉

这样做并不适用于写多读少的情况,并且也可能造成脏数据

同样的,我们先更新缓存,再更新数据库也会造成相同的问题。

既然我们更新缓存会出现问题,我们为什么不直接删除缓存呢?

与前面类似我们有两种方案,可以先删除缓存然后更新数据库,或者先更新数据库,再删除缓存

image.png

在串行的情况下,我们可以看到没有什么问题,但是在高并发情况下,就会出现问题了。

image.png 可以很明显的看到,这样是会出现问题的,其实这种状态是很少见的,因为查询肯定是比更新快很多的,所以这种状态只是一种假象态。

同样的,先删除缓存,再更新数据库也会出现问题。

image.png

然后就衍生出了一种解决方案,那就是延时双删,如下图

image.png

延时双删指的是,让写请求去删除两次缓存,从而达到一个刷脏数据的效果,不过这个方案也有一个弊端,那就是这个延时双删,该延时多久去删除这个数据?

这就需要我们去慎重考虑了,如果这个时间,设置的偏大的话,我们系统吞吐量会降低,设置的偏小的话,第二次删除会在写入缓存之前完成,无法达到刷脏的效果。

还有一种方案,那就是异步监听binlog + 删除缓存,如图

image.png 这种方案更为简单,但是对于系统来说是不太好的,因为这造成了代码的侵入,因为引入的消息队列,还要保证消息的可靠性等等一系列问题,这里binlog,简单提一嘴,binlog就是数据库的日志,这里就不再赘述如何保证消息队列消息的可靠性啦,其实我们可以思考一下,对于消息队列消息的可靠性,我们需要从三个方面来保证,也就是生产者,消息队列,消费者,好了本篇文章到此结束

猜你喜欢

转载自juejin.im/post/7124222818396405796