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);
            else
                dbSyncDelete(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,将秒转化为了毫秒

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

轻松上手,小白也能免费部署自己的炫酷静态网站!(如何免费搭建个人网站)

文章目录 📖 介绍 📖🏡 演示环境 🏡📒 使用方法 📒📝 操作步骤📝 更换主题⚓️ 相关链接 ⚓️📖 介绍 📖 在互联网时代,拥有一个个人网站无疑是展示自我、分享知识或建立品牌形象的有效途径。然而,许多人在追求这一目标的过程中,常常因为成本、时间和技…

【C++】1320. 时钟旋转(1)

问题 类型&#xff1a;整数运算 题目描述&#xff1a; 时钟上面的时针从 m 时走到 n 时旋转了多少度&#xff1f;&#xff08;m≤n&#xff0c;且 m 和 n 都是1∼12之间的整数&#xff09;。 输入&#xff1a; 2 个整数 m 和 n。 输出&#xff1a; 1 个整数代表时针旋转的…

[数据结构]插入和希尔排序

一、插入排序 插入排序的代码实现虽然没有冒泡排序和选择排序那么简单粗暴&#xff0c;但它的原理应该是最容易理解的了&#xff0c;因为只要打过扑克牌的人都应该能够秒懂。插入排序是一种最简单直观的排序算法&#xff0c;它的工作原理是通过构建有序序列&#xff0c;对于未排…

全球金融市场的汇率与政策利率演变:历史与未来的交汇

根据国际货币基金组织&#xff08;IMF&#xff09;等平台的数据&#xff0c;整理了全球各国的兑美元汇率&#xff0c;短期利率、长期利率、政策利率数据&#xff0c;时间范围最新至2024年3月&#xff0c;希望对大家有所帮助 一、数据介绍 数据名称&#xff1a;全球各国汇率、短…

O2OA(翱途)开发平台-快速入门开发一个门户实例

O2OA(翱途)开发平台[下称O2OA开发平台或者O2OA]拥有门户页面定制与集成的能力&#xff0c;平台通过门户定制&#xff0c;可以根据企业的文化&#xff0c;业务需要设计符合企业需要的统一信息门户&#xff0c;系统首页等UI界面。本篇主要介绍通过门户管理系统如何快速的进行一个…

DoubleU-Net:一种用于医学图像分割的深度卷积神经网络

DoubleU-Net&#xff1a;一种用于医学图像分割的深度卷积神经网络 摘要引言相关工作方法 DoubleU-Net A Deep Convolutional Neural Network for Medical Image Segmentation–2020 摘要 语义图像分割是将图像中的每个像素标记为相应的类的过程。基于编码器-解码器的方法&…

如何在Win10使用IIS服务搭建WebDAV网站并实现无公网IP访问内网文件内容

文章目录 前言1. 安装IIS必要WebDav组件2. 客户端测试3. 使用cpolar内网穿透&#xff0c;将WebDav服务暴露在公网3.1 安装cpolar内网穿透3.2 配置WebDav公网访问地址 4. 映射本地盘符访问 前言 在Windows上如何搭建WebDav&#xff0c;并且结合cpolar的内网穿透工具实现在公网访…

银行监管报送系统介绍(十二):非居民金融账户涉税信息报送

国家税务总局、财政部、中国人民银行、中国银行业监督管理委员会、中国证券监督管理委员会、国家金融监督管理总局2017年5月9日发布、2017年7月1日起施行的《非居民金融账户涉税信息尽职调查管理办法》。 一、《管理办法》出台的背景是什么&#xff1f;   受二十国集团&…

软件设计师24--概念设计阶段

软件设计师24--概念设计阶段 考点1&#xff1a;概念设计过程考点2&#xff1a;E-R图属性E-R模型-联系类型判断例题&#xff1a;E-R模型-联系类型判断扩充的E-R模型 考点1&#xff1a;概念设计过程 需求分析 --> 抽象数据 --> 设计局部ER模型 --> 合并局部模型消除冲突…

接口自动化测试要做什么?8个步骤讲的明明白白

&#x1f345; 视频学习&#xff1a;文末有免费的配套视频可观看 &#x1f345; 点击文末小卡片&#xff0c;免费获取软件测试全套资料&#xff0c;资料在手&#xff0c;涨薪更快 先了解下接口测试流程&#xff1a; 1、需求分析 2、Api文档分析与评审 3、测试计划编写 4、用例设…

2024年2月吸尘器行业线上电商(京东天猫淘宝)综合排行榜

鲸参谋监测的线上电商平台&#xff08;淘宝天猫京东&#xff09;2月吸尘器行业销售数据公开。 根据鲸参谋电商数据平台显示&#xff0c;吸尘器行业2月销量累计约53万件&#xff0c;环比上个月下滑29%&#xff0c;同比去年下滑19%&#xff1b;销售额累计约4亿&#xff0c;环比上…

原生js实现循环滚动效果

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

ES学习日记(一)-------单节点安装启动

基于ES7.4.1编写,其实一开始用的最新的8.1,但是问题太多了!!!!不稳定,降到7.4 下载好的安装包上传到服务器或虚拟机,创建ES目录,命令mkdir -p /路径xxxx 复制安装包到指定路径并解压: tar zxvf elasticsearch-8.1.0-linux-x86_64.tar.gz -C /usr/local/es/ 进入bin目录安装,命…

【操作系统复习之路】操作系统概述(复习的同学有福啦)

长话短说&#xff0c;就记下笔记&#xff0c;期待期末90&#xff0c;随便希望能帮助到有需要的同学。 目录 一、操作系统的目标和作用 二、操作系统的发展过程 2.1 无OS 2.2 有OS 【1】批处理系统 【2】分时系统 【3】实时操作系统 【4】三种基本操作系统的比较&#…

类的定义与实例化

一.类的定义 1.1 格式 定义类的一般格式如下&#xff1a; class 类名{ public:公有成员列表; protected:保护成员列表; private:私有成员列表; }; 构成元素&#xff1a; &#xff08;1&#xff09;类头&#xff08;class head&#xff09; “class 类名”称为类头。 &…

大视频上传断点续传解决方案

版本&#xff1a;6.5.40 代码&#xff1a;up6-jsp-springboot: Web大文件上传-jsp-springboot示例 - Gitee.com nosql示例 nosql示例不需要进行任何配置&#xff0c;可以直接访问测试。 SQL示例 1.创建数据库 2.配置数据库连接 3.自动下载maven依赖 4.启动项目 启动成功 6.访…

开源 | 电动汽车充换电解决方案,从智能硬件到软件系统,全部自主研发

文章目录 一、产品功能部分截图1.手机端&#xff08;小程序、安卓、ios&#xff09;2.PC端 二、小程序体验账号以及PC后台体验账号1.小程序体验账号2.PC后台体验账号关注公众号获取最新资讯 三、产品简介&#xff1f;1. 充电桩云平台&#xff08;含硬件充电桩&#xff09;&…

VTK——自定义二维图像涂抹Widget(支持任意值涂抹),擦除,恢复 vtkCustomPaintWidget

通过鼠标控制 涂抹区域&#xff0c;可以进行&#xff0c;后退&#xff0c;可以进行二维标注&#xff0c;也可以进行回退&#xff0c;也可以任意值涂抹。 vtkCustomPaintWidget 1.标注&#xff1a; 2.擦除 视频&#xff1a; 2D标注 vtkPaint VTK 2D 标注 描绘 2D 擦除&#x…

蓝桥杯-卡片换位

solution 有一个测试点没有空格&#xff0c;要特别处理&#xff0c;否则会有一个测试点运行错误&#xff01; 还有输入数据的规模在变&#xff0c;小心顺手敲错了边界条件 #include<iostream> #include<string> #include<queue> #include<map> #incl…

picgo + 腾讯云 cos 图床搭建

文章目录 0.背景&简介1.注册腾讯云并创建 COS 存储桶2.获取 API 密钥3.安装并配置 PicGo4.配置typora 0.背景&简介 背景: 文档笔记有图片但存储在本地, 想发送给别人需要连同图片目录一起绑定发送比较麻烦 这里使用对象存储 CDN搭建图床 简介: PicGo 是一款开源的图片…