Redis篇(缓存机制 - 多级缓存)(持续更新迭代)

news2024/10/1 13:32:43

目录

一、传统缓存的问题

二、JVM进程缓存

1. 导入案例

2. 初识Caffeine

3. 实现JVM进程缓存

3.1. 需求

3.2. 实现

三、Lua语法入门

1. 初识Lua

2. HelloWorld

3. 变量和循环

3.1. Lua的数据类型

3.2. 声明变量

3.3. 循环

4. 条件控制、函数

4.1. 函数

4.2. 条件控制

4.3. 案例

四、实现多级缓存

1. 安装OpenResty

2. OpenResty快速入门

2.1. 反向代理流程

2.2. OpenResty监听请求

2.3. 编写item.lua

3. 请求参数处理

3.1. 获取参数的API

3.2. 获取参数并返回

获取商品id

拼接 ID 并返回

重新加载并测试

4. 查询Tomcat

4.1. 发送http请求的API

4.2. 封装http工具

4.3. CJSON工具类

4.4. 实现 Tomcat 查询

4.5. 基于ID负载均衡

原理

实现

测试

5. Redis缓存预热

6. 查询Redis缓存

6.1. 封装Redis工具

6.2. 实现Redis查询

7. Nginx本地缓存

7.1. 本地缓存API

7.2. 实现本地缓存查询

五、缓存同步

1. 数据同步策略

2. 安装Canal

2.1. 认识Canal

2.2. 安装Canal

3. 监听Canal

3.1. 引入依赖

3.2. 编写配置

3.3. 修改Item实体类

3.4. 编写监听器


一、传统缓存的问题

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

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

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

  1. 浏览器访问静态资源时,优先读取浏览器本地缓存
  2. 访问非静态资源(ajax查询数据)时,访问服务端
  3. 请求到达 Nginx 后,优先读取 Nginx 本地缓存
  4. 如果 Nginx 本地缓存未命中,则去直接查询 Redis(不经过 Tomcat )
  5. 如果 Redis 查询未命中,则查询 Tomcat
  6. 请求进入 Tomcat 后,优先查询 JVM 进程缓存
  7. 如果 JVM 进程缓存未命中,则查询数据库

在多级缓存架构中,Nginx 内部需要编写本地缓存查询、Redis 查询、Tomcat 查询的业务逻辑,因此这样的

nginx 服务不再是一个反向代理服务器,而是一个编写业务的 Web 服务器了

因此这样的业务 Nginx 服务也需要搭建集群来提高并发,再有专门的 nginx 服务来做反向代理,如图:

另外,我们的 Tomcat 服务将来也会部署为集群模式:

可见,多级缓存的关键有两个:

  1. 一个是在 nginx 中编写业务,实现 nginx 本地缓存、Redis、Tomcat 的查询
  2. 另一个就是在 Tomcat 中实现 JVM 进程缓存

其中 Nginx 编程则会用到 OpenResty 框架结合 Lua 这样的语言。

二、JVM进程缓存

为了演示多级缓存的案例,我们先准备一个商品查询的业务。

1. 导入案例

参考《案例说明 待更新》

2. 初识Caffeine

缓存在日常开发中启动至关重要的作用,

由于是存储在内存中,数据的读取速度是非常快的,能大量减少对数据库的访问,减少数据库的压力。

我们把缓存分为两类:

1. 分布式缓存,例如 Redis:

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

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

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

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

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

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

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

我们今天会利用 Caffeine 框架来实现 JVM 进程缓存。

Caffeine是一个基于 Java8 开发的,提供了近乎最佳命中率的高性能的本地缓存库。

目前 Spring 内部的缓存使用的就是 Caffeine。

GitHub地址:GitHub - ben-manes/caffeine: A high performance caching library for Java

Caffeine的性能非常好,下图是官方给出的性能对比:

可以看到 Caffeine 的性能遥遥领先!

缓存使用的基本API:

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

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

    // 取数据
    String gf = cache.getIfPresent("gf");
    System.out.println("gf = " + gf);

    // 取数据,包含两个参数:
    // 参数一:缓存的key
    // 参数二:Lambda表达式,表达式参数就是缓存的key,方法体是查询数据库的逻辑
    // 优先根据key查询JVM缓存,如果未命中,则执行参数二的Lambda表达式
    String defaultGF = cache.get("defaultGF", key -> {
        // 根据key去数据库查询数据
        return "柳岩";
    });
    System.out.println("defaultGF = " + defaultGF);
}

Caffein e既然是缓存的一种,肯定需要有缓存的清除策略,不然的话内存总会有耗尽的时候。

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

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

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

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

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

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

注意:

在默认情况下,当一个缓存元素过期的时候,Caffeine 不会自动立即将其清理和驱逐。

