【事例】高同時ビジネス向けマルチレベルキャッシュアーキテクチャの整合性ソリューション

同時実行性の高いプロジェクトでは基本的にキャッシュと切り離せないので、キャッシュの導入以降、キャッシュとデータベースの間のデータの整合性の問題が発生します。

まず最初に、同時実行性の高いプロジェクトにおける Redis の 3 つの一般的なキャッシュ読み取りおよび書き込みモードを見てみましょう。

ここに画像の説明を挿入

キャッシュアサイド

读写分离模式,是最常见的Redis缓存模式,多数采用。
读写数据时需要先查找缓存,如果缓存中没有,则从数据库中查找数据。
如果查询到数据,需要将数据放到缓存中,下次访问再直接从缓存中获取数据,以提高访问效率。
写操作通常不会直接更新缓存,而是删除缓存,因为存储结构是hash、list,则更新数据需要遍历。
  • アドバンテージ
    • 読み取り効率が高く、キャッシュ ヒット率が高く、書き込み操作がデータベースと同期し、データの一貫性が高く、実装が比較的簡単です。
  • 欠点がある
    • データベースとキャッシュの間にデータの不整合があり、キャッシュの無効化やデータベースの更新操作によるキャッシュの不整合を考慮する必要があります。
  • アプリケーションシナリオ
    • これは、読み取り操作が非常に頻繁で書き込み操作が比較的少ない状況に適しています。たとえば、電子商取引 Web サイトの商品詳細ページでは、読み取り操作の数が更新および追加操作の数よりもはるかに多いです。

リード/ライトスルー

读写穿透模式,读写操作会直接修改缓存,然后再同步更新数据库中的数据,开发较为复杂,一般少用。
在Read/Write  Through模式下,每次数据的读写操作都会操作缓存,再同步到数据库,以保证缓存和数据库数据的一致性。
应用程序将缓存作为主要的数据源,数据库对于应用程序是透明的,更新数据库和从数据库的读取的任务都交给缓存来实现。
  • アドバンテージ
    • 書き込み動作速度が速く、一貫性が高く、キャッシュとデータベース内のデータに一貫性があり、キャッシュのヒット率が高くなります。
  • 欠点がある
    • 読み取り操作が遅い。キャッシュに利用可能なデータがない場合、毎回データベース クエリが実行されます。データ量が多い場合、パフォーマンスへの影響が大きくなります。
  • アプリケーションシナリオ
    • システムは、クラウド ストレージ Ceph など、頻繁な書き込み操作とまれな読み取り操作を伴うシナリオを処理します。

後書き

被称为Write Back模式或异步写入模式,一般较少使用。
如果有写操作,缓存会记录修改了缓存的数据,但是并不会立即同步到数据库中。
一般会把缓存中的数据更新到磁盘中,等到后续有查询数据操作时,再异步批量更新数据库中的数据。
该模式的优点就是写操作速度很快,不会对性能产生影响,同时也避免了频繁更新数据库的情况,提升了数据库性能。
  • アドバンテージ
    • 書き込み操作は高速で、パフォーマンスが高く、データの一貫性も一般的に高いです。
  • 欠点がある
    • 読み取り操作は遅く、データベースを非同期で更新するため、データの遅延が発生する可能性があります。
  • アプリケーションシナリオ:
    • ゲーム内のユーザー アクティビティ ポイントやその他の情報など、データの読み取りと書き込みの比率が高いシナリオで使用され、最初の書き込み操作のパフォーマンスは非常に高く、その後のクエリは比較的小さいです。

業務開発ではデータベースからキャッシュへの読み込みが基本ですが、キャッシュからデータベースへの読み込みと書き込みの順序はどうなっているのでしょうか?

シナリオ 1: 最初にデータベースを更新し、次にキャッシュを更新します

  • スレッド A がデータベースを更新します。データベースを更新した後、スレッド A がキャッシュを更新します。キャッシュは正常に更新されますが、スレッド A のデータベース トランザクションのコミットが失敗するか、メソッド本体が異常であるため、ロールバックが実行されます。キャッシュとデータベースの不整合が発生する可能性があります。

ここに画像の説明を挿入

