多级缓存(Nginx,OpenResty,Redis,Caffine,Canel)
架构
实际开发中往往使用多级缓存架构,如下图
- Java应用使用Caffine等缓存技术在JVM中缓存数据库的数据
- Redis(集群)做Java应用的缓存
- OpenResty(集群)做Redis(以及Java应用)的缓存
- 用户本地缓存
这种多级缓存结构可以大大的减少数据库甚至Web服务器的压力,因为大部分请求都被前面的缓存处理好了。但这也给实现数据一致性带来了一些挑战。
本篇文章不会事无巨细的介绍搭建的全过程,只介绍其中需要特别注意的地方,我这是看黑马程序员的网课学的,你想看全过程就去这里
OpenResty
OpenResty是用于Nginx平台上的一款Web服务器,可以使用lua语言进行业务逻辑的开发,emmm,最近正在研究的好多东西都用到lua。
OpenResty官方提供了Docker镜像所以可以很方便的使用Docker来搞它,下面是总体架构中的完整dockercompose文件中OpenResty的部分:
# docker-compose.yaml
nginx_inst1:
image: openresty/openresty:1.21.4.1-bullseye-fat
ports:
- 8080:8080
volumes:
- ./nginx/nginx-openresty-cluster.conf:/usr/local/openresty/nginx/conf/nginx.conf
- ./html/:/usr/local/openresty/nginx/html/
- ./lua/item.lua:/usr/local/openresty/nginx/lua/item.lua
- ./lua/common.lua:/usr/local/openresty/lualib/common.lua
OpenResty在整个的架构中连接着redis
和tomcat的业务集群,**它们持有着一份本地缓存,它们的查询路径是本地缓存
->Redis
->Tomcat
OpenResty本地缓存
由于要搭建OpenResty集群,所以每个集群中都有一个本地缓存,本地缓存通过下面的指令创建:
# nginx/nginx-openresty-cluster.conf
# shared_dict, 150m大小
lua_shared_dict item_cache 150m;
这样的话,集群中每个实例持有一个本地缓存,但它们不是共享的,如果你不想在其中保存冗余数据或降低缓存命中率的话,请将前台nginx反向代理的负载均衡算法设置成根据请求url进行hash,这样可以保证相同的url的缓存都被保存到同一个OpenResty节点上。
# nginx/nginx-front.conf
upstream nginx-cluster {
hash $request_uri;
server nginx_inst1:8080;
# ... more server ...
}
lua编程
在进行OpenResty的lua编程前,需要在配置文件的http
作用域下导入lualib和clib
# nginx/nginx-openresty-cluster.conf
lua_package_path "/usr/local/openresty/lualib/?.lua";
lua_package_cpath "/usr/local/openresty/lualib/?.so";
然后,对于想由lua处理的请求,通过content_by_lua_file
指令调用对应的lua
文件
server {
listen 8080;
# 正常处理的url(反向代理到tomcat集群)
location /item {
proxy_pass http://tomcatserver;
}
# 通过lua文件处理的url
location ~ /api/item/(\d+) {
default_type application/json;
content_by_lua_file lua/item.lua;
}
}
编写item.lua
,它其中要干这么几件事:
- 获取用户要查找的itemid,即url
/api/item/(\d+)
中的路径参数 - 根据此id获取本地缓存
- 若失败获取redis缓存
- 若失败转发到tomcat业务集群
- 将结果保存到本地缓存
代码会在后面放出
OpenResty本地缓存过期
在OpenResty中可以设置本地缓存的过期时间,可以根据业务对数据一致性的强弱不同来设置不同的时间,甚至不保存本地缓存。
不知道OpenResty能不能和各种MQ或Canal整合,如果可以,通过消息队列进行数据更改通知也是一条路子。
Tomcat集群
Redis数据预热
在我们的例子中,Tomcat和OpenResty都并不直接将数据添加到Redis(你当然可以根据你的业务做出不同的决策),那Redis中的缓存是怎么来的呢?
- 当数据发生增删改时,Tomcat会向Redis中操作
- Tomcat启动时会做数据预热,即将数据库数据全部保存到Redis中
一般情况下,负责数据分析的团队会分析出热点数据,然后我们只需要将热点数据保存到Redis中即可,这里我们直接将所有数据都导入进Redis作为演示
@Component
public class RedisCacheHandler implements InitializingBean {
// ....
private void serializeAndSaveToRedis(String key, Object o) throws JsonProcessingException {
String json = mapper.writeValueAsString(o);
template.opsForValue().set(key, json);
}
@Override
public void afterPropertiesSet() throws Exception {
for (Item item : itemService.list()) serializeAndSaveToRedis("item:"+item.getId(), item);
for (ItemStock item : stockService.list()) serializeAndSaveToRedis("stock:"+item.getId(), item);
}
}
Canal监听MySQL的binlog变化
Canal是阿里出品的一个可以监听MySQL变化并异步通知给其它人的工具。
它用起来有点像消息队列,它的实现原理是伪装成MySQL的从节点并连接到MySQL服务器,这时,MySQL会在binlog发生改变时(发生增删改)将这个改动通知给从节点,这样Canal就实现了数据库增删改的监听。
Canal提供了各种语言的客户端,以监听Canal的消息,所以Canal就是个在数据库和应用间提供监听功能的中间件
配置Canal时要注意的
- MySQL必须开启binlog
- binlog的类型是ROW
一个巨大的坑
当我完成了所有Canal的配置时,Canal和MySQL似乎能正确连接,并且Java客户端也能监听到Canal的部分消息。但它监听到的消息里只有TRANSACTIONBEGIN
和TRANSACTIONEND
,即它只监听到了事务的开启和关闭,并没有监听到行的改变。
我当时怀疑就是哪里的表名配置错了,但我检查了几个小时也没有发现任何问题,后来我看到了这样一篇文章:
里面说是一个转义字符引起的,我尝试把它去掉,就一切正常了
-canal.instance.filter.regex=hcache\\..*
+canal.instance.filter.regex=hcache\..*