多级缓存架构设计(四)

1 、多级缓存实现
 
一般来说,缓存有两个原则
 
一是越靠近用户的请求越好。比如,能用本地缓存的就不要发送 HTTP 请求,能用 CDN 缓存的就不要打到源站,能用 OpenResty 缓存的就不要打到数据库。
 
二是尽量使用本进程和本机的缓存解决。因为跨了进程和机器甚至机房,缓存的网络开销 就会非常大,这一点在高并发的时候会非常明显。
 
自然,在 OpenResty 中,缓存的设计和使用也遵循这两个原则。 OpenResty 中有两个缓存的组件: shared dict 缓存和 lru 缓存。前者只能缓存字符串对象,缓存的数据有且只有 一份,每一个 worker 都可以进行访问,所以常用于 worker 之间的数据通信。后者则可以 缓存所有的 Lua 对象,但只能在单个 worker 进程内访问,有多少个 worker ,就会有多少份缓存数据。
下面这两个简单的表格,可以说明 shared dict lru 缓存的区别:

 

先查看本地是否存在,如果存在直接返回如果不存在再去查询 redis 如果 redis 当中还不存在,就请求到源服务器,这是跨机器、跨网络的多级请求本地实际上也可以利用字典及lru 实现多级缓存
 
2 、缓存击穿问题
缓存击穿,是指某个极度 热点 数据在某个时间点过期时,恰好在这个时间点对这个 KEY 有大量的并发请求过来,这些请求发现缓存过期一般都会从 DB 加载数据并回设到缓存,但是这个时候大并发的请求可能会瞬间 DB 压垮。
 
先来设想下面这么一个场景。数据源在 MySQL 数据库中,缓存的数据放在共享字典中,超时时间为 1 分钟。在这 1 分钟内的时间里,所有的请求都从缓存中获取数据,MySQL 没有任何的压力。但是,一旦到达 1 分钟,也就是缓存数据失效的那一刻,如果正好有大量的并发请求进来,在缓存中没有查询到结果,就要触发查询数据源的函数,那么这些请求全部都将去查询 MySQL 数据库, 直接造成数据库服务器卡顿,甚至卡死。
 
对于一些设置了过期时间的 KEY ,如果这些 KEY 可能会在某些时间点被超高并发地访问,是一种非常 热点 的数据。这个时候,需要考虑这个个问题。
 
如何避免缓存击穿?
 
1 、主动更新缓存:默认缓存是被动更新的。只有在终端请求发现缓存失效时,它才会去数据库查询新的数据。那么,如果我们把缓存的更新,从被动改为主动,也就可以直接绕开缓存风 暴的问题了,在 OpenResty 中,我们可以使用 ngx.timer.every 来创建一个定时器去定时更新。
 
缺点:每一个缓存都要对应一个周期性的任务;而且缓存过期时间和计划任务的周期时间还要对应好,如果这中间出现了什么纰漏, 终端就可能 一直获取到的都是空数据
 
2 、使用互斥锁:请求发现缓存不存在后,去查询 DB 前,使用锁,保证有且只有一个请求去查询 DB ,并更新到缓存。流程如下:
  • 获取锁,直到成功或超时。如果超时,则抛出异常,返回。如果成功,继续向下执行。
  • 再去缓存中。如果存在值,则直接返回;如果不存在,则继续往下执行如果成功获取到锁的话,就可以保证只有一个请求去数据源更新数据,并更新到缓存中了。
  • 查询 DB ,并更新到缓存中,返回值。
openresty 可以利用 lua-resty-lock 加锁,利用的是 OpenResty 自带的 resty 库,它底层是基于共享字典,提供非阻塞的 lock API
不过,在上面 lua-resty-lock 的实现中,你需要自己来处理加锁、解锁、获取过期数据、重试、异常处理等各种问题,还是相当繁琐的 我们可以使用 lua-resty-mlcache,看下
 

OpenResty的快速自动化多级缓存

这个库可以作为 key/value存储来处理,但是可以缓存Lua类型和表,但是构建在lua_shared_dictlua-resty-lrucache上 ,这种组合可以实现高性能和灵活的缓存。

功能:

使用TTL缓存和负缓存,

通过lua-resty-lock构建互斥体,以防止在缓存失败时将狗堆影响到数据库/后端,

内置worker通信,传播缓存失效,并允许worker更新他们的L1 (lua-resty-lrucache )缓存的更改(set()delete() ),

可以创建多个独立实例来保存各种类型的数据,同时依赖同一lua_shared_dict L2缓存

缓存级别层次结构为:

  • L1 :使用Least-Recently-Used land缓存lua-resty-lrucache ,在填充时提供最快的查找,并避免耗尽worker的Lua虚拟机内存,
  • L2 :所有员工共享的lua_shared_dict内存区域,只有L1未命中时才能访问此级别,并防止工作人员请求L3缓存,
  • L3 :自定义函数,只由单个worker运行,以避免数据库/后端(通过lua-resty-lock )上的狗堆效应,通过L3获取的值将被设置为L2缓存,供其他员工访问

1)下载lua-resty-mlcache

git clone https://github.com/thibaultcha/lua-resty-mlcache.git

#将下载下来的lua-resty-mlcache/lib/resty文件下的文件拷到/usr/local/openresty/lualib/project/common/lualib/resty

2)nginx.conf文件

user  root;
worker_processes  2;
daemon off;#避免nginx在后台运行
#error_log  logs/error.log;
#error_log  logs/error.log  notice;
#error_log  logs/error.log  info;

#pid        logs/nginx.pid;


events {
   worker_connections  20480;#单个进程允许的客户端最大连接数
}


http {
    include       mime.types;
    #default_type  application/octet-stream;
    lua_code_cache off; #关闭代码缓存上线后去掉
    #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;
	
	lua_package_path "/usr/local/openresty/lualib/project/common/lualib/?.lua;;/usr/local/openresty/lualib/project/common/resty-redis-cluster/lib/?.lua;;";
	lua_package_cpath "/usr/local/openresty/lualib/project/common/resty-redis-cluster/src/?.so;;";
    
	lua_shared_dict redis_cluster_slot_locks 100k;
	lua_shared_dict redis_cluster_addr 20k;
	lua_shared_dict my_lock 20k; #锁
	lua_shared_dict my_cache 1M; #缓存
	lua_shared_dict ipc_shared_dict 1M; #进程间通讯

	init_worker_by_lua_file /usr/local/openresty/lualib/project/init.lua;
	server {
        listen       80;
        location / {
            root   /var/www/html;
			content_by_lua_file /usr/local/openresty/lualib/project/cache.lua;
			index index.php index.html index.htm;
        }

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

        location ~ \.php/?.* {
            root           /var/www/html;#php-fpm容器中的路径,不是nginx路径
            fastcgi_pass   127.0.0.1:9002;#对应容器的端口
            fastcgi_index  index.php;
            #为php-fpm指定的根目录
            fastcgi_param  SCRIPT_FILENAME $document_root$fastcgi_script_name; #加了这一项
            #定义变量$path_info,存放pathinfo信息
            set $path_info "";
            if ($fastcgi_script_name ~ "^(.+?\.php)(/.+)$") {
                #将文件地址赋值给变量 $real_script_name
                set $real_script_name $1;
                #将文件地址后的参数赋值给变量 $path_info
                set $path_info $2;
            }
            #配置fastcgi的一些参数
            fastcgi_param SCRIPT_NAME $real_script_name;
            fastcgi_param PATH_INFO $path_info;
            include        /usr/local/openresty/nginx/conf/fastcgi_params;
       }
    }
}

3)cache.lua文件

local mlcache = require "resty.mlcache"
local resty_lock = require ("resty.lock")
local key = ngx.re.match(ngx.var.request_uri,"/([0-9]+).html")
--1、L1缓存使用lua_resty_lrucache
local function fetch_shop(key)
   local lock = resty_lock:new("my_lock",{exptime=10,timeout=1})
       if not lock then
           ngx.log(ngx.ERR,"创建失败")
           return
       end
       local flag_lock,err = lock:lock(key)
       if err then
           ngx.log(ngx.ERR,"加锁失败")
       end
       --如果加锁失败,获取旧数据
       if not flag_lock then
           local res = cache:get_stale(key)
           ngx.log(ngx.ERR,"获取锁失败:占用")
           return res
       end
       --锁成功获取,可能已经有人将值放入到缓存当中了,再检查下
       local res,err = cache:get(key)
       if res then
         lock:unlock()
         return res
       end
      -- ngx.sleep(2)
       -- 如果仍然没有再请求服务器
       local req_data
       local method = ngx.var.request.method
       if method == "POST" then
           req_data = {method=ngx.HTTP_POST,body=ngx.req.read_body()}
       elseif method == "PUT" then
           req_data = {method=ngx.HTTP_PUT,body=ngx.req.read_body()}
       else
           req_data = {method=ngx.HTTP_GET}
       end
       local uri
       if not  ngx.var.request.uri then
          uri = ''
       end
       local res,err = ngx.location.capture(
           "/index.php"..uri,
           req_data
       )
       if res.status == 200 then
           ngx.say(res.body)
       end
       --解锁
       lock:unlock()
end
if type(key) == "table" then
    --创建L1缓存
    local cache,err = mlcache.new("cache_name","my_cache",{
            lru_size = 500,    -- 用于定义基础L1缓存(lua-resty-lrucache实例)的大小
            --ttl      = 3600,   -- 指定缓存值的到期时间
            ttl      = 5,   -- 指定缓存值的到期时间
            -- neg_ttl  = 30,     -- 指定缓存的未命中的过期时间(当L3回调返回时nil)
            neg_ttl  = 6,     -- 指定缓存的未命中的过期时间(当L3回调返回时nil)
            ipc_shm  = "ipc_shared_dict", --用于将L2的缓存设置到L1
    })
    --如果没有值记录错误日志
    if not cache then
         ngx.log(ngx.ERR,"创建失败",err)
    end
    ngx.say(key[1])
    --如果有值,获取值
    local res,err = cache:get(key[1],nil,fetch_shop,key[1])
    ngx.say(res)
    -- 在L2缓存中设置一个值,并将事件广播到其他工作进程,
    if not res then
        cache:set(key[1],nil,res)
    end
end

 

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值