一、可能出现的逻辑问题
1、一个 user 只能购买一张票, 即不能复购
2、不能出现超购,也是就多卖了
3、不能出现火车票遗留问题/库存遗留, 即火车票不能留下
二、初始化业务代码
新建原生的web项目模拟火车票抢购的场景
index.jsp
Redis中设置了键值:bj_cd:ticket=6
访问Tomcat启动后的web地址,进行抢票,查看后台信息
正常情况下是逻辑正常的,但是在高并发情况下(可以使用ab、jmeter压测工具模拟),就有可能出现超卖问题
Ubuntu安装ab工具:
如何使用ab工具?
ab -n 1000 -c 100 -p ~/postfile -T application/x-www-form-urlencoded http://192.168.198.1:8080/seckill/secKillServlet
(1) -n 1000 表示一共发出 1000 次 http 请求
(2) -c 100 表示并发时 100 次, 你可以理解 1000 次请求, 会在 10 次发送完毕
(3) -p ~/postfile 表示发送请求时, 携带的参数从当前目录的 postfile 文件读取 (事先要准备好)
(4) -T application/x-www-form-urlencoded 就是发送数据的编码是 基于表单的 url 编码
如图是使用ab工具模拟10s内1000次请求的结果,失败了967次,卖出去了33张票,但实际上初始值只有6张票
出现了超卖问题
三、连接池技术
在上述代码的核心方法doSecKill()中是每次请求都会通过Jedis新建一个连接,使用完进行close(),可以通过连接池进行代码优化,节省每次连接 redis 服务带来的消耗,把连接好的实例反复利用。
连接池参数:
- MaxTotal:控制一个 pool 可分配多少个 jedis 实例,通过 pool.getResource()来获取;如果赋值为-1,则表示不限制
- maxIdle:控制一个 pool 最多有多少个状态为 idle(空闲)的 jedis 实例
- MaxWaitMillis:表示当获取一个 jedis 实例时,最大的等待毫秒数,如果超过等待时间,则直接抛 JedisConnectionException
- testOnBorrow:获得一个 jedis 实例的时候是否检查连接可用性(ping());如果为 true,则得到的 jedis 实例均是可用的
修改SeckillWithJedis类中的代码,不再新建Jedis对象而是通过连接池获取
四、利用Redis事务机制解决超卖问题
控制超卖问题的关键代码
使用事务
重置Redis数据后,再次使用ab工具压测,没有再出现超卖:
五、库存遗留问题
例如:总共有600张票,但是在1000次高并发请求下,可能会出现请求结束后,还有剩余的票,这就是库存遗留问题
初始化Redis数据库票库存为600:set bj_cd:ticket 600
执行指令:
ab -n 1000 -c 100 -p ~/postfile -T application/x-www-form-urlencoded http://192.168.8.1:18080/seckill/secKillServlet
结果如下:
可以看到出现了库存遗留问题
LUA脚本解决问题
1、Lua 是一个小巧的脚本语言,Lua 脚本可以很容易的被 C/C++ 代码调用,也可以反过来调用 C/C++的函数,Lua 并没有提供强大的库,一个完整的 Lua 解释器不过 200k,所以 Lua 不适合作为开发独立应用程序的语言,而是作为嵌入式脚本语言。
2、很多应用程序、游戏使用 LUA 作为自己的嵌入式脚本语言,以此来实现可配置性、可扩展性。
3、将复杂的或者多步的 Redis 操作,写为一个脚本,一次提交给 redis 执行,减少反复连接 redis 的次数。提升性能。
4、LUA 脚本是类似 Redis 事务,有一定的原子性,不会被其他命令插队,可以完成一些 redis 事务性的操作
5、Redis 的 lua 脚本功能,只有在 Redis 2.6 以上的版本才可以使用
6、通过 lua 脚本解决争抢问题,实际上是 Redis 利用其单线程的特性,用任务队列的方式解决多任务并发问题
代码实现:
1、 编写lua脚本文件
2、编写加载脚本的程序
3、在Servlet中调用新的方法
4、进行测试
重置Redis中的数据为1000张票后再次执行ab指令,结果如下:
此时Redis数据库中票数为0,没有再出现库存遗留问题