• 首页 首页 icon
  • 工具库 工具库 icon
    • IP查询 IP查询 icon
  • 内容库 内容库 icon
    • 快讯库 快讯库 icon
    • 精品库 精品库 icon
    • 问答库 问答库 icon
  • 更多 更多 icon
    • 服务条款 服务条款 icon

SpringCloud微服务---学习笔记二--多级缓存

武飞扬头像
Swing_zzZ
帮助1

1 多级缓存

传统缓存策略
一般是请求到达Tomcat,先查询Redis,如果未命中则查询数据库。
存在问题:

· 请求要经过Tomcat处理,Tomcat的性能成为整个系统的瓶颈
· Redis缓存失效时,会对数据库产生冲击

学新通

多级缓存方案

多级缓存就是充分利用请求处理的每个环节,分别添加缓存,减轻Tomcat压力,提升服务性能

用作缓存的Nginx是业务Nginx,需要部署为集群,再有专门的Nginx用来做反向代理:
学新通

1.1 JVM进程缓存

缓存分两类:

· 分布式缓存,如Redis:
	· 优点:存储容量更大、可靠性更好、可以在集群间共享
	· 缺点:访问缓存有网络开销
	· 场景:缓存数据量较大、可靠性要求较高、需要在集群间共享
· 进程本地缓存,如HashMap、GuavaCache:
	· 优点:读取本地内存,没有网络开销,速度更快
	· 缺点:存储容量有限、可靠性较低、无法共享
	· 场景:性能要求较高,缓存数据量较小

1.1.1 Caffeine

Caffeine是基于Java8开发的,提供了近乎最佳命中率的高性能的本地缓存库(Spring内部的缓存使用的就是Caffeine)

@Test
    void testBasicOps() {
        // 创建缓存对象
        Cache<String, String> cache = Caffeine.newBuilder().build();

        // 存数据
        cache.put("gf", "迪丽热巴");

        // 取数据,不存在则返回null
        String gf = cache.getIfPresent("gf");
        System.out.println("gf = "   gf);

        // 取数据,不存在则去数据库查询
        String defaultGF = cache.get("defaultGF", key -> {
            // 这里可以去数据库根据 key查询value
            return "柳岩";
        });
        System.out.println("defaultGF = "   defaultGF);
    }
学新通

Caffeine提供三种缓存驱逐策略:
· 基于容量:设置缓存的数量上限
学新通

· 基于时间:设置缓存的有效时间
学新通

· 基于引用:设置缓存为软引用或弱引用,利用GC来回收缓存数据。性能较差

默认情况下,当一个缓存元素过期的时候,Caffeine不会自动立即将其清理和驱逐。而是在一次读或写操作后,或者在空闲时间完成对失效数据的驱逐。

1.1.2 实现进程缓存

1、创建bean,设置本地缓存Cache初始大小和上限:

@Configuration
public class CaffeineConfig {

    @Bean
    public Cache<Long, Item> itemCache(){
        return Caffeine.newBuilder()
                .initialCapacity(100)
                .maximumSize(10000)
                .build();
    }

    @Bean
    public Cache<Long, ItemStock> stockCache(){
        return Caffeine.newBuilder()
                .initialCapacity(100)
                .maximumSize(10000)
                .build();
    }
}

学新通

2、在Controller层调用本地缓存get方法:

@GetMapping("/{id}")
    public Item findById(@PathVariable("id") Long id){
        return itemCache.get(id, key -> itemService.query()
                .ne("status", 3).eq("id",key)
                .one()
        );
    }

    @GetMapping("/stock/{id}")
    public ItemStock findStockById(@PathVariable("id") Long id){
        return stockCache.get(id,key ->  stockService.getById(key));
    }

1.2 Lua语法入门

学新通

Lua是一种轻量小巧的脚本语言,用标准C语言编写并以源代码形式开放,其设计目的是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能

数据类型

