Redis核心功能實現

前言

學習是個輸入的過程,在進行輸入之后再進行一些輸出,比如寫寫文章,筆記,或者做一些技術串講,雖然需要花費不少時間,但是好處很多,首先是能通過輸出給自己的輸入帶來一些動力,然后也能在寫文章和總結時加強自己學習的印象,在一些細節上能夠更加仔細深刻地理解其中一些邏輯關系,最后也能夠把自己的理解分享給別人,如果別人能夠幫你指出一些錯誤,將會有更多意想不到的收獲。

最近無聊在學習redis的源碼,又把之前讀過的書《redis設計與實現》看了一遍,主要并不是想抄襲或者背下redis的源碼,而是想在redis源碼中找到一些靈感,或者收獲一些體系結構或者實現細節上的方案,任何技術都是會過時的,但是解決問題的思路永遠不會。如果看完一篇文章或者一本書,你背下了所有redis核心部分的源碼,在作者看來意義不大,但是如果你能根據學到的東西自己重新實現一個redis,甚至根據自己的想法做一些優化,這才是真正的進步,

為了加深自己記憶,方便自己以后回憶學習,我寫了這個文章,把書上的一些我認為比較重要的內容記錄下來

一 Redis摘要

redis對自己的定義是

Redis is the world’s fastest in-memory database. It provides cloud and on-prem solutions for caching, vector search, and NoSQL databases that seamlessly fit into any tech stack—making it simple for digital customers to build, scale, and deploy the fast apps our world runs on.

? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?? --Redis官方介紹 -2025/4/8

其實就是一個開源的內存數據庫,但是它提到三個重要的詞語,這也是它主打的東西

fastest:關于這點,類似絕對的宣傳聽說在歐美是違法的,作者在編寫文章時已經向歐盟提交了舉報。

simple:使用redis是方便快捷的

caching, vector search, and NoSQL databases:它的設計初衷是用于緩存,矢量搜索,數據存儲,盡管很多人把他當成消息隊列,分布式鎖使用,redis后面也提供了專門的事件發布和監聽能力,但是估計也是不情愿的。

二 Redis的數據結構和類型的實現

本章節會介紹redis內部實現的一些數據結構和各種類型的值的實現。

2.1 Redis內置的數據結構

redis內部實現了一些用于存儲數據和數據結構,因為redis的各種各樣類型的鍵值對都是基于這些數據結構的,所以很有必要先分別介紹一下它們,但是因為這些數據結構都是計算機通用的內容,所以這里只會簡單介紹一下并且說一下它們特殊的地方(如果有)而不會花太多篇幅

2.1.1 SDS(動態字符串)

redis封裝了自己的字符串表示結構sdshdr(simple dynamic string),而沒有使用C語言傳統的字符串表示(以空字符串結尾的字符數組),絕大多數的字符串表示都是使用的sds,只有極少數不需要修改字符串的情況下才會使用C語言傳統的字符串表示,比如打印日志。

struct sdshdr {//字符串長度(不包含最后面的空字符)unsigned int len;//指示buf數組中未使用字節數unsigned int free;//存儲字符串內容,以空字符結尾char buf[];
};

sds相比于C語言字符串的優勢主要體現在

1 更快獲取到字符串長度

2 更容易管理字符串,減少內存重分配次數,字符串內容更新變長時,避免緩沖區溢出(通過free記錄剩下的空間,如果不夠可創建一個新的sdshdr),截斷字符串時不需要釋放空間以防止內存泄漏等

2.1.2?鏈表

redis的鏈表實現其實和傳統的鏈表并沒有太多差別,這里不做太多贅述

鏈表節點結構體

typedef struct listNode {//前驅節點struct listNode *prev;//后續節點struct listNode *next;//值void *value;
} listNode;

鏈表結構體

typedef struct list {//頭節點listNode *head;//尾節點listNode *tail;//長度unsigned long len;//節點值的復制函數,釋放函數,對比函數的函數指針...
} list;

2.1.3 字典

redis的字典使用哈希表作為底層實現,一個哈希表包含了多個哈希表節點,每個哈希表節點就表示一個鍵值對。

哈希表節點

typedef struct dictEntry {//鍵void *key;//值union {void *val;uint64_t u64;int64_t s64;double d;} v;//指向另一個哈希表節點,用于避免哈希沖突struct dictEntry *next;
} dictEntry;

需要注意的是redis是通過拉鏈法來避免哈希沖突的,所以需要next用來放置相同哈希值的key

哈希表

typedef struct dictht {//存儲哈希表節點,每個元素就是一個哈希表節點的指針(不保存哈希表節點本身)dictEntry **table;//哈希表大小,table數組長度,不代表哈希表節點數量unsigned long size;//哈希表掩碼,用于計算索引值,總是對于size - 1 unsigned long sizemask;//哈希表的節點數unsigned long used;
} dictht;

注意table本身并不直接保存哈希表節點,里面每個元素都是哈希表結點指針。

字典

typedef struct dict {//保存這個字典的哈希函數和鍵值的復制,對比,銷毀方法dictType *type;//字典的私有數據,type中的函數會用到這些數據void *privdata;//存儲數據的哈希表,字典只會使用ht[0],ht[1]只會在進行reheah時使用dictht ht[2];//如果正在進行rehash,指示當前rehash的索引,不在rehash時為-1long rehashidx; /* rehashing not in progress if rehashidx == -1 */
} dict;

dictType是用來放置哈希函數,鍵值對復制,對比,銷毀函數的結構體,這些方法會用到的私有數據會放在privdata中,rehashidx在redis進行rehash使用,rehash的內容會在后面的章節具體描述,這里可以只關注ht[0]即可,

以下是一個正常狀態下(沒有正在進行rehash)的字典的結構示例圖

2.1.4 跳躍表

redis的跳躍表由跳躍表節點zskiplistNode組成,結構體如下

跳躍表節點

typedef struct zskiplistNode {//節點存儲的值robj *obj;//分值,是一個doule類型的浮點數,跳躍表中的所有節點都按分值從小到大來排序。double score;//后退指針,每次只能后退至前一個節點struct zskiplistNode *backward;//層,數組可以包含多個元素,每個元素都包含一個指向其他節點的指針,程序可以通過這些層來加快訪問其他節點的速度struct zskiplistLevel {//前進指針struct zskiplistNode *forward;//跨度, 用于記錄兩個節點之間的距離,指向NULL的所有前進指針的跨度都為0unsigned int span;} level[];
} zskiplistNode;

跳躍表

typedef struct zskiplist {//頭節點和尾節點struct zskiplistNode *header, *tail;//節點數量unsigned long length;//性來記錄節點的數量 ,程序可以在O(L)復雜度內返回跳躍表的長度意。//leve1屬性則用于在O(1)復雜度內獲取跳躍表中層高最大的那個節點的層數量,表頭節點的層高并不計算在內int level;
} zskiplist;

