摘要:Redis是一款基於鍵值對的NoSQL數據庫,它的值支持多種數據結構:字符串(strings)、哈希(hashes)、列表(lists)、集合(sets)、有序集合(sorted sets)等。
一、入門
Redis是一款基於鍵值對的NoSQL數據庫,它的值支持多種數據結構:字符串(strings)、哈希(hashes)、列表(lists)、集合(sets)、有序集合(sorted sets)等。
• Redis將所有的數據都存放在內存中,所以它的讀寫性能十分驚人,用作數據庫,緩存和消息代理。
• Redis具有內置的復制,Lua腳本,LRU逐出,事務和不同級別的磁盤持久性,並通過Redis Sentinel和Redis Cluster自動分區提供了高可用性。
• Redis典型的應用場景包括:緩存、排行榜、計數器、社交網絡、消息隊列等
1.1NoSql入門概述
1)單機Mysql的美好時代
瓶頸:
- 數據庫總大小一台機器硬盤內存放不下;
- 數據的索引(B + tree)一個機器的運行內存放不下;
- 訪問量(讀寫混合)一個實例不能承受;
2)Memcached(緩存)+ MySql + 垂直拆分
通過緩存來緩解數據庫的壓力,優化數據庫的結構和索引。
垂直拆分指的是:分成多個數據庫存儲數據(如:賣家庫與買家庫)。
3)MySql主從復制讀寫分離
- 主從復制:主庫來一條數據,從庫立刻插入一條;
- 讀寫分離:讀取(從庫Master),寫(主庫Slave);
4)分表分庫+水平拆分+MySql集群
- 主庫的寫壓力出現瓶頸(行鎖InnoDB取代表鎖MyISAM);
- 分庫:根據業務相關緊耦合在同一個庫,對不同的數據讀寫進行分庫(如注冊信息等不常改動的冷庫與購物信息等熱門庫分開);
- 分表:切割表數據(例如90W條數據,id 1-30W的放在A庫,30W-60W的放在B庫,60W-90W的放在C庫);
MySql擴展的瓶頸
- 大數據下IO壓力大
- 表結構更改困難
常用的Nosql
Redis
memcache
Mongdb
以上幾種Nosql 請到各自的官網上下載並參考使用
Nosql 的核心功能點
KV(存儲)
Cache(緩存)
Persistence(持久化)
……
1.2redis的介紹和特點:
問題:
傳統數據庫:持久化存儲數據。
solr索引庫:大量的數據的檢索。
在實際開發中,高並發環境下,不同的用戶會需要相同的數據。因為每次請求,
在后台我們都會創建一個線程來處理,這樣造成,同樣的數據從數據庫中查詢了N次。
而數據庫的查詢本身是IO操作,效率低,頻率高也不好。
總而言之,一個網站總歸是有大量的數據是用戶共享的,但是如果每個用戶都去數據庫查詢,效率就太低了。
解決:
將用戶共享數據緩存到服務器的內存中。
特點:
1、基於鍵值對
2、非關系型(redis)
關系型數據庫:存儲了數據以及數據之間的關系,oracle,mysql
非關系型數據庫:存儲了數據,redis,mdb.
3、數據存儲在內存中,服務器關閉后,持久化到硬盤中
4、支持主從同步
實現了緩存數據和項目的解耦。
redis存儲的數據特點:
大量數據
用戶共享數據
數據不經常修改。
查詢數據
redis的應用場景:
網站高並發的主頁數據
網站數據的排名
消息訂閱
1.3redis——數據結構和對象的使用介紹
我們下載第一個,然后基本一路默認就行了。
安裝后,服務自動啟動,以后也不用自動啟動。
出現這個表示我們連接上了。
1.3.1 String
數據結構
struct sdshdr{ //記錄buf數組中已使用字節的數量 int len; //記錄buf數組中未使用的數量 int free; //字節數組,用於保存字符串 char buf[]; }
常見操作
127.0.0.1:6379> set hello world OK 127.0.0.1:6379> get hello "world" 127.0.0.1:6379> del hello (integer) 1 127.0.0.1:6379> get hello (nil) 127.0.0.1:6379>
應用場景
String是最常用的一種數據類型,普通的key/value存儲都可以歸為此類,value其實不僅是String,也可以是數字:比如想知道什么時候封鎖一個IP地址(訪問超過幾次)。INCRBY命令讓這些變得很容易,通過原子遞增保持計數。
1.3.2 LIST
數據結構
typedef struct listNode{ //前置節點 struct listNode *prev; //后置節點 struct listNode *next; //節點的值 struct value; }
常見操作
> lpush list-key item (integer) 1 > lpush list-key item2 (integer) 2 > rpush list-key item3 (integer) 3 > rpush list-key item (integer) 4 > lrange list-key 0 -1 1) "item2" 2) "item" 3) "item3" 4) "item" > lindex list-key 2 "item3" > lpop list-key "item2" > lrange list-key 0 -1 1) "item" 2) "item3" 3) "item"
應用場景
Redis list的應用場景非常多,也是Redis最重要的數據結構之一。我們可以輕松地實現最新消息排行等功能。Lists的另一個應用就是消息隊列,可以利用Lists的PUSH操作,將任務存在Lists中,然后工作線程再用POP操作將任務取出進行執行。
1.3.3 HASH
數據結構
dictht是一個散列表結構,使用拉鏈法保存哈希沖突的dictEntry。
typedef struct dictht{ //哈希表數組 dictEntry **table; //哈希表大小 unsigned long size; //哈希表大小掩碼,用於計算索引值 unsigned long sizemask; //該哈希表已有節點的數量 unsigned long used; } typedef struct dictEntry{ //鍵 void *key; //值 union{ void *val; uint64_tu64; int64_ts64; } struct dictEntry *next; }
Redis的字典dict中包含兩個哈希表dictht,這是為了方便進行rehash操作。在擴容時,將其中一個dictht上的鍵值對rehash到另一個dictht上面,完成之后釋放空間並交換兩個dictht的角色。
typedef struct dict { dictType *type; void *privdata; dictht ht[2]; long rehashidx; /* rehashing not in progress if rehashidx == -1 */ unsigned long iterators; /* number of iterators currently running */ } dict;
rehash操作並不是一次性完成、而是采用漸進式方式,目的是為了避免一次性執行過多的rehash操作給服務器帶來負擔。
漸進式rehash通過記錄dict的rehashidx完成,它從0開始,然后沒執行一次rehash例如在一次 rehash 中,要把 dict[0] rehash 到 dict[1],這一次會把 dict[0] 上 table[rehashidx] 的鍵值對 rehash 到 dict[1] 上,dict[0] 的 table[rehashidx] 指向 null,並令 rehashidx++。
在 rehash 期間,每次對字典執行添加、刪除、查找或者更新操作時,都會執行一次漸進式 rehash。
采用漸進式rehash會導致字典中的數據分散在兩個dictht中,因此對字典的操作也會在兩個哈希表上進行。例如查找時,先從ht[0]查找,沒有再查找ht[1],添加時直接添加到ht[1]中。
常見操作
> hset hash-key sub-key1 value1 (integer) 1 > hset hash-key sub-key2 value2 (integer) 1 > hset hash-key sub-key1 value1 (integer) 0 > hgetall hash-key 1) "sub-key1" 2) "value1" 3) "sub-key2" 4) "value2" > hdel hash-key sub-key2 (integer) 1 > hdel hash-key sub-key2 (integer) 0 > hget hash-key sub-key1 "value1" > hgetall hash-key 1) "sub-key1" 2) "value1"
1.3.4 SET
常見操作
> sadd set-key item (integer) 1 > sadd set-key item2 (integer) 1 > sadd set-key item3 (integer) 1 > sadd set-key item (integer) 0 > smembers set-key 1) "item2" 2) "item" 3) "item3" > sismember set-key item4 (integer) 0 > sismember set-key item (integer) 1 > srem set-key item (integer) 1 > srem set-key item (integer) 0 > smembers set-key 1) "item2" 2) "item3"
應用場景
Redis為集合提供了求交集、並集、差集等操作,故可以用來求共同好友等操作。
1.3.5 ZSET
數據結構
typedef struct zskiplistNode{ //后退指針 struct zskiplistNode *backward; //分值 double score; //成員對象 robj *obj; //層 struct zskiplistLever{ //前進指針 struct zskiplistNode *forward; //跨度 unsigned int span; }lever[]; } typedef struct zskiplist{ //表頭節點跟表尾結點 struct zskiplistNode *header, *tail; //表中節點的數量 unsigned long length; //表中層數最大的節點的層數 int lever; }
跳躍表,基於多指針有序鏈實現,可以看作多個有序鏈表。
與紅黑樹等平衡樹相比,跳躍表具有以下優點:
- 插入速度非常快速,因為不需要進行旋轉等操作來維持平衡性。
- 更容易實現。
- 支持無鎖操作。
常見操作
> zadd zset-key 728 member1 (integer) 1 > zadd zset-key 982 member0 (integer) 1 > zadd zset-key 982 member0 (integer) 0 > zrange zset-key 0 -1 1) "member1" 2) "member0" > zrange zset-key 0 -1 withscores 1) "member1" 2) "728" 3) "member0" 4) "982" > zrangebyscore zset-key 0 800 withscores 1) "member1" 2) "728" > zrem zset-key member1 (integer) 1 > zrem zset-key member1 (integer) 0 > zrange zset-key 0 -1 withscores 1) "member0" 2) "982"
應用場景
以某個條件為權重,比如按頂的次數排序。ZREVRANGE命令可以用來按照得分來獲取前100名的用戶,ZRANK可以用來獲取用戶排名,非常直接而且操作容易。
Redis sorted set的使用場景與set類似,區別是set不是自動有序的,而sorted set可以通過用戶額外提供一個優先級(score)的參數來為成員排序,並且是插入有序的,即自動排序。
1.4 Spring整合Redis
引入依賴
- spring-boot-starter-data-redis
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
配置Redis
- 配置數據庫參數
# RedisProperties spring.redis.database=11#第11個庫,這個隨便 spring.redis.host=localhost spring.redis.port=6379#端口
- 編寫配置類,構造RedisTemplate
這個springboot已經幫我們配了,但是默認object,我想改成string
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.serializer.RedisSerializer; @Configuration public class RedisConfig { @Bean public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) { RedisTemplate<String, Object> template = new RedisTemplate<>(); template.setConnectionFactory(factory); // 設置key的序列化方式 template.setKeySerializer(RedisSerializer.string()); // 設置value的序列化方式 template.setValueSerializer(RedisSerializer.json()); // 設置hash的key的序列化方式 template.setHashKeySerializer(RedisSerializer.string()); // 設置hash的value的序列化方式 template.setHashValueSerializer(RedisSerializer.json()); template.afterPropertiesSet(); return template; } }
訪問Redis
- redisTemplate.opsForValue()
- redisTemplate.opsForHash()
- redisTemplate.opsForList()
- redisTemplate.opsForSet()
- redisTemplate.opsForZSet()
@RunWith(SpringRunner.class) @SpringBootTest @ContextConfiguration(classes = CommunityApplication.class) public class RedisTests { @Autowired private RedisTemplate redisTemplate; @Test public void testStrings() { String redisKey = "test:count"; redisTemplate.opsForValue().set(redisKey, 1); System.out.println(redisTemplate.opsForValue().get(redisKey)); System.out.println(redisTemplate.opsForValue().increment(redisKey)); System.out.println(redisTemplate.opsForValue().decrement(redisKey)); } @Test public void testHashes() { String redisKey = "test:user"; redisTemplate.opsForHash().put(redisKey, "id", 1); redisTemplate.opsForHash().put(redisKey, "username", "zhangsan"); System.out.println(redisTemplate.opsForHash().get(redisKey, "id")); System.out.println(redisTemplate.opsForHash().get(redisKey, "username")); } @Test public void testLists() { String redisKey = "test:ids"; redisTemplate.opsForList().leftPush(redisKey, 101); redisTemplate.opsForList().leftPush(redisKey, 102); redisTemplate.opsForList().leftPush(redisKey, 103); System.out.println(redisTemplate.opsForList().size(redisKey)); System.out.println(redisTemplate.opsForList().index(redisKey, 0)); System.out.println(redisTemplate.opsForList().range(redisKey, 0, 2)); System.out.println(redisTemplate.opsForList().leftPop(redisKey)); System.out.println(redisTemplate.opsForList().leftPop(redisKey)); System.out.println(redisTemplate.opsForList().leftPop(redisKey)); } @Test public void testSets() { String redisKey = "test:teachers"; redisTemplate.opsForSet().add(redisKey, "劉備", "關羽", "張飛", "趙雲", "諸葛亮"); System.out.println(redisTemplate.opsForSet().size(redisKey)); System.out.println(redisTemplate.opsForSet().pop(redisKey)); System.out.println(redisTemplate.opsForSet().members(redisKey)); } @Test public void testSortedSets() { String redisKey = "test:students"; redisTemplate.opsForZSet().add(redisKey, "唐僧", 80); redisTemplate.opsForZSet().add(redisKey, "悟空", 90); redisTemplate.opsForZSet().add(redisKey, "八戒", 50); redisTemplate.opsForZSet().add(redisKey, "沙僧", 70); redisTemplate.opsForZSet().add(redisKey, "白龍馬", 60); System.out.println(redisTemplate.opsForZSet().zCard(redisKey)); System.out.println(redisTemplate.opsForZSet().score(redisKey, "八戒")); System.out.println(redisTemplate.opsForZSet().reverseRank(redisKey, "八戒")); System.out.println(redisTemplate.opsForZSet().reverseRange(redisKey, 0, 2)); } @Test public void testKeys() { redisTemplate.delete("test:user"); System.out.println(redisTemplate.hasKey("test:user")); redisTemplate.expire("test:students", 10, TimeUnit.SECONDS); } }
這樣還是稍微有點麻煩,我們其實可以綁定key
// 多次訪問同一個key @Test public void testBoundOperations() { String redisKey = "test:count"; BoundValueOperations operations = redisTemplate.boundValueOps(redisKey); operations.increment(); operations.increment(); operations.increment(); operations.increment(); operations.increment(); System.out.println(operations.get()); }
二、數據結構原理總結
這部分在我看來是最有意思的,我們有必要了解底層數據結構的實現,這也是我最感興趣的。比如,
- 你知道redis中的字符串怎么實現的嗎?為什么這么實現?
- 你知道redis壓縮列表是什么算法嗎?
- 你知道redis為什么拋棄了紅黑樹反而采用了跳表這種新的數據結構嗎?
- 你知道hyperloglog為什么用如此小的空間就可以有這么好的統計性能和准確性嗎?
- 你知道布隆過濾器為什么這么有效嗎?有沒有數學證明過?
- 你是否還能很快寫出來快排?或者不斷優化性能的排序?是不是只會調庫了甚至庫函數怎么實現的都不知道?真的就是快排?
包括數據庫,持久化,處理事件、客戶端服務端、事務的實現、發布和訂閱等功能的實現,也需要了解。
2.1數據結構和對象的實現
-
1) 字符串
redis並未使用傳統的c語言字符串表示,它自己構建了一種簡單的動態字符串抽象類型。
在redis里,c語言字符串只會作為字符串字面量出現,用在無需修改的地方。
當需要一個可以被修改的字符串時,redis就會使用自己實現的SDS(simple dynamic string)。比如在redis數據庫里,包含字符串的鍵值對底層都是SDS實現的,不止如此,SDS還被用作緩沖區(buffer):比如AOF模塊中的AOF緩沖區以及客戶端狀態中的輸入緩沖區。
下面來具體看一下sds的實現:
struct sdshdr { int len;//buf已使用字節數量(保存的字符串長度) int free;//未使用的字節數量 char buf[];//用來保存字符串的字節數組 };
sds遵循c中字符串以'\0'結尾的慣例,這一字節的空間不算在len之內。這樣的好處是,我們可以直接重用c中的一部分函數。比如printf;
sds相對c的改進
獲取長度:c字符串並不記錄自身長度,所以獲取長度只能遍歷一遍字符串,redis直接讀取len即可。
緩沖區安全:c字符串容易造成緩沖區溢出,比如:程序員沒有分配足夠的空間就執行拼接操作。而redis會先檢查sds的空間是否滿足所需要求,如果不滿足會自動擴充。
內存分配:由於c不記錄字符串長度,對於包含了n個字符的字符串,底層總是一個長度n+1的數組,每一次長度變化,總是要對這個數組進行一次內存重新分配的操作。因為內存分配涉及復雜算法並且可能需要執行系統調用,所以它通常是比較耗時的操作。
redis內存分配:
1、空間預分配:如果修改后大小小於1MB,程序分配和len大小一樣的未使用空間,如果修改后大於1MB,程序分配 1MB的未使用空間。修改長度時檢查,夠的話就直接使用未使用空間,不用再分配。
2、惰性空間釋放:字符串縮短時不需要釋放空間,用free記錄即可,留作以后使用。
二進制安全
c字符串除了末尾外,不能包含空字符,否則程序讀到空字符會誤以為是結尾,這就限制了c字符串只能保存文本,二進制文件就不能保存了。
而redis字符串都是二進制安全的,因為有len來記錄長度。
-
2) 鏈表
作為一種常用數據結構,鏈表內置在很多高級語言中,因為c並沒有,所以redis實現了自己的鏈表。
鏈表在redis也有一定的應用,比如列表鍵的底層實現之一就是鏈表。(當列表鍵包含大量元素或者元素都是很長的字符串時)發布與訂閱、慢查詢、監視器等功能也用到了鏈表。
具體實現:
//redis的節點使用了雙向鏈表結構 typedef struct listNode { // 前置節點 struct listNode *prev; // 后置節點 struct listNode *next; // 節點的值 void *value; } listNode;
//其實學過數據結構的應該都實現過 typedef struct list { // 表頭節點 listNode *head; // 表尾節點 listNode *tail; // 鏈表所包含的節點數量 unsigned long len; // 節點值復制函數 void *(*dup)(void *ptr); // 節點值釋放函數 void (*free)(void *ptr); // 節點值對比函數 int (*match)(void *ptr, void *key); } list;
總結一下redis鏈表特性:
雙端、無環、帶長度記錄
多態:使用 void* 指針來保存節點值, 可以通過 dup 、 free 、 match 為節點值設置類型特定函數, 可以保存不同類型的值。
-
3)字典
其實字典這種數據結構也內置在很多高級語言中,但是c語言沒有,所以redis自己實現了。應用也比較廣泛,比如redis的數據庫就是字典實現的。不僅如此,當一個哈希鍵包含的鍵值對比較多,或者都是很長的字符串,redis就會用字典作為哈希鍵的底層實現。
來看看具體是實現:
//redis的字典使用哈希表作為底層實現 typedef struct dictht { // 哈希表數組 dictEntry **table; // 哈希表大小 unsigned long size; // 哈希表大小掩碼,用於計算索引值 // 總是等於 size - 1 unsigned long sizemask; // 該哈希表已有節點的數量 unsigned long used; } dictht;
table 是一個數組, 數組中的每個元素都是一個指向dictEntry 結構的指針, 每個 dictEntry 結構保存着一個鍵值對。
圖為一個大小為4的空哈希表。我們接着就來看dictEntry的實現:
typedef struct dictEntry { // 鍵 void *key; // 值 union { void *val; uint64_t u64; int64_t s64; } v; // 指向下個哈希表節點,形成鏈表 struct dictEntry *next; } dictEntry;
(v可以是一個指針, 或者是一個 uint64_t 整數, 又或者是一個 int64_t 整數。)
next就是解決鍵沖突問題的,沖突了就掛后面,這個學過數據結構的應該都知道吧,不說了。
下面我們來說字典是怎么實現的了。
typedef struct dict { // 類型特定函數 dictType *type; // 私有數據 void *privdata; // 哈希表 dictht ht[2]; // rehash 索引 int rehashidx; //* rehashing not in progress if rehashidx == -1 } dict;
type 和 privdata 是對不同類型的鍵值對, 為創建多態字典而設置的:
type 指向 dictType , 每個 dictType 保存了用於操作特定類型鍵值對的函數, 可以為用途不同的字典設置不同的類型特定函數。
而 privdata 屬性則保存了需要傳給那些類型特定函數的可選參數。
dictType就暫時不展示了,不重要而且字有點多。。。還是講有意思的東西吧
rehash(重新散列)
隨着我們不斷的操作,哈希表保存的鍵值可能會增多或者減少,為了讓哈希表的負載因子維持在合理的范圍內,有時需要對哈希表進行合理的擴展或者收縮。 一般情況下, 字典只使用 ht[0] 哈希表, ht[1] 哈希表只會在對 ht[0] 哈希表進行 rehash 時使用。
redis字典哈希rehash的步驟如下:
1)為ht[1]分配合理空間:如果是擴展操作,大小為第一個大於等於ht[0]*used*2的,2的n次冪。
如果是收縮操作,大小為第一個大於等於ht[0]*used的,2的n次冪。
2)將ht[0]中的數據rehash到ht[1]上。
3)釋放ht[0],將ht[1]設置為ht[0],ht[1]創建空表,為下次做准備。
漸進rehash
數據量特別大時,rehash可能對服務器造成影響。為了避免,服務器不是一次性rehash的,而是分多次。
我們維持一個變量rehashidx,設置為0,代表rehash開始,然后開始rehash,在這期間,每個對字典的操作,程序都會把索引rehashidx上的數據移動到ht[1]。
隨着操作不斷執行,最終我們會完成rehash,設置rehashidx為-1.
需要注意:rehash過程中,每一次增刪改查也是在兩個表進行的。
-
4)整數集合
整數集合(intset)是 Redis 用於保存整數值的集合抽象數據結構, 可以保存 int16_t 、 int32_t 、 int64_t 的整數值, 並且保證集合中不會出現重復元素。
實現較為簡單:
typedef struct intset { // 編碼方式 uint32_t encoding; // 集合包含的元素數量 uint32_t length; // 保存元素的數組 int8_t contents[]; } intset;
各個項在數組中從小到大有序地排列, 並且數組中不包含任何重復項。
雖然 intset 結構將 contents 屬性聲明為 int8_t 類型的數組, 但實際上 contents 數組並不保存任何 int8_t 類型的值 —— contents 數組的真正類型取決於 encoding 屬性的值:
- 如果 encoding 屬性的值為 INTSET_ENC_INT16 , 那么 contents 就是一個 int16_t 類型的數組, 數組里的每個項都是一個 int16_t 類型的整數值 (最小值為 -32,768 ,最大值為 32,767 )。
- 如果 encoding 屬性的值為 INTSET_ENC_INT32 , 那么 contents 就是一個 int32_t 類型的數組, 數組里的每個項都是一個 int32_t 類型的整數值 (最小值為 -2,147,483,648 ,最大值為 2,147,483,647 )。
- 如果 encoding 屬性的值為 INTSET_ENC_INT64 , 那么 contents 就是一個 int64_t 類型的數組, 數組里的每個項都是一個 int64_t 類型的整數值 (最小值為 -9,223,372,036,854,775,808 ,最大值為 9,223,372,036,854,775,807 )。
升級
c語言是靜態類型語言,不允許不同類型保存在一個數組。這樣第一,靈活性較差,第二,有時會用掉不必要的內存。
比如用long long儲存1
為了提高整數集合的靈活性和節約內存,我們引入升級策略。
當我們要將一個新元素添加到集合里, 並且新元素類型比集合現有元素的類型都要長時, 集合需要先進行升級。
分為三步進行:
- 根據新元素的類型, 擴展整數集合底層數組的空間大小, 並為新元素分配空間。
- 將底層數組現有的所有元素都轉換成與新元素相同的類型, 並將類型轉換后的元素放置到正確的位上
- 將新元素添加到底層數組里面。
因為每次添加新元素都可能會引起升級, 每次升級都要對已有元素類型轉換, 所以添加新元素的時間復雜度為 O(N) 。
因為引發升級的新元素比原數據都長,所以要么他是最大的,要么他是最小的。我們把它放在開頭或結尾即可。
降級
略略略,不管你們信不信,整數集合不支持降級操作。。我也不知道為啥
-
5)壓縮列表
壓縮列表是列表鍵和哈希鍵的底層實現之一。
當一個列表鍵只包含少量列表項,並且列表項都是小整數或者短字符串,redis就會用壓縮列表做列表鍵底層實現。
壓縮列表是 Redis 為了節約內存而開發的, 由一系列特殊編碼的連續內存塊組成的順序型(sequential)數據結構。
一個壓縮列表可以包含任意多個節點(entry), 每個節點可以保存一個字節數組或者一個整數值。
具體實現:
具體說一下entry:
由三個部分組成:
1、previous_entry_length:記錄上一個節點的長度,這樣我們就可以從最后一路遍歷到開頭。
2、encoding:記錄了content所保存的數據類型和長度。(具體編碼不寫了,不重要)
3、content:保存節點值,可以是字節數組或整數。(具體怎么壓縮的等我搞明白再補)
連鎖更新
前面說過, 每個節點的 previous_entry_length 屬性都記錄了前一個節點的長度:
- 如果前一節點的長度< 254 KB, 那么 previous_entry_length 需要用 1 字節長的空間
- 如果前一節點的長度>=254 KB, 那么 previous_entry_length 需要用 5 字節長的空間
現在, 考慮這樣一種情況: 在一個壓縮列表中, 有多個連續的、長度介於 250 字節到 253 字節之間的節點 ,這時, 如果我們將一個長度大於等於 254 字節的新節點 new 設置為壓縮列表的表頭節點。。。。
然后腦補一下,就會導致連鎖擴大每個節點的空間對吧?e(i)因為e(i-1)的擴大而擴大,i+1也是如此,以此類推... ...
刪除節點同樣會導致連鎖更新。
這個事情只是想說明一個問題:插入刪除操作的最壞時間復雜度其實是o(n*n),因為每更新一個節點都要o(n)。
但是,也不用太過擔心,因為這種特殊情況並不多見,這些命令的平均復雜度依舊是o(n)。
本文分享自華為雲社區《三次給你聊清楚Redis》之Redis是個啥》,原文作者:兔老大。