多级缓存

多级缓存

多级缓存概述

多级缓存是一种设计思路,充分利用请求的每个环境进行缓存,减少Tomcat的压力

  • 一级缓存:浏览器客户端缓存,主要由cookie或LocalStroage
  • 二级缓存:Nginx本地缓存
  • 三级缓存:Redis缓存,主要由Nginx用Lua发起向Redis的询问
  • 四级缓存:JVM缓存,进程缓存
  • 五级缓存:本地数据库

JVM缓存

JVM是一种基于本地进程的缓存,相较于Redis其使用本地缓存,没有网络开销(Redis可能使用其他服务器)速度更快,但是不能数据共享.

常见的JVM缓存就是以静态变量的形式进行缓存,线程未结束,变量就不会被释放.

Spring官方推荐使用Caffeine,其内部就是使用静态Map

创建Caffeine

java //创建构造器 Cache<String,String> cache = Caffeine.newBuilder().build();

创建构造器时可添加其他功能

  • initialCapacity(long):存储初始化大小
  • maximumSize(long):存储最大大小
  • expireAfterWrite(Duration.ofSeconds(long)):存储存long秒后释放

一般在Config里创建Caffeine

java @Configuration public class CaffeineConfig { @Bean public Cache<String,String> XXCache(){ Cache<String,String> cache = Caffeine.newBuilder() .initialCapacity(1) .maximumSize(1) .expireAfterWrite(Duration.ofSeconds(10)) .build(); return cache; } }

使用Caffeine

java //存数据 cache.put("键","值");

java //取数据,若无数据返回Null cache.getIfPresent("键"); //取数据,若无数据执行方法 cache.get("键",key->{ return ""; });

Nginx使用Redis

Nginx配置修改

使用前需要导入OpenResty

```nginx

user nobody;

worker_processes 1;
error_log logs/error.log;

events {
worker_connections 1024;
}

http {
include mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
#导入lua模块
lua_package_path "user/local/openrestry/lualib/?.lua;;";
#导入c模块
lua_package_cpath "user/local/openrestry/lualib/?.so;;";
#设置集群
upstream tomcat-cluster{
server tomcatIp1:端口;
server tomcatIp2:端口;
}
server {
#监听端口和域名
listen 8081;
server_name localhost;
#映射位置
location / {
root html;
index index.html index.htm;
}
location /item{
proxy_pass http://tomcat-cluster;
}
#设置某接口监听由lua脚本控制
location ~ /api/item/(\d+){
# 默认响应类型
default_type application/json;
# 响应结果由lua/item.lua文件决定
content_by_lua_file lua/item.lua
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
}
}
```

导入common.lua

```lua
-- 导入redis
local redis = require('resty.redis')
-- 初始化redis
local red = redis:new()
red:set_timeouts(1000,1000,1000)

-- 关闭redis连接的工具方法,其实是放入连接池
local function close_redis(red)
local pool_max_idle_time = 10000 -- 连接的空闲时间,单位是毫秒
local pool_size = 100 --连接池大小
local ok, err = red:set_keepalive(pool_max_idle_time, pool_size)
if not ok then
ngx.log(ngx.ERR, "放入redis连接池失败: ", err)
end
end
-- 查询redis的方法 ip和port是redis地址,key是查询的key
local function read_redis(ip, port, key)
-- 获取一个连接
local ok, err = red:connect(ip, port)
if not ok then
ngx.log(ngx.ERR, "连接redis失败 : ", err)
return nil
end
-- 查询redis
local resp, err = red:get(key)
-- 查询失败处理
if not resp then
ngx.log(ngx.ERR, "查询Redis失败: ", err, ", key = " , key)
end
--得到的数据为空处理
if resp == ngx.null then
resp = nil
ngx.log(ngx.ERR, "查询Redis数据为空, key = ", key)
end
close_redis(red)
return resp
end
-- 封装函数,发送http请求,并解析响应
local function read_http(path, params)
local resp = ngx.location.capture(path,{
method = ngx.HTTP_GET,
args = params,
})
if not resp then
-- 记录错误信息,返回404
ngx.log(ngx.ERR, "http not found, path: ", path , ", args: ", args)
ngx.exit(404)
end
return resp.body
end
-- 将方法导出
local _M = {
read_http = read_http,
read_redis = read_redis
}
return _M
```

导入item.lua