初看上去,很容易以為跨度和遍歷操作有關,但實際上并不是這樣,遍歷操作只使用前 進指針就可以完成了,跨度實際上是用來計算排位(rank )的:在查找某個節點的過程中, 將沿途訪問過的所有層的跨度累計起來,得到的結果就是目標節點在跳躍表中的排位。 舉個例子,圖5- 4用虛線標記了在跳躍表中查找分值為3. 0、成員對象為。3的節點時, 沿途經歷的層:查找的過程只經過了一個層,并且層的跨度為3,所以目標節點在跳躍表中 的排位為3。?

再舉個例子,圖5-5用虛線標記了在跳躍表中查找分值為2. 0、成員對象為o2的節點 時,沿途經歷的層:在查找節點的過程中,程序經過了兩個跨度為1的節點,因此可以計算出,目標節點在跳躍表中的排位為2。

在同 一個跳躍表中,各個節點保存的成員對象必須是唯 一的,但是多個節點保存的分值卻可以是相同的,分值相同的節點將按照成員對象在字典序中的大小來進行排序,成員對象較小的節點會排在前面(靠近表頭的方向),而成員對象較大的節點則會排在后面

2.1.5 整數集合

整數集合(intset) 是Redis用于保存整數值的集合抽象數據結構 , 它可以保存類型為 int16,int32或者int64的整數值,并且保證集合中不會出現重復元素,它是集合鍵的底層實現之一,當一個集合只包含整數值元素,并且這個集合的元素數量不多時,Redis就會使用整數集合作為集合鍵的底層實現。

typedef struct intset {//編碼方式uint32_t encoding;//集合長度uint32_t length;//保持元素的數組int8_t contents[];
} intset;

contents 數組是整數集合的底層實現:整數集合的每個元素都是contents 數組的一個數組項(item),各個項在數組中按值的大小從小到大有序地排列,并且數組中不包含任何重復項,

雖然intset結構將contents屬性聲明為int8_t 類型的數組,但實際上contents數 組并不保存任何int8_t 類型的值,contents數組的真正類型取決于encoding屬性的值: 又如果encoding屬性的值為INTSET_ENC_INT16,那么content s 就是一個int16t類型的數組,數組里的每個項都是一個int 16_t 類型的整數值(最小值 為-32768,最大值為32767)。 又如果encoding屬性的值為INTSET_ENC_INT32,那么contents 就是一個int32 _ t 類型的數組 , 數組里的每個項都是一個int32 _ t類型的整數值,又如果encoding 屬性的值為INTSET_ENC_INT64,那么contents 就是一個int 64_ t 類型的數組,數組里的每個項都是一個int64_t 類型的整數值。

升級和降級

每當我們要將 一個新元素添加到整數集合里面,并且新元素的類型比整數集合現有所有元素的類型都要長時,整數集合需要先進行升級(upgrade),然后才能將新元素添加到整數集合里面。整數集合不支持降級操作, 一旦對數組進行了升級,編碼就會一直保持升級后的狀態,這里我們不再具體深入升級和降級的細節。

2.1.6 壓縮列表

壓縮列表(ziplist) 是列表鍵和哈希鍵的底層實現之一 。當一個列表鍵只包含少量列表項,并且每個列表項要么就是小整數值,要么就是長度比較短的字符串,那么Redis 就 會使用壓縮列表來做列表鍵的底層實現。壓縮列表是Redis 為了節約內存而開發的,是由一系列特殊編碼的連續內存塊組成的順序型數據結構 。 一個壓縮列表可以包含任意多個節點, 每個節點可以保存 一個字節數組或者 一個整數值

各組成部分說明

2.1.7 對象

Redis使用對象來表示數據庫中的鍵和值,每次當我們在Redis的數據庫中新創建一個鍵值對時,我們至少會創建兩個對象,一個對象用作鍵值對的鍵(鍵對象),另一個對象用作鍵值對的值(值對象)。

Redis中的每個對象都由一個redisobject結構表示,該結構中和保存數據有關的三個屬性分別是type屬性、encoding屬性和ptr屬性。

typedef struct redisObject {//記錄了對象的類型,可以是REDIS_STRING,REDIS_LIST,REDSI_HASH,REDSI_SET,REDSI_ZSETunsigned type:4;//指向對象的底層實現數據結構,這些數據結構由對象的encoding屬性決定void *ptr;//記錄對象所使用的編碼,也就是說說這個對象使用了什么數據結構作為對象的底層實現unsigned encoding:4;...
} robj;

type的枚舉

encoding的枚舉

每種類型的對象都可以根據情況使用多種不同的編碼,表8-4列出了每種類型的對象可以使用的編碼。

2.2?字符串對象

字符串對象的編碼可以是int 、raw或者embstr,

如果一個字符串對象保存的是整數值,并且這個 整數值可以用long類型來表示, 那么字符串對象會將整數值保存在字符串對象結構的ptr屬性里面 (將void*轉換成long), 并將字符串對象的編碼設置為int

如果字符串對象保存的是一個字符串值,并且這個字符串值的長度小于等于32字節,那么字符串對象將使用embstr編碼的方式來保存這個字符串值。embstr編碼是專門用于保存短字符串的一種優化編碼方式,這種編碼和raw編碼一樣,都使用redisObject結構和sdshdr結構來表示字符串對象,但raw編碼會調用兩次內存分配函數來分別創建redisobject結構和sdshdr結構,而embstr編碼則通過調用一次內存分配函數來分配一塊連續的空間,空間中依次包含redisObject和sdshdr兩個結構

2.3 列表對象

列表對象的編碼可以是ziplist或者linkedlist。

ziplist編碼的列表對象使用壓縮列表作為底層實現,每個壓縮列表節點(entry)保存了一個列表元素。舉個例子,如果我們執行以下RPUSH命令,那么服務器將創建一個列表對象作為numbers鍵的值:

redis> RPUSH numbers 1 "three" 5

(integer) 3

如果numbers鍵的值對象使用的是zip1ist編碼,

linkedlist編碼的列表對象使用雙端鏈表作為底層實現,每個雙端鏈表節點(node)都保存了一個字符串對象,而每個字符串對象都保存了一個列表元素。

2.4 哈希表對象

哈希對象的編碼可以是ziplist或者hashtable。

ziplist編碼的哈希對象使用壓縮列表作為底層實現,每當有新的鍵值對要加入到哈希對象時,程序會先將保存了鍵的壓縮列表節點推入到壓縮列表表尾,然后再將保存了值的壓縮列表節點推入到壓縮列表表尾,因此:

1 保存了同一鍵值對的兩個節點總是緊挨在一起,保存鍵的節點在前,保存值的節點在后。

2 添加到哈希對象中的鍵值對會被放在壓縮列表的表頭方向,而后來添加到哈希對象中的鍵值對會被放在壓縮列表的表尾方向。

舉個例子,如果我們執行以下HSET命令,那么服務器將創建一個列表對象作為profile鍵的值:

其對應存儲結構如下

hashtable編碼的哈希對象使用字典作為底層實現,哈希對象中的每個鍵值對都使用一個字典鍵值對來保存:

1 字典的每個鍵都是一個字符串對象,對象中保存了鍵值對的鍵;

2 字典的每個值都是一個字符串對象,對象中保存了鍵值對的值。

其對應存儲結構如下

2.5 集合對象

集合對象的編碼可以是intset或者hashtable。

