寫在前面
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語言的字符串呢?
-
時間復雜度要求 redis的數據結構設計總是基於最優復雜度方案的,對每一個點的時間、空間復雜度要求非常高,這一點c語言的string就已經不滿足需求了,因為c自帶的字符串並不會記錄自身長度信息,所以每次獲取字符串長度的時間復雜度都是o(n),所以redis作者設計SDS時,有一個len字段,記錄了字符串的長度,這樣每次獲取長度時的時間復雜度就是O(1)了。
-
緩沖區溢出問題 其實也是c語言不記錄本身長度帶來的問題,當拼接字符串的時候,例如 hello + world 因為c不記錄長度,所以在拼接字符的時候需要手動為hello分配五個內存空間,然后才能+world,如果忘記分配內存,那么就會產生緩沖區溢出,而redis的解決方案是在SDS中分別記錄len和alloc,表示當前字符串長度和最大容量,這樣當進行字符串拼接的時候api直接去判斷最大容量是否滿足,滿足就直接插入,不滿足則對 char * 做一次擴容,然后插入,減少了人為出錯的概率,並且可以對alloc適當的進行空間預先分配,減少擴容次數,例如在創建字符串hello時,完全可以將alloc長度設置10,這樣在加入world時直接放進去就ok了。
-
實現了c語言字符串的識別特性,復用了c語言自帶的字符串函數 傳統的c語言使用的是n+1的char數組來表示長度n的字符串的,然后在n長度最后加上一個\0 , 所以redis的sds在設計的時候也加上了這個\0,這樣可以復用部分c語言字符串的函數。
-
二進制安全 c字符串中的字符必須符合某種編碼,比如 ASCII 並且除了字符串的末尾之外,字符串里面不能包含空字符(這里空字符指的是空(\0)不是空格、換行之類的字符),主要是不能存儲二進制的圖片、視頻、壓縮文件等內容,而我們知道redis是可以用來緩存圖片二進制數據的。因為redis記錄了字符長度。c沒有記錄長度的時候遇到\0就認為讀到字符結尾了。
可以看出,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一個命令同時有兩個動作,而且是原子操作,有兩個參數
- 將列表 source 中的最后一個元素(最右邊的元素)彈出,並返回給客戶端。
- 將 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
...持續更新