考虑

简单的秒杀案例需要考虑以下几个方面:

  1. 判断传入进来的用户 id 和商品 id 是否为 null
    1. 为空直接返回错误信息
  2. 连接 redis 服务
  3. 根据传入的 userIdproductId 来拼接存储在 redis 中的 key
  4. 根据商品的 key 来获取商品的库存
    1. 如果库存为 null,那么证明秒杀此时还没有开始
    2. 如果库存已经 小于等于0,那么商品秒杀失败
  5. 判断此用户是否成功参与过秒杀( 使用 set 集合来存储成功参与秒杀的用户 id )
    1. 如果本次已经成功参与过秒杀,则秒杀失败
  6. 开始秒杀
    1. 商品库存 -1
    2. 秒杀用户 +1

image.png

并发问题

上述代码设计在普通的并发量的情境下,可能不会出现很大问题
然而,当并发量太高的时候,可能就会出现“超卖”,或者连接超时的现象

连接超时其实很好理解 下雨天的时候,当雨势过大的时候,下水道也无法

通过在 Linux 平台下使用 ab 工具执行并发测试时发现,会出现如下情况
image.png

在“已秒光”之后出现“秒杀成功”,这显然是不合理的

并且,redis 中的库存数量,此时也会出现负数的情况
image.png

解决问题

可以使用乐观锁的方式来解决

乐观锁和悲观锁

乐观锁:通过“先取数据后判断”的方式来保证读取数据的正确性

乐观锁很乐观,每次去拿数据的时候都认为别人不会进行修改,所以不会进行“上锁”,但是在更新的时候会判断一下数据的版本是否一致(比较自己当前的数据版本和数据库中的数据版本),只有当数据版本一致的时候,才会进行更新,否则不会进行更新。
乐观锁,适用于多读的场景。

悲观锁:通过对数据进行“上锁”的方式来保证读取数据的正确性

悲观锁很悲观,每次取数据的时候都认为别人会对这个数据进行修改。因此,在取数据的时候,都会对数据进行上锁,从而在别人想拿这个数据的时候就会被阻塞。
悲观锁实际是一种”先取锁再取数据”的保守策略。
悲观锁可以很好的保证数据的正确性,但是在效率方面开销很大。
另外,在只读型业务中是不会产生冲突的,也没必要使用到锁

redis中实现乐观锁

我们可以在redis中通过 watch 命令,来监视某个 key 的 value,一旦在事务执行过程中,被监视的 key 的 value 发生变化,那么整个事务都会失败(无论在这个事务中有没有操作被监视的 key )
image.png

解决超卖的问题

可以将最后更新库存更新秒杀用户两步操作设置成一个事务,在此事务执行之前,对库存量进行监视。

一旦库存在事务的执行过程中被修改了,那么此次事务就不会执行,也就不会对 redis 里面的数据进行修改

解决redis连接超时的问题

可以使用 redis 连接池来解决此问题

库存遗留问题

上述采用乐观锁的方式确实可以帮助解决 “超卖”的问题,但是同样会引入一个新的问题,那就是“库存遗留”问题。
在某个人秒杀完并提交订单之后,如果有很多人读取了数据,那么他们都无法进行秒杀(根据乐观锁的性质,他们拿到的已经是旧数据了,自然无法进行秒杀)
这样一来,可能就会造成虽然有库存,但是请求量不足的问题
同时,redis 中不支持悲观锁
因此,我们可以采用 lua 脚本的方式来解决此问题

LUA 脚本在 Redis 中的优势

  • 将复杂的或者多步的 redis 操作,写为一个脚本,一次提交给 redis 执行,减少反复连接 redis 的次数,提升性能。
  • LUA 脚本是类似 redis 事务,有一定的原子性,不会被其他命令插队,可以完成一些 redis 事务性的操作。
    • 一段 lua 脚本在执行的时候,其他的请求是不会被执行的
  • 但是注意 redis 的 lua 脚本功能,只有在 Redis 2.6 以上的版本才可以使用。
  • 利用 lua 脚本淘汰用户,解决超卖问题,redis 2.6 版本以后,通过 lua 脚本解决争抢问题,实际上是 redis 利用其单线程的特性,用任务队列的方式解决多任务并发问题。