intset編碼的集合對象使用整數集合作底層實現,集合對象包含的所有元素都被保存在整數集合里面。舉個例子,以下代碼將創建一個如圖8-12所示的intset編碼集合對象:

redis> SADD numbers 135 (integer)3

hashtable編碼的集合對象使用字典作為底層實現,字典的每個鍵都是一個字符串對象,每個字符串對象包含了一個集合元素,而字典的值則全部被設置為NULL。

2.6 有序集合對象

有序集合的編碼可以是ziplist或者skiplist。

ziplist編碼的壓縮列表對象使用壓縮列表作為底層實現,每個集合元素使用兩個緊挨在一起的壓縮列表節點來保存,第一個節點保存元素的成員(member),而第二個元素則保存元素的分值(score)。壓縮列表內的集合元素按分值從小到大進行排序,分值較小的元素被放置在靠近表頭的方向,而分值較大的元素則被放置在靠近表尾的方向。舉個例子,如果我們執行以下ZADD命令,那么服務器將創建一個有序集合對象作price鍵的值:

redis> ZADD price 8.5 apple 5.0 banana 6.0 cherry

(integer) 3

skiplist編碼的有序集合對象使用zset結構作為底層實現,一個zset結構同時包含一個字典和一個跳躍表:

三 Redis過期策略的實現

我們在使用redis時有時候會指定一些key的過期時間ttl,對于用戶而言,這個key到了指定的過期時間就會自動消失,這個章節會介紹一下redis對于key的過期策略的實現。

3.1 key的過期時間存儲

redisDb結構的expires字典保存了數據庫中所有鍵的過期時間,我們稱這個字典為過期字典:

1?過期字典的鍵是一個指針,這個指針指向鍵空間中的某個鍵對象(也即是某個數據庫鍵)。

2?過期字典的值是一個long long類型的整數,這個整數保存了鍵所指向的數據庫鍵的過期時間一一個毫秒精度的UNIX時間戳。

圖展示了一個帶有過期字典的數據庫例子,在這個例子中,鍵空間保存了數據庫中的所有鍵值對,而過期字典則保存了數據庫鍵的過期時間

很容易想到,我們在指定,修改,刪除某個key的ttl時,redis只需要修改過期字典這個key相應的值就可以了。而獲取某個key剩余的生存時間時,也只需要在過期字典中得到這個key的過期時間,然后減去當前時間返回就可以了。如果需要判定一個key是否過期了,只需要判斷過期字典中這個key(如果有)是否大于當前時間,

3.2 過期key的刪除策略

3.2.1 常用的過期策略

當redis的某個key過期后,redis肯定是需要清理它來釋放內存的,說的清理策略,現在常用的方案有三種

定時刪除

定時刪除策略對內存是最友好的,通過使用定時器,定時刪除策略可以保證過期鍵會盡可能快地被刪除,并釋放過期鍵所占用的內存,但是他的缺點也很明顯,如果設置過期時間的key太多時候,就需要注冊大量的定時器,而現在大多數定期器的實現本質上都是定時掃描,每次都需要掃描所有定時任務查看是否到達執行時間,時間復雜度是o(n),無疑會給系統造成巨大的負擔。

惰性刪除

就是每次用的時候判斷這個key是否過期,過期就把它刪了,這種方式實現簡單,性能損耗小,但是對于一些過期后沒有再訪問的key,會造成內存泄漏。

定期刪除

就是每隔一段時間掃描一些key,判斷是否過期,過期則刪除。定期刪除策略的難點是確定刪除操作執行的時長和頻率,在內存浪費和性能之間找到一個平衡。

如果刪除操作執行得太頻繁,或者執行的時間太長,定期刪除策略就會退化成定時刪除策略,以至于將CPU時間過多地消耗在刪除過期鍵上面。

如果刪除操作執行得太少,或者執行的時間太短,定期刪除策略又會和惰性刪除策略一樣,出現浪費內存的情況。

3.2.2?redis的過期策略

在前面,我們討論了定時刪除、惰性刪除和定期刪除三種過期鍵刪除策略,Redis服務器實際使用的是惰性刪除和定期刪除兩種策略:通過配合使用這兩種刪除策略,服務器可以很好地在合理使用CPU時間和避免浪費內存空間之間取得平衡。

惰性策略

過期鍵的惰性刪除策略由db.c/expirelfNeeded函數實現,所有讀寫數據庫的Redis命令在執行之前都會調用expireIfNeded函數對輸人鍵進行檢查。

如果輸入鍵已經過期,那么expireIfNeeded函數將輸人鍵從數據庫中刪除。

如果輸入鍵未過期,那么expireIfNeeded函數不做動作。

定期刪除策略

過期鍵的定期刪除策略由redis.c/activeExpireCycle函數實現,每當Redis的服務器周期性操作redis.c/serverCron函數執行時,activeExpirecycle函數就會被調用,它在規定的時間內,分多次遍歷服務器中的各個數據庫,從數據庫的expires字典中隨機檢查一部分鍵的過期時間,并刪除其中的過期鍵。

函數每次運行時,都從一定數量的數據庫中取出一定數量的隨機鍵進行檢查,并刪除其中的過期鍵。

全局變量current_db會記錄當前activeExpireCycle函數檢查的進度,并在下一次activeExpirecycle函數調用時,接著上一次的進度進行處理。比如說,如果當前activeExpireCycle函數在遍歷10號數據庫時返回了,那么下次activeExpirecycle函數執行時,將從11號數據庫開始查找并刪除過期鍵

隨著activeExpirecycle函數的不斷執行,服務器中的所有數據庫都會被檢查一遍,這時函數將current_db變量重置為0,然后再次開始新一輪的檢查工作。

3.2.3?AOF、RDB和復制功能對過期鍵的處理

RDB

在執行SAVE命令或者BGSAVE命令創建一個新的RDB文件時,程序會對數據庫中的鍵進行檢查,已過期的鍵不會被保存到新創建的RDB文件中。

在執行SAVE命令或者BGSAVE命令創建一個新的RDB文件時,程序會對數據庫中的鍵進行檢查,已過期的鍵不會被保存到新創建的RDB文件中。如果服務器以主服務器模式運行,那么在載人RDB文件時,程序會對文件中保存的鍵進行檢查,未過期的鍵會被載人到數據庫中,而過期鍵則會被忽略,所以過期鍵對載入RDB文件的主服務器不會造成影響,如果服務器以從服務器模式運行,那么在載人RDB文件時,文件中保存的所有鍵,不論是否過期,都會被載入到數據庫中。不過,因為主從服務器在進行數據同步的時候,從服務器的數據庫就會被清空,所以一般來講,過期鍵對載入RDB文件的從服務器也不會造成影響。

AOF

當服務器以AOF持久化模式運行時,如果數據庫中的某個鍵已經過期,但它還沒有被惰性刪除或者定期刪除,那么AOF文件不會因為這個過期鍵而產生任何影響。當過期鍵被惰性刪除或者定期刪除之后,程序會向AOF文件追加(append)一條DEL命令,來顯式地記錄該鍵已被刪除。