而是在一次读或写操作后,或者在空闲时间完成对失效数据的驱逐。

3. 实现JVM进程缓存

3.1. 需求

利用 Caffeine 实现下列需求:

  1. 给根据 id 查询商品的业务添加缓存,缓存未命中时查询数据库
  2. 给根据 id 查询商品库存的业务添加缓存,缓存未命中时查询数据库
  3. 缓存初始大小为 100
  4. 缓存上限为 10000

3.2. 实现

首先,我们需要定义两个 Caffeine 的缓存对象,分别保存商品、库存的缓存数据。

在 item-service 的 com.zhengge.item.config 包下定义 CaffeineConfig 类:

package com.zhengge.item.config;

import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.project.item.pojo.Item;
import com.project.item.pojo.ItemStock;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

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

然后,修改item-service中的 com.zhengge.item.web 包下的 ItemController 类,添加缓存逻辑:

@RestController
@RequestMapping("item")
public class ItemController {

    @Autowired
    private IItemService itemService;
    @Autowired
    private IItemStockService stockService;

    @Autowired
    private Cache<Long, Item> itemCache;
    @Autowired
    private Cache<Long, ItemStock> stockCache;
    
    // ...其它略
    
    @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语法入门

Nginx 编程需要用到 Lua 语言,因此我们必须先入门 Lua 的基本语法。

1. 初识Lua

Lua 是一种轻量小巧的脚本语言,用标准 C 语言编写并以源代码形式开放,

其设计目的是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能。

官网:https://www.lua.org/

Lua 经常嵌入到 C 语言开发的程序中,例如游戏开发、游戏插件等。

Nginx 本身也是 C 语言开发,因此也允许基于 Lua 做拓展。

2. HelloWorld

CentOS7 默认已经安装了 Lua 语言环境,所以可以直接运行 Lua 代码。

  1. 在 Linux 虚拟机的任意目录下,新建一个 hello.lua 文件