シナリオ 2: 最初にキャッシュを削除してからデータベースを更新する

  • スレッド A はキャッシュを削除し、データベースを更新しますが、まだコミットしていません。このとき、スレッド B はキャッシュにアクセスし、データがないことを発見し、データベースにアクセスしてコミットされていないデータを読み取り、それをキャッシュ、つまり古いデータに置きます。スレッド A がコミット操作を実行しました。これにより、キャッシュは古いデータになり、データベースは新しいデータになり、不整合が発生します。

ここに画像の説明を挿入

シナリオ 3: 最初にキャッシュを削除し、次にデータベースを更新してからキャッシュを削除します

  • スレッド A はキャッシュを削除し、データベースを更新しますが、まだコミットしていません。スレッド B はキャッシュにアクセスし、データがないことがわかり、データベースにアクセスしてコミットされていないデータを読み取り、キャッシュ (古いデータ) に置きます。スレッド A がコミット操作を実行しました。スレッド A はキャッシュ データを再度削除します (この時点ではキャッシュは空であり、後続の読み取りは最新のデータになります)。データの一貫性は保証されますが、複数の IO が無駄になります。これは、毎回 Redis をもう一度削除することと同じです。

ここに画像の説明を挿入

さて、本題に戻りましょう。マルチレベル キャッシュ アーキテクチャとは何ですか?

マルチレベル キャッシュ アーキテクチャは、通常、アプリケーションのパフォーマンスを最適化するために使用される高可用性キャッシュ テクノロジであり、通常、複数のキャッシュ層で構成され、キャッシュの各層は、そのさまざまな特性や機能に応じて選択および調整できます。データの複数のコピーをキャッシュし、キャッシュの侵入、キャッシュの破壊、データの不整合を回避することで、アプリケーションのパフォーマンスと可用性を最大化します。

ここでは、Nginx+Lua+Canal+Redis+Mysqlアーキテクチャを使用します。つまり、読み取り操作は Lua を介して Nginx キャッシュにクエリを実行します。Nginx キャッシュにデータがない場合は、Redis キャッシュにクエリを実行します。Redis キャッシュにデータがない場合は、Redis キャッシュにデータがクエリされます。データがない場合は、 mysql に直接クエリします書き込み操作中、Canal はデータベース内の指定されたテーブルの増分変更を監視し、Java プログラムは Canal によって監視されている増分変更を消費して Redis に書き込みます。Java-canal プログラムは Redis キャッシュを操作し、Nginx ローカル キャッシュが適用されるか無効化されるかはプロジェクトの種類によって異なります。

ここに画像の説明を挿入

さて、運河って何ですか?

Alibaba の MySQL ベースの増分ログ解析およびサブスクリプション発行システムは、主にデータのサブスクリプションと消費の問題を解決するために使用されます。Canal は主に MySQL のバイナリログ分析をサポートしており、分析完了後に取得された関連データを処理するために Canal クライアントが使用されます。

初期の頃、杭州と米国にデュアル コンピューター ルームが導入されたため、コンピューター ルーム間の同期に対するビジネス要件があり、その実装方法は主にビジネス トリガーに基づいて段階的な変更を取得していました。2010 年以来、このビジネスは同期のための増分変更を取得するためにデータベース ログを解析することを徐々に試みており、そこから多数の増分データベース サブスクリプションおよび消費ビジネスが派生してきました。

  • ログの増分サブスクリプションと消費に基づくサービスには次のものがあります。

    • データベースミラーリング
    • データベースのリアルタイムバックアップ
    • インデックスの構築とリアルタイムのメンテナンス (分割異種インデックス、転置インデックスなど)
    • ビジネスキャッシュの更新
    • ビジネスロジックによる増分データ処理

現在の運河は、 5.1.x 、 5.5.x 、 5.6.x 、 5.7.x 、 8.0.x を含むソース MySQL バージョンをサポートしています。

ここに画像の説明を挿入

  • Canal は、MySQL スレーブのインタラクティブ プロトコルをシミュレートし、MySQL スレーブのふりをして、ダンプ プロトコルを MySQL マスターに送信します。
  • MySQL マスターはダンプ リクエストを受信し、バイナリ ログをスレーブ (つまり運河) にプッシュし始めます。
  • Canal はバイナリ ログ オブジェクト (元はバイト ストリーム) を解析し、挿入、更新、削除などの独立したデータ操作イベントに解析します。

環境整備

さて、前編では主にマルチレベルキャッシュアーキテクチャについて紹介しました。その後、いくつかの環境準備をしていきます。

