国产 无码 综合区,色欲AV无码国产永久播放,无码天堂亚洲国产AV,国产日韩欧美女同一区二区

黑馬Redis視頻教程高級(jí)篇(二:多級(jí)緩存)

這篇具有很好參考價(jià)值的文章主要介紹了黑馬Redis視頻教程高級(jí)篇(二:多級(jí)緩存)。希望對(duì)大家有所幫助。如果存在錯(cuò)誤或未考慮完全的地方,請大家不吝賜教,您也可以點(diǎn)擊"舉報(bào)違法"按鈕提交疑問。

目錄

一、什么是多級(jí)緩存?

二、JVM進(jìn)程緩存

2.1、導(dǎo)入案例

2.2、初識(shí)Caffeine

2.3、實(shí)現(xiàn)JVM進(jìn)程緩存

2.3.1、需求

2.3.2、實(shí)現(xiàn)

三、Lua語法入門

3.1、初識(shí)Lua

3.2、HelloWord

3.3、變量和循環(huán)

3.3.1、Lua的數(shù)據(jù)類型

3.3.2、聲明變量

3.3.3、循環(huán)

3.4、條件控制、函數(shù)

3.4.1、函數(shù)

3.4.2、條件控制

3.4.3、案例

四、實(shí)現(xiàn)多級(jí)緩存

4.1、安裝OpenResty

4.2、OpenResty快速入門

4.2.1、反向代理流程

4.2.2、OpenResty監(jiān)聽請求

4.2.3、編寫item.lua

4.3、請求參數(shù)處理

4.3.1、獲取參數(shù)的API

4.3.2、獲取參數(shù)并返回

4.4、查詢Tomcat

4.4.1、發(fā)送http請求的API

4.4.2、封裝http工具

4.4.3、CJSON工具類

4.4.4、實(shí)現(xiàn)Tomcat查詢

4.4.5、基于ID負(fù)載均衡

4.5、Redis緩存預(yù)熱

4.6、查詢Redis緩存

4.6.1、封裝Redis工具

4.6.2、實(shí)現(xiàn)Redis查詢

4.7、Nginx本地緩存

4.7.1、本地緩存API

4.7.2、實(shí)現(xiàn)本地緩存查詢

五、緩存同步

5.1、數(shù)據(jù)同步策略

5.2、安裝Canal

5.2.1、認(rèn)識(shí)Canal

5.2.2、安裝Canal

5.3、監(jiān)聽Canal

5.3.1、引入依賴

5.3.2、yml編寫配置

5.3.3、修改Item實(shí)體類

5.3.4、編寫監(jiān)聽器


一、什么是多級(jí)緩存?

傳統(tǒng)的緩存策略一般是請求到達(dá)Tomcat后,先查詢Redis,如果未命中則查詢數(shù)據(jù)庫,如圖:

黑馬Redis視頻教程高級(jí)篇(二:多級(jí)緩存)

存在下面的問題:

  • 請求要經(jīng)過Tomcat處理,Tomcat的性能成為整個(gè)系統(tǒng)的瓶頸。
  • Redis緩存失效時(shí),會(huì)對(duì)數(shù)據(jù)庫產(chǎn)生沖擊。

多級(jí)緩存就是充分利用請求處理的每個(gè)環(huán)節(jié),分別添加緩存,減輕Tomcat壓力,提升服務(wù)性能:

  • 瀏覽器訪問靜態(tài)資源時(shí),優(yōu)先讀取瀏覽器本地緩存

  • 訪問非靜態(tài)資源(ajax查詢數(shù)據(jù))時(shí),訪問服務(wù)端

  • 請求到達(dá)Nginx后,優(yōu)先讀取Nginx本地緩存

  • 如果Nginx本地緩存未命中,則去直接查詢Redis(不經(jīng)過Tomcat)

  • 如果Redis查詢未命中,則查詢Tomcat

  • 請求進(jìn)入Tomcat后,優(yōu)先查詢JVM進(jìn)程緩存

  • 如果JVM進(jìn)程緩存未命中,則查詢數(shù)據(jù)庫

黑馬Redis視頻教程高級(jí)篇(二:多級(jí)緩存)

在多級(jí)緩存架構(gòu)中,Nginx內(nèi)部需要編寫本地緩存查詢、Redis查詢、Tomcat查詢的業(yè)務(wù)邏輯,因此這樣的nginx服務(wù)不再是一個(gè)反向代理服務(wù)器,而是一個(gè)編寫業(yè)務(wù)的Web服務(wù)器了。

因此這樣的業(yè)務(wù)Nginx服務(wù)也需要搭建集群來提高并發(fā),再有專門的nginx服務(wù)來做反向代理,如圖:

黑馬Redis視頻教程高級(jí)篇(二:多級(jí)緩存)

另外,我們的Tomcat服務(wù)將來也會(huì)部署為集群模式:

黑馬Redis視頻教程高級(jí)篇(二:多級(jí)緩存)

可見,多級(jí)緩存的關(guān)鍵有兩個(gè):

  • 一個(gè)是在nginx中編寫業(yè)務(wù),實(shí)現(xiàn)nginx本地緩存、Redis、Tomcat的查詢

  • 另一個(gè)就是在Tomcat中實(shí)現(xiàn)JVM進(jìn)程緩存

其中Nginx編程則會(huì)用到OpenResty框架結(jié)合Lua這樣的語言。

二、JVM進(jìn)程緩存

為了演示多級(jí)緩存的案例,我們先準(zhǔn)備一個(gè)商品查詢的業(yè)務(wù)。

2.1、導(dǎo)入案例

參考我的另一篇文章:案例導(dǎo)入說明

2.2、初識(shí)Caffeine

緩存在日常開發(fā)中啟動(dòng)至關(guān)重要的作用,由于是存儲(chǔ)在內(nèi)存中,數(shù)據(jù)的讀取速度是非??斓模艽罅繙p少對(duì)數(shù)據(jù)庫的訪問,減少數(shù)據(jù)庫的壓力。我們把緩存分為兩類:

  • 分布式緩存,例如Redis:

    • 優(yōu)點(diǎn):存儲(chǔ)容量更大、可靠性更好、可以在集群間共享

    • 缺點(diǎn):訪問緩存有網(wǎng)絡(luò)開銷

    • 場景:緩存數(shù)據(jù)量較大、可靠性要求較高、需要在集群間共享

  • 進(jìn)程本地緩存,例如HashMap、GuavaCache:

    • 優(yōu)點(diǎn):讀取本地內(nèi)存,沒有網(wǎng)絡(luò)開銷,速度更快

    • 缺點(diǎn):存儲(chǔ)容量有限、可靠性較低(Tomcat重啟或者宕機(jī),數(shù)據(jù)就丟失了)、無法共享

    • 場景:性能要求較高,緩存數(shù)據(jù)量較小

我們今天會(huì)利用Caffeine框架來實(shí)現(xiàn)JVM進(jìn)程緩存。

Caffeine是一個(gè)基于Java8開發(fā)的,提供了近乎最佳命中率的高性能的本地緩存庫。目前Spring內(nèi)部的緩存使用的就是Caffeine。GitHub地址:GitHub - ben-manes/caffeine: A high performance caching library for Java

Caffeine的性能非常好,下圖是官方給出的性能對(duì)比:

黑馬Redis視頻教程高級(jí)篇(二:多級(jí)緩存)

可以看到Caffeine的性能遙遙領(lǐng)先!

緩存使用的基本API:

@Test
void testBasicOps() {
    // 構(gòu)建cache對(duì)象
    Cache<String, String> cache = Caffeine.newBuilder().build();

    // 存數(shù)據(jù)
    cache.put("gf", "迪麗熱巴");

    // 取數(shù)據(jù)
    String gf = cache.getIfPresent("gf");
    System.out.println("gf = " + gf);

    // 取數(shù)據(jù),包含兩個(gè)參數(shù):
    // 參數(shù)一:緩存的key
    // 參數(shù)二:Lambda表達(dá)式,表達(dá)式參數(shù)就是緩存的key,方法體是查詢數(shù)據(jù)庫的邏輯
    // 優(yōu)先根據(jù)key查詢JVM緩存,如果未命中,則執(zhí)行參數(shù)二的Lambda表達(dá)式
    String defaultGF = cache.get("defaultGF", key -> {
        // 根據(jù)key去數(shù)據(jù)庫查詢數(shù)據(jù)
        return "柳巖";
    });
    System.out.println("defaultGF = " + defaultGF);
}

