多级缓存:亿级流量的缓存方案

news2024/11/25 0:56:55

文章目录

  • 一.多级缓存的引入
  • 二.JVM进程缓存
  • 三.Lua语法入门
  • 四.多级缓存
    • 1.OpenResty
    • 2.查询Tomcat
    • 3.Redis缓存预热
    • 4.查询Redis缓存
    • 5.Nginx本地缓存
    • 6.缓存同步


一.多级缓存的引入

传统缓存的问题

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

  • 请求要经过Tomcat处理,Tomcat的性能成为整个系统的瓶颈

  • Redis缓存失效时,会对数据库产生冲击

在这里插入图片描述


多级缓存方案

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

在这里插入图片描述

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

在这里插入图片描述

二.JVM进程缓存

本地进程缓存

缓存在日常开发中启动至关重要的作用,由于是存储在内存中,数据的读取速度是非常快的,能大量减少对数据库的访问,减少数据库的压力。我们把缓存分为两类:

  • 分布式缓存,例如Redis:(已经学习过了)

    • 优点:存储容量更大、可靠性更好、可以在集群间共享

    • 缺点:访问缓存有网络开销

    • 场景:缓存数据量较大、可靠性要求较高、需要在集群间共享

  • 进程本地缓存,例如HashMap、GuavaCache:

    • 优点:读取本地内存,没有网络开销,速度更快

    • 缺点:存储容量有限、可靠性较低、无法共享

    • 场景:性能要求较高,缓存数据量较小


Caffeine

Caffeine是一个基于Java8开发的,提供了近乎最佳命中率的高性能的本地缓存库。目前Spring内部的缓存使用的就是Caffeine。GitHub地址:https://github.com/ben-manes/caffeine

Caffeine的读写速度:

在这里插入图片描述

Caffeine示例

可以通过item-service项目中的单元测试来学习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提供了四种缓存添加策略:手动加载,自动加载,手动异步加载和自动异步加载。(可以在官网查看到)

推荐使用 cache.get(key, k -> value) 操作来在缓存中不存在该key对应的缓存元素的时候进行计算生成(可以理解为直接查询mysql数据库等操作,当然,查询操作要写在get方法的第二个匿名内部类中)并直接写入至缓存内,而当该key对应的缓存元素存在的时候将会直接返回存在的缓存值。一次 cache.put(key, value) 操作将会直接写入或者更新缓存里的缓存元素,在缓存中已经存在的该key对应缓存值都会直接被覆盖。值得注意的是,当缓存的元素无法生成或者在生成的过程中抛出异常而导致生成元素失败,cache.get 也许会返回 null 。

当然,也可以使用Cache.asMap()所暴露出来的ConcurrentMap的方法对缓存进行操作。


Caffeine提供了三种缓存驱逐策略:

1.基于容量:设置缓存的数量上限

// 创建缓存对象
Cache<String, String> cache = Caffeine.newBuilder()
        .maximumSize(1) // 设置缓存大小上限为 1 
        .build();

2.基于时间:设置缓存的有效时间

// 创建缓存对象
Cache<String, String> cache = Caffeine.newBuilder()
        .expireAfterWrite(Duration.ofSeconds(10)) // 设置缓存有效期为 10 秒,从最后一次写入开始计时 
        .build();

3.基于引用:设置缓存为软引用或弱引用,利用GC来回收缓存数据。性能较差,不建议使用。

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


案例:实现进程缓存

针对实现商品的查询的本地进程缓存

利用Caffeine实现下列需求

  • 给根据id查询商品的业务添加缓存,缓存未命中时查询数据库

  • 给根据id查询商品库存的业务添加缓存,缓存未命中时查询数据库

  • 缓存初始大小为100

  • 缓存上限为10000

实现步骤:

1.初始化Bean:

@Configuration
public class CaffeineConfig {

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

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

2.修改业务(查询)代码

	@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));
    }

三.Lua语法入门

初识Lua

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


变量和循环

数据类型

在这里插入图片描述

其中,table数据类型可以是数组或者map集合,这取决于是如何声明的

可以利用type函数测试给定变量或者值的类型:

在这里插入图片描述


变量

Lua声明变量的时候,并不需要指定数据类型(很类似js):

在这里插入图片描述

访问table(下标从开始,不是从0开始):

在这里插入图片描述


循环