数据类型 描述
nil 只有值nil属于该类,表示一个无效值(在条件表达式中相当于false)
boolean 包含两个值:false和true
number 表示双精度类型的实浮点数
string 字符串由一对双引号或单引号来表示,采用 . . 进行拼接
function 由 C 或 Lua 编写的函数
table Lua中的表(table)其实是一个“关联数组”(associative arrays),数组的索引可以是数字、字符串或表类型。在Lua里,table的创建是通过“构造表达式”来完成,最简单构造表达式是{ },用来创建一个空表

变量
Lua声明变量时,不需要指定数据类型:
学新通
访问table:
学新通

循环
数组、table都可利用for循环遍历:
· 遍历数组:
学新通

· 遍历table:
学新通

条件控制、函数

函数
定义函数的语法:
学新通
条件控制
类似Java的条件控制,例如if、else语法:
学新通

学新通

1.3 多级缓存

1.3.1 OpenResty

OpenResty是一个基于Nginx的高性能Web平突然,用于方便搭建能够处理超高并发、扩展性极高的动态Web应用、Web服务和动态网关。

特点:

· 具备Nginx的完整功能
· 基于Lua语言进行扩展,集成了大量精良的Lua库、第三方模块
· 允许使用Lua自定义业务逻辑、自定义库

1.3.2 安装OpenResty

Linux虚拟机必须联网

1)安装开发库

首先要安装OpenResty的依赖开发库,执行命令:

yum install -y pcre-devel openssl-devel gcc --skip-broken
2)安装OpenResty仓库

你可以在你的 CentOS 系统中添加 openresty 仓库,这样就可以便于未来安装或更新我们的软件包(通过 yum check-update 命令)。运行下面的命令就可以添加我们的仓库:

yum-config-manager --add-repo https://openresty.org/package/centos/openresty.repo

如果提示说命令不存在,则运行:

yum install -y yum-utils 

然后再重复上面的命令

3)安装OpenResty

然后就可以像下面这样安装软件包,比如 openresty

yum install -y openresty
4)安装opm工具

opm是OpenResty的一个管理工具,可以帮助我们安装一个第三方的Lua模块。

如果你想安装命令行工具 opm,那么可以像下面这样安装 openresty-opm 包:

yum install -y openresty-opm
5)目录结构

默认情况下,OpenResty安装的目录是:/usr/local/openresty

OpenResty就是在Nginx基础上集成了一些Lua模块。

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
7)启动运行

运行方式

# 启动nginx
nginx
# 重新加载配置
nginx -s reload
# 停止
nginx -s stop

在Linux的控制台输入命令以启动nginx:

nginx

然后访问页面:http:/ /自己虚拟机地址 :8081

8)注意

学新通

在nginx.conf的http下面,添加 加载OpenResty的lua模块:

#lua 模块
lua_package_path "/usr/local/openresty/lualib/?.lua;;";
#c模块     
lua_package_cpath "/usr/local/openresty/lualib/?.so;;";  

在nginx.conf的server下,添加 /api/item路径的监听:

location /api/item {
	   	  # 响应类型,返回json
	   	  default_type application/json;
	   	  # 响应数据由 lua/item.lua文件决定
	   	  content_by_lua_file lua/item.lua;
	   } 

1.3.3 请求参数处理

学新通

1.3.4 查询Tomcat

nginx提供了内部API用以发送http请求:
学新通
返回的响应包括:

· resp.status:响应状态码
· resp.header:响应头(是一个table)
· resp.body:响应体(即响应数据)

注意
这里的path路径不包含IP、端口。该请求会被nginx内部的server监听并处理。需要编写一个server对该路径做反向代理:
学新通

封装http查询函数
1、在/usr/local/openresty/lualib目录下创建common.lua文件:

vi /usr/local/openresty/lualib/common.lua

2、在common.lua中封装http查询函数

-- 封装函数,发送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
学新通

