Redis 學習筆記(一)redis 數據類型和對象機制

Redis 簡介

Redis 是(key-value)的 NoSQL 資料庫,所有的 key 都是 String ,它的 value 可以是 String、hash、list、set、zset(有序集合)、Bitmaps(點陣圖)、HyperLogLog、GEO(地理資訊定位)等數據類型,這些類型都支援 push/pop、add/remove 及取交集和差集。而且這些操作都是原子性的。

Redis 的數據是快取在記憶體中,但是 Redis 會周期性的把更新的數據寫入磁碟或者把修改操作寫入追加的記錄文件中。在此基礎上實現了 master-slave (主從)同步

主從複製

Redis 提供複製功能,能實現多個相同數據的 Redis 副本,複製功能是分散式 Redis 的基礎

刪除數據命令

# 刪除指定的 key 數據
del key
# 根據 value 選擇非阻塞刪除,也就是現在是將 keys 從 keyspace 元數據中刪除,真正的刪除會在後續非同步操作
unlink key 

Redis 的數據類型

String

redis 中最基本的數據結構,所有的 key 都是 String 。String 類型的 Value 可以是 String、數字、jpg圖片或者序列化的對象(值不能超過 512MB)

常見命令

  1. set key value [ex seconds][px milliseconds][nx|xx]: 設置給定鍵和值

  2. get key : 獲取值

  3. del key :刪除存儲在給定鍵中的值

  4. incr key : 將 key 對應的值加1

  5. decr key : 將 key 對應的值減1

  6. incrby key amount: 將key 對應的值加上整數

  7. decrby key amount:將key 對應的值減去整數

應用場景

  • 快取:可以將常見的字元串、圖片等資訊快取在 redis,mysql 作為持久化層。降低 mysql 的讀寫壓力

  • 計數器: 實現快速計數、查詢快取,同時數據可以非同步落地到其他數據源。

  • 共享Session:分散式伺服器將用戶的 Session 進行集中的管理,每次用戶更新或者查詢登錄資訊都直接從 Redis 中集中獲取。

Hash

哈希類型指的是 value 本身又是一個鍵值對結構,比如 value = {{field1, value1}, … {fieldN, valueN}}。

常見命令

  1. hset hash-key sub-key1 value1 :添加鍵值對
  2. hget hash-key key1 : 獲取制定散列鍵的值
  3. hgetall hash-key :獲取哈希中包含的所有鍵值對
  4. hdel hash-key sub-key1: 在哈希中移除這個鍵

應用場景

  • 快取:能夠更加直觀,相比 String 來說更加節省空間。比如快取用戶資訊

List

Redis 中的 List 採用雙端鏈表來實現,可以用來存儲多個有序的字元創,列表最多可以存儲 2^32 – 1 個元素(element)。可以對列表兩端插入(push)和彈出(pop),還可以獲取制定範圍的元素列表,獲取指定索引下標的元素等。列表是一種比較靈活的數據結構,它可以充當棧和隊列的角色。

常見命令

rpush, lpush 分別是右邊和左邊插入,linsert 命令會從列表中找到等於某個值的元素,在其前或者後插入新的元素。可以轉換成其他的數據結構:

  • lpush+lpop=Stack(棧)
  • lpush+rpop=Queue(隊列)
  • lpush+ltrim=Capped Collection(有限集合)
  • lpush+brpop=Message Queue(消息隊列)

應用場景

  • 消息隊列lpush + brpop組合可以實現阻塞隊列,生產者使用 lpush 從左側插入元素,多個消費者使用 brpop 阻塞式搶列表尾部的元素。保證消費的負載均衡和高可用性

set

set 類型是用來保存多個字元串元素,但是 set 中不允許有重複元素,並且集合中的元素是無序的,不能通過索引下標獲取元素。它的底層是通過哈希表來實現的,因此添加、刪除、查找的複雜度都是 O(1)

常見命令

  1. sadd key value : 向集合中添加一個或者多個成員
  2. scard key : 獲取集合中的成員數
  3. smember key member : 返回集合中的所有成員
  4. sismember key member : 判斷 member 元素是否是集合 key 的成員

應用場景

  • 標籤:給用戶添加標籤,所有這樣有同一標籤或者類似的可以推薦關注的事情或者關注的人

zset

有序集合 zset 相對於 set 而言,其內部的元素可以進行排序,它是通過給每個元素設置一個分數來作為排序的依據。

