秒杀系统简单实现

参考

该项目源码

Github 仓库: Sunybyjava/seckill

Mooc 视频: Java 高并发秒杀

文档:

Spring Boot 2.x 中文文档

MyBatis 中文文档 3.4

MyBatis Generator 用户手册

Spring Data Redis- Version 2.1.3.RELEASE

spring-boot-starter-data-redis 翻译官方文档 5.3 - 5.6

数据库设计

提取秒杀的核心,数据库只建了两张表:

1
2
3
4
5
6
7
8
9
CREATE TABLE `product` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`name` varchar(100) NOT NULL,
`number` int(11) NOT NULL,
`start_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`end_time` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00',
`info` varchar(500) DEFAULT 'That`s good!',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8 COMMENT='秒杀商品表';
1
2
3
4
5
6
7
CREATE TABLE `user_buy_product` (
`phone` varchar(15) NOT NULL,
`id` bigint(20) NOT NULL,
PRIMARY KEY (`phone`,`id`),
KEY `fk_product` (`id`),
CONSTRAINT `fk_product` FOREIGN KEY (`id`) REFERENCES `product` (`id`) ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

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 层。这样自底向上的流程容易让人迷糊……每一步都不知道为了啥。

我觉得入门的话,应该从上到下。我缺少什么,然后再去做什么……这样理解起来会比较容易。

开发步骤

  1. 分析项目,设计数据结构、数据库、项目结构,考虑 数据交互异常处理 等细节。
  2. 分析交互,拆分设计 api。
  3. 对每一个 api 进行实现。
  4. 优化。

讨论一下第二步:

比如,首先用户进入网页,肯定会需要一个获取秒杀商品信息的 api。

商品需要在秒杀时段内才能暴露秒杀按钮,同时总不可能让用户不断刷新网页吧,所以前端需要做倒计时,并且动态暴露秒杀按钮。

秒杀操作,实际上还是执行了一个 url (也是 api),这个秒杀 url 不能在倒计时的阶段暴露,只有到了秒杀阶段才能暴露,我们怎么获取它呢?

可以通过写一个获取秒杀 api 的 api。

此时需要考虑到对秒杀 api 加密。(甚至随时间变化)

用户开始秒杀,这里异常情况最多。

此时应该考虑到事务存储过程 ……(SQL 怎么实现,框架怎么实现……)

同时这里也是并发点,后面进行优化也主要是围绕这一块。

api 列表

api参数返回值说明
/api/seckill/list秒杀商品列表
/api/seckill/exposerskillId(商品 id)商品的秒杀 url
/api/seckill/execute/{seckillId}/{md5Code}phone(手机号), seckillId(商品 id), md5Code(加密码)返回秒杀结果手机号从 cookie 获得,后两者从 url 获得
/api/time/now获取服务器端时间

api 是根据前端来的。前端要啥,后端写啥……

RESTful API 参考资料

  1. restful-api-design-references
  2. 理解 RESTful 架构 - 阮一峰 简单了解什么是 RESTFul
  3. RESTful API 设计指南 - 阮一峰
  4. Restful API 的设计规范 实战经验的总结,具有较强的启发意义

翻了很多资料感觉 RESTful 还没有很好的实践……

优化

优化集中在两个点:获取商品信息、秒杀。

为什么在这些地方?主要是这里涉及到与数据库的交互。比如对获取商品信息来说,由于商品信息的变动比较小,就不必每次查询就建立数据库连接。

在视频教程里,分别采用:

  • 使用 Redis 缓存商品信息。
  • 对于秒杀操作,采用存储过程。

因为秒杀涉及到修改两个表,那么至少会执行两条 SQL。在之前,我们是在 Service 层来执行这两个操作,并且通过 Spring 的声明式事务来管理的。我们可以把这些过程封装为 SQL 层面的一个存储过程,然后只需要在 Service 层调用这个存储过程就行了。

这里优化的是事务行级锁持有的时间。Service 执行的操作,我们放到 SQL 层来做,在更底层执行,效率会高一点。

不过视频教程不建议在项目过多依赖存储过程。这里的逻辑很简单,可以一用。

在查到的资料里,查询、秒杀的操作几乎也都是通过 Redis 来优化的。

如果直接在缓存进行秒杀(直接在缓存进行减库存操作)等操作,就又涉及到缓存与数据库的同步……

其他

/api/seckill/list 使用了缓存。

其他同理……

Redis 安装与使用

注意

再不进行任何配置的情况下,使用的是默认配置。执行 redis-server 后,该程序会在前台执行。而我们实际对 redis 进行操作需要打开 redis-cli,在这种情况下只能另开一个窗口打开 redis-cli

如果在当前窗口按 ctrl +zctrl + c 关闭,再执行 redis-cli,是没有效果的。

如果已经整合到 Spring,进行数据操作时会报错NOAUTH Authentication required

另外:执行 redis-server 如果遇到某些错误信息,其实当前窗口已经给出了解决办法。

我们需要后台运行 Redis:

  • 修改 /redis/redis.conf 中的 daemonize nodaemonize 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 来异步处理秒杀逻辑。之前执行秒杀会阻塞直到返回秒杀结果,现在会直接返回「正在排队执行秒杀」的信息,不会阻塞。

不过这样的话,前端怎么获取是否是否秒杀成功呢?

可以通过轮询,判断秒杀表是否存在 用户-商品 的对应。

不过现在,秒杀有三种状态。秒杀成功、秒杀失败、秒杀排队中……

怎么做?首先根据秒杀表可以获取秒杀成功,秒杀尚未成功这两种状态。然后怎么判断秒杀失败和秒杀排队呢?

可以这样:查询商品表,如果还有商品,就返回正在排队;如果没有商品了,就返回秒杀失败。