Redis数据库——键过期时间

一.设置键的生存时间或者过期时间

        我们可以在Redis客户端输入命令,可以以秒或者毫秒精度为数据库中的某个键设置生存时间,在指定秒数或者毫秒数之后,服务器会自动删除生存时间为0的键。

        1.1 设置过期时间

        Redis有四个不同的命令可以用于设置键的生存时间或者过期时间:

  • EXPIRE <KEY> <TTL> 命令用于将键key的生存时间设置为ttl秒。
  • PEXPIRE <KEY> <TTL>命令用于将键key的生存时间设置为ttl毫秒。
  • EXPIREAT <KEY> <timestamp> 命令用于将键key的过期时间设置为timestamp所指定秒数时间戳。
  • PEXPIREAT <KEY> <timestamp> 命令用于将键key的过期时间设置为timestamp所指定毫秒数时间戳。

        虽然有多种不同单位不同形式的设置命令,但实际上EXPIRE, PEXPIRE, EXPIREAT三个命令都是使用PEXPIREAT命令来实现的。

def EXPIRE(key, ttl_in_sec):#将TTL从秒装换成毫秒ttl_in_ms = sec_to_ms(ttl_in_sec)\PEXPIRE(key, ttl_in_ms)#PEXPIRE里面调用PEXPIREAT
def PEXPIRE(key, ttl_in_ms):#获取以毫秒计算的当前时间戳now_ms = get_current_unix_timestamp_in_ms()#当前时间加上ttl,得出毫秒格式的键的过期时间PEXPIREAT(key, now_ms + ttl_in_ms)#EXPIREAT里面调用PEXPIREAT
def EXPIREAT(key, expire_time_in_sec):#将过期时间从秒转化成毫秒expire_time_in_ms = sec_to_ms(expire_time_in_sec)PEXPIREAT(key, expire_time_in_ms)

        1.2 保存过期时间

        redisDb结构的expires字典保存了数据库的所有键的过期时间,我们称这个字典为过期字典。

  • 过期字典的键是一个指针,这个指针指向键空间中的某个键对象(也即是某个数据库的键)。
  • 过期字典的值是一个long long类型的整数,这个整数保存了键所指向的数据库键的过期时间——一个毫秒精度的时间戳。

         举个例子:

        如果数据库当前状态如下图:

        那么服务器执行一下命令之后,过期字典将新增一个键值对,其中键为message键对象,而值为1391234400000(2014年2月1日0时)

PEXPIREAT message 1391234400000

        执行PEXPIREAT命令的伪代码:

def PEXPIREAT(key, expire_time_in_ms):#如果给定的键不存在于键空间,那么不能设置过期时间if key not in redisDb.dict:return 0#在过期字典中关联键和过期时间redisDb.expires[key] = expire_time_in_msreturn 1

        1.3 移除过期时间

        PERSIST命令可以移除一个键的过期时间。

127.0.0.1:6379> set k1 'v1'
OK
127.0.0.1:6379> ttl k1
(integer) -1
127.0.0.1:6379> expire k1 1000
(integer) 1
127.0.0.1:6379> ttl k1
(integer) 998
127.0.0.1:6379> persist k1
(integer) 1
127.0.0.1:6379> ttl k1
(integer) -1
127.0.0.1:6379> 

        伪代码:

def PERSIST(key):#判断键是否在过期字典中if key not in redisDb.expires:return 0#删除过期字典中对应键值对redisDb.expires.remove(key)return 1 

        1.4 计算并返回剩余生存时间

        TTL命令是以秒为单位返回键的剩余生存时间,PTTL命令则是以毫秒为单位返回键的剩余生存时间:

127.0.0.1:6379> hset d1 k1 v1
(integer) 1
127.0.0.1:6379> expire d1 2000
(integer) 1
127.0.0.1:6379> ttl d1
(integer) 1994
127.0.0.1:6379> pttl d1
(integer) 1989776
127.0.0.1:6379> 

        伪代码:

def PTTL(key):#判断是否在键空间if key not in redisDb.dict:#已经过期return -2#判断是否存在过期字典if key not in redisDb.expires:#永不过期return -1#获得过期时间#如果键没有设置过期时间,返回Noneexpire_timne_in_ms = redisDb.expires[key]if expire_time_in_ms is None:return -1#获取当前时间now_ms = get_current_unix_timestamp_in_ms()return (expire_timne_in_ms - now_ms)def TTL(key):#获取以毫秒为单位的过期时间ttl_in_ms = PTTL(key)if ttl_in_ms < 0:return ttl_in_mselse:return ms_to_sec(ttl_in_ms)

        1.5 过期键的判定

        通过过期字典,程序可以用以下步骤检查一个给定的键是否过期:

  • 检查给定的键是否存在于过期字典,存在,获取过期时间。
  • 检查当前UNIX时间戳是否大于键的过期时间,如果是的话,表示键已经过期,否则未过期。

        伪代码:

def is_expired(key):#获取键的过期时间expire_time_in_ms = redisDb.expires.get(key)#键没有设置过期时间if expire_time_in_ms is None:return False#获取当前时间时间戳now_time_ms = get_current_unix_timestamp_in_ms()if now_timw_ms > expire_time_in_ms:return Trueelsereturn False

        我们也可以使用TTL或者PTTL命令来查看一个命令是否过期,大于等于0或者-1,表示未过期,-2表示过期。但是在redis代码中检查键是否过期的逻辑和上面的伪代码is_expired()方法一直,因为直接访问字典比执行一个命令快。

二. 过期键删除策略

        对于过期键删除有三种可选策略:

  • 定时删除:在设置键的过期时间的同时,创建一个定时器(timer),让定时器在键的过期时间来临时,立即执行对键的删除操作。
  • 惰性删除:放任键过期不管,但是每次从键空间中获取键时,检查取得的键是否过期,如果过期的话,键删除该键,如果没有过期,就要返回该键。
  • 定期删除:每个一段时间,程序就对数据库进行一次检查,删除里面的过期键。至于删除多少过期键,以及检查多少数据库,则有算法决定。

        在这三种策略中,第一种和第三种为主动删除策略,而第二种则为被动删除策略。

        3.1 定时删除

        定时删除策略对内存是最友好的:通过定时器,定时删除策略可以保证过期键会尽可能快的被删除,并释放过期键所占用的内存。

        但是定时删除策略对CPU时间是最不友好的:在过期键比较多的情况下,删除过期键这一行为可能会占用相当一部分CPU时间。在内存不紧张但是CPU时间非常紧张的情况下,将CPU时间在删除和当前任务无关的过期键上,会对服务器的响应时间和吞吐量造成影响。

        创建一个定时器需要用到Redis服务器中的时间事件,而当前时间事件是用无序链表实现的,查找一个键是否过期的时间复杂度为O(N),效率比较低。

        所以,要让服务器创建大量的定时器,从而实现定时删除策略,现阶段不大现实。

        3.2 惰性删除

        惰性删除策略对CPU时间来说是友好的:程序只会在取出键时才对键进行过期检查,这样删除的键仅限于当前处理的键,不会在删除其他无关过期键上花费任何CPU时间。

        惰性删除策略对内存来说是最不友好的:键已经过期,但是没有删除,仍然占用内存。

        当数据库中有非常多的过期键,而这些键又恰好你没有被访问到,那他们就永远不会被删除,除非使用FLUSHDB命令,这种情况相当于是内存泄漏。

        比如:日志,在某个时间点后,对它们的访问就会大大减少,甚至不在访问,这些过期数据就会一直占用Redis服务器内存,导致可使用的内存越来越少。

        3.3 定期删除

        定期删除是前面两种策略的折中:

  • 定期删除策略每隔一段时间执行一次删除过期键操作,并且通过限制删除操作执行的时长和频率来减少删除操作对CPU时间的影响。
  • 定期删除策略有效的减少因为过期键带来的内存浪费。

        但是定期删除策略的难点是确定删除操作的时长和频率:

  • 如果删除操作执行的太频繁,或者执行时间过长,就会退化成定时删除策略。占用CPU时间过长。
  • 如果删除操作执行的太少,或者执行的时间过短,就会退化成惰性删除,导致浪费内存。

        因此采用定期删除策略,服务器必须根据情况,合理的设置删除操作的执行时长和执行频率。