导入封装的函数
在item.lua中进行修改:

--导入common函数库
local common = require('common')
--获取common.lua中的封装函数
local read_http = common.read_http

JSON结果处理
cjson模块用来处理JSON的序列化和反序列化
· 引入cjson模块:

local cjson = require "cjson"

· 序列化:

local obj = {
	name = 'jack',
	age = 21
}
local json = cjson.encode(obj)

· 反序列化:

local json = '{"name":"jack","age":21}'
--反序列化
local obj = cjson.decode(json);
print(obj.name)

学新通
Tomcat集群负载均衡
学新通
学新通

1.3.5 redis缓存

冷启动
服务刚启动时,Redis中并没有缓存,如果所有商品数据都在第一次查询时添加缓存,可能会给数据库带来较大压力

缓存预热
可利用大数据统计用户访问的热点数据,在项目启动时将这些热点数据提前查询并保存到Redis中
1、Docker运行Redis容器

docker run --name redis -p 6379:6379 -d redis redis-server --appendonly yes

2、在item-service服务中引入Redis依赖

<dependency>
	<groupId>org.springframework.boot</groupId>
 	<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

3、配置Redis地址

spring:
	redis:
    	host: 192.168.159.128 # 换成自己虚拟机地址

4、编写初始化类RedisHandler

@Component
public class RedisHandler implements InitializingBean {
    @Autowired
    private StringRedisTemplate redisTemplate;
    
    @Autowired
    private IItemService itemService;
    
    @Autowired
    private IItemStockService stockService;

    //Spring里默认的JSON处理工具
    private static final ObjectMapper MAPPER = new ObjectMapper();


    @Override
    public void afterPropertiesSet() throws Exception {
        //初始化缓存
        // 1查询商品信息
        List<Item> itemList = itemService.list();
        // 2存入缓存
        for (Item item : itemList) {
            // 2.1 item序列化为JSON
            String s = MAPPER.writeValueAsString(item);
            // 2.2 存入redis
            redisTemplate.opsForValue().set("item:id:" item.getId(),s);
        }

        // 3查询库存信息
        List<ItemStock> stockList = stockService.list();
        // 2存入缓存
        for (ItemStock stock : stockList) {
            // 2.1 item序列化为JSON
            String json = MAPPER.writeValueAsString(stock);
            // 2.2 存入redis
            redisTemplate.opsForValue().set("item:stock:id:" stock.getId(),json);
        }
    }
}
学新通

查询Redis缓存
OpenResty提供了操作Redis的模块,在common.lua中引入:
· 引入Redis模块,并初始化Redis对象

学新通

· 封装函数,用来释放Redis连接(放入连接池)
学新通
· 封装函数,从Redis读数据并返回
学新通
· 将封装好的函数 暴露出去:
学新通
· 最后在item.lua文件中引用,保存并运行nginx -s reload重新加载nginx:
学新通

1.3.6 Nginx本地缓存

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

· 开启共享字典,在nginx.conf的http下添加配置:
学新通

· 操作共享字典,在item.lua中:
学新通
学新通

通过/usr/local/openresty/nginx/logs下的error.log日志来查看错误
cd /usr/local/openresty/nginx,再tail -f logs/error.log

1.4 缓存同步

1.4.1 缓存同步策略

常见方式:
· 设置有效期
给缓存设置有效期,到期后自动删除。再次查询时更新

· 优势:简单、方便
· 缺点:时效性差,缓存过期之前可能不一致
· 场景:更新频率较低,时效性要求低的业务

· 同步双写
在修改数据库的同时,直接修改缓存

· 优势:时效性强,缓存与数据库强一致
· 缺点:有代码侵入,耦合度高
· 场景:对一致性、时效性要求较高的缓存数据

· 异步通知
修改数据库时发送事件通知,相关服务监听到通知后修改缓存数据