まず MySQL をデプロイします。ここでは、Docker コンテナ化された方法で MySQL をデプロイします。デプロイメントが完了したら、MySQL の binlog ログを有効にする必要があります。

#创建目录
mkdir -p /home/data/mysql/

#部署
docker run \
    -p 3306:3306 \
    -e MYSQL_ROOT_PASSWORD=123456 \
    -v /home/data/mysql/conf:/etc/mysql/conf.d \
    -v /home/data/mysql/data:/var/lib/mysql:rw \
    --name mysql_test \
    --restart=always \
    -d mysql:8.0

設定ファイルを編集しmy.cnf、mysqld モジュールの下で、編集後に mysql サービスを再起動します。

# 开启 binlog, 可以不加,默认开启
log-bin=mysql-bin

# 选择 ROW 模式
binlog_format=row

#server_id不要和canal的slaveId重复
server-id=1

ここに画像の説明を挿入

以下は、MySQL の 3 つのバイナリ モードの紹介です。

STATEMENTフォーマット

Statement-Based Replication,SBR,每一条会修改数据的 SQL 都会记录在 binlog 中。
每一条会修改数据 SQL 都会记录在 binlog 中,性能高,发生的变更操作只记录所执行的 SQL 语句,而不记录具体变更的值。
不需要记录每一行数据的变化,极大的减少了 binlog 的日志量,避免了大量的 IO 操作,提升了系统的性能。
由于 Statement 模式只记录 SQL,而如果一些 SQL 中 包含了函数,那么可能会出现执行结果不一致的情况。
缺点:uuid() 函数,每次执行都会生成随机字符串,在 master 中记录了 uuid,当同步到 slave 后再次执行,结果不一样,now()之类的函数以及获取系统参数的操作, 都会出现主从数据不同步的问题。

ROW形式 (デフォルト)

Row-Based Replication,RBR,不记录 SQL 语句上下文信息,仅保存哪条记录被修改。
Row 格式不记录 SQL 语句上下文相关信息,仅记录某一条记录被修改成什么样子。
清楚地记录下每一行数据修改的细节,不会出现 Statement 中存在的那种数据无法被正常复制的情况,保证最高精度和粒度。
缺点:Row 格式存在问题,就是日志量太大,批量 update、整表 delete、alter 表等操作,要记录每一行数据的变化,此时会产生大量的日志,大量的日志也会带来 IO 性能问题。

MIXEDフォーマット

在 STATEMENT 和 ROW 之间自动进行切换的模式。在没有大量变更时使用 STATEMENT 格式。
而在发生大量变更时使用 ROW 格式,以确保日志具有高精度和粒度,同时保证存储空间的有效使用。

次のコマンドを実行して、binlog が有効になっているかどうかを確認します。SHOW VARIABLES LIKE 'log_bin'

ここに画像の説明を挿入

データベースの認可

-- 创建同步用户
CREATE USER 'canal'@'%';
-- 设置密码
ALTER USER 'canal'@'%' IDENTIFIED WITH mysql_native_password BY '123456';
-- 授予复制权限
GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'canal'@'%';
-- 刷新权限
FLUSH PRIVILEGES;

ここに画像の説明を挿入

次に、Redis をデプロイします。これも docker でデプロイされますが、これは非常に簡単です。

docker run -itd --name redis -p 6379:6379 \
--privileged=true \
-v /redis/data:/data --restart always redis \
--requirepass "psd"

ここに画像の説明を挿入

次に、canal-server をデプロイしましょう。docker デプロイメントも使用します。

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

コンテナに入り、構成ファイルを変更します

docker exec -it canal /bin/bash

# 修改配置文件
vi canal-server/conf/example/instance.properties

#################################################
## mysql serverId , 修改id,不要和mysql 主节点一致即可----------
canal.instance.mysql.slaveId=2
canal.instance.gtidon=false

# 修改 mysql 主节点的ip----------
canal.instance.master.address=ip:3306
canal.instance.tsdb.enable=true

# username/password 授权的数据库账号密码----------
canal.instance.dbUsername=canal
canal.instance.dbPassword=123456
canal.instance.connectionCharset = UTF-8
canal.instance.enableDruid=false

# mysql 数据解析关注的表,正则表达式. 多个正则之间以逗号(,)分隔,转义符需要双斜杠 \\,所有表:.* 或 .*\\..*
canal.instance.filter.regex=.*\\..*
canal.instance.filter.black.regex=

