Redis中的LRU算法分析

LRU算法

概述

Redis作为缓存使用时,一些场景下要考虑内容的空间消耗问题。Redis会删除过期键以释放空间,过期键的删除策略
有两种:

  • 1.惰性删除:每次从键空间中获取键时,都检查取得的键是否过期,如果过期的话,就删除该键;如果没有过期,就返回该键。
  • 2.定期删除:每隔一段事件,程序就对数据库进行一次检查,删除里面的过期键。
    Redis也可以开启LRU功能来自动淘汰一些键值对。

例子

~~~~~A~~~~~A~~~~~A~~~~A~~~~~A~~~~~A~~|
~~B~~B~~B~~B~~B~~B~~B~~B~~B~~B~~B~~B~|
~~~~~~~~~~C~~~~~~~~~C~~~~~~~~~C~~~~~~|
~~~~~D~~~~~~~~~~D~~~~~~~~~D~~~~~~~~~D|

举个例子,图中的A每隔5s访问一次,B每2s访问一次,C与D每10s访问一次,| 代表计算空闲时间的截止点。可以看到,LRU对ABC工作的很好,完美预测了将来被访问到的概率B>A>C,但对于D却预测了最少的空闲时间,但总体来说,LRU算法已经是一个性能足够好的算法了

LRU配置参数。

Redis配置中和LRU有关的有三个:

  • 1.maxmemory:配置Redis存储数据时指定限制的内存大小,比如100m。当缓存消耗的内存超过这个数值时,将触发数据淘汰。该数据配置为0时,表示缓存的数据量没有限制,即LRU功能不生效。64位的系统默认值为0,32位的系统默认内存限制为3GB
  • 2.maxmemory_policy:触发数据淘汰后的淘汰策略
  • 3.maxmemory_samples:随机采样的精度,也就是随机取出key的数目。该数值配置越大,越接近真实的LRU算法,但是数值越大,相应消耗也变高,对性能有一定影响,样本值默认为5.

淘汰策略

淘汰策略即maxmemory_policy的赋值有以下几种:

  • 1.noeviction:如果缓存数据超过了maxmemory限定值,并且客户端正在执行的命令(大部分的写入指令,但DEL和几个指令例外)会导致内存分配,则向客户端返回错误相应
  • 2.allkeys-lru:对所有的键都采取LRU淘汰
  • 3.volatile-lru:仅对设置了过期时间的键采取LRU淘汰
  • 4.allkeys-random:随机回收所有的键
  • 5.volatile-random:随机回收设置过期时间的键
  • 6.volatile-ttl:仅淘汰设置了过期时间的键—淘汰生存时间TTL(Time To Live)更小的键
  • volatile-lru,volatile-random和volatile-ttl这三个淘汰策略使用的不是全量数据,有可能无法淘汰出足够的内存空间,在没有过期键或者没有设置超时属性的键的情况下,这三种策略和noeviction差不多。
    一般的经验规则:
  • 1.使用allkeys-lru策略:当预期请求符合一个幂次分布(二八法则等),比如一部分的子集元素比其他元素被访问的更多时,可以选择
    这个策略
  • 2.使用allkeys-random:循环连续的访问所有的键时,或者与其请求分布平均(所有元素被访问的概率都差不多)
  • 3.使用volatile-ttl:要采取这个策略,缓存对象的TTL值最好有差异

volatile-lru和volatile-random策略,当你想要使用单一的Redis实例来同时实现缓存淘汰和持久化一些经常使用的键集合时很有用。未设置过期时间的键进行持久化保存,设置了过期时间的键参与缓存淘汰。不过一般运行两个实例是解决这个问题的更好办法

为键设置过期时间是需要消耗内存的,,所以使用allkeys-lru这种策略更加节省空间,因为这种策略可以不为键设置过期时间。

近似LRU算法

我们知道,LRU算法需要一个双向链表来记录数据的最近被访问顺序,但是处于节省内存的考虑,Redis的LRU算法并非完整的实现。
Redis并不会选择最久未被访问的键进行回收,相反它会尝试运行一个近似LRU的算法,通过对少量键进行取样,然后回收其中最久未被
访问的键,通过调整每次回收的采样杨数量maxmemory-samples,可以实现调整算法的精度。根据Redis作者的说法,每个RedisObject
可以挤出24bits,但24bits是不够存储两个指针的,而存储一个低位时间戳是足够的,RedisObject以秒为单位存储了对象新建或者更新时的
unix time,也就是LRU clock,24bits 能表示的最大数值为2^24-1,即16777215秒,16777215 / 86400(一天的秒数) = 194.56 ,因此得出
最长的时间跨度是约194天而缓存的数据更新非常频繁,已经足够了。
Redis的键空间是放在一个哈希表中的,要从所有的键中选出一个最久未被访问的键,需要另外一个数据结构存储这些源信息,这显然不划算,
最初,Redis只是随机的选3个key,然后从中淘汰,后来算法改进到了N个key,默认是5个。Redis3.0之后又改善了算法的性能,会提供一个
待淘汰候选key的pool,里面默认有16个key,按照空闲事件排好序。更新时从Redis键空间随机选择N个key,分别计算它们的空闲事件idle,
key只会在pool不满或者空闲时间大于pool里最小的时候,才会进入pool,然后从pool中选择空闲时间最大的key淘汰掉。

真实LRU算法与近似LRU的算法可以通过下面的图像对比

在这里插入图片描述

浅灰色是已经被淘汰的对象,灰色带是没有被淘汰的对象。可以看出,maxmemory-samples值为5时,
Redis3.0效果要比Redis2.0要好,使用10个采样大小的Redis3.0的近似LRU算法已经非常接近理论的性能,
数据访问模式非常接近幂次分布时,也就是大部分的访问集中于部分键时,LRU近似算法会处理得很好。
在模拟实验的过程中,我们发现如果使用幂次分布的访问模式,真实LRU算法和近似LRU算法几乎没有差别

LRU源码分析

Redis中的键与值都是redisObject对象

typedef struct redisObject {// 4位unsigned type:4;// 4位unsigned encoding:4;// 24位// LRU time (相对于全局的lru_clock) or// LFU data(低8位是频率,高16位是访问时间)unsigned lru:LRU_BITS; // 4个字节 32位int refcount;// 8个字节 64位void *ptr;
} robj;

unsigned的低24bits的lru记录了 redisObj的LRU time

Redis命令访问缓存的数据,均会调用函数的lookupkey:

robj *lookupKey(redisDb *db, robj *key, int flags) {dictEntry *de = dictFind(db->dict,key->ptr);if (de) {robj *val = dictGetVal(de);/* Update the access time for the ageing algorithm.* Don't do it if we have a saving child, as this will trigger* a copy on write madness. */if (server.rdb_child_pid == -1 &&server.aof_child_pid == -1 &&!(flags & LOOKUP_NOTOUCH)){if (server.maxmemory_policy & MAXMEMORY_FLAG_LFU) {updateLFU(val);} else {val->lru = LRU_CLOCK();}}return val;} else {return NULL;}
}

该函数在策略为LRU(非LFU)时会更新对象的lru值,设置为LRU_CLOCK()值:

/* Return the LRU clock, based on the clock resolution. This is a time* in a reduced-bits format that can be used to set and check the* object->lru field of redisObject structures. */
unsigned int getLRUClock(void) {return (mstime()/LRU_CLOCK_RESOLUTION) & LRU_CLOCK_MAX;
}/* This function is used to obtain the current LRU clock.* If the current resolution is lower than the frequency we refresh the* LRU clock (as it should be in production servers) we return the* precomputed value, otherwise we need to resort to a system call. */
unsigned int LRU_CLOCK(void) {unsigned int lruclock;if (1000/server.hz <= LRU_CLOCK_RESOLUTION) {atomicGet(server.lruclock,lruclock);} else {lruclock = getLRUClock();}return lruclock;
}

