マイクロサービス (マルチレベル キャッシュ)

目次

マルチレベルキャッシュ

1.多値キャッシュとは何ですか?

2.JVMプロセスキャッシュ

2.2. カフェインについての最初の紹介

2.3. JVM プロセスキャッシュの実装

2.3.1.要件

2.3.2. 実装

3. Lua 構文の概要

3.1. Lua の最初の紹介

3.1.HelloWorld

 3.2. 変数とループ

3.2.1.Lua データ型

3.2.2. 変数の宣言

3.2.3. ループ

3.3. 条件付き制御と関数

3.3.1. 機能

3.3.2.条件制御

3.3.3.ケース

4. マルチレベルキャッシュの実装

4.1. OpenRestyのインストール

1.インストール

1) 開発ライブラリをインストールする

2) OpenRestyリポジトリをインストールする

3) OpenRestyをインストールする

4) opm ツールをインストールする

5) ディレクトリ構造

6) nginx 環境変数を構成する

2. 起動して実行する

3.備考

4.2.OpenRestyクイックスタート

4.2.1.リバースプロキシ処理

4.2.2.OpenRestyはリクエストをリッスンします

4.2.3. item.lua を書き込む

 4.3. リクエストパラメータの処理

4.3.1. パラメータ取得用API

 4.3.2. パラメータを取得して返す

 4.4. Tomcat のクエリ

 4.4.1. http リクエストを送信するための API

 4.4.2. http ツールのカプセル化

4.4.3.CJSONツールクラス

4.4.4. Tomcat クエリの実装

4.4.5. ID ベースのロードバランシング

2) 実感する

3) テスト

 4.5.Redis キャッシュのウォームアップ

4.6. Redis キャッシュのクエリ

4.6.1. Redis ツールのカプセル化

4.6.2. Redis クエリの実装

4.7.Nginx ローカル キャッシュ

 4.7.1.ローカルキャッシュAPI

4.7.2. ローカルキャッシュクエリの実装

5. キャッシュの同期

5.1. データ同期戦略

5.2.運河の設置

5.2.1.運河について知る

 5.2.2.運河の設置

1. MySQL マスター/スレーブを開始します

1.1. バイナリログの開始

1.2.ユーザー権限の設定

 2.運河の設置

2.1.ネットワークの作成

2.3.運河の設置

5.3. モニター運河

5.3.1.依存関係を導入します。

5.3.2.設定の書き込み:

5.3.3. 項目エンティティクラスの変更

5.3.4. リスナーの作成


マルチレベルキャッシュ

1.多値キャッシュとは何ですか?

従来のキャッシュ戦略は一般に、リクエストが Tomcat に到達した後、最初に Redis にクエリを実行し、失敗した場合はデータベースにクエリを実行します。次の図を参照します。

 次の問題が存在します。

  • リクエストは Tomcat によって処理される必要があり、Tomcat のパフォーマンスがシステム全体のボトルネックになります。
  • Redis キャッシュに障害が発生すると、データベースに影響が生じます。

マルチレベル キャッシュは、リクエスト処理のあらゆる側面を最大限に活用し、それぞれキャッシュを追加して、Tomcat への負荷を軽減し、サービス パフォーマンスを向上させます。

  • ブラウザーが静的リソースにアクセスするときは、まずブラウザーのローカル キャッシュを読み取ります。
  • 非静的リソース(Ajaxクエリデータ)にアクセスする場合は、サーバーにアクセスします
  • リクエストが Nginx に到達すると、最初に Nginx ローカル キャッシュが読み取られます。
  • Nginx ローカル キャッシュが見つからない場合は、(Tomcat を経由せずに) Redis に直接クエリします。
  • Redis クエリが見つからない場合は、Tomcat にクエリを実行します
  • リクエストが Tomcat に入った後、最初に JVM プロセス キャッシュがクエリされます。
  • JVM プロセス キャッシュがミスした場合にデータベースにクエリを実行する

マルチレベルキャッシュアーキテクチャでは、ローカルキャッシュクエリ、Redisクエリ、TomcatクエリのビジネスロジックをNginxで記述する必要があるため、このようなnginxサービスはリバースプロキシサーバー       ではなく、ビジネスを記述するためのWebサーバーとなります

       したがって、このようなビジネス Nginx サービスも、次の図に示すように、同時実行性を向上させるためにクラスターを構築し、リバース プロキシを実行する専用の nginx サービスを用意する必要があります。

 さらに、Tomcat サービスも将来的にはクラスター モードで展開される予定です。

 マルチレベル キャッシュには 2 つの鍵があることがわかります。

  • 1 つは、nginx ローカル キャッシュ、Redis、Tomcat クエリを実装するために nginx でビジネスを記述することです。

  • もう 1 つは、Tomcat に JVM プロセス キャッシュを実装することです。

このうち、NginxプログラミングではOpenRestyフレームワークとLuaなどの言語を組み合わせて使用​​します。

2.JVMプロセスキャッシュ

2.2. カフェインについての最初の紹介

       キャッシュは日常の開発において重要な役割を果たしますが、メモリに保存されるためデータの読み取り速度が非常に速く、データベースへのアクセスが大幅に削減され、データベースへの負荷が軽減されますキャッシュを次の 2 つのカテゴリに分類します。

  • Redis などの分散キャッシュ:

    • 利点: ストレージ容量が大きく、信頼性が向上し、クラスター間で共有できる

    • 短所: キャッシュにアクセスするためのネットワーク オーバーヘッドが発生します。

    • シナリオ: キャッシュされたデータの量が多く、信頼性要件が高く、クラスター間で共有する必要がある

  • HashMap、GuavaCache などのローカル キャッシュを処理します。

    • 利点: ローカル メモリの読み取り、ネットワーク オーバーヘッドなし、高速

    • 欠点: ストレージ容量が限られている、信頼性が低い、共有できない

    • シナリオ: 高いパフォーマンス要件と少量のキャッシュ データ

今日は、Caffeine フレームワークを使用して JVM プロセス キャッシュを実装します。

        Caffeineは、Java8 に基づいて開発された高性能のローカル キャッシュ ライブラリで、最適に近いヒット率を提供します。現在、Spring の内部キャッシュは Caffeine を使用しています。GitHub アドレス: GitHub - ben-manes/caffeine: Java 用の高性能キャッシュ ライブラリ

Caffeine のパフォーマンスは非常に優れており、次の図は公式のパフォーマンス比較です。

 Caffeine のパフォーマンスがはるかに先を行っていることがわかります。