常見命令

  1. zadd zset-key int member1 : 將一個帶有給定分值的成員添加到有序的集合中
  2. zrange zset-key 0-1 : 根據元素在有序集合中所處的位置,從有序集合中獲取對應的元素
  3. zrem zset-key member1 : 如果給定元素存在於有序集合中,就移除該元素

應用場景

  • 排行榜:榜單可以按照用戶關注數,更新時間等打分,並做排行

HyperLogLogs

HyperLogLog並不是一種新的數據結構(實際類型為字元串類 型),而是一種基數演算法,通過HyperLogLog可以利用極小的記憶體空間 完成獨立總數的統計,比如註冊IP u數,每日訪問IP 數等等。它是一個基於基數估算的演算法,只能比較準確的估算出基數,可以使用少量固定的記憶體去存儲並識別集合中的唯一元素。而且這個估算的技術並不一定準確,它是一個帶有0.81%標準錯誤的近似值(對於一些可以接受容錯的業務場景可以忽略不計)

例如2016-03-06的訪問用戶是 uuid-1、uuid-2、uuid-3、uuid-4,2016-03-05的訪問用戶是uuid-4、uuid-5、uuid-6、uuid-7,如圖所示。

常用命令

  1. pfadd : 用於在基數統計中添加元素,添加成功會返回1

  2. pfcount:用於計算一個或者多個 HyperLogLogs 的獨立總數

  3. pfmerge:求出多個HyperLogLogs 的並集並賦值給 destkey

應用場景

  • IP 數 : 用於統計某個時段的 IP 或者用戶數

Bitmaps

它本身不是一種數據結構,實際上就是字元串,但是它可以對字元串的位進行操作

Bitmaps 相當於一個以位為單位的數組,數組的每個單元只能存儲0 和 1 , 數組的下標在 Bitmaps 中叫做偏移量。

常用命令

  1. setbit key offset value : 設置鍵和偏移量的值
  2. getbit key offset: 獲取鍵的第 offset 位的值
  3. bitcount key : 統計該鍵的次數值

應用場景

  • 活躍用戶分析: 存儲和統計一天中活躍的用戶

Geo

Redis3.2版本提供了GEO(地理資訊定位)功能,支援存儲地理位 置資訊用來實現諸如附近位置、搖一搖這類依賴於地理位置資訊的功能

常用命令

  1. geoadd: 添加地理位置資訊

  1. geopos: 獲取地理位置資訊

  2. geodist: 獲取兩個地理位置的距離

  3. georadius: 獲取範圍內的資訊位置集合–>附近的人

  4. geohash: 將二維經緯度轉換為一維的字元串

  5. zrem: 刪除地理位置資訊(實際上是利用 zset 中的命令實現對位置資訊的刪除)

應用場景

  • 附近的人
  • 推算兩地之間的距離

Redis 的數據結構

為什麼 Redis 會設計 RedisObject 對象,因為操作數據類型的命令除了要對鍵的類型進行檢查以外,還需要根據數據類型的不同編碼進行多態處理,所以 Redis 構建了自己的類型系統,主要有:

  • redisObject 的對象機制
  • redisObject 對象的類型檢查和多態
  • 對 redisObject 進行分配、共享和銷毀的機制

redisObject 的對象機制

/*
* Redis對象
*/
typedef struct redisObject {
    //類型
    unsigned type:4;
    
    //編碼方式
    unsigned encoding:4;
    
    //LRU 記錄最後一次訪問時間
    unsigned lru:LRU_BITS;
    
    //引用計數
    int refcount;
    
    //指向底層數據結構實例
    void *ptr
    
} robj;

type屬性

記錄了對象所保存的值類型,也就是常用的五個數據類型

/*
* 對象類型
*/
#define OBJ_STRING 0 // 字元串
#define OBJ_LIST 1 // 列表
#define OBJ_SET 2 // 集合
#define OBJ_ZSET 3 // 有序集
#define OBJ_HASH 4 // 哈希表

encoding 屬性

記錄了對象所保存的值的編碼,表示數據類型對應的編碼類型