数组、table都可以利用for循环来遍历:

  • 遍历数组:
    在这里插入图片描述

  • 遍历table:
    在这里插入图片描述


条件控制、函数

函数

定义函数的语法:

在这里插入图片描述

例如,定义一个函数,用来打印数组:

在这里插入图片描述


条件控制

类似Java的条件控制,例如if、else语法:

在这里插入图片描述

与java不同,布尔表达式中的逻辑运算是基于英文单词:

在这里插入图片描述

四.多级缓存

1.OpenResty

初识OpenResty

OpenResty® 是一个基于 Nginx的高性能 Web 平台,用于方便地搭建能够处理超高并发、扩展性极高的动态 Web 应用、Web 服务和动态网关。具备下列特点:

  • 具备Nginx的完整功能

  • 基于Lua语言进行扩展,集成了大量精良的 Lua 库、第三方模块

  • 允许使用Lua自定义业务逻辑、自定义库

官方网站:https://openresty.org/cn/

简单来说:OpenResty的目标是通过将Lua脚本语言嵌入Nginx中,使开发人员能够使用Lua来扩展Nginx的功能。


OpenResty快速入门

案例:实现商品详情页数据查询

商品详情页面目前展示的是假数据,在浏览器的控制台可以看到查询商品信息的请求:

在这里插入图片描述

而这个请求最终被反向代理到虚拟机的OpenResty集群:

在这里插入图片描述

需求:在OpenResty中接收这个请求,并返回一段商品的假数据。

实现步骤:

1.修改nginx.conf文件:

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

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

这两行代码主要是引入了Lua模块和Lua C模块,方便后面引入API来处理HTTP请求等

2.在nginx.conf的server下面,添加对/api/item这个路径的监听:

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

这段代码可以理解为SpringMVC框架中的表现层(Controller),监听一个"/api/item"路径,具体的响应数据为json,具体的业务逻辑代码在"item.lua"中编写(类似于业务层Service)

3.编写item.lua文件

-- 返回假数据,这里的ngx.say()函数,就是写数据到Response中
ngx.say('{"id":10001,"name":"SALSA AIR}')

这里先使用假数据,后面讲解动态返回数据


请求参数处理

OpenResty提供了各种API用来获取不同类型的请求参数:

在这里插入图片描述

案例:获取请求路径中的商品id信息,拼接到json结果中返回

在查询商品信息的请求中,通过路径占位符的方式,传递了商品id到后台:

在这里插入图片描述

需求:在OpenResty中接收这个请求,并获取路径中的id信息,拼接到结果的json字符串中返回

实现步骤:

1.在OpenResty的Nginx下的配置文件编写请求变量拦截

		location ~ /api/item/(\d+) {
        	  # 相应类型,这里返回json
        	  default_type application/json;
        	  # 响应数据由 lua/item.lua这个文件来决定
        	  content_by_lua_file lua/item.lua;
        }

2.在item.lua文件获取id变量并进行拼接返回

local id = ngx.var[1]

ngx.say('{"id":'..id..',"name":"SALSA AIR","title":"RIMOWA 26寸托运箱拉杆箱 SALSA AIR系列果绿色 820.70.36.4","price":18888,"image":"https://m.360buyimg.com/mobilecms/s720x720_jfs/t6934/364/1195375010/84676/e9f2c55f/597ece38N0ddcbc77.jpg!q70.jpg.webp","category":"拉杆箱","brand":"RIMOWA","spec":"","status":1,"createTime":"2019-04-30T16:00:00.000+00:00","updateTime":"2019-04-30T16:00:00.000+00:00","stock":2999,"sold":31290}')

2.查询Tomcat

实现多级缓存的步骤是,当OpenResty集群没有缓存时,查询Redis缓存,当Redis缓存未命中访问Tomcat进程缓存,进程缓存未命中时通知Tomcat服务器查询数据库返回数据并存入数据到OpenResty作为缓存,这里先忽略访问缓存,实现OpenResty请求访问Tomcat服务器查询数据

需要说明的是:当查询未命中多级缓存中任意一级时,当前所在的级就必须查询其他级(前一级只会查询后面的一级,因为前面的级包含的缓存数据都没有命中)来补充所需的缓存数据


案例:获取请求路径中的商品id信息,根据id向Tomcat查询商品信息

这里要修改item.lua,满足下面的需求(步骤):
1.获取请求参数中的id
2.根据id向Tomcat服务发送请求,查询商品信息
3.根据id向Tomcat服务发送请求,查询库存信息
4.组装商品信息、库存信息,序列化为JSON格式并返回