LRU_CLOCK():取决于LRU_CLOCK_RESOLUTION(默认值1000),LRU_CLOCK_RESOLUTION代表了LRU算法的精度,即一个LRU的单位是多长。server.hz代表服务器刷新的频率,如果服务器的时间更新精度比LRU的精度值要小,LRU_CLOCK()直接使用服务器的时间,减少开销。

freeMemoryIfNeededAndSafe函数

源码如下

int freeMemoryIfNeeded(void) {/* By default replicas should ignore maxmemory* and just be masters exact copies. */if (server.masterhost && server.repl_slave_ignore_maxmemory) return C_OK;size_t mem_reported, mem_tofree, mem_freed;mstime_t latency, eviction_latency;long long delta;int slaves = listLength(server.slaves);/* When clients are paused the dataset should be static not just from the* POV of clients not being able to write, but also from the POV of* expires and evictions of keys not being performed. */if (clientsArePaused()) return C_OK;if (getMaxmemoryState(&mem_reported,NULL,&mem_tofree,NULL) == C_OK)return C_OK;mem_freed = 0;if (server.maxmemory_policy == MAXMEMORY_NO_EVICTION)goto cant_free; /* We need to free memory, but policy forbids. */latencyStartMonitor(latency);while (mem_freed < mem_tofree) {int j, k, i, keys_freed = 0;static unsigned int next_db = 0;sds bestkey = NULL;int bestdbid;redisDb *db;dict *dict;dictEntry *de;if (server.maxmemory_policy & (MAXMEMORY_FLAG_LRU|MAXMEMORY_FLAG_LFU) ||server.maxmemory_policy == MAXMEMORY_VOLATILE_TTL){struct evictionPoolEntry *pool = EvictionPoolLRU;while(bestkey == NULL) {unsigned long total_keys = 0, keys;/* We don't want to make local-db choices when expiring keys,* so to start populate the eviction pool sampling keys from* every DB. */for (i = 0; i < server.dbnum; i++) {db = server.db+i;dict = (server.maxmemory_policy & MAXMEMORY_FLAG_ALLKEYS) ?db->dict : db->expires;if ((keys = dictSize(dict)) != 0) {evictionPoolPopulate(i, dict, db->dict, pool);total_keys += keys;}}if (!total_keys) break; /* No keys to evict. *//* Go backward from best to worst element to evict. */for (k = EVPOOL_SIZE-1; k >= 0; k--) {if (pool[k].key == NULL) continue;bestdbid = pool[k].dbid;if (server.maxmemory_policy & MAXMEMORY_FLAG_ALLKEYS) {de = dictFind(server.db[pool[k].dbid].dict,pool[k].key);} else {de = dictFind(server.db[pool[k].dbid].expires,pool[k].key);}/* Remove the entry from the pool. */if (pool[k].key != pool[k].cached)sdsfree(pool[k].key);pool[k].key = NULL;pool[k].idle = 0;/* If the key exists, is our pick. Otherwise it is* a ghost and we need to try the next element. */if (de) {bestkey = dictGetKey(de);break;} else {/* Ghost... Iterate again. */}}}}/* volatile-random and allkeys-random policy */// .../* Finally remove the selected key. */if (bestkey) {db = server.db+bestdbid;robj *keyobj = createStringObject(bestkey,sdslen(bestkey));propagateExpire(db,keyobj,server.lazyfree_lazy_eviction);/* We compute the amount of memory freed by db*Delete() alone.* It is possible that actually the memory needed to propagate* the DEL in AOF and replication link is greater than the one* we are freeing removing the key, but we can't account for* that otherwise we would never exit the loop.** AOF and Output buffer memory will be freed eventually so* we only care about memory used by the key space. */delta = (long long) zmalloc_used_memory();latencyStartMonitor(eviction_latency);if (server.lazyfree_lazy_eviction)dbAsyncDelete(db,keyobj);elsedbSyncDelete(db,keyobj);latencyEndMonitor(eviction_latency);latencyAddSampleIfNeeded("eviction-del",eviction_latency);latencyRemoveNestedEvent(latency,eviction_latency);delta -= (long long) zmalloc_used_memory();mem_freed += delta;server.stat_evictedkeys++;notifyKeyspaceEvent(NOTIFY_EVICTED, "evicted",keyobj, db->id);decrRefCount(keyobj);keys_freed++;/* When the memory to free starts to be big enough, we may* start spending so much time here that is impossible to* deliver data to the slaves fast enough, so we force the* transmission here inside the loop. */if (slaves) flushSlavesOutputBuffers();/* Normally our stop condition is the ability to release* a fixed, pre-computed amount of memory. However when we* are deleting objects in another thread, it's better to* check, from time to time, if we already reached our target* memory, since the "mem_freed" amount is computed only* across the dbAsyncDelete() call, while the thread can* release the memory all the time. */if (server.lazyfree_lazy_eviction && !(keys_freed % 16)) {if (getMaxmemoryState(NULL,NULL,NULL,NULL) == C_OK) {/* Let's satisfy our stop condition. */mem_freed = mem_tofree;}}}if (!keys_freed) {latencyEndMonitor(latency);latencyAddSampleIfNeeded("eviction-cycle",latency);goto cant_free; /* nothing to free... */}}latencyEndMonitor(latency);latencyAddSampleIfNeeded("eviction-cycle",latency);return C_OK;cant_free:/* We are here if we are not able to reclaim memory. There is only one* last thing we can try: check if the lazyfree thread has jobs in queue* and wait... */while(bioPendingJobsOfType(BIO_LAZY_FREE)) {if (((mem_reported - zmalloc_used_memory()) + mem_freed) >= mem_tofree)break;usleep(1000);}return C_ERR;
}/* This is a wrapper for freeMemoryIfNeeded() that only really calls the* function if right now there are the conditions to do so safely:** - There must be no script in timeout condition.* - Nor we are loading data right now.**/
int freeMemoryIfNeededAndSafe(void) {if (server.lua_timedout || server.loading) return C_OK;return freeMemoryIfNeeded();
}

freeMemoryIfNeededAndSafe()为释放内存的函数,
几种淘汰策略maxmemory_policy就是在这个函数里面实现的。当采用LRU时,可以看到,从0号数据库开始(默认16个)根据不同的策略,选择redisDb的dict(全部键)或者expires(有过期时间的键),,用来更新候选键池子pool,pool更新策略是evictionPoolPopulate()

evictionPoolPopulate函数

evictionPoolPopulate函数
```c++
void evictionPoolPopulate(int dbid, dict *sampledict, dict *keydict, struct evictionPoolEntry *pool) {int j, k, count;dictEntry *samples[server.maxmemory_samples];count = dictGetSomeKeys(sampledict,samples,server.maxmemory_samples);for (j = 0; j < count; j++) {unsigned long long idle;sds key;robj *o;dictEntry *de;de = samples[j];key = dictGetKey(de);/* If the dictionary we are sampling from is not the main* dictionary (but the expires one) we need to lookup the key* again in the key dictionary to obtain the value object. */if (server.maxmemory_policy != MAXMEMORY_VOLATILE_TTL) {if (sampledict != keydict) de = dictFind(keydict, key);o = dictGetVal(de);}/* Calculate the idle time according to the policy. This is called* idle just because the code initially handled LRU, but is in fact* just a score where an higher score means better candidate. */if (server.maxmemory_policy & MAXMEMORY_FLAG_LRU) {idle = estimateObjectIdleTime(o);} else if (server.maxmemory_policy & MAXMEMORY_FLAG_LFU) {/* When we use an LRU policy, we sort the keys by idle time* so that we expire keys starting from greater idle time.* However when the policy is an LFU one, we have a frequency* estimation, and we want to evict keys with lower frequency* first. So inside the pool we put objects using the inverted* frequency subtracting the actual frequency to the maximum* frequency of 255. */idle = 255-LFUDecrAndReturn(o);} else if (server.maxmemory_policy == MAXMEMORY_VOLATILE_TTL) {/* In this case the sooner the expire the better. */idle = ULLONG_MAX - (long)dictGetVal(de);} else {serverPanic("Unknown eviction policy in evictionPoolPopulate()");}/* Insert the element inside the pool.* First, find the first empty bucket or the first populated* bucket that has an idle time smaller than our idle time. */k = 0;while (k < EVPOOL_SIZE &&pool[k].key &&pool[k].idle < idle) k++;if (k == 0 && pool[EVPOOL_SIZE-1].key != NULL) {/* Can't insert if the element is < the worst element we have* and there are no empty buckets. */continue;} else if (k < EVPOOL_SIZE && pool[k].key == NULL) {/* Inserting into empty position. No setup needed before insert. */} else {/* Inserting in the middle. Now k points to the first element* greater than the element to insert.  */if (pool[EVPOOL_SIZE-1].key == NULL) {/* Free space on the right? Insert at k shifting* all the elements from k to end to the right. *//* Save SDS before overwriting. */sds cached = pool[EVPOOL_SIZE-1].cached;memmove(pool+k+1,pool+k,sizeof(pool[0])*(EVPOOL_SIZE-k-1));pool[k].cached = cached;} else {/* No free space on right? Insert at k-1 */k--;/* Shift all elements on the left of k (included) to the* left, so we discard the element with smaller idle time. */sds cached = pool[0].cached; /* Save SDS before overwriting. */if (pool[0].key != pool[0].cached) sdsfree(pool[0].key);memmove(pool,pool+1,sizeof(pool[0])*k);pool[k].cached = cached;}}/* Try to reuse the cached SDS string allocated in the pool entry,* because allocating and deallocating this object is costly* (according to the profiler, not my fantasy. Remember:* premature optimizbla bla bla bla. */int klen = sdslen(key);if (klen > EVPOOL_CACHED_SDS_SIZE) {pool[k].key = sdsdup(key);} else {memcpy(pool[k].cached,key,klen+1);sdssetlen(pool[k].cached,klen);pool[k].key = pool[k].cached;}pool[k].idle = idle;pool[k].dbid = dbid;}
}

Redis随机选择maxmemory_samples数量的key,然后计算这些key的空闲时间idle time,当满足条件时(比pool中的某些
键的空闲时间还大)就可以进pool。pool更新之后,就淘汰pool中空闲时间最大的键
estimateObjectIdleTime用来计算Redis对象的空闲时间:

/* Given an object returns the min number of milliseconds the object was never* requested, using an approximated LRU algorithm. */
unsigned long long estimateObjectIdleTime(robj *o) {unsigned long long lruclock = LRU_CLOCK();if (lruclock >= o->lru) {return (lruclock - o->lru) * LRU_CLOCK_RESOLUTION;} else {return (lruclock + (LRU_CLOCK_MAX - o->lru)) *LRU_CLOCK_RESOLUTION;}
}

空闲时间基本就是对象的lru和全局的LRU_CLOCK()的差值乘以精度LRU_CLOCK_RESOLUTION,将秒转化为了毫秒

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

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

相关文章

【Java面试题】Redis上篇(基础、持久化、底层数据结构)

文章目录 基础1.什么是Redis?2.Redis可以用来干什么&#xff1f;3.Redis的五种基本数据结构&#xff1f;4.Redis为什么这么快&#xff1f;5.什么是I/O多路复用&#xff1f;6.Redis6.0为什么使用了多线程&#xff1f; 持久化7.Redis的持久化方式&#xff1f;区别&#xff1f;8.…

生成式 AI 学习资源大汇总

这里汇聚了该领域的海量学习资源&#xff0c;从研究更新到面试技巧&#xff0c;从课程材料到免费课程&#xff0c;还有实用代码&#xff0c;一应俱全&#xff0c;是你工作流程中的得力助手&#xff01; 前沿研究&#xff1a;每月精心筛选的最佳生成式 AI 论文列表&#xff0c;让…

Linux shell编程学习笔记42:md5sum

0 前言 前几天在国产电脑上遇到一个问题&#xff0c;先后接到两个文件&#xff0c;如何判断这两个文件内容是否相同&#xff1f; 如果是在Windows系统&#xff0c;可以用fc命令&#xff0c;或者用我自己写的FileInfo&#xff0c;提取两个文件有MD5、SHA1、CRC32值进行比较来判…

redis-shake可视化监控

目录 一.redis-shake v4 1.镜像 2.shake.toml 3.启动redis-shake后 二.json-exporter配置 1.Dockerfile 2.config.yml 三.prometheus配置 1.prometheus.yml 2.redis-shake.json 四.grafana 一.redis-shake v4 1.镜像 ######################### Dockerfile #########…

Qt打印系统库的日志 - QLoggingCategory

Qt的动态库通过源码可以可以看到含有大量的qCInfo 和 qCDebug 等大量的日志&#xff0c; 但是我们正常运行Qt程序&#xff0c;这些动态库或插件里面的日志是不会输出到我们的控制台里面的。 所以本章主要记录怎么输出这些日志出来。 一&#xff1a; 步骤 主要使用的是Qt的 函…

Kubernetes中pod的概念

pod pod是什么&#xff1a;pod是k8s中基本的构建模块&#xff0c;一个pod可以包含多个和单个容器&#xff0c;包含多个容器时&#xff0c;这些容器总是运行在同一个工作节点上&#xff0c;因为一个pod绝不会跨多个工作节点。 了解pod&#xff1a; pod将容器绑定在一起&#xf…

【Golang入门教程】Go语言变量的初始化

文章目录 强烈推荐引言举例多个变量同时赋值总结强烈推荐专栏集锦写在最后 强烈推荐 前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击跳转到网站:人工智能 推荐一个个人工作&#xff0c;日常中比较常…

政安晨:【Keras机器学习实践要点】(七)—— 使用TensorFlow自定义fit()

政安晨的个人主页&#xff1a;政安晨 欢迎 &#x1f44d;点赞✍评论⭐收藏 收录专栏: TensorFlow与Keras实战演绎机器学习 希望政安晨的博客能够对您有所裨益&#xff0c;如有不足之处&#xff0c;欢迎在评论区提出指正&#xff01; 在TensorFlow中&#xff0c;fit()是一个非常…

Python+Django+Yolov5路面墙体桥梁裂缝特征检测识别html网页前后端

程序示例精选 PythonDjangoYolov5路面墙体桥梁裂缝特征检测识别html网页前后端 如需安装运行环境或远程调试&#xff0c;见文章底部个人QQ名片&#xff0c;由专业技术人员远程协助&#xff01; 前言 这篇博客针对《PythonDjangoYolov5路面墙体桥梁裂缝特征检测识别html网页前…

Parade Series - SVG Resource

iconfont https://www.iconfont.cn/?spma313x.search_index.i3.2.74e53a819tkkcG音符 <div class"form-group"><a href"Javascript:reload();" class"btn btn-icon btn-outline-light btn-block" style";"><svg t&q…

打造快乐成长的乐园:探索少儿教育项目的魅力

在当今社会&#xff0c;家长们越来越重视孩子的全面发展和个性培养&#xff0c;少儿教育项目因其独特的魅力吸引着越来越多的关注。本文将探讨少儿教育项目的特点、重要性&#xff0c;以及如何打造一个快乐成长的教育乐园。 ### 少儿教育项目的价值 少儿教育项目不仅仅是传授…

Python 基于 OpenCV 视觉图像处理实战 之 OpenCV 简单实战案例 之九 简单闪烁效果

Python 基于 OpenCV 视觉图像处理实战 之 OpenCV 简单实战案例 之九 简单闪烁效果 目录 Python 基于 OpenCV 视觉图像处理实战 之 OpenCV 简单实战案例 之九 简单闪烁效果 一、简单介绍 二、简单闪烁效果实现原理 三、简单闪烁效果案例实现简单步骤 四、注意事项 一、简单…

【开发篇】十二、GCeasy报告分析

文章目录 1、图一&#xff1a;正常情况2、图二&#xff1a;缓存对象过多3、图三&#xff1a;内存泄漏4、图四&#xff1a;频繁持续Full GC5、图五&#xff1a;元空间不足导致的Full GC 1、图一&#xff1a;正常情况 正常的堆内存如图&#xff1a; 锯齿状对象创建后内存占用上…

基础算法-去重字符串,辗转相除法,非递归前序遍历二叉树题型分析

目录 不同子串 辗转相除法-求最大公约数 二叉树非递归前序遍历 不同子串 从a开始&#xff0c;截取 a aa aaa aaab 从第二个下标开始a aa aab 从第三个 a ab 从第四个 b 使用set的唯一性&#xff0c;然后暴力遍历来去去重&#xff0c;从第一个下标开始截取aaab a aa aaa aaab…

ES学习日记(三)-------第三方插件选择

前言 在学习和使用Elasticsearch的过程中&#xff0c;必不可少需要通过一些工具查看es的运行状态以及数据。如果都是通过rest请求&#xff0c;未免太过麻烦&#xff0c;而且也不够人性化。 目前我了解的比较主流的插件就三个,head,cerebor和elasticHD 1.head 老牌插件,功能…

原生js实现循环滚动效果

原生js实现如下图循环滚动效果 核心代码 <div class"scroll"><div class"blist" id"scrollContainer"><div class"bitem"></div>......<div class"bitem"></div></div> </di…

Long long类型比较大小

long 与 Long long类型和Long类型是不一样&#xff0c;long类型属于基本的数据类型&#xff0c;而Long是long类型的包装类。 结论 long是基本数据类型&#xff0c;判断是否相等时使用 &#xff0c;即可判断值是否相等。&#xff08;基本数据类型没有equals()方法&#xff0…

局域网找不到共享电脑怎么办?

局域网找不到共享电脑是一种常见的问题&#xff0c;给我们的共享与合作带来一定的困扰。天联组网技术可以解决这个问题。本文将介绍天联组网的原理和优势&#xff0c;并探讨其在解决局域网找不到共享电脑问题中的应用。 天联组网的原理和优势 天联组网是一种基于加速服务器的远…

基于Pytorch的验证码识别模型应用

前言 在做OCR文字识别的时候&#xff0c;或多或少会接触一些验证码图片&#xff0c;这里收集了一些验证码图片&#xff0c;可以对验证码进行识别&#xff0c;可以识别4到6位&#xff0c;纯数字型、数字字母型和纯字母型的一些验证码&#xff0c;准确率还是相当高&#xff0c;需…

STM32 PWM通过RC低通滤波转双极性SPWM测试

STM32 PWM通过RC低通滤波转双极性SPWM测试 &#x1f4cd;参考内容《利用是stm32cubemx实现双极性spwm调制 基于stm32f407vet6》&#x1f4fa;相关视频链接&#xff1a;https://www.bilibili.com/video/BV16S4y147hB/?spm_id_from333.788 双极性SPWM调制讲解以及基于stm32的代码…