コンテナを再起動し、コンテナに入ってログを表示します。

docker restart canal

ここに画像の説明を挿入

docker exec -it canal /bin/bash

tail -100f canal-server/logs/example/example.log

ここに画像の説明を挿入

次に、Nginx の最後の項目をデプロイしましょう。ここでは、Nginx と Lua をカバーする OpenResty を直接デプロイします。

以下のコマンドを順番に実行します。

# add the yum repo:
wget https://openresty.org/package/centos/openresty.repo
sudo mv openresty.repo /etc/yum.repos.d/

# update the yum index:
sudo yum check-update

sudo yum install openresty

#安装命令行工具
sudo yum install openresty-resty

# 列出所有 openresty 仓库里的软件包
sudo yum --disablerepo="*" --enablerepo="openresty" list available

#查看版本
resty -V

ここに画像の説明を挿入

OK、フロントエンド サーバー環境の展開が完了しました。コーディング プロセスを開始しましょう。

まずデータベーステーブルを作成する必要があります。次に、このテーブルの追加、削除、変更、クエリ ロジックのセットを作成します。それでは商品テーブルを作成してみましょう。

CREATE TABLE `product` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `title` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL,
  `cover_img` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '封面图',
  `amount` decimal(10,2) DEFAULT NULL COMMENT '现价',
  `summary` varchar(2048) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL COMMENT '概要',
  `detail` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_bin COMMENT '详情',
  `gmt_modified` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  `gmt_create` datetime DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin ROW_FORMAT=DYNAMIC;

SpringBoot プロジェクトを作成し、redis、mysql、mybatis、canal の依存関係を追加し、yml ファイルを構成します。

     <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- redis -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
            <version>3.0.6</version>
        </dependency>
        <!--数据库连接-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
        <!--mybatis plus-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.4.0</version>
        </dependency>
        <!--lombok-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.16</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba.otter</groupId>
            <artifactId>canal.client</artifactId>
            <version>1.1.4</version>
        </dependency>
    </dependencies>

製品の追加、削除、変更、クエリロジックを記述します。ここでは特定のコードは示しませんが、いくつかの基本的な追加、削除、変更、チェックのみを示します。動作中にソース コード パッケージをダウンロードできます。CSDN にアップロードします。


/**
 * @author lixiang
 * @date 2023/6/25 17:20
 */
public interface ProductService {
    
    

    /**
     * 新增商品
     * @param product
     */
    void addProduct(ProductDO product);

    /**
     * 修改商品
     * @param product
     */
    void updateProduct(ProductDO product);

    /**
     * 删除商品
     * @param id
     */
    void deleteProductById(Long id);

    /**
     * 根据ID查询商品
     * @param id
     * @return
     */
    ProductDO selectProductById(Long id);

    /**
     * 分页查询商品信息
     * @param current
     * @param size
     * @return
     */
    Map<String,Object> selectProductList(int current, int size);
}

ここに画像の説明を挿入

プロジェクト検証の開始に問題はなく、次のステップに進みます。運河モニタリングを開発するときは、まずApplicationRunner とは何かを理解しましょう。

ApplicationRunnerSpring Bootはフレームワークによって提供されるインターフェイスであり、アプリケーションSpring Bootの起動後にいくつかのタスクまたはコードを実行するために使用されます。アプリケーションの起動時、起動後にいくつかのタスクや初期化操作を自動的に実行したい場合は、このインターフェイスを使用できます。

使用手順: クラスを作成し、インターフェイスを実装し、クラスに注釈を追加して、クラスがスキャンされ、そのメソッドが実行される@Componentようにします。Spring Bootrun

/**
 * @author lixiang
 * @date 2023/6/29 23:08
 */
@Component
@Slf4j
public class CanalRedisConsumer implements ApplicationRunner {
    
    

    @Override
    public void run(ApplicationArguments args) throws Exception {
    
    
        log.info("CanalRedisConsumer执行");
    }
}

ここに画像の説明を挿入

次に、主にコア ロジックの実現に焦点を当てます。コードに直接移動します。

/**
 * 这里我们直接操作redis的String类型
 * @author lixiang
 * @date 2023/6/29 23:08
 */
@Component
@Slf4j
public class CanalRedisConsumer implements ApplicationRunner {
    
    

