黑馬暢購商城---4.首頁高可用解決方案Lua+OpenResty+Nginx+Canal

學習目標

  • Lua介紹
  Lua語法 輸出、變量定義、數據類型、流程控制(if..)、循環操作、函數、表(數組)、模塊

 

  • OpenResty介紹(理解配置)
1
2
  封裝了Nginx,並且提供了Lua擴展,大大提升了Nginx對併發處理的能,10K-1000K
  Lua->廣告緩存操作

廣告緩存載入與讀取

  • Nginx講解1 2 3 限流操作:漏斗限流原理 1.控制速率 2.併發量控制

 

  • Canal講解
  實現數據同步操作->MySQL

 

  • Canal實現首頁緩存同步

1 首頁分析

首頁門戶系統需要展示各種各樣的廣告數據。如圖,以jd爲例:

變更頻率低的數據,如何提升訪問速度?

1
2
1.數據做成靜態頁[商品詳情頁]
2.做緩存[Redis]

基本的思路如下:

如上圖此種方式 簡單,直接通過數據庫查詢數據展示給用戶即可,但是通常情況下,首頁(門戶系統的流量一般非常的高)不適合直接通過mysql數據庫直接訪問的方式來獲取展示。

如下思路:

1.首先訪問nginx ,我們可以採用緩存的方式,先從nginx本地緩存中獲取,獲取到直接響應

2.如果沒有獲取到,再次訪問redis,我們可以從redis中獲取數據,如果有 則返回,並緩存到nginx中

3.如果沒有獲取到,再次訪問mysql,我們從mysql中獲取數據,再將數據存儲到redis中,返回。

而這裏面,我們都可以使用LUA腳本嵌入到程序中執行這些查詢相關的業務。

2 Lua(瞭解)

2.1 lua是什麼     

Lua [1] 是一個小巧的腳本語言。它是巴西里約熱內盧天主教大學(Pontifical Catholic University of Rio de Janeiro)裏的一個由Roberto Ierusalimschy、Waldemar Celes 和 Luiz Henrique de Figueiredo三人所組成的研究小組於1993年開發的。 其設計目的是爲了通過靈活嵌入應用程序中從而爲應用程序提供靈活的擴展和定製功能。Lua由標準C編寫而成,幾乎在所有操作系統和平臺上都可以編譯,運行。Lua並沒有提供強大的庫,這是由它的定位決定的。所以Lua不適合作爲開發獨立應用程序的語言。Lua 有一個同時進行的JIT項目,提供在特定平臺上的即時編譯功能。

簡單來說:

Lua 是一種輕量小巧的腳本語言,用標準C語言編寫並以源代碼形式開放, 其設計目的是爲了嵌入應用程序中,從而爲應用程序提供靈活的擴展和定製功能。

2.2 特性

  • 支持面向過程(procedure-oriented)編程和函數式編程(functional programming);
  • 自動內存管理;只提供了一種通用類型的表(table),用它可以實現數組,哈希表,集合,對象;
  • 語言內置模式匹配;閉包(closure);函數也可以看做一個值;提供多線程(協同進程,並非操作系統所支持的線程)支持;
  • 通過閉包和table可以很方便地支持面向對象編程所需要的一些關鍵機制,比如數據抽象,虛函數,繼承和重載等。

2.3 應用場景

  • 遊戲開發
  • 獨立應用腳本
  • Web 應用腳本
  • 擴展和數據庫插件如:MySQL Proxy 和 MySQL WorkBench
  • 安全系統,如入侵檢測系統
  • redis中嵌套調用實現類似事務的功能
  • web容器中應用處理一些過濾 緩存等等的邏輯,例如nginx。

2.4 lua的安裝

有linux版本的安裝也有mac版本的安裝。。我們採用linux版本的安裝,首先我們準備一個linux虛擬機。

安裝步驟,在linux系統中執行下面的命令。

1
2
3
4
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是否安裝成功

1
2
[root@localhost ~]# lua
Lua 5.1.4  Copyright (C) 1994-2008 Lua.org, PUC-Rio

2.5 入門程序

創建hello.lua文件,內容爲

編輯文件hello.lua

vi hello.lua

在文件中輸入:

print("hello");

保存並退出。

執行命令

lua hello.lua

輸出爲:

Hello

效果如下:

2.6 LUA的基本語法(瞭解)

lua有交互式編程和腳本式編程。

交互式編程就是直接輸入語法,就能執行。

腳本式編程需要編寫腳本,然後再執行命令 執行腳本纔可以。

一般採用腳本式編程。(例如:編寫一個hello.lua的文件,輸入文件內容,並執行lua hell.lua即可)

(1)交互式編程

Lua 提供了交互式編程模式。我們可以在命令行中輸入程序並立即查看效果。

Lua 交互式編程模式可以通過命令 lua -i 或 lua 來啓用:

lua -i

如下圖:

(2)腳本式編程

我們可以將 Lua 程序代碼保持到一個以 lua 結尾的文件,並執行,該模式稱爲腳本式編程,例如上面入門程序中將lua語法寫到hello.lua文件中。

2.6.1 註釋

一行註釋:兩個減號是單行註釋:

--

多行註釋:

1
2
3
4
--[[
 多行註釋
 多行註釋
 --]]

2.6.2 定義變量

全局變量,默認的情況下,定義一個變量都是全局變量,

如果要用局部變量 需要聲明爲local.例如:

1
2
3
4
-- 全局變量賦值
a=1
-- 局部變量賦值
local b=2 

如果變量沒有初始化:則 它的值爲nil 這和java中的null不同。

如下圖案例:

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 Lua 中的表(table)其實是一個“關聯數組”(associative arrays),數組的索引可以是數字、字符串或表類型。在 Lua 裏,table 的創建是通過“構造表達式”來完成,最簡單構造表達式是{},用來創建一個空表。

實例:

1
2
3
4
5
6
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 語句 由一個布爾表達式作爲條件判斷,其後緊跟其他語句組成。

語法:

1
2
3
4
if(布爾表達式)
then
   --[ 在布爾表達式爲 true 時執行的語句 --]
end

實例:

(2)if..else語句

Lua if 語句可以與 else 語句搭配使用, 在 if 條件表達式爲 false 時執行 else 語句代碼塊。

語法:

1
2
3
4
5
6
if(布爾表達式)
then
   --[ 布爾表達式爲 true 時執行該語句塊 --]
else
   --[ 布爾表達式爲 false 時執行該語句塊 --]
end

實例:

2.6.5 循環

學員完成

(1)while循環[==滿足條件就循環==]

Lua 編程語言中 while 循環語句在判斷條件爲 true 時會重複執行循環體語句。
語法:

1
2
3
4
while(condition)
do
   statements
end

實例:

1
2
3
4
5
6
a=10
while( a < 20 )
do
   print("a 的值爲:", a)
   a = a+1
end

效果如下:

(2)for循環

Lua 編程語言中 for 循環語句可以重複執行指定語句,重複次數可在 for 語句中控制。

語法: 1->10 1:exp1 10:exp2 2:exp3:遞增的數量

1
2
3
4
for var=exp1,exp2,exp3 
do  
    <執行體>  
end  

var 從 exp1 變化到 exp2,每次變化以 exp3 爲步長遞增 var,並執行一次 “執行體”。exp3 是可選的,如果不指定,默認爲1。

例子:

1
2
3
4
for i=1,9,2
do
   print(i)
end

for i=1,9,2:i=1從1開始循環,9循環數據到9結束,2每次遞增2

(3)repeat…until語句[==滿足條件結束==]

Lua 編程語言中 repeat…until 循環語句不同於 for 和 while循環,for 和 while 循環的條件語句在當前循環執行開始時判斷,而 repeat…until 循環的條件語句在當前循環結束後判斷。

語法:

1
2
3
repeat
   statements
until( condition )

案例:

2.6.6 函數

lua中也可以定義函數,類似於java中的方法。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
--[[ 函數返回兩個值的最大值 --]]
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))

執行之後的結果:

1
2
兩值比較最大值爲     10
兩值比較最大值爲     6

..:表示拼接

2.6.7 表

table 是 Lua 的一種數據結構用來幫助我們創建不同的數據類型,如:數組、字典等。