/*
* 對象編碼
*/
#define OBJ_ENCODING_RAW 0     /* Raw representation */
#define OBJ_ENCODING_INT 1     /* Encoded as integer */
#define OBJ_ENCODING_HT 2      /* Encoded as hash table */
#define OBJ_ENCODING_ZIPMAP 3  /* 注意:版本2.6後不再使用. */
#define OBJ_ENCODING_LINKEDLIST 4 /* 注意:不再使用了,舊版本2.x中String的底層之一. */
#define OBJ_ENCODING_ZIPLIST 5 /* Encoded as ziplist */
#define OBJ_ENCODING_INTSET 6  /* Encoded as intset */
#define OBJ_ENCODING_SKIPLIST 7  /* Encoded as skiplist */
#define OBJ_ENCODING_EMBSTR 8  /* Embedded sds string encoding */
#define OBJ_ENCODING_QUICKLIST 9 /* Encoded as linked list of ziplists */
#define OBJ_ENCODING_STREAM 10 /* Encoded as a radix tree of listpacks */

* ptr 指針

它指向實際保存值的數據結構,而數據結構類型是由前面的 encoding 和 type 兩個屬性來決定。如下圖,數據類型和編碼類型決定指向實際的數據結構。

lru 屬性

記錄的是對象最後一次被命令程式訪問的時間,那麼如何實現對對象的回收,這裡引入一個概念:空轉時長

空轉時長,也就是當前系統時間減去 鍵的值對象的 LRU 時間。如果伺服器用於回收記憶體的演算法是 Volatile-lru 或者 allkeys-lru。那麼當伺服器佔用的記憶體樹超過了 maxmemory 選項所設置的上限值時,空轉時長較高的那部分鍵會優先被伺服器所釋放。

refcount 屬性

用於計數,對指向這個對象的引用計數。

比如創建了一個值為 100 的 key A ,使用 OBJECT REFCOUNT 命令查看 key A 的值對象的引用計數 refcount ,發現引用計數為 2,說明這個值對象被兩個程式所引用,兩個程式共享了這個值對象的 key

那麼當對象的 refcount 值為 0 時,這個對象將會被記憶體回收釋放,這也是對象的銷毀機制。(對應 JVM 裡面的引用計數法標記)

redis 命令的類型檢查和多態

redis 當執行一個處理數據類型命令時,比如 LPOP key 命令redis 執行的步驟:

  1. 根據給定的 key,在資料庫字典中查找對應的 redisObject 對象,沒找到就返回null
  2. 檢查找到的 redisObject 的 type 屬性和執行命令所需要的類型是否相同,如果不相同就返回類型錯誤
  3. 根據 redisObject 的 encoding 屬性所指定的編碼,選擇合適的操作函數來處理底層的數據結構
  4. 最後返回命令的操作結構

redisObject 對象共享和銷毀

共享對象的出現是為了避免重複分配的麻煩。通過 refcount 來表示對象所引用的次數。比如創鍵一個 值為 100 的 key A,然後再創建一個值為 100 的 key B ,這個時候共享對象的引用計數值變為了 3

redis> SET A 100
OK
redis> SET B 100
OK
redis> OBJECT REFCOUNT A
(integer) 3

此外共享對象不單單只有字元串鍵可以使用, 那些在數據結構中嵌套了字元串對象的對象(linkedlist 編碼的列表對象、 hashtable 編碼的哈希對象、 hashtable 編碼的集合對象、以及 zset 編碼的有序集合對象)都可以使用這些共享對象。

為什麼redis 不共享包含value 為字元串的對象?

當伺服器考慮將一個共享對象設置為鍵的值對象時, 程式需要先檢查給定的共享對象和鍵想創建的目標對象是否完全相同, 只有在共享對象和目標對象完全相同的情況下, 程式才會將共享對象用作鍵的值對象, 而一個共享對象保存的值越複雜, 驗證共享對象和目標對象是否相同所需的複雜度就會越高, 消耗的 CPU 時間也會越多:

  • 如果共享對象是保存整數值的字元串對象, 那麼驗證操作的複雜度為 O(1) ;
  • 如果共享對象是保存字元串值的字元串對象, 那麼驗證操作的複雜度為 O(N) ;
  • 如果共享對象是包含了多個值(或者對象的)對象, 比如列表對象或者哈希對象, 那麼驗證操作的複雜度將會是 O(N^2) 。

因此, 儘管共享更複雜的對象可以節約更多的記憶體, 但受到 CPU 時間的限制, Redis 只對包含整數值的字元串對象進行共享。

引用計數及對象的銷毀

前面談到過,redisObject 中帶有一個 refcount 屬性,表示這個對象被引用了多少次。

  • 當對象被新程式共享時,其 refcount 值加1;
  • 當使用完一個對象後或者消除一個對象的引用後,程式將對象的 refcount 值減1
  • 當對象的 refcount 降為0 時,這個 redisObject 結構以及它所引用的數據結構的記憶體都會被釋放

參考資料

Tags: