【redis】redis基礎命令,分散式鎖,快取問題學習大集合

  • 2019 年 10 月 3 日
  • 筆記

寫在前面

Redis是一個高速的記憶體資料庫,它的應用十分廣泛,可以說是服務端必學必精的東西。然而,學以致用,無用則無為。學了的東西必須反覆的去用,去實踐,方能有真知。這篇文章記錄了我在redis學習過程中的筆記、理解和實踐,僅供參考。

本章介紹redis基礎中的基礎,常用命令的使用和效果。

如果你已經很厲害了,不需要看基礎命令,你可以跳轉:

【redis】redis應用場景,快取的各種問題解析

【redis】分散式鎖實現,與分散式定時任務

string

string類型是redis中最常見的類型了,通過簡單的set、get命令就可以對這個數據結構做增刪操作,應該也是redis最大眾的類型之一,存json、存自增數值、甚至快取圖片。 string的底層是redis作者自定義的一個叫SDS的struct。長下面這樣:

redis是使用c語言實現的

typedef char *sds;  // 省略  struct __attribute__ ((__packed__)) sdshdr64 {      uint64_t len; /* used */      uint64_t alloc; /* excluding the header and null terminator */      unsigned char flags; /* 3 lsb of type, 5 unused bits */      char buf[];  };

 

  • len 記錄了字元串的長度
  • alloc 表示字元串的最大容量(不包含最後多餘的那個位元組)。
  • flags 總是佔用一個位元組。其中的最低3個bit用來表示header的類型。源碼中的多個header是用來節省記憶體空間的。

這裡有一個疑問,為什麼作者要自定義一個sds而不是直接用c語言的字元串呢?

  1. 時間複雜度要求 redis的數據結構設計總是基於最優複雜度方案的,對每一個點的時間、空間複雜度要求非常高,這一點c語言的string就已經不滿足需求了,因為c自帶的字元串並不會記錄自身長度資訊,所以每次獲取字元串長度的時間複雜度都是o(n),所以redis作者設計SDS時,有一個len欄位,記錄了字元串的長度,這樣每次獲取長度時的時間複雜度就是O(1)了。

  2. 緩衝區溢出問題 其實也是c語言不記錄本身長度帶來的問題,當拼接字元串的時候,例如 hello + world 因為c不記錄長度,所以在拼接字元的時候需要手動為hello分配五個記憶體空間,然後才能+world,如果忘記分配記憶體,那麼就會產生緩衝區溢出,而redis的解決方案是在SDS中分別記錄len和alloc,表示當前字元串長度和最大容量,這樣當進行字元串拼接的時候api直接去判斷最大容量是否滿足,滿足就直接插入,不滿足則對 char * 做一次擴容,然後插入,減少了人為出錯的概率,並且可以對alloc適當的進行空間預先分配,減少擴容次數,例如在創建字元串hello時,完全可以將alloc長度設置10,這樣在加入world時直接放進去就ok了。

  3. 實現了c語言字元串的識別特性,復用了c語言自帶的字元串函數 傳統的c語言使用的是n+1的char數組來表示長度n的字元串的,然後在n長度最後加上一個 , 所以redis的sds在設計的時候也加上了這個,這樣可以復用部分c語言字元串的函數。

  4. 二進位安全 c字元串中的字元必須符合某種編碼,比如 ASCII 並且除了字元串的末尾之外,字元串裡面不能包含空字元(這裡空字元指的是空()不是空格、換行之類的字元),主要是不能存儲二進位的圖片、影片、壓縮文件等內容,而我們知道redis是可以用來快取圖片二進位數據的。因為redis記錄了字元長度。c沒有記錄長度的時候遇到就認為讀到字元結尾了。

可以看出,c語言中字元串沒有記錄長度是一個比較麻煩的事兒,如果沒有記錄長度就必須用佔位符確定字元末尾,導致二進位不安全。如果沒有記錄長度就必須每次統計長度,導致時間複雜度陡增。如果沒有記錄長度在分割字元串、拼接字元串時麻煩也不少。所以—總的來說,在設計字元串的時候,不要忘了記錄長度。

set命令

  • set [key] [value]

set一個key的value值,這個值可以是任意字元串。例如:

set redis:demo helloRedis  > OK  get redis:demo  > "helloRedis"

 

  • set [key] [value] [NX] [EX|PX]

set還可以指定另外兩個參數 [NX] 表示 SET if Not eXists , 指定這個參數就是告訴redis,如果key不存在才set。 [EX|PX] 這個參數表示超時時間,ex表示秒數,px表示毫秒數,一般redis通用的表示時間單位是 秒

set redis:demo:nxex helloRedis NX EX 20  > OK  set redis:demo:nxex hellostring NX EX 20  > (nil) // 設置失敗

 

這裡有一個值得注意的點是,set nx是跟普通的set互通的 ,什麼意思呢? 就是:

set redis:demo:nxex a  > OK  set redis:demo:nxex b NX EX 20  > (nil) // 普通的set在第二次設置nx的時候依然會設置失敗  del redis:demo:nxex  > OK  set redis:demo:nxex a NX EX 20  > OK  set redis:demo:nxex b  > OK // 就算是nx設置的值,在普通set下依然會成功覆蓋,並且丟失nx和ex的作用

 

  • mset [key] [value] [key] [value] …

批量設置key value,可以批量設置一堆key,並且它是原子的,也就是這些key要麼全部成功,要麼全部失敗.

請注意,mset是不可以指定過期時間和nx的,如果你希望批量設置key並且有過期時間,那麼你最好自己寫lua腳本來解決

mset a 1 b 2 c 3 NX EX 20  > (error) ERR wrong number of arguments for MSET

 

  • getset [key] [value]

set之前先get,返回set之前的值

set redis:getset:demo hello  > ok  getset redis:getset:demo world  > "hello"  get redis:getset:demo  > "world"

 

ps這個命令一般用來檢查set之前的值是否正常 注意這個也不能加nx和ex等屬性

get 命令

  • get [key]

獲取一個字元串類型的key的值,如果鍵 key 不存在, 那麼返回特殊值 nil ; 否則, 返回鍵 key 的值。

set redis:get:demo hello  > ok  get redis:get:demo  > "hello"  del redis:get:demo  > (integer) 1  get redis:get:demo  > (nil)

 

  • strlen [key]

獲取key字元串的長度

set redis:get:demo hello  > ok  strlen redis:get:demo  >  (integer) 5

 

  • mget [key] [key] …

批量獲取key的值,返回一個list結構

mset a 1 b 2  > ok  mget a b  >  (1) "1" (2) "2"

 

操作命令

  • append [key] [value]

這個命令就是用來拼接字元串的

set redis:append:demo hello  > ok  append redis:append:demo world  >  (integer) 10 // 返回了append之後的字元串的總長度,也就是上面說的sds中的len欄位,這時候這個key的free也已經被擴容  get redis:append:demo  > hello world

 

注意,當key不存在,append命令依然會成功,並且會當作key是一個字元串來拼接

integer

在redis中的integer類型是存儲為字元串對象,通過編碼的不同來表示不同的類型

set redis:int:demo 1  > OK  type redis:int:demo  > string // type依然是string  object encoding redis:int:demo  > "int" // 但是編碼現在是int

 

這裡也有一個注意的點,就是redis是不支援任意小數點的,例如你set a 0.5會被存儲為embstr編碼,這時候對它使用incr和decr會報錯

  • incr [key]

將key自增1

set redis:int:demo 1  > OK  incr redis:int:demo  > (integer) 2  set redis:int:demo 0.5  > OK  incr redis:int:demo  > (error) ERR value is not an integer or out of range

 

  • decr [key]

將key自減1 是可以減到負數的

set redis:int:demo 1  > OK  decr redis:int:demo  > (integer) 0  decr redis:int:demo  > (integer) -1

 

  • incrby [key] [integer]

將key自增指定的數字

set redis:int:demo 1  > OK  incrby redis:int:demo 2  > (integer) 3

 

  • decrby [key] [integer]

將key自減指定的數字

set redis:int:demo 1  > OK  decrby redis:int:demo 2  > (integer) -1

 

有趣的實驗

用decrby減去-1會是加法的效果嗎?

set redis:int:demo 1  > OK  decrby redis:int:demo -2  > (integer) 3

 

答案是會增加。

 

hash