Lua也是通過table來解決模塊(module)、包(package)和對象(Object)的。

案例:

1
2
3
4
5
6
7
8
-- 初始化表
mytable = {}

-- 指定值
mytable[1]= "Lua"

-- 移除引用
mytable = nil

2.6.7 模塊

(1)模塊定義

模塊類似於一個封裝庫,從 Lua 5.1 開始,Lua 加入了標準的模塊管理機制,可以把一些公用的代碼放在一個文件裏,以 API 接口的形式在其他地方調用,有利於代碼的重用和降低代碼耦合度。

創建一個文件叫module.lua,在module.lua中創建一個獨立的模塊,代碼如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
-- 文件名爲 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 "<模塊名>"

兩種都可以。

我們可以將上面定義的module模塊引入使用,創建一個test_module.lua文件,代碼如下:

1
2
3
4
5
6
7
-- test_module.lua 文件
-- module 模塊爲上文提到到 module.lua
require("module")

print(module.constant)

module.func3()

3 OpenResty介紹

OpenResty(又稱:ngx_openresty) 是一個基於 nginx的可伸縮的 Web 平臺,由中國人章亦春發起,提供了很多高質量的第三方模塊。   

OpenResty 是一個強大的 Web 應用服務器,Web 開發人員可以使用 Lua 腳本語言調動 Nginx 支持的各種 C 以及 Lua 模塊,更主要的是在性能方面,OpenResty可以 快速構造出足以勝任 10K 以上併發連接響應的超高性能 Web 應用系統。

360,UPYUN,阿里雲,新浪,騰訊網,去哪兒網,酷狗音樂等都是 OpenResty 的深度用戶。

OpenResty 簡單理解成 就相當於封裝了nginx,並且集成了LUA腳本,開發人員只需要簡單的其提供了模塊就可以實現相關的邏輯,而不再像之前,還需要在nginx中自己編寫lua的腳本,再進行調用了。

3.1 安裝openresty

linux安裝openresty:

1.添加倉庫執行命令

1
2
 yum install yum-utils
 yum-config-manager --add-repo https://openresty.org/package/centos/openresty.repo

2.執行安裝

yum install openresty

3.安裝成功後 會在默認的目錄如下:

/usr/local/openresty

3.2 安裝nginx

默認已經安裝好了nginx,在目錄:/usr/local/openresty/nginx 下。

修改/usr/local/openresty/nginx/conf/nginx.conf,將配置文件使用的根設置爲root,目的就是將來要使用lua腳本的時候 ,直接可以加載在root下的lua腳本。

1
2
cd /usr/local/openresty/nginx/conf
vi nginx.conf

修改代碼如下:

3.3 測試訪問

重啓下centos虛擬機,然後訪問測試Nginx

訪問地址:http://192.168.211.132/

4.廣告緩存的載入與讀取

4.1 需求分析

需要在頁面上顯示廣告的信息

 

4.2 Lua+Nginx配置

(1)實現思路-查詢數據放入redis中

實現思路:

定義請求:用於查詢數據庫中的數據更新到redis中。

a.連接mysql ,按照廣告分類ID讀取廣告列表,轉換爲json字符串。

b.連接redis,將廣告列表json字符串存入redis 。

定義請求:

1
2
3
4
5
6
請求:
	/update_content
參數:
	id  --指定廣告分類的id
返回值:
	json

請求地址:<http://192.168.211.132/update_content?id=1>

創建/root/lua目錄,在該目錄下創建update_content.lua: 目的就是連接mysql 查詢數據 並存儲到redis中。

上圖代碼如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
ngx.header.content_type="application/json;charset=utf8"
local cjson = require("cjson")
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 = "192.168.211.132",
    port = 3306,
    database = "changgou_content",
    user = "root",
    password = "123456"
}

local res = db:connect(props)
local select_sql = "select url,pic from tb_content where status ='1' and category_id="..id.." order by sort_order"
res = db:query(select_sql)
db:close()

local redis = require("resty.redis")
local red = redis:new()
red:set_timeout(2000)

local ip ="192.168.211.132"
local port = 6379
red:connect(ip,port)
red:set("content_"..id,cjson.encode(res))
red:close()