和生成RDB文件時類似,在執行AOF重寫的過程中,程序會對數據庫中的鍵進行檢查,已過期的鍵不會被保存到重寫后的AOF文件中。

四 Redis數據持久化的實現

任何機器都會有宕機的情況,redis服務器也不例外,一旦宕機,任何內存中的數據都會丟失,這是一個巨大的數據安全隱患,redis作為一個成熟的內存數據庫,在存儲數據的同時也會持久化這些數據,便于服務重啟或者宕機后的數據恢復,這就涉及到了性能,硬盤占用和數據恢復完整度之間的平衡,本章節會介紹一下redis實現的數據持久化的實現。

4.1 RDB持久化

因為Redis是內存數據庫,它將自己的數據庫狀態儲存在內存里面,所以如果不想辦法將儲存在內存中的數據庫狀態保存到磁盤里面,那么一旦服務器進程退出,服務器中的數據庫狀態也會消失不見。為了解決這個問題,Redis提供了RDB持久化功能,這個功能可以將Redis在內存中的數據庫狀態保存到磁盤里面,避免數據意外丟失。RDB持久化既可以手動執行,也可以根據服務器配置選項定期執行,該功能可以將某個時間點上的數據庫狀態保存到一個RDB文件中,RDB持久化功能所生成的RDB文件是一個經過壓縮的二進制文件,通過該文件可以還原生成RDB文件時的數據庫狀態。

因為RDB文件是保存在硬盤里面的,所以即使Redis服務器進程退出,甚至運行Redis服務器的計算機停機,但只要RDB文件仍然存在,Redis服務器就可以用它來還原數據庫狀態

4.1.1 RDB文件的創建和載入

有兩個Redis命令可以用于生成RDB文件,一個是SAVE,另一個是BGSAVE。SAVE命令會阻塞Redis服務器進程(客戶端發送的所有命令請求都會被拒絕),直到RDB文件創建完畢為止,在服務器進程阻塞期間,服務器不能處理任何命令請求,BGSAVE命令會派生出一個子進程,然后由子進程負責創建RDB文件,服務器進程(父進程)繼續處理命令請求。

和使用SAVE命令或者BGSAVE命令創建RDB文件不同,RDB文件的載入工作是在服務器啟動時自動執行的,所以Redis并沒有專門用于載人RDB文件的命令,只要Redis服務器在啟動時檢測到RDB文件存在,它就會自動載人RDB文件。服務器在載入RDB文件期間,會一直處于阻塞狀態(客戶端發送的所有命令請求都會被拒絕),直到載人工作完成為止。另外,因為AOF文件的更新頻率通常比RDB文件的更新頻率高,所以

1 如果服務器開啟了AOF持久化功能,那么服務器會優先使用AOF文件來還原數據庫狀態。

2 只有在AOF持久化功能處于關閉狀態時,服務器才會使用RDB文件來還原數據庫狀態。

SAVE,BGSAVE,BGREWRITEAOF執行關系

1 在BGSAVE命令執行期間,客戶端發送的SAVE命令會被服務器拒絕,服務器禁止SAVE命令和BGSAVE命令同時執行是了避免父進程(服務器進程)和子進程同時執行兩個rdbSave調用,防止產生竟爭條件

2?在BGSAVE命令執行期間,客戶端發送的BGSAVE命令會被服務器拒絕,因為同時執行兩個BGSAVE命令也會產生競爭條件

3?如果BGSAVE命令正在執行,那么客戶端發送的BGREWRITEAOF命令會被延遲到BGSAVE命令執行完畢之后執行。

4?如果BGREWRITEAOF命令正在執行,那么客戶端發送的BGSAVE命令會被服務器拒絕

BGREWRITEAOF和BGSAVE兩個命令的實際工作都由子進程執行,所以這兩個命令在操作方面并沒有什么沖突的地方,不能同時執行它們只是一個性能方面的考慮。

4.1.2?自動間隔性保持

功能

因為BGSAVE命令可以在不阻塞服務器進程的情況下執行,所以Redis允許用戶通過設置服務器配置的save選項,讓服務器每隔一段時間自動執行一次BGSAVE命令。用戶可以通過save選項設置多個保存條件,但只要其中任意一個條件被滿足,服務器就會執行BGSAVE命令。

舉個例子,如果我們向服務器提供以下配置:

save 900 1

save 300 10

save 60 10000

那么只要滿足以下三個條件中的任意一個,BGSAVE命令就會被執行:

1 服務器在900秒之內,對數據庫進行了至少1次修改。

2 服務器在300秒之內,對數據庫進行了至少10次修改

3 服務器在60秒之內,對數據庫進行了至少10000次修改。

如果用戶沒有主動設置save選項,那么服務器會為save選項設置默認條件,默認條件就是上面的例子,

保存save條件-saveparams

redis使用saveparams屬性來保存自動save的條件,saveparams屬性是一個數組,數組中的每個元素都是一個saveparam結構,每個saveparam結構都保存了一個save選項設置的保存條件

struct saveparam {time_t seconds;int changes;
};

比如上面的例子,saveparams的存儲結構如下

save信息保存-dirty和lastsave

服務器狀態還維持著一個dirty計數器,以及一個lastsave屬性:

1 dirty計數器記錄距離上一次成功執行SAVE命令或者BGSAVE命令之后,服務器對數據庫狀態(服務器中的所有數據庫)進行了多少次修改(包括寫入、刪除、更新等操作)。

2 lastsave屬性是一個UNIX時間戳,記錄了服務器上一次成功執行SAVE命令或者BGSAVE命令的時間

struct redisServer ( //...// 修改計數器 long long dirty;// 上一次執行保存的時間 time_t lastsave;//...
};

檢查保存條件是否満足

Redis的服務器周期性操作函數serverCron默認每隔100毫秒就會執行一次,該函數用于對正在運行的服務器進行維護,它的其中一項工作就是檢查save選項所設置的保存條件是否已經滿足,如果滿足的話,就執行BGSAVE命令.程序會遍歷并檢查saveparams數組中的所有保存條件,只要有任意一個條件被滿足,那么服務器就會執行BGSAVE命令。

4.1.3?RDB文件結構

圖10-10展示了一個完整RDB文件所包含的各個部分

RDB文件的最開頭是REDIS部分,這個部分的長度為5字節,保存著“REDIS"五個字符。通過這五個字符,程序可以在載人文件時,快速檢查所載人的文件是否RDB文件

文件的版本號,比如“0006"就代表RDB文件的版本為第六版。本章只介紹第六版RDB文件的結構。

databases部分包含著零個或任意多個數據庫,以及各個數據庫中的鍵值對數據,如果服務器的數據庫狀態為空(所有數據庫都是空的),那么這個部分也空,長度為0字節

EOF常量的長度為1字節,這個常量標志著RDB文件正文內容的結束,當讀入程序遇到這個值的時候,它知道所有數據庫的所有鍵值對都已經載人完畢了。

check_sum是一個8字節長的無符號整數,保存著一個校驗和,這個校驗和是程序通過對REDIS、db_version、databases、EOF四個部分的內容進行計算得出的。服務器在載入RDB文件時,會將載入數據所計算出的校驗和與check_sum所記錄的校驗和進行對比,以此來檢查RDB文件是否有出錯或者損壞的情況出現。