Caffeine既然是緩存的一種,肯定需要有緩存的清除策略,不然的話內(nèi)存總會(huì)有耗盡的時(shí)候。

Caffeine提供了三種緩存驅(qū)逐策略:

  • 基于容量:設(shè)置緩存的數(shù)量上限

// 創(chuàng)建緩存對(duì)象
Cache<String, String> cache = Caffeine.newBuilder()
    .maximumSize(1) // 設(shè)置緩存大小上限為1,意思就是緩存里最多存1個(gè)key
    .build();
  • 基于時(shí)間:設(shè)置緩存的有效時(shí)間
// 創(chuàng)建緩存對(duì)象
Cache<String, String> cache = Caffeine.newBuilder()
    // 設(shè)置緩存有效期為 10 秒,從最后一次寫入開始計(jì)時(shí) 
    .expireAfterWrite(Duration.ofSeconds(10)) 
    .build();
  • 基于引用:設(shè)置緩存為軟引用或弱引用,利用GC來回收緩存數(shù)據(jù)。性能較差,不建議使用。

注意:在默認(rèn)情況下,當(dāng)一個(gè)緩存元素過期的時(shí)候,Caffeine不會(huì)自動(dòng)立即將其清理和驅(qū)逐。而是在一次讀或?qū)懖僮骱螅蛘咴诳臻e時(shí)間完成對(duì)失效數(shù)據(jù)的驅(qū)逐。

2.3、實(shí)現(xiàn)JVM進(jìn)程緩存

2.3.1、需求

利用Caffeine實(shí)現(xiàn)下列需求:

  • 給根據(jù)id查詢商品的業(yè)務(wù)添加緩存,緩存未命中時(shí)查詢數(shù)據(jù)庫

  • 給根據(jù)id查詢商品庫存的業(yè)務(wù)添加緩存,緩存未命中時(shí)查詢數(shù)據(jù)庫

  • 緩存初始大小為100

  • 緩存上限為10000

2.3.2、實(shí)現(xiàn)

首先,我們需要定義兩個(gè)Caffeine的緩存對(duì)象,分別保存商品、庫存的緩存數(shù)據(jù)。

在item-service的com.heima.item.config包下定義CaffeineConfig類:

package com.heima.item.config;

import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.heima.item.pojo.Item;
import com.heima.item.pojo.ItemStock;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class CaffeineConfig {

    @Bean
    public Cache<Long, Item> itemCache(){
        return Caffeine.newBuilder()
                .initialCapacity(100)
                .maximumSize(10_000)
                .build();
    }

    @Bean
    public Cache<Long, ItemStock> stockCache(){
        return Caffeine.newBuilder()
                .initialCapacity(100)
                .maximumSize(10_000)
                .build();
    }
}

然后,修改item-service中的com.heima.item.web包下的ItemController類,添加緩存邏輯:

@RestController
@RequestMapping("item")
public class ItemController {

    @Autowired
    private IItemService itemService;
    @Autowired
    private IItemStockService stockService;

    @Autowired
    private Cache<Long, Item> itemCache;
    @Autowired
    private Cache<Long, ItemStock> stockCache;
    
    // ...其它略
    
    @GetMapping("/{id}")
    public Item findById(@PathVariable("id") Long id) {
        return itemCache.get(id, key -> itemService.query()
                .ne("status", 3).eq("id", key)
                .one()
        );
    }

    @GetMapping("/stock/{id}")
    public ItemStock findStockById(@PathVariable("id") Long id) {
        return stockCache.get(id, key -> stockService.getById(key));
    }
}

三、Lua語法入門

Nginx編程需要用到Lua語言,因此我們必須先入門Lua的基本語法。

3.1、初識(shí)Lua

Lua 是一種輕量小巧的腳本語言,用標(biāo)準(zhǔn)C語言編寫并以源代碼形式開放, 其設(shè)計(jì)目的是為了嵌入應(yīng)用程序中,從而為應(yīng)用程序提供靈活的擴(kuò)展和定制功能。官網(wǎng):The Programming Language Lua/

黑馬Redis視頻教程高級(jí)篇(二:多級(jí)緩存)

Lua經(jīng)常嵌入到C語言開發(fā)的程序中,例如游戲開發(fā)、游戲插件等。

Nginx本身也是C語言開發(fā),因此也允許基于Lua做拓展。

3.2、HelloWord

CentOS7默認(rèn)已經(jīng)安裝了Lua語言環(huán)境,所以可以直接運(yùn)行Lua代碼。

1)在Linux虛擬機(jī)的任意目錄下,新建一個(gè)hello.lua文件

黑馬Redis視頻教程高級(jí)篇(二:多級(jí)緩存)

2)添加下面的內(nèi)容

print("Hello World!")

3)運(yùn)行

黑馬Redis視頻教程高級(jí)篇(二:多級(jí)緩存)

3.3、變量和循環(huán)

學(xué)習(xí)任何語言必然離不開變量,而變量的聲明必須先知道數(shù)據(jù)的類型。

3.3.1、Lua的數(shù)據(jù)類型

Lua中支持的常見數(shù)據(jù)類型包括:

黑馬Redis視頻教程高級(jí)篇(二:多級(jí)緩存)

另外,Lua提供了type()函數(shù)來判斷一個(gè)變量的數(shù)據(jù)類型:

黑馬Redis視頻教程高級(jí)篇(二:多級(jí)緩存)

3.3.2、聲明變量

Lua聲明變量的時(shí)候無需指定數(shù)據(jù)類型,而是用local來聲明變量為局部變量:

-- 聲明字符串,可以用單引號(hào)或雙引號(hào),
local str = 'hello'
-- 字符串拼接可以使用 ..
local str2 = 'hello' .. 'world'
-- 聲明數(shù)字
local num = 21
-- 聲明布爾類型
local flag = true

Lua中的table類型既可以作為數(shù)組,又可以作為Java中的map來使用。數(shù)組就是特殊的table,key是數(shù)組角標(biāo)而已:

-- 聲明數(shù)組 ,key為角標(biāo)的 table
local arr = {'java', 'python', 'lua'}
-- 聲明table,類似java的map
local map =  {name='Jack', age=21}

Lua中的數(shù)組角標(biāo)是從1開始,訪問的時(shí)候與Java中類似:

-- 訪問數(shù)組,lua數(shù)組的角標(biāo)從1開始
print(arr[1])

Lua中的table可以用key來訪問:

-- 訪問table
print(map['name'])
print(map.name)

3.3.3、循環(huán)

對(duì)于table,我們可以利用for循環(huán)來遍歷。不過數(shù)組和普通table遍歷略有差異。

遍歷數(shù)組:

-- 聲明數(shù)組 key為索引的 table
local arr = {'java', 'python', 'lua'}
-- 遍歷數(shù)組
for index,value in ipairs(arr) do
    print(index, value) 
end

遍歷普通table:

-- 聲明map,也就是table
local map = {name='Jack', age=21}
-- 遍歷table
for key,value in pairs(map) do
   print(key, value) 
end

3.4、條件控制、函數(shù)

Lua中的條件控制和函數(shù)聲明與Java類似。

3.4.1、函數(shù)

function 函數(shù)名( argument1, argument2..., argumentn)
    -- 函數(shù)體
    return 返回值
end

例如,定義一個(gè)函數(shù),用來打印數(shù)組:

function printArr(arr)
    for index, value in ipairs(arr) do
        print(value)
    end
end

3.4.2、條件控制

類似Java的條件控制,例如if、else語法:

if(布爾表達(dá)式)
then
   --[ 布爾表達(dá)式為 true 時(shí)執(zhí)行該語句塊 --]
else
   --[ 布爾表達(dá)式為 false 時(shí)執(zhí)行該語句塊 --]
end

與java不同,布爾表達(dá)式中的邏輯運(yùn)算是基于英文單詞:

黑馬Redis視頻教程高級(jí)篇(二:多級(jí)緩存)

3.4.3、案例

需求:自定義一個(gè)函數(shù),可以打印table,當(dāng)參數(shù)為nil時(shí),打印錯(cuò)誤信息

function printArr(arr)
    if not arr then
        print('數(shù)組不能為空!')
    end
    for index, value in ipairs(arr) do
        print(value)
    end
end

四、實(shí)現(xiàn)多級(jí)緩存

4.1、安裝OpenResty

OpenResty? 是一個(gè)基于 Nginx的高性能 Web 平臺(tái),用于方便地搭建能夠處理超高并發(fā)、擴(kuò)展性極高的動(dòng)態(tài) Web 應(yīng)用、Web 服務(wù)和動(dòng)態(tài)網(wǎng)關(guān)。具備下列特點(diǎn):

  • 具備Nginx的完整功能

  • 基于Lua語言進(jìn)行擴(kuò)展,集成了大量精良的 Lua 庫、第三方模塊

  • 允許使用Lua自定義業(yè)務(wù)邏輯、自定義庫

官方網(wǎng)站: OpenResty? - 開源官方站/

黑馬Redis視頻教程高級(jí)篇(二:多級(jí)緩存)

安裝參考我的另一篇文章:安裝OpenResty

4.2、OpenResty快速入門

我們希望達(dá)到的多級(jí)緩存架構(gòu)如圖:

黑馬Redis視頻教程高級(jí)篇(二:多級(jí)緩存)

其中:

  • windows上的nginx用來做反向代理服務(wù),將前端的查詢商品的ajax請求代理到OpenResty集群

  • OpenResty集群用來編寫多級(jí)緩存業(yè)務(wù)

4.2.1、反向代理流程

現(xiàn)在,商品詳情頁使用的是假的商品數(shù)據(jù)。不過在瀏覽器中,可以看到頁面有發(fā)起ajax請求查詢真實(shí)商品數(shù)據(jù)。

這個(gè)請求如下:

黑馬Redis視頻教程高級(jí)篇(二:多級(jí)緩存)

請求地址是localhost,端口是80,就被windows上安裝的Nginx服務(wù)給接收到了。然后代理給了OpenResty集群:

黑馬Redis視頻教程高級(jí)篇(二:多級(jí)緩存)??

我們需要在OpenResty中編寫業(yè)務(wù),查詢商品數(shù)據(jù)并返回到瀏覽器。

但是這次,我們先在OpenResty接收請求,返回假的商品數(shù)據(jù)。

4.2.2、OpenResty監(jiān)聽請求

OpenResty的很多功能都依賴于其目錄下的Lua庫,需要在nginx.conf中指定依賴庫的目錄,并導(dǎo)入依賴:

1)添加對(duì)OpenResty的Lua模塊的加載

修改/usr/local/openresty/nginx/conf/nginx.conf文件,在其中的http下面,添加下面代碼:

#lua 模塊
lua_package_path "/usr/local/openresty/lualib/?.lua;;";
#c模塊     
lua_package_cpath "/usr/local/openresty/lualib/?.so;;";  

2)監(jiān)聽/api/item路徑

修改/usr/local/openresty/nginx/conf/nginx.conf文件,在nginx.conf的server下面,添加對(duì)/api/item這個(gè)路徑的監(jiān)聽:

location  /api/item {
    # 默認(rèn)的響應(yīng)類型
    default_type application/json;
    # 響應(yīng)結(jié)果由lua/item.lua文件來決定
    content_by_lua_file lua/item.lua;
}

這個(gè)監(jiān)聽,就類似于SpringMVC中的@GetMapping("/api/item")做路徑映射。

content_by_lua_file lua/item.lua則相當(dāng)于調(diào)用item.lua這個(gè)文件,執(zhí)行其中的業(yè)務(wù),把結(jié)果返回給用戶。相當(dāng)于java中調(diào)用service。

4.2.3、編寫item.lua

1)在/usr/loca/openresty/nginx目錄創(chuàng)建文件夾:lua

黑馬Redis視頻教程高級(jí)篇(二:多級(jí)緩存)

2)在/usr/loca/openresty/nginx/lua文件夾下,新建文件:item.lua ?

黑馬Redis視頻教程高級(jí)篇(二:多級(jí)緩存)

3)編寫item.lua,返回假數(shù)據(jù)

item.lua中,利用ngx.say()函數(shù)返回?cái)?shù)據(jù)到Response中

ngx.say('{"id":10001,"name":"SALSA AIR","title":"RIMOWA 21寸托運(yùn)箱拉桿箱 SALSA AIR系列果綠色 820.70.36.4","price":17900,"image":"https://m.360buyimg.com/mobilecms/s720x720_jfs/t6934/364/1195375010/84676/e9f2c55f/597ece38N0ddcbc77.jpg!q70.jpg.webp","category":"拉桿箱","brand":"RIMOWA","spec":"","status":1,"createTime":"2019-04-30T16:00:00.000+00:00","updateTime":"2019-04-30T16:00:00.000+00:00","stock":2999,"sold":31290}')

4)重新加載配置

nginx -s reload

刷新商品頁面:http://localhost/item.html?id=1001,即可看到效果:

黑馬Redis視頻教程高級(jí)篇(二:多級(jí)緩存)

4.3、請求參數(shù)處理

上一節(jié)中,我們在OpenResty接收前端請求,但是返回的是假數(shù)據(jù)。

要返回真實(shí)數(shù)據(jù),必須根據(jù)前端傳遞來的商品id,查詢商品信息才可以。

那么如何獲取前端傳遞的商品參數(shù)呢?

4.3.1、獲取參數(shù)的API

OpenResty中提供了一些API用來獲取不同類型的前端請求參數(shù):

黑馬Redis視頻教程高級(jí)篇(二:多級(jí)緩存)

4.3.2、獲取參數(shù)并返回

在前端發(fā)起的ajax請求如圖:

黑馬Redis視頻教程高級(jí)篇(二:多級(jí)緩存)

可以看到商品id是以路徑占位符方式傳遞的,因此可以利用正則表達(dá)式匹配的方式來獲取ID

1)獲取商品id

修改/usr/loca/openresty/nginx/nginx.conf文件中監(jiān)聽/api/item的代碼,利用正則表達(dá)式獲取ID:

location ~ /api/item/(\d+) {
    # 默認(rèn)的響應(yīng)類型
    default_type application/json;
    # 響應(yīng)結(jié)果由lua/item.lua文件來決定
    content_by_lua_file lua/item.lua;
}

2)拼接ID并返回

修改/usr/loca/openresty/nginx/lua/item.lua文件,獲取id并拼接到結(jié)果中返回:

-- 獲取商品id
local id = ngx.var[1]
-- 拼接并返回
ngx.say('{"id":' .. id .. ',"name":"SALSA AIR","title":"RIMOWA 21寸托運(yùn)箱拉桿箱 SALSA AIR系列果綠色 820.70.36.4","price":17900,"image":"https://m.360buyimg.com/mobilecms/s720x720_jfs/t6934/364/1195375010/84676/e9f2c55f/597ece38N0ddcbc77.jpg!q70.jpg.webp","category":"拉桿箱","brand":"RIMOWA","spec":"","status":1,"createTime":"2019-04-30T16:00:00.000+00:00","updateTime":"2019-04-30T16:00:00.000+00:00","stock":2999,"sold":31290}')

3)重新加載并測試

運(yùn)行命令以重新加載OpenResty配置:

nginx -s reload

刷新頁面可以看到結(jié)果中已經(jīng)帶上了ID:

黑馬Redis視頻教程高級(jí)篇(二:多級(jí)緩存)

4.4、查詢Tomcat

拿到商品ID后,本應(yīng)去緩存中查詢商品信息,不過目前我們還未建立nginx、redis緩存。因此,這里我們先根據(jù)商品id去tomcat查詢商品信息。我們實(shí)現(xiàn)如圖部分:

黑馬Redis視頻教程高級(jí)篇(二:多級(jí)緩存)

需要注意的是,我們的OpenResty是在虛擬機(jī),Tomcat是在Windows電腦上。兩者IP一定不要搞錯(cuò)了。 ?

黑馬Redis視頻教程高級(jí)篇(二:多級(jí)緩存)

4.4.1、發(fā)送http請求的API

nginx提供了內(nèi)部API用以發(fā)送http請求:

local resp = ngx.location.capture("/path",{
    method = ngx.HTTP_GET,   -- 請求方式
    args = {a=1,b=2},  -- get方式傳參數(shù)
})

返回的響應(yīng)內(nèi)容包括:

  • resp.status:響應(yīng)狀態(tài)碼

  • resp.header:響應(yīng)頭,是一個(gè)table

  • resp.body:響應(yīng)體,就是響應(yīng)數(shù)據(jù)

注意:這里的path是路徑,并不包含IP和端口。這個(gè)請求會(huì)被nginx內(nèi)部的server監(jiān)聽并處理。

但是我們希望這個(gè)請求發(fā)送到Tomcat服務(wù)器,所以還需要編寫一個(gè)server來對(duì)這個(gè)路徑做反向代理:

 location /path {
     # 這里是windows電腦的ip和Java服務(wù)端口,需要確保windows防火墻處于關(guān)閉狀態(tài)
     proxy_pass http://192.168.150.1:8081; 
 }

原理如圖:

黑馬Redis視頻教程高級(jí)篇(二:多級(jí)緩存)

4.4.2、封裝http工具

下面,我們封裝一個(gè)發(fā)送Http請求的工具,基于ngx.location.capture來實(shí)現(xiàn)查詢tomcat。

1)添加反向代理,到windows的Java服務(wù)

因?yàn)閕tem-service中的接口都是/item開頭,所以我們監(jiān)聽/item路徑,代理到windows上的tomcat服務(wù)。

修改 /usr/local/openresty/nginx/conf/nginx.conf文件,添加一個(gè)location:

location /item {
    proxy_pass http://192.168.150.1:8081;
}

以后,只要我們調(diào)用ngx.location.capture("/item"),就一定能發(fā)送請求到windows的tomcat服務(wù)。

2)封裝工具類

之前我們說過,OpenResty啟動(dòng)時(shí)會(huì)加載以下兩個(gè)目錄中的工具文件:

黑馬Redis視頻教程高級(jí)篇(二:多級(jí)緩存)

所以,自定義的http工具也需要放到這個(gè)目錄下。

/usr/local/openresty/lualib目錄下,新建一個(gè)common.lua文件:

vi /usr/local/openresty/lualib/common.lua

內(nèi)容如下:

-- 封裝函數(shù),發(fā)送http請求,并解析響應(yīng)
local function read_http(path, params)
    local resp = ngx.location.capture(path,{
        method = ngx.HTTP_GET,
        args = params,
    })
    if not resp then
        -- 記錄錯(cuò)誤信息,返回404
        ngx.log(ngx.ERR, "http請求查詢失敗, path: ", path , ", args: ", args)
        ngx.exit(404)
    end
    return resp.body
end
-- 將方法導(dǎo)出
local _M = {  
    read_http = read_http
}  
return _M

這個(gè)工具將read_http函數(shù)封裝到_M這個(gè)table類型的變量中,并且返回,這類似于導(dǎo)出。

使用的時(shí)候,可以利用require('common')來導(dǎo)入該函數(shù)庫,這里的common是函數(shù)庫的文件名。

3)實(shí)現(xiàn)商品查詢

最后,我們修改/usr/local/openresty/lua/item.lua文件,利用剛剛封裝的函數(shù)庫實(shí)現(xiàn)對(duì)tomcat的查詢:

-- 引入自定義common工具模塊,返回值是common中返回的 _M
local common = require("common")
-- 從 common中獲取read_http這個(gè)函數(shù)
local read_http = common.read_http
-- 獲取路徑參數(shù)
local id = ngx.var[1]
-- 根據(jù)id查詢商品
local itemJSON = read_http("/item/".. id, nil)
-- 根據(jù)id查詢商品庫存
local itemStockJSON = read_http("/item/stock/".. id, nil)

這里查詢到的結(jié)果是json字符串,并且包含商品、庫存兩個(gè)json字符串,頁面最終需要的是把兩個(gè)json拼接為一個(gè)json:

黑馬Redis視頻教程高級(jí)篇(二:多級(jí)緩存)

?這就需要我們先把JSON變?yōu)閘ua的table,完成數(shù)據(jù)整合后,再轉(zhuǎn)為JSON。

4.4.3、CJSON工具類

OpenResty提供了一個(gè)cjson的模塊用來處理JSON的序列化和反序列化。

官方地址: GitHub - openresty/lua-cjson: Lua CJSON is a fast JSON encoding/parsing module for Lua/

1)引入cjson模塊:

local cjson = require "cjson"

2)序列化:

local obj = {
    name = 'jack',
    age = 21
}
-- 把 table 序列化為 json
local json = cjson.encode(obj)

3)反序列化:

local json = '{"name": "jack", "age": 21}'
-- 反序列化 json為 table
local obj = cjson.decode(json);
print(obj.name)

4.4.4、實(shí)現(xiàn)Tomcat查詢

下面,我們修改之前的item.lua中的業(yè)務(wù),添加json處理功能:

-- 導(dǎo)入common函數(shù)庫
local common = require('common')
local read_http = common.read_http
-- 導(dǎo)入cjson庫
local cjson = require('cjson')

-- 獲取路徑參數(shù)
local id = ngx.var[1]
-- 根據(jù)id查詢商品
local itemJSON = read_http("/item/".. id, nil)
-- 根據(jù)id查詢商品庫存
local itemStockJSON = read_http("/item/stock/".. id, nil)

-- JSON轉(zhuǎn)化為lua的table
local item = cjson.decode(itemJSON)
local stock = cjson.decode(stockJSON)

-- 組合數(shù)據(jù)
item.stock = stock.stock
item.sold = stock.sold

-- 把item序列化為json 返回結(jié)果
ngx.say(cjson.encode(item))

4.4.5、基于ID負(fù)載均衡

剛才的代碼中,我們的tomcat是單機(jī)部署。而實(shí)際開發(fā)中,tomcat一定是集群模式:

黑馬Redis視頻教程高級(jí)篇(二:多級(jí)緩存)

因此,OpenResty需要對(duì)tomcat集群做負(fù)載均衡。

而默認(rèn)的負(fù)載均衡規(guī)則是輪詢模式,當(dāng)我們查詢/item/10001時(shí):

  • 第一次會(huì)訪問8081端口的tomcat服務(wù),在該服務(wù)內(nèi)部就形成了JVM進(jìn)程緩存

  • 第二次會(huì)訪問8082端口的tomcat服務(wù),該服務(wù)內(nèi)部沒有JVM緩存(因?yàn)镴VM緩存無法共享),會(huì)查詢數(shù)據(jù)庫

  • ...

你看,因?yàn)檩喸兊脑?,第一次查?081形成的JVM緩存并未生效,直到下一次再次訪問到8081時(shí)才可以生效,緩存命中率太低了。

怎么辦?

如果能讓同一個(gè)商品,每次查詢時(shí)都訪問同一個(gè)tomcat服務(wù),那么JVM緩存就一定能生效了。

也就是說,我們需要根據(jù)商品id做負(fù)載均衡,而不是輪詢。

1)原理

nginx提供了基于請求路徑做負(fù)載均衡的算法:

nginx根據(jù)請求路徑做hash運(yùn)算,把得到的數(shù)值對(duì)tomcat服務(wù)的數(shù)量取余,余數(shù)是幾,就訪問第幾個(gè)服務(wù),實(shí)現(xiàn)負(fù)載均衡。

例如:

  • 我們的請求路徑是 /item/10001

  • tomcat總數(shù)為2臺(tái)(8081、8082)

  • 對(duì)請求路徑/item/1001做hash運(yùn)算求余的結(jié)果為1

  • 則訪問第一個(gè)tomcat服務(wù),也就是8081

只要id不變,每次hash運(yùn)算結(jié)果也不會(huì)變,那就可以保證同一個(gè)商品,一直訪問同一個(gè)tomcat服務(wù),確保JVM緩存生效。

2)實(shí)現(xiàn)

修改/usr/local/openresty/nginx/conf/nginx.conf文件,實(shí)現(xiàn)基于ID做負(fù)載均衡。

首先,定義tomcat集群,并設(shè)置基于路徑做負(fù)載均衡:

upstream tomcat-cluster {
    hash $request_uri;
    server 192.168.150.1:8081;
    server 192.168.150.1:8082;
}

然后,修改對(duì)tomcat服務(wù)的反向代理,目標(biāo)指向tomcat集群:

location /item {
    proxy_pass http://tomcat-cluster;
}

重新加載OpenResty

nginx -s reload

3)測試

啟動(dòng)兩臺(tái)tomcat服務(wù):

黑馬Redis視頻教程高級(jí)篇(二:多級(jí)緩存)

同時(shí)啟動(dòng):

黑馬Redis視頻教程高級(jí)篇(二:多級(jí)緩存)

清空日志后,再次訪問頁面,可以看到不同id的商品,訪問到了不同的tomcat服務(wù):

黑馬Redis視頻教程高級(jí)篇(二:多級(jí)緩存)?黑馬Redis視頻教程高級(jí)篇(二:多級(jí)緩存)

4.5、Redis緩存預(yù)熱