三. Redis的过期键删除策略

        Redis服务器实际使用的是惰性删除和定期删除两种策略。

        3.1 惰性删除策略的实现

        过期键的惰性删除策略有db.c/expireIfNeeded函数实现,所有读写数据库的redis命令在执行之前都会调用该函数进行检查:

  • 如果输入键已经过期,那么expireIfNeeded函数将输入键从数据库中删除。
  • 如果输入键没有过期,那么expireIfNeeded函数不动作。
int expireIfNeeded(redisDb *db, robj *key) {if (!keyIsExpired(db,key)) return 0;/* If we are running in the context of a slave, instead of* evicting the expired key from the database, we return ASAP:* the slave key expiration is controlled by the master that will* send us synthesized DEL operations for expired keys.** Still we try to return the right information to the caller,* that is, 0 if we think the key should be still valid, 1 if* we think the key is expired at this time. */if (server.masterhost != NULL) return 1;/* If clients are paused, we keep the current dataset constant,* but return to the client what we believe is the right state. Typically,* at the end of the pause we will properly expire the key OR we will* have failed over and the new primary will send us the expire. */if (checkClientPauseTimeoutAndReturnIfPaused()) return 1;/* Delete the key */deleteExpiredKeyAndPropagate(db,key);return 1;
}

        命令调用expireIfNeeded函数过程如下图:

        另外,因为每一个被访问的键都可能因为过期而被expireIfNeeded函数删除,所以每一个命令的实现函数必须同时处理键存在以及键不存在的情况:

  • 如果键存在,命令按照键存在的情况执行。
  • 当键不存在或者因为过期而被expireIfNeeded函数删除,命令按照不存在的情况执行。

        下图展示了输入GET命令执行过程:

        3.2 定期删除策略实现 

         过期键定期删除策略由redis.c/activeExpireCycle函数实现,每当Redis的服务器周期操作redis.c/serverCron函数执行时,activeExpireCycle函数会被调用,它在规定的时间内,分多次遍历服务器中的各个数据库,从数据库的expires字典中随机抽查一部分键的过期时间,并删除过期键。