hash從源碼上看,底層在redis中其實叫dict(字典)

看一個插入函數

dictEntry *dictAddRaw(dict *d, void *key, dictEntry **existing) // addraw  {      long index;      dictEntry *entry;      dictht *ht;        if (dictIsRehashing(d)) _dictRehashStep(d); // 判斷是否正在rehash        /* Get the index of the new element, or -1 if       * the element already exists. */      if ((index = _dictKeyIndex(d, key, dictHashKey(d,key), existing)) == -1) // 通過hash演算法,得到key的hash值,如果是-1則返回null          return NULL;        /* Allocate the memory and store the new entry.       * Insert the element in top, with the assumption that in a database       * system it is more likely that recently added entries are accessed       * more frequently. */       // 判斷是否正在rehash 將元素插入到頂部      ht = dictIsRehashing(d) ? &d->ht[1] : &d->ht[0];      entry = zmalloc(sizeof(*entry));      entry->next = ht->table[index];      ht->table[index] = entry;      ht->used++;        /* Set the hash entry fields. */      dictSetKey(d, entry, key);      return entry;  }

 

字典和hash表的實現都大同小異 可以看到基本上原理是使用hash演算法加桶(table),通過拉鏈法解決hash衝突,當每個槽位的平均容積大於1:1觸發rehash等操作。

set命令

  • hset [key] [field] [value]

將哈希表 hash 中一個key的 field 的值設置為 value 。 如果給定的哈希表key並不存在, 那麼一個新的哈希表key將被創建並執行 HSET 操作。 如果域 field 已經存在於哈希表中, 那麼它的舊值將被新值 value 覆蓋。

hset redis:hash:demo com redis  > (integer) 1  hset redis:hash:demo com java // 設置同樣的field將更新field 但返回是0  > (integer) 0  hget redis:hash:demo com  > "java"

 

  • hmset [key] [field] [value] …

批量設置 hash 中一個key的field值為value 如果不存在,則新建再插入。

hmset redis:hash:demo com redis lan java  > OK // 這裡就不再是返回integer了,而是返回了ok  hget redis:hash:demo com  > "redis"

 

  • hsetnx [key] [field] [value]

這個命令與string中的nx參數是一樣的行為,即只有當field不存在key上時,field的設置才生效,否則set失敗 特別注意,這裡第二次nx設置時返回的既不是null也不是報錯,而是返回了0,這裡比較坑一點,所以要在hash中使用hsetnx,你可以嘗試使用lua腳本實現

hsetnx redis:hash:demo com redis  > (integer) 1  hsetnx redis:hash:demo com java  > (integer) 0 // 既不是null也不是報錯  hget redis:hash:demo com  > "redis" // 第二次設置未生效

 

get命令

  • hget [key] [field]

get一個key的field的值,key或field不存在時都返回為nil

hset redis:hash:demo com redis  > (integer) 1  hget redis:hash:demo com  > "redis"  hget redis:hash:demo empty  > (nil)

 

  • hmget [key] [field1] [field2] …

批量獲取field的值,這個值返回的是一個list

hmset redis:hash:demo com redis lan java  > OK  hmget redis:hash:demo com lan  > 1) "redis"  > 2) "java"

 

  • hlen [key]…

獲取key中的field數量

hmset redis:hash:demo com redis lan java  > OK  hlen redis:hash:demo  > (integer) 2

 

  • hkeys [key]

獲取key中的所有field的key,返回的是一個list

hmset redis:hash:demo com redis lan java  > OK  hkeys redis:hash:demo  > 1) "com"  > 2) "lan"

 

  • hvals [key]

獲取key中的所有field的value,返回的是一個list

hmset redis:hash:demo com redis lan java  > OK  hvals redis:hash:demo  > 1) "redis"  > 2) "java"

 

  • hgetall [key]

獲取key中的所有的東西,返回的是一個list,按 field,value,field,value的順序排列

hmset redis:hash:demo com redis lan java  > OK  hgetall redis:hash:demo  > 1) "com"  > 2) "redis"  > 3) "lan"  > 4) "java"

 

  • hexists [key] [field]

判斷key中的field是否存在, 返回integer,1表示存在 ,0 表示不存在