    @Autowired
    private RedisTemplate redisTemplate;

    @Override
    public void run(ApplicationArguments args) throws Exception {
    
    
        // 创建一个 CanalConnector 连接器
        CanalConnector canalConnector = CanalConnectors.newSingleConnector(new InetSocketAddress("payne.f3322.net", 11111),
                "example", "", "");
        try {
    
    
            // 连接 Canal Server,尝试多次重连
            while (true) {
    
    
                try {
    
    
                    canalConnector.connect();
                    break;
                } catch (CanalClientException e) {
    
    
                    log.info("Connect to Canal Server failed, retrying...");
                }
            }
            log.info("Connect to Canal Server success");
            //订阅数据库表,默认监听所有的数据库、表,等同于:.*\\..*
            canalConnector.subscribe(".*\\..*");
            // 回滚到上一次的 batchId,取消已经消费过的日志
            canalConnector.rollback();

            // 持续监听 Canal Server 推送的数据,并将数据写入 Redis 中
            while (true) {
    
    
                Message message = canalConnector.getWithoutAck(100);
                long batchId = message.getId();

                // 如果没有新数据,则暂停固定时间后继续获取
                if (batchId == -1 || message.getEntries().isEmpty()) {
    
    
                    try {
    
    
                        Thread.sleep(1000);
                        continue;
                    } catch (InterruptedException e) {
    
    
                        e.printStackTrace();
                    }
                }
                //处理数据
                for (CanalEntry.Entry entry : message.getEntries()) {
    
    
                    if (entry.getEntryType() == CanalEntry.EntryType.TRANSACTIONBEGIN || entry.getEntryType() == CanalEntry.EntryType.TRANSACTIONEND) {
    
    
                        continue;
                    }
                    CanalEntry.RowChange rowChange = null;
                    try {
    
    
                        rowChange = CanalEntry.RowChange.parseFrom(entry.getStoreValue());
                    } catch (Exception e) {
    
    
                        throw new RuntimeException("Error parsing Canal Entry.", e);
                    }

                    String table = entry.getHeader().getTableName();
                    CanalEntry.EventType eventType = rowChange.getEventType();
                    log.info("Canal监听数据变化,DB:{},Table:{},Type:{}",entry.getHeader().getSchemaName(),table,eventType);
                    // 变更后的新数据
                    for (CanalEntry.RowData rowData : rowChange.getRowDatasList()) {
    
    
                        if (eventType == CanalEntry.EventType.DELETE) {
    
    
                            deleteData(table, rowData);
                        } else {
    
    
                            insertOrUpdateData(table, rowData);
                        }
                    }
                }

                try {
    
    
                    canalConnector.ack(batchId);
                } catch (Exception e) {
    
    
                    // 回滚所有未确认的 Batch
                    canalConnector.rollback(batchId);
                }
            }

        } finally {
    
    
            canalConnector.disconnect();
        }
    }

    /**
     * 删除行数据
     */
    private void deleteData(String table, CanalEntry.RowData rowData) {
    
    
        List<CanalEntry.Column> columns = rowData.getBeforeColumnsList();
        JSONObject json = new JSONObject();
        columns.forEach(column->json.put(column.getName(), column.getValue()));
        String key = table + ":" + columns.get(0).getValue();
        log.info("Redis中删除Key为: {} 的数据",key);
        redisTemplate.delete(key);
    }

    /**
     * 新增或者修改数据
     */
    private void insertOrUpdateData(String table, CanalEntry.RowData rowData) {
    
    
        List<CanalEntry.Column> columns = rowData.getAfterColumnsList();
        JSONObject json = new JSONObject();
        columns.forEach(column->json.put(column.getName(), column.getValue()));
        String key = table + ":" + columns.get(0).getValue();
        log.info("Redis中新增或修改Key为: {} 的数据",key);
        redisTemplate.opsForValue().set(key, json);
    }
}

次に、製品の追加、削除、変更、クエリのためのインターフェイスを開発します。

/**
 * @author lixiang
 * @date 2023/6/30 17:48
 */
@RestController
@RequestMapping("/api/v1/product")
public class ProductController {
    
    

    @Autowired
    private ProductService productService;