作為例子,圖10-11展示了一個databases部分為空的RDB文件:文件開頭的"REDIS"表示這是一個RDB文件,之后的"0006"表示這是第六版的RDB文件,因為databases為空,所以版本號之后直接跟著EOF常量,最后的6265312314761917404是文件的校驗和。

databases部分

一個RDB文件的databases部分可以保存任意多個非空數據庫。例如,如果服務器的0號數據庫和3號數據庫非空,那么服務器將創建一個如圖10-12所示的RDB文件,圖中的database0代表0號數據庫中的所有鍵值對數據,而database3則代表3號數據庫中的所有鍵值對數據

每個非空數據庫在RDB文件中都可以保存為SELECTDB、db_number、key_value_pairs三個部分,如圖10-13所示。

SELECTDB常量的長度為1字節,當讀人程序遇到這個值的時候,它知道接下來要讀人的將是一個數據庫號碼

db_number保存著一個數據庫號碼,根據號碼的大小不同,這個部分的長度可以是1字節、2字節或者5字節。當程序讀入db-_number部分之后,服務器會調用SELECT命令,根據讀人的數據庫號碼進行數據庫切換,使得之后讀入的鍵值對可以載人到正確的數據庫中

key_value_pairs部分保存了數據庫中的所有鍵值對數據,如果鍵值對帶有過期時間,那么過期時間也會和鍵值對保存在一起。根據鍵值對的數量、類型、內容以及是否有過期時間等條件的不同,key_value_pairs部分的長度也會有所不同。每個key_value_Pairs部分都保存了一個或以上數量的鍵值對,如果鍵值對帶有過期時間的話,那么鍵值對的過期時間也會被保存在內

EXPIRETIME_MS常量的長度為1字節,它告知讀人程序,接下來要讀人的將是一個以毫秒為單位的過期時間。

ms是一個8字節長的帶符號整數,記錄著一個以毫秒為單位的UNIX時間戳,這個時間戳就是鍵值對的過期時間。

TYPE記錄了value的類型,長度1字節,值可以是以下常量的其中一個

? REDIS_RDB_TYPE_STRING

? REDIS_RDB_TYPE_LIST

? REDIS_RDB_TYPE_SET

? REDIS_RDB_TYPE_ZSET

? REDIS_RDB_TYPE_HASH

? REDIS_RDB_TYPE_LIST_ZIPLIST

? REDIS_RDB_TYPE_SET_INTSET

? REDIS_RDB_TYPE_ZSET_ZIPLIST

? REDIS_RDB_TYPE_HASH_ZIPLIST

以上列出的每個TYPE常量都代表了一種對象類型或者底層編碼,當服務器讀入RDB文件中的鍵值對數據時,程序會根據TYPE的值來決定如何讀人和解釋value的數據。key和value分別保存了鍵值對的鍵對象和值對象,其中key總是一個字符串對象,它的編碼方式和REDIS_RDB_TYPE_STRING類型的value一樣。根據內容長度的不同,key的長度也會有所不同。

4.2 AOF持久化

4.2.1 AOF流程和文件內容

除了RDB持久化功能之外,Redis還提供了AOF(AppendOnlyFile)持久化功能。與RDB持久化通過保存數據庫中的鍵值對來記錄數據庫狀態不同,AOF持久化是通過保存Redis服務器所執行的寫命令來記錄數據庫狀態的,如圖11-1所示。

被寫人AOF文件的所有命令都是以Redis的命令請求協議格式保存的,因為Redis的命令請求協議是純文本格式,所以我們可以直接打開一個AOF文件,觀察里面的內容。下面是一個AOF文件例子

redis> SET msg "hello"

OK


redis> SADD fruits "app le" "banana" "cherry"

(integer) 3


redis > RPUSH numbers 128 256 512

(integer) 3

*2 \r\n$6\r\nSELECT\r\n$1\r\n0\r\n
*3\r\n$3\r\nSET\r\n$3\r\nmsg\r\n$5\r\nhello\r\n *5\r\n$4\r\nSADD\r\n$6\r\nfruits\r\n$5\r\napple\r\n$6\r\nbanana\r\n$6\r\ncherry\r\n *5\r\n$5\r\RPUSH\r\n$7\r\nnumbers\r\n$3\r\n128\r\n$3\r\256\r\n$3\r\n512\r\n

在這個AOF文件里面,除了用于指定數據庫的SELECT命令是服務器自動添加的之外,其他都是我們之前通過客戶端發送的命令。服務器在啟動時,可以通過載人和執行AOF文件中保存的命令來還原服務器關閉之前的數據庫狀態。

4.2.2?AOF持久化的實現

AOF持久化功能的實現可以分為命令追加(append)、文件寫人、文件同步(sync)三個步驟。

命令追加

當AOF持久化功能處于打開狀態時,服務器在執行完一個寫命令之后,會以協議格式將被執行的寫命令追加到服務器狀態的aof_buf緩沖區的末尾。

struct redisServer (//...//AOF緩存區sds aof_buf;//...
};

AOF文件的寫入和同步

Redis的服務器進程就是一個事件循環(loop),這個循環中的文件事件負責接收客戶端的命令請求,以及向客戶端發送命令回復,而時間事件則負責執行像serverCron函數這樣需要定時運行的函數。因為服務器在處理文件事件時可能會執行寫命令,使得一些內容被追加到aof_buf緩沖區里面,所以在服務器每次結束一個事件循環之前,它都會調用flushappendOnlyFile函數,考慮是否需要將aof_buf緩沖區中的內容寫入和保存到AOF文件里面

flushAppendOnlyFile函數的行為由服務器配置的appendfsync選項的值來決定,各個不同值產生的行為如表11-1所示。

這里的同步指的就是強制刷盤(因為大部分系統對于文件讀寫有緩存)

如果用戶沒有主動為appendfsync選項設置值,那么appendfsync選項的默認值為everysec,關于appendfsync選項的更多信息,請參考Redis項目附帶的示例配置文件redis.conf。

AOF文件的載入與數據還原

因為AOF文件里面包含了重建數據庫狀態所需的所有寫命令,所以服務器只要讀人并重新執行一遍AOF文件里面保存的寫命令,就可以還原服務器關閉之前的數據庫狀態。Redis讀取AOF文件并還原數據庫狀態的詳細步驟如下

1?創建一個不帶網絡連接的偽客戶端(fakeclient):因為Redis的命令只能在客戶端上下文中執行,而載人AOF文件時所使用的命令直接來源于AOF文件而不是網絡連接,所以服務器使用了一個沒有網絡連接的偽客戶端來執行AOF文件保存的寫命令,偽客戶端執行命令的效果和帶網絡連接的客戶端執行命令的效果完全一樣

2?從AOF文件中分析并讀取出一條寫命令。

3?使用偽客戶端執行被讀出的寫命令。

4?一直執行步驟2和步驟3,直到AOF文件中的所有寫命令都被處理完畢為止

AOF 重寫和其實現