#define CRON_DBS_PER_CALL 16#define ACTIVE_EXPIRE_CYCLE_KEYS_PER_LOOP 20 /* Keys for each DB loop. */
#define ACTIVE_EXPIRE_CYCLE_FAST_DURATION 1000 /* Microseconds. */
#define ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC 25 /* Max % of CPU to use. */
#define ACTIVE_EXPIRE_CYCLE_ACCEPTABLE_STALE 10 /* % of stale keys after whichwe do extra efforts. */void activeExpireCycle(int type) {/* Adjust the running parameters according to the configured expire* effort. The default effort is 1, and the maximum configurable effort* is 10. */unsigned longeffort = server.active_expire_effort-1, /* Rescale from 0 to 9. */config_keys_per_loop = ACTIVE_EXPIRE_CYCLE_KEYS_PER_LOOP +ACTIVE_EXPIRE_CYCLE_KEYS_PER_LOOP/4*effort,config_cycle_fast_duration = ACTIVE_EXPIRE_CYCLE_FAST_DURATION +ACTIVE_EXPIRE_CYCLE_FAST_DURATION/4*effort,config_cycle_slow_time_perc = ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC +2*effort,config_cycle_acceptable_stale = ACTIVE_EXPIRE_CYCLE_ACCEPTABLE_STALE-effort;/* This function has some global state in order to continue the work* incrementally across calls. */static unsigned int current_db = 0; /* Next DB to test. */static int timelimit_exit = 0;      /* Time limit hit in previous call? */static long long last_fast_cycle = 0; /* When last fast cycle ran. */int j, iteration = 0;int dbs_per_call = CRON_DBS_PER_CALL;long long start = ustime(), timelimit, elapsed;/* 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 (checkClientPauseTimeoutAndReturnIfPaused()) return;if (type == ACTIVE_EXPIRE_CYCLE_FAST) {/* Don't start a fast cycle if the previous cycle did not exit* for time limit, unless the percentage of estimated stale keys is* too high. Also never repeat a fast cycle for the same period* as the fast cycle total duration itself. */if (!timelimit_exit &&server.stat_expired_stale_perc < config_cycle_acceptable_stale)return;if (start < last_fast_cycle + (long long)config_cycle_fast_duration*2)return;last_fast_cycle = start;}/* We usually should test CRON_DBS_PER_CALL per iteration, with* two exceptions:** 1) Don't test more DBs than we have.* 2) If last time we hit the time limit, we want to scan all DBs* in this iteration, as there is work to do in some DB and we don't want* expired keys to use memory for too much time. *///初始化检查的数据库数量if (dbs_per_call > server.dbnum || timelimit_exit)dbs_per_call = server.dbnum;/* We can use at max 'config_cycle_slow_time_perc' percentage of CPU* time per iteration. Since this function gets called with a frequency of* server.hz times per second, the following is the max amount of* microseconds we can spend in this function. */timelimit = config_cycle_slow_time_perc*1000000/server.hz/100;timelimit_exit = 0;if (timelimit <= 0) timelimit = 1;if (type == ACTIVE_EXPIRE_CYCLE_FAST)timelimit = config_cycle_fast_duration; /* in microseconds. *//* Accumulate some global stats as we expire keys, to have some idea* about the number of keys that are already logically expired, but still* existing inside the database. */long total_sampled = 0;long total_expired = 0;//遍历所有数据库for (j = 0; j < dbs_per_call && timelimit_exit == 0; j++) {/* Expired and checked in a single loop. */unsigned long expired, sampled;redisDb *db = server.db+(current_db % server.dbnum);/* Increment the DB now so we are sure if we run out of time* in the current DB we'll restart from the next. This allows to* distribute the time evenly across DBs. */current_db++;/* Continue to expire if at the end of the cycle there are still* a big percentage of keys to expire, compared to the number of keys* we scanned. The percentage, stored in config_cycle_acceptable_stale* is not fixed, but depends on the Redis configured "expire effort". */do {unsigned long num, slots;long long now, ttl_sum;int ttl_samples;iteration++;/* If there is nothing to expire try next DB ASAP. *///过期键数量if ((num = dictSize(db->expires)) == 0) {db->avg_ttl = 0;break;}slots = dictSlots(db->expires);now = mstime();/* When there are less than 1% filled slots, sampling the key* space is expensive, so stop here waiting for better times...* The dictionary will be resized asap. */if (slots > DICT_HT_INITIAL_SIZE &&(num*100/slots < 1)) break;/* The main collection cycle. Sample random keys among keys* with an expire set, checking for expired ones. */expired = 0;sampled = 0;ttl_sum = 0;ttl_samples = 0;if (num > config_keys_per_loop)num = config_keys_per_loop;/* Here we access the low level representation of the hash table* for speed concerns: this makes this code coupled with dict.c,* but it hardly changed in ten years.** Note that certain places of the hash table may be empty,* so we want also a stop condition about the number of* buckets that we scanned. However scanning for free buckets* is very fast: we are in the cache line scanning a sequential* array of NULL pointers, so we can scan a lot more buckets* than keys in the same time. */long max_buckets = num*20;long checked_buckets = 0;while (sampled < num && checked_buckets < max_buckets) {for (int table = 0; table < 2; table++) {if (table == 1 && !dictIsRehashing(db->expires)) break;unsigned long idx = db->expires_cursor;idx &= db->expires->ht[table].sizemask;dictEntry *de = db->expires->ht[table].table[idx];long long ttl;/* Scan the current bucket of the current table. */checked_buckets++;while(de) {/* Get the next entry now since this entry may get* deleted. */dictEntry *e = de;de = de->next;ttl = dictGetSignedIntegerVal(e)-now;if (activeExpireCycleTryExpire(db,e,now)) expired++;if (ttl > 0) {/* We want the average TTL of keys yet* not expired. */ttl_sum += ttl;ttl_samples++;}sampled++;}}db->expires_cursor++;}total_expired += expired;total_sampled += sampled;/* Update the average TTL stats for this database. */if (ttl_samples) {long long avg_ttl = ttl_sum/ttl_samples;/* Do a simple running average with a few samples.* We just use the current estimate with a weight of 2%* and the previous estimate with a weight of 98%. */if (db->avg_ttl == 0) db->avg_ttl = avg_ttl;db->avg_ttl = (db->avg_ttl/50)*49 + (avg_ttl/50);}/* We can't block forever here even if there are many keys to* expire. So after a given amount of milliseconds return to the* caller waiting for the other active expire cycle. */if ((iteration & 0xf) == 0) { /* check once every 16 iterations. */elapsed = ustime()-start;if (elapsed > timelimit) {timelimit_exit = 1;server.stat_expired_time_cap_reached_count++;break;}}/* We don't repeat the cycle for the current database if there are* an acceptable amount of stale keys (logically expired but yet* not reclaimed). */} while (sampled == 0 ||(expired*100/sampled) > config_cycle_acceptable_stale);}elapsed = ustime()-start;server.stat_expire_cycle_time_used += elapsed;latencyAddSampleIfNeeded("expire-cycle",elapsed/1000);/* Update our estimate of keys existing but yet to be expired.* Running average with this sample accounting for 5%. */double current_perc;if (total_sampled) {current_perc = (double)total_expired/total_sampled;} elsecurrent_perc = 0;server.stat_expired_stale_perc = (current_perc*0.05)+(server.stat_expired_stale_perc*0.95);
}

        activeExpireCycle工作模式:

  • 函数每次运行时,都从一定数量的数据库中取一定数量的随机键进行检查,并删除其中的过期键。
  • 全部变量current_db会记录当前activeExpireCycle函数检查的进度,并在下一次调用activeExpireCycle函数调用时,接着上一次的进度进行处理。比如:当前在执行activeExpireCycle函数在遍历10号数据库时返回了,那么下一次调用activeExpireCycle函数时,从11号数据库开始查找并删除过期键。
  • 随着activeExpireCycle函数的不断执行,服务器中的所有数据库都会检查一遍,这时函数current_db变量重置为0,然后再次开始新一轮的检查工作。

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

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