キャッシュで使用される基本的な API:

@Test
void testBasicOps() {
    // 构建cache对象
    Cache<String, String> cache = Caffeine.newBuilder().build();
​
    // 存数据
    cache.put("gf", "迪丽热巴");
​
    // 取数据
    String gf = cache.getIfPresent("gf");
    System.out.println("gf = " + gf);
​
    // 取数据,包含两个参数:
    // 参数一:缓存的key
    // 参数二:Lambda表达式,表达式参数就是缓存的key,方法体是查询数据库的逻辑
    // 优先根据key查询JVM缓存,如果未命中,则执行参数二的Lambda表达式
    String defaultGF = cache.get("defaultGF", key -> {
        // 根据key去数据库查询数据
        return "柳岩";
    });
    System.out.println("defaultGF = " + defaultGF);
}

Caffeine はキャッシュの一種であるため、キャッシュをクリアする戦略が必ず必要です。そうしないと、メモリが常に使い果たされてしまいます。

Caffeine は、次の 3 つのキャッシュ削除戦略を提供します。

  • 容量ベース: キャッシュ数の上限を設定します。
// 创建缓存对象
Cache<String, String> cache = Caffeine.newBuilder()
    .maximumSize(1) // 设置缓存大小上限为 1
    .build();
  • 時間ベース: キャッシュの有効時間を設定します。
// 创建缓存对象
Cache<String, String> cache = Caffeine.newBuilder()
    // 设置缓存有效期为 10 秒,从最后一次写入开始计时 
    .expireAfterWrite(Duration.ofSeconds(10)) 
    .build();
  • 参照ベース: キャッシュをソフト参照または弱参照に設定し、GC を使用してキャッシュされたデータをリサイクルします。パフォーマンスが悪いため、お勧めしません。

: デフォルトでは、Caffeine は有効期限が切れてもすぐにキャッシュ要素を自動的にクリーンアップして削除しません。代わりに、無効なデータの排除は、読み取りまたは書き込み操作の後、またはアイドル時間中に完了します。

2.3. JVM プロセスキャッシュの実装

2.3.1.要件

次の要件を達成するにはカフェインを使用します。

  • ID に基づいて製品をクエリするビジネスにキャッシュを追加し、キャッシュが見つからなかった場合にデータベースをクエリします。

  • ID に基づいて製品在庫をクエリするビジネスにキャッシュを追加し、キャッシュが見つからなかった場合にデータベースにクエリを実行します。

  • キャッシュの初期サイズは 100 です

  • キャッシュの制限は 10000 です

2.3.2. 実装

まず、製品と在庫のキャッシュ データをそれぞれ保存する 2 つの Caffeine キャッシュ オブジェクトを定義する必要があります。

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();
    }
}

次に、com.heima.item.webitem-service のパッケージの下にある ItemsController クラスを変更し、キャッシュ ロジックを追加します。

@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));
    }
}


3. Lua 構文の概要

Nginx プログラミングでは Lua 言語を使用する必要があるため、まず Lua の基本構文から始める必要があります。

3.1. Lua の最初の紹介

       Lua は、標準的な C 言語で書かれたソースコード形式でオープンな軽量かつコンパクトなスクリプト言語であり、アプリケーションに組み込むことを想定して設計されており、アプリケーションに柔軟な拡張機能やカスタマイズ機能を提供します。

公式ウェブサイト:プログラミング言語 Lua

Lua は、ゲーム開発やゲーム プラグインなど、C 言語で開発されたプログラムに組み込まれることがよくあります。

Nginx自体もC言語で開発されているため、Luaベースでの拡張も可能です。

3.1.HelloWorld

CentOS7 には Lua 言語環境がデフォルトでインストールされているため、Lua コードを直接実行できます。

1) Linux 仮想マシンの任意のディレクトリに新しい hello.lua ファイルを作成します。

 2) 以下の内容を追加します

print("Hello World!")  

3) 走る

 3.2. 変数とループ

言語の学習には変数が不可欠であり、変数の宣言ではまずデータの型を知る必要があります。

3.2.1.Lua データ型

Lua でサポートされる一般的なデータ型は次のとおりです。

さらに、Lua は変数のデータ型を決定する type() 関数を提供します。

3.2.2. 変数の宣言

Lua は変数を宣言するときにデータ型を指定する必要はなく、代わりに local を使用して変数をローカル変数として宣言します。

-- 声明字符串,可以用单引号或双引号,
local str = 'hello'
-- 字符串拼接可以使用 ..
local str2 = 'hello' .. 'world'
-- 声明数字
local num = 21
-- 声明布尔类型
local flag = true

      Lua のテーブル型は、Java の配列としてもマップとしても使用できます。配列は特別なテーブルであり、キーは単なる配列のインデックスです。

-- 声明数组 ,key为角标的 table
local arr = {'java', 'python', 'lua'}
-- 声明table,类似java的map
local map =  {name='Jack', age=21}

Lua の配列インデックスは 1 から始まり、アクセスは Java の場合と似ています。

-- 访问数组,lua数组的角标从1开始
print(arr[1])

Lua のテーブルには、キーを使用してアクセスできます。

-- 访问table
print(map['name'])
print(map.name)

3.2.3. ループ

テーブルの場合、for ループを使用して走査できます。ただし、配列の走査は通常のテーブルの走査とは少し異なります。

配列を走査します。

-- 声明数组 key为索引的 table
local arr = {'java', 'python', 'lua'}
-- 遍历数组
for index,value in ipairs(arr) do
    print(index, value) 
end

通常のテーブルを走査します。

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

3.3. 条件付き制御と関数

Lua の条件付き制御と関数宣言は Java に似ています。

3.3.1. 機能

関数を定義するための構文:

function 函数名( argument1, argument2..., argumentn)
    -- 函数体
    return 返回值
end

たとえば、配列を出力する関数を定義します。

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

3.3.2.条件制御

Java に似た条件付き制御 (if 構文や else 構文など):

if(布尔表达式)
then
   --[ 布尔表达式为 true 时执行该语句块 --]
else
   --[ 布尔表达式为 false 时执行该语句块 --]
end
​

Java とは異なり、ブール式の論理演算は英語の単語に基づいています。

3.3.3.ケース

要件: テーブルを出力し、パラメーターが nil の場合にエラー メッセージを出力できる関数をカスタマイズします。

function printArr(arr)
    if not arr then
        print('数组不能为空!')
    end
    for index, value in ipairs(arr) do
        print(value)
    end