```lua
-- 导入common库,获取发送http请求方法
local common = require('common')
local read_http = common.read_http
local read_redis = common.read_redis
-- 导入json库
local cjson = require('cjson')

-- 获取路径参数
local id = ngx.var[1]

-- 封装查询函数,先查redis,后发http
function read_data(key,path,params)
-- 先查redis
local resp = read_redis("127.0.0.1",6379,key)
-- 判断查询结果,未空则发http
if not resp then
ngx.log(ngx.ERR,"redis查询失败,尝试查询http,key:",key)
resp = read_http(path,params)
end
return resp
end
-- 查询
local itemJson = read_data("item.id"..id,"/item/"..id,nil)
local itemStockJson = read_data("item.stock.id"..id,"/item/stock/"..id,nil)
-- 组合json
local item = cjson.decode(itemJson)
local stock = cjson.decode(itemStockJson)
item.stock = stock.stock
item.sold = stock.sold

-- 返回结果
ngx.say(cjson.encode(item))
```

Nginx本地缓存

OpenResty为Nginx提供了shard dict的功能,在nginx的多个worker之间共享数据,实现缓存功能.

Nginx配置修改

在nginx.conf中添加配置

```nginx

开启本地缓存,名字为item_cache,大小为150m

lua_shard_dict item_cache 150m
```

```nginx

user nobody;

worker_processes 1;
error_log logs/error.log;

events {
worker_connections 1024;
}

http {
include mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
#导入lua模块
lua_package_path "user/local/openrestry/lualib/?.lua;;";
#导入c模块
lua_package_cpath "user/local/openrestry/lualib/?.so;;";
#开启本地缓存,名字为item_cache,大小为150m
lua_shard_dict item_cache 150m;
#设置集群
upstream tomcat-cluster{
server tomcatIp1:端口;
server tomcatIp2:端口;
}
server {
#监听端口和域名
listen 8081;
server_name localhost;
#映射位置
location / {
root html;
index index.html index.htm;
}
location /item{
proxy_pass http://tomcat-cluster;
}
#设置某接口监听由lua脚本控制
location ~ /api/item/(\d+){
# 默认响应类型
default_type application/json;
# 响应结果由lua/item.lua文件决定
content_by_lua_file lua/item.lua
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
}
}
```

本地缓存操作

在lua中操作

lua -- 获取本地缓存对象 local item_cache = ngx.shared.item_cache -- 存储,指定key,value,过期时间,单位s,默认0为永不过期 item_cache:set('key','value',1000) -- 读取 local val = item_cache:get('key')

修改item.lua

如在上文的item.lua中,可先查询本性再查询redis,最后发送http

```lua
-- 导入common库,获取发送http请求方法
local common = require('common')
local read_http = common.read_http
local read_redis = common.read_redis
-- 导入json库
local cjson = require('cjson')
-- 获取本地缓存对象
local item_cache = ngx.shared.item_cache

-- 获取路径参数
local id = ngx.var[1]

-- 封装查询函数,先查redis,后发http
function read_data(key,expire,path,params)
-- 查询本地缓存
local val = item_cache:get(key)
if not val then
-- 查redis
ngx.log(ngx.ERR,"本性缓存查询失败,尝试查询redis,key:",key)
val = read_redis("127.0.0.1",6379,key)
-- 判断查询结果,未空则发http
if not val then
ngx.log(ngx.ERR,"redis查询失败,尝试查询http,key:",key)
val = read_http(path,params)
end
end
item_cache:set(key,val,expire)
return val
end

-- 查询
local itemJson = read_data("item.id"..id,1800,"/item/"..id,nil)
local itemStockJson = read_data("item.stock.id"..id,60,"/item/stock/"..id,nil)
-- 组合json
local item = cjson.decode(itemJson)
local stock = cjson.decode(itemStockJson)
item.stock = stock.stock
item.sold = stock.sold

-- 返回结果
ngx.say(cjson.encode(item))
```

缓存同步

MQ同步

通过MQ,当item服务更新时,发布更新消息,cache服务更新redis

优点:快

缺点:依赖MQ可靠性

Cannel同步

通过监听mysql的binlog,一旦触发增删改事件,则同步更新redis

优点:快,低耦合(不用再item服务写代码)

缺点:依赖数据库可靠性

安装Cannel

1.修改msql配置

sh vi /tmp/mysql/conf/my.cnf

ini log-bin=/var/lib/mysql/mysql-bin binlog-do-db=heima

配置解读:

  • log-bin=/var/lib/mysql/mysql-bin:设置binary log文件的存放地址和文件名,叫做mysql-bin
  • binlog-do-db=heima:指定对哪个database记录binary log events,这里记录heima这个库