  1. 添加下面的内容
print("Hello World!")  
  1. 运行

3. 变量和循环

学习任何语言必然离不开变量,而变量的声明必须先知道数据的类型。

3.1. Lua的数据类型

Lua 中支持的常见数据类型包括:

另外,Lua 提供了 type() 函数来判断一个变量的数据类型:

3.2. 声明变量

Lua 声明变量的时候无需指定数据类型,而是用 local 来声明变量为局部变量:

-- 声明字符串,可以用单引号或双引号,
local str = 'hello'
-- 字符串拼接可以使用 ..
local str2 = 'hello' .. 'world'
-- 声明数字
local num = 21
-- 声明布尔类型
local flag = true

Lua 中的 table 类型既可以作为数组,又可以作为 Java 中的 map 来使用。

数组就是特殊的 table,key 是数组角标而已:

-- 声明数组 ,key为角标的 table
local arr = {'java', 'python', 'lua'}
-- 声明table,类似java的map
local map =  {name='Jack', age=21}

Lua 中的数组角标是从 1 开始,访问的时候与 Java 中类似:

-- 访问数组,lua数组的角标从1开始
print(arr[1])

Lua 中的 table 可以用 key 来访问:

-- 访问table
print(map['name'])
print(map.name)

3.3. 循环

对于 table,我们可以利用 for 循环来遍历。不过数组和普通 table 遍历略有差异。

遍历数组:

-- 声明数组 key为索引的 table
local arr = {'java', 'python', 'lua'}
-- 遍历数组
for index,value in ipairs(arr) do
    print(index, value) 
end

遍历普通 table

-- 声明map,也就是table
local map = {name='Jack', age=21}
-- 遍历table
for key,value in pairs(map) do
   print(key, value) 
end

4. 条件控制、函数

Lua 中的条件控制和函数声明与 Java 类似。

4.1. 函数

定义函数的语法:

function 函数名( argument1, argument2..., argumentn)
    -- 函数体
    return 返回值
end

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

function printArr(arr)
    for index, value in ipairs(arr) do
        print(value)
    end
end

4.2. 条件控制

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

if(布尔表达式)
then
   --[ 布尔表达式为 true 时执行该语句块 --]
else
   --[ 布尔表达式为 false 时执行该语句块 --]
end

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

4.3. 案例

需求:自定义一个函数,可以打印 table,当参数为 nil 时,打印错误信息

function printArr(arr)
    if not arr then
        print('数组不能为空!')
    end
    for index, value in ipairs(arr) do
        print(value)
    end
end

四、实现多级缓存

多级缓存的实现离不开 Nginx 编程,而 Nginx 编程又离不开 OpenResty。

1. 安装OpenResty

OpenResty 是一个基于 Nginx 的高性能 Web 平台,

用于方便地搭建能够处理超高并发、扩展性极高的动态 Web 应用、Web 服务和动态网关。

具备下列特点:

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

官方网站: OpenResty® - 开源官方站

安装 Lua 可以参考《安装OpenResty 待更新》:

2. OpenResty快速入门

我们希望达到的多级缓存架构如图:

其中:

windows 上的 nginx 用来做反向代理服务,将前端的查询商品的 ajax 请求代理到 OpenResty 集群

OpenResty 集群用来编写多级缓存业务

2.1. 反向代理流程

现在,商品详情页使用的是假的商品数据。不过在浏览器中,可以看到页面有发起 ajax 请求查询真实商品数据。

这个请求如下:

请求地址是 localhost,端口是 80,就被 windows 上安装的 Nginx 服务给接收到了。

然后代理给了 OpenResty 集群:

我们需要在 OpenResty 中编写业务,查询商品数据并返回到浏览器。

但是这次,我们先在 OpenResty 接收请求,返回假的商品数据。

2.2. OpenResty监听请求

OpenResty 的很多功能都依赖于其目录下的 Lua 库,需要在 nginx.conf 中指定依赖库的目录,并导入依赖:

1、添加对 OpenResty 的 Lua 模块的加载

修改 /usr/local/openresty/nginx/conf/nginx.conf 文件,在其中的 http 下面,添加下面代码:

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

2、监听 /api/item 路径

修改 /usr/local/openresty/nginx/conf/nginx.conf 文件,在 nginx.conf 的 server 下面,添加对 /api/item 这

个路径的监听:

location  /api/item {
    # 默认的响应类型
    default_type application/json;
    # 响应结果由lua/item.lua文件来决定
    content_by_lua_file lua/item.lua;
}

这个监听,就类似于 SpringMVC 中的 @GetMapping("/api/item") 做路径映射。

而 content_by_lua_file lua/item.lua 则相当于调用 item.lua 这个文件,执行其中的业务,把结果返回给用户。

相当于 java 中调用 service。

2.3. 编写item.lua

1、在 /usr/loca/openresty/nginx 目录创建文件夹:lua

2、在 /usr/loca/openresty/nginx/lua 文件夹下,新建文件:item.lua

3、编写item.lua,返回假数据

item.lua 中,利用 ngx.say() 函数返回数据到 Response 中

ngx.say('{"id":10001,"name":"SALSA AIR","title":"RIMOWA 21寸托运箱拉杆箱 SALSA AIR系列果绿色 820.70.36.4","price":17900,"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}')

3. 请求参数处理

上一节中,我们在 OpenResty 接收前端请求,但是返回的是假数据。

要返回真实数据,必须根据前端传递来的商品 id,查询商品信息才可以。

那么如何获取前端传递的商品参数呢?

3.1. 获取参数的API

OpenResty 中提供了一些 API 用来获取不同类型的前端请求参数:

3.2. 获取参数并返回

在前端发起的 ajax 请求如图:

可以看到商品 id 是以路径占位符方式传递的,因此可以利用正则表达式匹配的方式来获取 ID

获取商品id

修改 /usr/loca/openresty/nginx/nginx.conf 文件中监听 /api/item 的代码,利用正则表达式获取 ID:

location ~ /api/item/(\d+) {
    # 默认的响应类型
    default_type application/json;
    # 响应结果由lua/item.lua文件来决定
    content_by_lua_file lua/item.lua;
}
拼接 ID 并返回

修改 /usr/loca/openresty/nginx/lua/item.lua 文件,获取 id 并拼接到结果中返回:

-- 获取商品id
local id = ngx.var[1]
-- 拼接并返回
ngx.say('{"id":' .. id .. ',"name":"SALSA AIR","title":"RIMOWA 21寸托运箱拉杆箱 SALSA AIR系列果绿色 820.70.36.4","price":17900,"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}')
重新加载并测试

运行命令以重新加载 OpenResty 配置:

nginx -s reload

刷新页面可以看到结果中已经带上了 ID:

4. 查询Tomcat

拿到商品 ID 后,本应去缓存中查询商品信息,不过目前我们还未建立 nginx、redis 缓存。

因此,这里我们先根据商品 id 去 tomcat 查询商品信息。

我们实现如图部分:

需要注意的是,我们的 OpenResty 是在虚拟机,Tomcat 是在 Windows 电脑上。两者 IP 一定不要搞错了。

4.1. 发送http请求的API

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

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

返回的响应内容包括:

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

原理如图:

4.2. 封装http工具

下面,我们封装一个发送 Http 请求的工具,基于 ngx.location.capture 来实现查询 tomcat。

1、添加反向代理,到 windows 的 Java 服务

因为 item-service 中的接口都是 /item 开头,所以我们监听 /item 路径,代理到 windows 上的 tomcat 服务。

修改 /usr/local/openresty/nginx/conf/nginx.conf 文件,添加一个 location:

location /item {
    proxy_pass http://192.168.150.1:8081;
}

以后,只要我们调用 ngx.location.capture("/item") ,就一定能发送请求到 windows 的 tomcat 服务。

2、封装工具类

之前我们说过,OpenResty 启动时会加载以下两个目录中的工具文件:

所以,自定义的 http 工具也需要放到这个目录下。

在 /usr/local/openresty/lualib 目录下,新建一个 common.lua 文件:

vi /usr/local/openresty/lualib/common.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请求查询失败, path: ", path , ", args: ", args)
        ngx.exit(404)
    end
    return resp.body
end
-- 将方法导出
local _M = {  
    read_http = read_http
}  
return _M

这个工具将 read_http 函数封装到 _M 这个 table 类型的变量中,并且返回,这类似于导出。

使用的时候,可以利用 require('common') 来导入该函数库,这里的 common 是函数库的文件名。

3、实现商品查询

最后,我们修改 /usr/local/openresty/lua/item.lua 文件,利用刚刚封装的函数库实现对 tomcat 的查询:

-- 引入自定义common工具模块,返回值是common中返回的 _M
local common = require("common")
-- 从 common中获取read_http这个函数
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 字符串,页面最终需要的是把两个 json 拼接为

一个 json:

这就需要我们先把 JSON 变为 lua 的 table,完成数据整合后,再转为 JSON。

4.3. CJSON工具类

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

官方地址: GitHub - openresty/lua-cjson: Lua CJSON is a fast JSON encoding/parsing module for Lua

1. 引入cjson模块:

local cjson = require "cjson"

2. 序列化:

local obj = {
    name = 'jack',
    age = 21
}
-- 把 table 序列化为 json
local json = cjson.encode(obj)

3. 反序列化:

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

4.4. 实现 Tomcat 查询

下面,我们修改之前的 item.lua 中的业务,添加 json 处理功能:

-- 导入common函数库
local common = require('common')
local read_http = common.read_http
-- 导入cjson库
local cjson = require('cjson')

-- 获取路径参数
local id = ngx.var[1]
-- 根据id查询商品
local itemJSON = read_http("/item/".. id, nil)
-- 根据id查询商品库存
local itemStockJSON = read_http("/item/stock/".. id, nil)

-- JSON转化为lua的table
local item = cjson.decode(itemJSON)
local stock = cjson.decode(stockJSON)

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

-- 把item序列化为json 返回结果
ngx.say(cjson.encode(item))

4.5. 基于ID负载均衡

刚才的代码中,我们的 tomcat 是单机部署。而实际开发中,tomcat 一定是集群模式:

因此,OpenResty 需要对 tomcat 集群做负载均衡。

而默认的负载均衡规则是轮询模式,当我们查询 /item/10001 时:

  1. 第一次会访问 8081 端口的 tomcat 服务,在该服务内部就形成了 JVM 进程缓存
  2. 第二次会访问 8082 端口的 tomcat 服务,该服务内部没有 JVM 缓存(因为 JVM 缓存无法共享),会查询

数据库

  1. ...

你看,因为轮询的原因,第一次查询 8081 形成的 JVM 缓存并未生效,直到下一次再次访问到 8081 时才可以生

效,缓存命中率太低了。

怎么办?

如果能让同一个商品,每次查询时都访问同一个 tomcat 服务,那么 JVM 缓存就一定能生效了。

也就是说,我们需要根据商品 id 做负载均衡,而不是轮询。

原理

nginx 提供了基于请求路径做负载均衡的算法:

nginx 根据请求路径做 hash 运算,把得到的数值对 tomcat 服务的数量取余,余数是几,就访问第几个服务,实现负载均衡。

例如:

  1. 我们的请求路径是 /item/10001
  2. tomcat 总数为 2 台(8081、8082)
  3. 对请求路径 /item/1001 做 hash 运算求余的结果为 1
  4. 则访问第一个 tomcat 服务,也就是 8081

只要 id 不变,每次 hash 运算结果也不会变,那就可以保证同一个商品,一直访问同一个 tomcat 服务,确保

JVM 缓存生效。

实现

修改 /usr/local/openresty/nginx/conf/nginx.conf 文件,实现基于 ID 做负载均衡。

首先,定义tomcat集群,并设置基于路径做负载均衡:

upstream tomcat-cluster {
    hash $request_uri;
    server 192.168.150.1:8081;
    server 192.168.150.1:8082;
}

然后,修改对 tomcat 服务的反向代理,目标指向 tomcat 集群:

location /item {
    proxy_pass http://tomcat-cluster;
}

重新加载 OpenResty

nginx -s reload
测试

启动两台 tomcat 服务:

同时启动:

清空日志后,再次访问页面,可以看到不同id的商品,访问到了不同的tomcat服务:

5. Redis缓存预热

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.150.101

4、编写初始化类

缓存预热需要在项目启动时完成,并且必须是拿到 RedisTemplate 之后。

这里我们利用 InitializingBean 接口来实现,因为 InitializingBean 可以在对象被 Spring 创建并且成员变量全部

注入后执行。

package com.zhengge.item.config;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.project.item.pojo.Item;
import com.project.item.pojo.ItemStock;
import com.project.item.service.IItemService;
import com.project.item.service.IItemStockService;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import java.util.List;

@Component
public class RedisHandler implements InitializingBean {