end

4. マルチレベルキャッシュの実装

マルチレベル キャッシュの実装は Nginx プログラミングと切り離すことができず、Nginx プログラミングは OpenResty と切り離すことはできません。

4.1. OpenRestyのインストール

       OpenResty® は、Nginx をベースとした高性能 Web プラットフォームであり、超高同時実行性と高いスケーラビリティを処理できる動的 Web アプリケーション、Web サービス、動的ゲートウェイを簡単に構築するために使用されます。次のような特徴があります。

  • Nginx のすべての機能を備えています

  • Lua 言語に基づいて拡張され、多数の洗練された Lua ライブラリとサードパーティ モジュールが統合されています

  • Luaカスタム ビジネス ロジックカスタム ライブラリの使用を許可します。

公式ウェブサイト: OpenResty® - オープンソース公式ウェブサイト

具体的なインストール手順:

1.インストール

まず、Linux 仮想マシンがインターネットに接続されている必要があります

1) 開発ライブラリをインストールする

まず、OpenResty の依存関係開発ライブラリをインストールし、次のコマンドを実行します。

yum install -y pcre-devel openssl-devel gcc --skip-broken

2) OpenRestyリポジトリをインストールする

       CentOS システムにリポジトリを追加すると、 (コマンドopenrestyを使用して) パッケージの今後のインストールや更新を容易にすることができます。yum check-update次のコマンドを実行してリポジトリを追加します。

yum-config-manager --add-repo https://openresty.org/package/centos/openresty.repo

コマンドが存在しないと表示された場合は、次を実行します。

yum install -y yum-utils 

次に、上記のコマンドを繰り返します

3) OpenRestyをインストールする

次に、たとえば次のようにパッケージをインストールできますopenresty

yum install -y openresty

4) opm ツールをインストールする

opm は、サードパーティの Lua モジュールのインストールに役立つ OpenResty の管理ツールです。

コマンドラインツールをインストールしたい場合はopm、次のようにパッケージをインストールできますopenresty-opm

yum install -y openresty-opm

5) ディレクトリ構造

デフォルトでは、OpenResty がインストールされるディレクトリは /usr/local/openresty です。

 中にある nginx ディレクトリを見たことがありますか? OpenResty は、Nginx に基づいたいくつかの Lua モジュールを統合しています。

6) nginx 環境変数を構成する

設定ファイルを開きます。

vi /etc/profile

下部に 2 行を追加します。

export NGINX_HOME=/usr/local/openresty/nginx
export PATH=${NGINX_HOME}/sbin:$PATH

NGINX_HOME: OpenResty インストール ディレクトリの下の nginx ディレクトリが続きます

次に、設定を有効にします。

source /etc/profile

2. 起動して実行する

        OpenResty の最下層は Nginx に基づいています。OpenResty ディレクトリの nginx ディレクトリを確認してください。構造は基本的に Windows にインストールされている nginx と同じです。

 したがって、実行方法は基本的に nginx と同じです。

# 启动nginx
nginx
# 重新加载配置
nginx -s reload
# 停止
nginx -s stop

       nginx のデフォルト設定ファイルにはコメントが多すぎて、その後の編集に影響が出るので、ここでは nginx.conf 内のコメントを削除し、有効な部分は残しておきます。

/usr/local/openresty/nginx/conf/nginx.confファイルを次のように変更します。

#user  nobody;
worker_processes  1;
error_log  logs/error.log;

events {
    worker_connections  1024;
}

http {
    include       mime.types;
    default_type  application/octet-stream;
    sendfile        on;
    keepalive_timeout  65;

    server {
        listen       8081;
        server_name  localhost;
        location / {
            root   html;
            index  index.html index.htm;
        }
        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
            root   html;
        }
    }
}

Linux コンソールにコマンドを入力して、nginx を起動します。

nginx

次に、ページhttp://192.168.150.101:8081にアクセスします。IP アドレスが独自の仮想マシン IP に置き換えられることに注意してください。

3.備考

OpenResty の lua モジュールをロードします。

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

共通.lua

-- 封装函数,发送http请求,并解析响应
local function read_http(path, params)
    local resp = ngx.location.capture(path,{
        method = ngx.HTTP_GET,
        args = params,
    })
    if not resp then
        -- 记录错误信息,返回404
        ngx.log(ngx.ERR, "http not found, path: ", path , ", args: ", args)
        ngx.exit(404)
    end
    return resp.body
end
-- 将方法导出
local _M = {  
    read_http = read_http
}  
return _M

Redis 接続 API をリリースします。

-- 关闭redis连接的工具方法,其实是放入连接池
local function close_redis(red)
    local pool_max_idle_time = 10000 -- 连接的空闲时间,单位是毫秒
    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

Redis データを読み取るための API:

-- 查询redis的方法 ip和port是redis地址,key是查询的key
local function read_redis(ip, port, key)
    -- 获取一个连接
    local ok, err = red:connect(ip, port)
    if not ok then
        ngx.log(ngx.ERR, "连接redis失败 : ", err)
        return nil
    end
    -- 查询redis
    local resp, err = red:get(key)
    -- 查询失败处理
    if not resp then
        ngx.log(ngx.ERR, "查询Redis失败: ", err, ", key = " , key)
    end
    --得到的数据为空处理
    if resp == ngx.null then
        resp = nil
        ngx.log(ngx.ERR, "查询Redis数据为空, key = ", key)
    end
    close_redis(red)
    return resp
end

共有辞書をオンにします。

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

4.2.OpenRestyクイックスタート

私たちが達成したいと考えているマルチレベル キャッシュ アーキテクチャは次のとおりです。

で:

  • Windows 上の nginx は、製品クエリのフロントエンド Ajax リクエストを OpenResty クラスターにプロキシするリバース プロキシ サービスとして使用されます。

  • OpenResty クラスターはマルチレベル キャッシュ サービスの作成に使用されます

4.2.1.リバースプロキシ処理

       現在、商品詳細ページでは偽の商品データが使用されています。ただし、ブラウザーでは、ページが実際の商品データをクエリするための Ajax リクエストを開始していることがわかります。

リクエストは次のとおりです。

        リクエストのアドレスは localhost 、ポートは 80 で、Windows にインストールされている Nginx サービスによって受信されます。次に、プロキシが OpenResty クラスターに与えられました。

OpenResty でビジネスを記述し、製品データをクエリしてブラウザに返す必要があります。

しかし今回は、まず OpenResty でリクエストを受け取り、偽の商品データを返します。