ngx.say("{flag:true}")

修改/usr/local/openresty/nginx/conf/nginx.conf文件: 添加頭信息,和 location信息

代碼如下:

1
2
3
4
5
6
7
8
server {
    listen       80;
    server_name  localhost;

    location /update_content {
        content_by_lua_file /root/lua/update_content.lua;
    }
}

定義lua緩存命名空間,修改nginx.conf,添加如下代碼即可:

代碼如下:

lua_shared_dict dis_cache 128m;

請求<http://192.168.211.132/update_content?id=1>可以實現緩存的添加

(2)實現思路-從redis中獲取數據

實現思路:

定義請求,用戶根據廣告分類的ID 獲取廣告的列表。通過lua腳本直接從redis中獲取數據即可。

定義請求:

1
2
3
請求:/read_content
參數:id
返回值:json

在/root/lua目錄下創建read_content.lua:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
--設置響應頭類型
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("192.168.211.132", 6379)
--獲取key的值
local rescontent=red:get("content_"..id)
--輸出到返回響應中
ngx.say(rescontent)
--關閉連接
red:close()

在/usr/local/openresty/nginx/conf/nginx.conf中配置如下:

如圖:

代碼:

1
2
3
location /read_content {
     content_by_lua_file /root/lua/read_content.lua;
}

(3)加入openresty本地緩存

如上的方式沒有問題,但是如果請求都到redis,redis壓力也很大,所以我們一般採用多級緩存的方式來減少下游系統的服務壓力。參考基本思路圖的實現。

先查詢openresty本地緩存 如果 沒有

再查詢redis中的數據,如果沒有

再查詢mysql中的數據,但凡有數據 則返回即可。

修改read_content.lua文件,代碼如下:

上圖代碼如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
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 contentCache = cache_ngx:get('content_cache_'..id);

if contentCache == "" or contentCache == nil then
    local redis = require("resty.redis");
    local red = redis:new()
    red:set_timeout(2000)
    red:connect("192.168.211.132", 6379)
    local rescontent=red:get("content_"..id);

    if ngx.null == rescontent then
        local cjson = require("cjson");
        local mysql = require("resty.mysql");
        local db = mysql:new();
        db:set_timeout(2000)
        local props = {
            host = "192.168.211.132",
            port = 3306,
            database = "changgou_content",
            user = "root",
            password = "123456"
        }
        local res = db:connect(props);
        local select_sql = "select url,pic from tb_content where status ='1' and category_id="..id.." order by sort_order";
        res = db:query(select_sql);
        local responsejson = cjson.encode(res);
        red:set("content_"..id,responsejson);
        ngx.say(responsejson);
        db:close()
    else
        cache_ngx:set('content_cache_'..id, rescontent, 10*60);
        ngx.say(rescontent)
    end
    red:close()
else
    ngx.say(contentCache)
end

測試地址:http://192.168.211.132/update_content?id=1

此時會將分類ID=1的所有廣告查詢出來,並存入到Redis緩存。

測試地址:http://192.168.211.132/read_content?id=1

此時會獲取分類ID=1的所有廣告信息。

5 nginx限流

一般情況下,首頁的併發量是比較大的,即使 有了多級緩存,當用戶不停的刷新頁面的時候,也是沒有必要的,另外如果有惡意的請求 大量達到,也會對系統造成影響。

而限流就是保護措施之一。

5.1 生活中限流對比

  • 水壩泄洪,通過閘口限制洪水流量(控制流量速度)。

  • 辦理銀行業務:所有人先領號,各窗口叫號處理。每個窗口處理速度根據客戶具體業務而定,所有人排隊等待叫號即可。若快下班時,告知客戶明日再來(拒絕流量)

  • 火車站排隊買票安檢,通過排隊 的方式依次放入。(緩存帶處理任務)

5.2 nginx的限流

nginx提供兩種限流的方式:

  • 一是控制速率

  • 二是控制併發連接數

5.2.1 控制速率

控制速率的方式之一就是採用漏桶算法。

(1)漏桶算法實現控制速率限流

