整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

100W请求秒杀架构体系-静态资源处理

标1:秒杀设计-业务设计、架构设计、表设计

目标2:工程讲解

目标3:商品详情页开发

目标4:数据同步Canal学习

目标5:分布式任务调度elastic-job学习

目标6:静态页动态更新


1 秒杀设计

业务流程

电商项目中,秒杀属于技术挑战最大的业务。后台可以发布秒杀商品后或者将现有商品列入秒杀商品,热点分析系统会对商品进行分析,对热点商品做特殊处理。商城会员可以在秒杀活动开始的时间内进行抢购,抢购后可以在线进行支付,支付完成的订单由平台工作人员发货,超时未支付订单会自动取消。

当前秒杀系统中一共涉及到管理员后台、搜索系统、秒杀系统、抢单流程系统、热点数据发现系统,如下图:


秒杀架构

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增量数据同步利器

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定时执行该操作,我们看如下活动表,活动表中有一个活动开始时间和活动结束时间,我们可以在每次增加、修改的时候,动态创建一个定时任务,把活动结束时间作为任务执行时间。

学习记录的代码,部分参考自gitee码云的如下两个工程。这两个个工程有详尽的Spingboot1.x和2.x教程。鸣谢!

https://gitee.com/didispace/SpringBoot-Learning.git

https://gitee.com/jeff1993/springboot-learning-example.git

本学习记录的示例代码克隆地址,分支为develop

https://gitee.com/kutilion/MyArtifactForEffectiveJava.git

静态资源

通常实际的项目中会引入大量的静态资源。比如图片,样式表css,脚本js,静态html页面等。这章主要学习引入模板来实现访问静态资源。

一般Springboot提供的默认静态资源存放位置是/resources之下。html的文件一般存放在/resources/templates中。

渲染静态页面通常会用到模板。模板种类很多,这里介绍两种:

  • Thymeleaf
  • FreeMarker

另外比较常用的模板还有velocity,但是velocity在Springboot1.5开始就不被支持了。

示例相关代码如下:

Thymeleaf

  • studySpringboot.n06.webframework.web.WebController.java
  • templates/06_webframework/thymeleaf.html

FreeMarker

  • studySpringboot.n06.webframework.web.WebController.java
  • templates/06_webframework/freemarker.ftl

Thymeleaf

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>

执行结果:

FreeMarker

原理和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>

执行结果:

总结

  • 引入并且使用了Thymeleaf和FreeMarker模板
  • Springboot默认已经配置好这两种模板了,如果需要手动配置需要参考两种模板的文档

们可以把一个方法赋值给类的函数本身,而不是赋给它的 "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 的方法。

另一个例子是所谓的“工厂”方法。想象一下,我们需要通过几种方法来创建一个文章:

  1. 通过用给定的参数来创建(titledate 等)。
  2. 使用今天的日期来创建一个空的文章。
  3. ……其它方法。

第一种方法我们可以通过 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.compareAnimal.planet 是可被继承的,可以通过 Rabbit.compareRabbit.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 将会被调用。

它是如何工作的?再次,使用原型。你可能已经猜到了,extendsRabbit[[Prototype]] 指向了 Animal

所以,Rabbit extends Animal 创建了两个 [[Prototype]] 引用:

  1. Rabbit 函数原型继承自 Animal 函数。
  2. Rabbit.prototype 原型继承自 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 指向了 AB.[[Prototype]] = A。因此,如果一个字段在 B 中没有找到,会继续在 A 中查找。

任务