:

次に、Nginx をローカルにダウンロードします (公式 Web サイトをダウンロードします)。Windows で実行している場合は、ダウンロードします (インストール パスは中国語以外のディレクトリです)。


#user  nobody;
worker_processes  1;

events {
    worker_connections  1024;
}

http {
    include       mime.types;
    default_type  application/octet-stream;

    sendfile        on;
    #tcp_nopush     on;
    keepalive_timeout  65;
    
    #代理服务器ip 端口
    upstream nginx-cluster{
        server 192.168.10.104:8081;
    }
    server {
        listen       80;
        server_name  localhost;
    #代理服务器的地址
	location /api {
            proxy_pass http://nginx-cluster;
        }
        #nginx的静态资源
        location / {
            root   html;
            index  index.html index.htm;
        }

        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
            root   html;
        }
    }
}

コンソールに入ります:

nginx   启动nginx
nginx -s reload  重新启动nginx
nginx -s stop  停止nginx

 (nginx を複数回起動しないでください。現在実行中の nginx の構成が独自に構成されたソース ファイルであることを確認してください)

4.2.2.OpenRestyはリクエストをリッスンします

       OpenResty の多くの機能は、ディレクトリ内の Lua ライブラリに依存しているため、nginx.conf で依存ライブラリのディレクトリを指定し、依存関係をインポートする必要があります。

1) 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) /api/item パスを監視する

      ファイルを変更し/usr/local/openresty/nginx/conf/nginx.conf、nginx.conf のサーバーの下にあるパス /api/item の監視を追加します。

location  /api/item {
    # 默认的响应类型
    default_type application/json;
    # 响应结果由lua/item.lua文件来决定
    content_by_lua_file lua/item.lua;
}

この監視はSpringMVC の@GetMapping("/api/item")パス マッピングに似ています。

これはcontent_by_lua_file lua/item.lua、 item.lua ファイルを呼び出し、その中でビジネスを実行し、結果をユーザーに返すことと同じです。Java でのサービスの呼び出しに相当します。

4.2.3. item.lua を書き込む

1)/usr/loca/openresty/nginxディレクトリ内にフォルダーを作成します: lua

 2)/usr/loca/openresty/nginx/luaフォルダー内に新しいファイル item.lua を作成します。

 3) item.lua を書き込み、偽のデータを返す

item.lua では、ngx.say() 関数を使用してデータを Response に返します。

ngx.say('{"id":10001,"name":"SALSA AIR","title":"RIMOWA 21寸托运箱拉杆箱 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を更新すると、効果が確認できます。

 4.3. リクエストパラメータの処理

       OpenRestyでフロントエンドリクエストを受信しましたが、偽のデータが返されます。実際のデータを返すには、フロントエンドから渡された製品IDに基づいて製品情報をクエリする必要があります。そこで、フロントエンドから渡された製品パラメータを取得するにはどうすればよいですか? ?

4.3.1. パラメータ取得用API

OpenResty は、さまざまな種類のフロントエンド リクエスト パラメーターを取得するための API をいくつか提供しています。

 4.3.2. パラメータを取得して返す

フロントエンドで開始される ajax リクエストは次の図に示されています。

 製品 ID がパス プレースホルダーとして渡されるため、正規表現一致を使用して ID を取得できることがわかります。

1) プロダクトIDを取得する

/usr/loca/openresty/nginx/nginx.confファイル内の /api/item を監視するコードを変更し、正規表現を使用して ID を取得します。

location ~ /api/item/(\d+) {
    # 默认的响应类型
    default_type application/json;
    # 响应结果由lua/item.lua文件来决定
    content_by_lua_file lua/item.lua;
}

2) ID を結合して返す

ファイルを変更し/usr/loca/openresty/nginx/lua/item.lua、ID を取得して、それを結果に結合して返します。

-- 获取商品id
local id = ngx.var[1]
-- 拼接并返回
ngx.say('{"id":' .. id .. ',"name":"SALSA AIR","title":"RIMOWA 21寸托运箱拉杆箱 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) リロードしてテストする

コマンドを実行して OpenResty 構成を再ロードします。

nginx -s reload

ページを更新すると、結果に ID が含まれていることがわかります。

 4.4. Tomcat のクエリ

       製品 ID を取得した後、キャッシュ内の製品情報をクエリする必要がありますが、現時点では nginx または redis キャッシュを確立していません。したがって、ここではまず Tomcat に移動して、製品 ID に基づいて製品情報をクエリします。図に示す部分を実装します。

  

       OpenResty は仮想マシン上にあり、Tomcat は Windows コンピュータ上にあることに注意してください。2 つの IP を間違えないでください。

 4.4.1. http リクエストを送信するための API

nginx は、http リクエストを送信するための内部 API を提供します。

local resp = ngx.location.capture("/path",{
    method = ngx.HTTP_GET,   -- 请求方式
    args = {a=1,b=2},  -- get方式传参数
})

返される応答コンテンツには次のものが含まれます。

  • resp.status: 応答ステータスコード

  • resp.header: 応答ヘッダー、テーブルです。

  • resp.body: 応答データである応答本文

注: ここでのパスはパスであり、IP とポートは含まれません。このリクエストは、nginx 内のサーバーによって監視および処理されます。

ただし、このリクエストを Tomcat サーバーに送信したいので、このパスをリバース プロキシするサーバーも作成する必要があります。

 location /path {
     # 这里是windows电脑的ip和Java服务端口,需要确保windows防火墙处于关闭状态
     proxy_pass http://192.168.150.1:8081; 
 }

原理は図に示すとおりです。

 4.4.2. http ツールのカプセル化

次に、HTTP リクエストを送信するツールをカプセル化し、ngx.location.capture に基づいて Tomcat をクエリします。

1) WindowsのJavaサービスにリバースプロキシを追加する

       item-service のインターフェイスはすべて /item で始まるため、/item パスを監視し、Windows 上の Tomcat サービスにプロキシします。

ファイルを変更して/usr/local/openresty/nginx/conf/nginx.conf場所を追加します。

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

 注: リバース プロキシを使用する場合、最初に ping を使用して、ネットワーク (プロキシのアドレス) に ping できるかどうかを確認できます。ローカル アドレス: 仮想マシン アドレスの最初の 3 桁は変更されず、最後の桁は 1 です。そして最後にローカルの Tomcat ポートに入ります。

      将来的には、 を呼び出す限りngx.location.capture("/item")、Windows の Tomcat サービスにリクエストを送信できるようになります。

