[Caso] Solución de consistencia para arquitectura de caché multinivel para negocios de alta concurrencia

Básicamente, somos inseparables del caché en proyectos de alta concurrencia, por lo que, desde la introducción del caché, habrá un problema de consistencia de datos entre el caché y la base de datos.

En primer lugar, echemos un vistazo a los tres modos comunes de lectura y escritura de caché de Redis en proyectos de alta simultaneidad.

inserte la descripción de la imagen aquí

Caché a un lado

读写分离模式,是最常见的Redis缓存模式,多数采用。
读写数据时需要先查找缓存,如果缓存中没有,则从数据库中查找数据。
如果查询到数据,需要将数据放到缓存中,下次访问再直接从缓存中获取数据,以提高访问效率。
写操作通常不会直接更新缓存,而是删除缓存,因为存储结构是hash、list,则更新数据需要遍历。
  • ventaja
    • La eficiencia de lectura es alta, la tasa de aciertos de caché es alta, la operación de escritura está sincronizada con la base de datos, la consistencia de los datos es alta y la implementación es relativamente simple.
  • defecto
    • Hay inconsistencia de datos entre la base de datos y la memoria caché, y se debe considerar la inconsistencia de la memoria caché causada por la invalidación de la memoria caché y las operaciones de actualización de la base de datos.
  • Escenario de aplicación
    • Es adecuado para situaciones en las que las operaciones de lectura son muy frecuentes y las operaciones de escritura son relativamente pocas. Por ejemplo, en la página de detalles del producto de un sitio web de comercio electrónico, el número de operaciones de lectura es mucho mayor que el de las operaciones de actualización y adición.

Lectura/Escritura completa

读写穿透模式,读写操作会直接修改缓存,然后再同步更新数据库中的数据,开发较为复杂,一般少用。
在Read/Write  Through模式下,每次数据的读写操作都会操作缓存,再同步到数据库,以保证缓存和数据库数据的一致性。
应用程序将缓存作为主要的数据源,数据库对于应用程序是透明的,更新数据库和从数据库的读取的任务都交给缓存来实现。
  • ventaja
    • La velocidad de la operación de escritura es rápida, la consistencia es alta, los datos en el caché y la base de datos son consistentes y la tasa de aciertos en el caché es alta.
  • defecto
    • La operación de lectura es lenta. Si no hay datos disponibles en el caché, la consulta de la base de datos se realizará cada vez. Cuando la cantidad de datos es grande, tendrá un mayor impacto en el rendimiento.
  • Escenario de aplicación
    • El sistema maneja escenarios con operaciones de escritura frecuentes y operaciones de lectura poco frecuentes, como almacenamiento en la nube Ceph

escribir detrás

被称为Write Back模式或异步写入模式,一般较少使用。
如果有写操作,缓存会记录修改了缓存的数据,但是并不会立即同步到数据库中。
一般会把缓存中的数据更新到磁盘中,等到后续有查询数据操作时,再异步批量更新数据库中的数据。
该模式的优点就是写操作速度很快,不会对性能产生影响,同时也避免了频繁更新数据库的情况,提升了数据库性能。
  • ventaja
    • La operación de escritura es rápida, el rendimiento es alto y la consistencia de los datos es generalmente alta.
  • defecto
    • La operación de lectura es lenta y, debido a la forma asincrónica de actualizar la base de datos, puede haber demoras en los datos.
  • Escenario de aplicación:
    • Se utiliza en escenarios donde la proporción de lectura y escritura de datos es alta, como puntos de actividad del usuario en juegos y otra información. Al principio, el rendimiento de las operaciones de escritura es muy alto y las consultas posteriores son relativamente pequeñas.

En el desarrollo empresarial, básicamente se lee de la base de datos a la caché, entonces, ¿cuál es la secuencia de lectura y escritura de la caché a la base de datos?

Escenario 1: Actualice la base de datos primero, luego actualice el caché

  • El subproceso A actualiza la base de datos. Después de actualizar la base de datos, el subproceso A actualiza la memoria caché. La memoria caché se actualiza correctamente, pero la confirmación de la transacción de la base de datos del subproceso A falla, o el cuerpo del método es anómalo, y se realiza la reversión. Puede causar inconsistencias en la memoria caché y la base de datos.

inserte la descripción de la imagen aquí

Escenario 2: elimine el caché primero, luego actualice la base de datos

  • El subproceso A elimina la memoria caché y actualiza la base de datos, pero aún no se ha confirmado. En este momento el hilo B accede a la caché y encuentra que no hay datos, va a la base de datos para leer los datos no comprometidos y ponerlos en la caché, es decir, los datos antiguos. El subproceso A realizó una operación de confirmación. Esto también hará que el caché sea información antigua y la base de datos sea información nueva, inconsistente.

inserte la descripción de la imagen aquí