hmset redis:hash:demo com redis lan java  > OK  hexists redis:hash:demo com  > (integer) 1 // 1表示存在

 

操作命令

  • hincrby [key] [field] [integer]

與string的incrby表現一致,將key中的field自增一個integer值, 字元和帶小數點不可用

hset redis:hash:demo inta 1  > (integer) 1  hincrby redis:hash:demo inta 2  > (integer) 3  // 同樣的,可以給定一個負數,這樣就變成自減了  hincrby redis:hash:demo inta -2  > (integer) 1

 

 

list

 

基礎命令

  • lpush [key] [value1] [value2] …

將多個value插入一個key,這裡注意lpush和rpush的區別,lpush是從list的左邊插入數據,rpush則是從右邊。

rpush redis:list:demo 1 2 3  > (integer) 3  // 使用lrange查找  lrange redis:list:demo 0 -1  > 1) "3"  > 2) "2"  > 3) "1"   // 這裡對應的值是從左往右插入的

 

  • rpush [key] [value1] [value2] …

將多個value插入一個key,這裡注意lpush和rpush的區別,lpush是從list的左邊插入數據,rpush則是從右邊。

lpush redis:list:demo 1 2 3  > (integer) 3  // 使用lrange查找  lrange redis:list:demo 0 -1  > 1) "1"  > 2) "2"  > 3) "3"   // 這裡對應的值是從右往左插入的

 

注意lpush和rpush都是在key不存在的時候,自動創建一個類型list的key,而當這個key已存在但類型不是list時,命令報錯

del redis:list:demo // 刪掉確保不存在  > (integer) n  type redis:list:demo  > none  lpush redis:list:demo 1 2 3  > (integer) 3  type redis:list:demo  > list // 自動創建了key並且類型是list  set redis:string:demo hello  > OK  lpush redis:string:demo 1 2 3  > (error) WRONGTYPE Operation against a key holding the wrong kind of value // key已經存在了但不是list類型

 

  • lrange [key] [start] [end]

讀取一個list,從start下標開始end下標結束,end可以設置為負數

lpush redis:list:demo 1 2 3  > (integer) 3  lrange redis:list:demo 0 1  > 1) 3  > 2) 2  lrange redis:list:demo 0 -1  > 1) "3"  > 2) "2"  > 3) "1"  lrange redis:list:demo 0 -2  > 1) "3"  > 2) "2"

 

  • lpushx [key] [value]

將單個value插入一個類型為list且必須存在的key 如果key不存在,返回0,並不會報錯,lpushx是從list的左邊插入數據,rpushx則是從右邊。

lpushx redis:list:demo 4  > (integer) 4  lpushx empty:key 1  > (integer) 0

 

  • rpushx [key] [value]

將單個value插入一個類型為list且必須存在的key 如果key不存在,返回0,並不會報錯,lpushx是從list的左邊插入數據,rpushx則是從右邊。

rpushx redis:list:demo 5  > (integer) 5  rpushx empty:key 1  > (integer) 0

 

  • rpoplpush [source list] [destination list]

rpoplpush一個命令同時有兩個動作,而且是原子操作,有兩個參數

  1. 將列表 source 中的最後一個元素(最右邊的元素)彈出,並返回給客戶端。
  2. 將 source 彈出的元素插入到列表 destination ,作為 destination 列表的的頭元素(也就是最左邊的元素)

簡單來說就是從list:a取出一個元素丟到list:b

例如 list:a = 1 2 3 list:b = 4 5 6

執行rpoplpush a b 之後:

list:a = 1 2 list:b = 3 4 5 6

返回客戶端被操作的數 3

rpush list:a 1 2 3  > (integer) 3  rpush list:b 4 5 6  > (integer) 3  rpoplpush list:a list:b  > "3" // 返回客戶端被操作的數  // 查看執行後的情況  lrange list:a 0 -1  > 1) "1"  > 2) "2"  lrange list:b 0 -1  > 1) "3"  > 2) "4"  > 3) "5"  > 4) "6"

 

  • lindex [key] [index]

這個命令簡單實用,獲取key的index下標的元素,不存在返回nil

lindex redis:list:demo 0  > "4"  lindex redis:list:demo 999  > (nil)

 

  • lset [key] [index] [value]

直接設置key的index的value

