Bootstrap

多级缓存学习

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.变量和循环

Lua数据类型
数据类型描述
nil这个最简单,只有值nil属于该类,表示一个无效值(在条件表达式中相当于false)
boolean包含两个值: false和true
number表示双精度类型的实浮点数
string字符串由双引号或单引号表示
function由C或Lua编写的函数
tableLua中的表(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
Lua逻辑运算符
操作符描述实例
and逻辑与操作符。若A为false,则返回A,否则返回B.(A and B)为false
or逻辑或操作符。若A为true,则返回A,否则返回B.(A or B) 为true
not逻辑非操作符。与逻辑运算结果相反,如果条件为true,逻辑非为falsenot(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获取请求参数

OpenResty获取请求参数的API
参数格式参数示例参数解析代码示例
路径占位符/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) {

    }
}

;