多级缓存快速上手

news2024/9/26 5:22:14

 哈喽~大家好,这篇来看看多级缓存。

 🥇个人主页:个人主页​​​​​             

🥈 系列专栏:【微服务】       

🥉与这篇相关的文章:            

JAVA进程和线程JAVA进程和线程-CSDN博客
HttpClient 入门使用示例HttpClient 入门使用示例-CSDN博客
Spring Task 快速入门Spring Task 快速入门-CSDN博客

目录

一、前言

1、什么是多级缓存?

2、集群模式

3、前期准备

二、Caffeine

1、什么是Caffeine?

2、缓存使用的基本API

2.1、基于大小设置驱逐策略

2.2、基于时间设置驱逐策略

三、实现多级缓存

1、前期准备

2、反向代理流程

3、OpenResty监听请求

4、代码解析

4.1、获取参数的API

4.2、查询Tomcat

4.3、CJSON工具类

4.4、基于ID负载均衡

4.5、Redis缓存预热

四、缓存同步

1、数据同步策略

2、监听Canal


一、前言

1、什么是多级缓存?

传统的缓存策略一般是请求到达Tomcat后,先查询Redis,如果未命中则查询数据库,这个是没有问题的,但是这存在一些问题(请求要经过Tomcat处理,Tomcat的性能成为整个系统的瓶颈 ;Redis缓存失效时,大量的数据操作会对数据库产生冲击 )。

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

  • 浏览器访问静态资源时,优先读取浏览器本地缓存

  • 访问非静态资源(ajax查询数据)时,访问服务端

  • 请求到达Nginx后,优先读取Nginx本地缓存

  • 如果Nginx本地缓存未命中,则去直接查询Redis(不经过Tomcat)

  • 如果Redis查询未命中,则查询Tomcat

  • 请求进入Tomcat后,优先查询JVM进程缓存

  • 如果JVM进程缓存未命中,则查询数据库

在多级缓存架构中,nginx是一个编写业务的Web服务器,不是作为反向代理的服务器了。

2、集群模式

也就是说,nginx与tomcat服务要部署为集群模式。

3、前期准备

准备好需要的素材,部署好nginx(注:将其拷贝到一个非中文目录下 ),打开conf里面的nginx.conf配置文件,编写好关键配置(nginx集群的ip地址:端口号;监听/api路径,反向代理到nginx集群)。

此时 192.168.227.131 是我虚拟机的ip地址(这里你写的时候记得换上自己的)

二、Caffeine

1、什么是Caffeine?

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

缓存在日常开发中启动至关重要的作用 ,能大量减少对数据库的访问,减少数据库的压力 ,我们把缓存分为两类:

  • 分布式缓存,例如Redis:

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

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

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

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

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

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

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

我们的思路是:当我们的请求到nginx中,首先先查询本地缓存,当本地缓存没有时,再去查询redis,redis没有时,再去查询jvm进程,当这些都没有命中时,再最后查数据库。

2、缓存使用的基本API

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

    // 存数据
    cache.put("gf", "ddf");

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

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

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

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

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

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

    // 创建缓存对象
    Cache<String, String> cache = Caffeine.newBuilder()
        // 设置缓存有效期为 10 秒,从最后一次写入开始计时 
        .expireAfterWrite(Duration.ofSeconds(10)) 
        .build();
     
  • 基于引用:设置缓存为软引用或弱引用,利用GC来回收缓存数据。性能较差,不建议使用。

2.1、基于大小设置驱逐策略

    @Test
    void testEvictByNum() throws InterruptedException {
        // 创建缓存对象
        Cache<String, String> cache = Caffeine.newBuilder()
                // 设置缓存大小上限为 1
                .maximumSize(1)
                .build();
        // 存数据
        cache.put("gf1", "a");
        cache.put("gf2", "b");
        cache.put("gf3", "c");
        // 延迟10ms,给清理线程一点时间
        Thread.sleep(10L);
        // 获取数据
        System.out.println("gf1: " + cache.getIfPresent("gf1"));
        System.out.println("gf2: " + cache.getIfPresent("gf2"));
        System.out.println("gf3: " + cache.getIfPresent("gf3"));
    }

2.2、基于时间设置驱逐策略

    @Test
    void testEvictByTime() throws InterruptedException {
        // 创建缓存对象
        Cache<String, String> cache = Caffeine.newBuilder()
                .expireAfterWrite(Duration.ofSeconds(1)) // 设置缓存有效期为 10 秒
                .build();
        // 存数据
        cache.put("gf", "aaa");
        // 获取数据
        System.out.println("gf: " + cache.getIfPresent("gf"));
        // 休眠一会儿
        Thread.sleep(1200L);
        System.out.println("gf: " + cache.getIfPresent("gf"));
    }

三、实现多级缓存

1、前期准备

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

下载与安装步骤这里就不做过多的描述了,OpenResty底层是基于Nginx的,查看OpenResty目录的nginx目录,所以运行方式与nginx基本一致:

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

修改/usr/local/openresty/nginx/conf/nginx.conf文件,内容如下:


#user  nobody;
worker_processes  1;
error_log  logs/error.log;

events {
    worker_connections  1024;
}

http {
    include       mime.types;
    default_type  application/octet-stream;
    sendfile        on;
    keepalive_timeout  65;

    server {
        listen       8081;
        server_name  localhost;
        location / {
            root   html;
            index  index.html index.htm;
        }
        error_page   500 502 503 504  /50x.html;
        location = /50x.html {
            root   html;
        }
    }
}

2、反向代理流程

打开案例,他的请求路径是这个:【微服务】       

请求地址是localhost,端口是80,就被windows上安装的Nginx服务给接收到了。然后代理给了OpenResty集群,这就是ip为:192.168.227.131。

3、OpenResty监听请求

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

修改/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;;";  

监听/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")做路径映射,而返回类型就是json。

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

/usr/loca/openresty/nginx目录创建文件夹:lua;在/usr/loca/openresty/nginx/lua文件夹下,新建文件:item.lua。

item.lua代码

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

-- 封装查询函数
function read_data(key, expire,  path, params)
	local var = item_cache:get(key)
	if not var then
		ngx.log(ngx.ERR, "本地缓存查询失败,尝试查询redis, key: ", key)
	    -- 查询redis缓存
	    var = read_redis("127.0.0.1", 6379, key)
	    -- 判断查询结果
	    if not var then
	        ngx.log(ngx.ERR, "redis查询失败,尝试查询http, key: ", key)
	        -- redis查询失败,去查询http
	        var = read_http(path, params)
	    end
	end
	-- 查询成功,根据不同的数据设置不同的缓存时间,并且写入到本地缓存
	item_cache:set(key, var, expire)
	-- 返回数据
    return var
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))

在nginx.cpnf里面添加

		# 添加反向代理,到windows的Java服务
		# 该指令是用来设置代理服务器的地址,可以是主机名称,IP地址加端口号等形式。
		location /item {
    			proxy_pass http://tomcat-cluster;
		}
     upstream tomcat-cluster{
    		hash $request_uri;
    		server 192.168.177.196:8081;
    		server 192.168.177.196:8082;
     }

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请求,并解析响应( ngx.location.capture)
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

然后重新加载配置:nginx -s reload。

4、代码解析

4.1、获取参数的API

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

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

里面的  ~ /api/item/(\d+) 对应的就是 http://localhost/api/item/10003 (前端发来的路径,这里拿到了商品的id)

4.2、查询Tomcat

拿到商品ID后,本应去缓存中查询商品信息,不过目前我们还未建立nginx、redis缓存。因此,这里我们先根据商品id去tomcat查询商品信息。

发送http请求的API

举个例子:

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://你自己的ip:8081; 
 }

在item.lua文件当中,有这一串:

-- 引入自定义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)
ngx.say(itemStockJSON )

他的作用是接受到请求路径,然后根据id来查询数据库,返回json数据。

里查询到的结果是json字符串,并且包含商品、库存两个json字符串,页面最终需要的是把两个json拼接为一个json:

这就需要我们先把JSON变为lua的table,完成数据整合后,再转为JSON(序列化与反序列化)。

4.3、CJSON工具类

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

举个例子:

引入cjson模块:

local cjson = require "cjson"

序列化:

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

反序列化:

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

那么实现Tomcat'查询是:

-- 导入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.4、基于ID负载均衡

刚才的代码中,我们的tomcat是单机部署。而实际开发中,tomcat一定是集群模式,因此,OpenResty需要对tomcat集群做负载均衡。

如何做?

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

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

思路

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

举个例子

  • 我们的请求路径是 /item/10001

  • tomcat总数为2台(8081、8082)

  • 对请求路径/item/1001做hash运算求余的结果为1

  • 则访问第一个tomcat服务,也就是8081

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

在nginx.conf文件里面添加这一段(hash $request_uri;)

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

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

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

重新加载OpenResty

nginx -s reload

4.5、Redis缓存预热

Redis缓存会面临冷启动问题:

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

缓存预热:在实际开发中,我们可以利用大数据统计用户访问的热点数据,在项目启动时将这些热点数据提前查询并保存到Redis中。

由于数据较少所以这里将所有的数据都存入缓存中。

具体代码

@Component
public class RedisHandler implements InitializingBean {

    @Autowired
    private StringRedisTemplate redisTemplate;
    @Autowired
    private IItemService itemService;
    @Autowired
    private IItemStockService itemStockService;

    /**
     * Jackson提供了ObjectMapper来供程序员“定制化控制”序列化、反序列化的过程。
     * objectMapper在调用writeValue()序列化 或 调用readValue()反序列化方法之前,
     * 往往需要设置 ObjectMapper 的相关配置信息,这些配置信息作用在 java 对象的所有属性上,
     * 表示在进行序列化和反序列化时进行一些特殊的处理。
     */
    private static final ObjectMapper MAPPER = new ObjectMapper();

    @Override
    public void afterPropertiesSet() throws Exception {
        // 查询商品
        List<Item> itemList = itemService.list();
        // 商品集合序列化,存入redis
        for (Item item : itemList) {
            String itemJson = MAPPER.writeValueAsString(item);
            redisTemplate.opsForValue().set("item:id:" + item.getId(), itemJson);
        }

        // 查询库存
        List<ItemStock> stockList = itemStockService.list();
        // 库存集合序列化,存入redis
        for (ItemStock stock : stockList) {
            String stockJson = MAPPER.writeValueAsString(stock);
            redisTemplate.opsForValue().set("item:stock:id:" + stock.getId(), stockJson);
        }

    }

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

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

}

InitializingBean接口为bean提供了初始化方法的方式,它只包括afterPropertiesSet方法,凡是继承该接口的类,在初始化bean的时候都会执行该方法。

ObjectMapper:Jackson提供了ObjectMapper来供程序员“定制化控制”序列化、反序列化的过程。objectMapper在调用writeValue()序列化 或 调用readValue()反序列化方法之前,往往需要设置 ObjectMapper 的相关配置信息,这些配置信息作用在 java 对象的所有属性上,表示在进行序列化和反序列化时进行一些特殊的处理。

四、缓存同步

大多数情况下,浏览器查询到的都是缓存数据,当我们管理员修改数据时,缓存没有及时更新,这就会出大问题了。

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

1、数据同步策略

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

  • 优势:简单、方便

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

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

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

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

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

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

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

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

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

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

这里我们使用Canal(基于Canal的通知 )

2、监听Canal

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

我们可以利用Canal提供的Java客户端,监听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: 192.168.150.101:11111 # canal服务地址

修改实体类

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

@TableName("tb_item"):要监听的表名

@Id:告诉他谁是id(主键)

@Column(name = "name"):当DB里面的字段与实体类对应不上时,用name对应。

@Transient:告诉它,谁不是表中的字段。

编写监听器

通过实现EntryHandler<T>接口编写监听器,监听Canal消息。注意两点:

  • 实现类通过@CanalTable("tb_item")指定监听的表信息

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

不积跬步无以至千里,趁年轻,使劲拼,给未来的自己一个交代!向着明天更好的自己前进吧!

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

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

相关文章

【数据结构/C++】栈和队列_链队列

#include <iostream> using namespace std; // 链队列 typedef int ElemType; typedef struct LinkNode {ElemType data;struct LinkNode *next; } LinkNode; typedef struct {LinkNode *front, *rear; } LinkQueue; // 初始化 void InitQueue(LinkQueue &Q) {Q.fron…

Redis深入理解-Socket连接建立流程以及文件事件处理机制

Redis Server 运行原理图 Redis 服务器中 Socket 网络建立以及文件事件模型 一个 redis 单机&#xff0c;可以抗几百上千的并发&#xff0c;这里的并发指的就是同时可以有几百个 client 对这个 redis server 发起请求&#xff0c;都需要去建立网络连接&#xff0c;同时间可能会…

Attention is All You Need:Transformer各模块详解

Transformer encoder-decoder架构 Encoder&#xff1a;将输入序列转换为一个连续向量空间中的表示。Encoder通常是一个循环神经网络&#xff08;RNN&#xff09;或者卷积神经网络&#xff08;CNN&#xff09;&#xff0c;通过对输入序列中的每个元素进行编码&#xff0c;得到…

SQLite3 数据库学习(六):Qt 嵌入式 Web 服务器详解

参考引用 SQLite 权威指南&#xff08;第二版&#xff09;SQLite3 入门 1. Apache 搭建 cgi 环境 1.1 什么是 Apache Apache 是世界使用排名第一的 Web 服务器软件 它可以运行在几乎所有广泛使用的计算机平台上&#xff0c;由于其跨平台和安全性被广泛使用 1.2 具体搭建流程…

代码随想录算法训练营Day 59 || 503.下一个更大元素II、42. 接雨水

503.下一个更大元素II 力扣题目链接(opens new window) 给定一个循环数组&#xff08;最后一个元素的下一个元素是数组的第一个元素&#xff09;&#xff0c;输出每个元素的下一个更大元素。数字 x 的下一个更大的元素是按数组遍历顺序&#xff0c;这个数字之后的第一个比它更…

数据中台建设方法论

1、数仓的概念和了解--业务的痛点 产生的痛点&#xff1a;数据资产比较模糊、数据的质量比较低、重复建设、代码的耦合性比较强。 2、数据仓库中的常见的模型&#xff1a; 1、心型模型&#xff1a;中间是一张事实表&#xff0c;周围都是维度表。 对于心型模型的主要的特点&a…

可上手 JVM 调优实战指南

文章目录 为什么要学 JVM一、JVM 整体布局二、Class 文件规范三、类加载模块四、执行引擎五、GC 垃圾回收1 、JVM内存布局2 、 JVM 有哪些主要的垃圾回收器&#xff1f;3 、分代垃圾回收工作机制 六、对 JVM 进行调优的基础思路七、 GC 情况分析实例八、最后总结 全程可上手JVM…

快速了解敏捷测试!解密敏捷测试的难点!

随着敏捷开发模式的普及&#xff0c;越来越多的测试同仁也开始了敏捷测试。那么究竟什么是敏捷测试&#xff1f;敏捷测试与传统测试的主要区别是什么&#xff1f;敏捷测试的难点又是什么&#xff1f;本文会对这三个问题进行讲解。注意&#xff1a;本文只是讲解敏捷测试概念相关…

String 、StringBuffer 和 StringBuilder 的区别?

String 使用 String 声明一个字符串的时候&#xff0c;该字符串会存放在堆中的字符串常量池中。因为在java中所有的String 都是以常量表示&#xff0c;且由 final 修饰&#xff0c;因此在线程池中它的线程是安全的 且 不可变的 。每个 String 在被创建后就不再发生任何变化。 …

新王加冕,GPT-4V 屠榜视觉问答

当前&#xff0c;多模态大型模型&#xff08;Multi-modal Large Language Model, MLLM&#xff09;在视觉问答&#xff08;VQA&#xff09;领域展现了卓越的能力。然而&#xff0c;真正的挑战在于知识密集型 VQA 任务&#xff0c;这要求不仅要识别视觉元素&#xff0c;还需要结…

【SpringMVC】 对请求的不同响应

前言 本文学习如何运用不同的注解来返回不同的响应. 1.返回静态页面Controller 返回index.html页面 Controller 和 RestController的区别 controller 只有加上这个注解,Spring才会帮我们管理这个代码.后续我们访问时才能访问到. RestController 等同于 Controller ResponseBo…

前端学习--React(3)

一、Redux 集中状态管理工具&#xff0c;不需要react即可使用&#xff0c;每个store的数据都是独立于组件之外的 vue小链接&#xff1a;vuex/pinia 基本使用 Redux将数据修改流程分成三个概念&#xff0c;state、action和reducer state - 一个对象 存放我们管理的数据状态 a…

EI论文故障识别程序:DBN深度置信/信念网络的故障识别Matlab程序,数据由Excel导入,直接运行!

​适用平台&#xff1a;Matlab2021b版及以上 本程序参考中文EI期刊《基于变分模态分解和改进灰狼算法优化深度置信网络的自动转换开关故障识别》中的深度置信网络&#xff08;Deep Belief Network&#xff0c;DBN&#xff09;部分进行故障识别&#xff0c;程序注释清晰&#x…

OpenStack云计算平台-Networking 服务

目录 一、网络服务概览 二、网络&#xff08;neutron&#xff09;概念 三、安装并配置控制节点 1、先决条件 2、配置网络选项&#xff08;公共网络&#xff09; &#xff08;1&#xff09;安装组件 &#xff08;2&#xff09;配置服务组件 &#xff08;3&#xff09;配…

Notepad-- ubuntu下载安装

Notepad-- ubuntu下载安装 下载 Gitee链接&#xff1a; https://gitee.com/cxasm/notepad– 安装 sudo apt install *.deb运行 /opt/apps/com.hmja.notepad/files/Notepad--出错 需要安装qt5 sudo apt-get install qt5-default

【UE】用样条线实现测距功能(下)

目录 效果 步骤 一、实现多次测距功能 二、通过控件蓝图来进行测距 在上一篇&#xff08;【UE】用样条线实现测距功能&#xff08;上&#xff09;&#xff09;文章基础上继续实现多次测距和清除功能。 效果 步骤 一、实现多次测距功能 打开蓝图“BP_Spline”&#xff0c…

UML建模图文详解教程05——包图

版权声明 本文原创作者&#xff1a;谷哥的小弟作者博客地址&#xff1a;http://blog.csdn.net/lfdfhl本文参考资料&#xff1a;《UML面向对象分析、建模与设计&#xff08;第2版&#xff09;》吕云翔&#xff0c;赵天宇 著 包图概述 包图(package diagram)是用来描述模型中的…

DAOS低时延与高性能RDMA网络

什么是RDMA RDMA&#xff08;Remote Direct Memory Access&#xff09;远程直接内存访问是一种技术&#xff0c;它使两台联网的计算机能够在主内存中交换数据&#xff0c;而无需依赖任何一台计算机的处理器、缓存或操作系统。与基于本地的直接内存访问 ( DMA ) 一样&#xff0c…

抖音本地生活服务商申请怎么做?无保证金的申请方法来了

想做抖音的本地生活服务项目&#xff0c;却不知道去哪里申请&#xff0c;或者如何申请&#xff0c;其实&#xff0c;官方的通道在今年上半年还是有的&#xff0c;自己去平台上提交资料申请就可以了&#xff0c;但需要缴纳高额的保证金。 而在今年下半年&#xff0c;平台已经关…