漏桶(Leaky Bucket)算法思路很簡單,水(請求)先進入到漏桶裏,漏桶以一定的速度出水(接口有響應速率),當水流入速度過大會直接溢出(訪問頻率超過接口響應速率),然後就拒絕請求,可以看出漏桶算法能強行限制數據的傳輸速率.示意圖如下:

(2)nginx的配置

配置示意圖如下:

修改/usr/local/openresty/nginx/conf/nginx.conf:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
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;
        }
    }
}

配置說明:

1
2
3
binary_remote_addr 是一種key,表示基於 remote_addr(客戶端IP) 來做限流,binary_ 的目的是壓縮內存佔用量。
zone:定義共享內存區來存儲訪問信息, contentRateLimit:10m 表示一個大小爲10M,名字爲contentRateLimit的內存區域。1M能存儲16000 IP地址的訪問信息,10M可以存儲16W IP地址訪問信息。
rate 用於設置最大訪問速率,rate=10r/s 表示每秒最多處理10個請求。Nginx 實際上以毫秒爲粒度來跟蹤請求信息,因此 10r/s 實際上是限制:每100毫秒處理一個請求。這意味着,自上一個請求處理完後,若後續100毫秒內又有請求到達,將拒絕處理該請求.我們這裏設置成2 方便測試。

測試:

重新加載配置文件

1
2
3
cd /usr/local/openresty/nginx/sbin

./nginx -s reload

訪問頁面:http://192.168.211.132/read_content?id=1 ,連續刷新會直接報錯。

(3)處理突發流量

上面例子限制 2r/s,如果有時正常流量突然增大,超出的請求將被拒絕,無法處理突發流量,可以結合 burst 參數使用來解決該問題。

例如,如下配置表示:

上圖代碼如下:

1
2
3
4
5
6
7
8
9
10
11
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;
        content_by_lua_file /root/lua/read_content.lua;
    }
}

burst 譯爲突發、爆發,表示在超過設定的處理速率後能額外處理的請求數,當 rate=10r/s 時,將1s拆成10份,即每100ms可處理1個請求。

此處,**burst=4 **,若同時有4個請求到達,Nginx 會處理第一個請求,剩餘3個請求將放入隊列,然後每隔500ms從隊列中獲取一個請求進行處理。若請求數大於4,將拒絕處理多餘的請求,直接返回503.

不過,單獨使用 burst 參數並不實用。假設 burst=50 ,rate依然爲10r/s,排隊中的50個請求雖然每100ms會處理一個,但第50個請求卻需要等待 50 * 100ms即 5s,這麼長的處理時間自然難以接受。

因此,burst 往往結合 nodelay 一起使用。

例如:如下配置:

1
2
3
4
5
6
7
8
9
10
11
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個請求的時候,沒有延遲,等到完成之後,按照正常的速率處理。

如上兩種配置結合就達到了速率穩定,但突然流量也能正常處理的效果。完整配置代碼如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
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;
        }
    }
}

測試:如下圖 在1秒鐘之內可以刷新4次,正常處理。

但是超過之後,連續刷新5次,拋出異常。

5.2.2 控制併發量(連接數)

ngx_http_limit_conn_module 提供了限制連接數的能力。主要是利用limit_conn_zone和limit_conn兩個指令。

利用連接數限制 某一個用戶的ip連接的數量來控制流量。

注意:並非所有連接都被計算在內 只有當服務器正在處理請求並且已經讀取了整個請求頭時,纔會計算有效連接。此處忽略測試。

配置語法:

1
2
3
Syntax:	limit_conn zone number;
Default: —;
Context: http, server, location;

(1)配置限制固定連接數

如下,配置如下:

上圖配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
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;
        }
    }
}

表示:

1
2
3
limit_conn_zone $binary_remote_addr zone=addr:10m;  表示限制根據用戶的IP地址來顯示,設置存儲地址爲的內存大小10M

limit_conn addr 2;   表示 同一個地址只允許連接2次。

測試:

此時開3個線程,測試的時候會發生異常,開2個就不會有異常

(2)限制每個客戶端IP與服務器的連接數,同時限制與虛擬服務器的連接總數。(瞭解)