前置知识1:nginx内部发送Http请求

nginx提供了内部API用以发送http请求:

local resp = ngx.location.capture("/path",{
    method = ngx.HTTP_GET,   -- 请求方式
    args = {a=1,b=2},  -- get方式传参数
    body = "c=3&d=4" -- post方式传参数
})

返回的响应内容包括:

  • resp.status:响应状态码

  • resp.header:响应头,是一个table

  • resp.body:响应体,就是响应数据

注意:这里的path是路径,并不包含IP和端口。这个请求会被nginx内部的server监听并处理。

但是我们希望这个请求发送到Tomcat服务器,所以还需要编写一个server来对这个路径做反向代理:

location /path {
     # 这里是windows电脑的ip和Java服务端口,需要确保windows防火墙处于关闭状态
     proxy_pass http://192.168.150.1:8081; 
 }

案例实现在配置文件编写监听/item路径即可

前置知识2:封装http查询函数

由于和Tomcat通信在一个新项目是十分频繁的过程,这里可以封装http查询的函数,放到OpenResty函数库中,方便后期使用。

步骤:

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

这里指定在/usr/local/openresty/lualib目录是因为在OpenResty下的Nginx配置文件中添加了对模块的加载,接下来就可以直接使用这个库

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

实现案例:

1.修改item.lua文件

-- 引入自定义工具模块
local common = require("common")
local read_http = common.read_http

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

-- 根据id查询商品
local itemJSON = read_http("/item/".. id, nil)
-- 根据id查询商品库存
local itemStockJSON = read_http("/item/stock/".. id, nil)

查询到的是商品、库存的json格式数据,我们需要将两部分数据组装,需要用到JSON处理函数库。

2.拼接JSON数据并进行返回

JSON结果处理

OpenResty提供了一个cjson模块用来处理JSON的序列化和反序列化

官方地址:https://github.com/openresty/lua-cjson/

步骤主要分为以下两个:

1.引入cjson模块:

local cjson = require "cjson"

2.(1)序列化:

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

2.(2)反序列化:

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

简单来说:序列化就是encode,反序列化就是decode

了解JSON序列化和反序列化就能进行结果拼接返回了:

-- 反序列化JSON数据/JSON转换成lua的table
local item = cjson.decode(itemJSON)
local stock = cjson.decode(itemStockJSON)

-- 组合数据
item.stock = stock.stock
item.sold = stock.sold

-- 把item序列化为json且返回数据
ngx.say(cjson.encode(item))

访问id为10002返回的数据(记得关闭防火墙):

在这里插入图片描述


总结案例:

当一个url为http://localhost/item.html?id=10002访问时,执行的大致操作:

1.前端解析url拼接为http://localhost/api/item/10002

2.windows上的nginx服务器监控到/api的请求后,反向代理到OpenResty集群,并做了负载均衡到具体的ip+端口

3.虚拟机上的OpenResty服务器监测到/api/item/(\d+)的请求,执行item.lua内的业务逻辑

4.item.lua内使用函数接收到请求和路径变量id,请求路径/item/id或/item/stock/id并被自身监听到,反向代理url到windows上的Tomcat服务器并被controller监测到,执行查询业务返回JSON数据到OpenResty服务器,服务器使用resp变量接收到返回数据,并组合数据返回到页面

需要注意的是:导入模块使用的函数为require;操作序列化反序列化JSON数据使用的模块为cjson


Tomcat集群的负载均衡

在实际业务中,OpenResty服务器请求Tomcat服务器,Tomcat服务器肯定是一个集群,这就涉及到了请求的分散(负载均衡)来减轻单个Tomcat服务器的压力,在OpenResty服务器请求Tomcat服务器时,Tomcat服务器不仅会查询数据返回给OpenResty服务器,还会保存查询数据到进程缓存中

但是,如果OpenResty服务器使用的是轮询的方式访问Tomcat集群,这就导致了如果是同一个id请求服务器,每一台Tomcat服务器都必须要查询数据库,进程缓存就显得没有用了(直到每一个Tomcat服务器都查询数据库才都有这个id的进程缓存,进程缓存才开始生效,这不是我们希望看到的)所以OpenResty服务器的负载均衡策略必须改变

这里采用的hash负载均衡策略(hash $request_uri),这种策略会对id进行hash运算,得到一个具体值会访问一台具体的Tomcat服务器,如果是同一个id,就会hash到同一个结果访问到同一台Tomcat服务器,从而导致请求会利用到线程缓存

在这里插入图片描述


3.Redis缓存预热

在多级缓存中,当OpenResty服务器中查询不到缓存时,应该查询下一级缓存即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地址(直接配置即可)

4.编写初始化类

@Component
public class RedisHandler implements InitializingBean {

    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    @Autowired
    private IItemService itemService;

    @Autowired
    private IItemStockService itemStockService;

    private static final ObjectMapper MAPPER=new ObjectMapper();

    /**
     * 这个方法会在这个类注册成bean后执行
     *
     * @throws Exception
     */
    @Override
    public void afterPropertiesSet() throws Exception {
        //查询数据库数据
        List<Item> itemList = itemService.list();
        List<ItemStock> itemStockList = itemStockService.list();

        //保存到redis中
        for (Item item : itemList) {
            //转换数据为JSON对象
            
            String jsonItem =MAPPER.writeValueAsString(item);
            redisTemplate.opsForValue().set("item:id:" + item.getId(), jsonItem);
        }
        for (ItemStock itemStock : itemStockList) {
            //转换数据为JSON对象
            
            String jsonItemStock =MAPPER.writeValueAsString(itemStock);
            redisTemplate.opsForValue().set("item:stock:id:" + itemStock.getId(), jsonItemStock);
        }
    }
}

需要注意的有三点:

1.缓存预热使用到接口类为InitializingBean

2.实现接口类中的方法后,这个方法会在整个RedisHandler注册成为bean实例后执行

3.可以用Spring原始的序列化工具ObjectMapper来将对象转换为JSON字符串(调用方法writeValueAsString)


4.查询Redis缓存

OpenResty的Redis模块

OpenResty提供了操作Redis的模块,我们只要引入该模块就能直接使用:

  • 引入Redis模块,并初始化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

别忘了对外暴露调用函数:

local _M = {  
    read_http = read_http,
	read_redis = read_redis
} 

案例:查询商品时,优先Redis缓存查询

需求:

  • 修改item.lua,封装一个函数read_data,实现先查询Redis,如果未命中,再查询tomcat

  • 修改item.lua,查询商品和库存时都调用read_data这个函数

实现代码:

-- 引入cjson的模块用来处理JSON的序列化和反序列化
local cjson = require("cjson")
-- 引入自定义工具模块
local common = require("common")

-- 获取模块中的函数
local read_http = common.read_http
local read_redis = common.read_redis

-- 定义查询函数
local function read_data(key,path,params)
	-- 查询redis
	local resp = read_redis("127.0.0.1",6379,key)
	-- 判断redis是否命中
	if not resp then
		-- redis查询失败,查询http
		resp = read_http(path,params)
	end
	return resp
end

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

-- 根据id查询商品
local itemJSON = read_data("item:id:"..id, "/item/"..id, nil)

-- 根据id查询商品库存
local itemStockJSON = read_data("item:stock:id:"..id, "/item/stock/"..id, nil)

-- 反序列化JSON数据/JSON转换成lua的table
local item = cjson.decode(itemJSON)
local stock = cjson.decode(itemStockJSON)

-- 组合数据
item.stock = stock.stock
item.sold = stock.sold

-- 把item序列化为json且返回数据
ngx.say(cjson.encode(item))

需要解释的是:对于其中的代码:

-- 查询redis
	local resp = read_redis("127.0.0.1",6379,key)

填写127.0.0.1是因为OpenResty服务器和Redis服务器都存在一台虚拟机上,使用回环地址即可访问


5.Nginx本地缓存

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

  • 开启共享字典,在nginx.conf的http下添加配置:
# 共享字典,也就是本地缓存,名称叫做:item_cache,大小150m
lua_shared_dict item_cache 150m; 
  • 操作共享字典
-- 获取本地缓存对象
local item_cache = ngx.shared.item_cache
-- 存储, 指定key、value、过期时间,单位s,默认为0代表永不过期
item_cache:set('key', 'value', 1000)
-- 读取
local val = item_cache:get('key')

案例:在查询商品时,优先查询OpenResty的本地缓存

需求:

  • 修改item.lua中的read_data函数,优先查询本地缓存,未命中时再查询Redis、Tomcat

  • 查询Redis或Tomcat成功后,将数据写入本地缓存,并设置有效期

  • 商品基本信息,有效期30分钟

  • 库存信息,有效期1分钟

实现代码:

1.先在nginx.conf配置文件添加共享词典配置

# 共享字典,也就是本地缓存,名称叫做:item_cache,大小150m
	lua_shared_dict item_cache 150m;

2.编写业务逻辑(.lua文件中)

-- 引入cjson的模块用来处理JSON的序列化和反序列化
local cjson = require("cjson")
-- 引入自定义工具模块
local common = require("common")

-- 获取模块中的函数
local read_http = common.read_http
local read_redis = common.read_redis

-- 导入共享词典,本地缓存
local item_cache = ngx.shared.item_cache

-- 定义查询函数
local function read_data(key,path,params,validTime)
	-- 先查询本地缓存
	local val = item_cache:get(key)
	-- 判断本地缓存是否命中
	if not val then
		ngx.log(ngx.ERR, "本地缓存查询失败,尝试查询Redis, key:", key)
		-- 本地缓存查询失败,查询redis
		val = read_redis("127.0.0.1",6379,key)
		-- 判断redis是否命中
		if not val then
			ngx.log(ngx.ERR, "redis查询失败,尝试查询http, key:", key)
			-- redis查询失败,查询http
			val = read_http(path,params)
		end
	end
	item_cache:set(key,val,validTime)
	return val
end

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

-- 根据id查询商品
local itemJSON = read_data("item:id:"..id, "/item/"..id, nil, 1800)

-- 根据id查询商品库存
local itemStockJSON = read_data("item:stock:id:"..id, "/item/stock/"..id, nil, 60)

-- 反序列化JSON数据/JSON转换成lua的table
local item = cjson.decode(itemJSON)
local stock = cjson.decode(itemStockJSON)

-- 组合数据
item.stock = stock.stock
item.sold = stock.sold

-- 把item序列化为json且返回数据
ngx.say(cjson.encode(item))

6.缓存同步

缓存同步策略

缓存数据同步的常见方式有三种:

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

    • 优势:简单、方便

    • 缺点:时效性差,缓存过期之前可能不一致

    • 场景:更新频率较低,时效性要求低的业务

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

    • 优势:时效性强,缓存与数据库强一致

    • 缺点:有代码侵入,耦合度高;

    • 场景:对一致性、时效性要求较高的缓存数据

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

    • 优势:低耦合,可以同时通知多个缓存服务

    • 缺点:时效性一般,可能存在中间不一致状态

    • 场景:时效性要求一般,有多个服务需要同步

对于异步通知,有以下两种:

1.基于MQ的异步通知:

在这里插入图片描述

2.基于Canal的异步通知:

在这里插入图片描述


初识Canal

Canal [kə’næl],译意为水道/管道/沟渠,canal是阿里巴巴旗下的一款开源项目,基于Java开发。基于数据库增量日志解析,提供增量数据订阅&消费。GitHub的地址:https://github.com/alibaba/canal,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的客户端,进而完成对其它数据库的同步。

在这里插入图片描述

对于安装Canal需要注意的是:1.首先开启mysql的主从 2.安装canal,并使cancl和mysql处于同一网络下


监听Canal

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

在这里插入图片描述

Canal提供了各种语言的客户端,当Canal监听到binlog变化时,会通知Canal的客户端。不过这里我们会使用GitHub上的第三方开源的canal-starter。(操作Canal会更加方便)地址:链接: https://github.com/NormanGyllenhaal/canal-client

实现步骤:

引入依赖:

		<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.109.130:11111 # canal地址

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

这里就使用案例来演示了:

package com.heima.item.canal;

import com.github.benmanes.caffeine.cache.Cache;
import com.heima.item.config.RedisHandler;
import com.heima.item.pojo.Item;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import top.javatool.canal.client.annotation.CanalTable;
import top.javatool.canal.client.handler.EntryHandler;

@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) {
        itemCache.put(item.getId(), item);

        redisHandler.saveItem(item);
    }

    @Override
    public void update(Item before, Item after) {
        itemCache.put(after.getId(), after);

        redisHandler.saveItem(after);
    }

    @Override
    public void delete(Item item) {
        itemCache.invalidate(item.getId());

        redisHandler.deleteItemById(item.getId());
    }
}

具体来说:

在这里插入图片描述