    @Autowired
    private StringRedisTemplate redisTemplate;

    @Autowired
    private IItemService itemService;
    @Autowired
    private IItemStockService stockService;

    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 json = MAPPER.writeValueAsString(item);
            // 2.2.存入redis
            redisTemplate.opsForValue().set("item:id:" + item.getId(), json);
        }

        // 3.查询商品库存信息
        List<ItemStock> stockList = stockService.list();
        // 4.放入缓存
        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);
        }
    }
}

6. 查询Redis缓存

现在,Redis 缓存已经准备就绪,我们可以再 OpenResty 中实现查询 Redis 的逻辑了。

如下图红框所示:

当请求进入 OpenResty 之后:

  1. 优先查询 Redis 缓存
  2. 如果 Redis 缓存未命中,再查询 Tomcat

6.1. 封装Redis工具

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

但是为了方便,我们将 Redis 操作封装到之前的 common.lua 工具库中。

修改 /usr/local/openresty/lualib/common.lua 文件:

  1. 引入 Redis 模块,并初始化 Redis 对象
-- 导入redis
local redis = require('resty.redis')
-- 初始化redis
local red = redis:new()
red:set_timeouts(1000, 1000, 1000)
  1. 封装函数,用来释放 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
  1. 封装函数,根据 key 查询 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
  1. 导出
-- 将方法导出
local _M = {  
    read_http = read_http,
    read_redis = read_redis
}  
return _M

完整的 common.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查询失败, path: ", path , ", args: ", args)
        ngx.exit(404)
    end
    return resp.body
end
-- 将方法导出
local _M = {  
    read_http = read_http,
    read_redis = read_redis
}  
return _M

6.2. 实现Redis查询

接下来,我们就可以去修改 item.lua 文件,实现对 Redis 的查询了。

查询逻辑是:

  1. 根据 id 查询 Redis
  2. 如果查询失败则继续查询 Tomcat
  3. 将查询结果返回

1. 修改 /usr/local/openresty/lua/item.lua 文件,添加一个查询函数:

-- 导入common函数库
local common = require('common')
local read_http = common.read_http
local read_redis = common.read_redis
-- 封装查询函数
function read_data(key, path, params)
    -- 查询本地缓存
    local val = read_redis("127.0.0.1", 6379, key)
    -- 判断查询结果
    if not val then
        ngx.log(ngx.ERR, "redis查询失败,尝试查询http, key: ", key)
        -- redis查询失败,去查询http
        val = read_http(path, params)
    end
    -- 返回数据
    return val
end

2. 而后修改商品查询、库存查询的业务:

3. 完整的item.lua代码:

-- 导入common函数库
local common = require('common')
local read_http = common.read_http
local read_redis = common.read_redis
-- 导入cjson库
local cjson = require('cjson')

-- 封装查询函数
function read_data(key, path, params)
    -- 查询本地缓存
    local val = read_redis("127.0.0.1", 6379, key)
    -- 判断查询结果
    if not val then
        ngx.log(ngx.ERR, "redis查询失败,尝试查询http, key: ", key)
        -- redis查询失败,去查询http
        val = read_http(path, params)
    end
    -- 返回数据
    return val
end

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

-- 查询商品信息
local itemJSON = read_data("item:id:" .. id,  "/item/" .. id, nil)
-- 查询库存信息
local stockJSON = read_data("item:stock:id:" .. id, "/item/stock/" .. id, nil)

-- JSON转化为lua的table
local item = cjson.decode(itemJSON)
local stock = cjson.decode(stockJSON)
-- 组合数据
item.stock = stock.stock
item.sold = stock.sold

-- 把item序列化为json 返回结果
ngx.say(cjson.encode(item))

7. Nginx本地缓存

现在,整个多级缓存中只差最后一环,也就是 nginx 的本地缓存了。

如图:

7.1. 本地缓存API

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

1、开启共享字典,在 nginx.conf 的 http 下添加配置:

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