2) パッケージツールクラス

前に述べたように、OpenResty は起動時に次の 2 つのディレクトリにツール ファイルを読み込みます。

したがって、カスタム http ツールもこのディレクトリに配置する必要があります。

/usr/local/openresty/lualibディレクトリ内に、新しい common.lua ファイルを作成します。

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

内容は以下の通りです。

-- 封装函数,发送http请求,并解析响应
local function read_http(path, params)
    local resp = ngx.location.capture(path,{
        method = ngx.HTTP_GET,
        args = params,
    })
    if not resp then
        -- 记录错误信息,返回404
        ngx.log(ngx.ERR, "http请求查询失败, path: ", path , ", args: ", args)
        ngx.exit(404)
    end
    return resp.body
end
-- 将方法导出
local _M = {  
    read_http = read_http
}  
return _M

       このツールは、read_http 関数をテーブル タイプ _M の変数にカプセル化し、それを返します。これはエクスポートと同様です。

require('common')これを使用すると、関数ライブラリをインポートするために使用できます。common は関数ライブラリのファイル名です。

3) 製品クエリを実装する

         最後に、ファイルを変更し/usr/local/openresty/lua/item.lua、カプセル化された関数ライブラリを使用して Tomcat にクエリを実行します。

-- 引入自定义common工具模块,返回值是common中返回的 _M
local common = require("common")
-- 从 common中获取read_http这个函数
local read_http = common.read_http
-- 获取路径参数
local id = ngx.var[1]
-- 根据id查询商品
local itemJSON = read_http("/item/".. id, nil)
-- 根据id查询商品库存
local itemStockJSON = read_http("/item/stock/".. id, nil)

       ここでクエリされた結果は json 文字列であり、product と inventory の 2 つの json 文字列が含まれています。ページで最終的に必要なのは、2 つの json 文字列を 1 つの json に結合することです。

そのためには、まず JSON を Lua テーブルに変換し、データ統合が完了した後に JSON に変換する必要があります。

4.4.3.CJSONツールクラス

OpenResty は、JSON のシリアル化と逆シリアル化を処理する cjson モジュールを提供します。

公式アドレス: GitHub - openresty/lua-cjson: Lua CJSON は Lua 用の高速 JSON エンコード/解析モジュールです

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. Tomcat クエリの実装

次に、前の item.lua のビジネスを変更し、json 処理関数を追加します。

-- 导入common函数库
local common = require('common')
local read_http = common.read_http
-- 导入cjson库
local cjson = require('cjson')

-- 获取路径参数
local id = ngx.var[1]
-- 根据id查询商品
local itemJSON = read_http("/item/".. id, nil)
-- 根据id查询商品库存
local itemStockJSON = read_http("/item/stock/".. id, nil)

-- JSON转化为lua的table
local item = cjson.decode(itemJSON)
local itemStock = cjson.decode(itemStockJSON)  -- 改为itemStockJSON

-- 组合数据
item.stock = itemStock.stock  -- 改为itemStock
item.sold = itemStock.sold    -- 改为itemStock

-- 把item序列化为json 返回结果
ngx.say(cjson.encode(item))

4.4.5. ID ベースのロードバランシング

先ほどのコードでは、Tomcat が 1 台のマシンにデプロイされています。実際の開発では、Tomcat はクラスター モードである必要があります。

 したがって、OpenResty は Tomcat クラスターの負荷分散を行う必要があります。

デフォルトのロード バランシング ルールはポーリング モードです。/item/10001 をクエリすると、次のようになります。

  • ポート 8081 で Tomcat サービスに初めてアクセスすると、サービス内に JVM プロセス キャッシュが形成されます。

  • 2 回目はポート 8082 で Tomcat サービスにアクセスします。サービス内には JVM キャッシュがなく (JVM キャッシュは共有できないため)、データベースがクエリされます。

  • ...

      ポーリングにより、初めて 8081 をクエリして形成された JVM キャッシュは、次に 8081 がアクセスされるまで有効になりません。キャッシュ ヒット率が低すぎます。どうすればよいですか? 同じ製品がクエリされるたびに同じ Tomcat サービスにアクセスできる場合は、JVM キャッシュが確実に有効になるため、ポーリングではなく製品 ID に基づいて負荷分散を行う必要があります。

1) 原則

nginx は、リクエスト パスに基づいて負荷分散アルゴリズムを提供します。

nginx は、リクエスト パスに基づいてハッシュ演算を実行し、取得した値の tomcat サービスの数の余りを取得し、余りが何であっても、どのサービスにアクセスして負荷分散を実現します。

例えば:

  • リクエストのパスは /item/10001 です

  • トムキャットの総数は 2 匹 (8081、8082)

  • リクエスト パス /item/1001 のハッシュ演算の残りは 1 です

  • 次に、最初の Tomcat サービス (8081) にアクセスします。

       ID が変更されず、各ハッシュ操作の結果が変わらない限り、同じ製品が常に同じ Tomcat サービスにアクセスし、JVM キャッシュが有効になることが保証されます。

2) 実感する

/usr/local/openresty/nginx/conf/nginx.confID に基づいて負荷分散を実装するようにファイルを変更します。

まず、Tomcat クラスターを定義し、パスベースの負荷分散を設定します。

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

次に、ターゲットが Tomcat クラスターを指すように Tomcat サービスのリバース プロキシを変更します。

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

OpenRestyをリロードする

nginx -s reload

3) テスト

2 つの Tomcat サービスを開始します。

 同時に開始:

 ログをクリアしてページに再度アクセスすると、別の ID を持つ製品が表示され、別の Tomcat サービスにアクセスできるようになります。

 4.5.Redis キャッシュのウォームアップ

Redis キャッシュはコールド スタートの問題に直面します。

コールド スタート: サービスが開始されたばかりのとき、Redis にはキャッシュがありません。最初のクエリ中にすべての製品データがキャッシュされると、データベースに大きな負荷がかかる可能性があります。

キャッシュのウォームアップ: 実際の開発では、ビッグデータを使用してユーザーがアクセスしたホットデータをカウントし、これらのホットデータを事前にクエリし、プロジェクトの開始時にそれらを Redis に保存できます。統計関連関数 現在、起動時にすべてのデータをキャッシュに入れることが可能です。

1) Docker を使用して Redis をインストールする

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

2) Redis 依存関係を item-service サービスに導入する

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

3) Redis アドレスを構成する

spring:
  redis:
    host: 192.168.150.101

