缓存框架JetCache源码解析-缓存定时刷新

作为一个缓存框架,JetCache支持多级缓存,也就是本地缓存和远程缓存,但是不管是使用着两者中的哪一个或者两者都进行使用,缓存的实时性一直都是我们需要考虑的问题,通常我们为了尽可能地保证缓存的实时性,都会去采用一些策略,比如更新数据库时同时删除缓存,又或者是使用缓存双删等策略,而在JetCache中,它还支持定时地去刷新缓存,又进一步地能够保证缓存的实时性

功能使用

编程式

 QuickConfig idQc = QuickConfig.newBuilder(":user:cache:id:").cacheType(CacheType.BOTH).expire(Duration.ofHours(2)).refreshPolicy(RefreshPolicy.newPolicy(1,TimeUnit.HOURS)).syncLocal(true).build();
idUserCache = cacheManager.getOrCreateCache(idQc);

在编程式的方式中,通过refreshPolicy属性配置,指定缓存刷新的时间间隔,比如上面定义的是每隔1小时刷新一次缓存

声明式

@Cached(name = ":user:cache:id:", cacheType = CacheType.BOTH, key = "#userId", cacheNullValue = true)
@CacheRefresh(refresh = 60, timeUnit = TimeUnit.MINUTES)
public User findById(Long userId) {return userMapper.findById(userId);
}

声明式的方式JetCache中提供的是CacheRefresh注解,在该注解中的refresh属性中可以配置缓存的刷新时间

源码解析

RefreshCache的继承体系

  • Cache:Cache实例的顶级接口,定义了Cache实例常用的一些api,例如get,put等等
  • ProxyCache:继承于Cache接口,通过名字可以知道ProxyCache就是用于代理某一个Cache实例的,而它也提供了一个getTargetCache方法,该方法可以返回这个被代理的Cache实例
  • SimpleProxyCache:它是ProxyCache接口的一个实现,里面有一个Cache实例的成员变量,这个Cache实例其实就是被代理的Cache实例,而SimpleProxyCache中的方法也很简单,都是实现Cache接口以及ProxyCache接口的方法,在实现的方法中调用成员变量Cache实例的同名方法而已,其实就是一个代理模式
  • LoadingCache:通过名字可以知道这个Cache具有加载数据的功能,所谓的加载数据就是从某一处数据源(比如数据库,es等)进行数据加载然后再放到整个被代理的Cache实例中
  • RefreshCache:缓存定时刷新的核心实现,在里面实现了通过定时器去进行缓存的刷新

缓存定时刷新任务RefreshTask

