三次給你講清楚Redis之Redis是個啥

華為雲開發者社群發表於2021-04-08
摘要: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)一個機器的執行記憶體放不下;
  • 訪問量(讀寫混合)一個例項不能承受;

三次給你講清楚Redis之Redis是個啥

2)Memcached(快取)+ MySql + 垂直拆分

通過快取來緩解資料庫的壓力,優化資料庫的結構和索引。

垂直拆分指的是:分成多個資料庫儲存資料(如:賣家庫與買家庫)。

三次給你講清楚Redis之Redis是個啥

3)MySql主從複製讀寫分離

  1. 主從複製:主庫來一條資料,從庫立刻插入一條;
  2. 讀寫分離:讀取(從庫Master),寫(主庫Slave);

三次給你講清楚Redis之Redis是個啥

​4)分表分庫+水平拆分+MySql叢集

  1. 主庫的寫壓力出現瓶頸(行鎖InnoDB取代表鎖MyISAM);
  2. 分庫:根據業務相關緊耦合在同一個庫,對不同的資料讀寫進行分庫(如註冊資訊等不常改動的冷庫與購物資訊等熱門庫分開);
  3. 分表:切割表資料(例如90W條資料,id 1-30W的放在A庫,30W-60W的放在B庫,60W-90W的放在C庫);

三次給你講清楚Redis之Redis是個啥

MySql擴充套件的瓶頸

  1. 大資料下IO壓力大
  2. 表結構更改困難

常用的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——資料結構和物件的使用介紹

redis官網

微軟寫的windows下的redis

三次給你講清楚Redis之Redis是個啥

我們下載第一個,然後基本一路預設就行了。

安裝後,服務自動啟動,以後也不用自動啟動。

三次給你講清楚Redis之Redis是個啥

​出現這個表示我們連線上了。

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()

三次給你講清楚Redis之Redis是個啥

​@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 結構儲存著一個鍵值對。

三次給你講清楚Redis之Redis是個啥

圖為一個大小為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

為了提高整數集合的靈活性和節約記憶體,我們引入升級策略。

當我們要將一個新元素新增到集合裡, 並且新元素型別比集合現有元素的型別都要長時, 集合需要先進行升級。

分為三步進行:

  1. 根據新元素的型別, 擴充套件整數集合底層陣列的空間大小, 併為新元素分配空間。
  2. 將底層陣列現有的所有元素都轉換成與新元素相同的型別, 並將型別轉換後的元素放置到正確的位上
  3. 將新元素新增到底層陣列裡面。

因為每次新增新元素都可能會引起升級, 每次升級都要對已有元素型別轉換, 所以新增新元素的時間複雜度為 O(N) 。

因為引發升級的新元素比原資料都長,所以要麼他是最大的,要麼他是最小的。我們把它放在開頭或結尾即可。

降級

略略略,不管你們信不信,整數集合不支援降級操作。。我也不知道為啥

  • 5)壓縮列表

壓縮列表是列表鍵和雜湊鍵的底層實現之一。

當一個列表鍵只包含少量列表項,並且列表項都是小整數或者短字串,redis就會用壓縮列表做列表鍵底層實現。

壓縮列表是 Redis 為了節約記憶體而開發的, 由一系列特殊編碼的連續記憶體塊組成的順序型(sequential)資料結構。

一個壓縮列表可以包含任意多個節點(entry), 每個節點可以儲存一個位元組陣列或者一個整數值。

具體實現:

三次給你講清楚Redis之Redis是個啥三次給你講清楚Redis之Redis是個啥

​具體說一下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是個啥》,原文作者:兔老大。

點選關注,第一時間瞭解華為雲新鮮技術~

相關文章