Canal推送给canal-client的是被修改的这一行数据(row),而我们引入的canal-client则会帮我们把行数据封装到Item实体类中。这个过程中需要知道数据库与实体的映射关系,要用到JPA的几个注解:

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 java.util.Date;

@Data
@TableName("tb_item")
public class Item {
    @TableId(type = IdType.AUTO)
    @Id
    private Long id;//商品id
    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;
}

具体来说:

在这里插入图片描述

到此为止,可以实现mysql数据库修改,同步对redis的数据进行变更,而对于OpenResty内的数据,推荐缓存不是更新速度很频繁的数据并且使用了设置过期时间进行数据更新

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1325762.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

FA2016ASA (MHz范围晶体单元,内置热敏电阻) 汽车

FA2016ASA是爱普生推出的一款内置热敏电阻、频率范围为38.4MHz的晶振&#xff0c;确保数据的准确传输&#xff0c;同时有效避免频谱干扰的出现。可以在-40C to 125C 的温度内稳定工作。在汽车内部空间有限的情况下&#xff0c;FA2016ASA以其小型超薄的外形尺寸2.0 1.6 0.68mm…

【分享】如何给Excel加密?码住这三种方法!

想要给Excel文件进行加密&#xff0c;方法有很多&#xff0c;今天分享三种Excel加密方法给大家。 打开密码 设置了打开密码的excel文件&#xff0c;打开文件就会提示输入密码才能打开excel文件&#xff0c;只有输入了正确的密码才能打开并且编辑文件&#xff0c;如果密码错误…

Jenkins 构建环境指南

目录 Delete workspace before build starts&#xff08;常用&#xff09; Use secret text(s) or file(s) &#xff08;常用&#xff09; Add timestamps to the Console Output &#xff08;常用&#xff09; Inspect build log for published build scans Terminate a …

MFC 消息映射机制

目录 消息映射机制概述 宏展开 宏展开的作用 消息映射机制的执行流程 消息处理 消息映射机制概述 MFC的消息映射映射机制是可以在不重写WindowProc虚函数的大前提下&#xff0c;仍然可以处理消息。 类必须具备的要件 类内必须添加声明宏 DECLARE_MESSAGE_MAP() 类外…

【音视频 | AAC】AAC格式音频文件解析

&#x1f601;博客主页&#x1f601;&#xff1a;&#x1f680;https://blog.csdn.net/wkd_007&#x1f680; &#x1f911;博客内容&#x1f911;&#xff1a;&#x1f36d;嵌入式开发、Linux、C语言、C、数据结构、音视频&#x1f36d; &#x1f923;本文内容&#x1f923;&a…

33 在Vue3中如何通过插槽向父组件传值

概述 通过插槽向父组件传值&#xff0c;是一种比较高级的&#xff0c;但是非常使用的技术&#xff0c;在很多UI组件库里面经常看到。 这节课我们来学习一下这种用法。 基本用法 我们创建src/components/Demo33.vue&#xff0c;代码如下&#xff1a; <script setup> …

U盘无法读取怎么办?U盘无法读取修复方法

U盘无法读取是常见的故障&#xff0c;可能的原因包括U盘驱动程序未安装、U盘损坏、文件系统损坏等。为了解决这个问题&#xff0c;可以尝试重新安装U盘驱动程序、格式化U盘、检查U盘是否损坏等方法。如果以上方法均无效&#xff0c;建议寻求专业人士的帮助。 U盘无法读取怎么办…

MySQL——基础篇

学习视频链接&#xff1a;https://www.bilibili.com/video/BV1Kr4y1i7ru/?spm_id_from333.999.0.0&vd_source619f8ed6df662d99db4b3673d1d3ddcb 前言✴️ 基础篇——MySQL概述、SQL、函数、约束、多表查询、事务 进阶篇——存储引擎、索引、SQL优化、视图/存储过程/触发…

磁盘类型选择对阿里云RDS MySQL的性能影响

测试说明 这是一个云数据库性能测试系列&#xff0c;旨在通过简单标准的性能测试&#xff0c;帮助开发者、企业了解云数据库的性能&#xff0c;以选择适合的规格与类型。这个系列还包括&#xff1a; * 云数据库(RDS MySQL)性能深度测评与对比 * 阿里云RDS标准版(x86) vs 经济…

GitHub two-factor authentication开启教程