/*** 缓存刷新任务,以key为维度,每一个key对应一个RefreshTask* 这个任务主要做的事:以key为维度,每隔一段时间(配置的refreshMillis)执行一次,通过loader加载出数据更新到缓存中* 如果使用的是多级缓存,那么就会先loader数据到远程缓存中,然后再更新到本地缓存中*/
class RefreshTask implements Runnable {private Object taskId;/*** 目标刷新的key*/private K key;/*** loader*/private CacheLoader<K, V> loader;/*** 上一次访问时间*/private long lastAccessTime;private ScheduledFuture future;RefreshTask(Object taskId, K key, CacheLoader<K, V> loader) {this.taskId = taskId;this.key = key;this.loader = loader;}private void cancel() {logger.debug("cancel refresh: {}", key);future.cancel(false);taskMap.remove(taskId);}/*** 通过loader加载出数据,如果有必要则还需把加载出的数据放到Cache中*/private void load() throws Throwable {CacheLoader<K, V> l = loader == null ? config.getLoader() : loader;if (l != null) {// 获取到一个ProxyLoader,这个ProxyLoader会去对loader进行代理,在原来loader加载数据的功能上增加了其他功能l = CacheUtil.createProxyLoader(cache, l, eventConsumer);// 加载数据V v = l.load(key);// 条件成立:说明加载出来的数据需要更新到Cache中if (needUpdate(v, l)) {// 假如这个被代理的cache是一个多级缓存,那么此时本地缓存和远程缓存都会被更新到了cache.PUT(key, v);}}}/*** 该方法的作用是:如果目标key已经到达了缓存刷新时间则会使用loader去进行加载这个key的数据,加载出来的数据会放入到缓存中,* 在使用loader加载数据的过程中,会以key为维度去加一把分布式锁,目的就是为了防止多个机器节点同时进行loader,因为只需要一台机器实例去loader数据到远程缓存即可* 如果抢到分布式锁成功,那么就会把加载到的数据放到Cache缓存实例中,如果抢不到分布式锁,则说明已经有其他机器loader数据到远程缓存中了,这时候就不用loader数据了,* 但是还需要注意的是如果这时候使用的是多级缓存,则还需要更新本地缓存,所以就会从远程缓存中获取到数据然后更新本地缓存** @param concreteCache 远程缓存Cache实例* @param currentTime   当前时间,用于判断key是否到达刷新时间了*/private void externalLoad(final Cache concreteCache, final long currentTime) throws Throwable {byte[] newKey = ((AbstractExternalCache) concreteCache).buildKey(key);byte[] lockKey = combine(newKey, LOCK_KEY_SUFFIX);// 获取到刷新缓存时加锁的过期时间long loadTimeOut = RefreshCache.this.config.getRefreshPolicy().getRefreshLockTimeoutMillis();// 刷新缓存的时间间隔long refreshMillis = config.getRefreshPolicy().getRefreshMillis();byte[] timestampKey = combine(newKey, TIMESTAMP_KEY_SUFFIX);// 从缓存中获取到这个key上一次的刷新时间CacheGetResult refreshTimeResult = concreteCache.GET(timestampKey);// 是否应该对这个key进行刷新boolean shouldLoad = false;// 如果这个key存在,并且这个key已经到达了下一次的刷新时间,那么就需要对它进行刷新if (refreshTimeResult.isSuccess()) {shouldLoad = currentTime >= Long.parseLong(refreshTimeResult.getValue().toString()) + refreshMillis;}// 如果这个key并不存在,那么也应该对它进行刷新else if (refreshTimeResult.getResultCode() == CacheResultCode.NOT_EXISTS) {shouldLoad = true;}// 条件成立:不需要对这个key进行刷新if (!shouldLoad) {// 如果是多级缓存,那么此时从最后一级缓存中获取数据刷新到前面层级的缓存中if (multiLevelCache) {refreshUpperCaches(key);}return;}// 抢到分布式锁之后会去执行这个runnable// runnable做的事情:通过loader加载出数据,如果有必要则还需把加载出的数据放到Cache实例中Runnable r = () -> {try {// 通过loader加载出数据,如果有必要则还需把加载出的数据放到缓存中load();// 更新这个key最近的一次刷新时间concreteCache.put(timestampKey, String.valueOf(System.currentTimeMillis()));} catch (Throwable e) {throw new CacheException("refresh error", e);}};// 尝试去获取锁,如果获取到锁就执行rboolean lockSuccess = concreteCache.tryLockAndRun(lockKey, loadTimeOut, TimeUnit.MILLISECONDS, r);// 获取锁失败,说明有其他线程获取到锁了,也就说明其他线程正在加载数据到远程缓存中if (!lockSuccess && multiLevelCache) {// 如果被代理的Cache实例是多级缓存的话还需要从远程缓存中获取到数据,然后把数据刷新到上层的本地缓存// 延迟执行,因为此时其他线程还在加载数据到远程缓存中JetCacheExecutor.heavyIOExecutor().schedule(() -> refreshUpperCaches(key), (long) (0.2 * refreshMillis), TimeUnit.MILLISECONDS);}}/*** 从最后一级缓存中获取数据刷新到前面层级的缓存中** @param key key*/private void refreshUpperCaches(K key) {MultiLevelCache<K, V> targetCache = (MultiLevelCache<K, V>) getTargetCache();Cache[] caches = targetCache.caches();int len = caches.length;// 从最后一级缓存中获取到这个key的缓存数据CacheGetResult cacheGetResult = caches[len - 1].GET(key);if (!cacheGetResult.isSuccess()) {return;}// 刷新前面层级的缓存for (int i = 0; i < len - 1; i++) {caches[i].PUT(key, cacheGetResult.getValue());}}@Overridepublic void run() {try {if (config.getRefreshPolicy() == null || (loader == null && !hasLoader())) {cancel();return;}long now = System.currentTimeMillis();// 获取到停止缓存刷新的时间间隔long stopRefreshAfterLastAccessMillis = config.getRefreshPolicy().getStopRefreshAfterLastAccessMillis();if (stopRefreshAfterLastAccessMillis > 0) {// 条件成立:上一次访问这个key的时间 + 停止缓存刷新的时间间隔 < 当前时间// 也就是说这个key距离上一次访问已经有一段时间了,这时候可能意味着这个key并不是一个频繁被访问的key,所以此时可以把它的缓存刷新定时任务取消掉if (lastAccessTime + stopRefreshAfterLastAccessMillis < now) {logger.debug("cancel refresh: {}", key);// 取消这个key的缓存刷新定时任务cancel();return;}}logger.debug("refresh key: {}", key);// 获取到被代理的Cache实例,如果被代理的Cache实例是多级缓存的Cache实例,那么就返回最后一级缓存的Cache实例Cache concreteCache = concreteCache();// 如果这个Cache实例是远程缓存实例,那么就执行externalLoadif (concreteCache instanceof AbstractExternalCache) {// 在externalLoad方法中会使用loader加载数据到远程缓存中externalLoad(concreteCache, now);}// 如果这个Cache实例不是远程缓存实例,那么就直接load数据到本地缓存中else {load();}} catch (Throwable e) {logger.error("refresh error: key=" + key, e);}}
}

RefreshTask实现了Runnable接口,也就是说它是运行在子线程中的,而每一个RefreshTask任务都只会服务于一个key,也就是说当我们访问了某一个key的数据,那么这个key就会创建出对应的RefreshTask去对它进行定时的刷新

(1)run
public void run() {try {if (config.getRefreshPolicy() == null || (loader == null && !hasLoader())) {cancel();return;}long now = System.currentTimeMillis();// 获取到停止缓存刷新的时间间隔long stopRefreshAfterLastAccessMillis = config.getRefreshPolicy().getStopRefreshAfterLastAccessMillis();if (stopRefreshAfterLastAccessMillis > 0) {// 条件成立:上一次访问这个key的时间 + 停止缓存刷新的时间间隔 < 当前时间// 也就是说这个key距离上一次访问已经有一段时间了,这时候可能意味着这个key并不是一个频繁被访问的key,所以此时可以把它的缓存刷新定时任务取消掉if (lastAccessTime + stopRefreshAfterLastAccessMillis < now) {logger.debug("cancel refresh: {}", key);// 取消这个key的缓存刷新定时任务cancel();return;}}logger.debug("refresh key: {}", key);// 获取到被代理的Cache实例,如果被代理的Cache实例是多级缓存的Cache实例,那么就返回最后一级缓存的Cache实例Cache concreteCache = concreteCache();// 如果这个Cache实例是远程缓存实例,那么就执行externalLoadif (concreteCache instanceof AbstractExternalCache) {// 在externalLoad方法中会使用loader加载数据到远程缓存中externalLoad(concreteCache, now);}// 如果这个Cache实例不是远程缓存实例,那么就直接load数据到本地缓存中else {load();}} catch (Throwable e) {logger.error("refresh error: key=" + key, e);}
}

首先会去获取stopRefreshAfterLastAccessMillis这个配置值,这个配置值表示停止缓存刷新的时间间隔,也就是说如果一个key的上一次访问时间距离当前时间已经超过了这个时间值了,那么就可以停止掉这个key的缓存刷新定时任务了,接着就会去通过loader加载器去加载数据了,但是加载的时候需要去判断当前被代理的Cache是否有使用远程缓存,如果没有使用远程缓存就会执行load方法

/*** 通过loader加载出数据,如果有必要则还需把加载出的数据放到Cache中*/
private void load() throws Throwable {CacheLoader<K, V> l = loader == null ? config.getLoader() : loader;if (l != null) {// 获取到一个ProxyLoader,这个ProxyLoader会去对loader进行代理,在原来loader加载数据的功能上增加了其他功能l = CacheUtil.createProxyLoader(cache, l, eventConsumer);// 加载数据V v = l.load(key);// 条件成立:说明加载出来的数据需要更新到Cache中if (needUpdate(v, l)) {// 假如这个被代理的cache是一个多级缓存,那么此时本地缓存和远程缓存都会被更新到了cache.PUT(key, v);}}
}
protected boolean needUpdate(V loadedValue, CacheLoader<K, V> loader) {if (loadedValue == null && !config.isCacheNullValue()) {return false;}// 是否禁止缓存的更新,默认不禁止if (loader.vetoCacheUpdate()) {return false;}return true;
}

load方法很简单,其实就是调用loader的load方法去加载数据,然后再把加载数据的数据交给needUpdate方法,在needUpdate方法中会判断是否设置缓存null值的配置,如果加载出来的数据为null,并且设置了允许缓存null值,那么就会进一步再判断loader的vetoCacheUpdate方法,该方法表示加载出来的时候是否允许更新到缓存中,默认为false,表示允许更新到缓存中,最后再把加载的数据放到被代理的Cache实例中

(2)externalLoad
/*** 该方法的作用是:如果目标key已经到达了缓存刷新时间则会使用loader去进行加载这个key的数据,加载出来的数据会放入到缓存中,* 在使用loader加载数据的过程中,会以key为维度去加一把分布式锁,目的就是为了防止多个机器节点同时进行loader,因为只需要一台机器实例去loader数据到远程缓存即可* 如果抢到分布式锁成功,那么就会把加载到的数据放到Cache缓存实例中,如果抢不到分布式锁,则说明已经有其他机器loader数据到远程缓存中了,这时候就不用loader数据了,* 但是还需要注意的是如果这时候使用的是多级缓存,则还需要更新本地缓存,所以就会从远程缓存中获取到数据然后更新本地缓存** @param concreteCache 远程缓存Cache实例* @param currentTime   当前时间,用于判断key是否到达刷新时间了*/
private void externalLoad(final Cache concreteCache, final long currentTime) throws Throwable {byte[] newKey = ((AbstractExternalCache) concreteCache).buildKey(key);byte[] lockKey = combine(newKey, LOCK_KEY_SUFFIX);// 获取到刷新缓存时加锁的过期时间long loadTimeOut = RefreshCache.this.config.getRefreshPolicy().getRefreshLockTimeoutMillis();// 刷新缓存的时间间隔long refreshMillis = config.getRefreshPolicy().getRefreshMillis();byte[] timestampKey = combine(newKey, TIMESTAMP_KEY_SUFFIX);// 从缓存中获取到这个key上一次的刷新时间CacheGetResult refreshTimeResult = concreteCache.GET(timestampKey);// 是否应该对这个key进行刷新boolean shouldLoad = false;// 如果这个key存在,并且这个key已经到达了下一次的刷新时间,那么就需要对它进行刷新if (refreshTimeResult.isSuccess()) {shouldLoad = currentTime >= Long.parseLong(refreshTimeResult.getValue().toString()) + refreshMillis;}// 如果这个key并不存在,那么也应该对它进行刷新else if (refreshTimeResult.getResultCode() == CacheResultCode.NOT_EXISTS) {shouldLoad = true;}// 条件成立:不需要对这个key进行刷新if (!shouldLoad) {// 如果是多级缓存,那么此时从最后一级缓存中获取数据刷新到前面层级的缓存中if (multiLevelCache) {refreshUpperCaches(key);}return;}// 抢到分布式锁之后会去执行这个runnable// runnable做的事情:通过loader加载出数据,如果有必要则还需把加载出的数据放到Cache实例中Runnable r = () -> {try {// 通过loader加载出数据,如果有必要则还需把加载出的数据放到缓存中load();// 更新这个key最近的一次刷新时间concreteCache.put(timestampKey, String.valueOf(System.currentTimeMillis()));} catch (Throwable e) {throw new CacheException("refresh error", e);}};// 尝试去获取锁,如果获取到锁就执行rboolean lockSuccess = concreteCache.tryLockAndRun(lockKey, loadTimeOut, TimeUnit.MILLISECONDS, r);// 获取锁失败,说明有其他线程获取到锁了,也就说明其他线程正在加载数据到远程缓存中if (!lockSuccess && multiLevelCache) {// 如果被代理的Cache实例是多级缓存的话还需要从远程缓存中获取到数据,然后把数据刷新到上层的本地缓存// 延迟执行,因为此时其他线程还在加载数据到远程缓存中JetCacheExecutor.heavyIOExecutor().schedule(() -> refreshUpperCaches(key), (long) (0.2 * refreshMillis), TimeUnit.MILLISECONDS);}
}

当使用了远程缓存时,就会执行externalLoad方法去加载数据,那么为什么使用了远程缓存之后就需要单独使用externalLoad方法去加载而不是load方法呢?原因就是我们部署的服务如果是有多个节点的话,对于每一个节点来说,相同的key它们都会去起一个RefreshTask子线程去从数据库中加载数据然后put到远程缓存中,但是这个操作需要每一个节点都执行吗?显然不需要,只需要其中一个节点把数据库的数据加载到远程缓存中,然后其他节点在这个过程中阻塞等待缓存结果即可

首先会判断这个key是否到达了刷新时间了,如果还没到达刷新时间,那么就把远程缓存的数据刷新到上层缓存(通常是本地缓存)即可,反之如果key到达了刷新时间了,那么就会去尝试获取锁(因为上面也说到了,只需要一个节点执行就行了),如果获取锁成功了,那么就执行load方法从数据库中加载数据到被代理的Cache实例中(如果这个被代理的Cache实例是一个多级缓存,那么它的本地缓存和远程缓存都会被刷新了),如果没有抢到锁,说明已经有其他节点抢到锁去加载数据了,如果这时候是多级缓存,那么此时就会延迟五分之一的刷新时间再去从远程缓存刷新数据到本地缓存中,那么为什么要延迟执行呢?这是因为如果抢不到锁就立刻去从远程缓存获取数据的话,这时候加载数据的节点可能还没有加载完成,也就是说远程缓存这时候还是旧的数据,所以JetCache这里就适当地做了一下延迟,但是这样还会导致另一个问题,那就是这个延迟不就会导致节点间本地缓存不一致了吗?单单从这个缓存定时刷新机制来看是会导致这个问题的,但是JetCache为了保证多节点间的本地缓存尽可能一致性,还有一个缓存更新通知机制作为兜底,有了这个缓存更新通知机制,上述的问题也就能够解决了

RefreshTask任务的添加

上面讲了整个RefreshTask任务的执行流程,那么它是在那里被添加的呢?

    private ConcurrentHashMap<Object, RefreshTask> taskMap = new ConcurrentHashMap<>();

在RefreshCache中有一个map,key可以理解为就是key值,value存放的这个key对应的RefreshTask任务

@Override
public V get(K key) throws CacheInvokeException {// 如果配置缓存刷新策略,并且也有设置了对应的loaderif (config.getRefreshPolicy() != null && hasLoader()) {// 给这个key添加一个缓存刷新的定时任务addOrUpdateRefreshTask(key, null);}return super.get(key);
}@Override
public Map<K, V> getAll(Set<? extends K> keys) throws CacheInvokeException {// 如果配置缓存刷新策略,并且也有设置了对应的loaderif (config.getRefreshPolicy() != null && hasLoader()) {// 给每一个key都添加一个缓存刷新的定时任务for (K key : keys) {addOrUpdateRefreshTask(key, null);}}return super.getAll(keys);
}

而在RefreshCache中,会去重写get和getAll这个两个方法,当我们每次去访问一个key对应的缓存数据的时候,如果这个key在taskMap中没有对应的RefreshTask,那么就会调用addOrUpdateRefreshTask方法创建一个

/*** 给指定的key添加一个缓存刷新定时任务RefreshTask,如果这个key已经存在对应的RefreshTask,那么就更新一下这个RefreshTask的访问时间* @param key   key* @param loader    loader*/
protected void addOrUpdateRefreshTask(K key, CacheLoader<K, V> loader) {// 获取配置的缓存刷新策略RefreshPolicy refreshPolicy = config.getRefreshPolicy();// 如果没有配置缓存刷新策略,那么直接returnif (refreshPolicy == null) {return;}// 获取缓存刷新的时间间隔long refreshMillis = refreshPolicy.getRefreshMillis();if (refreshMillis > 0) {// 根据key获取到对应的taskIdObject taskId = getTaskId(key);// 根据taskId从taskMap中获取一个缓存刷新任务RefreshTask,如果taskMap中没有则创建一个RefreshTask refreshTask = taskMap.computeIfAbsent(taskId, tid -> {logger.debug("add refresh task. interval={},  key={}", refreshMillis, key);// 创建一个RefreshTaskRefreshTask task = new RefreshTask(taskId, key, loader);task.lastAccessTime = System.currentTimeMillis();ScheduledFuture<?> future = JetCacheExecutor.heavyIOExecutor().scheduleWithFixedDelay(task, refreshMillis, refreshMillis, TimeUnit.MILLISECONDS);task.future = future;return task;});// 更新这个key的访问时间为当前的最新时间refreshTask.lastAccessTime = System.currentTimeMillis();}
}

如果这个key在taskMap中已经存在对应的RefreshTask了,那么就取出这个RefreshTask,更新一下它的lastAccessTime即可,在执行这个RefreshTask任务的时候,如果配置了stopRefreshAfterLastAccessMillis的话,就会通过去比较lastAccessTime来决定是否还需要定时更新这个key,如果不需要,则会把这个RefreshTask任务取消掉,然后再从taskMap移除

总结

当我们去访问每一个key的时候,JetCache都会对这个key生成一个RefreshTask任务,然后会把这个RefreshTask任务交给定时任务调度器。在执行RefreshTask任务的过程中,主要就是分为两种情况,一种是当前使用了远程缓存,一种是没有使用远程缓存,如果使用了远程缓存,那么此时会先去获取锁,只有获取锁成功了,才会去从数据库中加载数据然后刷新到远程缓存中,如果没有抢到锁,但是又使用了多级缓存(本地缓存+远程缓存),那么这时候就会延迟一段时间才会去把远程缓存的时候刷新到本地缓存,延迟的原因是因为抢到锁的节点这时候可能还没有完成数据的加载到远程缓存中,而正是因为这个延迟,就会有可能导致多节点间本地缓存数据不一致的情况,针对这种情况,JetCache则是通过缓存更新通知机制去进行兜底

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

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

相关文章

酒吧收银系统解决方案——未来之窗行业应用跨平台架构

一、酒吧管理数字化 1. 提高效率&#xff1a;能够快速处理订单&#xff0c;减少顾客等待时间&#xff0c;提高服务效率&#xff0c;从而提升顾客满意度。 2. 精确计费&#xff1a;准确计算酒水、小吃等各类消费项目的费用&#xff0c;避免人工计算错误导致的经济损失。 3. 库存…

vue后台管理系统从0到1(5)

文章目录 vue后台管理系统从0到1&#xff08;5&#xff09;完善侧边栏修改bug渲染header导航栏 vue后台管理系统从0到1&#xff08;5&#xff09; 接上一期&#xff0c;我们需要完善我们的侧边狼 完善侧边栏 我们在 element 组件中可以看见&#xff0c;这一个侧边栏是符合我们…

windows下Qt的安装方法

Qt Creator是个人非常喜欢的一款开发工具&#xff0c;喜欢用其来开发C和CPC平台项目&#xff0c;当然也可以用其来开发Android和Auto平台项目&#xff0c;但其现在采用离线安装&#xff0c;限于网络问题&#xff0c;安装速度非常慢。 现在介绍一种可以完成快速的安装方法。 下…

群晖通过 Docker 安装 MySQL

1. 打开 Docker 应用&#xff0c;并在注册表搜索 MySQL 2. 下载 MySQL 镜像&#xff0c;并选择版本 3. 在 Docker 文件夹中创建 MySQL&#xff0c;并创建子文件夹 4. 设置权限 5. 选择 MySQL 映像运行&#xff0c;创建容器 6. 配置 MySQL 容器 6.1 使用高权限执行容器 6.2 启…

圆周率的估算

圆周率的估算有多种方案&#xff1a; 方案一&#xff1a;无穷级数4/1 - 4/3 4/5 - 4/7 ……的和是圆周率π&#xff0c;这一无穷级数前n项的和即可估算圆周率值。 方案二&#xff1a;利用求单位正方形与内接圆面积的比例关系来求的π的近似值。单位圆的1/4面积是一个扇形&am…

Java调用大模型 - Spring AI 初体验

Spring AI&#xff1a;为Java开发者提供高效的大模型应用框架 当前Java调用大模型时面临缺乏高效AI应用框架的问题。Spring作为资深的Java应用框架提供商&#xff0c;通过推出Spring AI来解决这一挑战。它借鉴了LangChain的核心理念&#xff0c;并结合了Java面向对象编程的优势…

Linux隐藏权限介绍

隐藏权限概览 在Linux系统中&#xff0c;有时即便是以root用户身份&#xff0c;你也可能遇到无法修改特定文件的情况。这种限制往往源自chattr命令的应用&#xff0c;该命令用于为文件或目录设置“隐藏权限”&#xff0c;即底层属性&#xff0c;以增强系统安全性。值得注意的是…

100个人物介绍字幕动画PR视频模板MOGRT

Premiere Pro 模板&#xff0c;5类100个人物介绍(用户)界面元素PR剪辑视频素材包。 不需要插件。 通用表达式。 模块化结构。 组织良好。 快速简单的定制。 https://prmuban.com/41688.html

华为原生鸿蒙操作系统正式发布,为开发者开启的全新机遇与挑战

华为原生鸿蒙操作系统正式发布&#xff1a;开启全场景智能生活新篇章 概述 2024年10月22日&#xff0c;华为在“原生鸿蒙之夜暨华为全场景新品发布会”上正式发布了我国首个国产移动操作系统——华为原生鸿蒙操作系统&#xff08;HarmonyOS NEXT&#xff09;。这标志着华为在…

AI大模型平台详解与AI创作示范

AI大模型平台详解与AI创作示范 在全球人工智能&#xff08;AI&#xff09;领域&#xff0c;中国的AI大模型平台取得了快速发展&#xff0c;涌现了多个具有代表性的平台&#xff0c;诸如百度的飞桨&#xff08;PaddlePaddle&#xff09;、阿里的达摩院M6、华为的MindSpore、腾讯…

JMeter详细介绍和相关概念

JMeter是一款开源的、强大的、用于进行性能测试和功能测试的Java应用程序。 本篇承接上一篇 JMeter快速入门示例 &#xff0c; 对该篇中出现的相关概念进行详细介绍。 JMeter测试计划 测试计划名称和注释&#xff1a;整个测试脚本保存的名称&#xff0c;以及对该测试计划的注…

【日志】Unity3D模型导入基本问题以及浅谈游戏框架

2024.10.22 真正的谦逊从来不是人与人面对时的谦卑&#xff0c;而是当你回头看那个曾经的自己时&#xff0c;依旧保持肯定与欣赏。 【力扣刷题】 暂无 【数据结构】 暂无 【Unity】 导入外部模型资源报错问题 在导入外部资源包的时候一般都会报错&#xff0c;不是这个资源模…

NVR小程序接入平台/设备EasyNVR多品牌NVR管理工具/设备的多维拓展与灵活应用

在数字化安防时代&#xff0c;NVR批量管理软件/平台EasyNVR作为一种先进的视频监控系统设备&#xff0c;正逐步成为各个领域监控解决方案的首选。NVR批量管理软件/平台EasyNVR作为一款基于端-边-云一体化架构的国标视频融合云平台&#xff0c;凭借其部署简单轻量、功能多样、兼…

优化多表联表查询的常见方法归纳

目录 一、使用mybatis的嵌套查询 二、添加表冗余字段&#xff0c;减少联表查询需求 三、分表预处理&#xff0c;前端再匹配 一、使用mybatis的嵌套查询 【场景说明】 前端需要展示一张列表&#xff0c;其中的字段来源于多张表&#xff0c;如何进行查询优化&#xff1f; 【…

鸿蒙网络编程系列32-基于拦截器的性能监控示例

1. 拦截器简介 在Web开发中拦截器是一种非常有用的模式&#xff0c;它允许开发者在请求发送到服务器之前或响应返回给客户端之前执行一些预处理或后处理操作。这种机制特别适用于需要对所有网络请求或响应进行统一处理的情况&#xff0c;比如添加全局错误处理、请求头的修改、…

PostgreSQL中触发器递归的处理 | 翻译

许多初学者在某个时候都会陷入触发器递归的陷阱。通常&#xff0c;解决方案是完全避免递归。但对于某些用例&#xff0c;您可能必须处理触发器递归。本文将告诉您有关该主题需要了解的内容。如果您曾经被错误消息“超出堆栈深度限制”所困扰&#xff0c;那么这里就是解决方案。…

电脑视频剪辑大比拼,谁更胜一筹?

随着短视频的火爆&#xff0c;越来越多的人开始尝试自己动手制作视频&#xff0c;无论是记录生活点滴还是创作个性短片&#xff0c;一款好用的视频剪辑软件是必不可少的。今天&#xff0c;我们就从短视频运营的角度&#xff0c;来聊聊几款热门的电脑视频剪辑软件&#xff0c;看…

FineReport 数据筛选过滤

从大量的数据当中&#xff0c;获取到符合条件的数据&#xff0c;经常会使用到数据筛选过滤功能&#xff0c;在FineReort产品中实现筛选过滤的方法有三种 1&#xff09;直接通过 SQL 语句取出满足条件的的数据&#xff0c;如修改数据集 SQL 语句为&#xff1a;SELECT * FROM 订单…

YOLOv8改进,YOLOv8采用WTConv卷积(感受野的小波卷积),二次创新C2f结构,ECCV 2024

摘要 WTConv(基于小波变换的卷积层),用于在卷积神经网络(CNN)中实现大感受野。作者通过利用小波变换,设计了一个卷积层,可以在保持少量可训练参数的情况下大幅扩大感受野。WTConv 被设计为可以无缝替换现有 CNN 架构中的深度卷积层,适用于图像分类、语义分割、物体检测…

Vue-插槽slot

当我们封装一个组件时&#xff0c;不希望里面的内容写死&#xff0c;希望使用的时候能够自定义里面的内容&#xff0c;这时我们就需要使用到插槽 插槽是什么呢 插槽是子组件提供给父组件的一个占位符&#xff0c;用slot标签表示&#xff0c;父组件可以在这个标签填写任何模板代…