第5章 Lua、Canal實作廣告緩存
1. 首頁分析
首頁門戶系統需要展示各種各樣的廣告資料。以京東為例:

頁面中的廣告一般來說變變更頻率較低,對于這種資料該如何進行處理?
(1) 第一種方式
如圖所示,首頁通路廣告服務,而廣告服務從資料庫中查詢資料,而後傳回給首頁進行展示。這種方式最為簡單。但是首頁的通路量一般非常高,不适合直接通過MySQL資料庫直接通路的方式來擷取展示。
(2) 第二種方式
1.首先通路Nginx ,采用緩存的方式,先從Nginx本地緩存中擷取,擷取到直接響應
2.如果沒有擷取到則通路Redis,從Redis中擷取資料,如果有資料則傳回,并緩存到Nginx中
3.如果沒有擷取則通路MySQL,從MySQL中擷取資料,再将資料存儲到Redis中,傳回。
2. Lua介紹
2.1 Lua是什麼
Lua [1] 是一種輕量小巧的腳本語言。其設計目的是為了嵌入應用程式中,進而為應用程式提供靈活的擴充和定制功能的弱語言,不需要編譯可以直接運作。Lua是由标準C編寫而成,所有Lua腳本可以容易被C/C++代碼調用,其他所有作業系統和平台也可以進行編譯、運作
Lua是一種功能強大、高效、輕量級、可嵌入的腳本語言,支援過程程式設計、面向對象程式設計、函數程式設計、資料驅動程式設計和資料描述;
2.2 優勢
-
輕量級
輕量級Lua語言的官方版本隻包含一個簡潔的核心和最基本的庫,Lua體積小、啟動速度快,5.0.2版本Lua核心隻有120kb,适合嵌入别的程式;
-
可擴充
Lua由标準C編寫,是以C/C++的功能都可以使用,而且還可以擴充Java、C#、Smalltalk、Fortran、Ada、Perl和Ruby;
-
可移植
Lua使用C編寫,是以适用所有作業系統和平台(Windiows/Unix、IOS、Android、BREW、Symbian、WindowPhone、Rabbit等等);
-
完全開源免費
Lua是免費的開源軟體,可以用于任何目的,包括商業目的完全免費。
2.3 應用場景
- 遊戲開發
- 獨立應用腳本
- Web 應用腳本
- 擴充和資料庫插件如:MySQL Proxy 和 MySQL WorkBench
- 安全系統,如入侵檢測系統
- Redis中嵌套調用實作類似事務的功能
- web容器中應用處理一些過濾 緩存等等的邏輯,例如Nginx。
2.4 阿裡雲安裝Lua
安裝步驟,在伺服器中執行下面的指令。
curl -R -O http://www.lua.org/ftp/lua-5.3.5.tar.gz
tar zxf lua-5.3.5.tar.gz
cd lua-5.3.5
make linux test
注意:此時安裝,有可能會出現如下錯誤:
此時需要安裝lua相關依賴庫的支援,執行如下指令即可:
yum install libtermcap-devel ncurses-devel libevent-devel readline-devel
此時再執行lua測試看lua是否安裝成功
[[email protected] ~]# lua
Lua 5.1.4 Copyright (C) 1994-2008 Lua.org, PUC-Rio
2.5 入門程式
(1) 建立hello.lua檔案并編輯
vi hello.lua
(2)在檔案中輸入print(“hello lua”)後儲存并退出。
(3)執行指令并檢視輸出
lua hello.lua
效果如下:
2.6 Lua的基本文法
Lua有互動式程式設計和腳本式程式設計。
- 互動式程式設計:是直接輸入文法,通過指令 lua -i 或 lua 來啟用
- 腳本式程式設計:将Lua程式代碼儲存到一個以.lua結尾的檔案并執行
一般采用腳本式程式設計
2.6.1 注釋
-- 單行注釋
--[[
多行注釋
多行注釋
--]]
2.6.2 定義變量
預設的情況下,定義一個變量都是全局變量,如果要用局部變量需要聲明為local。
-- 全局變量指派
a=1
-- 局部變量指派
local b=2
如果變量沒有初始化:則 它的值為nil 這和java中的null不同。
如下圖案例:
2.6.3 Lua中的資料類型
Lua 是動态類型語言,變量不要類型定義,隻需要為變量指派。 值可以存儲在變量中,作為參數傳遞或結果傳回。
Lua 中基本類型如下:
資料類型 | 描述 |
---|---|
nil | 隻有值nil屬于該類,表示一個無效值(在條件表達式中相當于false)。 |
boolean | false和true |
number | 雙精度類型的實浮點數 |
string | 字元串由一對雙引号或單引号來表示 |
function | 由 C 或 Lua 編寫的函數 |
userdata | 表示任意存儲在變量中的C資料結構 |
thread | 表示執行的獨立線路,用于執行協同程式 |
table | 其實是一個"關聯數組,數組的索引可以是數字、字元串或表類型 |
執行個體:
print(type("Hello world")) --> string
print(type(10.4*3)) --> number
print(type(print)) --> function
print(type(type)) --> function
print(type(true)) --> boolean
print(type(nil)) --> nil
2.6.4 流程控制
(1)if語句
Lua中
if 語句
由一個布爾表達式作為條件判斷,其後緊跟其他語句組成。
文法:
if(布爾表達式)
then
--[ 在布爾表達式為 true 時執行的語句 --]
end
執行個體:
(2)if…else語句
Lua中 if 語句可以與 else 語句搭配使用, 在 if 條件表達式為 false 時執行 else 語句代碼塊。
文法:
if(布爾表達式)
then
--[ 布爾表達式為 true 時執行該語句塊 --]
else
--[ 布爾表達式為 false 時執行該語句塊 --]
end
執行個體:
2.6.5 循環
(1) while循環[滿足條件就循環]
Lua 程式設計語言中 while 循環語句在判斷條件為 true 時會重複執行循環體語句。
文法:
while(condition)
do
statements
end
執行個體:
a=10
while( a < 20 )
do
print("a 的值為:", a)
a = a+1
end
效果如下:
(2) for循環
Lua 程式設計語言中 for 循環語句可以重複執行指定語句,重複次數可在 for 語句中控制。
文法:
for var=exp1,exp2,exp3
do
<執行體>
end
var 從 exp1 變化到 exp2,每次變化以 exp3 為步長遞增 var,并執行一次
"執行體"
。exp3 是可選的,如果不指定,預設為1。若想執行遞減操作,exp3應為負數
執行個體:
for i=1,9,2
do
print(i)
end
i從1開始循環,當循環到 i=9 時停止,每次 i 遞增 2
效果如下:
(3)repeat…until語句[滿足條件結束]
Lua 程式設計語言中 repeat…until 循環語句不同于 for 和 while循環,for 和 while 循環的條件語句在目前循環執行開始時判斷,而 repeat…until 循環的條件語句在目前循環結束後判斷,相當于do while循環
文法:
repeat
statements
until( condition )
執行個體:
num =5
repeat
print(num)
num=num-1
until (num==0)
效果如下:
2.6.6 函數
lua中也可以定義函數,類似于java中的方法。結束需要添加end
執行個體:
--[[ 函數傳回兩個值的最大值 --]]
function max(num1, num2)
if (num1 > num2) then
result = num1;
else
result = num2;
end
return result;
end
-- 調用函數
print("兩值比較最大值為 ",max(10,4))
-- .. 表示拼接
print("兩值比較最大值為 "..max(5,6))
效果如下:
2.6.7 表
table 是 Lua 的一種資料結構用來幫助我們建立不同的資料類型,如:數組、字典等。
Lua也是通過table來解決子產品(module)、包(package)和對象(Object)的。
執行個體:
-- 初始化表
mytable = {}
-- 指定值
mytable[1]= "Lua"
-- 移除引用
mytable = nil
2.6.7 子產品
(1) 子產品定義
子產品類似于一個封裝庫,從 Lua5.1開始,Lua加入了标準的子產品管理機制,可以把一些公用的代碼放在一個檔案裡,以 API 接口的形式在其他地方調用,有利于代碼的重用和降低代碼耦合度。
建立一個檔案叫module.lua,在module.lua中建立一個獨立的子產品,代碼如下:
-- 檔案名為 module.lua
-- 定義一個名為 module 的子產品
module = {}
-- 定義一個常量
module.constant = "這是一個常量"
-- 定義一個函數
function module.func1()
print("這是一個公有函數")
end
local function func2()
print("這是一個私有函數!")
end
function module.func3()
func2()
end
return module
由上可知,子產品的結構就是一個 table 的結構,是以可以像操作調用 table 裡的元素那樣來操作調用子產品裡的常量或函數。
上面的 func2 聲明為程式塊的局部變量,即表示一個私有函數,是以是不能從外部通路子產品裡的這個私有函數,必須通過子產品裡的公有函數來調用.
(2) require 函數
require 用于 引入其他的子產品,類似于java中的類要引用别的類的效果。
用法:
require("<子產品名>")
require "<子產品名>"
(3) 應用
将上面定義的module子產品引入使用,建立一個test_module.lua檔案
-- test_module.lua 檔案
-- module 子產品為上文提到到 module.lua
require("module")
print(module.constant)
module.func3()
效果如下:
3. OpenResty®
3.1 OpenResty介紹
OpenResty® 是一個基于Nginx與Lua的高性能 Web 平台,其内部內建了大量精良的Lua庫、第三方子產品以及大多數的依賴項。用于友善地搭建能夠處理超高并發、擴充性極高的動态 Web 應用、Web 服務和動态網關。
OpenResty® 通過彙聚各種設計精良的Nginx子產品(主要由 OpenResty 團隊自主開發),進而将 Nginx 有效地變成一個強大的通用 Web 應用平台。這樣,Web 開發人員和系統工程師可以使用Lua腳本語言調動Nginx支援的各種C以及Lua子產品,快速構造出足以勝任10K乃至1000K以上單機并發連接配接的高性能 Web 應用系統。
OpenResty® 的目标是讓你的Web服務直接跑在 Nginx 服務内部,充分利用 Nginx 的非阻塞 I/O 模型,不僅僅對 HTTP 用戶端請求,甚至于對遠端後端諸如 MySQL、PostgreSQL、Memcached 以及 Redis 等都進行一緻的高性能響應。
3.2 安裝OpenResty
OpenResty提供了各種伺服器環境的安裝方式,因為我是CentOS, 是以選擇了以下指令,其他類型可以到官網檢視對應方法。
1.添加倉庫執行指令
yum install yum-utils
yum-config-manager --add-repo https://openresty.org/package/centos/openresty.repo
2.執行安裝
yum install openresty
3.安裝成功後 預設在/usr/local/目錄,如下圖
3.3 配置Nginx
預設已經安裝好了Nginx,在目錄:
/usr/local/openresty/nginx
下。
修改
/usr/local/openresty/nginx/conf/nginx.conf
,将配置檔案使用的根設定為root,目的就是将來要使用lua腳本的時候 ,直接可以加載在root下的lua腳本。
vi /usr/local/openresty/nginx/conf/nginx.conf
修改代碼如下:
3.4 測試通路
(1) 修改nginx.conf配置檔案
vi /usr/local/openresty/nginx/conf/nginx.conf
添加如下代碼
(2) 啟動OpenResty-Nginx服務
--啟動
/usr/local/openresty/nginx/sbin/nginx
--停止
/usr/local/openresty/nginx/sbin/nginx -s stop
--重新開機
/usr/local/openresty/nginx/sbin/nginx -s reload
--檢驗nginx配置是否正确
/usr/local/openresty/nginx/sbin/nginx -t
(3) 浏覽器通路:
http://www.xiexun.top:9100/test
注意阿裡雲伺服器安全組需要開啟9100端口
4. 廣告緩存的載入與讀取
4.1 邏輯簡述
- 定義請求名稱
- 書寫Lua腳本,查詢Nginx緩存是否有資料,若無資料,依次查詢Redis與MySQL并對資料進行儲存
- 發起請求,擷取資料
4.2 功能實作
4.2.1 請求位址
http://www.xiexun.top:9100/read_advert?id=1
4.2.2 腳本編寫
在/root/lua目錄下建立update_advert.lua腳本
腳本如下:
ngx.header.content_type = "application/json;charset=utf8"
local uri_args = ngx.req.get_uri_args();
local id = uri_args["id"];
--擷取本地緩存
local cache_ngx = ngx.shared.dis_cache;
--根據ID 擷取本地緩存資料
local advertCache = cache_ngx:get('advert_cache_' .. id);
if advertCache == "" or advertCache == nil then
ngx.say("本地緩存中無資料");
ngx.say("開始從Redis中擷取資料.....")
local redis = require("resty.redis");
local red = redis:new()
red:set_timeout(10000)
red:connect("39.105.162.100", 6379)
local redisAdvert = red:get("advert_" .. id);
if ngx.null==redisAdvert then
ngx.say("Redis中無資料");
ngx.say("開始從資料庫中擷取資料.....")
local cjson = require("cjson");
local mysql = require("resty.mysql");
local db = mysql:new();
db:set_timeout(10000)
local props = {
host = "39.105.162.100",
port = 3306,
database = "changgou_advert",
user = "root",
password = "root"
}
local res = db:connect(props);
local select_sql = "select url,pic from tb_advert where status ='1' and category_id=" .. id .. " order by sort_order";
res = db:query(select_sql);
ngx.say("資料庫查詢成功");
local responsejson = cjson.encode(res);
red:set("advert_" .. id, responsejson);
ngx.say(responsejson);
db:close()
else
ngx.say("Redis查詢成功")
cache_ngx:set('advert_cache_' .. id, redisAdvert, 10 * 60);
ngx.say(redisAdvert)
end
red:close()
else
ngx.say(advertCache)
end
4.2.3 Nginx配置
vi /usr/local/openresty/nginx/conf/nginx.conf
添加如下代碼
location /read_advert {
limit_req zone=advertRateLimit;
content_by_lua_file /root/lua/read_advert.lua;
}
如下圖所示
4.3 測試
測試位址:http://www.xiexun.top:9100/read_advert?id=1
5. Nginx限流
一般情況下,首頁的并發量是比較大的。當使用者不停的重新整理首頁時,即使是多級緩存,也會産生一定壓力。是以需要引入限流來進行保護
5.1 生活中限流對比
- 水壩洩洪:通過閘口限制洪水流量(控制流量速度)
- 辦理銀行業務:所有人先領号,各視窗叫号處理。每個視窗處理速度根據客戶具體業務而定,所有人排隊等待叫号即可。若快下班時,告知客戶明日再來(拒絕流量)
- 火車站排隊買票安檢:通過排隊的方式依次放入。(緩存帶處理任務)
5.2 nginx的限流
(1) Nginx限流的方式:
- 控制速率:limit_req_zone
- 控制并發連接配接數:limit_conn_zone
(2) Nginx限流算法
令牌桶算法
- 令牌以固定速率産生,并緩存到令牌桶中;
- 令牌桶放滿時,多餘的令牌被丢棄;
- 請求要消耗等比例的令牌才能被處理;
- 令牌不夠時,請求被緩存。
漏桶算法
- 水(請求)從上方倒入水桶,從水桶下方流出(被處理)
- 來不及流出的水存在水桶中(緩沖),以固定速率流出
- 水桶滿後水溢出(丢棄)
- 這個算法的核心是:緩存請求、勻速處理、多餘的請求直接丢棄
相比漏桶算法,令牌桶算法不同之處在于它不但有一隻“桶”,還有個隊列,這個桶是用來存放令牌的,隊列才是用來存放請求的。
從作用上來說,漏桶和令牌桶算法最明顯的差別就是是否允許突發流量(burst)的處理,漏桶算法能夠強行限制資料的實時傳輸(處理)速率,對突發流量不做額外處理;而令牌桶算法能夠在限制資料的平均傳輸速率的同時允許某種程度的突發傳輸。
Nginx按請求速率限速子產品使用的是漏桶算法,即能夠強行保證請求的實時處理速度不會超過設定的門檻值。
5.2.1 控制速率
(1) limit_req_zone 參數配置講解
limit_req_zone $binary_remote_addr zone=one:10m rate=1r/s;
- $binary_remote_addr:表示通過remote_addr這個辨別來做限制,“binary_”的目的是縮寫記憶體占用量,是限制同一用戶端IP位址
- zone=one:10m:表示生成一個大小為10M,名字為one的記憶體區域,用來存儲通路的頻次資訊
- rate=1r/s:表示允許相同辨別的用戶端的通路頻次,這裡限制的是每秒1次,還可以30r/m
limit_req zone=one burst=5 nodelay;
- zone=one:表示設定使用哪個配置區域來做限制,與上面limit_req_zone 裡的name對應
- burst=5:表示設定一個大小為5的緩沖區,當有大量請求(爆發)過來時,超過了通路頻次限制的請求可以先放到這個緩沖區内
- nodelay:如果設定,超過通路頻次而且緩沖區也滿了的時候就會直接傳回503,如果沒有設定,則所有請求會等待排隊。
(2) 修改Nginx.conf
代碼如下:
user root root;
worker_processes 1;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
#cache
lua_shared_dict dis_cache 128m;
#限流設定
limit_req_zone $binary_remote_addr zone=contentRateLimit:10m rate=2r/s;
sendfile on;
#tcp_nopush on;
#keepalive_timeout 0;
keepalive_timeout 65;
#gzip on;
server {
listen 80;
server_name localhost;
location /update_content {
content_by_lua_file /root/lua/update_content.lua;
}
location /read_content {
#使用限流配置
limit_req zone=contentRateLimit;
content_by_lua_file /root/lua/read_content.lua;
}
}
}
(3) 測試
重新加載配置檔案
通路頁面:http://www.xiexun.top:9100/read_content?id=1,連續重新整理會直接報錯。
(4) 處理突發流量
上面例子限制 2r/s,如果有時正常流量突然增大,超出的請求将被拒絕,無法處理突發流量,可以結合 burst 參數使用來解決該問題。
設定**burst=4 **,若同時有4個請求到達,Nginx 會處理第一個請求,剩餘3個請求将放入隊列,然後每隔500ms從隊列中擷取一個請求進行處理。若請求數大于4,将拒絕處理多餘的請求,直接傳回503.
不過,單獨使用 burst 參數并不實用。假設 burst=50 ,rate依然為10r/s,排隊中的50個請求雖然每100ms會處理一個,但第50個請求卻需要等待 50 * 100ms即 5s,這麼長的處理時間自然難以接受。
是以,burst 往往結合 nodelay 一起使用。
例如:如下配置:
server {
listen 80;
server_name localhost;
location /update_content {
content_by_lua_file /root/lua/update_content.lua;
}
location /read_content {
limit_req zone=contentRateLimit burst=4 nodelay;
content_by_lua_file /root/lua/read_content.lua;
}
}
如上表示:
平均每秒允許不超過2個請求,突發不超過4個請求,并且處理突發4個請求的時候,沒有延遲,等到完成之後,按照正常的速率處理。
如上兩種配置結合就達到了速率穩定,但突然流量也能正常處理的效果。完整配置代碼如下:
user root root;
worker_processes 1;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
#cache
lua_shared_dict dis_cache 128m;
#限流設定
limit_req_zone $binary_remote_addr zone=contentRateLimit:10m rate=2r/s;
sendfile on;
#tcp_nopush on;
#keepalive_timeout 0;
keepalive_timeout 65;
#gzip on;
server {
listen 80;
server_name localhost;
location /update_content {
content_by_lua_file /root/lua/update_content.lua;
}
location /read_content {
limit_req zone=contentRateLimit burst=4 nodelay;
content_by_lua_file /root/lua/read_content.lua;
}
}
}
5.2.2 控制并發量
(1) ngx_http_limit_conn_module 參數配置講解
這個子產品用來限制單個IP的請求數。并非所有的連接配接都被計數。隻有在伺服器處理了請求并且已經讀取了整個請求頭時,連接配接才被計數。
(2) 配置限制固定連接配接數
配置如下:
http {
include mime.types;
default_type application/octet-stream;
#cache
lua_shared_dict dis_cache 128m;
#限流設定
limit_req_zone $binary_remote_addr zone=contentRateLimit:10m rate=2r/s;
#根據IP位址來限制,存儲記憶體大小10M
limit_conn_zone $binary_remote_addr zone=addr:1m;
sendfile on;
#tcp_nopush on;
#keepalive_timeout 0;
keepalive_timeout 65;
#gzip on;
server {
listen 80;
server_name localhost;
#所有以brand開始的請求,通路本地changgou-service-goods微服務
location /brand {
limit_conn addr 2;
proxy_pass http://192.168.211.1:18081;
}
location /update_content {
content_by_lua_file /root/lua/update_content.lua;
}
location /read_content {
limit_req zone=contentRateLimit burst=4 nodelay;
content_by_lua_file /root/lua/read_content.lua;
}
}
}
其中:
limit_conn_zone $binary_remote_addr zone=addr:10m 表示限制根據使用者的IP位址來顯示,設定存儲位址為的記憶體大小10M
limit_conn addr 2 表示 同一個位址隻允許連接配接2次。
(3) 限制每個用戶端IP與伺服器的連接配接數,同時限制與虛拟伺服器的連接配接總數
如下配置:
limit_conn_zone $binary_remote_addr zone=perip:10m;
limit_conn_zone $server_name zone=perserver:10m;
server {
listen 80;
server_name localhost;
charset utf-8;
location / {
limit_conn perip 10;#單個用戶端ip與伺服器的連接配接數.
limit_conn perserver 100; #限制與伺服器的總連接配接數
root html;
index index.html index.htm;
}
}
6. Canal介紹
Canal是阿裡巴巴旗下的一款開源項目,純Java開發。基于資料庫增量日志解析,提供增量資料訂閱&消費,目前主要支援了MySQL(也支援mariaDB)
6.1 工作原理
6.1.1 MySQL主從複制實作
- master将改變記錄到二進制日志(binary log)中。這些記錄叫做二進制日志事件,binary log events,可以通過show binlog events進行檢視
- slave将master的binary log events拷貝到它的中繼日志(relay log)
- slave重做中繼日志中的事件,将改變反映它自己的資料
6.1.2 工作原理
- canal模拟mysql slave的互動協定,僞裝自己為mysql slave,向mysql master發送dump協定
- mysql master收到dump請求,開始推送binary log給slave(也就是canal)
- canal解析binary log對象(原始為byte流)
6.2 安裝canal
6.2.1 下載下傳鏡像
docker canal/canal-server:v1.1.4
6.2.2 啟動容器
docker run -p 11111:11111 --name canal -d canal/canal-server:v1.1.4
6.2.3 配置instance.properties
vi canal-server/conf/example/instance.properties
詳細配置可參考Canal AdminGuide
6.2.4 重新開機canal
docker update --restart=always canal
docker restart canal
6.3 開啟binlog模式
在使用canal時,需要開啟MySQL的binlog模式。同時要求MySQL的版本為5.7及以下。
6.3.1 修改mysqld.cnf
docker exec -it mysql /bin/bash
cd /etc/mysql/mysql.conf.d
vi mysqld.cnf
添加如下配置:
log-bin /var/lib/mysql/mysql-bin
server-id=12345
6.3.2 建立賬号
使用root賬号建立使用者并授予權限
create user [email protected]'%' IDENTIFIED by 'canal';
GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT,SUPER ON *.* TO 'canal'@'%';
FLUSH PRIVILEGES;
6.3.3 重新開機MySQL
docker restart mysql
7. 廣告同步實作
當使用者執行資料庫的操作的時候,binlog日志會被canal捕獲到,并解析出資料。然後将解析出來的資料進行同步到Redis中即可。
7.1 canal微服務
7.1.1 搭建canal微服務
在
thankson-springcloud-provider
下建立
thankson-springcloud-canal
工程,并引入相關配置。
項目結構如下
7.1.2 pom.xml配置
<?xml version="1.0" encoding="UTF-8"?>
<?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>thankson-springcloud-provider</artifactId>
<groupId>com.thankson.springcloud</groupId>
<version>1.0.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>thankson-springcloud-canal</artifactId>
<dependencies>
<dependency>
<groupId>com.thankson.springcloud</groupId>
<artifactId>thankson-springcloud-mall-api-advert</artifactId>
<version>1.0.0</version>
</dependency>
</dependencies>
</project>
7.1.3 application.yml配置
server:
port: 9100
spring:
application:
name: changgou-canal
redis:
host: www.xiexun.top
port: 6379
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:8761/eureka
instance:
prefer-ip-address: true
feign:
hystrix:
enabled: true
#canal配置
canal:
client:
instances:
example:
host: www.xiexun.top
port: 11111
clusterEnabled: false
retryCount: 5
7.1.4 建立監聽器
在包
com.thankson.canal.listener
下建立
CanalDataEventListener
類,實作對表增删改操作的監聽,代碼如下:
@CanalEventListener
public class CanalDataEventListener {
@Autowired
private AdvertFeign advertFeign;
@Autowired
private StringRedisTemplate stringRedisTemplate;
@ListenPoint(destination = "example",
schema = "changgou_advert",
table = {"tb_advert", "tb_advert_category"},
eventType = {
CanalEntry.EventType.UPDATE,
CanalEntry.EventType.DELETE,
CanalEntry.EventType.INSERT})
public void onEventCustomUpdate(CanalEntry.EventType eventType, CanalEntry.RowData rowData) {
//1.擷取列名 為category_id的值
String categoryId = getColumnValue(eventType, rowData);
//2.調用feign 擷取該分類下的所有的廣告集合
Result<List<Advert>> categoryResult = advertFeign.findByCategory(Integer.valueOf(categoryId));
List<Advert> data = categoryResult.getData();
for (Advert datum : data) {
System.out.println(datum);
}
//3.使用redisTemplate存儲到redis中
stringRedisTemplate.boundValueOps("advert_" + categoryId).set(JSON.toJSONString(data));
}
private String getColumnValue(CanalEntry.EventType eventType, CanalEntry.RowData rowData) {
String categoryId = "";
//判斷 如果是删除 則擷取BeforeColumnsList
if (eventType == CanalEntry.EventType.DELETE) {
for (CanalEntry.Column column : rowData.getBeforeColumnsList()) {
if (column.getName().equalsIgnoreCase("category_id")) {
categoryId = column.getValue();
return categoryId;
}
}
} else {
//判斷 如果是添加 或者是更新 擷取AfterColumnsList
for (CanalEntry.Column column : rowData.getAfterColumnsList()) {
if (column.getName().equalsIgnoreCase("category_id")) {
categoryId = column.getValue();
return categoryId;
}
}
}
return categoryId;
}
}
7.1.5 啟動類建立
在
com.thankson.canal
包下建立啟動類CanalApplication,代碼如下:
@SpringBootApplication(exclude={DataSourceAutoConfiguration.class})
@EnableEurekaClient
@EnableCanalClient
@EnableFeignClients(basePackages = {"com.thankson.mall.advert.feign"})
public class CanalApplication {
public static void main(String[] args) {
SpringApplication.run(CanalApplication.class,args);
}
}
7.2 advert微服務
7.2.1 搭建advert微服務
- 在
子產品下建立thankson-springcloud-mall-service
微服務thankson-springcloud-mall-service-advert
- 在
子產品下建立thankson-springcloud-mall-api
微服務thankson-springcloud-mall-api-advert
項目結構如下
7.2.2 pom.xml配置
- api-advert
<?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>thankson-springcloud-mall-api</artifactId>
<groupId>com.thankson.springcloud</groupId>
<version>1.0.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>thankson-springcloud-mall-api-advert</artifactId>
</project>
- service-advert
<?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>thankson-springcloud-mall-service</artifactId>
<groupId>com.thankson.springcloud</groupId>
<version>1.0.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>thankson-springcloud-mall-service-advert</artifactId>
<dependencies>
<dependency>
<groupId>com.thankson.springcloud</groupId>
<artifactId>thankson-springcloud-mall-api-advert</artifactId>
<version>1.0.0</version>
</dependency>
</dependencies>
</project>
7.2.3 application.yml配置
- service-advert
server:
port: 10102
spring:
application:
name: mall-advert
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://www.xiexun.top:3306/changgou_advert?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC
username: canal
password: canal
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:8761/eureka
instance:
prefer-ip-address: true
feign:
hystrix:
enabled: true
mybatis:
configuration:
map-underscore-to-camel-case: true #開啟駝峰功能
hystrix:
command:
default:
execution:
timeout:
#如果enabled設定為false,則請求逾時交給ribbon控制
enabled: true
isolation:
strategy: SEMAPHORE
7.2.4 業務代碼
因為采用腳本生成代碼,是以不進行過多的介紹。具體代碼可以參考項目源碼。
7.2.5 啟動類建立
@SpringBootApplication
@EnableEurekaClient
@MapperScan(basePackages = {"com.thankson.mall.advert.dao"})
public class AdvertApplication {
public static void main(String[] args) {
SpringApplication.run(AdvertApplication.class);
}
}
7.2.6 測試
1、啟動eureka、canal、advert微服務,效果如下
2、檢視Redis資料
3、修改表tb_advert中廣告分類為1的資料後檢視redis
由上圖可見 代碼已正确執行
8. 結束語
至此,廣告同步功能已經完成。至于其他功能在後續使用時會進行開發
- Github位址:https://github.com/Thankson2020/SpringCloud-ChangGou
- 碼雲位址:https://gitee.com/thankson2020/SpringCloud-ChangGou