问题描述 最近登录GitHub个人页面动不动就有一个提示框”… two-factor authentication will be required for your account starting Jan 4, 2024 …“&#xff0c;点击去看了一下原来是GitHub对所有的用户登录都要开启双重身份认证&#xff0c;要在1月4号前完成 解决办法 …

Jenkins + gitlab 持续集成和持续部署的学习笔记

1. Jenkins 介绍 软件开发生命周期(SLDC, Software Development Life Cycle)&#xff1a;它集合了计划、开发、测试、部署的集合。 软件开发瀑布模型 软件的敏捷开发 1.1 持续集成 持续集成 (Continuous integration 简称 CI): 指的是频繁的将代码集成到主干。 持续集成的流…

清风数学建模学习笔记-斯皮尔曼相关系数

内容&#xff1a;斯皮尔曼相关系数 一.原理&#xff1a; 二.算法&#xff1a; 1.MATLAB: 2.SPSS&#xff1a; 分析-相关-双变量相关-勾选标注显著性相关性 3. 相关性系数的选择&#xff1a;

三大主流前端框架介绍及选型

在前端项目中&#xff0c;可以借助某些框架&#xff08;如React、Vue、Angular等&#xff09;来实现组件化开发&#xff0c;使代码更容易复用。此时&#xff0c;一个网页不再是由一个个独立的HTML、CSS和JavaScript文件组成&#xff0c;而是按照组件的思想将网页划分成一个个组…

IDEA版SSM入门到实战(Maven+MyBatis+Spring+SpringMVC) -SpringMVC搭建框架

第一章 初识SpringMVC 1.1 SpringMVC概述 SpringMVC是Spring子框架 SpringMVC是Spring 为**【展现层|表示层|表述层|控制层】**提供的基于 MVC 设计理念的优秀的 Web 框架&#xff0c;是目前最主流的MVC 框架。 SpringMVC是非侵入式&#xff1a;可以使用注解让普通java对象&…

自动化测试|Eolink Apikit 如何保存、使用测试用例

测试用例是测试过程中很重要的一类文档&#xff0c;它是测试工作的核心&#xff0c;是一组在测试时输入和输出的标准&#xff0c;是软件需求的具体对照。 测试用例可以帮助测试人员理清测试思路&#xff0c;确保测试覆盖率&#xff0c;发现需求漏洞&#xff0c;提高软件质量&a…

HarmonyOS 应用事件打点开发指导

简介 传统的日志系统里汇聚了整个设备上所有程序运行的过程流水日志&#xff0c;难以识别其中的关键信息。因此&#xff0c;应用开发者需要一种数据打点机制&#xff0c;用来评估如访问数、日活、用户操作习惯以及影响用户使用的关键因素等关键信息。 HiAppEvent 是在系统层面…

计算机模拟仿真:技术与应用

计算机模拟仿真&#xff1a;技术与应用 一、引言 计算机模拟仿真是一种利用计算机技术对现实世界或系统进行模拟和仿真的方法。随着计算机技术的不断发展&#xff0c;计算机模拟仿真已经成为许多领域中不可或缺的技术工具。本文将介绍计算机模拟仿真的基本概念、技术原理、应用…

在做题中学习(34):两整数之和(不准用运算符+)

371. 两整数之和 - 力扣&#xff08;LeetCode&#xff09; 思路&#xff1a;异或&#xff08;两个数异或可看作无进位相加&#xff09; 当进位b为全0的时候&#xff0c;那异或的结果就是真正相加的结果。 class Solution { public:int getSum(int a, int b) {while(b!0){int…

2023年度佳作:AIGC、AGI、GhatGPT 与人工智能大模型的创新与前景展望

&#x1f3ac; 鸽芷咕&#xff1a;个人主页 &#x1f525; 个人专栏:《linux深造日志》《粉丝福利》 ⛺️生活的理想&#xff0c;就是为了理想的生活! ⛳️ 写在前面参与规则 ✅参与方式&#xff1a;关注博主、点赞、收藏、评论&#xff0c;任意评论&#xff08;每人最多评论…

系统设计架构——互联网案例

Netflix 的技术栈 移动和网络:Netflix 采用 Swift 和 Kotlin 来构建原生移动应用。对于其 Web 应用程序,它使用 React。 前端/服务器通信:Netflix 使用 GraphQL。 后端服务:Netflix 依赖 ZUUL、Eureka、Spring Boot 框架和其他技术。 数据库:Netflix 使用 EV 缓存、Cas…