如下配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
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可以用來監控數據庫數據的變化,從而獲得新增數據,或者修改的數據。

canal是應阿里巴巴存在杭州和美國的雙機房部署,存在跨機房同步的業務需求而提出的。

阿里系公司開始逐步的嘗試基於數據庫的日誌解析,獲取增量變更進行同步,由此衍生出了增量訂閱&消費的業務。

6.1 Canal工作原理

原理相對比較簡單:

  1. canal模擬mysql slave的交互協議,僞裝自己爲mysql slave,向mysql master發送dump協議
  2. mysql master收到dump請求,開始推送binary log給slave(也就是canal)
  3. canal解析binary log對象(原始爲byte流)

canal需要使用到mysql,我們需要先安裝mysql,給大家發的虛擬機中已經安裝了mysql容器,但canal是基於mysql的主從模式實現的,所以必須先開啓binlog.

6.2 開啓binlog模式

先使用docker 創建mysql容器,此處不再演示.

(1) 連接到mysql中,並修改/etc/mysql/mysql.conf.d/mysqld.cnf 需要開啓主 從模式,開啓binlog模式。

執行如下命令,編輯mysql配置文件

命令行如下:

1
2
3
docker exec -it mysql /bin/bash
cd /etc/mysql/mysql.conf.d
vi mysqld.cnf

修改mysqld.cnf配置文件,添加如下配置:

上圖配置如下:

1
2
log-bin/var/lib/mysql/mysql-bin
server-id=12345

(2) 創建賬號 用於測試使用,

使用root賬號創建用戶並授予權限

1
2
3
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 pull docker.io/canal/canal-server

容器安裝

docker run -p 11111:11111 --name canal -d docker.io/canal/canal-server

進入容器,修改核心配置canal.properties 和instance.properties,canal.properties 是canal自身的配置,instance.properties是需要同步數據的數據庫連接配置。

執行代碼如下:

1
2
3
4
5
docker exec -it canal /bin/bash
cd canal-server/conf/
vi canal.properties
cd example/
vi instance.properties

修改canal.properties的id,不能和mysql的server-id重複,如下圖:

修改instance.properties,配置數據庫連接地址:

這裏的canal.instance.filter.regex有多種配置,如下:

可以參考地址如下:

https://github.com/alibaba/canal/wiki/AdminGuide
1
2
3
4
5
6
7
8
9
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。

1
2
docker update --restart=always canal
docker restart canal

6.4 canal微服務搭建

當用戶執行 數據庫的操作的時候,binlog 日誌會被canal捕獲到,並解析出數據。我們就可以將解析出來的數據進行同步到redis中即可。

思路:創建一個獨立的程序,並監控canal服務器,獲取binlog日誌,解析數據,將數據更新到redis中。這樣廣告的數據就更新了。

(1)安裝輔助jar包

canal\spring-boot-starter-canal-master中有一個工程starter-canal,它主要提供了SpringBoot環境下canal的支持,我們需要先安裝該工程,在starter-canal目錄下執行mvn install,如下圖:

(2)canal微服務工程搭建

在changgou-service下創建changgou-service-canal工程,並引入相關配置。

pom.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<?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配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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類,實現對錶增刪改操作的監聽,代碼如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
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中創建啓動類,代碼如下:

1
2
3
4
5
6
7
8
9
@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配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<?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配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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)啓動類創建

1
2
3
4
5
6
7
8
9
@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查詢廣告數據,代碼如下:

1
2
3
4
5
6
/***
 * 根據categoryId查詢廣告集合
 * @param id
 * @return
 */
List<Content> findByCategory(Long id);

修改changgou-service-content的com.changgou.content.service.impl.ContentServiceImpl接口實現類,添加根據分類ID查詢廣告數據,代碼如下:

1
2
3
4
5
6
7
8
9
10
11
12
/***
 * 根據分類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查詢廣告數據,代碼如下:

1
2
3
4
5
6
7
8
9
/***
 * 根據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,代碼如下:

1
2
3
4
5
6
7
8
9
10
@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中,代碼如下:

上圖代碼如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
@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;
    }
}

 

 

測試:

修改數據庫數據,可以看到Redis中的緩存跟着一起變化

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章