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的快速自动化多级缓存
- 源代码名称:lua-resty-mlcache
- 源代码网址:https://github.com/thibaultcha/lua-resty-mlcache
这个库可以作为 key/value存储来处理,但是可以缓存Lua类型和表,但是构建在lua_shared_dict,lua-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