考虑
简单的秒杀案例需要考虑以下几个方面:
- 判断传入进来的用户 id 和商品 id 是否为 null
- 为空直接返回错误信息
- 连接 redis 服务
- 根据传入的
userId
和productId
来拼接存储在 redis 中的 key - 根据商品的 key 来获取商品的库存
- 如果库存为
null
,那么证明秒杀此时还没有开始 - 如果库存已经 小于等于0,那么商品秒杀失败
- 如果库存为
- 判断此用户是否成功参与过秒杀( 使用
set
集合来存储成功参与秒杀的用户 id )- 如果本次已经成功参与过秒杀,则秒杀失败
- 开始秒杀
- 商品库存 -1
- 秒杀用户 +1
并发问题
上述代码设计在普通的并发量的情境下,可能不会出现很大问题
然而,当并发量太高的时候,可能就会出现“超卖”,或者连接超时的现象
连接超时其实很好理解 下雨天的时候,当雨势过大的时候,下水道也无法
通过在 Linux 平台下使用 ab 工具执行并发测试时发现,会出现如下情况
在“已秒光”之后出现“秒杀成功”,这显然是不合理的
并且,redis 中的库存数量,此时也会出现负数的情况
解决问题
乐观锁和悲观锁
乐观锁:通过“先取数据后判断”的方式来保证读取数据的正确性
乐观锁很乐观,每次去拿数据的时候都认为别人不会进行修改,所以不会进行“上锁”,但是在更新的时候会判断一下数据的版本是否一致(比较自己当前的数据版本和数据库中的数据版本),只有当数据版本一致的时候,才会进行更新,否则不会进行更新。
乐观锁,适用于多读的场景。
悲观锁:通过对数据进行“上锁”的方式来保证读取数据的正确性
悲观锁很悲观,每次取数据的时候都认为别人会对这个数据进行修改。因此,在取数据的时候,都会对数据进行上锁,从而在别人想拿这个数据的时候就会被阻塞。
悲观锁实际是一种”先取锁再取数据”的保守策略。
悲观锁可以很好的保证数据的正确性,但是在效率方面开销很大。
另外,在只读型业务中是不会产生冲突的,也没必要使用到锁
redis中实现乐观锁
我们可以在redis中通过 watch 命令,来监视某个 key 的 value,一旦在事务执行过程中,被监视的 key 的 value 发生变化,那么整个事务都会失败(无论在这个事务中有没有操作被监视的 key )
解决超卖的问题
可以将最后更新库存
和更新秒杀用户
两步操作设置成一个事务,在此事务执行之前,对库存量进行监视。
一旦库存在事务的执行过程中被修改了,那么此次事务就不会执行,也就不会对 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 利用其单线程的特性,用任务队列的方式解决多任务并发问题。