OpenResty(lua+nginx)+Canal多级缓存架构实现

987 阅读9分钟

示例图:

image.png

1. OpenResty

官网地址:openresty.org/cn/

OpenResty® 是一款基于 NGINX 和 LuaJIT(lua plus) 的 Web 平台。

1.1 OpenResty简介

OpenResty 是一个基于 Nginx 与 Lua 的高性能 Web 平台,其内部集成了大量精良的 Lua 库、第三方模块以及大多数的依赖项。用于方便地搭建能够处理超高并发、扩展性极高的动态 Web 应用、Web 服务和动态网关。

OpenResty® 通过汇聚各种设计精良的 Nginx 模块(主要由 OpenResty 团队自主开发),从而将 Nginx 有效地变成一个强大的通用 Web 应用平台。这样,Web 开发人员和系统工程师可以使用 Lua 脚本语言调动 Nginx 支持的各种 C 以及 Lua 模块,快速构造出足以胜任 10K 乃至 1000K 以上单机并发连接的高性能 Web 应用系统。

OpenResty® 的目标是让你的Web服务直接跑在 Nginx 服务内部,充分利用 Nginx 的非阻塞 I/O 模型,不仅仅对 HTTP 客户端请求,甚至于对远程后端诸如 MySQL、PostgreSQL、Memcached 以及 Redis 等都进行一致的高性能响应。

1.2 编译安装OpenResty

此处可以跳转至另一篇博客

juejin.cn/post/713730…

2. lua

image.png

2.1 安装及简单语法

juejin.cn/post/713731…

3. 多级缓存

mysql有缓存

redis有缓存

nginx有缓存,也可以利用lua直接调用redis缓存

image.png

tomcat支持的并发数并不多,所以要尽量减少访问,而且,你访问redis也要经过tomcat,tomcat死了,唇亡齿寒

3.1 静态页面动静分离

有关nginx的内容就不再写了

location / {
    root 你的静态页面存放地址
}

3.2 注解缓存

@EnableCaching:

在项目启动类上加,表示允许使用注解的方式进行缓存操作。

@Cacheable:

(缓存的增加)
可用于类或方法上;在目标方法执行前,会根据key先去缓存中查询看是否有数据,
有就直接返回缓存中的key对应的value值,不再执行目标方法;
无则执行目标方法,并将方法的返回值作为value,并以键值对的形式存入缓存。

@CacheEvict:

(缓存的删除)
可用于类或方法上;在执行完目标方法后,清除缓存中对应key的数据(如果缓存中有对应key的数据缓存的话)。

@CachePut:

(缓存的更新)
(查到什么都加到缓存里面,不管缓存里有没有)
可用于类或方法上;在执行完目标方法后,并将方法的返回值作为value,并以键值对的形式存入缓存中。

@Caching:

此注解即可作为@Cacheable@CacheEvict@CachePut三种注解中的的任何一种或几种来使用。

@CacheConfig:

可以用于配置@Cacheable@CacheEvict@CachePut这三个注解的一些公共属性,例如cacheNames、keyGenerator。

注意这里是存到redis的,所以要配置一下redis的地址

例子:

image.png

image.png

@Cacheable这里第二次查的时候明显感觉快很多,也可以打印一些句子,发现根本没有走Spring,而是走的缓存

@CachePut会发现每次都是打的数据库,不会查缓存,用于修改

@CacheEvict就是删除的时候顺带删掉缓存

@CacheConfig作用如上图,在一个bean的头部加

3.3 Lua Redis缓存

ok,如上(tomcat支持的并发数并不多,所以要尽量减少访问,而且,你访问redis也要经过tomcat,tomcat死了,唇亡齿寒)所述,接下来记录如何通过lua+nginx跳过nginx实现调用redis

image.png

按照上面分析的架构,可以每次在Nginx的时候使用Lua脚本查询Redis,如果Redis有数据,则将数据存入到Nginx缓存,再将数据响应给用户,此时我们需要实现使用Lua将数据从Redis中加载出来。

我们在/usr/local/openresty/nginx/lua中创建文件sc.lua,脚本如下:

--数据响应类型JSON
ngx.header.content_type="application/json;charset=utf8"
--Redis库依赖
local redis = require("resty.redis");
local cjson = require("cjson");
​
--获取id参数(type)这里只是个例子,你可以写上面的注解缓存里面的key
local id = ngx.req.get_uri_args()["id"];
--key组装
local key = "ad-items-skus::"..id
--创建链接对象
local red = redis:new()
--设置超时时间
red:set_timeout(2000)
--设置服务器链接信息
red:connect("你的redis地址", 6379)
--查询指定key的数据
local result=red:get(key);
​
--关闭Redis链接
red:close()
​
if result==nil or result==null or result==ngx.null then
    return true
else
    --输出数据
    ngx.say(result)
end

修改nginx.conf添加如下配置:(最后记得将content_by_lua_file改成rewrite_by_lua_file)

location /sku/aditems/type {
    content_by_lua_file /usr/local/openresty/nginx/lua/aditem.lua;
}

3.4 Nginx缓存

1)开启代理缓存

修改nginx.conf,添加如下配置:

proxy_cache_path /usr/local/openresty/nginx/cache levels=1:2 keys_zone=proxy_cache:10m max_size=1g inactive=60m use_temp_path=off;

image.png 修改nginx.conf,添加如下配置:

#门户发布
server {
    listen       80;
    server_name  www.scscscscs.com;
​
    #推广产品查询
    location /sku/aditems/type {
        #先找Nginx缓存
        rewrite_by_lua_file /usr/local/openresty/nginx/lua/aditem.lua;
        #启用缓存openresty_cache
        proxy_cache proxy_cache;
        #针对指定请求缓存
        #proxy_cache_methods GET;
        #设置指定请求会缓存
        proxy_cache_valid 200 304 60s;
        #最少请求1次才会缓存
        proxy_cache_min_uses 1;
        #如果并发请求,只有第1个请求会去服务器获取数据
        #proxy_cache_lock on;
        #唯一的key
        proxy_cache_key $host$uri$is_args$args;
        #动态代理
        proxy_pass http://192.168.100.1:8081;
    }
​
​
    #其他所有请求
    location / {
        root   /usr/local/web/static/frant;
    }
}

重启nginx或者重新加载配置文件nginx -s reload,再次测试,可以发现下面个规律:

1:先查找Redis缓存
2:Redis缓存没数据,直接找Nginx缓存
3:Nginx缓存没数据,则找真实服务器

我们还可以发现cache目录下多了目录和一个文件,这就是Nginx缓存

参数说明:

【proxy_cache_path】指定缓存存储的路径,缓存存储在/usr/local/openresty/nginx/cache目录
​
【levels=1:2】设置一个两级目录层次结构存储缓存,在单个目录中包含大量文件会降低文件访问速度,因此我们建议对大多数部署使用两级目录层次结构。如果 levels 未包含该参数,Nginx 会将所有文件放在同一目录中。
​
【keys_zone=proxy_cache:10m】设置共享内存区域,用于存储缓存键和元数据,例如使用计时器。拥有内存中的密钥副本,Nginx 可以快速确定请求是否是一个 HIT 或 MISS 不必转到磁盘,从而大大加快了检查速度。1 MB 区域可以存储大约 8,000 个密钥的数据,因此示例中配置的 10 MB 区域可以存储大约 80,000 个密钥的数据。
​
【max_size=1g】设置缓存大小的上限。它是可选的; 不指定值允许缓存增长以使用所有可用磁盘空间。当缓存大小达到限制时,一个称为缓存管理器的进程将删除最近最少使用的缓存,将大小恢复到限制之下的文件。
​
【inactive=60m】指定项目在未被访问的情况下可以保留在缓存中的时间长度。在此示例中,缓存管理器进程会自动从缓存中删除 60 分钟未请求的文件,无论其是否已过期。默认值为 10 分钟(10m)。非活动内容与过期内容不同。Nginx 不会自动删除缓存 header 定义为已过期内容(例如 Cache-Control:max-age=120)。过期(陈旧)内容仅在指定时间内未被访问时被删除。访问过期内容时,Nginx 会从原始服务器刷新它并重置 inactive 计时器。
​
【use_temp_path=off】表示NGINX会将临时文件保存在缓存数据的同一目录中。这是为了避免在更新缓存时,磁盘之间互相复制响应数据,我们一般关闭该功能。

2)Proxy_Cache属性:

proxy_cache:设置是否开启对后端响应的缓存,如果开启的话,参数值就是zone的名称,比如:proxy_cache。
​
proxy_cache_valid:针对不同的response code设定不同的缓存时间,如果不设置code,默认为200,301,302,也可以用any指定所有code。
​
proxy_cache_min_uses:指定在多少次请求之后才缓存响应内容,这里表示将缓存内容写入到磁盘。
​
proxy_cache_lock:默认不开启,开启的话则每次只能有一个请求更新相同的缓存,其他请求要么等待缓存有数据要么限时等待锁释放;nginx 1.1.12才开始有。
配套着proxy_cache_lock_timeout一起使用。
​
proxy_cache_key:缓存文件的唯一key,可以根据它实现对缓存文件的清理操作。

image.png

(执行顺序也有点迷,等会了再写)

删除:

这个模块在上面编译安装的时候就已经安装了

在上面请求的网址路径前面加上/purge

#清理缓存
location ~ /purge(/.*) {
    #清理缓存
    proxy_cache_purge proxy_cache $host$1$is_args$args;
}

4. 数据一致性canal

早期阿里巴巴因为杭州和美国双机房部署,存在跨机房同步的业务需求,实现方式主要是基于业务 trigger 获取增量变更。从 2010 年开始,业务逐步尝试数据库日志解析获取增量变更进行同步,由此衍生出了大量的数据库增量订阅和消费业务。 基于日志增量订阅和消费的业务包括

  • 数据库镜像
  • 数据库实时备份
  • 索引构建和实时维护(拆分异构索引、倒排索引等)
  • 业务 cache 刷新
  • 带业务逻辑的增量数据处理

当前的 canal 支持源端 MySQL 版本包括 5.1.x , 5.5.x , 5.6.x , 5.7.x , 8.0.x。

Canal 工作原理

  • canal 模拟 MySQL slave 的交互协议,伪装自己为 MySQL slave ,向 MySQL master 发送dump 协议
  • MySQL master 收到 dump 请求,开始推送 binary log 给 slave (即 canal )
  • canal 解析 binary log 对象(原始为 byte 流)

1603725769771

4.1 MySQL开启binlog

对于MySQL , 需要先开启 Binlog 写入功能,配置 binlog-format 为 ROW 模式,my.cnf 中配置如下

docker exec -it mysql /bin/bash
cd /etc/mysql/mysql.conf.d
vi mysqld.cnf

在最文件尾部添加如下配置:

log-bin=mysql-bin # 开启 binlog
binlog-format=ROW # 选择 ROW 模式
server_id=1 # 配置 MySQL replaction 需要定义,不要和 canal 的 slaveId 重复

注意: 针对阿里云 RDS for MySQL , 默认打开了 binlog , 并且账号默认具有 binlog dump 权限 , 不需要任何权限或者 binlog 设置,可以直接跳过这一步。

授权 canal 链接 MySQL 账号具有作为 MySQL slave 的权限, 如果已有账户可直接 grant:

CREATE USER canal IDENTIFIED BY 'canal';
​
GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'canal'@'%';
​
-- GRANT ALL PRIVILEGES ON *.* TO 'canal'@'%' ;
FLUSH PRIVILEGES;

重启mysql容器

docker restart canal

查看是否开启binlog:

show variables like 'log_bin';

4.2 Canal安装

我们采用docker安装方式:

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

进入容器,修改核心配置canal.properties 和instance.properties,canal.properties 是canal自身的配置,instance.properties是需要同步数据的数据库连接配置。

修改配置如下:

# position info
canal.instance.master.address=192.168.100.130:3306

另一处配置:

# table regex
#canal.instance.filter.regex=.*\..*
#监听配置
canal.instance.filter.regex=shop_goods.ad_items

配置完成后,重启canal容器

docker restart canal

4.3 Canal微服务搭建

这里我也没搞懂,等搞懂了再写

    <!--springboot-canal快速构建依赖包-->
    <dependency>
        <groupId>top.javatool</groupId>
        <artifactId>canal-spring-boot-starter</artifactId>
        <version>1.2.1-RELEASE</version>
    </dependency>
server:
  port: 8083
......
#Canal配置
canal:
  server: 地址+docker的端口号
  destination: example
#日志配置
logging:
  pattern:
    console: "%msg%n"
  level:
    root: error

创建监听类:listener.AdItemsHandler

@CanalTable(value = "ad_items")
@Component
public class AdItemsHandler implements EntryHandler<AdItems> {
​
   
}

创建启动类:

@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)

(根据学习心得和gupao云课堂资料所写,侵删OvO)