多级缓存快速上手

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

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

🥈 系列专栏:【微服务】       

🥉与这篇相关的文章:            

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 "asdSystem.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、基于大小设置驱逐策略

    @Testvoid 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、基于时间设置驱逐策略

    @Testvoid 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 thenngx.log(ngx.ERR, "本地缓存查询失败,尝试查询redis, key: ", key)-- 查询redis缓存var = read_redis("127.0.0.1", 6379, key)-- 判断查询结果if not var thenngx.log(ngx.ERR, "redis查询失败,尝试查询http, key: ", key)-- redis查询失败,去查询httpvar = read_http(path, params)endend-- 查询成功,根据不同的数据设置不同的缓存时间,并且写入到本地缓存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 thenngx.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 thenngx.log(ngx.ERR, "连接redis失败 : ", err)return nilend-- 查询redislocal resp, err = red:get(key)-- 查询失败处理if not resp thenngx.log(ngx.ERR, "查询Redis失败: ", err, ", key = " , key)end--得到的数据为空处理if resp == ngx.null thenresp = nilngx.log(ngx.ERR, "查询Redis数据为空, key = ", key)endclose_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-- 记录错误信息,返回404ngx.log(ngx.ERR, "http请求查询失败, path: ", path , ", args: ", args)ngx.exit(404)endreturn 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 {@Autowiredprivate StringRedisTemplate redisTemplate;@Autowiredprivate IItemService itemService;@Autowiredprivate IItemStockService itemStockService;/*** Jackson提供了ObjectMapper来供程序员“定制化控制”序列化、反序列化的过程。* objectMapper在调用writeValue()序列化 或 调用readValue()反序列化方法之前,* 往往需要设置 ObjectMapper 的相关配置信息,这些配置信息作用在 java 对象的所有属性上,* 表示在进行序列化和反序列化时进行一些特殊的处理。*/private static final ObjectMapper MAPPER = new ObjectMapper();@Overridepublic void afterPropertiesSet() throws Exception {// 查询商品List<Item> itemList = itemService.list();// 商品集合序列化,存入redisfor (Item item : itemList) {String itemJson = MAPPER.writeValueAsString(item);redisTemplate.opsForValue().set("item:id:" + item.getId(), itemJson);}// 查询库存List<ItemStock> stockList = itemStockService.list();// 库存集合序列化,存入redisfor (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)@Idprivate 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)@Transientprivate Integer stock;@TableField(exist = false)@Transientprivate 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> {@Autowiredprivate RedisHandler redisHandler;@Autowiredprivate Cache<Long, Item> itemCache;@Overridepublic void insert(Item item) {// 写数据到JVM进程缓存itemCache.put(item.getId(), item);// 写数据到redisredisHandler.saveItem(item);}@Overridepublic void update(Item before, Item after) {// 写数据到JVM进程缓存itemCache.put(after.getId(), after);// 写数据到redisredisHandler.saveItem(after);}@Overridepublic void delete(Item item) {// 删除数据到JVM进程缓存itemCache.invalidate(item.getId());// 删除数据到redisredisHandler.deleteItemById(item.getId());}
}

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

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

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

相关文章

不做机器视觉工程师,转行,转岗的建议与想法

正所谓外行看热闹&#xff0c;内行看门道。提前咨询前辈们&#xff0c;多问问&#xff0c;多看看。要做就做&#xff0c;一定要提前做好防范。 无论你是要转行或者是转岗&#xff0c;看你有没有本钱和试错成本 有些人&#xff0c;家庭好&#xff0c;可以一直去试错和从头再来。…

无线WiFi安全渗透与攻防(国外篇):使用 Aircrack-ng 破解 WEP 密码

使用 Aircrack-ng 破解 WEP 密码 使用 Aircrack-ng 破解 WEP 密码一. 用 Aircrack-ng 破解 WEP 密码 - 背景知识网卡与网卡芯片WEP 加密协议WEP 所使用的身份认证协议二. 使用 Aircrack-ng 破解 WEP 密码 - 破解原理破解机理三. 使用 Aircrack-ng 破解 WEP 密码 - aircrack-ng …

学习.NET验证模块FluentValidation的基本用法(续1:其它常见用法)

FluentValidation模块支持链式验证方法调用&#xff0c;也就是说&#xff0c;除了 RuleFor(r > r.UserName).NotEmpty()调用方式之外&#xff0c;还可以将对单个属性的多种验证函数以链式调用方式串接起来&#xff0c;比如UserName属性不能为空&#xff0c;长度在5~10之间&a…

__attribute__((constructor))用法解析

__attribute__((constructor))是GCC和兼容的编译器中的一个特性&#xff0c;用于指示编译器将一个函数标记为在程序启动时自动执行的初始化函数。 同样的还有__attribute__((destructor))在main()函数后调用。 当你在一个函数声明或定义前加上__attribute__((constructor))属…

浅谈 Guava 中的 ImmutableMap.of 方法的坑

作者&#xff1a;明明如月学长&#xff0c; CSDN 博客专家&#xff0c;大厂高级 Java 工程师&#xff0c;《性能优化方法论》作者、《解锁大厂思维&#xff1a;剖析《阿里巴巴Java开发手册》》、《再学经典&#xff1a;《EffectiveJava》独家解析》专栏作者。 热门文章推荐&…

vue项目下.env.development环境变量配置文件

.env.development 文件是一个用于开发环境配置的文件。在许多应用程序中&#xff0c;开发环境和生产环境具有不同的配置需求。.env.development 文件允许你在开发环境中定义特定的环境变量和配置选项。 一般来说&#xff0c;.env.development 文件用于存储开发环境相关的配置信…

国自然项目基金撰写的隐藏技巧、范例分析及提交前的自我审查

目录 一、基金项目申请要求、重点及项目介绍 二、基金的撰写技巧 三、基金撰写的隐藏技巧 四、范例分析及提交前的自我审查 更多应用 基金项目申请需要进行跨学科的技术融合&#xff0c;申请人需要与不同领域结合&#xff0c;形成多学科交叉的研究。基金项目申请在新时期更…

由红黑树引出的HashMap扩容机制的思考

红黑树是什么&#xff1f; 三大特点&#xff1a; 根节点是黑色&#xff0c;叶节点是不存储数据的黑色空节点 任何相邻的两个节点不能同时为红色 任意节点到其可到达的节点间包含相同数量的黑色节点 联想&#xff1a;Java HashMap底层红黑树原理 HashMap基于哈希表Map接口实…

快速掌握Pyqt5的三种主窗口

PyQt5是一个强大的跨平台GUI框架&#xff0c;它提供了多种不同类型的主窗口类&#xff0c;以满足不同的应用需求。下面是PyQt5中最常见的几种主窗口类型及其创建方式的简介&#xff1a; 1. QMainWindow QMainWindow是用于创建具有菜单栏、工具栏、状态栏和中心窗口部件&#…

内存池 示例一

内存池是一种管理内存分配和释放的技术&#xff0c;用于优化内存的使用效率。它通过预先分配一块内存区域&#xff0c;并将其划分为多个较小的块&#xff08;内存块池&#xff09;&#xff0c;然后按需分配这些内存块来减少内存碎片化和频繁的系统调用。这些内存块可以是相同大…

Centos7.9配置nfs共享及rsync同步

客户需求对oracle数据库做一个跨机房的备份&#xff0c;原环境已做rman备份和每天expdp全库导出&#xff0c;远端只有虚拟化环境&#xff0c;可提供一个虚拟机&#xff0c;2个机房间网络互通。 首先配置nfs服务端 查看操作系统版本 [rootnas199 ~]# more /etc/redhat-relea…

Python面经【1】

一、协程的相关概念 协程&#xff08;又称微线程&#xff09;运行在线程之上&#xff0c;更加轻量级&#xff0c;协程并没有增加线程总数&#xff0c;只是在线程的基础上通过分时复用的方式运行多个协程&#xff0c;大大提高工程效率。 协程的特点&#xff1a; 轻量级&#…

WordPress站点屏蔽过滤垃圾评论教程(Akismet反垃圾评论插件)

前段时间我的WordPress站点经常收到垃圾评论的轰炸&#xff0c;严重时一天会收到几十条垃圾评论。我这个小破站一没啥流量&#xff0c;二又不盈利&#xff0c;实在是不太理解为啥有人要这么执着地浪费资源在上面。 Akismet反垃圾评论插件 其实用了 Akismet 反垃圾评论插件后&a…

快速掌握Pyqt5的6种按钮

在PyQt5中&#xff0c;按钮是构建用户界面的基本元素之一&#xff0c;用于执行命令、启动功能或触发事件。PyQt5提供了多种类型的按钮&#xff0c;每种都适用于不同的场景和需求。 1. QPushButton QPushButton 是最常用的按钮类型&#xff0c;适用于大多数情况&#xff0c;如…

ARCore:在Android上构建令人惊叹的增强现实体验

ARCore&#xff1a;在Android上构建令人惊叹的增强现实体验 一、 AR 介绍1.1 AR技术简介1.2 AR技术原理1.3 AR技术应用领域 二、Google的增强现实平台ARCore2.1 ARCore简介2.2 ARCore API介绍2.3 ARCore API使用示例 三、总结 一、 AR 介绍 增强现实 Augmented Reality&#x…

【算法-字符串2】替换空格 + 反转单词

今天&#xff0c;带来字符串相关算法的讲解。文中不足错漏之处望请斧正&#xff01; 理论基础点这里 1. 替换空格 题目描述&#xff1a;请实现一个函数&#xff0c;把字符串 s 中的每个空格替换成"%20"。 来源&#xff1a;力扣&#xff08;LeetCode&#xff09; 难…

Lettuce使用详解

简介特点连接池连接池特点连接池管理连接池优势连接池配置参数 监控常用监控工具通过JMX监控通过Prometheus监控 代码示例拓展springboot中通过jmx上报到Prometheus代码示例更多Redis相关内容 简介 Lettuce 是一个高级的、线程安全的 Redis 客户端&#xff0c;用于与 Redis 数…

深度学习基础概念

1. 神经网络基础 神经元&#xff08;Neuron&#xff09;&#xff1a; 了解神经网络的基本组成单元。激活函数&#xff08;Activation Function&#xff09;&#xff1a; 学习常见的激活函数&#xff0c;如Sigmoid、ReLU等&#xff0c;以及它们在神经网络中的作用。前馈神经网络…

An issue was found when checking AAR metadata

一、报错信息 An issue was found when checking AAR metadata:1. Dependency androidx.activity:activity:1.8.0 requires libraries and applications that depend on it to compile against version 34 or later of the Android APIs.:app is currently compiled against …

Python 异步套接字编程

异步套接字编程是异步编程在网络通信中的应用&#xff0c;它使用异步 IO 操作和事件循环来实现高并发的网络应用。Python 中的 asyncio 模块提供了对异步套接字编程的支持&#xff0c;以下是异步套接字编程的一些重要概念和使用方法&#xff1a; 1. 异步套接字服务器&#xff…