lset redis:list:demo 0 5  > OK  lindex redis:list:demo 0  > "5"

 

隊列和棧

因為list提供的命令的便利性和多樣性,可以實現很多種數據結構,用的最多的就是隊列和棧兩個地方了,通過不同的方法分支成各種不同類型的隊列,例如雙端隊列,優先順序隊列等。

  • lpop [key] [timeout]

移除並返回列表 key 的左邊第一個元素,當 key 不存在時,返回 nil。

del redis:list:demo  > (integer) n  lpush redis:list:demo 1 2 3  > (integer) 3  lpop redis:list:demo  > "3" // 從左邊取出的數  lrange redis:list:demo 0 -1  > 1) "2" // 刪掉了最左邊的3  > 2) "1"

 

  • rpop [key][timeout]

移除並返回列表 key 的右邊第一個元素,當 key 不存在時,返回 nil。

del redis:list:demo  > (integer) n  lpush redis:list:demo 1 2 3  > (integer) 3  rpop redis:list:demo  > "1" // 從右邊取出的數  lrange redis:list:demo 0 -1  > 1) "3" // 刪掉了最右邊的1  > 2) "2"

 

  • blpop [key] [key …] [timeout]

阻塞式的lpop,它可以設置多個key和一個timeout,將在這多個key裡面選擇一個列表不為空的key,lpop一個值出來,timeout可以指定一個超時時間,超過將會斷開鏈接。

什麼是阻塞式呢?

就是說這個操作是需要等待的,可以理解為下面的偽程式碼:

while ((n = list.lpop()) != null) {      return n;  }

 

就是說如果list的lpop取出不為null時就立刻返回,否則就一直循環了。

如果timeout指定為0則表示沒有超時時間,一直等待

下面的示例請打開兩個終端窗口

// terminal a  lpush redis:list:demo 1 2 3  > (integer) 3  blpop redis:list:demo 0 // 0 表示一直等待  > "3"  blpop redis:list:demo 0 // 0 表示一直等待  > "2"  blpop redis:list:demo 0 // 0 表示一直等待  > "1"  lrange redis:list:demo 0 -1  > (nil) // 此時list已經空了  blpop redis:list:demo 0 // 會一直等待list有新的命令插入    // 等待terminal b    > 1) "redis:list:demo" 等待後返回的結果  > 2) "4"  > (18.83s)  // terminal b  lpush redis:list:demo 4  > (integer) 1 // terminal a 會獲取到這個4

 

可以看到最後一步,當terminal a 最終等到terminal b,push了一個值之後,返回的數據與正常pop的數據不一樣

  • brpop [key] [key …] [timeout]

參考blpop。基本行為一致,只是brpop是從list的右側pop,而blpop是左側

  • brpoplpush [source list] [destination list] [timeout]

brpoplpush 是 rpoplpush的阻塞版本,你可以直接參考上面rpoplpush命令的解釋,只是rpop變成了brpop,多了等待這一步。

分割

  • ltrim [key] [start] [end]

對一個列表進行修剪(trim),就是說,讓列表只保留指定區間內的元素,不在指定區間之內的元素都將被刪除。

注意,這裡不要搞反了,是將start和end中間的保留,刪除其餘的

del redis:demo:list  > (integer) n  lpush redis:demo:list 1 2 3 4  > (integer) 4  ltrim redis:demo:list 1 2 // 只需要1到2  > OK  lrange redis:demo:list 0 -1  > 1) "3"  > 2) "2"

 

  • lrem [key] [count] [value]

移除count個與value相等的元素。

del redis:demo:list  > (integer) n  lpush redis:demo:list 1 2 2 3 4  > (integer) 5  lrem redis:demo:list 1 2 // 移除1個2  > OK  lrange redis:demo:list 0 -1  > 1) "4"  > 2) "3"  > 3) "2"  > 4) "1"

 

tips,使用這個命令,你可以配合lua腳本做一個不重複的list, 就是每次在push一個value之前先lrem一下這個value

del redis:demo:list  > (integer) n  lrem redis:demo:list 1 a // 先檢查刪除  > (integer) 0  lpush redis:demo:list a // 再push  > (integer) 1

 

 …持續更新

 

github: https://github.com/294678380/redis-lerning

hash