相关文章

【网络编程】基于UDP数据报实现回显服务器/客户端程序

个人主页&#xff1a;兜里有颗棉花糖 欢迎 点赞&#x1f44d; 收藏✨ 留言✉ 加关注&#x1f493;本文由 兜里有颗棉花糖 原创 收录于专栏【网络编程】【Java系列】 本专栏旨在分享学习网络编程的一点学习心得&#xff0c;欢迎大家在评论区交流讨论&#x1f48c; 前言 我们如果…

插入排序详解(C语言)

前言 插入排序是一种简单直观的排序算法&#xff0c;在小规模数据排序或部分有序的情况下插入排序的表现十分良好&#xff0c;今天我将带大家学习插入排序的使用。let’s go ! ! ! 插入排序 插入排序的基本思想是将待排序的序列分为已排序和未排序两部分。初始时&#xff0c…

3D模型人物换装系统(二 优化材质球合批降低DrawCall)

3D模型人物换装系统 介绍原理合批材质对比没有合批材质核心代码完整代码修改总结 介绍 本文使用2018.4.4和2020.3.26进行的测试 本文没有考虑法线贴图合并的问题&#xff0c;因为生成法线贴图有点问题&#xff0c;放在下一篇文章解决在进行优化 如果这里不太明白换装的流程可以…