因為AOF持久化是通過保存被執行的寫命令來記錄數據庫狀態的,所以隨著服務器運行時間的流逝,AOF文件中的內容會越來越多,文件的體積也會越來越大,如果不加以控制的話,體積過大的AOF文件很可能對Redis服務器、甚至整個宿主計算機造成影響,并且AOF文件的體積越大,使用AOF文件來進行數據還原所需的時間就越多

為了解決AOF文件體積膨脹的問題,Redis提供了AOF文件重寫(rewrite)功能。通過該功能,Redis服務器可以創建一個新的AOF文件來替代現有的AOF文件,新舊兩個AOF文件所保存的數據庫狀態相同,但新AOF文件不會包含任何浪費空間的冗余命令,所以新AOF文件的體積通常會比IAOF文件的體積要小得多。

雖然Redis將生成新AOF文件替換舊日AOF文件的功能命名為“AOF文件重寫”,但實際上,AOF文件重寫并不需要對現有的AOF文件進行任何讀取、分析或者寫人操作,這個功能是通過讀取服務器當前的數據庫狀態來實現的

AOF重寫程序放在子進程里執行,這樣做可以同時達到兩個目的

1子進程進行AOF重寫期間,服務器進程(父進程)可以繼續處理命令請求。

2子進程帶有服務器進程的數據副本,使用子進程而不是線程,可以在避免使用鎖的情況下,保證數據的安全性。

不過,使用子進程也有一個問題需要解決,因為子進程在進行AOF重寫期間,服務器進程還需要繼續處理命令請求,而新的命令可能會對現有的數據庫狀態進行修改,從而使得服務器當前的數據庫狀態和重寫后的AOF文件所保存的數據庫狀態不一致,為了解決這種數據不一致問題,Redis服務器設置了一個AOF重寫緩沖區,這個緩沖區在服務器創建子進程之后開始使用,當Redis服務器執行完一個寫命令之后,它會同時將這個寫命令發送給AOF緩沖區和AOF重寫緩沖區,如圖11-4所示。

當子進程完成AOF重寫工作之后,它會向父進程發送一個信號,父進程在接到該信號之后,會調用一個信號處理函數,并執行以下工作:

1將AOF重寫緩沖區中的所有內容寫人到新AOF文件中,這時新AOF文件所保存的數據庫狀態將和服務器當前的數據庫狀態一致。

2?對新的AOF文件進行改名,原子地(atomic)覆蓋現有的AOF文件,完成新舊兩個AOF文件的替換。

這個信號處理函數執行完畢之后,父進程就可以繼續像往常一樣接受命令請求了,在整個AOF后臺重寫過程中,只有信號處理函數執行時會對服務器進程(父進程)造成阻塞,在其他時候,AOF后臺重寫都不會阻塞父進程,這將AOF重寫對服務器性能造成的影響降到了最低。

五 Redis服務器實現

5.1 事件

Redis服務器是一個事件驅動程序,服務器需要處理以下兩類事件:

文件事件(fileevent):Redis服務器通過套接字與客戶端(或者其他Redis服務器)進行連接,而文件事件就是服務器對套接字操作的抽象。服務器與客戶端(或者其他服務器)的通信會產生相應的文件事件,而服務器則通過監聽并處理這些事件來完成一系列網絡通信操作。

時間事件(timeevent):Redis服務器中的一些操作(比如serverCron函數)需要在給定的時間點執行,而時間事件就是服務器對這類定時操作的抽象。

下面將對文件事件和時間事件進行介紹,說明這兩種事件在Redis服務器中的應用,它們的實現方法,以及處理這些事件的API等等。

5.1.1 文件事件

Redis基于Reactor模式開發了自己的網絡事件處理器:這個處理器被稱為文件事件處理器(file event handler):

文件事件處理器使用I/O多路復用(multiplexing)程序來同時監聽多個套接字,并根據套接字目前執行的任務來為套接字關聯不同的事件處理器。

當被監聽的套接字準備好執行連接應答(accept)、讀取(read)、寫人(write)、關閉(close)等操作時,與操作相對應的文件事件就會產生,這時文件事件處理器就會調用套接字之前關聯好的事件處理器來處理這些事件。雖然文件事件處理器以單線程方式運行,但通過使用I/O多路復用程序來監聽多個套接字,文件事件處理器既實現了高性能的網絡通信模型,又可以很好地與Redis服務器中其他同樣以單線程方式運行的模塊進行對接,這保持了Redis內部單線程設計的簡單性。

文件事件處理器的四個組成部分,它們分別是套接字、1/O多路復用程序、文件事件分派器(dispatcher),以及事件處理器。

套接字

文件事件是對套接字操作的抽象,每當一個套接字準備好執行連接應答命令請求處理器(accept)、寫人、讀取、關閉等操作時,就會產生一個文件事件。因為一個服務器命冬回復處理器通常會連接多個套接字,所以多個文件事件有可能會并發地出現。

I/O多路復用程序

I/O多路復用程序負責監聽多個套接字,并向文件事件分派器傳送那些產生了事件的套接字。盡管多個文件事件可能會并發地出現,但1/O多路復用程序總是會將所有產生事件的套接字都放到一個隊列里面,然后通過這個隊列,以有序(sequentially)、同步(synchronously)、每次一個套接字的方式向文件事件分派器傳送套接字。當上一個套接字產生的事件被處理完畢之后(該套接字為事件所關聯的事件處理器執行完畢),1/O多路復用程序才會繼續向文件事件分派器傳送下一個套接字。

Redis的I/O多路復用程序的所有功能都是通過包裝常見的select、epo11、evport和kqueue這些1/O多路復用函數庫來實現的,每個I/O多路復用函數庫在Redis源碼中都對應一個單獨的文件,比如ae_select.c、ae_epoll.c、ae_kqueue.C,諸如此類。因為Redis為每個1/0多路復用函數庫都實現了相同的API,所以1/O多路復用程序的底層實現是可以互換的編譯時會根據系統,硬件等情況自動選擇系統中性能最高的1/O多路復用函數庫來作為Redis的1/O多路復用程序的底層實現:

文件事件分派器

文件事件分派器接收1/O多路復用程序傳來的套接字,并根據套接字產生的事件的類型,調用相應的事件處理器。

事件處理器

服務器會為執行不同任務的套接字關聯不同的事件處理器,這些處理器是一個個函數,它們定義了某個事件發生時,服務器應該執行的動作。Redis為文件事件編寫了多個處理器,這些事件處理器分別用于實現不同的網絡通信需求,比如說:

1 為了對連接服務器的各個客戶端進行應答,服務器要為監聽套接字關聯連接應答處理器。

2 為了接收客戶端傳來的命令請求,服務器要為客戶端套接字關聯命令請求處理器。

3 為了向客戶端返回命令的執行結果,服務器要為客戶端套接字關聯命令回復處理器。

4 當主服務器和從服務器進行復制操作時,主從服務器都需要關聯特別為復制功能編寫的復制處理器。

在這些事件處理器里面,服務器最常用的要數與客戶端進行通信的連接應答處理器、命令請求處理器和命令回復處理器。

事件的類型

I/O多路復用程序可以監聽多個套接字的ae.h/AE_READABLE事件和ae.h/AE_WRITABLE事件,這兩類事件和套接字操作之間的對應關系如下:

1 當套接字變得可讀時(客戶端對套接字執行write操作,或者執行close操作),或者有新的可應答(acceptable)套接字出現時(客戶端對服務器的監聽套接字執行connect操作),套接字產生AE_READABLE事件。

2 當套接字變得可寫時(客戶端對套接字執行read操作),套接字產生AB_WRITABLE事件。

I/O多路復用程序允許服務器同時監聽套接字的AE_READABLE事件和AE_WRITABLE事件,

如果一個套接字同時產生了這兩種事件,那么文件事件分派器會優先處理AB_READABLE事件,等到AB_READABIE事件處理完之后,才處理AB_WRITABLE事件。這也就是說,如果一個套接字又可讀又可寫的話,那么服務器將先讀套接字,后寫套接字

5.1.2 時間事件

介紹

Redis的時間事件分為以下兩類:

1 定時事件:讓一段程序在指定的時間之后執行一次。比如說,讓程序X在當前時間的30毫秒之后執行一次。

2 周期性事件:讓一段程序每隔指定時間就執行一次。比如說,讓序Y每隔30毫秒就執行一次。

一個時間事件主要由以下三個屬性組成:

1 id:服務器為時間事件創建的全局唯一1D(標識號)。ID號按從小到大的順序遞增,新事件的ID號比舊事件的ID號要大。

2 when:毫秒精度的UNIX時間戳,記錄了時間事件的到達(arrive)時間。

3 timeProc:時間事件處理器,一個函數。當時間事件到達時,服務器就會調用相應的處理器來處理事件。

一個時間事件是定時事件還是周期性事件取決于時間事件處理器的返回值,如果事件處理器返回ae.h/AB_NOMORE,那么這個事件為定時事件:該事件在達到一次之后就會被刪除,之后不再到達。如果事件處理器返回一個非AE_NOMORE的整數值,那么這個事件為周期性時間:當一個時間事件到達之后,服務器會根據事件處理器返回的值,對時間事件的when屬性進行更新,讓這個事件在一段時間之后再次到達,并以這種方式一直更新并運行下去。比如說,如果一個時間事件的處理器返回整數值30,那么服務器應該對這個時間事件進行更新,讓這個事件在30毫秒之后再次到達。

實現

服務器將所有時間事件都放在一個無序鏈表中,每當時間事件執行器運行時,它就遍歷鱉個鏈表,查找所有已到達的時間事件,并調用相應的事件處理器。圖12-8展示了一個保存時間事件的鏈表的例子,鏈表中包含了三個不同的時間事件:因為新的時間事件總是插入到鏈表的表頭,所以三個時間事件分別按ID逆序排序,表頭事件的ID為3,中間事件的ID為2,表尾事件的ID為1。

注意,我們說保存時間事件的鏈表無序鏈表,指的不是鏈表不按ID排序,而是說,該鏈表不按when屬性的大小排序。正因為鏈表沒有按when屬性進行排序,所以當時間事件執行器運行的時候,它必須遍歷鏈表中的所有時間事件,這樣才能確保服務器中所有已到達的時間事件都會被處理。

時間事件應用實例-serverCron函數

持續運行的Redis服務器需要定期對自身的資源和狀態進行檢查和調整,從而確保服務器可以長期、穩定地運行,這些定期操作由redis.c/serverCron函數負責執行,它的主要工作包括:

1 更新服務器的各類統計信息,比如時間、內存占用、數據庫占用情況等。

2 清理數據庫中的過期鍵值對。

3 關閉和清理連接失效的客戶端。

4 嘗試進行AOF或RDB持久化操作。

5 如果服務器是主服務器,那么對從服務器進行定期同步。又如果處于集群模式,對集群進行定期同步和連接測試。

Redis服務器以周期性事件的方式來運行serverCron函數,在服務器運行期間,每隔一段時間,serverCron就會執行一次,直到服務器關閉為止。在Redis2.6版本,服務器默認規定serverCron每秒運行10次,平均每間隔100毫秒運行一次。從Redis2.8開始,用戶可以通過修改hz選項來調整serverCron的每秒執行次數,具體信息請參考示例配置文件redis.conf關于hz選項的說明。

5.1.3 事件的調度與執行

因為服務器中同時存在文件事件和時間事件兩種事件類型,所以服務器必須對這兩種事件進行調度,決定何時應該處理文件事件,何時又應該處理時間事件,以及花多少時間來處理它們等等。事件的調度和執行由ae.c/aeProcessEvents函數負責,以下是該函數的偽代碼表示:

def aeProcessEvents () :

? ? # 獲取到達時間離當前時間最接近的時間事件

? ? time_event = aeSearchNearestTimer ()

? ? # 計算最接近的時間事件距離到達還有多少毫秒

? ? remaind ms_ = time_event.when - unix_ts_now()

? ? #如果事件已到達,那么remaind_ms的值可能為負數 ,將它設定為0

? ? if remaind_ms < 0:

? ? ? ? remaind_ms = 0

????# 根據 remaind_ms 的值,創建timeval 結構

? ? timeval = create_timeval_with_ms (remaind_ms)

? ? # 阻塞并等待文件事件產生,最大阻塞時間由傳入的timeval結構決定

? ? # 如果remaind_ms的值為0,那么aeApiPoll調用之后馬上返回 ,不阻塞

? ? aeApiPoll (timeval)

? ? # 處理所有已產生的文件事件

? ? processFileEvents()

? ? # 處理所有已到達的時間事件

? ? processTimeEvents ()

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/web/75566.shtml
繁體地址,請注明出處:http://hk.pswp.cn/web/75566.shtml
英文地址,請注明出處:http://en.pswp.cn/web/75566.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

RPA VS AI Agent

圖片來源網絡 RPA&#xff08;機器人流程自動化&#xff09;和AI Agent&#xff08;人工智能代理&#xff09;在自動化和智能化領域各自扮演著重要角色&#xff0c;但它們之間存在顯著的區別。以下是對兩者區別的詳細分析&#xff1a; 一、定義與核心功能 RPA&#xff08;機…

多模態大語言模型arxiv論文略讀(十五)

Jailbreaking GPT-4V via Self-Adversarial Attacks with System Prompts ?? 論文標題&#xff1a;Jailbreaking GPT-4V via Self-Adversarial Attacks with System Prompts ?? 論文作者&#xff1a;Yuanwei Wu, Xiang Li, Yixin Liu, Pan Zhou, Lichao Sun ?? 研究機構…

第1節:計算機視覺發展簡史

計算機視覺與圖像分類概述&#xff1a;計算機視覺發展簡史 計算機視覺&#xff08;Computer Vision&#xff09;作為人工智能領域的重要分支&#xff0c;是一門研究如何使機器"看"的科學&#xff0c;更具體地說&#xff0c;是指用攝影機和計算機代替人眼對目標進行識…

【工具】Fiddler抓包

本文主要講解如何使用Fiddler抓HTTP包&#xff0c;可通過所抓包內容分析HTTP請求/響應的細節 安裝與配置 1.下載與安裝 下載地址: https://www.telerik.com/fiddler/ 點擊了鏈接后&#xff0c;跳轉到以下頁面&#xff1a; 點擊Fiddler Classic(免費版)后&#xff0c;跳轉到以…

