1.多级缓存必要性
1.传统缓存问题
传统的缓存策略一般是请求到达Tmocat后,先查询Redis,如果未命中则查询数据库,存在几个问题:
1.请求要经过Tomcat处理,Tomcat的性能成为整个系统的瓶颈
2.Redis缓存失效时,会对数据库产生冲击
2.多级缓存方案
多级缓存就是充分利用请求处理的每个环节,分别添加缓存,减轻Tomcat压力,提升服务性能
2.JVM进程缓存
缓存在日常开发中起至关重要的作用,由于是存储在内存中,数据的读取速度是非常快的,能大量减少对数据库的访问,减少数据库的压力。缓存分为两类:
1.分布式缓存,如:redis
优点:存储容量跟更大,可靠性更好,可以在集群间共享
缺点:访问缓存有网络开销
场景:缓存数据量较大,可靠性要求较高,需要在集群间共享
2.进程本地缓存,如:HashMap,GuavaCache
优点:读取本地内存,没有网络开销,速度更快
缺点:存储容量有限,可靠性较低,无法共享
场景:性能要求较高,缓存数据量较小
1.Caffeine
是一个基于Java8开发的,提供了近乎最近命中率的高性能的本地缓存库。Spring用的就是这个。
1.基本使用
/*
基本用法测试
*/
@Test
void testBasicOp() {
// 构建cache对象
Cache<String, String> cache = Caffeine.newBuilder().build();
// 存数据
cache.put("girlFriend", "没有");
// 取数据 如果未命中则返回Null
String girlFriend = cache.getIfPresent("girlFriend");
System.out.println("girlFriend = " + girlFriend);
// 取数据 如果未命中则查询数据库
String girlFriend1 = cache.get("girlFriend", key -> {
// 根据key去数据库查询数据
return "没有";
});
System.out.println("girlFriend1 = " + girlFriend1);
}
2.缓存驱逐策略
默认情况下,当一个缓存元素过期的时候,Caffeine不会自动立即将其清理和驱逐。而是在一次读或写操作后,或者在空闲时间完成对失效数据的驱逐。
1.基于容量
设置缓存的数量上限
// 创建缓存对象
Cache<String, String> cache = Caffeine.newBuilder()
// 设置缓存大小上限为 1
.maximumSize(1)
.build();
2.基于时间
设置缓存的有效时间
// 创建缓存对象
Cache<String, String> cache = Caffeine.newBuilder()
.expireAfterWrite(Duration.ofSeconds(1)) // 设置缓存有效期为 10 秒
.build();
3.基于引用
设置缓存为软引用或弱引用,利用GC来回收缓存数据。性能较差,不建议使用。
3.Lua语法
Lua是一种轻量小巧的脚本语言,用标准C语言编写
1.实现
1.Hello world
1.新建hello.lua文件
touch hello.lua
2.写入执行语句
print("hello world")
3.执行
lua hello.lua
2.变量和循环
数据类型 | 描述 |
nil | 这个最简单,只有值nil属于该类,表示一个无效值(在条件表达式中相当于false) |
boolean | 包含两个值: false和true |
number | 表示双精度类型的实浮点数 |
string | 字符串由双引号或单引号表示 |
function | 由C或Lua编写的函数 |
table | Lua中的表(table)其实是一个“关联数组”(associative arrays),数组的索引可以是数字,字符串或表类型。在Lua里,table的创建是通过“构造表达式”来完成,最简单构造表达式是{},用来创建一个空表 |
可以利用type函数测试给定变量或者值了类型
# lua命令,进入lua控制台,可以直接执行lua脚本
lua
print(type("hello"))
Lua声明变量的时候,并不需要指定数据类型。local声明的局部变量,只能在同一行使用,去掉local则为全局变量,可以换行访问。
-- 声明字符串,字符串拼接用 ..
local str = "hello" .. "world"
-- 声明数字
local num = 20
-- 声明布尔类型
local flag = true
-- 声明数组 key为索引的 table
local arr = {"java", "python", "lua"}
-- 声明table,类似java的map
local map = {name="Jack", age = 21}
访问table
-- 访问数组,lua的数组的角标从1开始
print(arr[1])
-- 访问table
print(map["name"])
print(map.name)
数组,table都可以利用for循环来遍历
遍历数组
-- 声明数组 key为索引的 table
local arr = {"java", "python", "lua"}
-- 遍历数组
for index,value in ipairs(arr) do
print(index, value)
end
遍历table
-- 声明table,类似java的map
local map = {name="Jack", age = 21}
-- 遍历table
for key, value in pairs(map) do
print(key, value)
end
3.函数
定义函数
function 函数名(argument1, argument2..., argumentn)
-- 函数体
return 返回值
end
4.条件控制
if(布尔表达式)
then
-- 布尔表达式为 true 时执行的语句
else
-- 布尔表达式为 false 时执行的语句
end
操作符 | 描述 | 实例 |
and | 逻辑与操作符。若A为false,则返回A,否则返回B. | (A and B)为false |
or | 逻辑或操作符。若A为true,则返回A,否则返回B. | (A or B) 为true |
not | 逻辑非操作符。与逻辑运算结果相反,如果条件为true,逻辑非为false | not(A and B)为true |
5.导入工具包
-- 导入common的函数库,common文件在同目录下建立
local common = require("common")
4.多级缓存实现
1.安装OpenResty
OpenResty是一个基于Nginx的高性能Web平台,用于方便的搭建能够处理超高并发,扩展性极高的动态Web应用,Web服务和动态网关。有如下特点:
1.具备Nginx的完整功能
2.基于Lua语言进行扩展,集成了大量精良的Lua库,第三方模块
3.允许使用Lua自定义业务逻辑,自定义库
安装时虚拟机必须有网
1.首先要安装OpenResty的依赖开发库,执行命令:
yum install -y pcre-devel openssl-devel gcc --skip-broken
2.安装OpenResty仓库
yum-config-manager --add-repo https://openresty.org/package/centos/openresty.repo
如果命令不存在,则运行:
yum install -y yum-utils
然后在执行上面的命令
3.安装OpenResty
yum install -y openresty
4.安装opm工具
opm是OpenResty的一个管理工具,可以帮助我们安装一个第三方的Lua模块。
yum install -y openresty-opm
5.目录结构
默认情况下,OpenResty安装的目录是:/usr/local/openresty
6.配置nginx的环境变量
打开配置文件:
vi /etc/profile
在最下面加入两行:
export NGINX_HOME=/usr/local/openresty/nginx
export PATH=${NGINX_HOME}/sbin:$PATH
NGINX_HOME:后面是OpenResty安装目录下的nginx的目录
然后让配置生效:
source /etc/profile
2.启动和运行
OpenResty底层是基于Nginx的,查看OpenResty目录的nginx目录,结构与windows中安装的nginx基本一致,所以运行方式与nginx基本一致:
# 启动nginx
nginx
# 重新加载配置
nginx -s reload
# 停止
nginx -s stop
3.OpenResty获取请求参数
参数格式 | 参数示例 | 参数解析代码示例 |
路径占位符 | /path/param | 1.正则表达式匹配 location ~ /path/(\s+) { content_by_lua_file lua/result.lua } 2.匹配到的参数会存入ngx.var数组中,可以用角标获取 local param = ngx.var[1] |
请求头 | id: 1 | 获取请求头,返回值是table类型 local headers = ngx.req.get_headers() |
Get请求参数 | ?id=1 | 获取Get请求参数,返回值是table类型 local getParams = ngx.req.get_uri_args() |
Post表单参数 | id=1 | 读取请求体 ngx.req.read_body() 获取POST表单参数,返回值是table类型 local postParams = ngx.req.get_post_args() |
JSON参数 | {"id":1} | 读取请求体 ngx.req.read_body 获取body中的json参数,返回值是string类型 local jsonBody = ngx.req.get_body_data() |
4.发生http请求到tmocat
把http查询的请求封装未一个函数,放到OpenResty函数库里面
1.加载OpenResty的lua模块
#lua 模块
lua_package_path "/usr/local/openresty/lualib/?.lua;;";
#c模块
lua_package_cpath "/usr/local/openresty/lualib/?.so;;";
2.在对应文件夹下建立对应的lua文件
-- 封装函数,发送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
}
return _M
5.反向代理配置和tomcat集群配置
location /api {
proxy_pass http://tomcat-cluster;
}
# 集群名称自定义
upstream tomcat-cluster{
hash $request_uri; # hash算法
server 192.168.1.57:8082;
server 192.168.1.57:8082;
}
5.OpenResty的Redis模块
引入Reids模块,并初始化Redis对象
-- 引入redis模块
local redis = require("resty.redis")
-- 初始化Redis对象
local red = redis:new()
-- 设置Redis的超时时间
red:set_timeouts(1000, 1000, 1000)
封装函数,用来释放Redis连接,其实是放入连接池
-- 关闭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读数据并返回
-- 查询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
6.OpenResty开启nginx本地缓存
OpenResty为Nginx提供了shard dict的功能,可以在nginx的多个worker之间共享数据,实现缓存功能
1.开启共享词典,在nginx.conf的http下添加配置
# 共享字典,也就是本地缓存,名称叫做:item_cache,大小150m
lua_shared_dict item_cache 150m;
2.操作共享字典
-- 获取本地缓存对象
local hzj_cache = ngx.shard.hzj_cache
-- 存储,指定key,value,过期时间,单位s,默认为0代表永不过期
hzj_cache:set("key", "value", 1000)
-- 读取
local val = hzj_cache:get("key")
3.封装查询函数(最终版)
-- 封装查询函数
function read_data(key, expire, path, params)
-- 查询本地缓存
local val = hzj_cache:get(key)
if not val then
ngx.log(ngx.ERR, "本地缓存查询失败,尝试查询Redis, key:", key)
-- 查询redis
val = read_redis("ip", port, key)
-- 判断查询结果
if not val then
ngx.log(ngx.ERR, "redis查询失败,尝试查询http,key:" key)
-- redis查询失败,去重新http
val = read_http(path, params)
end
end
-- 查询成功,把数据写入本地缓存
hzj_cache:set(key, val, exprie)
-- 返回数据
return val
end
5.缓存同步
1.数据同步策略
1.设置有效期:给缓存设置有效期,到期后自动删除。再次查询时更新
优点:简单,方便
缺点:时效性差,缓存过期之前可能不一致
场景:更新频率较低,时效性要求较低的业务
2.同步双写:在修改数据库的同时,直接修改缓存
优点:时效性强,缓存与数据库强一致
缺点:有代码侵入,耦合度高
场景:对一致性,时效性要求较高的缓存数据
3.异步通知:修改数据库时发送事件通知,相关服务监听到通知后修改缓存数据
优点:低耦合,可以同时通知多个缓存服务
缺点:时效性一般,可能存在中间不一致状态
场景:时效性要求一般,有多个服务需要同步
2.基于Canal的异步通知
Canal是基于mysql的主从同步来实现的,MySql主从同步原理如下:
1.MySQL master将数据变更写入二进制日志(binary log),其中记录的数据叫做binary log events
2.MySQL slave将master的binary log events拷贝到它的中继日志(relay log)
3.MySQL slave重放relay log中事件,将数据变更反映它自己的数据
Canal就是把自己伪装成MySQL的一个slave节点,从而监听master的binary log变化,再把得到的变化信息通知给Canal的客户端,进而完成对其它数据库的同步。
1.安装Canal
打开mysql容器挂载的配置文件,修改文件conf,添加内容:
log-bin=/var/lib/mysql/mysql-bin # 设置binary log文件的存放地址和文件名,叫做mysql-bin
binlog-do-db=hzj # 指定对哪个database记录binary log events,这里记录hzj这个库
设置用户权限,对数据库操作
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;
创建网络,我们需要创建一个网络,将MySQL、Canal、MQ放到同一个Docker网络中:
docker network create hzj
让mysql加入这个网络:
docker network connect hzj mysql
下载并运行Canal容器
docker run -p 11111:11111 --name canal \
-e canal.destinations=hzj \ # canal集群名称
-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=hzj\\..* \ # 监听的库
--network hzj\
-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
2.Canal实现
引入依赖
<!-- canal -->
<dependency>
<groupId>top.javatool</groupId>
<artifactId>canal-spring-boot-starter</artifactId>
<version>1.2.1-RELEASE</version>
</dependency>
编写配置
canal:
destination: hzj
server: ip:port
编写监听器,监听Canal消息:
@CanalTable("table_name") // 指定表名
@Component
public class ItemHandler implements EntryHandler<Item> { // 指定表映射的实体类
@Autowired
RedisTemplate redisTemplate;
@Autowired
private Cache<Long, Item> cache;
@Override
public void insert(Item item) {
// 写数据到JVM进程缓存
cache.put(item.getId(), item);
// 写数据到redis
redisTemplate.opsForSet().add(item.getId(), item);
}
@Override
public void update(Item before, Item after) {
}
@Override
public void delete(Item item) {
}
}