Escenario 3: primero elimine el caché, luego actualice la base de datos y luego elimine el caché

  • El subproceso A elimina la memoria caché y actualiza la base de datos, pero aún no se ha confirmado. El subproceso B accede a la memoria caché y descubre que no hay datos. Va a la base de datos para leer los datos no confirmados y los coloca en la memoria caché (datos antiguos). El subproceso A realizó una operación de confirmación. El subproceso A elimina los datos de la memoria caché nuevamente (la memoria caché está vacía en este momento y las lecturas posteriores son los datos más recientes). La consistencia de los datos está garantizada, pero se desperdician múltiples IO, lo que equivale a eliminar Redis una vez más cada vez.

inserte la descripción de la imagen aquí

Bien, volvamos al tema, ¿qué es una arquitectura de caché multinivel?

Una arquitectura de caché multinivel es una tecnología de almacenamiento en caché de alta disponibilidad que generalmente se utiliza para optimizar el rendimiento de la aplicación. Por lo general, consta de múltiples capas de caché, donde cada capa de caché se puede seleccionar y ajustar de acuerdo con sus diferentes características y funciones. Maximice el rendimiento y la disponibilidad de las aplicaciones mediante el almacenamiento en caché de varias copias de datos, evitando la penetración de caché, la ruptura de caché y la inconsistencia de datos.

Aquí usamos la arquitectura Nginx+Lua+Canal+Redis+Mysql , es decir, la operación de lectura consulta el caché de Nginx a través de Lua, si el caché de Nginx no tiene datos, consultará el caché de Redis, si el caché de Redis tiene sin datos, consultará directamente mysql . Durante la operación de escritura, Canal monitorea los cambios incrementales de la tabla especificada en la base de datos, y el programa Java consume los cambios incrementales monitoreados por Canal y los escribe en Redis. El programa Java-canal opera el caché de Redis, y si el caché local de Nginx se aplica o invalida depende del tipo de proyecto.

inserte la descripción de la imagen aquí

Vale, ¿qué es Canal?

El sistema de publicación de suscripción y análisis de registro incremental basado en MySQL de Alibaba se utiliza principalmente para resolver problemas de suscripción y consumo de datos. Canal admite principalmente el análisis binlog de MySQL, y el cliente de canal se utiliza para procesar los datos relevantes obtenidos una vez que se completa el análisis.

En los primeros días, debido a la implementación de salas de computadoras duales en Hangzhou y los Estados Unidos, hubo un requisito comercial para la sincronización entre salas de computadoras. El método de implementación se basó principalmente en disparadores comerciales para obtener cambios incrementales. Desde 2010, la empresa ha intentado analizar gradualmente los registros de la base de datos para obtener cambios incrementales para la sincronización, y de esto se ha derivado una gran cantidad de negocios de suscripción y consumo de bases de datos incrementales.

  • Los servicios basados ​​en la suscripción y el consumo incrementales de registro incluyen

    • reflejo de la base de datos
    • Copia de seguridad en tiempo real de la base de datos
    • Construcción de índices y mantenimiento en tiempo real (índice heterogéneo dividido, índice invertido, etc.)
    • Actualización de caché empresarial
    • Procesamiento incremental de datos con lógica de negocios

El canal actual es compatible con las versiones fuente de MySQL, incluidas 5.1.x, 5.5.x, 5.6.x, 5.7.x, 8.0.x

inserte la descripción de la imagen aquí

  • Canal simula el protocolo interactivo del esclavo MySQL, pretende ser el esclavo MySQL y envía el protocolo de volcado al maestro MySQL.
  • El maestro MySQL recibe una solicitud de volcado y comienza a enviar el registro binario al esclavo (es decir, al canal).
  • Canal analiza el objeto de registro binario (originalmente un flujo de bytes) y lo analiza en eventos de operación de datos independientes, como inserción, actualización, eliminación, etc.

Preparación ambiental

Bien, la parte anterior introdujo principalmente una arquitectura de caché de varios niveles, y luego haremos algunos preparativos ambientales.

Primero implemente MySQL, aquí implementamos MySQL en forma de contenedores docker. Una vez completada la implementación, se debe habilitar el registro binlog de MySQL.

#创建目录
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

Edite el archivo de configuración my.cnf, en el módulo mysqld, reinicie el servicio mysql después de editar.

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

# 选择 ROW 模式
binlog_format=row

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

inserte la descripción de la imagen aquí

La siguiente es una introducción a los tres modos binlog de MySQL

STATEMENTFormato

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

ROWformato (predeterminado)

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

MIXEDFormato

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

Ejecute el siguiente comando para verificar si binlog está habilitado:SHOW VARIABLES LIKE 'log_bin'

inserte la descripción de la imagen aquí

autorización de base de datos

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

inserte la descripción de la imagen aquí

Luego implementamos Redis, que también se implementa con docker, que es muy simple.

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

inserte la descripción de la imagen aquí

Luego, implementemos canal-server, también usamos la implementación de docker.

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

Ingrese al contenedor y modifique el archivo de configuración

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=

