参考
Github 仓库: Sunybyjava/seckill
Mooc 视频: Java 高并发秒杀
文档:
Spring Data Redis- Version 2.1.3.RELEASE
spring-boot-starter-data-redis 翻译官方文档 5.3 - 5.6
数据库设计
提取秒杀的核心,数据库只建了两张表:
1 | CREATE TABLE `product` ( |
1 | CREATE TABLE `user_buy_product` ( |
PS:上面两张表值得一提的点在于外键约束。有讨论认为数据库层不加约束,在 Service 层控制好些……
秒杀的难点主要在处理并发与并发时的优化。
技术栈
- 前端:Vue CLI3
- 后端:Spring Boot。
后端
核心:Spring Boot + MyBatis
版本控制:Gradle
插件:MyBatis-Generator(数据库表生成 Bean)
使用 spring boot + gradle + mybatisGenerator 实现代码自动生成
前端
核心:Vue CLI3
异步请求:axios
界面:Nes.css
开发思路
从交互开始分析,提取 api,再层层分解,一步一步实现。
按照上面的参考视频,从 dao 层到 service 层,再到 controller 层。这样自底向上的流程容易让人迷糊……每一步都不知道为了啥。
我觉得入门的话,应该从上到下。我缺少什么,然后再去做什么……这样理解起来会比较容易。
开发步骤
- 分析项目,设计数据结构、数据库、项目结构,考虑 数据交互、异常处理 等细节。
- 分析交互,拆分设计 api。
- 对每一个 api 进行实现。
- 优化。
讨论一下第二步:
比如,首先用户进入网页,肯定会需要一个获取秒杀商品信息的 api。
商品需要在秒杀时段内才能暴露秒杀按钮,同时总不可能让用户不断刷新网页吧,所以前端需要做倒计时,并且动态暴露秒杀按钮。
秒杀操作,实际上还是执行了一个 url (也是 api),这个秒杀 url 不能在倒计时的阶段暴露,只有到了秒杀阶段才能暴露,我们怎么获取它呢?
可以通过写一个获取秒杀 api 的 api。
此时需要考虑到对秒杀 api 加密。(甚至随时间变化)
用户开始秒杀,这里异常情况最多。
此时应该考虑到事务 , 存储过程 ……(SQL 怎么实现,框架怎么实现……)
同时这里也是并发点,后面进行优化也主要是围绕这一块。
api 列表
api | 参数 | 返回值 | 说明 |
---|---|---|---|
/api/seckill/list | 无 | 秒杀商品列表 | |
/api/seckill/exposer | skillId(商品 id) | 商品的秒杀 url | |
/api/seckill/execute/{seckillId}/{md5Code} | phone(手机号), seckillId(商品 id), md5Code(加密码) | 返回秒杀结果 | 手机号从 cookie 获得,后两者从 url 获得 |
/api/time/now | 获取服务器端时间 |
api 是根据前端来的。前端要啥,后端写啥……
RESTful API 参考资料
- restful-api-design-references
- 理解 RESTful 架构 - 阮一峰 简单了解什么是 RESTFul
- RESTful API 设计指南 - 阮一峰
- Restful API 的设计规范 实战经验的总结,具有较强的启发意义
翻了很多资料感觉 RESTful 还没有很好的实践……
优化
优化集中在两个点:获取商品信息、秒杀。
为什么在这些地方?主要是这里涉及到与数据库的交互。比如对获取商品信息来说,由于商品信息的变动比较小,就不必每次查询就建立数据库连接。
在视频教程里,分别采用:
- 使用 Redis 缓存商品信息。
- 对于秒杀操作,采用存储过程。
因为秒杀涉及到修改两个表,那么至少会执行两条 SQL。在之前,我们是在 Service 层来执行这两个操作,并且通过 Spring 的声明式事务来管理的。我们可以把这些过程封装为 SQL 层面的一个存储过程,然后只需要在 Service 层调用这个存储过程就行了。
这里优化的是事务行级锁持有的时间。Service 执行的操作,我们放到 SQL 层来做,在更底层执行,效率会高一点。
不过视频教程不建议在项目过多依赖存储过程。这里的逻辑很简单,可以一用。
在查到的资料里,查询、秒杀的操作几乎也都是通过 Redis 来优化的。
如果直接在缓存进行秒杀(直接在缓存进行减库存操作)等操作,就又涉及到缓存与数据库的同步……
其他
对 /api/seckill/list
使用了缓存。
其他同理……
Redis 安装与使用
注意
再不进行任何配置的情况下,使用的是默认配置。执行 redis-server
后,该程序会在前台执行。而我们实际对 redis 进行操作需要打开 redis-cli
,在这种情况下只能另开一个窗口打开 redis-cli
。
如果在当前窗口按 ctrl +z
或 ctrl + c
关闭,再执行 redis-cli
,是没有效果的。
如果已经整合到 Spring,进行数据操作时会报错NOAUTH Authentication required
。
另外:执行 redis-server
如果遇到某些错误信息,其实当前窗口已经给出了解决办法。
我们需要后台运行 Redis:
- 修改
/redis/redis.conf
中的daemonize no
为daemonize yes
- 进入
/redis/src
- 执行
./redis-server ../redis.conf
意思就是以自己的配置(后台执行)来运行redis-server
- OK
ps -ef|grep redis
检查是否后台启动.
Spring Boot 2.x 整合 Redis
配置文件 /config/RedisConfiguration.java
配置 application.yml
Redis 提供了常见的数据结构的存储,如 String、List……
在本项目中 Redis 使用 jackjson 把对象转换为 json,以 String 来储存。所以只用了 k/v 的方式。 json 也是序列化的一种嘛。
直接序列化和 json 怎么选型呢?
可以这样:只读取用 json,涉及到修改用普通序列化。
看场景……
前端
数据库存时间类型为 timestamp
前后端数据交换,解析时遇到一些问题。
倒计时的实现
先与服务器交互一次,以服务器的时间为准,然后靠着轮循倒计时。
update
[update-2019-05-23]
增加了 RabbitMQ 来异步处理秒杀逻辑。之前执行秒杀会阻塞直到返回秒杀结果,现在会直接返回「正在排队执行秒杀」的信息,不会阻塞。
不过这样的话,前端怎么获取是否是否秒杀成功呢?
可以通过轮询,判断秒杀表是否存在 用户-商品
的对应。
不过现在,秒杀有三种状态。秒杀成功、秒杀失败、秒杀排队中……
怎么做?首先根据秒杀表可以获取秒杀成功,秒杀尚未成功这两种状态。然后怎么判断秒杀失败和秒杀排队呢?
可以这样:查询商品表,如果还有商品,就返回正在排队;如果没有商品了,就返回秒杀失败。