4) 初期化クラスの書き込み

キャッシュの予熱はプロジェクトの開始時に完了する必要があり、RedisTemplate を取得した後に実行する必要があります。

       ここでは、InitializingBean インターフェースを使用して実装します。これは、InitializingBean は、Spring によってオブジェクトが作成され、すべてのメンバー変数が注入された後に実行できるためです。


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 キャッシュのクエリ

       Redis キャッシュの準備ができたので、OpenResty で Redis をクエリするロジックを実装できます。以下の図の赤枠内に示すように、

リクエストが OpenResty に入った後:

  • 最初に Redis キャッシュにクエリを実行する

  • Redis キャッシュが見つからない場合は、Tomcat を再度クエリします。

4.6.1. Redis ツールのカプセル化

       OpenRestyではRedisを操作するためのモジュールが提供されており、モジュールを導入すればそのまま利用することができます。ただし、便宜上、Redis 操作を以前の common.lua ツール ライブラリにカプセル化します。

ファイルを変更します/usr/local/openresty/lualib/common.lua:

1) Redis モジュールを導入し、Redis オブジェクトを初期化します。

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

2) Redis 接続を解放するために使用されるカプセル化機能は、実際には接続プールに入れられます。

-- 关闭redis连接的工具方法,其实是放入连接池
local function close_redis(red)
    local pool_max_idle_time = 10000 -- 连接的空闲时间,单位是毫秒
    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) キーに基づいて Redis データをクエリする関数をカプセル化します。

-- 查询redis的方法 ip和port是redis地址,key是查询的key
local function read_redis(ip, port, key)
    -- 获取一个连接
    local ok, err = red:connect(ip, port)
    if not ok then
        ngx.log(ngx.ERR, "连接redis失败 : ", err)
        return nil
    end
    -- 查询redis
    local resp, err = red:get(key)
    -- 查询失败处理
    if not resp then
        ngx.log(ngx.ERR, "查询Redis失败: ", err, ", key = " , key)
    end
    --得到的数据为空处理
    if resp == ngx.null then
        resp = nil
        ngx.log(ngx.ERR, "查询Redis数据为空, key = ", key)
    end
    close_redis(red)
    return resp
end

4) 輸出

-- 将方法导出
local _M = {  
    read_http = read_http,
    read_redis = read_redis
}  
return _M

common.lua を完了します。

-- 导入redis
local redis = require('resty.redis')
-- 初始化redis
local red = redis:new()
red:set_timeouts(1000, 1000, 1000)
​
-- 关闭redis连接的工具方法,其实是放入连接池
local function close_redis(red)
    local pool_max_idle_time = 10000 -- 连接的空闲时间,单位是毫秒
    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
​
-- 查询redis的方法 ip和port是redis地址,key是查询的key
local function read_redis(ip, port, key)
    -- 获取一个连接
    local ok, err = red:connect(ip, port)
    if not ok then
        ngx.log(ngx.ERR, "连接redis失败 : ", err)
        return nil
    end
    -- 查询redis
    local resp, err = red:get(key)
    -- 查询失败处理
    if not resp then
        ngx.log(ngx.ERR, "查询Redis失败: ", err, ", key = " , key)
    end
    --得到的数据为空处理
    if resp == ngx.null then
        resp = nil
        ngx.log(ngx.ERR, "查询Redis数据为空, key = ", key)
    end
    close_redis(red)
    return resp
end
​
-- 封装函数,发送http请求,并解析响应
local function read_http(path, params)
    local resp = ngx.location.capture(path,{
        method = ngx.HTTP_GET,
        args = params,
    })
    if not resp then
        -- 记录错误信息,返回404
        ngx.log(ngx.ERR, "http查询失败, path: ", path , ", args: ", args)
        ngx.exit(404)
    end
    return resp.body
end
-- 将方法导出
local _M = {  
    read_http = read_http,
    read_redis = read_redis
}  
return _M

4.6.2. Redis クエリの実装

次に、Redis をクエリするために item.lua ファイルを変更できます。

クエリロジックは次のとおりです。

  • ID に基づいて Redis をクエリする

  • クエリが失敗した場合は、Tomcat のクエリを続行します。

  • クエリ結果を返す

1)/usr/local/openresty/lua/item.luaファイルを変更し、クエリ関数を追加します。

-- 导入common函数库
local common = require('common')
local read_http = common.read_http
local read_redis = common.read_redis
-- 封装查询函数
function read_data(key, path, params)
    -- 查询本地缓存
    local val = read_redis("127.0.0.1", 6379, key)
    -- 判断查询结果
    if not val then
        ngx.log(ngx.ERR, "redis查询失败,尝试查询http, key: ", key)
        -- redis查询失败,去查询http
        val = read_http(path, params)
    end
    -- 返回数据
    return val
end

2) 次に、商品問い合わせと在庫問い合わせの業務を変更します。

 3) item.lua コードを完成させます:

-- 导入common函数库
local common = require('common')
local read_http = common.read_http
local read_redis = common.read_redis
-- 导入cjson库
local cjson = require('cjson')
​
-- 封装查询函数
function read_data(key, path, params)
    -- 查询本地缓存
    local val = read_redis("127.0.0.1", 6379, key)
    -- 判断查询结果
    if not val then
        ngx.log(ngx.ERR, "redis查询失败,尝试查询http, key: ", key)
        -- redis查询失败,去查询http
        val = read_http(path, params)
    end
    -- 返回数据
    return val
end
​
-- 获取路径参数
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转化为lua的table
local item = cjson.decode(itemJSON)
local stock = cjson.decode(stockJSON)
-- 组合数据
item.stock = stock.stock
item.sold = stock.sold
​
-- 把item序列化为json 返回结果
ngx.say(cjson.encode(item))

4.7.Nginx ローカル キャッシュ

現在、マルチレベル キャッシュ全体で最後のリンクだけが失われています。これは nginx のローカル キャッシュです。図に示すように:

 4.7.1.ローカルキャッシュAPI

       OpenRestyは、nginxの複数のワーカー間でデータを共有し、キャッシュ機能を実装できるNginx用のshard dict機能を提供します。

1) 共有辞書を有効にし、nginx.conf の http に設定を追加します。

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

2) 共有辞書を操作します。

-- 获取本地缓存对象
local item_cache = ngx.shared.item_cache
-- 存储, 指定key、value、过期时间,单位s,默认为0代表永不过期
item_cache:set('key', 'value', 1000)
-- 读取
local val = item_cache:get('key')