Redis緩存會(huì)面臨冷啟動(dòng)問題:

冷啟動(dòng):服務(wù)剛剛啟動(dòng)時(shí),Redis中并沒有緩存,如果所有商品數(shù)據(jù)都在第一次查詢時(shí)添加緩存,可能會(huì)給數(shù)據(jù)庫帶來較大壓力。

緩存預(yù)熱:在實(shí)際開發(fā)中,我們可以利用大數(shù)據(jù)統(tǒng)計(jì)用戶訪問的熱點(diǎn)數(shù)據(jù),在項(xiàng)目啟動(dòng)時(shí)將這些熱點(diǎn)數(shù)據(jù)提前查詢并保存到Redis中。

我們數(shù)據(jù)量較少,并且沒有數(shù)據(jù)統(tǒng)計(jì)相關(guān)功能,目前可以在啟動(dòng)時(shí)將所有數(shù)據(jù)都放入緩存中。

1)利用Docker安裝Redis

docker run --name redis -p 6379:6379 -d redis redis-server --appendonly yes

2)在item-service服務(wù)中引入Redis依賴

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

3)配置Redis地址

spring:
  redis:
    host: 192.168.150.101

4)編寫初始化類

緩存預(yù)熱需要在項(xiàng)目啟動(dòng)時(shí)完成,并且必須是拿到RedisTemplate之后。

這里我們利用InitializingBean接口來實(shí)現(xiàn),因?yàn)镮nitializingBean可以在對(duì)象被Spring創(chuàng)建并且成員變量全部注入后執(zhí)行。

package com.heima.item.config;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.heima.item.pojo.Item;
import com.heima.item.pojo.ItemStock;
import com.heima.item.service.IItemService;
import com.heima.item.service.IItemStockService;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import java.util.List;

@Component
public class RedisHandler implements InitializingBean {

    @Autowired
    private StringRedisTemplate redisTemplate;

    @Autowired
    private IItemService itemService;
    @Autowired
    private IItemStockService stockService;

    private static final ObjectMapper MAPPER = new ObjectMapper();

    @Override
    public void afterPropertiesSet() throws Exception {
        // 初始化緩存
        // 1.查詢商品信息
        List<Item> itemList = itemService.list();
        // 2.放入緩存
        for (Item item : itemList) {
            // 2.1.item序列化為JSON
            String json = MAPPER.writeValueAsString(item);
            // 2.2.存入redis
            redisTemplate.opsForValue().set("item:id:" + item.getId(), json);
        }

        // 3.查詢商品庫存信息
        List<ItemStock> stockList = stockService.list();
        // 4.放入緩存
        for (ItemStock stock : stockList) {
            // 2.1.item序列化為JSON
            String json = MAPPER.writeValueAsString(stock);
            // 2.2.存入redis
            redisTemplate.opsForValue().set("item:stock:id:" + stock.getId(), json);
        }
    }
}

4.6、查詢Redis緩存

現(xiàn)在,Redis緩存已經(jīng)準(zhǔn)備就緒,我們可以再OpenResty中實(shí)現(xiàn)查詢Redis的邏輯了。如下圖紅框所示:

黑馬Redis視頻教程高級(jí)篇(二:多級(jí)緩存)

當(dāng)請求進(jìn)入OpenResty之后:

  • 優(yōu)先查詢Redis緩存

  • 如果Redis緩存未命中,再查詢Tomcat

4.6.1、封裝Redis工具

OpenResty提供了操作Redis的模塊,我們只要引入該模塊就能直接使用。但是為了方便,我們將Redis操作封裝到之前的common.lua工具庫中。

修改/usr/local/openresty/lualib/common.lua文件:

1)引入Redis模塊,并初始化Redis對(duì)象

-- 導(dǎo)入redis
local redis = require('resty.redis')
-- 初始化redis
local red = redis:new()
red:set_timeouts(1000, 1000, 1000)

2)封裝函數(shù),用來釋放Redis連接,其實(shí)是放入連接池

-- 關(guān)閉redis連接的工具方法,其實(shí)是放入連接池
local function close_redis(red)
    local pool_max_idle_time = 10000 -- 連接的空閑時(shí)間,單位是毫秒
    local pool_size = 100 --連接池大小
    local ok, err = red:set_keepalive(pool_max_idle_time, pool_size)
    if not ok then
        ngx.log(ngx.ERR, "放入redis連接池失敗: ", err)
    end
end

3)封裝函數(shù),根據(jù)key查詢Redis數(shù)據(jù)

-- 查詢r(jià)edis的方法 ip和port是redis地址,key是查詢的key
local function read_redis(ip, port, key)
    -- 獲取一個(gè)連接
    local ok, err = red:connect(ip, port)
    if not ok then
        ngx.log(ngx.ERR, "連接redis失敗 : ", err)
        return nil
    end
    -- 查詢r(jià)edis
    local resp, err = red:get(key)
    -- 查詢失敗處理
    if not resp then
        ngx.log(ngx.ERR, "查詢Redis失敗: ", err, ", key = " , key)
    end
    --得到的數(shù)據(jù)為空處理
    if resp == ngx.null then
        resp = nil
        ngx.log(ngx.ERR, "查詢Redis數(shù)據(jù)為空, key = ", key)
    end
    close_redis(red)
    return resp
end

4)導(dǎo)出

-- 將方法導(dǎo)出
local _M = {  
    read_http = read_http,
    read_redis = read_redis
}  
return _M

完整的common.lua:

-- 導(dǎo)入redis
local redis = require('resty.redis')
-- 初始化redis
local red = redis:new()
red:set_timeouts(1000, 1000, 1000)

-- 關(guān)閉redis連接的工具方法,其實(shí)是放入連接池
local function close_redis(red)
    local pool_max_idle_time = 10000 -- 連接的空閑時(shí)間,單位是毫秒
    local pool_size = 100 --連接池大小
    local ok, err = red:set_keepalive(pool_max_idle_time, pool_size)
    if not ok then
        ngx.log(ngx.ERR, "放入redis連接池失敗: ", err)
    end
end

-- 查詢r(jià)edis的方法 ip和port是redis地址,key是查詢的key
local function read_redis(ip, port, key)
    -- 獲取一個(gè)連接
    local ok, err = red:connect(ip, port)
    if not ok then
        ngx.log(ngx.ERR, "連接redis失敗 : ", err)
        return nil
    end
    -- 查詢r(jià)edis
    local resp, err = red:get(key)
    -- 查詢失敗處理
    if not resp then
        ngx.log(ngx.ERR, "查詢Redis失敗: ", err, ", key = " , key)
    end
    --得到的數(shù)據(jù)為空處理
    if resp == ngx.null then
        resp = nil
        ngx.log(ngx.ERR, "查詢Redis數(shù)據(jù)為空, key = ", key)
    end
    close_redis(red)
    return resp
end

-- 封裝函數(shù),發(fā)送http請求,并解析響應(yīng)
local function read_http(path, params)
    local resp = ngx.location.capture(path,{
        method = ngx.HTTP_GET,
        args = params,
    })
    if not resp then
        -- 記錄錯(cuò)誤信息,返回404
        ngx.log(ngx.ERR, "http查詢失敗, path: ", path , ", args: ", args)
        ngx.exit(404)
    end
    return resp.body
end
-- 將方法導(dǎo)出
local _M = {  
    read_http = read_http,
    read_redis = read_redis
}  
return _M

4.6.2、實(shí)現(xiàn)Redis查詢

接下來,我們就可以去修改item.lua文件,實(shí)現(xiàn)對(duì)Redis的查詢了。

查詢邏輯是:

  • 根據(jù)id查詢Redis

  • 如果查詢失敗則繼續(xù)查詢Tomcat

  • 將查詢結(jié)果返回

1)修改/usr/local/openresty/lua/item.lua文件,添加一個(gè)查詢函數(shù):

-- 導(dǎo)入common函數(shù)庫
local common = require('common')
local read_http = common.read_http
local read_redis = common.read_redis
-- 封裝查詢函數(shù)
function read_data(key, path, params)
    -- 查詢本地緩存
    local val = read_redis("127.0.0.1", 6379, key)
    -- 判斷查詢結(jié)果
    if not val then
        ngx.log(ngx.ERR, "redis查詢失敗,嘗試查詢http, key: ", key)
        -- redis查詢失敗,去查詢http
        val = read_http(path, params)
    end
    -- 返回?cái)?shù)據(jù)
    return val
end

2)而后修改商品查詢、庫存查詢的業(yè)務(wù):

