每秒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());
}
}
 
 
 
posted @ 2023-05-25 00:03  十一vs十一  阅读(82)  评论(0)    收藏  举报