4.7.2. ローカルキャッシュクエリの実装

1)/usr/local/openresty/lua/item.luaファイルを変更し、read_data クエリ関数を変更し、ローカル キャッシュ ロジックを追加します。

-- 导入共享词典,本地缓存
local item_cache = ngx.shared.item_cache
​
-- 封装查询函数
function read_data(key, expire, path, params)
    -- 查询本地缓存
    local val = item_cache:get(key)
    if not val then
        ngx.log(ngx.ERR, "本地缓存查询失败,尝试查询Redis, key: ", key)
        -- 查询redis
        val = read_redis("127.0.0.1", 6379, key)
        -- 判断查询结果
        if not val then
            ngx.log(ngx.ERR, "redis查询失败,尝试查询http, key: ", key)
            -- redis查询失败,去查询http
            val = read_http(path, params)
        end
    end
    -- 查询成功,把数据写入本地缓存
    item_cache:set(key, val, expire)
    -- 返回数据
    return val
end

2) item.lua で製品と在庫をクエリするビジネスを変更し、最新の read_data 関数を実装します。

       実際には、さらに多くのキャッシュ時間パラメータがあります。有効期限が切れると、nginx キャッシュは自動的に削除されます。キャッシュは、次回アクセスするときに更新できます。ここでは、基本的な製品情報のタイムアウト時間を 30 分に設定し、インベントリインベントリの更新頻度が高いため、キャッシュ時間が長すぎるとデータベースと大きく異なる可能性があります。

3) item.lua ファイルを完成させます。

-- 导入common函数库
local common = require('common')
local read_http = common.read_http
local read_redis = common.read_redis
-- 导入cjson库
local cjson = require('cjson')
-- 导入共享词典,本地缓存
local item_cache = ngx.shared.item_cache
​
-- 封装查询函数
function read_data(key, expire, path, params)
    -- 查询本地缓存
    local val = item_cache:get(key)
    if not val then
        ngx.log(ngx.ERR, "本地缓存查询失败,尝试查询Redis, key: ", key)
        -- 查询redis
        val = read_redis("127.0.0.1", 6379, key)
        -- 判断查询结果
        if not val then
            ngx.log(ngx.ERR, "redis查询失败,尝试查询http, key: ", key)
            -- redis查询失败,去查询http
            val = read_http(path, params)
        end
    end
    -- 查询成功,把数据写入本地缓存
    item_cache:set(key, val, expire)
    -- 返回数据
    return val
end
​
-- 获取路径参数
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转化为lua的table
local item = cjson.decode(itemJSON)
local stock = cjson.decode(stockJSON)
-- 组合数据
item.stock = stock.stock
item.sold = stock.sold
​
-- 把item序列化为json 返回结果
ngx.say(cjson.encode(item))

5. キャッシュの同期

       ほとんどの場合、ブラウザはキャッシュされたデータをクエリします。キャッシュされたデータとデータベースのデータに大きな違いがある場合、重大な結果を引き起こす可能性があります。そのため、データベースのデータとキャッシュされたデータの一貫性を確保する必要があります。これがキャッシュです。データベースとの同期。

5.1. データ同期戦略

キャッシュ データを同期するには、次の 3 つの一般的な方法があります。

有効期間の設定: キャッシュの有効期間を設定します。有効期限が切れると自動的に削除されます。再度クエリするときに更新します

  • 利点: シンプルで便利

  • 短所: 適時性が低い、有効期限が切れる前にキャッシュが不整合になる可能性がある

  • シナリオ: 更新頻度が低く、適時性要件が低いビジネス

同期二重書き込み: データベースを変更しながらキャッシュを直接変更します。

  • 利点: 強力な適時性、キャッシュとデータベース間の強力な一貫性

  • 短所: コードの侵入と高い結合。

  • シナリオ: 高い一貫性と適時性の要件を備えたデータのキャッシュ

非同期通知:データベースが変更されるとイベント通知が送信され、関連するサービスは通知をリッスンした後にキャッシュされたデータを変更します。

  • 利点: 低結合、複数のキャッシュ サービスに同時に通知可能

  • 短所: 適時性は平均的ですが、途中で不一致が発生する可能性があります。

  • シナリオ: 適時性の要件は平均的で、同期する必要があるサービスが複数あります。

非同期実装は、MQ または Canal に基づいて実装できます。

1) MQ ベースの非同期通知:

 解釈:

  • 製品サービスはデータの変更を完了したら、MQ にメッセージを送信するだけで済みます。

  • キャッシュ サービスは MQ メッセージをリッスンし、キャッシュへの更新を完了します。

まだ少量のコード侵入が存在します。

2) カナルベースの通知

解釈:

  • 製品サービスによる製品の変更が完了すると、コードの侵入は一切なく、ビジネスは直接終了します。

  • Canal は MySQL の変更を監視し、変更が発見されるとすぐにキャッシュ サービスに通知します。

  • キャッシュ サービスは運河通知を受信し、キャッシュを更新します。

コード侵入ゼロ

5.2.運河の設置

5.2.1.運河について知る

Canal [kə'næl]、水路/パイプライン/溝と訳される運河は、Alibaba 傘下のオープンソース プロジェクトであり、Java に基づいて開発されています。データベースの増分ログ分析に基づいて、増分データのサブスクリプションと消費が提供されます。GitHub アドレス: GitHub - alibaba/canal: Alibaba MySQL binlog 増分サブスクリプションおよび消費コンポーネント

Canal は mysql のマスター/スレーブ同期に基づいて実装されており、MySQL のマスター/スレーブ同期の原理は次のとおりです。

  • 1) MySQL マスターはデータの変更をバイナリ ログ (バイナリ ログ) に書き込み、記録されたデータはバイナリ ログ イベントと呼ばれます。

  • 2) MySQL スレーブは、マスターのバイナリ ログ イベントをリレー ログ (リレー ログ) にコピーします。

  • 3) MySQL スレーブはリレー ログ内のイベントを再生し、データの変更を自身のデータに反映します。

       Canal は自身を MySQL のスレーブ ノードとして偽装し、マスターのバイナリ ログの変更を監視します。そして、取得した変更情報をCanalクライアントに通知し、他のデータベースの同期が完了します。

 5.2.2.運河の設置

次に、mysql のマスター/スレーブ同期メカニズムを有効にして、Canal にスレーブをシミュレートさせます。

1. MySQL マスター/スレーブを開始します

