标1:秒杀设计-业务设计、架构设计、表设计
目标2:工程讲解
目标3:商品详情页开发
目标4:数据同步Canal学习
目标5:分布式任务调度elastic-job学习
目标6:静态页动态更新
业务流程
电商项目中,秒杀属于技术挑战最大的业务。后台可以发布秒杀商品后或者将现有商品列入秒杀商品,热点分析系统会对商品进行分析,对热点商品做特殊处理。商城会员可以在秒杀活动开始的时间内进行抢购,抢购后可以在线进行支付,支付完成的订单由平台工作人员发货,超时未支付订单会自动取消。
当前秒杀系统中一共涉及到管理员后台、搜索系统、秒杀系统、抢单流程系统、热点数据发现系统,如下图:
秒杀架构
B2B2C商城秒杀商品数据一般都是非常庞大,流量特别高,尤其是双十一等节日,所以设计秒杀系统,既要考虑系统抗压能力,也要考虑系统数据存储和处理能力。秒杀系统虽然流量特别高,但往往高流量抢购的商品为数不多,因此我们系统还需要对抢购热门的商品进行有效识别
商品详情页的内容除了数量变更频率较高,其他数据基本很少发生变更,像这类变更频率低的数据,我们可以考虑采用模板静态化技术处理。
秒杀系统需要考虑抗压能力,编程语言的选择也有不少讲究。项目发布如果采用Tomcat,单台Tomcat抗压能力能调整到大约1000左右,占用资源较大。Nginx抗压能力轻飘的就能到5万,并且Nginx占用资源极小,运行稳定。如果单纯采用Java研发秒杀系统,用Tomcat发布项目,在抗压能力上显然有些不足,如果采用Lua脚本开发量大的功能,采用Nginx+Lua处理用户的请求,那么并发处理能力将大大提升。
下面是当前秒杀系统的架构图:
数据库设计
数据库名字: seckill_goods
秒杀订单数据库
秒杀订单表: tb_order
管理员数据库
管理员表: tb_admin
用户数据库
用户表: tb_user
技术栈介绍
分析
秒杀活动中,热卖商品的详情页访问频率非常高,详情页的数据加载,我们可以采用直接从数据库查询加载,但这种方式会给数据库带来极大的压力,甚至崩溃,这种方式我们并不推荐。
商品详情页主要有商品介绍、商品标题、商品图片、商品价格、商品数量等,大部分数据几乎不变,可能只有数量会变,因此我们可以考虑把商品详情页做成静态页,每次访问只需要加载库存数量,这样就可以大大降低数据库的压力。
我们这里将采用freemarker来实现商品详情页的静态化,关于freemarker的语法我们就不在这里讲解了,大家可以自行去网上查阅相关API。并发处理能力
1、降低了数据库查询频率
2、使用Nginx实现详情页访问效率远高于Tomcat
canal主要用途是基于 MySQL 数据库增量日志解析,并能提供增量数据订阅和消费,应用场景十分丰富。
github地址:https://github.com/alibaba/canal
版本下载地址: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流)
Canal安装
参考文档:https://github.com/alibaba/canal/wiki/QuickStart
MySQL Bin-log开启
1)MySQL开启bin-log
a.进入mysql容器
docker exec ‐it ‐u root mysql /bin/bash
cd /etc/mysql/mysql.conf.d
b.开启mysql的binlog
在mysqld.cnf最下面添加如下配置
# 开启 binlog
log‐bin=/var/lib/mysql/mysql‐bin
# 选择 ROW 模式
binlog‐format=ROW
# 配置 MySQL replaction 需要定义,不要和 canal 的 slaveId 重复
server‐id=12345
c.创建账号并授权
授权 canal 链接 MySQL 账号具有作为 MySQL slave 的权限, 如果已有账户可直接 grant:
create user canal@'%' IDENTIFIED by 'canal';
GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT,SUPER ON *.* TO 'canal'@'%';
FLUSH PRIVILEGES;
d.重启mysql
docker restart mysql
开启bin-log后,我们可以用sql语句查看下:
show variables like '%log_bin%'
效果如下:
Canal安装
1)拉取镜像
docker pull canal/canal‐server:v1.1.1
2)安装容器
a.安装canal-server容器
docker run ‐p 11111:11111 ‐‐name canal ‐d docker.io/canal/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 ,配置要监听的数据库服务地址和监听数据变化的数据库以及表,修改如下:
指定监听数据库表的配置如下 canal.instance.filter.regex :
mysql 数据解析关注的表,Perl正则表达式.
多个正则之间以逗号(,)分隔,转义符需要双斜杠(\)
常见例子:
1. 所有表:.* or .*\..*
2. canal schema下所有表: canal\..*
3. canal下的以canal打头的表:canal\.canal.*
4. canal schema下的一张表:canal.test1
5. 多个规则组合使用:canal\..*,mysql.test1,mysql.test2 (逗号分隔)
注意:此过滤条件只针对row模式的数据有效(ps. mixed/statement因为不解析sql,所以无法准确提取tableName进行过滤)
重启canal
docker restart canal
Canal微服务
我们搭建一个微服务,用于读取canal监听到的变更日志,微服务名字叫 seckill-canal 。该项目我们需要引入canal-spring-boot-autoconfigure 包,并且需要实现 EntryHandler<T> 接口,该接口中有3个方法,分别为insert 、 update 、 delete ,这三个方法用于监听数据增删改变化。
参考地址:https://github.com/NormanGyllenhaal/canal-client
静态页同步
只需要添加Feign包,注入SkuPageFeign,根据增删改不同的需求实现生成静态页或删除静态页。修改SkuHandler
分布式任务调度介绍
很多时候,我们需要定时执行一些程序完成一些预定要完成的操作,如果手动处理,一旦任务量过大,就非常麻烦,所以用定时任务去操作是个非常不错的选项。
现在的应用多数是分布式或者微服务,所以我们需要的是分布式任务调度,那么现在分布式任务调度流行的主要有elastic-job、xxl-job、quartz等
elastic-job讲解
官网:http://elasticjob.io/index_zh.html
静态任务案例
使用elastic-job很容易,我们接下来学习下elastic-job的使用,这里的案例我们先实现静态任务案例,静态任务案例也就是执行时间事先写好。
实现步骤:
1.引入依赖包
2.配置zookeeper节点以及任务名称命名空间
3.实现自定义任务,需要实现SimpleJob接口
索引和静态资源的更新功能已经完成,所有秒杀商品都只是参与一段时间活动,活动时间过了需要将秒杀商品从索引中移除,同时删除静态页。我们需要有这么一个功能,在秒杀商品活动结束的时候,将静态页删除、索引库数据删除。
此时我们可以使用elastic-job定时执行该操作,我们看如下活动表,活动表中有一个活动开始时间和活动结束时间,我们可以在每次增加、修改的时候,动态创建一个定时任务,把活动结束时间作为任务执行时间。
https://gitee.com/didispace/SpringBoot-Learning.git
https://gitee.com/jeff1993/springboot-learning-example.git
https://gitee.com/kutilion/MyArtifactForEffectiveJava.git
通常实际的项目中会引入大量的静态资源。比如图片,样式表css,脚本js,静态html页面等。这章主要学习引入模板来实现访问静态资源。
一般Springboot提供的默认静态资源存放位置是/resources之下。html的文件一般存放在/resources/templates中。
渲染静态页面通常会用到模板。模板种类很多,这里介绍两种:
另外比较常用的模板还有velocity,但是velocity在Springboot1.5开始就不被支持了。
示例相关代码如下:
Thymeleaf
FreeMarker
build.gradle
为了使用Thymeleaf模板,需要在build.gradle脚本中引入模板引擎的依赖
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf
WebController.java
控制器类中声明访问路径,并且为模板添加一个变量
@RequestMapping("/thymeleaf") public String ThymeleafTest(ModelMap map) { map.addAttribute("host", "http://blog.kutilionThymeleaf.com"); return "06_webframework/thymeleaf"; }
注意这个方法的返回值,因为静态页面没有直接放在templates文件夹下,而是放在templates文件夹的子文件夹06_webframework中,所以返回值中要把路径带上
thymeleaf.html
静态页面中使用了el表达式,可以将java变量反映到页面上
<!DOCTYPE html> <html> <head lang="en"> <meta charset="UTF-8" /> <title></title> </head> <body> <h1 th:text="${host}">This Thymeleaf framework test page.</h1> </body> </html>
执行结果:
原理和Thymeleaf基本是一样的
build.gradle
implementation 'org.springframework.boot:spring-boot-starter-freemarker'
WebController.java
@RequestMapping("/freemarker") public String FreeMarkerTest(ModelMap map) { map.addAttribute("host", "http://blog.kutilionFreemarker.com"); return "06_webframework/thymeleaf"; }
freemarker.ftl
<!DOCTYPE html> <html> <head lang="en"> <meta charset="UTF-8" /> <title></title> </head> <body> <h1 th:text="${host}">This Freemarker framework test page.</h1> </body> </html>
执行结果:
们可以把一个方法赋值给类的函数本身,而不是赋给它的 "prototype"。这样的方法被称为 静态的(static)。
在一个类中,它们以 static 关键字开头,如下所示:
class User {
static staticMethod() {
alert(this === User);
}
}
User.staticMethod(); // true
这实际上跟直接将其作为属性赋值的作用相同:
class User { }
User.staticMethod = function() {
alert(this === User);
};
User.staticMethod(); // true
在 User.staticMethod() 调用中的 this 的值是类构造器 User 自身(“点符号前面的对象”规则)。
通常,静态方法用于实现属于该类但不属于该类任何特定对象的函数。
例如,我们有对象 Article,并且需要一个方法来比较它们。一个自然的解决方案就是添加 Article.compare 方法,像下面这样:
class Article {
constructor(title, date) {
this.title = title;
this.date = date;
}
static compare(articleA, articleB) {
return articleA.date - articleB.date;
}
}
// 用法
let articles = [
new Article("HTML", new Date(2019, 1, 1)),
new Article("CSS", new Date(2019, 0, 1)),
new Article("JavaScript", new Date(2019, 11, 1))
];
articles.sort(Article.compare);
alert( articles[0].title ); // CSS
这里 Article.compare 代表“上面的”文章,意思是比较它们。它不是文章的方法,而是整个 class 的方法。
另一个例子是所谓的“工厂”方法。想象一下,我们需要通过几种方法来创建一个文章:
第一种方法我们可以通过 constructor 来实现。对于第二种方式,我们可以创建类的一个静态方法来实现。
就像这里的 Article.createTodays():
class Article {
constructor(title, date) {
this.title = title;
this.date = date;
}
static createTodays() {
// 记住 this = Article
return new this("Today's digest", new Date());
}
}
let article = Article.createTodays();
alert( article.title ); // Today's digest
现在,每当我们需要创建一个今天的文章时,我们就可以调用 Article.createTodays()。再说明一次,它不是一个文章的方法,而是整个 class 的方法。
静态方法也被用于与数据库相关的公共类,可以用于搜索/保存/删除数据库中的条目, 就像这样:
// 假定 Article 是一个用来管理文章的特殊类
// 静态方法用于移除文章:
Article.remove({id: 12345});
静态的属性也是可能的,它们看起来就像常规的类属性,但前面加有 static:
class Article {
static publisher = "Levi Ding";
}
alert( Article.publisher ); // Levi Ding
这等同于直接给 Article 赋值:
Article.publisher = "Levi Ding";
静态属性和方法是可被继承的。
例如,下面这段代码中的 Animal.compare 和 Animal.planet 是可被继承的,可以通过 Rabbit.compare 和 Rabbit.planet 来访问:
class Animal {
static planet = "Earth";
constructor(name, speed) {
this.speed = speed;
this.name = name;
}
run(speed = 0) {
this.speed += speed;
alert(`${this.name} runs with speed ${this.speed}.`);
}
static compare(animalA, animalB) {
return animalA.speed - animalB.speed;
}
}
// 继承于 Animal
class Rabbit extends Animal {
hide() {
alert(`${this.name} hides!`);
}
}
let rabbits = [
new Rabbit("White Rabbit", 10),
new Rabbit("Black Rabbit", 5)
];
rabbits.sort(Rabbit.compare);
rabbits[0].run(); // Black Rabbit runs with speed 5.
alert(Rabbit.planet); // Earth
现在我们调用 Rabbit.compare 时,继承的 Animal.compare 将会被调用。
它是如何工作的?再次,使用原型。你可能已经猜到了,extends 让 Rabbit 的 [[Prototype]] 指向了 Animal。
所以,Rabbit extends Animal 创建了两个 [[Prototype]] 引用:
结果就是,继承对常规方法和静态方法都有效。
这里,让我们通过代码来检验一下:
class Animal {}
class Rabbit extends Animal {}
// 对于静态的
alert(Rabbit.__proto__ === Animal); // true
// 对于常规方法
alert(Rabbit.prototype.__proto__ === Animal.prototype); // true
静态方法被用于实现属于整个类的功能。它与具体的类实例无关。
举个例子, 一个用于进行比较的方法 Article.compare(article1, article2) 或一个工厂(factory)方法 Article.createTodays()。
在类生命中,它们都被用关键字 static 进行了标记。
静态属性被用于当我们想要存储类级别的数据时,而不是绑定到实例。
语法如下所示:
class MyClass {
static property = ...;
static method() {
...
}
}
从技术上讲,静态声明与直接给类本身赋值相同:
MyClass.property = ...
MyClass.method = ...
静态属性和方法是可被继承的。
对于 class B extends A,类 B 的 prototype 指向了 A:B.[[Prototype]] = A。因此,如果一个字段在 B 中没有找到,会继续在 A 中查找。
*请认真填写需求信息,我们会在24小时内与您取得联系。