Spark Shell的简单使用

简介 Spark shell是一个特别适合快速开发Spark原型程序的工具&#xff0c;可以帮助我们熟悉Scala语言。即使你对Scala不熟悉&#xff0c;仍然可以使用这个工具。Spark shell使得用户可以和Spark集群交互&#xff0c;提交查询&#xff0c;这便于调试&#xff0c;也便于初学者使用…

蓝桥杯c/c++程序设计——数位排序

数位排序【第十三届】【省赛】【C组】 题目描述 小蓝对一个数的数位之和很感兴趣&#xff0c;今天他要按照数位之和给数排序。 当两个数各个数位之和不同时&#xff0c;将数位和较小的排在前面&#xff0c;当数位之和相等时&#xff0c;将数值小的排在前面。 例如&#xff0…

【序列化和反序列化】

&#x1f341;什么是序列化和反序列化&#xff1f; &#x1f341;典型解析&#x1f341;拓展知识仓&#x1f341;如何进行序列化和反序列化&#x1f341;未实现Serializable&#xff0c;可以序列化吗? &#x1f341;典型解析 在Java中&#xff0c;我们可以通过多种方式来创建对…

大师计划1.0 - log2 CRTO笔记

CRTOⅠ笔记 log2 这个笔记是我在2023年11月23日-12月22日中&#xff0c;学习CRTO所做的一些笔记。 事实上TryHackMe的路径和htb学院包含了许多CRTO的知识并且甚至还超出了CRTO&#xff08;CS除外&#xff09;&#xff0c;所以很多东西在THM和htb学院学过&#xff0c;这次CRTO等…

如何使用PatchaPalooza对微软每月的安全更新进行全面深入的分析

关于PatchaPalooza PatchaPalooza是一款针对微软每月安全更新的强大分析工具&#xff0c;广大研究人员可以直接使用该工具来对微软每月定期推送的安全更新代码进行详细、全面且深入的安全分析。 PatchaPalooza使用了微软MSRC CVRF API的强大功能来获取、存储和分析安全更新数…

探索 HTTP 请求的世界:get 和 post 的奥秘(下)

&#x1f90d; 前端开发工程师&#xff08;主业&#xff09;、技术博主&#xff08;副业&#xff09;、已过CET6 &#x1f368; 阿珊和她的猫_CSDN个人主页 &#x1f560; 牛客高级专题作者、在牛客打造高质量专栏《前端面试必备》 &#x1f35a; 蓝桥云课签约作者、已在蓝桥云…

MY FILE SERVER: 1

下载地址 https://download.vulnhub.com/myfileserver/My_file_server_1.ova 首先我们需要发现ip 我的kali是59.162所以167就是靶机的 然后我们拿nmap扫一下端口 nmap -sV -p- 192.168.59.167 扫完发现有七个端口开放 按照习惯先看80 没看到有啥有用信息,用nikto扫一下 nik…

Kafka日志文件存储

日志文件 kafka在server.properties配置文件中通过log.dir属性指定了Kafka的日志存储路径 核心文件 1. log文件 实际存储消息的日志文件, 大小固定1G(参数log.segment.bytes可配置), 写满后就会新增一个新的文件, 文件名是第一条消息的偏移量 2. index文件 以偏移量为索引…

IP代理科普| 共享IP还是独享IP?两者的区别与优势