Reinicie el contenedor e ingrese al contenedor para ver los registros.

docker restart canal

inserte la descripción de la imagen aquí

docker exec -it canal /bin/bash

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

inserte la descripción de la imagen aquí

A continuación, implementemos el último elemento de Nginx. Aquí implementamos directamente OpenResty, que cubre Nginx y Lua.

Ejecute los siguientes comandos en secuencia.

# 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

inserte la descripción de la imagen aquí

Bien, la implementación del entorno del servidor front-end está completa, comencemos el proceso de codificación.

Primero necesitamos crear una tabla de base de datos. Luego escriba un conjunto de lógica de adición, eliminación, modificación y consulta para esta tabla. Ahora vamos a crear una tabla de productos.

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;

Cree un proyecto SpringBoot, agregue redis, mysql, mybatis, dependencias de canal y configure el archivo 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>

Escribir la lógica de adición, eliminación, modificación y consulta de productos. El código específico no se mostrará aquí, solo algunas adiciones, eliminaciones, modificaciones y comprobaciones básicas. Puede descargar el paquete de código fuente durante la operación y lo subiré a 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);
}

inserte la descripción de la imagen aquí

No hay problema en iniciar la verificación del proyecto, y comenzamos el siguiente paso. Al desarrollar el monitoreo de canales, primero comprendamos qué es ApplicationRunner .

ApplicationRunner es Spring Bootuna interfaz proporcionada por el marco, que se utiliza para Spring Bootejecutar algunas tareas o códigos después de que se inicia la aplicación. Cuando se inicia la aplicación, si desea realizar automáticamente algunas tareas u operaciones de inicialización después del inicio, puede utilizar esta interfaz.

Pasos a seguir: Cree una clase, implemente la interfaz y agregue anotaciones a la clase @Componentpara asegurarse Spring Bootde que la clase se pueda escanear y sus runmétodos se ejecuten.

/**
 * @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执行");
    }
}

inserte la descripción de la imagen aquí

Bien, a continuación nos centraremos principalmente en la realización de la lógica central. Ir directamente al código.

/**
 * 这里我们直接操作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);
    }
}

Luego, desarrollamos una interfaz para agregar, eliminar, modificar y consultar productos.

/**
 * @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;
    }
}

Agrega un artículo para verificar.

inserte la descripción de la imagen aquíinserte la descripción de la imagen aquí
inserte la descripción de la imagen aquí

A continuación, empaquetamos el programa SpringBoot y lo ejecutamos en el servidor.

mvn clean package

inserte la descripción de la imagen aquí

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

inserte la descripción de la imagen aquíinserte la descripción de la imagen aquí

Hemos verificado la caché de sincronización de cambios de base de datos, y luego desarrollaremos y leeremos directamente la parte de Redis a través de Nginx.

En primer lugar, comprendamos qué es OpenResty, ¿por qué usar OpenResty?

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

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

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

性能强大,OpenResty可以快速构造出1万以上并发连接响应的超高性能Web应用系统。
  • Para algunos servicios de alto rendimiento, puede usar directamente OpenResty para acceder a Mysql o Redis, etc.

  • No es necesario acceder a la base de datos y volver a través de lenguajes de terceros (PHP, Python, Ruby), etc., lo que mejora mucho el rendimiento de la aplicación.

Entonces, ¿qué es un guión de Lua?

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

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

Ok, entonces comenzaremos a desarrollar Nginx para leer directamente la parte de Redis a través de los scripts de Lua.

-- 引入需要使用到的库
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()

Coloque el script de Lua en el directorio especificado por el servidor.

inserte la descripción de la imagen aquí

Cree un nuevo directorio lua y cree un archivo cache.lua.

inserte la descripción de la imagen aquí

Luego, configuramos nginx.

  • Nginx configura el proxy inverso, combinado con el script lua para leer redis.
  • Si la memoria caché redis acierta, los datos almacenados en la memoria caché se leen y se devuelven directamente.
  • Si el caché falla, el proxy inverso solicita a la interfaz de back-end que recupere los datos.
#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 se inició correctamente.

inserte la descripción de la imagen aquí

Bien, probemos y verifiquemos a continuación, primero acceda a los datos almacenados en caché en Redis. Acceda a Redis directamente a través de Nginx.

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

inserte la descripción de la imagen aquí

Entonces estamos accediendo a datos que no están almacenados en caché en Redis. Sin tocar el caché, penetra directamente en el programa SpringBoot.

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

inserte la descripción de la imagen aquíinserte la descripción de la imagen aquí

Cuando se agreguen nuevos datos, se sincronizarán con 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"
}

inserte la descripción de la imagen aquí

Bien, hasta ahora el caso de caché multinivel está completo. ¡Recuerda darle al blogger un tres en uno!

inserte la descripción de la imagen aquí

Supongo que te gusta

Origin blog.csdn.net/weixin_47533244/article/details/131493468
Recomendado
Clasificación