· 优势:低耦合,可同时通知多个缓存服务
· 缺点:时效性一般,可能存在中间不一致状态
· 场景:时效性要求一般,有多个服务需要同步

1.4.2 Canal

canal是阿里巴巴旗下一款开源项目,基于Java开发。基于数据库增量日志解析,提供增量数据订阅&消费。

Canal是基于mysql的主从同步实现的。
MySQL主从同步的原理:

· MySQL master 将数据变更写入二进制日志(binary log),其中记录的数据叫binary log events
· MySQL slave 将 master 的 binary log events拷贝到它的中继日志(relay log)
· MySQL slave 重放 relay log 中事件,将数据变更反映它自己的数据

Canal把自己伪装成MySQL的一个slave节点,从而监听master的binary log变化。再把得到的变化信息通知给Canal客户端,进而完成对其它数据库同步。

1 开启MySQL主从

Canal是基于MySQL的主从同步功能,因此必须先开启MySQL的主从功能才可以。

这里以之前用Docker运行的mysql为例:

1.1.开启binlog

打开mysql容器挂载的日志文件,在/tmp/mysql/conf目录
修改文件:

vi /tmp/mysql/conf/my.cnf

添加内容:

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这个库

最终效果:

[mysqld]
skip-name-resolve
character_set_server=utf8
datadir=/var/lib/mysql
server-id=1000
log-bin=/var/lib/mysql/mysql-bin
binlog-do-db=heima
1.2.设置用户权限

接下来添加一个仅用于数据同步的账户,出于安全考虑,这里仅提供对heima这个库的操作权限。(在Navicat的heima数据库中新建查询)

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;

学新通
学新通
当 从节点的position与主 不一致时,需要获取新的log

2 安装Canal
2.1.创建网络

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

docker network create heima

让mysql加入这个网络:

docker network connect heima mysql

学新通

2.2.安装Canal

拉取镜像,然后运行命令创建Canal容器:

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 

通过docker logs -f canal命令可查看是否开启成功
学新通
通过docker exec -it canal bash进入容器,tail -f canal-server/logs/canal/canal.log查看日志
学新通

1.4.3 监听Canal

Canal提供各种语言的客户端,当Canal监听到binlog变化时,会通知Canal的客户端

使用第三方开源的canal-starter
引入依赖:

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

编写配置:

canal:
  destination: heima # canal实例名称,要跟canal-server运行时设置的destination一致
  server: 192.168.159.128:11111 # canal地址

编写监听器,监听Canal消息:

@CanalTable("tb_item")
@Component
public class ItemHandler implements EntryHandler<Item> {

    @Autowired
    private RedisHandler redisHandler;

    @Autowired
    private Cache<Long,Item> itemCache;

    @Override
    public void insert(Item item) {
        // 写数据到redis
        redisHandler.saveItem(item);
        //写数据到JVM缓存
        itemCache.put(item.getId(),item);
    }

    @Override
    public void update(Item before, Item after) {
        // 写数据到redis
        redisHandler.saveItem(after);
        //写数据到JVM缓存
        itemCache.put(after.getId(),after);
    }

    @Override
    public void delete(Item item) {
        // 删除redis数据
        redisHandler.deleteById(item.getId());
        // 删除JVM缓存数据
        itemCache.invalidate(item.getId());
    }
}
学新通

RedisHandler中准备好增加、删除操作
学新通

Canal客户端
Canal推送给canal-client的是被修改的这一行数据(row)。 而我们引入的canal-client会帮我们把行数据封装到Item实体类中。
学新通

1.5 总结

多级缓存架构
学新通

这篇好文章是转载于:学新通技术网

  • 版权申明: 本站部分内容来自互联网,仅供学习及演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,请提供相关证据及您的身份证明,我们将在收到邮件后48小时内删除。
  • 本站站名: 学新通技术网
  • 本文地址: /boutique/detail/tanhgaabfk
系列文章
更多 icon
同类精品
更多 icon
继续加载