STM32F103復用JTAG/SWD引腳為GPIO

普中-精靈1開發板&#xff0c;主芯片為STM32F103C8T6&#xff0c;4個獨立按鍵K1~K4依次接PA15~PA12&#xff0c;按下為低電平&#xff0c;8個LED燈D1~D8&#xff0c;依次接PA0~PA7。查詢手冊得知&#xff1a;PA15主功能為JTDI&#xff0c;PA14為JTCK/SWCLK&#xff0c;PA13為JT…

難度偏低,25西電人工智能學院821、833、834考研錄取情況

1、人工智能學院各個方向 2、人工智能學院近三年復試分數線對比 學長、學姐分析 由表可看出&#xff1a; 1、智能院25年院線相對于24年院線 全部專業下降比較多&#xff0c;其中控制科學與工程下降20分&#xff0c;計算機科學與技術下降20分&#xff0c;計算機技術[專碩]下降…

達夢數據校驗系統(DMDVS):數據完整性保障的不二之選

產品概述 達夢數據校驗系統(DMDVS)是一款企業級數據一致性管理平臺,提供跨數據庫、跨平臺的數據比對與修復能力。系統采用模塊化架構設計,支持靜態校驗、動態校驗、單向校驗及分布式校驗四大核心模式,適用于數據遷移驗證、容災備份核查、實時同步監控等關鍵場景,??更多…

【3dSwap】3D-Aware Face Swapping

文章目錄 3D-Aware Face Swapping背景points貢獻方法從2D圖像推斷3D先驗通過潛在代碼操縱進行人臉交換聯合樞軸調整目標函數實驗與二維人臉交換方法比較進一步分析3D感知人臉交換消融實驗局限性3D-Aware Face Swapping 會議/期刊:CVPR 2023 作者: code:https://lyx0208.gi…

客戶案例 | 日事清×初心家居:多部門協作實現新品上架自動化

1、客戶背景 佛山市初心家居有限公司&#xff0c;主營家居類目&#xff0c;年營收額近億元。初心家居有自己的家居生產工廠&#xff08;可為第三方提供生產&#xff09;&#xff0c;店內產品均為自主研發設計&#xff0c;所以新品開發也是初心家居的核心。 2、客戶工作場景及需…

KWDB創作者計劃—KWDB多副本集群保姆級部署

&#x1f4e2;&#x1f4e2;&#x1f4e2;&#x1f4e3;&#x1f4e3;&#x1f4e3; 作者&#xff1a;IT邦德 中國DBA聯盟(ACDU)成員&#xff0c;10余年DBA工作經驗 Oracle、PostgreSQL ACE CSDN博客專家及B站知名UP主&#xff0c;全網粉絲10萬 擅長主流Oracle、MySQL、PG、高斯…

micro ubuntu 安裝教程

micro ubuntu 安裝教程 官網地址 : https://micro-editor.github.io 以下是在 Ubuntu 系統中安裝 micro 編輯器 的詳細教程&#xff1a; 方法 1&#xff1a;通過 ?apt?? 直接安裝&#xff08;推薦&#xff09; 適用于 Ubuntu 20.04 及以上版本&#xff08;官方倉庫已收錄…

Docker 鏡像 的常用命令介紹

拉取鏡像 $ docker pull imageName[:tag][:tag] tag 不寫時&#xff0c;拉取的 是 latest 的鏡像查看鏡像 查看所有本地鏡像 docker images or docker images -a查看完整的鏡像的數字簽名 docker images --digests查看完整的鏡像ID docker images --no-trunc只查看所有的…

從零搭建微服務項目Pro(第0章——微服務項目腳手架搭建)

前言&#xff1a; 在本專欄Base第0章曾介紹一種入門級的微服務項目搭建&#xff0c;盡管后續基于此框架上實現了Nacos、Eureka服務注冊發現、配置管理、Feign調用、網關模塊、OSS文件存儲、JSR參數校驗、LogBack日志配置&#xff0c;鑒權模塊、定時任務模塊等&#xff0c;但由于…

VS Code下開發FPGA——FPGA開發體驗提升__下

上一篇&#xff1a;IntelliJ IDEA下開發FPGA-CSDN博客 Type&#xff1a;Quartus 一、安裝插件 在應用商店先安裝Digtal IDE插件 安裝后&#xff0c;把其他相關的Verilog插件禁用&#xff0c;避免可能的沖突。重啟后&#xff0c;可能會彈出下面提示 這是插件默認要求的工具鏈&a…

使用Python從零開始構建端到端文本到圖像 Transformer大模型

簡介&#xff1a;通過特征向量從文本生成圖像 回顧&#xff1a;多模態 Transformer 在使用Python從零實現一個端到端多模態 Transformer大模型中&#xff0c;我們調整了字符級 Transformer 以處理圖像&#xff08;通過 ResNet 特征&#xff09;和文本提示&#xff0c;用于視覺…

Webpack中的文件指紋:給資源戴上個“名牌”

你是否想過&#xff0c;當你修改代碼后&#xff0c;瀏覽器為什么仍然拿著舊版資源不放&#xff1f;秘密就在于——文件指紋&#xff01;簡單來說&#xff0c;文件指紋就像給每個構建出來的文件貼上獨一無二的“姓名牌”&#xff0c;告訴瀏覽器&#xff1a;“嘿&#xff0c;我更…

python可變對象與不可變對象

文章目錄 Python 中的可變對象與不可變對象不可變對象(Immutable Objects)可變對象(Mutable Objects)重要區別 Python 中的可變對象與不可變對象 在 Python 中&#xff0c;對象可以分為可變對象(mutable)和不可變對象(immutable)&#xff0c;這是 Python 中非常重要的概念&…

DeepSeek私有化部署性能怎么樣?企業級AI落地實戰解析!

1. 私有化部署是什么&#xff1f;為什么企業需要它&#xff1f; 很多公司在考慮用AI時都會問&#xff1a;“DeepSeek私有化部署性能怎么樣&#xff1f;能不能在我們自己的服務器上跑&#xff1f;” 私有化部署的意思就是把AI模型裝在你自己的機房或者云服務器上&#xff0c;而…

SQL學習--基礎語法學習

SQL和excle對比 學習目標 單表查詢 項目背景 SQL 練習環境 SQL Online Compiler - Next gen SQL Editor 商品信息表&#xff1a;https://study-zhibo.oss-cn-shanghai.aliyuncs.com/test/%E5%95%86%E5%93%81%E4%BF%A1%E6%81%AF%E8%A1%A8.csv 訂單明細表&#xff1a;https://…

【Docker基礎-網絡】--查閱筆記4

目錄 Docker 網絡網絡類型none 網絡host 網絡bridge 網絡自定義網絡 容器間通信IP 通信Docker DNS Serverjoined 容器 容器與外部通信容器訪問外部外部訪問容器 Docker 網絡 學習Docker提供的幾種原生網絡如何創建自定義網絡容器間通信&#xff0c;容器于外界交互 Docker 安裝…