2、操作共享字典:

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

7.2. 实现本地缓存查询

1、修改 /usr/local/openresty/lua/item.lua 文件,修改 read_data 查询函数,添加本地缓存逻辑:

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

-- 封装查询函数
function read_data(key, expire, path, params)
    -- 查询本地缓存
    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)
        -- 判断查询结果
        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, expire)
    -- 返回数据
    return val
end

2、修改 item.lua 中查询商品和库存的业务,实现最新的 read_data 函数:

其实就是多了缓存时间参数,过期后 nginx 缓存会自动删除,下次访问即可更新缓存。

这里给商品基本信息设置超时时间为 30 分钟,库存为 1 分钟。

因为库存更新频率较高,如果缓存时间过长,可能与数据库差异较大。

  1. 完整的 item.lua 文件:
-- 导入common函数库
local common = require('common')
local read_http = common.read_http
local read_redis = common.read_redis
-- 导入cjson库
local cjson = require('cjson')
-- 导入共享词典,本地缓存
local item_cache = ngx.shared.item_cache

-- 封装查询函数
function read_data(key, expire, path, params)
    -- 查询本地缓存
    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)
        -- 判断查询结果
        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, expire)
    -- 返回数据
    return val
end

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

-- 查询商品信息
local itemJSON = read_data("item:id:" .. id, 1800,  "/item/" .. id, nil)
-- 查询库存信息
local stockJSON = read_data("item:stock:id:" .. id, 60, "/item/stock/" .. id, nil)

-- JSON转化为lua的table
local item = cjson.decode(itemJSON)
local stock = cjson.decode(stockJSON)
-- 组合数据
item.stock = stock.stock
item.sold = stock.sold

-- 把item序列化为json 返回结果
ngx.say(cjson.encode(item))

五、缓存同步

大多数情况下,浏览器查询到的都是缓存数据,如果缓存数据与数据库数据存在较大差异,可能会产生比较严重的

后果。

所以我们必须保证数据库数据、缓存数据的一致性,这就是缓存与数据库的同步。

1. 数据同步策略

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

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

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

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

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

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

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

而异步实现又可以基于 MQ 或者 Canal 来实现:

1、基于 MQ 的异步通知:

解读:

  • 商品服务完成对数据的修改后,只需要发送一条消息到 MQ 中。
  • 缓存服务监听 MQ 消息,然后完成对缓存的更新

依然有少量的代码侵入。

2、基于 Canal 的通知

解读:

  • 商品服务完成商品修改后,业务直接结束,没有任何代码侵入
  • Canal监听MySQL变化,当发现变化后,立即通知缓存服务
  • 缓存服务接收到canal通知,更新缓存

代码零侵入

2. 安装Canal

2.1. 认识Canal

Canal [kə'næl],译意为水道/管道/沟渠,canal 是阿里巴巴旗下的一款开源项目,基于 Java 开发。

基于数据库增量日志解析,提供增量数据订阅 & 消费。

GitHub的地址:GitHub - alibaba/canal: 阿里巴巴 MySQL binlog 增量订阅&消费组件

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 的客户端,进而完成对其它数据库的同步。

2.2. 安装Canal

安装和配置 Canal 待更新

3. 监听Canal

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

我们可以利用 Canal 提供的 Java 客户端,监听 Canal 通知消息。当收到变化的消息时,完成对缓存的更新。

不过这里我们会使用 GitHub 上的第三方开源的 canal-starter 客户端。

地址:GitHub - NormanGyllenhaal/canal-client: spring boot canal starter 易用的canal 客户端 canal client

与 SpringBoot 完美整合,自动装配,比官方客户端要简单好用很多。

3.1. 引入依赖

<dependency>
    <groupId>top.javatool</groupId>

    <artifactId>canal-spring-boot-starter</artifactId>

    <version>1.2.1-RELEASE</version>

</dependency>

3.2. 编写配置

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

3.3. 修改Item实体类

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

package com.zhengge.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;
}

3.4. 编写监听器

通过实现EntryHandler 接口编写监听器,监听 Canal 消息。

注意两点:

  1. 实现类通过 @CanalTable("tb_item") 指定监听的表信息
  2. EntryHandler 的泛型是与表对应的实体类
package com.zhengge.item.canal;

import com.github.benmanes.caffeine.cache.Cache;
import com.project.item.config.RedisHandler;
import com.project.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) {
        // 写数据到JVM进程缓存
        itemCache.put(item.getId(), item);
        // 写数据到redis
        redisHandler.saveItem(item);
    }

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

    @Override
    public void delete(Item item) {
        // 删除数据到JVM进程缓存
        itemCache.invalidate(item.getId());
        // 删除数据到redis
        redisHandler.deleteItemById(item.getId());
    }
}

