目錄
一、整體架構描述
1.1 方案v1.0
在優化舊的設計框架前,先看一下常見的查詢請求處理
步驟如下:
- ① 請求到 Nginx 或 restApi 服務後, 向 Redis 請求數據
- ②、③ Redis 查看緩存中是否有數據,若有數據則直接進入流程⑥,沒有則進入流程④
- ④ 、⑤ 查詢數據庫數據,並更新數據到 Redis ,以免下次還需要訪問數據庫
- ⑥ 返回數據給用戶
1.2 方案v1.0優化
在方案v1.0,存在了大量簡單的請求,如在IM軟件中查詢用戶/羣組信息、在商城項目中查詢商品信息等,這些簡單的查詢卻頻繁的查詢無疑會對服務造成一定壓力。那麼是否有辦法進行優化呢。
對方案v1.0進行優化後,方案v2.0結合了 OpenResty、Lua、Redis 實現了二級緩存。利用 Nginx 高併發的特性,使得上述這些請求無需再通過 restAPI 服務,而是通過Lua腳本直接查詢和操作 redis 和 mysql,降低服務壓力。
步驟如下:
- ①、② 請求到 Nginx 後,Nginx 先查詢 Nginx-Cache,若緩存中沒有數據,則調用對應的 Lua 腳本
- ③、④ Lua 腳本查詢 Redis,若Redis 緩存中有數據則直接返回並更新Nginx緩存, 沒有數據則調用 Lua腳本,查詢數據庫
- ⑤ 查詢數據庫數據並更新Redis緩存
注意:
方案v2.0 採用的是逐級緩存的方式:
- 第一次訪問:nginx 和 redis 中均沒有緩存,數據從數據庫中查出,並存入 redis 緩存
- 第二次訪問:nginx 中沒有緩存,數據從 redis 中查出,並存入 nginx 緩存
- 第三次訪問:數據從 nginx 中查出
這樣做有以下原因:
- nginx 緩存過期時間小於 redis 緩存過期時間,有利於降低 redis 雪崩的危險
- 若同時對 nginx 和 redis 設置緩存,則數據訪問到的都是 nginx。當數據庫變化時,無法在不訪問數據庫的情況下實現快速響應。解決方案爲使用 Canal 實現數據庫和 redis 的數據同步,將 nginx 的有效時間設置得更短,做到防止大量併發請求到數據庫的同時,又能拿到最新數據
1.3 方案對比
根據不同的項目需求和實際方案,選擇使用哪一種方案。
方案v1.0 | 方案v2.0 | |
靈活性 | 訪問 redis 和數據庫由 restApi 控制,可處理複雜請求。但如果有修改必須重啓服務 |
利用 Nginx 高併發特性處理大量簡單請求,且修改腳本無需重啓後端服務,只要重新加載 Nginx 即可。 但只能處理簡單的請求,且需要有特殊的識別方式,如加特定的 API |
併發性 | 高 | 高,相對來說肯定比方案v1.0高,但要根據實際情況使用,而不是爲了炫技而部署 |
部署難度 | 簡單 | 中等。需要學習 OpenResty框架和Lua語言 |
適用場景 | 通用場景 |
更適用於存在大量簡單的查詢請求的項目,如倉庫管理系統、商城系統等。 同時這個方案還可以處理權限控制,如直接在 Nginx 拒絕不攜帶 token 的請求,或進行 token 驗證等。 |
二、方案2.0部署
2.1 OpenResty 安裝
安裝流程:https://blog.csdn.net/qq_34416331/article/details/106421783
2.2 Lua腳本編寫
Lua 的基本用法:https://blog.csdn.net/qq_34416331/article/details/106419100
Lua 腳本編寫:
# 創建存放 lua 腳本的文件夾,名字自定義
mkdir /usr/local/lua_conf
# 進入文件夾
cd /usr/local/lua_conf
# 創建 lua 腳本
vim read_conf.lua
ngx.header.content_type="application/json;charset=utf8"
local uri_args = ngx.req.get_uri_args();
local id = uri_args["id"];
-- 加載nginx緩存模塊
local cache_ngx = ngx.shared.dis_cache;
-- 根據ID獲取本地緩存數據
local contentCache = cache_ngx:get('content_cache_'..id);
-- 獲取IP信息,可刪除
-- [[
local headers=ngx.header;
local ip=headers["X-REAL-IP"] or headers["X_FORWARDED_FOR"] or ngx.var.remote_addr or "0.0.0.0"
ngx.say(ip)
ngx.say(contentCache)
]]--
-- 若nginx中沒有相應的緩存信息
if contentCache == "" or contentCache == nil then
-- 獲取redis模塊
local redis = require("resty.redis");
local red = redis:new()
red:set_timeout(2000)
red:connect("192.168.47.142", 6379)
local rescontent=red:get("content_"..id);
-- 若redis模塊也沒有這個信息
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.47.142",
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);
-- 存儲到redis中
red:set("content_"..id,responsejson);
-- 返回數據
ngx.say(responsejson);
-- 關閉數據庫連接
db:close()
else
-- 若redis中有緩存,則設置的到nginx緩存中,並返回
-- 2*60 表示設置 nginx 緩存時間爲2分鐘,應根據實際情況修改
cache_ngx:set('content_cache_'..id, rescontent, 2*60);
ngx.say(rescontent)
end
-- 關閉redis連接
red:close()
else
-- 若nginx中有對應的信息,則返回
ngx.say(contentCache)
end
2.3 配置 OpenResty
要讓 OpenResty 來使用 Lua 腳本,只需要配置 Nginx 的配置文件即可。
### 修改 Nginx 配置文件 ###
# 進入 OpenResty 自帶的 Nginx 目錄
cd /usr/local/openresty/nginx/conf
# 編輯配置文件
vim nginx.conf
lua_shared_dict 的作用是聲明一個共享內存區域 name,以充當基於 Lua 字典的共享存儲。簡單來說就是當nginx運行時的 lua 腳本緩存空間大小
在http中配置要訪問的接口:
重啓 nginx
# 重新加載 nginx
/usr/local/openresty/nginx/sbin/nginx -s reload
三、測試
修改 read_content.lua
ngx.header.content_type="application/json;charset=utf8"
local uri_args = ngx.req.get_uri_args();
local id = uri_args["id"];
-- 加載nginx緩存模塊
local cache_ngx = ngx.shared.dis_cache;
-- 根據ID獲取本地緩存數據
local contentCache = cache_ngx:get('content_cache_'..id);
--[[
local headers=ngx.header;
local ip=headers["X-REAL-IP"] or headers["X_FORWARDED_FOR"] or ngx.var.remote_addr or "0.0.0.0"
ngx.say(ip)
ngx.say(contentCache)
]]--
if contentCache==nil then
ngx.say("nginx緩存爲空")
else
ngx.say("從nginx緩存中查到了數據")
end
-- 若nginx中沒有相應的緩存信息
if contentCache == "" or contentCache == nil then
-- 獲取redis模塊
local redis = require("resty.redis");
local red = redis:new()
red:set_timeout(2000)
red:connect("192.168.47.142", 6379)
local rescontent=red:get("content_"..id);
if ngx.null == rescontent then
ngx.say("redis爲空")
else
ngx.say("nginx爲空,從redis中查到了數據")
end
-- 若redis模塊也沒有這個信息
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.47.142",
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);
-- 存儲到redis中
red:set("content_"..id,responsejson);
-- 返回數據
ngx.say(responsejson);
-- 關閉數據庫連接
db:close()
else
-- 若redis中有緩存,則設置的到nginx緩存中,並返回
cache_ngx:set('content_cache_'..id, rescontent, 30);
end
-- 關閉redis連接
red:close()
else
-- 若nginx中有對應的信息,則返回
ngx.say(contentCache)
end
清理redis
訪問:
第一次訪問
第二次訪問:
第三次訪問: