缓存分享(1)——Guava Cache原理及最佳实践

news/2024/5/18 22:03:22

Guava Cache原理及最佳实践

  • 1. Guava Cache是什么
    • 1.1 简介
    • 1.2 核心功能
    • 1.3 适用场景
  • 2. Guava Cache的使用
    • 2.1 创建LoadingCache缓存
    • 2.2 创建CallableCache缓存
    • 2.3 其他用法
  • 3.缓存失效回收策略
    • 3.1 基于容量回收
    • 3.2 定时回收
    • 3.3 基于引用回收
    • 3.4 显式清除
  • 4、缓存失效回收时机
  • 5、源码分析(简短分析)
  • 6、刷新
  • 7、参考材料

缓存的种类有很多,需要根据不同的应用场景来选择不同的cache,比如分布式缓存如redis、memcached,还有本地(进程内)缓存如:ehcache、GuavaCache、Caffeine。本篇主要围绕全内存缓存-Guava Cache做一些详细的讲解和分析。

1. Guava Cache是什么

1.1 简介

Guava cache是一个支持高并发的线程安全的本地缓存。多线程情况下也可以安全的访问或者更新Cache。这些都是借鉴了ConcurrentHashMap的结果,不过,guava cache 又有自己的特性 :

"automatic loading of entries into the cache"

即 :当cache中不存在要查找的entry的时候,它会自动执行用户自定义的加载逻辑,加载成功后再将entry存入缓存并返回给用户未过期的entry,如果不存在或者已过期,则需要load,同时为防止多线程并发下重复加载,需要先锁定,获得加载资格的线程(获得锁的线程)创建一个LoadingValueRefrerence并放入map中,其他线程等待结果返回。

1.2 核心功能

  • 自动将entry节点加载进缓存结构中;
  • 当缓存的数据超过设置的最大值时,使用LRU算法移除;
  • 具备根据entry节点上次被访问或者写入时间计算它的过期机制;
  • 缓存的key被封装在WeakReference引用内;
  • 缓存的Value被封装在WeakReferenceSoftReference引用内;
  • 统计缓存使用过程中命中率、异常率、未命中率等统计数据。

小结:
Guava Cache说简单点就是一个支持LRU的ConcurrentHashMap,并提供了基于容量,时间和引用的缓存回收方式。(简单概括)

1.3 适用场景

  • 愿意消耗一些内存空间来提升速度(以空间换时间,提升处理速度);
    • 能够预计某些key会被查询一次以上;
    • 缓存中存放的数据总量不会超出内存容量(Guava Cache是单个应用运行时的本地缓存)。
  • 计数器(如可以利用基于时间的过期机制作为限流计数)

2. Guava Cache的使用

GuavaCache使用时主要分二种模式:LoadingCacheCallableCache
核心区别在于:LoadingCache创建时需要有合理的默认方法来加载或计算与键关联的值,CallableCache创建时无需关联固定的CacheLoader使用起来更加灵活。

前置准备:

  1. 引入jar包
<dependency><groupId>com.google.guava</groupId><artifactId>guava</artifactId><version>29.0-jre</version>
</dependency>
  1. 了解CacheBuilder的配置方法
    在这里插入图片描述

  2. mock RPC调用方法,用于获取数据

private static List<String> rpcCall(String cityId) {// 模仿从数据库中取数据try {switch (cityId) {case "0101":System.out.println("load cityId:" + cityId);return ImmutableList.of("上海", "北京", "广州", "深圳");}} catch (Exception e) {// 记日志}return Collections.EMPTY_LIST;
}

2.1 创建LoadingCache缓存

使用CacheBuilder来构建LoadingCache实例,可以链式调用多个方法来配置缓存的行为。其中CacheLoader可以理解为一个固定的加载器,在创建LoadingCache时指定,然后简单地重写V load(K key) throws Exception方法,就可以达到当检索不存在的时候自动加载数据的效果。

//创建一个LoadingCache,并可以进行一些简单的缓存配置
private static LoadingCache<String, Optional<List<String>> > loadingCache = CacheBuilder.newBuilder()//配置最大容量为100,基于容量进行回收.maximumSize(100)//配置写入后多久使缓存过期-下文会讲述.expireAfterWrite(3, TimeUnit.SECONDS)//配置写入后多久刷新缓存-下文会讲述.refreshAfterWrite(3, TimeUnit.SECONDS)//key使用弱引用-WeakReference.weakKeys()//当Entry被移除时的监听器-下文会讲述.removalListener(notification -> System.out.println("notification=" + notification))//创建一个CacheLoader,重写load方法,以实现"当get时缓存不存在,则load,放到缓存并返回的效果.build(new CacheLoader<String, Optional<List<String>>>() {//重点,自动写缓存数据的方法,必须要实现@Overridepublic Optional<List<String>> load(String cityId) throws Exception {return Optional.ofNullable(rpcCall(cityId));}//异步刷新缓存-下文会讲述@Overridepublic ListenableFuture<Optional<List<String>>> reload(String cityId, Optional<List<String>> oldValue) throws Exception {return super.reload(cityId, oldValue);}});// 测试
public static void main(String[] args) {try {System.out.println("load from cache once : " + loadingCache.get("0101").orElse(Lists.newArrayList()));Thread.sleep(4000);System.out.println("load from cache two : " + loadingCache.get("0101").orElse(Lists.newArrayList()));Thread.sleep(2000);System.out.println("load from cache three : " + loadingCache.get("0101").orElse(Lists.newArrayList()));Thread.sleep(2000);System.out.println("load not exist key from cache : " + loadingCache.get("0103").orElse(Lists.newArrayList()));} catch (ExecutionException | InterruptedException e) {//记录日志}
}

执行结果
在这里插入图片描述

2.2 创建CallableCache缓存

在上面的build方法中是可以不用创建CacheLoader的,不管有没有CacheLoader,都是支持Callable的。Callable在get时可以指定,效果跟CacheLoader一样,区别就是两者定义的时间点不一样,Callable更加灵活,可以理解为Callable是对CacheLoader的扩展。CallableCache的方式最大的特点在于可以在get的时候动态的指定load的数据源

//创建一个callableCache,并可以进行一些简单的缓存配置
private static Cache<String, Optional<List<String>>> callableCache = CacheBuilder.newBuilder()//最大容量为100(基于容量进行回收).maximumSize(100)//配置写入后多久使缓存过期-下文会讲述.expireAfterWrite(3, TimeUnit.SECONDS)//key使用弱引用-WeakReference.weakKeys()//当Entry被移除时的监听器.removalListener(notification -> System.out.println("notification=" + notification))//不指定CacheLoader.build();// 测试
public static void main(String[] args) {try {System.out.println("load from callableCache once : " + callableCache.get("0101", () -> Optional.ofNullable(rpcCall("0101"))).orElse(Lists.newArrayList()));Thread.sleep(4000);System.out.println("load from callableCache two : " + callableCache.get("0101", () -> Optional.ofNullable(rpcCall("0101"))).orElse(Lists.newArrayList()));Thread.sleep(2000);System.out.println("load from callableCache three : " + callableCache.get("0101", () -> Optional.ofNullable(rpcCall("0101"))).orElse(Lists.newArrayList()));Thread.sleep(2000);System.out.println("load not exist key from callableCache : " + callableCache.get("0103", () -> Optional.ofNullable(rpcCall("0103"))).orElse(Lists.newArrayList()));} catch (ExecutionException | InterruptedException e) {//记录日志}
}

执行结果:
在这里插入图片描述

2.3 其他用法

// 声明一个CallableCache,不需要CacheLoader
private static  Cache<String, Optional<List<String>>> localCache = CacheBuilder.newBuilder().maximumSize(100).expireAfterAccess(10, TimeUnit.MINUTES).removalListener(notification -> System.out.println("notification=" + notification)).build();// 测试。使用时自主控制get、put等操作
public static void main(String[] args) {try {String cityId = "0101";Optional<List<String>> ifPresent1 = localCache.getIfPresent(cityId);System.out.println("load from localCache one : " + ifPresent1);// 做判空,不存在时手工获取并put数据到localCache中if (ifPresent1 == null || ifPresent1.isPresent() || CollectionUtils.isEmpty(ifPresent1.get())) {List<String> stringList = rpcCall(cityId);if (CollectionUtils.isNotEmpty(stringList)) {localCache.put(cityId, Optional.ofNullable(stringList));}}Optional<List<String>> ifPresent2 = localCache.getIfPresent(cityId);System.out.println("load from localCache two : " + ifPresent2);// 失效某个key,或者loadingCache.invalidateAll() 方法localCache.invalidate(cityId);Optional<List<String>> ifPresent3 = localCache.getIfPresent(cityId);System.out.println("load from localCache three : " + ifPresent3);} catch (Exception e) {throw new RuntimeException(e);}
}

执行结果
在这里插入图片描述

通过上面三个案例的讲解,相信大家对于guava cache的使用应该没啥问题了,接下来一起学习缓存的失效机制!

3.缓存失效回收策略

前面说到Guava Cache与ConcurrentHashMap很相似,包括其并发策略,数据结构等,但也不完全一样。最基本的区别是ConcurrentHashMap会一直保存所有添加的元素,直到显式地移除,而guava cache可以自动回收元素,在某种情况下Guava Cache 会根据一定的算法自动移除一些条目,以确保缓存不会占用太多内存,避免内存浪费。

3.1 基于容量回收

基于容量的回收是一种常用策略。在构建缓存时使用 CacheBuilder 的 maximumSize 方法来设置缓存的最大条目数。当缓存中的条目数量超过了最大值时,Guava Cache 会根据LRU(最近最少使用)算法来移除一些条目。例如:

Cache<KeyType, ValueType> cache = CacheBuilder.newBuilder()// 缓存最多可以存储1000个条目.maximumSize(1000) .build();

除了 maximumSize,Guava Cache 还提供了 maximumWeight 方法和 weigher 方法,允许你根据每个条目的权重来限制缓存,而不是简单的条目数量。这在缓存的条目大小不一致时特别有用。需要注意的是,淘汰的顺序仍然是根据条目的访问顺序,而不是权重大小。 例如:

Cache<KeyType, ValueType> cache = CacheBuilder.newBuilder()// 缓存最多可以存储的总权重.maximumWeight(10000) .weigher(new Weigher<KeyType, ValueType>() {public int weigh(KeyType key, ValueType value) {// 定义如何计算每个条目的权重return getSizeInBytes(key, value);}}).build();

注意事项📢
1、权重是在缓存创建时计算的,因此要考虑权重计算的复杂度。

3.2 定时回收

Guava Cache提供了两种基于时间的回收策略。

  1. 基于写操作的回收(expireAfterWrite)

使用 expireAfterWrite 方法设置的缓存条目在给定时间内没有被写访问(创建或覆盖),则会被回收。这种策略适用于当信息在一段时间后就不再有效或变得陈旧时。 例如,下面的代码创建了一个每当条目在30分钟内没有被写访问(创建或覆盖)就会过期的缓存:

Cache<KeyType, ValueType> cache = CacheBuilder.newBuilder().expireAfterWrite(30, TimeUnit.MINUTES).build();
  1. 基于访问操作的回收(expireAfterAccess)

使用 expireAfterAccess 方法设置的缓存条目在给定时间内没有被读取或写入,则会被回收。这种策略适用于需要回收那些可能很长时间都不会被再次使用的条目。 例如,下面的代码创建了一个每当条目在15分钟内没有被访问(读取或写入)就会过期的缓存:

Cache<KeyType, ValueType> cache = CacheBuilder.newBuilder().expireAfterAccess(15, TimeUnit.MINUTES).build();

3.3 基于引用回收

Guava Cache 提供了基于引用的回收机制,这种机制允许缓存通过使用弱引用(weak references)或软引用(soft references)来存储键(keys)或值(values),以便在内存紧张时能够自动回收这些缓存条目。

  1. 弱引用键(Weak Keys)

使用 weakKeys() 方法配置的缓存会对键使用弱引用。当键不再有其他强引用时,即使它还在缓存中,也可能被垃圾回收器回收。

Cache<KeyType, ValueType> cache = CacheBuilder.newBuilder().weakKeys().build();

弱引用键的缓存主要用于缓存键是可丢弃的或由外部系统管理生命周期的对象。例如,缓存外部资源的句柄,当句柄不再被应用程序使用时,可以安全地回收。

  1. 软引用值(Soft Values)
    使用 softValues() 方法配置的缓存会对值使用软引用。软引用对象在内存充足时会保持不被回收,但在JVM内存不足时,软引用对象可能被垃圾回收器回收。
Cache<KeyType, ValueType> cache = CacheBuilder.newBuilder().softValues().build();

软引用值的缓存适合用于缓存占用内存较大的对象,例如图片或文档数据。当应用程序内存需求增加时,这些大对象可以被回收以释放内存。

注意事项📢
1、基于引用的回收策略不是由缓存大小或元素的存活时间决定的,而是与JVM的垃圾回收机制紧密相关,而垃圾回收的行为会受到JVM配置和当前内存使用情况的影响,因此,引用回收策略下缓存回收具有不确定性,会导致缓存行为的不可预测性。
2、基于引用的回收策略通常不应与需要精确控制内存占用的场景混用。在使用基于引用的回收策略时,应该仔细考虑应用程序的内存需求和垃圾回收行为,以确保缓存能够按照预期工作。

3.4 显式清除

Guava Cache 提供了几种显式清除缓存条目的方法,允许你手动移除缓存中的某个或某些条目。

  1. 移除单个条目
    使用 invalidate(key) 方法可以移除缓存中的特定键对应的条目。
cache.invalidate(key);
  1. 移除多个条目
    使用 invalidateAll(keys) 方法可以移除缓存中所有在给定集合中的键对应的条目。
cache.invalidateAll(keys);
  1. 移除所有条目
    使用 invalidateAll() 方法可以移除缓存中的所有条目。
cache.invalidateAll();
  1. 使用 Cache.asMap() 视图进行移除
    通过缓存的 asMap() 方法获取的 ConcurrentMap 视图,你可以使用 Map 接口提供的方法来移除条目。
// 移除单个条目
cache.asMap().remove(key);// 批量移除条目
for (KeyType key : keys) {cache.asMap().remove(key);
}// 移除满足特定条件的条目
cache.asMap().entrySet().removeIf(entry -> entry.getValue().equals(someValue));

注意事项📢
asMap 视图提供了缓存的 ConcurrentMap 形式,这种方式在使用时和直接操作缓存的交互有区别,如下:
1、cache.asMap()包含当前所有加载到缓存的项。因此cache.asMap().keySet()包含当前所有已加载键;
2、asMap().get(key)实质上等同于 cache.getIfPresent(key),而且不会引起缓存项的加载。这和 Map 的语义约定一致。
3、所有读写操作都会重置相关缓存项的访问时间,包括 Cache.asMap().get(Object)方法和 Cache.asMap().put(K, V)方法,但不包括 Cache.asMap().containsKey(Object)方法,也不包括在 Cache.asMap()的集合视图上的操作。比如,遍历 Cache.asMap().entrySet()不会重置缓存项的读取时间。

  1. 注册移除监听器
    可以在构建缓存时注册一个移除监听器(RemovalListener),它会在每次条目被移除时调用。
Cache<KeyType, ValueType> cache = CacheBuilder.newBuilder().removalListener(new RemovalListener<KeyType, ValueType>() {@Overridepublic void onRemoval(RemovalNotification<KeyType, ValueType> notification) {// 处理移除事件}}).build();

在实际项目实践中,往往是多种回收策略一起使用,让Guava Cache缓存提供多层次的回收保障。

4、缓存失效回收时机

缓存回收策略讲清楚后,那么这些策略到底是在什么时候触发的呢?我们直接说结论:

Guava Cache基于容量和时间的回收策略,清理操作不是实时的。缓存的维护清理通常发生在写操作期间,如新条目的插入或现有条目的替换,以及在读操作期间的偶然清理。这意味着,缓存可能会暂时超过最大容量限制和时间限制,直到下一次写操作触发清理。

Guava 文档中提到,清理工作通常是在写操作期间完成的,但是在某些情况下,读操作也会导致清理,尤其是当缓存的写操作比较少时。这是为了确保即使在没有写操作的情况下,缓存也能够维护其大小和条目的有效性。如果你需要确定缓存何时被清理,或者你想手动控制清理操作的时机可以通过「显式清除」的方式,条目删除操作会立即执行。

为了更好的理解上述说的结论,我们通过上面LoadingCache缓存的使用 结合idea debug执行分析一下。 源码分析见下一部分。

5、源码分析(简短分析)

以下是guava-20.0版本的源码分析。

  1. Segment中的get方法
@Override
// 1、执行LocalLoadingCache中的get方法
public V get(K key) throws ExecutionException {return localCache.getOrLoad(key);
}// 2、执行get 或 load方法
V getOrLoad(K key) throws ExecutionException {return get(key, defaultLoader);
}// 3、核心get方法  
V get(K key, CacheLoader<? super K, V> loader) throws ExecutionException {int hash = hash(checkNotNull(key));// segmentFor方法根据hash的高位从segments数组中取出相应的segment实例,执行segment实例的get方法return segmentFor(hash).get(key, hash, loader);
}// 4、Segment中的get方法
V get(K key, int hash, CacheLoader<? super K, V> loader) throws ExecutionException {checkNotNull(key);checkNotNull(loader);try {// 当前Segment中存活的条目个数不为0if (count != 0) { // read-volatile// don't call getLiveEntry, which would ignore loading values// getEntry会校验key,所以key为弱引用被回收的场景,取到的e是null。稍后展开介绍该方法LocalCache.ReferenceEntry<K, V> e = getEntry(key, hash);if (e != null) {long now = map.ticker.read();// 此处有个getLiveValue(),这个方法是拿到当前存活有效的缓存值,稍后展开介绍该方法V value = getLiveValue(e, now);if (value != null) {// 记录该缓存被访问了。此时expireAfterAccess相关的时间会被刷新recordRead(e, now);// 记录缓存击中statsCounter.recordHits(1);// 用来判断是直接返回现有value,还是等待刷新return scheduleRefresh(e, key, hash, value, now, loader);}LocalCache.ValueReference<K, V> valueReference = e.getValueReference();// 只有key存在,但是value不存在(被回收)、或缓存超时的情况会到达这里// 如果已经有线程在加载缓存了,后面的线程不会重复加载,而是等待加载的结果if (valueReference.isLoading()) {return waitForLoadingValue(e, key, valueReference);}}}// at this point e is either null or expired;// 如果不存在或者过期,就通过loader方法进行加载(该方法会对当前整个Segment加锁,直到从数据源加载数据,更新缓存);// 走到这里的场景:// 1)segment为空// 2)key或value不存在(没有缓存,或者弱引用、软引用被回收),// 3)缓存超时(expireAfterAccess或expireAfterWrite触发的)return lockedGetOrLoad(key, hash, loader);} catch (ExecutionException ee) {Throwable cause = ee.getCause();if (cause instanceof Error) {throw new ExecutionError((Error) cause);} else if (cause instanceof RuntimeException) {throw new UncheckedExecutionException(cause);}throw ee;} finally {postReadCleanup();}
}

注意事项📢
在cache get数据的时候,如果链表上找不到entry,或者value已经过期,则调用lockedGetOrLoad()方法,这个方法会锁住整个segment,直到从数据源加载数据,更新缓存。如果并发量比较大,又遇到很多key失效的情况就会很容易导致线程block。 项目实践中需要慎重考虑这个问题,可考虑采用定时refresh机制规避该问题(下文会讲述refresh机制)。

  1. 根据hash和key获取键值对:getEntry
    @NullableReferenceEntry<K, V> getEntry(Object key, int hash) {// getFirst用来根据hash获取table中相应位置的链表的头元素for (ReferenceEntry<K, V> e = getFirst(hash); e != null; e = e.getNext()) {// hash不相等的,key肯定不相等。hash判等是int判等,比直接用key判等要快得多if (e.getHash() != hash) {continue;}K entryKey = e.getKey();// entryKey == null的情况,是key为软引用或者弱引用,已经被GC回收了。直接清理掉if (entryKey == null) {// tryDrainReferenceQueues();continue;}if (map.keyEquivalence.equivalent(key, entryKey)) {return e;}}return null;}
  1. getLiveValue方法
V getLiveValue(LocalCache.ReferenceEntry<K, V> entry, long now) {// 软引用或者弱引用的key被清理掉了if (entry.getKey() == null) {// 清理非强引用的队列tryDrainReferenceQueues();return null;}V value = entry.getValueReference().get();// 软引用的value被清理掉了if (value == null) {// 清理非强引用的队列tryDrainReferenceQueues();return null;}// 在这里map.isExpired(entry, now)满足条件执行清除tryExpireEntries(now)if (map.isExpired(entry, now)) {tryExpireEntries(now);return null;}return value;
}

源码分析部分先写到这里。我们掌握了,基于容量、时间的回收策略,不是实时执行的。回收清理通常是在写操作期间顺带进行的,或者可以通过调用 cleanUp() 方法来显式触发。读操作也可能偶尔触发清理,尤其是在写操作较少时。更详细的源码分析推荐网上看到的另一篇博文: Guava Cache:原理详解和源码分析

6、刷新

了解了Guava Cache的使用和回收策略后,我们会发现这种用法还存在以下两个问题:

  1. 缓存击穿。数据大批量过期会导致对后端存储的高并发访问,加载数据过程中会锁住整个segment,很容易导致线程block。
  2. 数据不新鲜。缓存中的数据不是最新的,特别是对于那些定期变化的数据无法做到定期刷新。

Guava Cache 的刷新机制允许缓存项在满足特定条件时自动刷新。这意味着缓存项的值将被重新计算和替换,但这个过程是异步的,即刷新操作不会阻塞对缓存项的读取请求。

刷新机制主要通过 LoadingCache的refresh方法来实现,该方法会根据缓存的 CacheLoader重新加载缓存项的值。通过 CacheBuilder 的 refreshAfterWrite 方法设置自动刷新的触发条件,即在写入缓存项后的指定时间间隔。例如:

LoadingCache<KeyType, ValueType> cache = CacheBuilder.newBuilder()// 在写入后的10分钟后自动刷新.refreshAfterWrite(10, TimeUnit.MINUTES) .build(new CacheLoader<KeyType, ValueType>() {@Overridepublic ValueType load(KeyType key) {// 缓存项不存在时加载数据的方法return loadData(key);}@Overridepublic ListenableFuture<ValueType> reload(KeyType key, ValueType oldValue) throws Exception {// 异步刷新缓存项的方法// 使用ListenableFuture来异步执行刷新操作return listeningExecutorService.submit(() -> loadData(key));}});

在上述代码中,refreshAfterWrite 设置了自动刷新的条件,而 CacheLoader 的 reload 方法定义了如何异步刷新缓存项。当缓存项在指定的时间间隔后被访问时,Guava Cache 会调用 reload 方法来异步加载新值。在新值加载期间,旧值仍然会返回给任何请求它的调用者。

需要注意的是,reload 方法应该返回一个 ListenableFuture 对象,这样刷新操作就可以异步执行,而不会阻塞其他缓存或线程操作。如果 reload 方法没有被重写,Guava Cache 将使用 load 方法进行同步刷新。

7、参考材料

  1. Guava github
  2. 美团技术团队-缓存那些事
  3. Guava Cache:原理详解和源码分析
  4. Java工具库Guava本地缓存Cache的使用、回收、刷新、统计等示例

http://www.mrgr.cn/p/32014830

相关文章

redis中的双写一致性问题

双写一致性问题 1.先删除缓存或者先修改数据库都可能出现脏数据。 2.删除两次缓存&#xff0c;可以在一定程度上降低脏数据的出现。 3.延时是因为数据库一般采用主从分离&#xff0c;读写分离。延迟一会是让主节点把数据同步到从节点。 1.读写锁保证数据的强一致性 因为一般放…

计算机毕业设计Python+Spark知识图谱高考志愿推荐系统 高考数据分析 高考可视化 高考大数据 大数据毕业设计

毕业设计&#xff08;论文&#xff09;任务书 毕业设计&#xff08;论文&#xff09;题目&#xff1a; 基于大数据的高考志愿推荐系统 设计&#xff08;论文&#xff09;的主要内容与要求&#xff1a; 主要内容&#xff1a; 高…

【消息队列】RabbitMQ五种消息模式

RabbitMQ RabbitMQRabbitMQ安装 常见的消息模型基本消息队列SpringAMQPWorkQueue消息预取发布订阅模式Fanout ExchangeDirectExchangeTopicExchange 消息转换器 RabbitMQ RabbitMQ是基于Erlang语言开发的开源消息通信中间件 官网地址&#xff1a;https://www.rabbitmq.com/ R…

腾讯云IM即时通信引入(React Web端组件式)

开发环境要求 React ≥ v18.0 &#xff08;17.x 版本不支持&#xff09; TypeScript node&#xff08;12.13.0 ≤ node 版本 ≤ 17.0.0, 推荐使用 Node.js 官方 LTS 版本 16.17.0&#xff09; npm&#xff08;版本请与 node 版本匹配&#xff09; chat-uikit-react 集成 …

Unity 编辑器工具 - 资源引用查找器

在Unity项目开发过程中&#xff0c;管理和维护资源之间的引用关系是至关重要的。当然我们项目也是需要这个功能 毕竟项目大了之后查找资源引用还是交给 资源引用查找器 比较好。 功能概述 资源引用查找器允许开发者选择一个目标资源&#xff0c;并在整个项目中查找引用了该资…

vue3--element-plus-抽屉文件上传和富文本编辑器

一、封装组件 article/components/ArticleEdit.vue <script setup> import { ref } from vue const visibleDrawer ref(false)const open (row) > {visibleDrawer.value trueconsole.log(row) }defineExpose({open }) </script><template><!-- 抽…

读天才与算法:人脑与AI的数学思维笔记18_心流机

读天才与算法:人脑与AI的数学思维笔记18_心流机1. 心流机 1.1. 在音乐中你会期盼旋律从不稳定解决到稳定,最终实现某种张力的解决 1.2. 将马尔可夫链系统中的自由与约束条件结合起来,从而形成一种更具结构化的组合 1.3. 美籍匈牙利心理学家米哈里契克森米哈赖(Mihaly Csiks…

Jmeter05:配置环境变量

1 Jmeter 环境 1.1 什么是环境变量&#xff1f;path什么用&#xff1f; 系统设置之一&#xff0c;通过设置PATH&#xff0c;可以让程序在DOS命令行直接启动 1.2 path怎么用 如果想让一个程序可以在DOS直接启动&#xff0c;需要将该程序目录配置进PATH 1.3 PATH和我们的关系…

(四)小程序学习笔记——自定义组件

1、组件注册——usingComponents &#xff08;1&#xff09;全局注册&#xff1a;在app.json文件中配置 usingComponents进行注册&#xff0c;注册后可以在任意页面使用。 &#xff08;2&#xff09;局部注册&#xff0c;在页面的json文件中配置suingComponents进行注册&#…

【Linux】awk命令学习

最近用的比较多&#xff0c;学习总结一下。 文档地址&#xff1a;https://www.gnu.org/software/gawk/manual/gawk.html 一、awk介绍二、语句结构1.条件控制语句1&#xff09;if2&#xff09;for3&#xff09;while4&#xff09;break&continue&next&exit 2.比较运…

uniapp 微信开发工具上访问正常,真机调试一直跨域报错

微信小程序真机调试时&#xff0c;出现跨域问题&#xff0c;需要同时在后端设置多种允许跨域的设置&#xff1a; // 指定允许其他域名访问 header(Access-Control-Allow-Origin:*); // 响应类型 header(Access-Control-Allow-Methods:GET,POST,OPTION); // 响应头设置 header(…

Mysql报错红温集锦(一)(ipynb配置、pymysql登录、密码带@、to_sql如何加速、触发器SIGNAL阻止插入数据)

一、jupyter notebook无法使用%sql来添加sql代码 可能原因&#xff1a; 1、没装jupyter和notebook库、没装ipython-sql库 pip install jupyter notebook ipython-sql 另外如果是vscode的话还需要安装一些相关的插件 2、没load_ext %load_ext sql 3、没正确的登录到mysql…

扫雷实现详解【递归展开+首次必展开+标记雷+取消标记雷】

扫雷 一.扫雷设计思路二.扫雷代码逐步实现1.创建游戏菜单2.初始化棋盘3.打印棋盘4.随机布置雷5.统计周围雷的个数6.递归展开棋盘7.标记雷8.删除雷的标记9.保证第一次排雷的安全性棋盘必定展开10.排查雷11.判断输赢 三.扫雷总代码四.截图 一.扫雷设计思路 1.创建游戏菜单。  2.…

深度学习500问——Chapter08:目标检测(7)

文章目录 8.3.8 RFBNet 8.3.9 M2Det 8.3.8 RFBNet RFBNet有哪些创新点 1. 提出RF block&#xff08;RFB&#xff09;模块 RFBNet主要想利用一些技巧使得轻量级模型在速度和精度上达到很好的trade-off的检测器。灵感来自人类视觉的感受野结构Receptive Fields&#xff08;RFs…

分布式websocket IM即时通讯聊天开源项目如何启动

前言 自己之前分享了分布式websocket的视频有同学去fork项目了&#xff0c;自己启动一下更方便理解项目嘛。然后把项目启动需要的东西全部梳理出来。支持群聊单聊,表情包以及发送图片。 支持消息可靠&#xff0c;消息防重&#xff0c;消息有序。同时基础架构有分布式权限&…

axios.get请求 重复键问题??

封装的接口方法&#xff1a; 数据&#xff1a; 多选框多选后 能得到对应的数组 但是请求的载荷却是这样的,导致会请求不到数据 departmentChecks 的格式看起来是一个数组&#xff0c;但是通常 HTTP 请求的查询参数不支持使用相同的键&#xff08;key&#xff09;名多次。如…

分享一个网站实现永久免费HTTPS访问的方法

免费SSL证书作为一种基础的网络安全工具&#xff0c;以其零成本的优势吸引了不少网站管理员的青睐。要实现免费HTTPS访问&#xff0c;您可以按照以下步骤操作&#xff1a; 一、 选择免费SSL证书提供商 选择一个提供免费SSL证书的服务商。如JoySSL&#xff0c;他们是国内为数不…

JVM知识总汇(JVM面试题篇5.1)

个人理解&#xff0c;所学有限&#xff0c;若有不当&#xff0c;还请指出 1.JVM是由哪些部分组成&#xff0c;运行流程是什么&#xff1f; JVM为java虚拟机&#xff0c;是java程序的运行环境&#xff08;其实是java字节码文件的运行环境&#xff09;&#xff0c;能够实现一次编…

电路板/硬件---器件

电阻 电阻作用 电阻在电路中扮演着重要的角色&#xff0c;其作用包括&#xff1a; 限制电流&#xff1a;电阻通过阻碍电子流动的自由而限制电流。这是电阻最基本的功能之一。根据欧姆定律&#xff0c;电流与电阻成正比&#xff0c;电阻越大&#xff0c;通过电阻的电流就越小。…

【副本向】Lua副本逻辑

副本生命周期 OnCopySceneTick() 子线程每次心跳调用 --副本心跳 function x3323_OnCopySceneTick(elapse)if x3323_g_IsPlayerEnter 0 thenreturn; -- 如果没人进入&#xff0c;则函数直接返回endif x3323_g_GameOver 1 thenif x3323_g_EndTick > 0 thenx3323_CountDown…