【分布式】多级缓存架构,缓存预热,进程缓存,caffeine本地缓存,Lua脚本与语法,nginx+openResty+lua网管缓存,缓存同步方案Canal ,高并发压测

                                自启动缓存预热

缓存简历每日刷新的次数

run:当上下文加载完去运行

多级缓存架构的出现

单缓存架构

缓存中间件的目的,是为了降低数据库的崩溃风险,减轻数据库的访问压力。而且也能提升服务接口的响应时间。从而整体接口的性能并发能力都能提高。

但是,我们使用单个缓存,单个redis的话,他也有自己的上限,如果并发一直很大,其实也会有瓶颈。尤其是的千万级甚至亿级别的流量,单个缓存中间件大概率是吃不消的。

先看一下咱们的架构:

Redis的并发可以支持很大,单个几万没问题。但是,图中的流量最先进入到微服务的接口中,服务是通过springboot内部tomcat运行的,tomcat并发也撑死了也就200,优化的不错的情况下。所以几万并发进来肯定是不行的。肯定会挂掉,都没到达缓存,服务就直接打死了。所以,现在的系统瓶颈是出现在了tomcat所处的接口服务位置。

这个时候,有人会说,没关系啊,我们不是微服务嘛,可以做限流啊,但是如果我们不是微服务呢?仅仅只是单体应用架构呢?这个时候限流是没有的,所以tomcat依然是瓶颈。

其次,缓存是会失效的,即使是没有设置过期时间的数据,redis容量达到上限后,冷数据会被动清理掉,一旦失效,那么高并发瞬间流量进来,会直接击穿redis,到达数据库,那么数据库直接打死。我们的防护屏障太少了,只有一层redis,保护的不够周全。

所以这个时候,就需要有多级缓存来更好的保护系统的抗风险能力了。尤其出现在一级高并发页面的时候,更应该增加多级缓存的处理

举例:比如我们在后台设置完如下图阔值后,app使用用户求职者就会在我的页面(一级页面)最先看到刷新简历按钮,时不时就会点击 

多级缓存架构

多级缓存其实本质就是在请求所在的各个节点出去添加缓存,以此来分摊tomcat的压力,去提升整体接口的访问性能。如此更加提高系统的稳定性与并发能力。

  • 客户端缓存:静态数据(通过nginx设置)
  • nginx本地缓存
  • redis缓存
  • 接口缓存:本地缓存(进程内缓存)- JVM缓存 Caffeine

大蛇丸:三重罗生门

初代柱间:五重罗生门(多级缓存)

 JVM缓存 Caffeine

分布式缓存:

  • 存储容量大,可靠性高,可以拓展集群形态,海量数据存储

  • 但是,读写缓存存在网络开销(CAP定理)适用大量数据缓存存储,系统平台对所有需要服务的节点缓存数据同步的共享。

JVM缓存 :
     是堆缓存。也就是本地创建的一些全局容器,比如List、Set、Map等等。我们可以使用这些容器用来进行数据的存储。

但是,这样做会存在一些问题:

  • 对于一些无用数据无法做到很好的淘汰规则。
  • 并发处理能力差,而且很多缓存的功能都需要自己手动实现,比如过期处理,数据加载刷新延迟等等都自己实现,实现了说不定性能查但  也有bug。(我们有个十几年前的老项目,本地缓存就是这么做的,相当恶心)

本地缓存框架:

  • 读写本地缓存,没有网络开销,速度更快

  • 但是容量有限,无法集群共享,适用缓存数据量小,对某些数据进行缓存。

  • ehcache 这个相信大家都不陌生,EhCache 是一个纯 Java 的进程内缓存框架,很快很小,也是Hibernate默认集成的,我们使用mybatis也可以使用ehcache。

  • Guava Cache Guava是Google提供的一套Java工具包,而Guava Cache是一套非常完善的本地缓存机制(JVM缓存)。 Guava Cache的设计来源于CurrentHashMap,可以按照多种策略来清理存储在其中的缓存值且保持很高的并发读写性能。

  • Caffeine 早前我们可以一般使用Guava Cache就可以了,不过Java8以后,我们就可以使用Caffeine了,Caffeine是基于java8的高性能缓存框架,而且也是基于Guava Cache的缓存api实现,非常的灵活方便。目前spring内部使用的缓存就是Caffeine。