在这里对 Redis 的操作都封装到了 RedisHandler 这个对象中,是我们之前做缓存预热时编写的一个类,

内容如下:

package com.zhengge.item.config;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.project.item.pojo.Item;
import com.project.item.pojo.ItemStock;
import com.project.item.service.IItemService;
import com.project.item.service.IItemStockService;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import java.util.List;

@Component
public class RedisHandler implements InitializingBean {

    @Autowired
    private StringRedisTemplate redisTemplate;

    @Autowired
    private IItemService itemService;
    @Autowired
    private IItemStockService stockService;

    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 json = MAPPER.writeValueAsString(item);
            // 2.2.存入redis
            redisTemplate.opsForValue().set("item:id:" + item.getId(), json);
        }

        // 3.查询商品库存信息
        List<ItemStock> stockList = stockService.list();
        // 4.放入缓存
        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);
        }
    }

    public void saveItem(Item item) {
        try {
            String json = MAPPER.writeValueAsString(item);
            redisTemplate.opsForValue().set("item:id:" + item.getId(), json);
        } catch (JsonProcessingException e) {
            throw new RuntimeException(e);
        }
    }

    public void deleteItemById(Long id) {
        redisTemplate.delete("item:id:" + id);
    }
}

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

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

相关文章

足球青训后台管理系统:Spring Boot实现指南

2 相关技术简介 2.1 Java技术 Java是一门伟大的纯面向对象的编程语言和编程语言。同时&#xff0c;它还是Java语言从嵌入式开发到企业级开发的平台。Java凭借其一次编译&#xff0c;任何地方执行的优点&#xff0c;使得盛行的web应用程序有大量的Java编译&#xff0c;很好地支…

生信初学者教程(十九):免疫浸润细胞

文章目录 介绍加载R包导入数据所需函数运行ImmuCellAI其他免疫浸润方法输出结果总结介绍 免疫浸润分析在癌症研究中扮演着至关重要的角色,它有助于理解癌症微环境中免疫细胞的组成及其作用。bulk转录组基因表达数据的反卷积技术,如CIBERSORT算法,是实现这一分析的重要工具。…

云原生数据库 PolarDB

简介&#xff1a;云原生数据库 PolarDB 是阿里云自研产品&#xff0c;在存储计算分离架构下&#xff0c;利用了软硬件结合的优势&#xff0c;为用户提供秒级弹性、高性能、海量存储、安全可靠的数据库服务。100%兼容MySQL和PostgreSQL生态&#xff0c;支持分布式扩展&#xff0…

Spring整合Mybatis Plus

Mybatis Plus是原始Mybatis的增强&#xff0c;框架内部自动实现了Mapper的CRUD操作&#xff0c;极大的提高了编程效率。对单表操作基本无需编写Mapper.xml文件内容&#xff0c;对复杂的多表关联查询时&#xff0c;需要额外在Mapper.xml编写对应的sql语句。 Spring整合Mybatis P…

《如何高效学习》

有道云笔记 第一部分 整体性学习策略 结构 结构就像思想中的一座城市&#xff0c;有很多建筑物&#xff0c;建筑物之间有道路相连&#xff0c;有高大而重要的与其他建筑有上百条路相连&#xff0c;无关紧要的建筑只有少数泥泞的小道与外界相通。 建立良好的知识结构就是绘制…

仿真设计|基于51单片机的土壤温湿度监测及自动浇花系统仿真

目录 具体实现功能 设计介绍 51单片机简介 资料内容 仿真实现&#xff08;protues8.7&#xff09; 程序&#xff08;Keil5&#xff09; 全部内容 资料获取 具体实现功能 &#xff08;1&#xff09;DS18B20实时检测环境温度&#xff0c;LCD1602实时显示土壤温湿度&…

【C++】vector详解:接口使用、迭代器、内存理解、与模拟实现

文章目录 1. 前言2. 内存角度 理解3. vector的使用定义 | 构造函数vector iteratorvector 空间增长问题vector 增删查改vector 迭代器失效避免迭代器失效的建议 4. 如何理解 二维动态vector5. 模拟实现 vector6. 相关文档 1. 前言 vector 是 C 标准模板库&#xff08;STL&…

实例说明机器学习框架

机器学习框架是用于构建和训练机器学习模型的工具集合&#xff0c;它们提供了丰富的功能和库&#xff0c;帮助开发者简化模型开发流程。以下是几个流行的机器学习框架及其应用实例&#xff1a; 1. TensorFlow TensorFlow 是由 Google 开发的开源机器学习框架&#xff0c;广泛…

记一次使用python编写exp