黑馬Redis視頻教程高級(jí)篇(二:多級(jí)緩存)

3)完整的item.lua代碼: ?

-- 導(dǎo)入common函數(shù)庫
local common = require('common')
local read_http = common.read_http
local read_redis = common.read_redis
-- 導(dǎo)入cjson庫
local cjson = require('cjson')

-- 封裝查詢函數(shù)
function read_data(key, path, params)
    -- 查詢本地緩存
    local val = read_redis("127.0.0.1", 6379, key)
    -- 判斷查詢結(jié)果
    if not val then
        ngx.log(ngx.ERR, "redis查詢失敗,嘗試查詢http, key: ", key)
        -- redis查詢失敗,去查詢http
        val = read_http(path, params)
    end
    -- 返回?cái)?shù)據(jù)
    return val
end

-- 獲取路徑參數(shù)
local id = ngx.var[1]

-- 查詢商品信息
local itemJSON = read_data("item:id:" .. id,  "/item/" .. id, nil)
-- 查詢庫存信息
local stockJSON = read_data("item:stock:id:" .. id, "/item/stock/" .. id, nil)

-- JSON轉(zhuǎn)化為lua的table
local item = cjson.decode(itemJSON)
local stock = cjson.decode(stockJSON)
-- 組合數(shù)據(jù)
item.stock = stock.stock
item.sold = stock.sold

-- 把item序列化為json 返回結(jié)果
ngx.say(cjson.encode(item))

4.7、Nginx本地緩存

現(xiàn)在,整個(gè)多級(jí)緩存中只差最后一環(huán),也就是nginx的本地緩存了。如圖:

黑馬Redis視頻教程高級(jí)篇(二:多級(jí)緩存)

4.7.1、本地緩存API

OpenResty為Nginx提供了shard dict的功能,可以在nginx的多個(gè)worker之間共享數(shù)據(jù),實(shí)現(xiàn)緩存功能。

1)開啟共享字典,在nginx.conf的http下添加配置:

 # 共享字典,也就是本地緩存,名稱叫做:item_cache,大小150m
 lua_shared_dict item_cache 150m; 

2)操作共享字典:

-- 獲取本地緩存對(duì)象
local item_cache = ngx.shared.item_cache
-- 存儲(chǔ), 指定key、value、過期時(shí)間,單位s,默認(rèn)為0代表永不過期
item_cache:set('key', 'value', 1000)
-- 讀取
local val = item_cache:get('key')

4.7.2、實(shí)現(xiàn)本地緩存查詢

1)修改/usr/local/openresty/lua/item.lua文件,修改read_data查詢函數(shù),添加本地緩存邏輯:

-- 導(dǎo)入共享詞典,本地緩存
local item_cache = ngx.shared.item_cache

-- 封裝查詢函數(shù)
function read_data(key, expire, path, params)
    -- 查詢本地緩存
    local val = item_cache:get(key)
    if not val then
        ngx.log(ngx.ERR, "本地緩存查詢失敗,嘗試查詢Redis, key: ", key)
        -- 查詢r(jià)edis
        val = read_redis("127.0.0.1", 6379, key)
        -- 判斷查詢結(jié)果
        if not val then
            ngx.log(ngx.ERR, "redis查詢失敗,嘗試查詢http, key: ", key)
            -- redis查詢失敗,去查詢http
            val = read_http(path, params)
        end
    end
    -- 查詢成功,把數(shù)據(jù)寫入本地緩存
    item_cache:set(key, val, expire)
    -- 返回?cái)?shù)據(jù)
    return val
end

2)修改item.lua中查詢商品和庫存的業(yè)務(wù),實(shí)現(xiàn)最新的read_data函數(shù):

黑馬Redis視頻教程高級(jí)篇(二:多級(jí)緩存)

其實(shí)就是多了緩存時(shí)間參數(shù),過期后nginx緩存會(huì)自動(dòng)刪除,下次訪問即可更新緩存。

這里給商品基本信息設(shè)置超時(shí)時(shí)間為30分鐘,庫存為1分鐘。

因?yàn)閹齑娓骂l率較高,如果緩存時(shí)間過長,可能與數(shù)據(jù)庫差異較大。

3)完整的item.lua文件:

-- 導(dǎo)入common函數(shù)庫
local common = require('common')
local read_http = common.read_http
local read_redis = common.read_redis
-- 導(dǎo)入cjson庫
local cjson = require('cjson')
-- 導(dǎo)入共享詞典,本地緩存
local item_cache = ngx.shared.item_cache

-- 封裝查詢函數(shù)
function read_data(key, expire, path, params)
    -- 查詢本地緩存
    local val = item_cache:get(key)
    if not val then
        ngx.log(ngx.ERR, "本地緩存查詢失敗,嘗試查詢Redis, key: ", key)
        -- 查詢r(jià)edis
        val = read_redis("127.0.0.1", 6379, key)
        -- 判斷查詢結(jié)果
        if not val then
            ngx.log(ngx.ERR, "redis查詢失敗,嘗試查詢http, key: ", key)
            -- redis查詢失敗,去查詢http
            val = read_http(path, params)
        end
    end
    -- 查詢成功,把數(shù)據(jù)寫入本地緩存
    item_cache:set(key, val, expire)
    -- 返回?cái)?shù)據(jù)
    return val
end

-- 獲取路徑參數(shù)
local id = ngx.var[1]

-- 查詢商品信息
local itemJSON = read_data("item:id:" .. id, 1800,  "/item/" .. id, nil)
-- 查詢庫存信息
local stockJSON = read_data("item:stock:id:" .. id, 60, "/item/stock/" .. id, nil)

-- JSON轉(zhuǎn)化為lua的table
local item = cjson.decode(itemJSON)
local stock = cjson.decode(stockJSON)
-- 組合數(shù)據(jù)
item.stock = stock.stock
item.sold = stock.sold

-- 把item序列化為json 返回結(jié)果
ngx.say(cjson.encode(item))

五、緩存同步

大多數(shù)情況下,瀏覽器查詢到的都是緩存數(shù)據(jù),如果緩存數(shù)據(jù)與數(shù)據(jù)庫數(shù)據(jù)存在較大差異,可能會(huì)產(chǎn)生比較嚴(yán)重的后果。

所以我們必須保證數(shù)據(jù)庫數(shù)據(jù)、緩存數(shù)據(jù)的一致性,這就是緩存與數(shù)據(jù)庫的同步。

5.1、數(shù)據(jù)同步策略

緩存數(shù)據(jù)同步的常見方式有三種:

設(shè)置有效期:給緩存設(shè)置有效期,到期后自動(dòng)刪除。再次查詢時(shí)更新

  • 優(yōu)勢:簡單、方便

  • 缺點(diǎn):時(shí)效性差,緩存過期之前可能不一致

  • 場景:更新頻率較低,時(shí)效性要求低的業(yè)務(wù)

同步雙寫:在修改數(shù)據(jù)庫的同時(shí),直接修改緩存

  • 優(yōu)勢:時(shí)效性強(qiáng),緩存與數(shù)據(jù)庫強(qiáng)一致

  • 缺點(diǎn):有代碼侵入,耦合度高;

  • 場景:對(duì)一致性、時(shí)效性要求較高的緩存數(shù)據(jù)

異步通知:修改數(shù)據(jù)庫時(shí)發(fā)送事件通知,相關(guān)服務(wù)監(jiān)聽到通知后修改緩存數(shù)據(jù)

  • 優(yōu)勢:低耦合,可以同時(shí)通知多個(gè)緩存服務(wù)

  • 缺點(diǎn):時(shí)效性一般,可能存在中間不一致狀態(tài)

  • 場景:時(shí)效性要求一般,有多個(gè)服務(wù)需要同步

而異步實(shí)現(xiàn)又可以基于MQ或者Canal來實(shí)現(xiàn):

1)基于MQ的異步通知:

黑馬Redis視頻教程高級(jí)篇(二:多級(jí)緩存)

解讀:

  • 商品服務(wù)完成對(duì)數(shù)據(jù)的修改后,只需要發(fā)送一條消息到MQ中。

  • 緩存服務(wù)監(jiān)聽MQ消息,然后完成對(duì)緩存的更新

依然有少量的代碼侵入。

2)基于Canal的通知

黑馬Redis視頻教程高級(jí)篇(二:多級(jí)緩存)

