每秒100w秒杀架构 elastic-job 分布式调度 秒杀设计-业务设计、架构设计、表设计 暂时没用
第1章 100W请求秒杀架构体系-静态资源处理
目标1:秒杀设计-业务设计、架构设计、表设计
目标2:工程讲解
目标3:商品详情页开发
目标4:数据同步
Canal学习
目标5:分布式任务调度
elastic-job学习
目标6:静态页动态更新
1 秒杀设计
1.1 业务流程
电商项目中,秒杀属于技术挑战最大的业务。后台可以发布秒杀商品后或者将现有商品列入秒杀商品,
热点分析系统会对商品进行分析,对热点商品做特殊处理。商城会员可以在秒杀活动开始的时间内进行
抢购,抢购后可以在线进行支付,支付完成的订单由平台工作人员发货,超时未支付订单会自动取消。
当前秒杀系统中一共涉及到管理员后台、搜索系统、秒杀系统、抢单流程系统、热点数据发现系统,如
下图:

1.2 秒杀架构
B2B2C商城秒杀商品数据一般都是非常庞大,流量特别高,尤其是双十一等节日,所以设计秒杀系统,
既要考虑系统抗压能力,也要考虑系统数据存储和处理能力。秒杀系统虽然流量特别高,但往往高流量
抢购的商品为数不多,因此我们系统还需要对抢购热门的商品进行有效识别。
商品详情页的内容除了数量变更频率较高,其他数据基本很少发生变更,像这类变更频率低的数据,我
们可以考虑采用模板静态化技术处理。
秒杀系统需要考虑抗压能力,编程语言的选择也有不少讲究。项目发布如果采用Tomcat,单台Tomcat
抗压能力能调整到大约1000左右,占用资源较大。Nginx抗压能力轻飘的就能到5万,并且Nginx占用资
源极小,运行稳定。如果单纯采用Java研发秒杀系统,用Tomcat发布项目,在抗压能力上显然有些不
足,如果采用Lua脚本开发量大的功能,采用Nginx+Lua处理用户的请求,那么并发处理能力将大大提
升。
下面是当前秒杀系统的架构图:

1.3 数据库设计
1.3.1 秒杀商品数据库
数据库名字: seckill_goods
秒杀活动表: tb_activity
商品品牌表:
tb_brand
秒杀商品分类表: tb_category
秒杀时刻表: tb_seckill_time
CREATE TABLE `tb_activity` (
`id` varchar(60) NOT NULL,
`name` varchar(100) NOT NULL,
`status` int(2) NOT NULL DEFAULT '1' COMMENT '状态:1开启,2未开启',
`startdate` date DEFAULT NULL,
`begintime` datetime DEFAULT NULL COMMENT '开始时间,单位:时分秒',
`endtime` datetime DEFAULT NULL COMMENT '结束时间,单位:时分秒',
`total_time` float DEFAULT NULL,
`is_del` in
t(
1
)
D
EFAULT '1' COMMENT '删除:1未删除,2已删除',
PRIMARY KEY
(
`
i
d
`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `tb_brand` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '品牌id',
`name` varchar(100) NOT NULL COMMENT '品牌名称',
`image` varchar(1000) DEFAULT '' COMMENT '品牌图片地址',
`letter` char(1) DEFAULT '' COMMENT '品牌的首字母',
`seq` int(11) DEFAULT NULL COMMENT '排序',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=325468 DEFAULT CHARSET=utf8 COMMENT='品牌表';
CREATE TABLE `tb_category` (
`id` int(20) NOT NULL AUTO_INCREMENT COMMENT '分类ID',
`name` varchar(50) DEFAULT NULL COMMENT '分类名称',
`goods_num` int(11) DEFAULT '0' COMMENT '商品数量',
`is_show` char(1) DEFAULT NULL COMMENT '是否显示',
`is_menu` char(1) DEFAULT NULL COMMENT '是否导航',
`seq` int(11) DEFAULT NULL COMMENT '排序',
`parent_id` int(20) DEFAULT NULL COMMENT '上级ID',
`template_id` int(11) DEFAULT NULL COMMENT '模板ID',
PRIMARY KEY (`id`),
KEY `parent_id` (`parent_id`)
) ENGINE=InnoDB AUTO_INCREMENT=11177 DEFAULT CHARSET=utf8 COMMENT='商品类目';
秒杀商品表:
tb_sku
秒杀商品活动关联表: tb_sku_act
CREATE TABLE `tb_seckill_time` (
`id` int(10) NOT NULL AUTO_INCREMENT,
`name` varchar(20) NOT NULL COMMENT '秒杀分类名字,双十一秒杀,每日时段秒杀等',
`starttime` time NOT NULL COMMENT '开始时间',
`endtime` time DEFAULT NULL,
`total_time` float(11,2) DEFAULT NULL COMMENT '秒杀时长,按小时计算',
`status` int(2) DEFAULT '1' COMMENT '是否启用,1:启用,2:停用',
`sort` int(2) DEFAULT NULL,
PRIMARY KEY
(
`
id
`
)
) ENGINE=Inno
DB
A
UT
O
_INCREMENT=13 DEFAULT CHARSET=utf8;
CREATE TABLE `tb_sku` (
`id` varchar(60) NOT NULL COMMENT '商品id',
`name` varchar(200) NOT NULL COMMENT 'SKU名称',
`price` int(20) NOT NULL DEFAULT '1' COMMENT '价格(分)',
`seckill_price` int(20) DEFAULT NULL COMMENT '单位,分',
`num` int(10) DEFAULT '100' COMMENT '库存数量',
`alert_num` int(11) DEFAULT NULL COMMENT '库存预警数量',
`image` varchar(200) DEFAULT NULL COMMENT '商品图片',
`images` varchar(2000) DEFAULT NULL COMMENT '商品图片列表',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
`spu_id` varchar(60) DEFAULT NULL COMMENT 'SPUID',
`category1_id` int(10) DEFAULT NULL COMMENT '类目ID',
`category2_id` int(10) DEFAULT NULL,
`category3_id` int(10) DEFAULT NULL,
`category1_name` varchar(20) DEFAULT NULL,
`category2_name` varchar(20) DEFAULT NULL,
`category3_name` varchar(20) DEFAULT NULL COMMENT '类目名称',
`brand_id` int(11) DEFAULT NULL,
`brand_name` varchar(100) DEFAULT NULL COMMENT '品牌名称',
`spec` varchar(200) DEFAULT NULL COMMENT '规格',
`sale_num` int(11) DEFAULT '0' COMMENT '销量',
`comment_num` int(11) DEFAULT '0' COMMENT '评论数',
`seckill_end` datetime DEFAULT NULL COMMENT '秒杀结束时间',
`seckill_begin` datetime DEFAULT NULL COMMENT '秒杀开始时间',
`status` int(1) DEFAULT '1' COMMENT '商品状态 1普通商品,2参与秒杀',
`islock` int(1) DEFAULT '1' COMMENT '是否锁定,1:未锁定,2:锁定',
`seckill_num` int(11) DEFAULT NULL COMMENT '秒杀数量',
PRIMARY KEY (`id`),
KEY `cid` (`category1_id`),
KEY `status` (`status`),
KEY `updated` (`update_time`),
KEY `spu_id` (`spu_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='商品表';
1.3.2 秒杀订单数据库
秒杀订单表:
tb_order
1.3.3 管理员数据库
管理员表: tb_admin
CREATE TABLE `tb_sku_act` (
`sku_id` varchar(60) NOT NULL,
`activity_id` varchar(60) NOT NULL,
PRIMARY KEY (`sku_id`,`activity_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
CREATE TABLE `tb_order` (
`id` varchar(60) COLLATE utf8_bin NOT NULL COMMENT '订单id',
`total_num` int(11) DEFAULT NULL COMMENT '数量合计',
`pay_type` varchar(1) COLLATE utf8_bin DEFAULT NULL COMMENT '支付类型,1、在线支
付、0 货到付款',
`create_time` datetime DEFAULT NULL COMMENT '订单创建时间',
`update_time` datetime DEFAULT NULL COMMENT '订单更新时间',
`pay_time` datetime DEFAULT NULL COMMENT '付款时间',
`consign_time` datetime DEFAULT NULL COMMENT '发货时间',
`end_time` datetime DEFAULT NULL COMMENT '交易完成时间',
`close_time` datetime DEFAULT NULL COMMENT '交易关闭时间',
`receiver_contact` varchar(50) COLLATE utf8_bin DEFAULT NULL COMMENT '收货人',
`receiver_mobile` varchar(12) COLLATE utf8_bin DEFAULT NULL COMMENT '收货人手
机',
`receiver_address` varchar(200) COLLATE utf8_bin DEFAULT NULL COMMENT '收货人地
址',
`transaction_id` varchar(30) COLLATE utf8_bin DEFAULT NULL COMMENT '交易流水
号',
`order_status` char(1) COLLATE utf8_bin DEFAULT NULL COMMENT '订单状态,0:未完
成,1:已完成,2:已退货',
`pay_status` char(1) COLLATE utf8_bin DEFAULT NULL COMMENT '支付状态,0:未支付,
1:已支付,2:支付失败',
`consign_status` char(1) COLLATE utf8_bin DEFAULT NULL COMMENT '发货状态,0:未发
货,1:已发货,2:已收货',
`is_delete` char(1) COLLATE utf8_bin DEFAULT NULL COMMENT '是否删除',
`sku_id` varchar(60) COLLATE utf8_bin DEFAULT NULL,
`name` varchar(200) COLLATE utf8_bin DEFAULT NULL,
`price` int(11) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `create_time` (`create_time`),
KEY `status` (`order_status`),
KEY `payment_type` (`pay_type`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin;
1.3.4 用户数据库
用户表: tb_user
收件地址表: tb_address
2 项目介绍
2.1 技术栈介绍
CREATE TABLE `tb_admin` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`login_name` varchar(20) DEFAULT NULL COMMENT '用户名',
`password` varchar(60) DEFAULT NULL COMMENT '密码',
`status` char(1) DEFAULT NULL COMMENT '状态',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;
CREATE TABLE `tb_user` (
`username` varchar(50) NOT NULL COMMENT '用户名',
`password` varchar(100) NOT NULL COMMENT '密码,加密存储',
`phone` varchar(20) DEFAULT NULL COMMENT '注册手机号',
`email` varchar(50) DEFAULT NULL COMMENT '注册邮箱',
`created` datetime NOT NULL COMMENT '创建时间',
`updated` datetime NOT NULL COMMENT '修改时间',
`nick_name` varchar(50) DEFAULT NULL COMMENT '昵称',
`name` varchar(50) DEFAULT NULL COMMENT '真实姓名',
PRIMARY KEY (`username`),
UNIQUE KEY `username` (`username`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='用户表';
CREATE TABLE `tb_address` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(50) DEFAULT NULL COMMENT '用户名',
`provinceid` varchar(20) DEFAULT NULL COMMENT '省',
`cityid` varchar(20) DEFAULT NULL COMMENT '市',
`areaid` varchar(20) DEFAULT NULL COMMENT '县/区',
`phone` varchar(20) DEFAULT NULL COMMENT '电话',
`address` varchar(200) DEFAULT NULL COMMENT '详细地址',
`contact` varchar(50) DEFAULT NULL COMMENT '联系人',
`is_default` varchar(1) DEFAULT NULL COMMENT '是否是默认 1默认 0否',
`alias` varchar(50) DEFAULT NULL COMMENT '别名',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
2.2 项目结构

此次项目并不打算从0-1的去研发,我们打算遵循企业开发模式,从1-2的方式进行研发,所以这里将直
接导入已经搭建好的工程。看到
2.3 项目导入
项目导入后,结构如下:
由于我们注册中心使用的是Nacos,数据库用的是MySQL,数据搜索Elasticsearch,所以当前开发环境
配置的虚拟机已经直接安装好了这些软件,采用了docker的安装方式。
虚拟机:
IP:192.168.211.137
账号:root
密码:123456
MySQL:
端口:3306
账号:root
密码:123456
Elasticsearch:
TCP:9300
Http:9200
Elastic-head:
Http:9100
//..略
2.4 工程案例
由于秒杀系统回归了企业开发,采用了1-2的模式,我们例举一下增删改查在当前项目中的一个案例。
项目中相关服务地址采用了别名,需要将别名配置到 C:\Windows\System32\drivers\etc\hosts 文
件中:
我们找一个项目中的案例,梳理和解读一下项目的访问流程,流程图如下:
在代码中,我们讲解一下用户查询所有品牌的案例:
请求路径: /api/brand
调用:
① seckill-gateway
② seckill-service/seckill-goods/BrandController.findAll()
代码图解:
192.168.211.1
37 db-server
192.168.211.137 redis-server
192.168.2
1
1
.
1
3
7
z
k
-s
e
rv
e
r
192.168.2
1
1
.
1
3
7
ka
f
ka
-
s
e
rver
192.168.
2
1
1
.
1
3
7
na
c
o
s-
se
r
ver
192.168.
2
1
1
.
1
3
7
e
s
-
se
rv
e
r
192.168.211.137 canal-server
192.168.211.137 seata-server
192.168.211.137 druid-server
3 商品详情页
3.1 分析
秒杀活动中,热卖商品的详情页访问频率非常高,详情页的数据加载,我们可以采用直接从数据库查询
加载,但这种方式会给数据库带来极大的压力,甚至崩溃,这种方式我们并不推荐。
商品详情页主要有商品介绍、商品标题、商品图片、商品价格、商品数量等,大部分数据几乎不变,可
能只有数量会变,因此我们可以考虑把商品详情页做成静态页,每次访问只需要加载库存数量,这样就
可以大大降低数据库的压力。
我们这里将采用freemarker来实现商品详情页的静态化,关于freemarker的语法我们就不在这里讲解
了,大家可以自行去网上查阅相关API。
并发处理能力:
3.2 工程搭建
我们创建一个静态页生成工程,用于生成商品详情页。
pom.xml
1、降低了数据库查询频率
2、使用Nginx实现详情页访问效率远高于Tomcat
bootstrap.yml
<dependencies>
<!--依赖web-->
<dependency>
<groupId>com.seckill</groupId>
<artifactId>seckill-web</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
<!--api--
>
<dependen
cy>
<groupId>com.seckill</groupId>
<a
rt
i
f
ac
t
Id
>
s
e
c
k
i
ll
-g
o
o
d
s
-a
p
i
<
/
a
rtifactId>
<v
e
r
s
io
n>
0
.
0
.
1
-S
N
A
PS
HO
T
<
/
v
e
r
s
i
o
n>
</de
pendency>
<!--freemarker-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
</dependencies>
server:
port: 18087
spring:
application:
name: seckill-page
cloud:
nacos:
config:
file-extension: yaml
server-addr: nacos-server:8848
discovery:
#Nacos的注册地址
server-addr: nacos-server:8848
profiles:
active: dev #dev 开发环境 #test测试环境 #pro生产环境
main:
allow-bean-definition-overriding: true
#freemarker配置
freemarker:
cache: false #关闭模板缓存,方便测试
settings:
template_update_delay: 0 #检查模板更新延迟时间,设置为0表示立即检查,如果时间大于
0会有缓存不方便进行模板测试
template-loader-path: classpath:/templates
charset: UTF-8
check-template-location: true
suffix: .ftl
content-type: text/html
expose-request-attributes: true
expose-session-attributes: true
request-context-attribute: request
创建文件生成对象:
BaseProcess 介绍:
#超时配置
ribbon:
ReadTimeout: 300000
ConnectTimeout: 30000
#静态页位置
htmlPath: D:/page/html
public cla
ss BaseProcess {
@Aut
owired
private Configuration configuration;
/***
* 生成静态页
* @param dataMap
* dataMap.templateName: 模板名字
* dataMap.path: 生成文件存储路径
* dataMap.name: 生成的文件名字
* @throws Exception
*/
public void writerPage(Map<String,Object> dataMap) throws Exception {
//获取模板名字
String templateName = dataMap.get("templateName").toString();
//文件生存的路径
String path = dataMap.get("path").toString();
//文件路径如果不存在,则创建
File file = new File(path);
if(!file.exists()){
file.mkdirs();
}
//获取文件名字
String fileName = dataMap.get("name").toString();
//获取模板对象
Template template = configuration.getTemplate(templateName);
//模板处理,获取生成的文件字符串
String content =
FreeMarkerTemplateUtils.processTemplateIntoString(template, dataMap);
//生成文件
FileUtils.writeStringToFile(new File(path,fileName),content);
}
}
该类用于生成静态文件,调用 writerPage 方法即可实现,其中有3个变量,变量注释如下:
3.3 商品详情静态页生成
生成商品详情页,我们需要提供商品信息
(sku),并且将商品数据存储到数据模型Map中,然后在页面渲
染。
1)静态页生成
在 seckill-page 中创建 com.seckill.page.service.SkuPageService ,添加生成静态页方法,代码
如下:
在 seckill-page 中创建 com.seckill.page.service.impl.SkuPageServiceImpl ,添加生成静态页
方法,代码如下:
在 seckill-page 中创建 com.seckill.page.controller.SkuPageController ,添加生成静态页方
法,代码如下:
templateName:模板名字,例如item.ftl,模板放到templates目录下
path:生成文件的路径,例如D:/page/html
name:生成静态页文件的名字,例如:1.html
/***
* 生成静态页
*/
void itemPage(Map<String,Object> dataMap) throws Exception;
@Service
public class SkuPageServiceImpl extends BaseProcess implements SkuPageService {
/***
* 生成静态页
*/
@Override
public void itemPage(Map<String,Object> dataMap) throws Exception {
dataMap.put("username","王五");
//生成静态页
super.writerPage(dataMap);
}
}
@RestController
@RequestMapping(value = "/page")
public class SkuPageController {
@Autowired
private SkuPageService skuPageService;
@Value("${htmlPath}")
注意:其中bootstrap.yml中添加 htmlPath: D:/page/html 配置。
2)Feign调用
在 seckill-page-api 中创建 com.seckill.page.feign.SkuPageFeign 实现调用,代码如下:
3.4 静态页删除
当商品变成普通商品或者商品售罄的时候,需要删除详情页,因此还需要实现一个根据id删除详情页的
方法。
在 seckill-page 中创建 com.seckill.page.service.SkuPageService ,添加删除静态页方法,代码
如下:
private String htmlPath;
/****
* 生成商品详情静态页
*/
@PostMapping(value = "/html")
public Result html(@RequestBody Sku sku) throws Exception {
//数据模型
Map<String,Object> dataMap = new HashMap<String,Object>();
dataM
a
p
.
p
u
t
(
"
na
m
e
"
,
s
k
u
.g
et
I
d
(
)
+
"
.
h
t
m
l
"
)
;
/
/
生
成静
态
页的文件名字
dataM
a
p
.
p
u
t
(
"p
a
t
h
"
,h
t
m
l
Pa
t
h
)
;
/
/
文件
路
径
da
t
a
M
a
p
.
p
u
t
(
"
t
e
m
p
l
a
t
e
Na
m
e
"
,
"
i
t
e
m
.
f
t
l
"
)
;
/
/模
板
名
字
da
t
a
M
a
p
.
p
u
t
(
"
s
k
u
"
,
s
k
u)
;
/
/商
品
数
据
//生成静态页
skuPageService.itemPage(dataMap);
return new Result(true, StatusCode.OK,"生成成功!");
}
}
@FeignClient(value = "seckill-page")
public interface SkuPageFeign {
/****
* 生成商品详情静态页
*/
@PostMapping(value = "/page/html")
Result html(@RequestBody Sku sku) throws Exception;
}
/***
* 删除静态页
* @param id
* @param htmlPath
*/
void delItemPage(String id,String htmlPath);
在 seckill-page 中创建 com.seckill.page.service.impl.SkuPageServiceImpl ,添加删除静态页
方法,代码如下:
在 seckill-page 中创建 com.seckill.page.controller.SkuPageController ,添加删除静态页方
法,代码如下:
2)Feign调用
在 seckill-page-api 中创建 com.seckill.page.feign.SkuPageFeign 实现调用,代码如下:
4 Canal增量数据同步利器(自学)
4.1 Canal介绍(自学)
canal主要用途是基于 MySQL 数据库增量日志解析,并能提供增量数据订阅和消费,应用场景十分丰
富。
github地址:https://github.com/alibaba/canal
/***
* 删除静态页
* @param id
* @param htm
lPath
*/
@Override
public vo
id
d
e
l
It
e
m
Pa
g
e
(S
t
r
i
ng
id
,
S
t
ri
n
g
ht
m
l
Path) {
File f
i
l
e
=
ne
w
F
i
le
(
ht
m
l
P
at
h,
id
+
"
.h
t
ml
"
)
;
if(f
il
e
.e
x
i
st
s
()
)
{
f
i
le
.
de
l
et
e
(
)
;
}
}
/****
* 删除商品详情静态页
*/
@DeleteMapping(value = "/html/{id}")
public Result delHtml(@PathVariable(value = "id")String id) throws Exception {
//删除静态页
skuPageService.delItemPage(id,htmlPath);
return new Result(true, StatusCode.OK,"删除成功!");
}
/****
* 删除商品详情静态页
*/
@DeleteMapping(value = "/page/html/{id}")
Result delHtml(@PathVariable(value = "id")String id) throws Exception;
版本下载地址:https://github.com/alibaba/canal/releases
文档地址:https://github.com/alibaba/canal/wiki/Docker-QuickStart
Canal应用场景
1.电商场景下商品、用户实时更新同步到至Elasticsearch、solr等搜索引擎;
2.价格、库存发生变更实
时同步到redis;
3.数据库异地备份、数据同步;
4.代替使用轮询数据库方式来监控数据库变更,有效
改善轮询耗费数据库资源。
MySQL主从复制原理
1. MySQL master 将数据变更写入二进制日志( binary log , 其中记录叫做二进制日志事件 binary
log events ,可以通过 show binlog events 进行查看) 2. MySQL slave 将 master 的 binary log
events 拷贝到它的中继日志( relay log ) 3. MySQL slave 重放 relay log 中事件,将数据变更反
映它自己的数据
Canal工作原理
1.canal 模拟 MySQL slave 的交互协议,伪装自己为 MySQL slave ,向 MySQL master 发送dump
协议 2. MySQL master 收到 dump 请求,开始推送 binary log 给 slave (即 canal ) 3.canal 解析
binary log 对象(原始为 byte 流)
4.2 Canal安装(自学)
参考文档:https://github.com/alibaba/canal/wiki/QuickStart
4.2.1 MySQL Bin-log开启
1)MySQL开启bin-log
a.进入mysql容器
b.开启mysql的binlog
c.创建账号并授权
授权 canal 链接 MySQL 账号具有作为 MySQL slave 的权限, 如果已有账户可直接 grant:
docker exec -it -u root mysql /bin/bash
cd /etc/mysql/mysql.conf.d
在mysqld.cnf最下面添加如下配置
# 开启 binlog
log-bin=/var/lib/mysql/mysql-bin
# 选择 ROW 模式
binlog-format=ROW
# 配置 MySQL replaction 需要定义,不要和 canal 的 slaveId 重复
server-id=12345
create user canal@'%' IDENTIFIED by 'canal';
GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT,SUPER ON *.* TO 'canal'@'%';
FLUSH PRIVILEGES;
d.重启mysql
开启bin-log后,我们可以用
sql语句查看下:
效果如下:
4.2.2 Canal安装
1)拉取镜像
2)安装容器
a.安装canal-server容器
b.配置canal-server
修改 /home/admin/canal-server/conf/canal.properties ,将它的id属性修改成和mysql数据库中
server-id不同的值,如下图:
c.修改 /home/admin/canal-server/conf/example/instance.properties ,配置要监听的数据库服务
地址和监听数据变化的数据库以及表,修改如下:
docker restart mysql
show variables like '%log_bin%'
docker pull canal/canal-server:v1.1.1
docker run -p 11111:11111 --name canal -d docker.io/canal/canal-server
指定监听数据库表的配置如下
canal.instance.filter.regex :
重启canal:
4.3 Canal微服务
我们搭建一个微服务,用于读取canal监听到的变更日志,微服务名字叫 seckill-canal 。该项目我们
需要引入 canal-spring-boot-autoconfigure 包,并且需要实现 EntryHandler<T> 接口,该接口中有
3个方法,分别为 insert 、 update 、 delete ,这三个方法用于监听数据增删改变化。
参考地址:https://github.com/NormanGyllenhaal/canal-client
1)pom.xml
mysql 数据
解
析
关
注
的
表
,
P
e
rl
正
则
表
达
式
.
多个正则之间
以
逗
号
(
,)
分
隔
,
转
义
符
需
要
双
斜
杠(\\)
常见例子:
1. 所有表
:
.*
o
r
.*
\\
.
.
*
2. cana
l
sc
h
e
m
a
下
所
有
表
:
c
anal\\..*
3. canal下的以canal打头的表:canal\\.canal.*
4. canal schema下的一张表:canal.test1
5. 多个规则组合使用:canal\\..*,mysql.test1,mysql.test2 (逗号分隔)
注意:此过滤条件只针对row模式的数据有效(ps. mixed/statement因为不解析sql,所以无法准确提
取tableName进行过滤)
docker restart canal
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>seckill-service</artifactId>
<groupId>com.seckill</groupId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>seckill-canal</artifactId>
<dependencies>
<!--web-->
bootstrap.yml配置
<dependency>
<groupId>com.seckill</groupId>
<artifactId>seckill-web</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
<!--esAPI-->
<dependency>
<groupId>com.seckill</groupId>
<
a
rt
i
f
ac
t
Id
>
s
e
c
k
i
ll
-
s
e
a
r
c
h
-a
p
i<
/
artifactId>
<
v
e
r
s
io
n>
0
.
0
.
1
-S
N
A
PS
H
O
T
<
/
v
e
r
si
o
n>
</
dependency>
<
!
-
-g
o
o
d
sA
P
I-->
<d
e
p
e
n
d
en
cy
>
<groupId>com.seckill</groupId>
<artifactId>seckill-goods-api</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
<!--canal-->
<dependency>
<groupId>top.javatool</groupId>
<artifactId>canal-spring-boot-autoconfigure</artifactId>
<version>1.2.1-RELEASE</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<!-- 指定该Main Class为全局的唯一入口 -->
<mainClass>com.seckill.CanalApplication</mainClass>
<layout>ZIP</layout>
</configuration>
<executions>
<execution>
<goals>
<goal>repackage</goal><!--可以把依赖的包都打包到生成的
Jar包中-->
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
server:
port: 18088
2)创建 com.seckill.handler.SkuHandler 实现 EntryHandler 接口,代码如下:
spring:
application:
name: seckill-canal
cloud:
nacos:
config:
file-extension: yaml
server-addr: nacos-server:8848
discovery:
#Naco
s
的
注
册
地
址
serve
r-
a
dd
r
:
nacos-server:8848
#超时配置
ribbon:
ReadTi
meout: 3000000
#Canal配置
canal:
server: canal-server:11111
destination: example
#日志
logging:
level:
root: error
@Component
@CanalTable(value = "tb_sku")
public class SkuHandler implements EntryHandler<Sku> {
/***
* 增加数据
* @param sku
*/
@Override
public void insert(Sku sku) {
System.out.println("===========insert:"+sku);
}
/***
* 修改数据
* @param before
* @param after
*/
@Override
public void update(Sku before, Sku after) {
System.out.println("===========update-before:"+before);
System.out.println("===========update-after:"+after);
}
/***
* 删除数据
* @param sku
*/
@Override
3)创建启动类
程序启动后,修改 tb_sku 数据,可以看到控制会打印修改前后的数据:
4.4 静态页同步
只需要添加Feign包,注入SkuPageFeign,根据增删改不同的需求实现生成静态页或删除静态页。修改
SkuHandler ,代码如下:
public void delete(Sku sku) {
System.out.println("===========delete:"+sku);
}
}
@SpringBootAp
plication
public class CanalApplication {
public
static void main(String[] args) {
SpringApplication.run(CanalApplication.class,args);
}
}
@Component
@CanalTable(value = "tb_sku")
public class SkuHandler implements EntryHandler<Sku> {
@Autowired
private SkuInfoFeign skuInfoFeign;
@Autowired
private SkuPageFeign skuPageFeign;
/***
* 增加数据
* @param sku
*/
@SneakyThrows
@Override
public void insert(Sku sku) {
//将Sku转换成SkuInfo
SkuInfo skuInfo = JSON.parseObject( JSON.toJSONString(sku)
,SkuInfo.class);
//同步索引
skuInfoFeign.modify(1,skuInfo);
//生成静态页
skuPageFeign.html(sku);
}
/***
* 修改数据
* @pa
r
a
m
be
fo
r
e
* @pa
r
a
m
a
ft
e
r
*/
@Sne
akyThrows
@Override
public void update(Sku before, Sku after) {
int type=2;
//将Sku转换成SkuInfo
SkuInfo skuInfo = JSON.parseObject( JSON.toJSONString(after)
,SkuInfo.class);
if(skuInfo.getStatus()==1 || after.getSeckillNum()<=0){
//商品变成了普通商品,或者商品库存为0,则需要删除索引数据
type=3;
}
//同步索引
skuInfoFeign.modify(type,skuInfo);
if(type==3){
//删除静态页
skuPageFeign.delHtml(after.getId());
}else{
//生成静态页
skuPageFeign.html(after);
}
}
/***
* 删除数据
* @param sku
*/
@SneakyThrows
@Override
public void delete(Sku sku) {
//将Sku转换成SkuInfo
SkuInfo skuInfo = JSON.parseObject( JSON.toJSONString(sku)
,SkuInfo.class);
//同步索引
skuInfoFeign.modify(3,skuInfo);
//删除静态页
skuPageFeign.delHtml(sku.getId());
}
}
同时不要忘了添加feign包: @EnableFeignClients(basePackages =
{"com.seckill.search.feign","com.seckill.page.feign"})
测试的时候,在数据对数据进行增删改,静态页也会同步操作。
5 分布式任务调度
5.1 分布式任务调度介绍
很多时候,我们需要定时执行一些程序完成一些预定要完成的操作,如果手动处理,一旦任务量过大,
就非常麻烦,所以用定时任务去操作是个非常不错的选项。
现在的应用多数是分布式或者微服务,所以我们需要的是分布式任务调度,那么现在分布式任务调度流
行的主要有elastic-job、xxl-job、quartz等,我们这里做一个对比:
feature
quartz
elastic-job
xxl-job
antares
opencron
依赖
mysql
jdk1.7+,
zookeeper
3.4.6+
,maven3.0.4+
mysql ,jdk1.7+ ,
maven3.0+
jdk 1.7+ ,
redis ,
zookeeper
jdk1.7+ ,
Tomcat8.0+
HA
多节点部署,通
过竞
争
数
据库
锁
来保
证
只有
一
个
节点执行任务
通过
zookeeper的
注
册
与
发
现
,
可
以
动
态
的添
加
服务
器
。 支
持水
平
扩容
集群部署
集群部署
—
任务分
片
—
支持
支持
支持
—
文档完
善
完善
完善
完善
文档略少
文档略少
管理界
面
无
支持
支持
支持
支持
难易程
度
简单
简单
简单
一般
一般
公司
OpenSymphony
当当网
个人
个人
个人
高级功
能
—
弹性扩容,多
种作业模式,
失效转移,运
行状态收集,
多线程处理数
据,幂等性,
容错处理,
spring命名空
间支持
弹性扩容,分片
广播,故障转
移,Rolling实时
日志,GLUE(支
持在线编辑代
码,免发布)
,任
务进度监控,任
务依赖,数据加
密,邮件报警,
运行报表,国际
化
任务分
片,
失效
转移,弹
性扩容 ,
时间规则支
持quartz和
crontab ,
kill任务,
现场执行,
查询任务运
行状态
使用企
业
大众化产品,对
分布式调度要求
不高的公司大面
积使用
36氪,当当
网,国美,金
柚网,联想,
唯品会,亚
信,平安,猪
八戒
大众点评,运满
满,优信二手
车,拍拍贷
—
—
5.2 elastic-job讲解
官网:http://elasticjob.io/index_zh.html
5.2.1 静态任务案例
使用elastic-job
很容易,我们接下来学习下elastic-job的使用,这里的案例我们先实现静态任务案例,
静态任务案例也就是执行时间事先写好。
实现步骤:
1)在 seckill-goods 中引入依赖
2)配置elastic-job
在 bootstrap.yml 中配置 elastic-job ,如下:
讲解:
这里我们只展示了部分常用的参数,还有很多参数,但不一定常用,大家可以参考下面地址学习:
3)任务创建
1.引入依赖包
2.配置zookeeper节点以及任务名称命名空间
3.实现自定义任务,需要实现SimpleJob接口
<!-- ElasticJobAutoConfiguration自动配置类作用-->
<dependency>
<groupId>com.github.kuhn-he</groupId>
<artifactId>elastic-job-lite-spring-boot-starter</artifactId>
<version>2.1.5</version>
</dependency>
elaticjob:
zookeeper:
server-lists: zk-server:3181
namespace: updatetask
server-lists:zookeeper的地址
namespace:定时任务命名空间
创建 com.seckill.goods.task.statictask.ElasticjobTask ,代码如下:
讲解:
5.2.2 动态任务案例
参考地址:https://github.com/LuoLiangDSGA/spring-learning/tree/master/boot-elasticjob
动态任务案例主要是讲解程序在运行时,动态添加定时任务,这种场景应用非常广泛。使用elastic-job
实现动态添加定时任务的实现有点复杂,我们接下来实际操作一次。
步骤:
1)监听器创建
监听器采用AOP模式,类似前置通知和后置通知, doBeforeJobExecutedAtLastStarted 和
doAfterJobExecutedAtLastCompleted 分别会在任务执行前和执行后调用,我们创建一个监听器实
现任务调度前后拦截, com.seckill.goods.task.dynamic.ElasticJobListener :
@ElasticSimpleJob(
cron = "5/10 * * * * ?",
jobName = "updateTask",
shardingTotalCount = 1
)
@Component
public class
ElasticjobTask implements SimpleJob {
/***
* 执行
任
务
* @pa
r
am
shardingContext
*/
@Ove
rride
public void execute(ShardingContext shardingContext) {
System.out.println("-----------执行!");
}
}
cron:定时表达式
jobName:这里和bootstrap.yml中的namespace保持一致
shardingTotalCount:分片数量
1.配置初始化的zookeeper地址
2.配置的定时任务命名空间(不一定会使用)
3.注册初始化数据
4.监听器->任务执行前后监听(可有可无)
5.动态添加定时任务实现
6.自定义任务处理过程-实现SimpleJob
public class ElasticJobListener extends AbstractDistributeOnceElasticJobListener
{
2)注册中心配置
在bootstrap.yml中配置zk和namespace
创建配置类配置注册中心信息, com.seckill.goods.task.dynamic.ElasticJobConfig :
/****
* 构造函数
* @param startedTimeoutMilliseconds
* @param completedTimeoutMilliseconds
*/
public ElasticJobListener(long startedTimeoutMilliseconds, long
completedTimeoutMilliseconds) {
super(startedTimeoutMilliseconds, completedTimeoutMilliseconds);
}
/***
* 任务
初始化前要做的事情,类似前置通知
* @
param shardingContexts
*/
@Override
public void doBeforeJobExecutedAtLastStarted(ShardingContexts
shardingContexts) {
System.out.println("========doBeforeJobExecutedAtLastStarted========"+
TimeUtil.date2FormatHHmmss(new Date()));
}
/***
* 任务执行完成后要做的事情,类似后置通知
* @param shardingContexts
*/
@Override
public void doAfterJobExecutedAtLastCompleted(ShardingContexts
shardingContexts) {
System.out.println("=======doAfterJobExecutedAtLastCompleted============="+
TimeUtil.date2FormatHHmmss(new Date()));
}
}
#配置动态任务案例的zk和namespace
zk: zk-server:3181
namesp: autotask
@Configuration
public class ElasticJobConfig {
//配置文件中的zookeeper的ip和端口
@Value(value = "${zk}")
private String serverlists;
//指定一个命名空间
@Value("${namesp}")
3)任务构建
我们创建一个动态配置任务的类,任何逻辑代码需要创建定时任务,可以直接调用该类的指定方法即
可。创建类: com.seckill.goods.task.dynamic.ElasticJobHandler ,代码如下:
private String namespace;
/***
* 配置Zookeeper和namespace
* @return
*/
@Bean
public ZookeeperConfiguration zkConfig() {
return new ZookeeperConfiguration(serverlists, namespace);
}
/***
* 向z
ookeeper注册初始化信息
* @
p
a
ra
m
config
* @
r
et
u
r
n
*/
@Bean(initMethod = "init")
public ZookeeperRegistryCenter regCenter(ZookeeperConfiguration config) {
return new ZookeeperRegistryCenter(config);
}
/****
* 创建ElasticJob的监听器实例
* @return
*/
@Bean
public ElasticJobListener elasticJobListener() {
//初始化要给定超时多少秒重连
return new ElasticJobListener(100L,100L);
}
}
@Component
public class ElasticJobHandler {
@Resource
private ZookeeperRegistryCenter registryCenter;
@Resource
private ElasticJobListener elasticJobListener;
/**
* @param jobName:任务的命名空间
* @param jobClass:执行的定时任务对象
* @param shardingTotalCount:分片个数
* @param cron:定时周期表达式
* @param id:自定义参数
* @return
*/
private static LiteJobConfiguration.Builder simpleJobConfigBuilder(String
jobName,
Class<?
extends SimpleJob> jobClass,
int
shardingTotalCount,
String
cron,
String
id) {
//创建
任务构建对象
L
i
t
e
Jo
b
C
o
n
fi
g
u
r
at
i
o
n
.
Bu
i
l
d
e
r
b
ui
ld
e
r
=
LiteJobCo
n
f
ig
u
r
a
ti
o
n
.
ne
w
B
u
il
d
e
r
(
n
ew
Si
m
pl
e
Jo
b
C
onfiguration(
J
o
b
C
o
r
e
C
o
n
fi
gu
ra
t
io
n
.
/
/
任
务
命
名
空
间
名
字、任务执行周期表达式、分片个数
newBuilder(jobName, cron, shardingTotalCount).
//自定义参数
jobParameter(id).
build(),
jobClass.getCanonicalName()));
//本地配置是否可覆盖注册中心配置
builder.overwrite(true);
return builder;
}
/**
* 添加一个定时任务
* @param cron:周期执行表达式
* @param id:自定义参数
* @param jobName:命名空间
* @param instance:任务对象
*/
public void addPublishJob(String cron,String id,String jobName,SimpleJob
instance) {
LiteJobConfiguration jobConfig = simpleJobConfigBuilder(
jobName,
instance.getClass(),
1,
cron,
id).overwrite(true).build();
//DynamicTask为具体的任务执行逻辑类
new SpringJobScheduler(instance, registryCenter, jobConfig,
elasticJobListener).init();
}
/***
* Date转cron表达式
*/
public static final String CRON_DATE_FORMAT = "ss mm HH dd MM ? yyyy";
/**
* 获得定时
* @param date
* @return
*/
public static String getCron(final Date date) {
SimpleDateFormat simpleDateFormat = new
SimpleDateFormat(CRON_DATE_FORMAT);
4)执行逻辑
我们接着创建一个类,用于执行自己所需要操作的逻辑,
com.seckill.goo
ds.task.dynamic.DynamicTask ,代码如下:
5)调用测试
创建 com.seckill.goods.controller.TaskController 动态调用创建任务的方法,代码如下:
return simpleDateFormat.format(date);
}
}
public cla
ss DynamicTask implements SimpleJob {
@Ove
r
r
i
d
e
publ
i
c
v
o
id execute(ShardingContext shardingContext) {
//传递的参数
String id = shardingContext.getJobParameter();
try {
//具体任务逻辑
System.out.println("执行你的逻辑代码!param:"+id);
} catch (Exception e) {
e.printStackTrace();
}
}
}
@RestController
@RequestMapping(value = "/task")
public class TaskController {
@Autowired
ElasticJobHandler elasticJobHandler;
/***
* 动态创建任务
* @param times:延迟时间,为了测试到效果,所以在当前时间往后延迟
* @param jobname:任务名字
* @param param:自定义参数
* @return
*/
@GetMapping
public Result add(Long times,String jobname,String param){
//在当前指定时间内延迟times毫秒执行任务
Date date = new Date(System.currentTimeMillis()+times);
//需要传递给定时任务的参数
String cron = ElasticJobHandler.getCron(date);
//执行任务
elasticJobHandler.addPublishJob(cron,param,jobname,new DynamicTask());
return new Result(true, StatusCode.OK,"添加任务成功!");
6)测试
访问: http://localhost:18081/task?times=15000&jobname=asyncname¶m=No001
后台执行效果如下:
6 静态页动态更新
6.1 分析
索引和静态资源的更新功能已经完成,所有秒杀商品都只是参与一段时间活动,活动时间过了需要将秒
杀商品从索引中移除,同时删除静态页。我们需要有这么一个功能,在秒杀商品活动结束的时候,将静
态页删除、索引库数据删除。
此时我们可以使用elastic-job定时执行该操作,我们看如下活动表,活动表中有一个活动开始时间和活动
结束时间,我们可以在每次增加、修改的时候,动态创建一个定时任务,把活动结束时间作为任务执行
时间。
实现步骤:
6.2 更新操作
我们在上面动态案例上进行修改,当添加和修改活动时,执行一个定时任务,定时任务以活动结束的时
间为任务执行时间,将活动ID作为任务名字。
}
}
CREATE TABLE `tb_activity` (
`id` varchar(60) NOT NULL,
`name` varchar(100) NOT NULL,
`status` int(2) NOT NULL DEFAULT '1' COMMENT '状态:1开启,2未开启',
`startdate` date DEFAULT NULL,
`begintime` datetime DEFAULT NULL COMMENT '开始时间',
`endtime` datetime DEFAULT NULL COMMENT '结束时间',
`total_time` float DEFAULT NULL,
`is_del` int(1) DEFAULT '1' COMMENT '删除:1未删除,2已删除',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
1.编写动态定时任务
2.修改/增加秒杀活动,添加动态定时任务,执行时间为秒杀活动结束时间
1)创建定时任务对象
com.seckill.goods.task.dynamic.DynamicAsyncTask 代码如下:
2)调用操作
在活动修改和增加的时候,添加定时任务,修改
com.seckill.goods.service.impl.ActivityServiceImpl 的add和update方法,代码如下:
public class DynamicAsyncTask implements SimpleJob {
@Autowired
private S
kuActMapper skuActMapper;
@Autow
i
re
d
privat
e
S
kuMapper skuMapper;
@Ove
r
r
i
d
e
publ
i
c
v
o
id execute(ShardingContext shardingContext) {
//传递的活动ID
String id = shardingContext.getJobParameter();
try {
mldify(id);
} catch (Exception e) {
e.printStackTrace();
}
}
/*****
* 修改商品状态
* @param id
*/
public void mldify(String id){
//根据活动ID查询所有秒杀商品ID
SkuAct skuAct = new SkuAct();
skuAct.setActivityId(id);
List<SkuAct> skuActs = skuActMapper.select(skuAct);
//获取所有id
List<String> ids = new ArrayList<String>();
for (SkuAct act : skuActs) {
ids.add(act.getSkuId());
}
//活动结束,秒杀商品变更成普通商品
Sku sku = new Sku();
sku.setStatus("1");
Example example = new Example(Sku.class);
Example.Criteria criteria = example.createCriteria();
criteria.andIn("id",ids);
criteria.andEqualTo("status","2");
skuMapper.updateByExample(sku,example);
}
}
@Autowired
private ElasticJobHandler elasticJobHandler;
/**
* 增加Activity
* @param activity
*/
@Override
public void a
dd(Activity activity){
//选中的id集合
List<
Integer> seckillIds = activity.getSeckillIds();
//循环
添
加
活
动
到
数
据
库
中
for
(I
nt
e
ge
r
s
ec
ki
ll
Id : seckillIds) {
Activity oneActivity = new Activity();
BeanUtils.copyProperties(activity,oneActivity);
//查询当前活动对应的信息
SeckillTime seckillTime =
seckillTimeMapper.selectByPrimaryKey(seckillId);
oneActivity.setId("A"+idWorker.nextId());
oneActivity.setBegintime(seckillTime.getStarttime());
oneActivity.setEndtime(seckillTime.getEndtime());
float times = TimeUtil.dif2hour(oneActivity.getBegintime(),
oneActivity.getEndtime());
oneActivity.setTotalTime(times);
//添加
activityMapper.insertSelective(oneActivity);
//添加定时任务
elasticJobHandler.addPublishJob(ElasticJobHandler.getCron(oneActivity.getEndtim
e()),oneActivity.getId(),oneActivity.getId(), new DynamicAsyncTask());
}
}
浙公网安备 33010602011771号