使用的漏洞是企望制造ERP系统 RCE漏洞 POC POST /mainFunctions/comboxstore.action HTTP/1.1 Host: Cache-Control: max-age0 Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.…

影刀RPA实战:Excel拆分与合并工作表

1.影刀操作excel的优势 Excel&#xff0c;大家都不陌生&#xff0c;它是微软公司推出的一款电子表格软件&#xff0c;它是 Microsoft Office 套件的一部分。Excel 以其强大的数据处理、分析和可视化功能而闻名&#xff0c;广泛应用于商业、教育、科研等领域。可以说&#xff0…

生信初学者教程(二十):免疫浸润分析

文章目录 介绍加载R包导入数据所需函数堆积图箱线图热图相关性矩阵图输出结果总结介绍 在本章节中,将详细探讨免疫细胞的组成结构、其在不同个体和分组之间的相对丰度差异,并通过热图等可视化手段,对这些差异进行直观而深入的解析。这些分析将有助于科研人员更好地理解免疫细…

828华为云征文|华为云 Flexus X 实例之家庭娱乐中心搭建

话接上文《828华为云征文&#xff5c;华为云Flexus X实例初体验》&#xff0c;这次我们利用手头的 Flexus X 实例来搭建家庭影音中心和密码管理环境。 前置环境 为了方便小白用户甚至运维人员&#xff0c;我觉得现阶段的宝塔面板 和 1Panel 都是不错的选择。我这里以宝塔为例…

动态规划最低票价

前言&#xff1a;之前看到过这个题目归结到动态规划&#xff0c;当初还没什么思路&#xff0c;其实就是定义好dp [ i ] 为到第 i 个的最小费用就行&#xff0c;我们可以用upper_bound来优化我们的查找下标 题目地址 class Solution { public:int mincostTickets(vector<int&…

应对集运仓库丢件问题:集运系统的视频监控验货功能

在集运行业中&#xff0c;包裹丢件问题一直是令企业头疼的问题之一。客户投诉、纠纷处理不仅消耗了大量的人力物力&#xff0c;还可能影响企业的信誉和客户满意度。集运系统提供的视频验货服务&#xff0c;为解决这一难题提供了有效的解决方案。 一、集运仓库丢件问题的现状 集…

人口普查管理系统基于VUE+SpringBoot+Spring+SpringMVC+MyBatis开发设计与实现

目录 1. 系统概述 2. 系统架构设计 3. 技术实现细节 3.1 前端实现 3.2 后端实现 3.3 数据库设计 4. 安全性设计 5. 效果展示 ​编辑​编辑 6. 测试与部署 7. 示例代码 8. 结论与展望 一个基于 Vue Spring Boot Spring Spring MVC MyBatis 的人口普查管理…

MyBatis 学习总结

1. MyBatis 简介 MyBatis 是一款优秀的持久层框架&#xff0c;简化了 Java 程序与数据库的交互&#xff0c;通过 SQL 映射将 SQL 语句与 Java 对象关联起来。它基于传统 JDBC 的操作进行了封装&#xff0c;使得开发者无需手动编写复杂的 SQL 操作代码。 MyBatis 的特点包括&a…

《大厂算法冲锋:字符串数字求和的精妙之道》

前言 &#x1f680; 博主介绍&#xff1a;大家好&#xff0c;我是无休居士&#xff01;一枚任职于一线Top3互联网大厂的Java开发工程师&#xff01; &#x1f680; &#x1f31f; 欢迎大家关注我的微信公众号【JavaPersons】&#xff01;在这里&#xff0c;你将找到通往Java技…

云手机可以解决TikTok运营的哪些问题?

随着社交媒体的飞速发展&#xff0c;TikTok迅速崛起&#xff0c;成为个人和企业进行品牌宣传和内容创作的首选平台。然而&#xff0c;在运营TikTok账号的过程中&#xff0c;不少用户会遇到各种问题。本文将详细阐述云手机如何帮助解决这些问题。 1. 多账号管理的高效便捷 通过云…

[大语言模型-论文精读] 利用多样性进行大型语言模型预训练中重要数据的选择

[大语言模型-论文精读] 利用多样性进行大型语言模型预训练中重要数据的选择 论文信息&#xff1a; Harnessing Diversity for Important Data Selection in Pretraining Large Language Models Authors: Chi Zhang, Huaping Zhong, Kuan Zhang, Chengliang Chai, Rui Wang, X…

栈与队列相关知识(二)

目录 Java中栈&#xff08;Stack&#xff09; 一. 常用方法 1.push(E item) 2.pop() 3.peek() 4.empty() 二. 常用方法扩展 1. search(Object o) 2. clone() 3. contains(Object o) 4. size() 5. toArray() Java中队列&#xff08;Queue&#xff09; 一.常用方法&…