標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小時內與您取得聯系。