Canal は MySQL のマスター/スレーブ同期機能をベースとしているため、最初に MySQL のマスター/スレーブ機能を有効にする必要があります。

以前に Docker で実行された mysql の例を次に示します。

1.1. バイナリログの開始

mysql コンテナによってマウントされたログ ファイルを開きます。私のログ ファイルは次の/tmp/mysql/confディレクトリにあります。

 ファイルを変更します:

vi /tmp/mysql/conf/my.cnf

コンテンツを追加します:

log-bin=/var/lib/mysql/mysql-bin
binlog-do-db=heima

構成の解釈:

  • log-bin=/var/lib/mysql/mysql-bin: mysql-bin というバイナリ ログ ファイルの保存先アドレスとファイル名を設定します。

  • binlog-do-db=heima: バイナリ ログ イベントを記録するデータベースを指定します。ここには heima ライブラリが記録されます。

最終的な効果:

[mysqld]
skip-name-resolve
character_set_server=utf8
datadir=/var/lib/mysql
server-id=1000
log-bin=/var/lib/mysql/mysql-bin
binlog-do-db=heima
1.2.ユーザー権限の設定

次に、データ同期専用のアカウントを追加しますが、ここではセキュリティ上の理由からheimaライブラリの操作権限のみを付与します。

create user canal@'%' IDENTIFIED by 'canal';
GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT,SUPER ON *.* TO 'canal'@'%' identified by 'canal';
FLUSH PRIVILEGES;

mysqlコンテナを再起動するだけです

docker restart mysql

設定が成功したかどうかをテストします。mysql コンソールまたは Navicat で、次のコマンドを入力します。

show master status;

 2.運河の設置

2.1.ネットワークの作成

ネットワークを作成し、MySQL、Canal、および MQ を同じ Docker ネットワークに配置する必要があります。

docker network create heima

mysql をこのネットワークに参加させます。

docker network connect heima mysql
2.3.運河の設置

canal の画像圧縮パッケージは公式 Web サイトからダウンロードできます。

これを仮想マシンにアップロードし、次のコマンドを使用してインポートできます。

docker load -i canal.tar

次に、コマンドを実行して Canal コンテナを作成します。

docker run -p 11111:11111 --name canal \
-e canal.destinations=heima \
-e canal.instance.master.address=mysql:3306  \
-e canal.instance.dbUsername=canal  \
-e canal.instance.dbPassword=canal  \
-e canal.instance.connectionCharset=UTF-8 \
-e canal.instance.tsdb.enable=true \
-e canal.instance.gtidon=false  \
-e canal.instance.filter.regex=heima\\..* \
--network heima \
-d canal/canal-server:v1.1.5

例証します:

  • -p 11111:11111: これは運河のデフォルトのリスニングポートです

  • -e canal.instance.master.address=mysql:3306: データベースのアドレスとポート。mysql コンテナのアドレスがわからない場合は、次の方法でdocker inspect 容器id確認できます。

  • -e canal.instance.dbUsername=canal:データベースのユーザー名

  • -e canal.instance.dbPassword=canal: データベースのパスワード

  • -e canal.instance.filter.regex=: 監視対象テーブル名

テーブル名のリスニングでサポートされている構文:

MySQL データ解析はテーブル、Perl 正規表現に重点を置いています。
複数の正規表現はカンマ (,) で区切られ、エスケープ文字には二重スラッシュ (\\) が必要です。一般的な例: 
1.
すべてのテーブル: .* または .*\\ 。 .* 
2. canal スキーマの下のすべてのテーブル: canal\\..* 
3. canal の下の canal で始まるテーブル: canal\\.canal.* 
4. canal スキーマの下の 1 つのテーブル: canal.test1 
5. 複数のルールを組み合わせて使用​​するカンマで区切ります: canal\\..*,mysql.test1,mysql.test2

5.3. モニター運河

Canal はさまざまな言語でクライアントを提供しており、Canal がバイナリログの変更を監視すると、Canal のクライアントに通知します。

       Canal が提供する Java クライアントを使用して、Canal 通知メッセージをリッスンできます。変更メッセージを受信すると、キャッシュが更新されます。

       ただし、ここでは GitHub 上のサードパーティのオープンソース canal-starter クライアントを使用します。アドレス: GitHub - NormanGyllenhaal/canal-client: スプリングブート カナルスターター 使いやすいカナルクライアント カナルクライアント

SpringBoot と完全に統合され、自動的に組み立てられるため、公式クライアントよりもはるかにシンプルで使いやすいです。

5.3.1.依存関係を導入します。

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

5.3.2.設定の書き込み:

canal:
  destination: heima # canal的集群名字,要与安装canal时设置的名称一致
  server: 192.168.150.101:11111 # canal服务地址

5.3.3. 項目エンティティクラスの変更

@Id、@Column、その他の注釈を使用して、Item フィールドとデータベース テーブル フィールドの間のマッピングを完了します。

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;//商品标题
    private Long price;//价格(分)
    private String image;//商品图片
    private String category;//分类名称
    private String brand;//品牌名称
    private String spec;//规格
    private Integer status;//商品状态 1-正常,2-下架
    private Date createTime;//创建时间
    private Date updateTime;//更新时间
    @TableField(exist = false)
    @Transient
    private Integer stock;
    @TableField(exist = false)
    @Transient
    private Integer sold;
}

5.3.4. リスナーの作成

Canal メッセージをリッスンするインターフェイスを実装してEntryHandler<T>リスナーを作成します。次の 2 つの点に注意してください。

  • 実装クラスは@CanalTable("tb_item")監視するテーブル情報を指定します

  • EntryHandler のジェネリック型は、テーブルに対応するエンティティ クラスです。

```java
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) {
        // 写数据到JVM进程缓存
        itemCache.put(item.getId(), item);
        // 写数据到redis
        redisHandler.saveItem(item);
    }

    @Override
    public void update(Item before, Item after) {
        // 写数据到JVM进程缓存
        itemCache.put(after.getId(), after);
        // 写数据到redis
        redisHandler.saveItem(after);
    }

    @Override
    public void delete(Item item) {
        // 删除数据到JVM进程缓存
        itemCache.invalidate(item.getId());
        // 删除数据到redis
        redisHandler.deleteItemById(item.getId());
    }
}
```

      ここでの Redis 上の操作は、キャッシュの予熱を行うときに作成したクラスである RedisHandler オブジェクトにカプセル化されており、その内容は次のとおりです。

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);
    }
}

おすすめ

転載: blog.csdn.net/dfdbb6b/article/details/132436118