第4章 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是否安裝成功
[root@localhost ~]# 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有交互式編程和腳本式編程。
交互式編程就是直接輸入語法,就能執行。
腳本式編程需要編寫腳本,然後再執行命令 執行腳本纔可以。
一般採用腳本式編程
(1)交互式編程
Lua 交互式編程模式可以通過命令 lua -i 或 lua 來啓用:
(2)腳本式編程
將 Lua 程序代碼保存到一個以 lua 結尾的文件並執行,該模式稱爲腳本式編程。如入門程序
2.6.1 註釋
-- 單行註釋
--[[
多行註釋
多行註釋
--]]
2.6.2 定義變量
默認的情況下,定義一個變量都是全局變量,如果要用局部變量需要聲明爲local。
-- 全局變量賦值
a=1
-- 局部變量賦值
local b=2
如果變量沒有初始化:則 它的值爲nil 這和java中的null不同。
如下圖案例:
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-qIrCtuDi-1585063109729)(images\1564436763084.png)]
2.6.3 Lua中的數據類型
Lua 是動態類型語言,變量不要類型定義,只需要爲變量賦值。 值可以存儲在變量中,作爲參數傳遞或結果返回。
Lua 中有 8 個基本類型分別爲:nil、boolean、number、string、userdata、function、thread 和 table。
數據類型 | 描述 |
---|---|
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) 模塊定義
模塊類似於一個封裝庫,從 Lua 5.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®介紹
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.1 安裝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.2 安裝nginx
默認已經安裝好了nginx,在目錄:/usr/local/openresty/nginx
下。
修改/usr/local/openresty/nginx/conf/nginx.conf
,將配置文件使用的根設置爲root,目的就是將來要使用lua腳本的時候 ,直接可以加載在root下的lua腳本。
cd /usr/local/openresty/nginx/conf
vi nginx.conf
修改代碼如下:
3.3 測試訪問
(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) 測試效果
執行命令
curl http://localhost:9000/
如下圖所示:
或者通過瀏覽器訪問:http://服務器公網IP:9000
4.廣告緩存的載入與讀取
4.1 需求分析
在首頁顯示廣告信息
4.2 Lua+Nginx配置
(1)查詢數據放入redis中
需求分析:
1、定義請求用於查詢數據庫中的數據更新到Redis中。
2、連接MySQL ,按照廣告分類ID讀取廣告列表。
3、連接Redis,將廣告信息存入Redis中。
功能實現:
1、請求地址:http://你的公網IP/update_advert?id=1
2、在/root/lua目錄下創建update_advert.lua腳本,實現連接MySQL數據庫,並將查詢到的數據保存到Redis中
腳本如下:
ngx.header.content_type="application/json;charset=utf8"
--引入JSON模塊
local cjson = require("cjson")
--引入MySQL模塊
local mysql = require("resty.mysql")
-- 獲取請求中的參數
local uri_args = ngx.req.get_uri_args()
local id = uri_args["id"]
--創建數據庫對象
local db = mysql:new()
--設置超時時間
db:set_timeout(1000)
--數據庫配置
local props = {
host = "你的公網IP",
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)
--關閉數據庫連接
db:close()
--引入redis模塊
local redis = require("resty.redis")
--創建Redis對象
local red = redis:new()
--設置超時時間
red:set_timeout(2000)
--Redis Ip
local ip ="你的公網IP"
--Redis 端口號
local port = 6379
--連接Redis
red:connect(ip,port)
--保存數據至Redis
red:set("content_"..id,cjson.encode(res))
--關閉Redis連接
red:close()
--打印
ngx.say(cjson.encode(res))
3、修改nginx.conf
修改location信息
server {
listen 80;
server_name localhost;
location /update_content {
content_by_lua_file /root/lua/update_advert.lua;
}
}
定義Lua緩存命名空間
lua_shared_dict dis_cache 128m;
效果如下:
測試:調用http://你的公網IP/update_advert?id=1
(2)從Redis中獲取數據
需求分析:
1、定義請求用於查詢Redis中的數據。
2、通過Lua腳本直接衝Redis中獲取數據
功能實現:
1、請求地址:http://你的公網IP/read_advert?id=1
2、在/root/lua目錄下創建read_advert.lua腳本,實現從Redis中獲取數據
腳本如下
--設置響應頭類型
ngx.header.content_type="application/json;charset=utf8"
--獲取請求中的參數ID
local uri_args = ngx.req.get_uri_args();
local id = uri_args["id"];
--引入redis庫
local redis = require("resty.redis");
--創建redis對象
local red = redis:new()
--設置超時時間
red:set_timeout(2000)
--連接
local ok, err = red:connect("你的公網IP", 6379)
--獲取key的值
local rescontent=red:get("content_"..id)
--輸出到返回響應中
ngx.say(rescontent)
--關閉連接
red:close()
3、修改nginx.conf
修改location信息
server {
location /read_advert{
content_by_lua_file /root/lua/read_advert.lua;
}
}
測試:調用http://你的公網IP/update_advert?id=1
(3)加入OpenResty本地緩存
如上的方式並沒有什麼問題,但請求都到Redis的話Redis的壓力會很大。所以我們採用多級緩存的方式來減少下游系統的服務壓力。
需求分析:
1、查詢OpenResty本地緩存,如果有則直接返回緩存中的數據
2、如果本地緩存中沒有數據,查詢Redis中的數據
3、如果Redis中沒有查詢到數據,那麼便查詢數據庫中的數據
功能實現:
1、修改read_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
測試地址:http://www.xiexun.top/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;
}
}
}
測試:
重新加載配置文件
訪問頁面:http://192.168.211.132/read_content?id=1
,連續刷新會直接報錯。
(3)處理突發流量
上面例子限制 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 工作原理
(1) mysql主從複製實現
-
master將改變記錄到二進制日誌(binary log)中。這些記錄叫做二進制日誌事件,binary log events,可以通過show binlog events進行查看
-
slave將master的binary log events拷貝到它的中繼日誌(relay log)
-
slave重做中繼日誌中的事件,將改變反映它自己的數據
(2)Canal的工作原理
-
canal模擬mysql slave的交互協議,僞裝自己爲mysql slave,向mysql master發送dump協議
-
mysql master收到dump請求,開始推送binary log給slave(也就是canal)
-
canal解析binary log對象(原始爲byte流)
6.2 開啓binlog模式
(1) 連接到mysql中,並修改/etc/mysql/mysql.conf.d/mysqld.cnf 需要開啓主從模式,開啓binlog模式。
編輯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
(2) 創建賬號 用於測試使用,
使用root賬號創建用戶並授予權限
create user canal@'%' IDENTIFIED by 'canal';
GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT,SUPER ON *.* TO 'canal'@'%';
FLUSH PRIVILEGES;
(3)重啓mysql容器
docker restart mysql
6.3 canal容器安裝
下載鏡像:
docker canal/canal-server:v1.1.4
容器安裝
docker run -p 11111:11111 --name canal -d canal/canal-server:v1.1.4
進入容器,修改核心配置canal.properties 和instance.properties,canal.properties 是canal自身的配置,instance.properties是需要同步數據的數據庫連接配置。
執行代碼如下:
docker exec -it canal /bin/bash
cd canal-server/conf/
vi canal.properties
cd example/
vi instance.properties
修改canal.properties的id,不能和mysql的server-id重複,如下圖:
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-BUw8atpz-1585063109736)(images\1560814792482.png)]
修改instance.properties,配置數據庫連接地址:
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-OfCapPNF-1585063109737)(images\1560814968391.png)]
這裏的canal.instance.filter.regex
有多種配置,如下:
可以參考地址如下:
https://github.com/alibaba/canal/wiki/AdminGuide
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 update --restart=always canal
docker restart canal
6.4 canal微服務搭建
當用戶執行 數據庫的操作的時候,binlog 日誌會被canal捕獲到,並解析出數據。我們就可以將解析出來的數據進行同步到Redis中即可。
思路:創建一個canal微服務,獲取binlog日誌,解析數據,將數據更新到redis中。這樣廣告的數據就更新了。
6.4.1 安裝輔助jar包
後續將會提供。亦可github上下載springboot-start-canal源碼進行安裝,安裝方式自行百度
6.4.1 canal微服務工程搭建
在changgou-service下創建changgou-service-canal工程,並引入相關配置。
pom.xml配置
<?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>changgou-service</artifactId>
<groupId>com.changgou</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>changgou-service-canal</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!--canal依賴-->
<dependency>
<groupId>com.xpand</groupId>
<artifactId>starter-canal</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
</dependencies>
</project>
application.yml配置
server:
port: 18082
spring:
application:
name: canal
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:7001/eureka
instance:
prefer-ip-address: true
feign:
hystrix:
enabled: true
#hystrix 配置
hystrix:
command:
default:
execution:
timeout:
#如果enabled設置爲false,則請求超時交給ribbon控制
enabled: true
isolation:
strategy: SEMAPHORE
#canal配置
canal:
client:
instances:
example:
host: 192.168.211.132
port: 11111
(3)監聽創建
創建一個CanalDataEventListener類,實現對錶增刪改操作的監聽,代碼如下:
package com.changgou.canal.listener;
import com.alibaba.otter.canal.protocol.CanalEntry;
import com.xpand.starter.canal.annotation.*;
@CanalEventListener
public class CanalDataEventListener {
/***
* 增加數據監聽
* @param eventType
* @param rowData
*/
@InsertListenPoint
public void onEventInsert(CanalEntry.EventType eventType, CanalEntry.RowData rowData) {
rowData.getAfterColumnsList().forEach((c) -> System.out.println("By--Annotation: " + c.getName() + " :: " + c.getValue()));
}
/***
* 修改數據監聽
* @param rowData
*/
@UpdateListenPoint
public void onEventUpdate(CanalEntry.RowData rowData) {
System.out.println("UpdateListenPoint");
rowData.getAfterColumnsList().forEach((c) -> System.out.println("By--Annotation: " + c.getName() + " :: " + c.getValue()));
}
/***
* 刪除數據監聽
* @param eventType
*/
@DeleteListenPoint
public void onEventDelete(CanalEntry.EventType eventType) {
System.out.println("DeleteListenPoint");
}
/***
* 自定義數據修改監聽
* @param eventType
* @param rowData
*/
@ListenPoint(destination = "example", schema = "changgou_content", table = {"tb_content_category", "tb_content"}, eventType = CanalEntry.EventType.UPDATE)
public void onEventCustomUpdate(CanalEntry.EventType eventType, CanalEntry.RowData rowData) {
System.err.println("DeleteListenPoint");
rowData.getAfterColumnsList().forEach((c) -> System.out.println("By--Annotation: " + c.getName() + " :: " + c.getValue()));
}
}
(4)啓動類創建
在com.changgou中創建啓動類,代碼如下:
@SpringBootApplication(exclude={DataSourceAutoConfiguration.class})
@EnableEurekaClient
@EnableCanalClient
public class CanalApplication {
public static void main(String[] args) {
SpringApplication.run(CanalApplication.class,args);
}
}
(5)測試
啓動canal微服務,然後修改任意數據庫的表數據,canal微服務後臺輸出如下:
6.5 廣告同步
如上圖,每次執行廣告操作的時候,會記錄操作日誌到,然後將操作日誌發送給canal,canal將操作記錄發送給canal微服務,canal微服務根據修改的分類ID調用content微服務查詢分類對應的所有廣告,canal微服務再將所有廣告存入到Redis緩存。
6.5.1 content微服務搭建
在changgou-service中搭建changgou-service-content微服務,對應的dao、service、controller、pojo由代碼生成器生成。
首先在changgou-service-api中創建changgou-service-content-api,將pojo拷貝到API工程中,如下圖:
(1)pom.xml配置
<?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>changgou-service</artifactId>
<groupId>com.changgou</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>changgou-service-content</artifactId>
<dependencies>
<dependency>
<groupId>com.changgou</groupId>
<artifactId>changgou-common</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.changgou</groupId>
<artifactId>changgou-service-content-api</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
</project>
(2)application.yml配置
server:
port: 18084
spring:
application:
name: content
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://192.168.211.132:3306/changgou_content?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTC
username: root
password: 123456
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:7001/eureka
instance:
prefer-ip-address: true
feign:
hystrix:
enabled: true
mybatis:
configuration:
map-underscore-to-camel-case: true #開啓駝峯功能
#hystrix 配置
hystrix:
command:
default:
execution:
timeout:
#如果enabled設置爲false,則請求超時交給ribbon控制
enabled: true
isolation:
strategy: SEMAPHORE
(3)啓動類創建
@SpringBootApplication
@EnableEurekaClient
@MapperScan(basePackages = {"com.changgou.content.dao"})
public class ContentApplication {
public static void main(String[] args) {
SpringApplication.run(ContentApplication.class);
}
}
6.5.2 廣告查詢
在content微服務中,添加根據分類查詢廣告。
(1)業務層
修改changgou-service-content的com.changgou.content.service.ContentService接口,添加根據分類ID查詢廣告數據,代碼如下:
/***
* 根據categoryId查詢廣告集合
* @param id
* @return
*/
List<Content> findByCategory(Long id);
修改changgou-service-content的com.changgou.content.service.impl.ContentServiceImpl接口實現類,添加根據分類ID查詢廣告數據,代碼如下:
/***
* 根據分類ID查詢
* @param id
* @return
*/
@Override
public List<Content> findByCategory(Long id) {
Content content = new Content();
content.setCategoryId(id);
content.setStatus("1");
return contentMapper.select(content);
}
(2)控制層
修改changgou-service-content的com.changgou.content.controller.ContentController,添加根據分類ID查詢廣告數據,代碼如下:
/***
* 根據categoryId查詢廣告集合
*/
@GetMapping(value = "/list/category/{id}")
public Result<List<Content>> findByCategory(@PathVariable Long id){
//根據分類ID查詢廣告集合
List<Content> contents = contentService.findByCategory(id);
return new Result<List<Content>>(true,StatusCode.OK,"查詢成功!",contents);
}
(3)feign配置
在changgou-service-content-api工程中添加feign,代碼如下:
@FeignClient(name="content")
@RequestMapping(value = "/content")
public interface ContentFeign {
/***
* 根據分類ID查詢所有廣告
*/
@GetMapping(value = "/list/category/{id}")
Result<List<Content>> findByCategory(@PathVariable Long id);
}
6.5.3 同步實現
在canal微服務中修改如下:
(1)配置redis
修改application.yml配置文件,添加redis配置,如下代碼:
(2)啓動類中開啓feign
修改CanalApplication,添加@EnableFeignClients
註解,代碼如下:
(3)同步實現
修改監聽類CanalDataEventListener,實現監聽廣告的增刪改,並根據增刪改的數據使用feign查詢對應分類的所有廣告,將廣告存入到Redis中,代碼如下:
上圖代碼如下:
@CanalEventListener
public class CanalDataEventListener {
@Autowired
private ContentFeign contentFeign;
//字符串
@Autowired
private StringRedisTemplate stringRedisTemplate;
//自定義數據庫的 操作來監聽
//destination = "example"
@ListenPoint(destination = "example",
schema = "changgou_content",
table = {"tb_content", "tb_content_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<Content>> categoryresut = contentFeign.findByCategory(Long.valueOf(categoryId));
List<Content> data = categoryresut.getData();
//3.使用redisTemplate存儲到redis中
stringRedisTemplate.boundValueOps("content_" + categoryId).set(JSON.toJSONString(data));
}
private String getColumnValue(CanalEntry.EventType eventType, CanalEntry.RowData rowData) {
String categoryId = "";
//判斷 如果是刪除 則獲取beforlist
if (eventType == CanalEntry.EventType.DELETE) {
for (CanalEntry.Column column : rowData.getBeforeColumnsList()) {
if (column.getName().equalsIgnoreCase("category_id")) {
categoryId = column.getValue();
return categoryId;
}
}
} else {
//判斷 如果是添加 或者是更新 獲取afterlist
for (CanalEntry.Column column : rowData.getAfterColumnsList()) {
if (column.getName().equalsIgnoreCase("category_id")) {
categoryId = column.getValue();
return categoryId;
}
}
}
return categoryId;
}
}