    /**
     * 新增
     * @param product
     * @return
     */
    @PostMapping("/save")
    public String save(@RequestBody ProductDO product){
    
    
        int flag = productService.addProduct(product);
        return flag==1?"SUCCESS":"FAIL";
    }

    /**
     * 修改
     * @param product
     * @return
     */
    @PostMapping("/update")
    public String update(@RequestBody ProductDO product){
    
    
        int flag = productService.updateProduct(product);
        return flag==1?"SUCCESS":"FAIL";
    }

    /**
     * 根据ID查询
     * @param id
     * @return
     */
    @GetMapping("/findById")
    public ProductDO update(@RequestParam("id") Long id){
    
    
        ProductDO productDO = productService.selectProductById(id);
        return productDO;
    }

    /**
     * 分页查询
     * @param current
     * @param size
     * @return
     */
    @GetMapping("/page")
    public Map<String, Object> update(@RequestParam("current") int current,@RequestParam("size") int size){
    
    
        Map<String, Object> stringObjectMap = productService.selectProductList(current, size);
        return stringObjectMap;
    }
}

確認する項目を追加します。

ここに画像の説明を挿入ここに画像の説明を挿入
ここに画像の説明を挿入

次に、SpringBoot プログラムをパッケージ化し、サーバー上で実行します。

mvn clean package

ここに画像の説明を挿入

守护进程启动  nohup java -jar multi-level-cache-1.0-SNAPSHOT.jar  & 

ここに画像の説明を挿入ここに画像の説明を挿入

DB変更同期キャッシュの検証を行った後、Redis部分を開発してNginx経由で直接読み込んでいきます。

まず、OpenResty とは何か、なぜ OpenResty を使用するのかを理解しましょう。

OpenResty由章亦春发起,是基于Ngnix和Lua的高性能web平台,内部集成精良的LUa库、第三方模块、依赖, 开发者可以方便搭建能够处理高并发、扩展性极高的动态web应用、web服务、动态网关。 

OpenResty将Nginx核心、LuaJIT、许多有用的Lua库和Nginx第三方模块打包在一起。

Nginx是C语言开发,如果要二次扩展是很麻烦的,而基于OpenResty,开发人员可以使用 Lua 编程语言对 Nginx 核心模块进行二次开发拓展。

性能强大,OpenResty可以快速构造出1万以上并发连接响应的超高性能Web应用系统。
  • 一部の高パフォーマンス サービスでは、OpenResty を使用して Mysql や Redis などに直接アクセスできます。

  • サードパーティ言語(PHP、Python、Ruby)などを介してデータベースにアクセスして返す必要がなくなり、アプリケーションのパフォーマンスが大幅に向上します

では、Lua スクリプトとは何でしょうか?

Lua 由标准 C 编写而成,没有提供强大的库,但可以很容易的被 C/C++ 代码调用,也可以反过来调用 C/C++ 的函数。 

在应用程序中可以被广泛应用,不过Lua是一种脚本/动态语言,不适合业务逻辑比较重的场景,适合小巧的应用场景

それでは、Lua スクリプトを通じて Redis 部分を直接読み取る Nginx の開発を開始します。

-- 引入需要使用到的库
local redis = require "resty.redis"
local redis_server = "ip地址"
local redis_port = 6379
local redis_pwd = "123456"

-- 获取 Redis 中存储的数据
local function get_from_redis(key)
    local red = redis:new()

    local ok, err = red:connect(redis_server, redis_port)
    red:auth(redis_pwd)
    if not ok then
    -- 如果从 Redis 中获取数据失败,将错误信息写入 Nginx 的错误日志中
        ngx.log(ngx.ERR, "failed to connect to Redis: ", err)
        return ""
    end
    local result, err = red:get(key)
    if not result then
        ngx.log(ngx.ERR, "failed to get ", key, " from Redis: ", err)
        return ""
    end
    -- 将 Redis 连接放回连接池中
    red:set_keepalive(10000, 100)
    return result
end