Caffeine官网:https://github.com/ben-manes/caffeine

读 100%

读(75%)/写(25%)

写(100%)

性能更强劲,命中率更高。

Caffeine的基本使用

在获取缓存值时,如果想要在缓存值不存在时,原子地将值写入缓存,则可以调用get(key, k -> value)方法,该方法将避免写入竞争。调用invalidate()方法,将手动移除缓存。在多线程情况下,当使用get(key, k -> value)时,如果有另一个线程同时调用本方法进行竞争,则后一线程会被阻塞,直到前一线程更新缓存完成;而若另一线程调用getIfPresent()方法,则会立即返回null,不会被阻塞。

淘汰策略

可见只存在一个缓存个数,age覆盖了name,sex覆盖了age                                                           

caffeine与redis的淘汰策略一样,是被动删除,而不是主动删除,也就是失效以后不会主动删除,而且在被重新读取的时候,再去删除。因为主动删除的话,会有有额外的线程开销,影响性能。

SpringBoot集成Caffeine

创建本地缓存配置类:

在容器中声明缓存的实例bean对象:

应用

 没有做缓存预热maxCountsStr会报空指针错,就得做判断,空默认返回某值。针对多个值,比较静态的冷数据也是可以放在缓存中,但是需要 从数据库里查询

测试:第一次看是否进箭头函数,第二次再看会否进入。

LUA脚本入门

nginx端的业务控制需要使用lua脚本,就像我们在项目里使用java语言来控制业务,本质一样,nginx是需要结合lua脚本的。

官网:http://www.lua.org/

(本图来自百度百科)

centos自带lua,可以不需要安装。

入门示例:

运行:

 LUA脚本 - 数据类型

  • nil:类似java的null,表示无效值(判断表达式中等同为false)
  • boolean:true/false
  • number:代表数值
  • string:字符串
  • function:通过 C/Lua 编写的函数,函数也可以作为变量,类似JavaScript
  • userdata:C数据结构,存储数据变量,可以把任何C语言中的任意数据类型存储到lua的变量中使用
  • thread:线程(协同程序-协程,线程对象)
  • table:表,本质是数组,是一种数据结构,可以用于创建存储不同的数据类型,比如数组list、字典map等。

示例:

type :看下变量类型,
# :查看长度,
..(两个点) :进行拼接