### 2.设置用户权限

接下来添加一个仅用于数据同步的账户,出于安全考虑,这里仅提供对heima这个库的操作权限。

mysql create user canal@'%' IDENTIFIED by 'canal'; GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT,SUPER ON *.* TO 'canal'@'%' identified by 'canal'; FLUSH PRIVILEGES;

重启mysql容器即可

docker restart mysql

测试设置是否成功:在mysql控制台,或者Navicat中,输入命令:

show master status;

3.创建网络

我们需要创建一个网络,将MySQL、Canal、MQ放到同一个Docker网络中:

sh docker network create heima

让mysql加入这个网络:

sh docker network connect heima mysql

4.安装Cannel

网络上下载cannel,可以上传到虚拟机,然后通过命令导入:

docker load -i canal.tar

然后运行命令创建Canal容器:

sh docker run -p 11111:11111 --name canal \ -e canal.destinations=heima \ -e canal.instance.master.address=mysql:3306 \ -e canal.instance.dbUsername=canal \ -e canal.instance.dbPassword=canal \ -e canal.instance.connectionCharset=UTF-8 \ -e canal.instance.tsdb.enable=true \ -e canal.instance.gtidon=false \ -e canal.instance.filter.regex=heima\\..* \ --network heima \ -d canal/canal-server:v1.1.5

说明:

  • -p 11111:11111:这是canal的默认监听端口
  • -e canal.instance.master.address=mysql:3306:数据库地址和端口,如果不知道mysql容器地址,可以通过docker inspect 容器id来查看
  • -e canal.instance.dbUsername=canal:数据库用户名
  • -e canal.instance.dbPassword=canal :数据库密码
  • -e canal.instance.filter.regex=:要监听的表名称

表名称监听支持的语法:

mysql 数据解析关注的表,Perl正则表达式. 多个正则之间以逗号(,)分隔,转义符需要双斜杠(\\) 常见例子: 1. 所有表:.* or .*\\..* 2. canal schema下所有表: canal\\..* 3. canal下的以canal打头的表:canal\\.canal.* 4. canal schema下的一张表:canal.test1 5. 多个规则组合使用然后以逗号隔开:canal\\..*,mysql.test1,mysql.test2

使用Cannel

导入依赖

xml <dependency> <groupId>top.javatool</groupId> <artifactId>canal-spring-boot-starter</artifactId> <version>1.2.1-RELEASE</version> </dependency>

编写配置

yaml canal: destination: heima # canal的集群名字,要与安装canal时设置的名称一致 server: 192.168.150.101:11111 # canal服务地址

修改实体类

通过@Id、@Column、等注解完成Item与数据库表字段的映射:

```java
package com.heima.item.pojo;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import org.springframework.data.annotation.Id;
import org.springframework.data.annotation.Transient;

import javax.persistence.Column;
import java.util.Date;

@Data
@TableName("tb_item")
public class Item {
@TableId(type = IdType.AUTO)
@Id
private Long id;//商品id
@Column(name = "name")
private String name;//商品名称
private String title;//商品标题
private Long price;//价格(分)
private String image;//商品图片
private String category;//分类名称
private String brand;//品牌名称
private String spec;//规格
private Integer status;//商品状态 1-正常,2-下架
private Date createTime;//创建时间
private Date updateTime;//更新时间
@TableField(exist = false)
@Transient
private Integer stock;
@TableField(exist = false)
@Transient
private Integer sold;
}
```

编写监听类

通过实现EntryHandler<T>接口编写监听器,监听Canal消息。注意两点:

  • 实现类通过@CanalTable("tb_item")指定监听的表信息
  • EntryHandler的泛型是与表对应的实体类

```java
@CanalTable("tb_item")
@Component
public class ItemHandler implements EntryHandler {

@Autowired
private RedisHandler redisHandler;
@Autowired
private Cache<Long, Item> itemCache;

//发生新增时触发事件
@Override
public void insert(Item item) {
    // 写数据到JVM进程缓存
    ....
    // 写数据到redis
    ...
}

//发生更新时触发事件
@Override
public void update(Item before, Item after) {
    // 写数据到JVM进程缓存
    ...
    // 写数据到redis
    ...
}

//发生删除时触发事件
@Override
public void delete(Item item) {
    // 删除数据到JVM进程缓存
    ...
    // 删除数据到redis
    ...
}

}
```

CC BY-NC-SA 4.0 Deed | 署名-非商业性使用-相同方式共享
最后更新时间:2025-07-19 05:39:21