-- 获取缓存数据
local function get_cache_data()
		-- 获取当前请求的 URI
    local uri = ngx.var.uri
    -- 获取当前请求的 id 参数
    local id = ngx.var.arg_id
		-- 将 URI 写入 Nginx 的错误日志中
    ngx.log(ngx.ERR, "URI: ", uri) 
    -- 将当前请求的所有参数写入 Nginx 的错误日志中
    ngx.log(ngx.ERR, "Args: ", ngx.var.args)

    local start_pos = string.find(uri, "/", 6) + 1  
    local end_pos = string.find(uri, "/", start_pos)
    -- 截取第三个和第四个斜杠之间的子串
    local cache_prefix = string.sub(uri, start_pos, end_pos - 1)   
    -- Redis 中键的名称由子串和 id 组成
    local key = cache_prefix .. ":" .. id

    local result = get_from_redis(key)

    if result == nil or result == ngx.null or result == "" then
				-- Redis 中未命中,需要到服务器后端获取数据
        ngx.log(ngx.ERR, "not hit cache, key = ", key)
    else
        -- Redis 命中,返回结果
        ngx.log(ngx.ERR, "hit cache, key = ", key)
        -- 直接将 Redis 中存储的结果返回给客户端
        ngx.say(result)
        -- 结束请求,客户端无需再等待响应
        ngx.exit(ngx.HTTP_OK)
    end
end

-- 执行获取缓存数据的功能
get_cache_data()

Lua スクリプトをサーバーによって指定されたディレクトリに置きます。

ここに画像の説明を挿入

新しい lua ディレクトリを作成し、cache.lua ファイルを作成します。

ここに画像の説明を挿入

次にnginxの設定を行います。

  • Nginx は、redis を読み取るための lua スクリプトと組み合わせて、リバース プロキシを構成します。
  • Redis キャッシュがヒットした場合、キャッシュされたデータが読み取られて直接返されます。
  • キャッシュがミスした場合、リバース プロキシはバックエンド インターフェイスにデータを取り戻すように要求します。
#user  nobody;
worker_processes  1;

#error_log  logs/error.log;
#error_log  logs/error.log  notice;
#error_log  logs/error.log  info;

#pid        logs/nginx.pid;

events {
    worker_connections  1024;
}

http {
    include       mime.types;
    default_type  application/octet-stream;
    #配置下编码,不然浏览器会乱码
    charset utf-8;
    #log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
    #                  '$status $body_bytes_sent "$http_referer" '
    #                  '"$http_user_agent" "$http_x_forwarded_for"';

    #access_log  logs/access.log  main;

    sendfile        on;
    #tcp_nopush     on;

    #keepalive_timeout  0;
    keepalive_timeout  65;

    #gzip  on;
    # 这里设置为 off,是为了避免每次修改之后都要重新 reload 的麻烦。
		# 在生产环境上需要 lua_code_cache 设置成 on。
		
    lua_code_cache off;
     # 虚拟机主机块,还需要配置lua文件扫描路径
    lua_package_path "$prefix/lualib/?.lua;;";
    lua_package_cpath "$prefix/lualib/?.so;;";
	
		#配置反向代理到后端spring boot程序
    upstream backend {
      server 127.0.0.1:8888;
    }

    server {
        listen       80;
        server_name  localhost;
        location /api {
            default_type 'text/plain';
            if ($request_method = GET) {
                access_by_lua_file /usr/local/openresty/lua/cache.lua;
            }
            proxy_pass http://backend;
            proxy_set_header Host $http_host;
        }
    }
}
./nginx -c /usr/local/openresty/nginx/conf/nginx.conf -s reload

nginxが正常に起動しました。

ここに画像の説明を挿入

OK、次にテストして検証しましょう。まず Redis のキャッシュされたデータにアクセスします。Nginx 経由で Redis に直接アクセスします。

http://payne.f3322.net:8888/api/v1/product/findById?id=3

ここに画像の説明を挿入

次に、Redis にキャッシュされていないデータにアクセスします。キャッシュにアクセスせずに、SpringBoot プログラムに直接侵入します。

http://payne.f3322.net:8888/api/v1/product/findById?id=2

ここに画像の説明を挿入ここに画像の説明を挿入

新しいデータが追加されると、Redis に同期されます。

http://payne.f3322.net:8888/api/v1/product/save

{
    
    
    "title":"Mac Pro 13",
    "coverImg":"/group/4.png",
    "amount":"19999.00",
    "summary":"Mac Pro 13",
    "detail":"Mac Pro 13"
}

ここに画像の説明を挿入

OK、これまでのところ、マルチレベルキャッシュのケースは完了しています。ブロガーにスリーインワンを与えることを忘れないでください。

ここに画像の説明を挿入

おすすめ

転載: blog.csdn.net/weixin_47533244/article/details/131493468