lua
> print(type("Hello"))
> print(type(100/2))
> print(type(print))
> print(type(true))
> print(type(nil))
> print(type({'1','2'}))
> print(type({name='imooc', age=18}))
> print("imooc长度为:" .. #"imooc")

声明变量:

local s = "Hello"
local num = 123
local bool = false
local arr = {'1','2'}
local json = {name='imooc', age=18}

访问table:

print(arr[1])
print(json['name'])
print(json.age)

LUA脚本 - 循环

for do end:循环,ipairs:数组,pairs:map

数组打印:

local arr = {"123", "abc"}

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

map打印:

local json = {name="imooc", birthday="2025-12-25"}

for key,value in pairs(json) do
    print(key, value);
end

LUA脚本 - 条件判断与函数

if not res then return end含义:返回的结果auth_res不对进行的操作

条件判断

函数

需求:通过lua脚本,获得一个数组中的所有string类型数据和所有number类型数据,如果不是string和number,则作为其他数据类型。

totalArrs[1]

totalArrs[2]

totalArrs[3]

附脚本:

print("================")
-- 获得数组中的string/number/其他类型
function getInnerArray(arr)
    strArr = {};
    numArr = {};
    otherArr = {};
    for index,value in ipairs(arr) do
--      print(index, value);
        if ("string" == type(value)) then
            table.insert(strArr, value);
        elseif ("number" == type(value)) then
            table.insert(numArr, value);
        else
            table.insert(otherArr, value);
        end
    end
    totalArr = {};
    table.insert(totalArr, strArr);
    table.insert(totalArr, numArr);
    table.insert(totalArr, otherArr);
    return totalArr;
end

local temp = {"123", "abc", 456, 789, false, true, {"x"}};
totalArrs = getInnerArray(temp);
print(totalArrs);

print("=========== 打印输出 ============");
for index,value in ipairs(totalArrs[3]) do
    print(index,value);
end

-- print(#totalArrs)

Nginx概述与安装

Nginx能干嘛

  • 反向代理
  • 集群(携带负载均衡)
  • 静态资源服务器(动静分类)
  • 网关(多级网关) 

安装Nginx

真实使用是把nginx作为独立的服务器去进行使用 ,所以 不在docker里安装

  1. 官网http://nginx.org/下载对应的nginx包,推荐使用稳定版本

  2. 上传ngxin到linux系统

  3. 安装依赖环境

    1. 安装gcc环境
      • yum install gcc-c++
    2. 安装PCRE库,用于解析正则表达式
      • yum install -y pcre pcre-devel
    3. zlib压缩和解压缩依赖,
      • yum install -y zlib zlib-devel
    4. SSL 安全的加密的套接字协议层,用于HTTP安全传输,也就是https
      • yum install -y openssl openssl-devel
  4. 解压,需要注意,解压后得到的是源码,源码需要编译后才能安装 tar -zxvf nginx-1.16.1.tar.gz

  5. 编译之前,先创建nginx临时目录,如果不创建,在启动nginx的过程中会报错 mkdir /var/temp/nginx -p

  6. 在nginx目录,输入如下命令进行配置,目的是为了创建makefile文件

    ./configure \
    --prefix=/usr/local/nginx \        # 一般中间件都会安装该目录下 
    --pid-path=/var/run/nginx/nginx.pid \   #指定nginx的pid
    --lock-path=/var/lock/nginx.lock \        #锁定安装的文件,防止篡改,误操作
    --error-log-path=/var/log/nginx/error.log \  #日志
    --http-log-path=/var/log/nginx/access.log \  #日志
    --with-http_gzip_static_module \        # 启动gzip模块,可以在线实时压缩输出的输出流
    --http-client-body-temp-path=/var/temp/nginx/client \   #临时文件
    --http-proxy-temp-path=/var/temp/nginx/proxy \          #临时文件
    --http-fastcgi-temp-path=/var/temp/nginx/fastcgi \      #临时文件
    --http-uwsgi-temp-path=/var/temp/nginx/uwsgi \          #临时文件
    --http-scgi-temp-path=/var/temp/nginx/scgi              #临时文件
    

        注:\ 代表在命令行中换行,用于提高可读性
        
    配置命令:
    | 命令 | 解释 | | --- | --- | | --prefix | 指定nginx安装目录 | | --pid-path | 指向nginx的pid | | --lock-path | 锁定安装文件,防止被恶意篡改或误操作 | | --error-log | 错误日志 | | --http-log-path | http日志 | | --with-httpgzipstatic_module | 启用gzip模块,在线实时压缩输出数据流 | | --http-client-body-temp-path | 设定客户端请求的临时目录 | | --http-proxy-temp-path | 设定http代理临时目录 | | --http-fastcgi-temp-path | 设定fastcgi临时目录 | | --http-uwsgi-temp-path | 设定uwsgi临时目录 | | --http-scgi-temp-path | 设定scgi临时目录 |=/var/temp/nginx/scgi |

  7. make编译
     编译完可查看验证

    conf配置目录

    进入nginx.conf文件修改默认端口号,vim nginx.conf

  8. 安装 make install

  9. 进入sbin目录启动nginx ./nginx
      可查看当前进程

    其他命令

    • 停止:./nginx -s stop
    • 重新加载:./nginx -s reload
      # 启动(可配置环境变量,全局使用)
      nginx 
      # 停止
      nginx -s stop
      # 重新加载配置(配置文件发生更改需要重新加载)
      nginx -s reload
      #修改一些配置文件需要检查下语法是否正确
      nginx -t
      #查看简单信息
      nginx -v
      #查看详细信息
      nginx -V
  10. 打开浏览器,访问虚拟机所处内网ip即可打开nginx默认页面,显示如下便表示安装成功:
    路由所在目录

nginx.conf 核心配置文件

  1. 设置worker进程的用户,指的linux中的用户,会涉及到nginx操作目录或文件的一些权限,默认为nobody

    user root;
    
  2. worker进程工作数设置,一般来说CPU有几个,就设置几个,或者设置为N-1也行

    worker_processes 1;
    
  3. nginx 日志级别debug | info | notice | warn | error | crit | alert | emerg,错误级别从左到右越来越大

  4. 设置nginx进程 pid

    pid        logs/nginx.pid;
    
  5. 设置工作模式

    events {
        # 默认使用epoll
        use epoll;
        # 每个worker允许连接的客户端最大连接数
        worker_connections  10240;
    }
    
  6. http 是指令块,针对http网络传输的一些指令配置

    http {
    }
    
  7. include 引入外部配置,提高可读性,避免单个配置文件过大

    include       mime.types;
    



  8. 设定日志格式,main为定义的格式名称,如此 access_log 就可以直接使用这个变量了

    | 参数名 | 参数意义 | | --- | --- |
    | $remoteaddr | 客户端ip |
    | $remote
    user | 远程客户端用户名,一般为:’-’ |
    | $timelocal | 时间和时区 |
    | $request | 请求的url以及method |
    | $status | 响应状态码 |
    | $body
    bytessend | 响应客户端内容字节数 |
    | $http
    referer | 记录用户从哪个链接跳转过来的 |
    | $httpuseragent | 用户所使用的代理,一般来时都是浏览器 |
    | $httpxforwarded_for | 通过代理服务器来记录客户端的ip |

  9. sendfile使用高效文件传输,提升传输性能。启用后才能使用tcp_nopush,是指当数据表累积一定大小后才发送,提高了效率。on打开

    sendfile        on;
    tcp_nopush      on;
    
  10. keepalive_timeout设置客户端与服务端请求的超时时间,保证客户端多次请求的时候不会重复建立新的连接,节约资源损耗。

    #keepalive_timeout  0;
    keepalive_timeout  65;
    
  11. gzip启用压缩,html/js/css压缩后传输会更快

    gzip on;
    
  12. server可以在http指令块中设置多个虚拟主机

    • listen 监听端口
    • server_name localhost、ip、域名
    • location 请求路由映射,匹配拦截
    • root 请求位置
    • index 首页设置
       
          server {
                  listen       88;
                  server_name  localhost;       
                 
                 location / {
                       root   html;
                       index  index.html index.htm; #理解为controller里的某个方法
                     }
              }
                                                                                                  

OpenResty概述与安装

OpenResty是一个基于nginx的高性能web平台,用于更方便的搭建web服务和网关,可以是的网关更具有高并发能力、可扩展能力、动态网关能力。特点如下:

  • 配合nginx,有完整的nginx能力
  • 可以基于lua进行扩展
  • 可以使用lua进行自定义的逻辑处理

官网地址:https://openresty.org/cn

安装步骤

1. 安装依赖库
yum install -y yum-utils pcre-devel openssl-devel gcc curl
2. 配置OpenResty软件包的仓库
yum-config-manager --add-repo https://openresty.org/package/centos/openresty.repo
3. 安装OpenResty
yum install -y openresty
4. 安装OpenResty管理工具opm
yum install -y openresty-opm

安装成功

安装目录位置为:/usr/local/openresty

  • bin:可执行文件
  • luajit/lualib:资源库,可以编辑处理业务,第三方扩展
  • nginx:openresty集成了nginx,或者说基于nginx做的扩展,添加了各项功能,因为本质上nginx也是开源的嘛

运行 nginx

配置nginx环境变量

openrestry本质上就是nginx的启动,所以启动哪个都一样。在这里可以添加nginx的环境变量,如此,在任意目录都可以更方便的启动停止nginx。

:$PATH:追加到path的意思

vim /etc/profile

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

source /etc/profile

测试访问页面

修改 nginx.conf 端口,并且访问打开页面进行测试。

3台openresty的端口分别为:9011,9022,9033

Ngin x反向代理OpenResty集群

openresty 反向代理微服务网关

upstream springcloud-gateway {
    server 192.168.1.4:8000;
}

……

location /resume/refresh {
    proxy_pass http://springcloud-gateway;
}


另外两个小网关也是这么配置

Nginx 跨域问题

add_header 'Access-Control-Allow-Origin' '*' always;
add_header 'Access-Control-Allow-Credentials' 'true';
add_header 'Access-Control-Allow-Methods' 'GET, POST, PATCH, DELETE, PUT, OPTIONS';
add_header 'Access-Control-Allow-Headers'  '*';
if ($request_method = 'OPTIONS') {
    return 200;
}

url 优化

出现跨域冲突,此时把微服务网关的跨域配置注释即可

优化跨域配置

由于openresty是二级网关,我们不会对外开放,所以跨域配置不需要配置在里面。前置放在一级网关中即可。

大网关nginx配置

把现有的前端请求url进行改造:

改写为如下: [serverUrl:port]/resume/refresh?userId=1001&resumeId=1002 这个时候,后台接口无法满足要求进行路由映射。则nginx可以进行路径规则的匹配来处理映射并且进行反向代理。

在nginx网关中添加如下配置:注意大网关不要做太多处理,不是我们的逻辑网关,逻辑网关是openresty,所以去掉if ($request_method = 'OPTIONS') { return 200; }

upstream openresty-cluster {
    server 192.168.1.121:9011;
    server 192.168.1.122:9022;
    server 192.168.1.123:9033;
}

……

location /resume {
    proxy_pass http://openresty-cluster;
}

这里的resume,你可以认为是我们项目的专属,只要符合这个resume这个标识,都是可以放行去请求后面的接口的。相当于说,这个就是公司的门口,门口保安大爷看到是resume这家公司的,你就可以进门了,否则不给进。

OpenResty 基本演绎与lua插件

修改hostname,可以区分每个tab是哪台虚拟机

Step1 查看主机名
hostname

Step2  修改主机名
hostnamectl set-hostname abc

Step3  再次查看主机名
hostname 
无需重启只需新开会话便可变为新的主机名

openresty结合lua

  1. 加载需要的模块依赖

     

    # openresty 加载lua模块
    lua_package_path "/usr/local/openresty/lualib/?.lua;;";
    
    # openresty 加载c模块
    lua_package_cpath "/usr/local/openresty/lualib/?.so;;";
    
  2. 使用lua来返回对应的数据

    location /resume/refresh {
        # 返回的数据类型
        default_type application/json;
        # 返回的数据内容从哪来
        content_by_lua_file lua/resume.lua;
    }
    

    如下:

  3. 加载外部lua文件逻辑: 创建的外部文件夹所处位置和conf同级

    复制浏览器的json响应,做一个固定返回:
    resume.lua

    1.  重新测试
       
    1.                                  

idea插件

idea 插件:EmmyLua,方便lua开发,语法提示等

vscode 插件:Lua Debug

运行lua:

善后

把另外的openresty节点,也同样设置。
 

Nginx 负载均衡

前提:先把openresty的lua配置到每个openresty节点。才能测试负载均衡。

负载均衡之轮询

负载均衡之权重

配置不同可以使用该配置,默认权重为1

权重越大,分配的流量也就越多

负载均衡之 ip_hash

ip_hash 可以保证用户访问可以请求到上游服务中的固定的服务器,前提是用户ip没有发生更改。 使用ip_hash的注意点: 不能把后台服务器直接移除,只能标记down.

If one of the servers needs to be temporarily removed, it should be marked with the down parameter in order to preserve the current hashing of client IP addresses.

upstream tomcats {
        ip_hash;

        server 192.168.1.173:8080;
        server 192.168.1.174:8080 down;
        server 192.168.1.175:8080;
}

参考: http://nginx.org/en/docs/http/ngxhttpupstreammodule.html#iphash

负载均衡之 urlhash 与 leastconn

根据每次请求的url地址,hash后访问到固定的服务器节点。

upstream tomcats {
    # url hash
    hash $request_uri;

    server 192.168.1.173:8080;
    server 192.168.1.174:8080;
    server 192.168.1.175:8080;
}

 拓展:一致性哈希算法

增加和减少服务都会进行重新计算,带来问题是用户在原来的服务器里的会话全部丢失,设置的相应缓存也会请求不到,用户在请求的时候时间就会更长,解决办法一致性哈希算法

用户安照顺时针离那个服务器近访问那个

节点3宕机了,访问节点3的用户会话,缓存都会丢失,用户转为访问节点4,其他节点与用户的会话,缓存没有丢失,不受影响

增加节点服务器同理

 VSCode 远程SSH编辑工具

先安装工具

远程编辑器:

可以在本地远程编写。很多文档编辑器也都支持,比如ue编辑器等。

添加host:        输入自己的地址选择配置文件,第一个当前用户之下 第二个全局文件都行host添加成功

选择自己的主机并且输入密码:

点击上面箭头所指的位置,弹出新会话:

操作根目录

如此可以看到linux上的文件结构了:

可以进行修改了  

OpenResty 各种请求参数的获得

http协议的版本

ngx.say("http协议版本:" .. ngx.req.http_version());

请求方法

ngx.say("请求method:" .. ngx.req.get_method());

原始请求头

ngx.say("原始请求头内容:" .. ngx.req.raw_header());

请求头信息

token: abc uid: 1001

local myHeader = ngx.req.get_headers()
ngx.say("token: " .. myHeader.token);
ngx.say("uid: " .. myHeader.uid);

获得请求参数

local args = ngx.req.get_uri_args()
ngx.say("resumeId:" .. args["resumeId"]);
ngx.say("userId:" .. args["userId"]);

获得body参数

获得的类型是json字符串

ngx.req.read_body(); --读取请求体(流)
local body_raw = ngx.req.get_body_data() --定义一下
ngx.say(body_raw)

获得post请求表单参数

获得的类型是table类型

-- 读取请求体
ngx.req.read_body();
local form_data = ngx.req.get_post_args();
for key,value in pairs(form_data) do
    ngx.say(key, value);
end

OpenResty 转发HTTP请求

local response = ngx.location.capture("[nginx虚拟server监听path]", {
    method = ngx.HTTP_GET,       -- 请求方法
    args = {name=imooc,age=18},  -- 请求参数
    body = "name=imooc&age=18"   -- post body参数
})
response.status  -- 状态码
response.body    -- 返回的数据
response.header  -- 返回的头信息

封装通用方法类

编写一个http请求工具类,可以放入到lualib路径中,如此可以更加的通用化

local function send_post(path, params) 
    local response = ngx.location.capture(path, {
        method = ngx.HTTP_POST,   
        args = params  
    });
    -- 目标路径不存在,无法响应客户端
    if not response then
        ngx.exit(404);
    end
    -- 只要有响应,不管是200还是5xx状态,直接return
    return response.body;
end

-- 导出函数
local http = {
    send_post = send_post
}
return http;

lua发请求

调整ngxin的路由,否则controller无法接收:

调用http请求发送
'http' : 文件名称

测试

OpenResty 集成Redis

OpenResty结合lua脚本,可以实现对redis的操作,如此,我们就可以直接在nginx中使用redis缓存,而不需要再把请求放到微服务节点里去请求。

https://github.com/openresty/lua-resty-redis

使用redis:

-- 导入redis
local resty_redis = require("resty.redis");
-- 初始化redis对象
local redis = resty_redis:new();
-- 设置超时时间,单位ms
-- 1. 建立连接的超时时间
-- 2. 发送请求的超时时间
-- 3. 响应数据的超时时间
redis:set_timeouts(5000, 5000, 5000);

redis-lua 工具类

-- 引入redis
local redis = require('resty.redis');
-- 初始化redis
local red = redis:new();
red:set_timeouts(5000, 5000, 5000);

-- 设置全局变量
redis_ip = "192.168.1.121";
redis_port = 6379;

-- 从redis中查询数据
local function get_data(key)
    -- 建立连接
    local ok, err = red:connect(redis_ip, redis_port);
    if not ok then
        ngx.log(ngx.ERR, "failed to connect: ", err)
        return
    end

    local res, err = red:auth("imooc")
    if not res then
        ngx.log(ngx.ERR, "failed to authenticate: ", err)
        return
    end

    -- 查询redis
    local res, err = red:get(key)
    -- 最后一行设置连接池
    do_gracefully_redis_pool(red);
    return res;
end

-- 在redis中设置数据
local function set_data(key, value)
    -- 建立连接
    local ok, err = red:connect(redis_ip, redis_port);
    if not ok then
        ngx.log(ngx.ERR, "failed to connect: ", err)
        return
    end

    local res, err = red:auth("imooc")
    if not res then
        ngx.log(ngx.ERR, "failed to authenticate: ", err)
        return
    end

    --  设值
    ok, err = red:set(key, value);
    ok, err = red:expire(key, 1500);
    -- 最后一行设置连接池
    do_gracefully_redis_pool(red);
end


-- 导出函数
local _M = {
    get_data = get_data,
    set_data = set_data
}
return _M;

连接池设置

redis连接池,本质上其实是一个优雅的关闭redis的方法:

-- 连接池,优雅关闭redis连接
local function do_gracefully_redis_pool(redis) 

    -- redis对象 如果存在初始化则不要继续向下执行
    if not redis then 
        return;
    end

    -- 连接的最大空闲时间,单位ms,时间单位内redis不会关闭,如果空闲则关闭
    local pool_max_idel_time = 15000; 
    -- 连接池大小
    local pool_size = 50; 
    -- 设定连接池参数
    local ok, err = redis:set_keepalive(pool_max_idel_time, pool_size);

    if not ok then
        ngx.log(ngx.ERR, "failed to set keepalive: ", err)
        return;
    end
end

调用工具类

local redis = require('redis_util');

local name = redis.get_data("name");
ngx.say("redis_key:name:", name);

redis.set_data("nginx-lua", "hello lua~~~");

测试

注意需要密码

local res, err = red:auth("imooc")
if not res then
    ngx.log(ngx.ERR, "failed to authenticate: ", err)
    return
end

是否使用keepalive,未使用:

已使用:

其他两个节点设置一下

OpenResty 实现网关缓存

目标: 先查询缓存,如果缓存都有数据,则在网关中判断返回即可。 如果网关中缓存不存在,则请求向微服务转发。

代码脚本

ngx.null是对象判空,null基本类型判空

local today = os.date("%Y-%m-%d");
-- ngx.say("today:", today);

-- 获得最大刷新次数
local max_resume_refresh_counts = redis.get_data("max_resume_refresh_counts");
-- ngx.say("max_resume_refresh_counts:", max_resume_refresh_counts);

-- 获得用户id
local userId = args["userId"]
-- ngx.say("userId:", userId);
-- 从redis中获得用户在当天的已刷新次数
local user_already_refreshed_counts = redis.get_data("user_already_refreshed_counts:" .. today .. ":" .. userId);
-- ngx.say("user_already_refreshed_counts:", user_already_refreshed_counts);
if user_already_refreshed_counts == ngx.null then
    user_already_refreshed_counts = 0;
end
-- ngx.say("user_already_refreshed_counts:", user_already_refreshed_counts);

-- ngx.say("user_already_refreshed_counts:", user_already_refreshed_counts);
-- ngx.say("max_resume_refresh_counts:", max_resume_refresh_counts);
-- 判断,一旦超过次数,则拒绝
if (tonumber(user_already_refreshed_counts) >= tonumber(max_resume_refresh_counts)) then
    ngx.say('{"status":5711,"msg":"本日刷新次数已达上限!","success":false,"data":null}');
    return;
end
-- 未超过次数,向微服务转发请求
local result = send_post("/resume/refresh", args);
ngx.say(result);

测试

其他节点保持一致

Nginx 本地缓存共享字典的实现

前提:目前3个节点的openresty,都把redis进行了整合实现。

开启本地缓存

# 开启本地缓存,共享词典
lua_shared_dict resume_refresh_cache 10m;
#解释:lua_shared_dict是开启本地缓存,dictionary_name 是字典的名称,size 是字典的大小,可以使用k/m/g来指定KB/MB/GB,10m为10M

 缓存存取更多数据,可以把大小设置大一些,不建议把所有大量的缓存都放到nginx本地缓存里,建议存储相对高并发量比较多,热数据 

使用本地缓存

resume.lua

-- 引入ngx的本地缓存
local local_cache = ngx.shared.resume_refresh_cache;
-- key, value, expire 时间单位:秒(0表示永不过期)
local_cache:set("age", 18, 10);
local get_cache = local_cache:get("age");
ngx.say("get_cache: ", get_cache);

结合业务实现

  • 先查询ngx本地缓存,如果没有,再查redis
  • 如果一开始本地缓存和redis都没有缓存,则向后转发请求
  • 转发请求之后,redis中必定存在缓存值,则查询redis再设置到ngx本地缓存中(或者前置在转发请求之前,存入本地缓存)
  • 其他业务:如果没有缓存预热,如果是根据请求查询的信息数据,比如一个对象信息,那么,则需要在请求转发之后,在存入到ngx本地缓存。

-- 获得最大刷新次数
local max_resume_refresh_counts = local_cache:get("local_max_counts");
-- ngx.say("local_max_counts: ", max_resume_refresh_counts);
-- 如果本地缓存中没有,则从redis中获取
if (max_resume_refresh_counts == nil) or (max_resume_refresh_counts == ngx.null) then
    ngx.log(ngx.ERR, "ngx本地缓存为空,从redis中获得...")
    local redis_max_counts = redis.get_data("max_resume_refresh_counts");
    max_resume_refresh_counts = redis_max_counts;
    -- 从redis中获取之后,在本地缓存中设值
    local_cache:set("local_max_counts", redis_max_counts, 20);
end

其他节点保持一致,配置一下

多级缓存数据同步方案

  • 有效时间过期(不能及时更新,可能造成缓存击穿)
  • 双删多删(可能缓存不一致,代码侵入性大)
  • 消息队列(推荐3颗星:异步,稍微一点的代码侵入性)
  • Canal(推荐4颗星:异步,解耦性更高)
  • Zookeeper(推荐:异步)

数据库读取的数据存储在redis和mq或者其他消息队列中,nginx和微服务读取redis,微服务本地缓存咖啡因通过多个消费者监听消息队列。nginx和微服务都存在redis缓存, 可以只存在nginx中redis缓存,微服务的redis缓存去掉,nginx也有本地缓存,可以不用更新,我们在共享字典里过期时间可以设置短一些半分钟,一分钟左右。业务是限定用户的刷新并没有要求太大的一致性。

通过canal去监听我们数据库中的biglog,就可以同步到我们微服务各个节点的本地缓存中,覆盖本地咖啡因缓存,

数据写入到数据库之后,service再向我们zookeeper写入节点数据,节点有监听机制,监听我们的节点数据,一旦有数据的变更,就会在各自的微服务节点把变更的数据覆盖本地咖啡因数据。 

Docker更新Canal环境变量

查看配置环境:

docker inspect canal

ENV环境变量 

更新环境参数:

  • 为了不影响之前的,可以重新创建新的canal容器再进行同步
  • 可以删除原来的镜像,更新监听数据表策略表达式
  • 直接更新docker中指定容器的环境配置参数文件config.v2.json

先停止docker,否则无法重置环境参数:

systemctl stop docker

进入到docker的容器配置文件:

/var/lib/docker/containers/容器ID/config.v2.json

把新添加的参数配置进去:重启docker:(修改之前停止docker,不去修改,直接修改,做重启,会被覆盖,会恢复成原来的样子 )

sudo systemctl restart docker

再次查看:进入容器查看日志:

docker exec -it canal bash

cd /home/admin/canal-server/logs/imooc
tail -n 500 imooc.log

imooc使我们的一个命名空间

Canal 缓存数据同步

配置监听

同步本地缓存

更新到redis:更新微服务的本地缓存:

nginx的本地缓存,时间设置短一些即可。

测试

从admin端修改阈值,然后用户刷新,测试缓存的变动

刷新到上限,更改更大的阈值,然后进行测试
nginx本地缓存失效,会去查redis,canal监听已经存到redis中了,

思考

canal监听到以后,是否需要把redis放在监听中进行更新缓存,还是redis放在执行更新的业务中?哪个好?放在监听中比较好,因为可以和业务解耦,但是如果有n个微服务节点监听,则redis会修改n次,由于是多级缓存,可以在各个微服务监听中去进行更新,更新到redis中,所以不容易造成缓存击穿,实际应用场景基本是2,3个微服务节点,如果只有一个redis做缓存,可能会产生缓存击穿,则不建议这么做。

多级缓存高并发压测

nginx+openresty+caffeine+redis

caffeine+redis

整体要比nginx要快,因为他没有nginx的两个网关,不需要负载均衡,所以请求链路少了,整体测试就快了。

通过nginx来测试,在nginx排除openresty缓存:

结果:响应时间增多了

redis

去掉本地缓存caffeine:

由此可见,随着缓存一级一级的减少,接口的响应时间增多了

测试结果:为了方便,恢复微服务网关的跨域配置,后续开发还是走微服务,不走nginx


 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值