解讀:

  • 商品服務(wù)完成商品修改后,業(yè)務(wù)直接結(jié)束,沒有任何代碼侵入

  • Canal監(jiān)聽MySQL變化,當(dāng)發(fā)現(xiàn)變化后,立即通知緩存服務(wù)

  • 緩存服務(wù)接收到canal通知,更新緩存

代碼零侵入

5.2、安裝Canal

5.2.1、認(rèn)識(shí)Canal

Canal [k?'n?l],譯意為水道/管道/溝渠,canal是阿里巴巴旗下的一款開源項(xiàng)目,基于Java開發(fā)?;跀?shù)據(jù)庫增量日志解析,提供增量數(shù)據(jù)訂閱&消費(fèi)。GitHub的地址:GitHub - alibaba/canal: 阿里巴巴 MySQL binlog 增量訂閱&消費(fèi)組件

Canal是基于mysql的主從同步來實(shí)現(xiàn)的,MySQL主從同步的原理如下:

黑馬Redis視頻教程高級(jí)篇(二:多級(jí)緩存)

  • 1)MySQL master 將數(shù)據(jù)變更寫入二進(jìn)制日志( binary log),其中記錄的數(shù)據(jù)叫做binary log events

  • 2)MySQL slave 將 master 的 binary log events拷貝到它的中繼日志(relay log)

  • 3)MySQL slave 重放 relay log 中事件,將數(shù)據(jù)變更反映它自己的數(shù)據(jù)

而Canal就是把自己偽裝成MySQL的一個(gè)slave節(jié)點(diǎn),從而監(jiān)聽master的binary log變化。再把得到的變化信息通知給Canal的客戶端,進(jìn)而完成對(duì)其它數(shù)據(jù)庫的同步。

黑馬Redis視頻教程高級(jí)篇(二:多級(jí)緩存)

5.2.2、安裝Canal

參考我的另一篇文章:黑馬Redis視頻教程高級(jí)篇(安裝Canal)

5.3、監(jiān)聽Canal

Canal提供了各種語言的客戶端,當(dāng)Canal監(jiān)聽到binlog變化時(shí),會(huì)通知Canal的客戶端。

黑馬Redis視頻教程高級(jí)篇(二:多級(jí)緩存)

我們可以利用Canal提供的Java客戶端,監(jiān)聽Canal通知消息。當(dāng)收到變化的消息時(shí),完成對(duì)緩存的更新。

不過這里我們會(huì)使用GitHub上的第三方開源的canal-starter客戶端。地址:GitHub - NormanGyllenhaal/canal-client: spring boot canal starter 易用的canal 客戶端 canal client

與SpringBoot完美整合,自動(dòng)裝配,比官方客戶端要簡單好用很多。

5.3.1、引入依賴

<dependency>
    <groupId>top.javatool</groupId>
    <artifactId>canal-spring-boot-starter</artifactId>
    <version>1.2.1-RELEASE</version>
</dependency>

5.3.2、yml編寫配置

canal:
  destination: heima # canal的集群名字,要與安裝canal時(shí)設(shè)置的名稱一致
  server: 192.168.150.101:11111 # canal服務(wù)地址

5.3.3、修改Item實(shí)體類

通過@Id、@Column、等注解完成Item與數(shù)據(jù)庫表字段的映射:

package com.heima.item.pojo;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import org.springframework.data.annotation.Id;
import org.springframework.data.annotation.Transient;

import javax.persistence.Column;
import java.util.Date;

@Data
@TableName("tb_item")
public class Item {
    @TableId(type = IdType.AUTO)
    @Id
    private Long id;//商品id
    @Column(name = "name")
    private String name;//商品名稱
    private String title;//商品標(biāo)題
    private Long price;//價(jià)格(分)
    private String image;//商品圖片
    private String category;//分類名稱
    private String brand;//品牌名稱
    private String spec;//規(guī)格
    private Integer status;//商品狀態(tài) 1-正常,2-下架
    private Date createTime;//創(chuàng)建時(shí)間
    private Date updateTime;//更新時(shí)間
    @TableField(exist = false)
    @Transient
    private Integer stock;
    @TableField(exist = false)
    @Transient
    private Integer sold;
}

5.3.4、編寫監(jiān)聽器

通過實(shí)現(xiàn)EntryHandler<T>接口編寫監(jiān)聽器,監(jiān)聽Canal消息。注意兩點(diǎn):

  • 實(shí)現(xiàn)類通過@CanalTable("tb_item")指定監(jiān)聽的表信息

  • EntryHandler的泛型是與表對(duì)應(yīng)的實(shí)體類

package com.heima.item.canal;

import com.github.benmanes.caffeine.cache.Cache;
import com.heima.item.config.RedisHandler;
import com.heima.item.pojo.Item;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import top.javatool.canal.client.annotation.CanalTable;
import top.javatool.canal.client.handler.EntryHandler;

@CanalTable("tb_item")
@Component
public class ItemHandler implements EntryHandler<Item> {

    @Autowired
    private RedisHandler redisHandler;
    @Autowired
    private Cache<Long, Item> itemCache;

    @Override
    public void insert(Item item) {
        // 寫數(shù)據(jù)到JVM進(jìn)程緩存
        itemCache.put(item.getId(), item);
        // 寫數(shù)據(jù)到redis
        redisHandler.saveItem(item);
    }

    @Override
    public void update(Item before, Item after) {
        // 寫數(shù)據(jù)到JVM進(jìn)程緩存
        itemCache.put(after.getId(), after);
        // 寫數(shù)據(jù)到redis
        redisHandler.saveItem(after);
    }

    @Override
    public void delete(Item item) {
        // 刪除數(shù)據(jù)到JVM進(jìn)程緩存
        itemCache.invalidate(item.getId());
        // 刪除數(shù)據(jù)到redis
        redisHandler.deleteItemById(item.getId());
    }
}

在這里對(duì)Redis的操作都封裝到了RedisHandler這個(gè)對(duì)象中,是我們之前做緩存預(yù)熱時(shí)編寫的一個(gè)類,內(nèi)容如下:文章來源地址http://www.zghlxwxcb.cn/news/detail-487568.html

package com.heima.item.config;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.heima.item.pojo.Item;
import com.heima.item.pojo.ItemStock;
import com.heima.item.service.IItemService;
import com.heima.item.service.IItemStockService;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import java.util.List;

@Component
public class RedisHandler implements InitializingBean {

    @Autowired
    private StringRedisTemplate redisTemplate;

    @Autowired
    private IItemService itemService;
    @Autowired
    private IItemStockService stockService;

    private static final ObjectMapper MAPPER = new ObjectMapper();

    @Override
    public void afterPropertiesSet() throws Exception {
        // 初始化緩存
        // 1.查詢商品信息
        List<Item> itemList = itemService.list();
        // 2.放入緩存
        for (Item item : itemList) {
            // 2.1.item序列化為JSON
            String json = MAPPER.writeValueAsString(item);
            // 2.2.存入redis
            redisTemplate.opsForValue().set("item:id:" + item.getId(), json);
        }

        // 3.查詢商品庫存信息
        List<ItemStock> stockList = stockService.list();
        // 4.放入緩存
        for (ItemStock stock : stockList) {
            // 2.1.item序列化為JSON
            String json = MAPPER.writeValueAsString(stock);
            // 2.2.存入redis
            redisTemplate.opsForValue().set("item:stock:id:" + stock.getId(), json);
        }
    }

    public void saveItem(Item item) {
        try {
            String json = MAPPER.writeValueAsString(item);
            redisTemplate.opsForValue().set("item:id:" + item.getId(), json);
        } catch (JsonProcessingException e) {
            throw new RuntimeException(e);
        }
    }

    public void deleteItemById(Long id) {
        redisTemplate.delete("item:id:" + id);
    }
}

到了這里,關(guān)于黑馬Redis視頻教程高級(jí)篇(二:多級(jí)緩存)的文章就介紹完了。如果您還想了解更多內(nèi)容,請?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!

本文來自互聯(lián)網(wǎng)用戶投稿,該文觀點(diǎn)僅代表作者本人,不代表本站立場。本站僅提供信息存儲(chǔ)空間服務(wù),不擁有所有權(quán),不承擔(dān)相關(guān)法律責(zé)任。如若轉(zhuǎn)載,請注明出處: 如若內(nèi)容造成侵權(quán)/違法違規(guī)/事實(shí)不符,請點(diǎn)擊違法舉報(bào)進(jìn)行投訴反饋,一經(jīng)查實(shí),立即刪除!

