前言
代码
练习了一下秒杀,网站搭建尽量从简,也算是比较轻车熟路了.
提取秒杀的核心,数据库只建了两张表:(直接从自动生成的脚本拷贝过来的…有些不必要项,大致还是能看明白.)
数据库
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='秒杀商品表';
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:上过数据库的课了,建表时还是该考虑考虑设计范式…上面两张表值得一提的点在于外键约束.
秒杀的难点主要在处理并发与并发时的优化.
参考
Github 仓库: Sunybyjava/seckill
Mooc 视频: Java 高并发秒杀
视频讲的很好,虽然项目很简单,但是视频从一定高度讲解了秒杀的场景,收获很大…
文档:
Spring Data Redis- Version 2.1.3.RELEASE
spring-boot-starter-data-redis 翻译官方文档 5.3 - 5.6
技术栈
前后端分离,前端用 vue-cli3 快速搭建,后端用 springboot.
使用 spring boot 与 mybatis 整合,有些繁琐的配置,我弄完后传到了下面的仓库,以后直接拿来用…
使用 spring boot + gradle + mybatisGenerator 实现代码自动生成
后端
核心: spring boot + mybatis
版本控制: gradle
插件: mybatis-generator 工具
前端
核心: vue-cli3
异步请求: axios
cookie 保存状态,操作 cookie 封装了一个类.
界面: Nes.css
思路
开发过程:从交互分析,提取 api,再层层分解,一步一步实现.
按照上面的参考,从 dao 层到 service 层,再到 controller 层,这样自底向上的流程容易让人迷糊…每一步都不知道为了啥.应该从上到下,我缺少什么,然后再去做什么…这样理解起来会比较容易.
见 开发步骤 .
开发步骤
- 搭建后端项目基本框架.放在 MyBatisGenerator-Tool .完成 mybatis 的相关繁琐配置.
- 分析项目,设计数据结构,数据库,项目结构.考虑 数据交互,异常处理 等细节.
- 分析交互,设计 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 设计
参考资料
Github 仓库 : restful-api-design-references
主要这三篇:
- 理解 RESTful 架构 - 阮一峰 简单了解什么是 RESTFul
- RESTful API 设计指南 - 阮一峰
- Restful API 的设计规范 实战经验的总结,具有较强的启发意义
执行情况
各种规范(即便是自己总结的)都自有道理,我在 Restful API 的设计规范 这篇文章看到 不要包装 的建议,所以选择在开发过程中统一利用注解 RestController
直接返回对象(框架帮我们把对象解析为 json 数据).
在我参考的仓库中,封装了一个 dto 的 ajax 交互类.
不包装的话,对异常怎么处理呢?
可以采用 ResponseEntity
.
@RequestMapping(value = "/exposer", method = RequestMethod.GET)
public ResponseEntity<?> exposer(Long skillId) {
return ResponseEntity.ok("ok");
// return ResponseEntity.ok(new Object());
// return ResponseEntity.status(422).body("bad");
}
如上,ResponseEntity 可以携带各种数据返回,而且可以设置返回的 http 的状态码.
也有人建议通过设置状态码来区分异常.
我采用的方式是:
http 的状态码始终返回 200.
自定义错误信息,返回自定义的状态码与错误信息
先建立:运行时异常类,维护错误信息的枚举类,异常信息交互类(ErrorResult.class).
先看这三个类.
异常:
public class SkillException extends RuntimeException {
private SkillStatusEnum statusEnum;
...
}
真正的错误信息被枚举类维护:
public enum SkillStatusEnum {
PRODUCT_NOT_EXIST(1000, "this is in not in products."),
SKILL_DATE_NOT_START(1001, "the skill is not started."),
SKILL_DATE_END(1002, "the skill is over."),
SKILL_URL_ERROR(2000, "秒杀地址被篡改。"),
SKILL_REPEAT(2001, "秒杀成功!无需再次秒杀。"),
PRODUCT_COUNT_OVER(2002, "库存不足。"),
SKILL_ERROR(3000, "其他异常");
private int code;
private String message;
SkillStatusEnum() {
}
SkillStatusEnum(int state, String info) {
this.code = state;
this.message = info;
}
public int getCode() {return code;}
public String getMessage() {return message;}
}
ErrorResult
简单地封装了 code,message 两条属性.
public class ErrorResult {
// 自定义错误码
private int code;
// 错误信息
private String message;
...
}
如果遇到了运行时异常,就把对应的错误信息(在枚举类中)传至异常类,然后抛出.
最后在 controller 层捕获,把异常信息取出,封装至交互类 ErrorResult.class,再通过 ResponseEntity
携带错误信息返回给前端.
回头看上面 controller 层的代码!!我使用了多态(ResponseEntity<?>
),这样可以保证我在正常情况下可以向前端传送某些对象,遇到异常时也可以返回 ErrorResult
的实例!
做成这个鬼样也不是我一开始想到的.
比如本来没打算做一个异常类,但是在做 事务 的时候,需要抛出 runtimeexcetpion,才会回滚,为了统一处理异常,就不如自己封装了一下.
PS:有更优雅的方式吗?
这样处理还不如创建个交互类直观…
前端就这样处理:
import axios from 'axios'
axios.get('/api/test')
.then(function (res) {
if (res.data.code != undefined) {
console.log(response.data)
} else {
// 由后端抛出的错误
alert(res.data.message)
}
}).catch(function (error) {
// 由网络或者服务器抛出的错误
alert(error.toString())
})
也挺好…
优化
忘了就看看视频,挺短的,也都是干货…
优化是重点.
在这里,优化集中在两个点,看 api 列表,一个是获取商品信息,一个是秒杀.
为什么在这些地方?主要是这里涉及到与数据库的交互,比如获取商品信息,商品信息的变动比较小,就不必每次查询就建立数据库连接.
在视频教程里,分别采用:
使用 redis 缓存商品信息.对于秒杀操作,采用存储过程.
对于前者,redis 的作用就不赘叙.对于后者,什么是存储过程也不多言了.
因为秒杀涉及到修改两个表,那么至少会执行两条 sql,在之前,我们是在 Service 层来执行这两个操作,并且通过 Spring 的声明式事务来管理的.我们可以把这些过程封装为 SQL 层面的一个存储过程,然后只需要在 Service 层调用这个存储过程就行了.
这里优化的是事务行级锁持有的时间.Service 执行的操作,我们放到 SQL 层来做,在更底层执行,效率会高一点.
不过视频教程不建议过多依赖存储过程.
这里的逻辑很简单,可以一用.
查资料,查询,秒杀的操作几乎也都是通过 redis 来优化的.
如果直接在缓存进行秒杀(直接在缓存进行减库存操作)等操作,就又涉及到缓存与数据库的同步…
其他
对 /api/seckill/list
使用了缓存.
其他同理…
其他优化
进一步的优化,就是分布式之类的了…接下来稍微学习一下.
redis 安装与使用
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.
命令不是死的,灵活变通.比如使用默认值执行了 make install
,会在 /usr/local/bin
生成安装目录,可全局执行 redis
相关命令…
ps -ef|grep redis
检查是否后台启动.
springboot2.x 整合 redis
配置文件 /config/RedisConfiguration.java
配置 application.yml
redis 是提供了常见的数据结构的存储你,如 String,List…
在本项目中,redis 使用 jackjson 把对象转换为 json,以 String 来储存,所以只用了 k/v 的方式. json 也是序列化的一种嘛.
redisTemplate.opsForValue()
直接序列化和 json 怎么选型呢?
可以这样:只读取用 json,涉及到修改用普通序列化.
看场景嘛…
前端
数据库存时间类型为 timestamp
前后端数据交换,解析时遇到一些问题.
倒计时的实现
先与服务器交互一次,以服务器的时间为准,然后靠着轮循倒计时…