通俗地讲&#xff0c;共享IP就像乘坐公共汽车一样&#xff0c;您可以到达目的地&#xff0c;但将与其他乘客共享旅程&#xff0c;座位很可能是没有的。独享IP就像坐出租车一样&#xff0c;您可以更快到达目的地&#xff0c;由于车上只有您一个人&#xff0c;座位是您一个人专用…

java实现深度优先搜索 (DFS) 算法

度优先搜索&#xff08;Depth First Search&#xff0c;DFS&#xff09;算法是一种用于遍历或搜索图或树的算法。这种算法从一个节点开始&#xff0c;沿着一条路径尽可能深地搜索&#xff0c;直到遇到不能继续前进的节点时返回上一个节点&#xff0c;然后继续搜索其他路径。具体…

智能优化算法应用:基于法医调查算法3D无线传感器网络(WSN)覆盖优化 - 附代码

智能优化算法应用&#xff1a;基于法医调查算法3D无线传感器网络(WSN)覆盖优化 - 附代码 文章目录 智能优化算法应用&#xff1a;基于法医调查算法3D无线传感器网络(WSN)覆盖优化 - 附代码1.无线传感网络节点模型2.覆盖数学模型及分析3.法医调查算法4.实验参数设定5.算法结果6.…

vs code 代码统计 插件 (webstorm统计代码)

https://blog.csdn.net/aikudexiaohai/article/details/129367503 安装插件 VS Code Counter使用快捷键 Ctrl Shift P&#xff0c;搜素“VSCodeCounter”&#xff0c;选择 Count lines in directory。 在文件路径搜索框中&#xff0c;补充待统计的目录&#xff0c;如&#x…

家校互通小程序实战开发01需求分析

目录 1 角色的划分2 用例分析3 创建业务数据源4 创建登录用户数据源总结 最近几年&#xff0c;随着移动互联网的深入发展&#xff0c;我们的日常生活和工作和微信已经紧密绑定。其实&#xff0c;有时候生活和工作的界限已经不明显&#xff0c;在我们的微信好友里既有家人、朋友…

看图了解ODF光纤配线架,详细熔接过程学习

弱电工程&#xff0c;远距离传输离不开光纤&#xff0c;只有光纤才能让网络传输的更远&#xff0c;今天了解光纤的配套产品&#xff0c;光纤配线架&#xff08;Optical Distribution Frame&#xff09;用于光纤通信系统中局端主干光缆的成端和分配&#xff0c;可方便地实现光纤…

多维时序 | MATLAB实CNN-BiGRU-Mutilhead-Attention卷积网络结合双向门控循环单元网络融合多头注意力机制多变量时间序列预测

多维时序 | MATLAB实现CNN-BiGRU-Mutilhead-Attention卷积网络结合双向门控循环单元网络融合多头注意力机制多变量时间序列预测 目录 多维时序 | MATLAB实现CNN-BiGRU-Mutilhead-Attention卷积网络结合双向门控循环单元网络融合多头注意力机制多变量时间序列预测预测效果基本介…

一起玩儿物联网人工智能小车(ESP32)——14. 用ESP32的GPIO控制智能小车运动起来(二)

摘要&#xff1a;本文主要讲解如何使用Mixly实现对单一车轮的运动控制。 下面就该用程序控制我们的小车轮子转起来了。打开Mixly软件&#xff0c;然后单击顶部“文件”菜单中的“新建”功能&#xff0c;我们来开启一个新程序的开发工作。 我们的工作同样是先从最简单的开始&am…

等级保护实施指南与定级指南标准

目录 前言 等级保护实施指南标准 主要思路 主要概念 实例 主要流程 等级保护定级指南标准 安全保护等级 定级原理 级别划分表 定级方法 业务信息安全保护等级矩阵表 系统服务安全保护等级矩阵表 补充内容 前言 《实施指南》介绍和描述了实施信息系统等级保护过…