領(lǐng)支付寶紅包贊助服務(wù)器費(fèi)用

相關(guān)文章

  • 黑馬Redis視頻教程實(shí)戰(zhàn)篇(五)

    黑馬Redis視頻教程實(shí)戰(zhàn)篇(五)

    目錄 一、達(dá)人探店 1.1、發(fā)布探店筆記 1.2、查看探店筆記 1.3、點(diǎn)贊功能 1.4、點(diǎn)贊排行榜 二、好友關(guān)注 2.1、關(guān)注和取消關(guān)注 2.2、共同關(guān)注 2.3、Feed流實(shí)現(xiàn)方案 2.4、推送到粉絲收件箱 2.4、實(shí)現(xiàn)分頁查詢收郵箱 發(fā)布探店筆記 探店筆記類似點(diǎn)評(píng)網(wǎng)站的評(píng)價(jià),往往是圖文結(jié)合。對(duì)

    2024年02月07日
    瀏覽(33)
  • 黑馬Redis視頻教程實(shí)戰(zhàn)篇(二)

    黑馬Redis視頻教程實(shí)戰(zhàn)篇(二)

    目錄 一、什么是緩存? 1.1 為什么要使用緩存? 1.2 如何使用緩存? 二、添加商戶緩存 2.1?緩存模型和思路 2.2 代碼實(shí)現(xiàn) 三、緩存更新策略 2.1 數(shù)據(jù)庫緩存不一致解決方案 2.2?數(shù)據(jù)庫和緩存不一致采用什么方案 四、實(shí)現(xiàn)商鋪和緩存與數(shù)據(jù)庫雙寫一致 五、緩存穿透問題的解決思

    2024年02月07日
    瀏覽(37)
  • 黑馬Redis視頻教程實(shí)戰(zhàn)篇(一)

    黑馬Redis視頻教程實(shí)戰(zhàn)篇(一)

    目錄 一、短信登錄 1.1、導(dǎo)入黑馬點(diǎn)評(píng)項(xiàng)目 (1)導(dǎo)入黑馬點(diǎn)評(píng)sql腳本 (2)導(dǎo)入后端項(xiàng)目 (3)導(dǎo)入前端項(xiàng)目 ?1.2、基于Session實(shí)現(xiàn)登錄流程 1.3 、實(shí)現(xiàn)發(fā)送短信驗(yàn)證碼功能 1.4?、實(shí)現(xiàn)登錄攔截功能 ?1.5?、隱藏用戶敏感信息 (1)在登錄方法處修改 ?(2)在攔截器處 ?(3)在

    2024年02月07日
    瀏覽(19)
  • 微服務(wù)學(xué)習(xí)筆記--高級(jí)篇--(多級(jí)緩存)

    安裝OpenResty OpenResty快速入門 請求參數(shù)處理 查詢Tomcat Redis緩存預(yù)熱 查詢Redis緩存 Nginx本地緩存 初識(shí)OpenResty OpenResty是一個(gè)基于Nginx的高性能Web平臺(tái),用于方便地搭建能夠處理高并發(fā)、擴(kuò)展性極高的動(dòng)態(tài)Web應(yīng)用、Web服務(wù)和動(dòng)態(tài)網(wǎng)關(guān)。具備下列特點(diǎn): 具備Nginx的完整功能 基于Lua語

    2024年02月09日
    瀏覽(39)
  • 【Redis】多級(jí)緩存(nginx緩存、redis緩存及tomcat緩存)

    【Redis】多級(jí)緩存(nginx緩存、redis緩存及tomcat緩存)

    傳統(tǒng)的緩存策略一般是請求到達(dá) tomcat 后,先查詢r(jià)edis,如果未命中則查詢數(shù)據(jù)庫。這種方式存在以下兩個(gè)問題: 請求要經(jīng)過 tomcat 處理, tomcat 的性能成為整個(gè)系統(tǒng)的瓶頸。 redis緩存失效時(shí),會(huì)對(duì)數(shù)據(jù)庫產(chǎn)生沖擊。 多級(jí)緩存 就是充分利用請求處理的每個(gè)環(huán)節(jié),分別添加緩存

    2023年04月21日
    瀏覽(21)
  • 多級(jí)緩存架構(gòu)(四)Redis緩存

    多級(jí)緩存架構(gòu)(四)Redis緩存

    通過本文章,可以完成多級(jí)緩存架構(gòu)中的Redis緩存。 在 docker/docker-compose.ym l中,添加redis服務(wù)塊 在 spirngboot 項(xiàng)目啟動(dòng)時(shí),將固定的熱點(diǎn)數(shù)據(jù)提前加載到 redis 中。 1. 引入依賴 pom.xml 添加如下依賴 application.yml 添加如下配置 2. handler類實(shí)現(xiàn) 新建 config.RedisHandler 類,內(nèi)容如下,主要

    2024年01月22日
    瀏覽(29)
  • 微服務(wù)學(xué)習(xí)筆記--高級(jí)篇--(多級(jí)緩存意義及JVM進(jìn)程緩存)

    億級(jí)流量的緩存方案 傳統(tǒng)緩存的問題 傳統(tǒng)的緩存策略一般是請求到達(dá)Tomcat后,先查詢Redis,如果未命中則查詢數(shù)據(jù)庫,存在下面的問題: 請求要經(jīng)過Tomcat處理,Tomcat的性能成為整個(gè)系統(tǒng)的瓶頸 Redis緩存失效時(shí),會(huì)對(duì)數(shù)據(jù)庫產(chǎn)生沖擊 多級(jí)緩存方案 多級(jí)緩存就是充分利用請求處

    2024年02月09日
    瀏覽(29)
  • Redis多級(jí)緩存

    Redis多級(jí)緩存

    傳統(tǒng)的緩存策略一般是請求到達(dá)Tomcat后,先查詢Redis,如果未命中則查詢數(shù)據(jù)庫,會(huì)存在以下問題: 請求需要經(jīng)過Tomcat處理,Tomcat的性能成為整個(gè)系統(tǒng)的瓶頸。 Redis緩存失效時(shí),會(huì)對(duì)數(shù)據(jù)庫產(chǎn)生沖擊。 而多級(jí)緩存就是充分利用請求處理的每個(gè)環(huán)節(jié),分別添加緩存,減輕Tomca

    2024年02月15日
    瀏覽(32)
  • 多級(jí)緩存(nginx本地緩存、JVM進(jìn)程緩存、redis緩存)

    多級(jí)緩存(nginx本地緩存、JVM進(jìn)程緩存、redis緩存)

    Caffeine示例 封裝完函數(shù)之后,我們對(duì)nginx.conf進(jìn)行修改(請求進(jìn)來之后會(huì)去尋找item.lua) item.lua文件內(nèi)容 上面的item.lua文件中需要進(jìn)行拼接數(shù)據(jù),我們需要JSON結(jié)果處理 在實(shí)際生產(chǎn)中tomcat是肯定以集群的方式存在 當(dāng)我們修改nginx.conf發(fā)送請求為集群的時(shí)候,如下圖 這個(gè)時(shí)候存在

    2024年01月17日
    瀏覽(53)
  • Redis學(xué)習(xí)(三)分布式緩存、多級(jí)緩存、Redis實(shí)戰(zhàn)經(jīng)驗(yàn)、Redis底層原理

    Redis學(xué)習(xí)(三)分布式緩存、多級(jí)緩存、Redis實(shí)戰(zhàn)經(jīng)驗(yàn)、Redis底層原理

    單節(jié)點(diǎn)Redis存在著: 數(shù)據(jù)丟失問題:單節(jié)點(diǎn)宕機(jī),數(shù)據(jù)就丟失了。 并發(fā)能力和存儲(chǔ)能力問題:單節(jié)點(diǎn)能夠滿足的并發(fā)量、能夠存儲(chǔ)的數(shù)據(jù)量有限。 故障恢復(fù)問題:如果Redis宕機(jī),服務(wù)不可用,需要一種自動(dòng)的故障恢復(fù)手段。 RDB持久化 RDB(Redis database backup file,Redis數(shù)據(jù)庫備份

    2024年02月16日
    瀏覽(32)

覺得文章有用就打賞一下文章作者

支付寶掃一掃打賞

博客贊助

微信掃一掃打賞

請作者喝杯咖啡吧~博客贊助

支付寶掃一掃領(lǐng)取紅包,優(yōu)惠每天領(lǐng)

二維碼1

領(lǐng)取紅包

二維碼2

領(lǐng)紅包