Redis(二)數據結構與鍵管理

  1、基礎知識java

  1.全局命令python

  • keys *   :查看全部鍵
  • dbsize:返回當前數據庫中鍵的總數
  • exists key:檢查鍵是否存在
  • del key ... :刪除鍵
  • expire key seconds:對鍵添加過時時間
  • ttl key:返回鍵的剩餘過時時間(-1鍵沒設置過時時間,-2鍵不存在)
  • type key:返回鍵的數據結構類型
bigjun@myubuntu:/$ redis-cli -h 127.0.0.1 -p 6379
127.0.0.1:6379> set hello world
OK
127.0.0.1:6379> set java jedis
OK
127.0.0.1:6379> set python redis-py
OK
127.0.0.1:6379> keys *
1) "hello"
2) "python"
3) "java"
4) "myname"
127.0.0.1:6379> dbsize
(integer) 4
127.0.0.1:6379> exists java
(integer) 1
127.0.0.1:6379> exists not_exist_key
(integer) 0
127.0.0.1:6379> del java
(integer) 1
127.0.0.1:6379> exists java
(integer) 0
127.0.0.1:6379> keys *
1) "hello"
2) "python"
3) "myname"
127.0.0.1:6379> del python myname
(integer) 2
127.0.0.1:6379> keys *
1) "hello"
127.0.0.1:6379> get hello
"world"
127.0.0.1:6379> expire hello 10
(integer) 1
127.0.0.1:6379> ttl hello
(integer) 5
127.0.0.1:6379> ttl hello
(integer) 3
127.0.0.1:6379> ttl hello
(integer) 2
127.0.0.1:6379> ttl hello
(integer) 1
127.0.0.1:6379> ttl hello
(integer) 0
127.0.0.1:6379> ttl hello
(integer) -2
127.0.0.1:6379> ttl hello
(integer) -2
127.0.0.1:6379> get hello
(nil)
127.0.0.1:6379> del hello
(integer) 0
127.0.0.1:6379> keys *
(empty list or set)
127.0.0.1:6379> set a b
OK
127.0.0.1:6379> type a
string
127.0.0.1:6379> rpush mylist a b c d e f g
(integer) 7
127.0.0.1:6379> type mylist
list
127.0.0.1:6379> type not_exsit_key
none
127.0.0.1:6379> shutdown nosave
not connected> 

  2.數據結構和內部編碼mysql

  使用type key命令能夠返回當前鍵的數據結構類型,分別包括:string(字符串)、hash(哈希)、list(列表)、set(集合)、zset(有序集合)。redis

  實際上Redis每種數據結構都有本身底層的內部編碼實現,並且是多種實現,這樣Redis會在合適的場景選擇合適的內部編碼。算法

  

  能夠經過object encoding命令查詢內部編碼:sql

127.0.0.1:6379> keys *
1) "a"
127.0.0.1:6379> rpush mylist a b c
(integer) 3
127.0.0.1:6379> object encoding a
"embstr"
127.0.0.1:6379> object encoding mylist
"quicklist"

  Redis這樣設計有兩個好處:數據庫

  • 能夠改進內部編碼,而對外的數據結構和命令沒有影響。
  • 多種內部編碼實現能夠在不一樣場景下發揮各自的優點。

  3.單線程架構編程

  Redis使用單線程架構和I/O多路複用模型來實現高性能的內存數據庫服務。ubuntu

  Redis客戶端調用都經歷了發送命令、執行命令、返回結果三個過程。後端

  

  由於Redis是單線程來處理命令的,因此一條命令從客戶端到服務器端不會馬上被執行,全部命令都會進入一個隊列中,而後逐個被執行,能夠肯定不會有兩條命令被同時執行,不會產生併發問題,這就是Redis單線程的基本模型。

  爲何Redis使用單線程模型會達到每秒萬級別的處理能力呢:

  • 純內存訪問,Redis將全部數據放在內存中,內存的相應時長大約爲100納秒,這是Redis達到每秒萬級別訪問的重要基礎。
  • 非阻塞I/O,Redis使用epoll做爲I/O多路複用技術的實現,在加上Redis自身的事件處理模型將epoll中的鏈接、讀寫、關閉都轉換爲事件,不在網絡I/O上浪費過多的時間。
  • 單線程避免了線程切換和竟態產生的消耗。

  單線程能帶來幾個好處:

  • 單線程能夠簡化數據結構和算法的實現,併發數據結構實現不但困難並且開發測試比較麻煩。
  • 單線程避免了線程切換和竟態產生的消耗,對於服務端開發來講,鎖和線程切換一般是性能殺手。

  單線程會有一個問題:

  對於每一個命令的執行時間是有要求的,若是某個命令執行過長,會形成其餘命令的阻塞,對於Redis這種高性能的服務來講是致命的,因此Redis是面向快速執行場景的數據庫。

  2、字符串

  字符串類型是Redis最基礎的數據結構。首先鍵都是字符串類型,值能夠是字符串(簡單的字符串、複雜的字符串(JSON、XML))、數字(整數、浮點數)、甚至是二進制(圖片、音頻、視頻),可是值最大不能超多512MB。

  

  1.命令

  (1)經常使用命令

  • 設置值
set key [ex seconds] [px milliseconds] [nx|xx]
ex seconds:爲鍵設置秒級過時時間
px milliseconds:爲鍵設置毫秒級過時時間
nx:鍵必須不存在,才能夠設置成功,用於添加
xx:與nx相反,鍵必須存在,才能夠設置成功,用於更新

  除了set選項,Redis還提供setex和setnx兩個命令:

setex key seconds value:做用和ex選項同樣
setnx key value:做用和nx選項同樣

  先來驗證nx和xx兩個選項:

127.0.0.1:6379> exists hello
(integer) 0
127.0.0.1:6379> set hello world 
OK
127.0.0.1:6379> setnx hello redis
(integer) 0
127.0.0.1:6379> get hello
"world"
127.0.0.1:6379> set hello jedis xx
OK
127.0.0.1:6379> get hello
"jedis"

  因爲鍵hello已存在,因此setnx失敗,返回結果爲0,而set xx成功,返回結果爲OK。

  因爲Redis的單線程命令處理機制,若是有多個客戶端同時執行setnx key value,根據setnx特性只有一個客戶端能設置成功,setnx能夠做爲分佈式鎖的一種實現方案。

  • 獲取值(若是要獲取的值不存在,則返回nil(空))
127.0.0.1:6379> get hello
"jedis"
127.0.0.1:6379> get not_exist_key
(nil)
  • 批量設置值
mset key value [key value...]

  經過mset命令一次性設置4個鍵值對:

127.0.0.1:6379> mset a 1 b 2 c 3 d 4
OK
  • 批量獲取值
127.0.0.1:6379> mget a b c d
1) "1"
2) "2"
3) "3"
4) "4"

  若是某些鍵不存在,則它的值爲空:

127.0.0.1:6379> mget a b f d
1) "1"
2) "2"
3) (nil)
4) "4"

  執行n次get命令的時間:n次get時間=n次網絡時間+n次命令時間

  

  執行1次mget命令的時間:1次mget時間=1次網絡時間+n次命令時間

  

  批量操做有助於提升業務處理效率,可是每次批量操做所發送的命令數不是無節制的,若是數量過多可能形成Redis阻塞或者網絡阻塞。

  • 計數
incr key
用於對值作自增操做,返回結果分爲三種狀況:
1.值不是整數,返回錯誤。
2.值是整數,返回自增後的結果。
3.鍵不存在,按照值爲0自增,返回結果爲1。

  除了incr命令,Redis還提供了decr(自減)、incrby(自增指定數字)、decrby(自減指定數字)、incrbyfloat(自增浮點數)

(integer) 0
127.0.0.1:6379> incr key
(integer) 1
127.0.0.1:6379> incr key
(integer) 2
127.0.0.1:6379> incr hello
(error) ERR value is not an integer or out of range

  (2)不經常使用命令

  • 追加值:append key value
127.0.0.1:6379> get hello
"jedis"
127.0.0.1:6379> append hello Java
(integer) 9
127.0.0.1:6379> get hello
"jedisJava"
  • 字符串長度:strlen key
127.0.0.1:6379> get hello
"jedisJava"
127.0.0.1:6379> strlen hello
(integer) 9
  • 設置並返回原值:getset key value
127.0.0.1:6379> exists hello
(integer) 0
127.0.0.1:6379> getset hello world
(nil)
127.0.0.1:6379> getset hello redis
"world"
  • 設置指定位置的字符:setrange key offset value
127.0.0.1:6379> set redis pest
OK
127.0.0.1:6379> setrange redis 0 b
(integer) 4
127.0.0.1:6379> get redis
"best"
  • 獲取部分字符串:getrange key start end
127.0.0.1:6379> getrange redis 0 1
"be"

  2.內部編碼

  字符串類型的內部編碼有3種:

  • int:8個字節的長整型。
  • embstr:小於等於39個字節的字符串。
  • raw:大於39個字節的字符串。

  Redis會根據當前值的類型和長度決定使用哪一種內部編碼實現。

  (1)整數類型:

127.0.0.1:6379> set int 8023
OK
127.0.0.1:6379> object encoding int
"int"

  (2)短字符串類型:

127.0.0.1:6379> set shortstring "hello world"
OK
127.0.0.1:6379> object encoding shortstring
"embstr"

  (3)場字符串類型:  

127.0.0.1:6379> set longstring "I am a string which has greater than 39 byte, you know?"
OK
127.0.0.1:6379> object encoding longstring
"raw"
127.0.0.1:6379> strlen longstring
(integer) 55

  3.使用場景

  (1)緩存功能

  比較典型的緩存使用場景是,Redis做爲緩存層,MySQL做爲存儲層,絕大部分請求的數據都是從Redis中獲取。

  

  因爲Redis具備支撐高併發的特性,因此緩存一般能起到加速讀寫和下降後端壓力的做用。

  例如這麼一個場景:用戶想要獲取用戶信息的話,首先須要根據用戶提供的id,先去Redis中尋找用戶信息,若是沒有從Redis中獲取到用戶信息,那就須要從MySQL中進行獲取,並將結果回寫到Redis,添加1小時過時時間,若是這一個小時之內用戶再次想獲取信息的話,就直接從Redis中去獲取到信息而不用再返回到MySQL中了。

UserInfo getUserInfo(long id){
  // 根據用戶提供的id,定義Redis中鍵key的值   userRedisKey
= "user:info:" + id
  // 根據鍵key的值,從Redis中獲取到對應的value值
  value = redis.get(userRedisKey);
  // 聲明UserInfo類的對象變量   UserInfo userInfo;
  // 若是從Redis中獲取到了value值   
if (value != null) {
    // 將Redis中存的value值反序列化爲Java對象userInfo     userInfo
= deserialize(value);   } else {
    // 若是沒有從Redis中獲取到值,那麼就從MySQL中去尋找     userInfo
= mysql.get(id);
    // 若是從MySQL中獲取到了值     
if (userInfo != null)
      // 將從MySQL中獲取到的值序列化並回寫到Redis中並設置過時時間1小時       redis.setex(userRedisKey,
3600, serialize(userInfo));   }   return userInfo; }

  (2)計數

  許多應用都會使用Redis做爲計數的基礎工具,它能夠實現快速計數、查詢緩存的功能,同時數據能夠異步落地到其餘數據源。視頻播放數系統就是使用Redis做爲視頻播放數計數的基礎組件,用戶每播放一次視頻,相應的視頻播放數就會自增1:

long incrVideoCounter(long id) {
  key = "video:playCount:" + id;
  return redis.incr(key);
}

  (3)共享Session

  一個分佈式Web服務將用戶的Session信息(例如用戶登陸信息)保存在各自服務器中,這樣會形成一個問題,出於負載均衡的考慮,分佈式服務會將用戶的訪問均衡到不一樣服務器上,用戶刷新一次訪問可能會發現須要從新登陸,這個問題是用戶沒法容忍的。

  爲了解決這個問題,可使用Redis將用戶的Session進行集中管理,在這種模式下只要保證Redis是高可用和擴展性的,每次用戶更新或者查詢登陸信息都直接從Redis中集中獲取。

  

  (4)限速

  不少應用出於安全的考慮,會在每次進行登陸時,讓用戶輸入手機驗證碼,從而肯定是不是用戶本人。可是爲了短信接口不被頻繁訪問,會限制用戶每分鐘獲取驗證碼的頻率,例如一分鐘不能超過5次:

phoneNum = "138xxxxxxxx";
key = "shortMsg:limit:" + phoneNum;
// SET key value EX 60 NX
isExists = redis.set(key,1,"EX 60","NX");
if(isExists != null || redis.incr(key) <=5){
  // 經過
}else{
  // 限速
}

  上述就是利用Redis實現了限速功能,例如一些網站限制一個IP地址不能在一秒鐘以內訪問超過n次也能夠採用相似的思路。

  3、哈希

  Redis中,哈希類型是指鍵值自己又是一個鍵值對結構,形如value={{field1,value1},...{fieldN,valueN}}。

  

  1.命令

  (1)設置值:hset key field value (設置成功返回1,反之返回0。和setnx命令同樣有hsetnx命令)

127.0.0.1:6379> hset user:1 name tom
(integer) 1

  (2)獲取值:hget key field

127.0.0.1:6379> hget user:1 name
"tom"
127.0.0.1:6379> hget user:2 name
(nil)
127.0.0.1:6379> hget user:1 age
(nil)

  (3)刪除field:hdel key field [field...](返回成功刪除field的個數)

127.0.0.1:6379> hdel user:1 name
(integer) 1
127.0.0.1:6379> hget user:1 name
(nil)
127.0.0.1:6379> hdel user:1 age
(integer) 0

  (4)計算field個數:hlen key

127.0.0.1:6379> hset user:1 name LianJiang
(integer) 1
127.0.0.1:6379> hset user:1 age 23
(integer) 1
127.0.0.1:6379> hset user:1 sex boy
(integer) 1
127.0.0.1:6379> hlen user:1
(integer) 3

  (5)批量設置或獲取field-value:hmget key field [field...]         hmset key field value [field value ...]

127.0.0.1:6379> hmset user:1 name QiaoJiang age 22 sex boy
OK
127.0.0.1:6379> hmget user:1 name age
1) "QiaoJiang"
2) "22"

  (6)判斷field是否存在:hexists key field

127.0.0.1:6379> hexists user:1 name
(integer) 1
127.0.0.1:6379> hexists user:1 city
(integer) 0

  (7)獲取全部field: hkey key

127.0.0.1:6379> hkeys user:1
1) "name"
2) "age"
3) "sex"

  (8)獲取全部value:hvals key

127.0.0.1:6379> hvals user:1
1) "QiaoJiang"
2) "22"
3) "boy"

  (9)獲取全部的field-value:hgetall key

127.0.0.1:6379> hgetall user:1
1) "name"
2) "QiaoJiang"
3) "age"
4) "22"
5) "sex"
6) "boy"

  (10)自增指定數字,自增指定浮點數:hincrby key field     hincrbyfloat key field

  (11)計算value的字符串長度:hstrlen key field

127.0.0.1:6379> hstrlen user:1 name
(integer) 9

  2.內部編碼

  • ziplist(壓縮列表):當哈希類型元素個數小於hash-max-ziplist-entries配置(默認512個)、同時全部值都小於hash-max-ziplist-value配置(默認64字節)時,Redis會使用ziplist做爲哈希的內部實現,ziplist使用更加緊湊的結構實現多個元素的連續存儲,因此在節省內存方面比hashtable更加優秀。
  • hashtable(哈希表):當哈希類型沒法知足ziplist的條件時,Redis會使用hashtable做爲哈希的內部實現,由於此時ziplist的讀寫效率會降低,而hashtable的讀寫時間複雜度爲O(1)。

  (1)當field個數比較少且沒有大的value時,內部編碼爲ziplist

  (2)當有value大於64字節,內部編碼會由ziplist變爲hashtable

  (3)當field個數超過512,內部編碼也會有ziplist變爲hashtable

  3.使用場景

  將關係型數據庫和Redis哈希類型數據庫作對比:

  

  相比於使用字符串序列化緩存用戶信息,哈希類型變得更加直觀,而且在更新操做上會更加便捷。能夠將每一個用戶的id定義爲鍵後綴,多對field-value對應每一個用戶的屬性:

UserInfo getUserInfo(long id){
  // 用戶 id 做爲 key 後綴
  userRedisKey = "user:info:" + id;
  // 使用 hgetall 獲取全部用戶信息映射關係
  userInfoMap = redis.hgetAll(userRedisKey);
  UserInfo userInfo;
  if (userInfoMap != null) {
    // 將映射關係轉換爲 UserInfo
    userInfo = transferMapToUserInfo(userInfoMap);
  } else {
  // 從 MySQL 中獲取用戶信息
  userInfo = mysql.get(id);
  // 將 userInfo 變爲映射關係使用 hmset 保存到 Redis 中
  redis.hmset(userRedisKey, transferUserInfoToMap(userInfo));
  // 添加過時時間
  redis.expire(userRedisKey, 3600);
  }
return userInfo;
}

  可是須要注意的是哈希類型和關係型數據庫有兩點不一樣之處:

  • 哈希類型是稀疏的,而關係型數據庫是徹底結構化的,例如哈希類型每一個鍵能夠有不一樣的field,而關係型數據庫一旦添加新的列,全部行都要爲其設置值(即便爲NULL)。

  

  • 關係型數據庫能夠作複雜的關係查詢,而Redis去模擬關係型複雜查詢開發困難,維護成本高。

  總結一下三種緩存用戶信息的方法並給出三種方案的實現方法和優缺點分析。

  • 原生字符串類型:每一個屬性一個鍵。
set user:1:name tom
set user:1:age 23
set user:1:city beijing

  優勢:簡單直觀,每一個屬性都支持更新操做。
  缺點:佔用過多的鍵,內存佔用量較大,同時用戶信息內聚性比較差,因此此種方案通常不會在生產環境使用。

  • 序列化字符串類型:將用戶信息序列化後用一個鍵保存。
set user:1 serialize(userInfo)

  優勢:簡化編程,若是合理的使用序列化能夠提升內存的使用效率。
  缺點:序列化和反序列化有必定的開銷,同時每次更新屬性都須要把所有數據取出進行反序列化,更新後再序列化到Redis中。

  • 哈希類型:每一個用戶屬性使用一對field-value,可是隻用一個鍵保存。
hmset user:1 name tomage 23 city beijing

  優勢:簡單直觀,若是使用合理能夠減小內存空間的使用。
  缺點:要控制哈希在ziplist和hashtable兩種內部編碼的轉換,hashtable會消耗更多內存。

  4、列表

  列表(list)類型是用來存儲多個有序的字符串,如圖2-18所示,a、b、c、d、e五個元素從左到右組成了一個有序的列表,列表中的每一個字符串稱爲元素(element),一個列表最多能夠存儲2 32 -1個元素。在Redis中,能夠對列表兩端插入(push)和彈出(pop),還能夠獲取指定範圍的元素列表、獲取指定索引下標的元素等(如圖2-18和圖2-19所示)。列表是一種比較靈活的數據結構,它能夠充當棧和隊列的角色,在實際開發上有不少應用場景。

  

  列表類型有兩個特色:

  • 第一,列表中的元素是有序的,這就意味着能夠經過索引下標獲取某個元素或者某個範圍內的元素列表。
  • 第二,列表中的元素能夠是重複的。

  1.命令

  (1)添加 rpush  lpush linsert

  • 從右邊插入元素:rpush key value [value...](lrange listkey 0 -1命令能夠從左到右獲取列表的全部元素)
127.0.0.1:6379> rpush listkey c b a
(integer) 3
127.0.0.1:6379> lrange listkey 0 -1
1) "c"
2) "b"
3) "a"
  • 從左邊插入元素:lpush key value [value...]
127.0.0.1:6379> lpush listkey d e f
(integer) 6
127.0.0.1:6379> lrange listkey 0 -1
1) "f"
2) "e"
3) "d"
4) "c"
5) "b"
6) "a"
  • 向某個元素前或後插入元素:linsert key before|after pivot value
127.0.0.1:6379> linsert listkey before b java
(integer) 7
127.0.0.1:6379> lrange listkey 0 -1
1) "f"
2) "e"
3) "d"
4) "c"
5) "java"
6) "b"
7) "a"

  (2)查找 lrange lindex lien

  • 獲取指定範圍內的元素列表:lrange key start end

  索引下標有兩個特色:

  ①索引下標從左到右分別是0到N-1,可是從右到左分別是-1和-N。

  ②lrange中的end選項包含了自身,若是想落得第2到第4個元素,end的值就要爲3

127.0.0.1:6379> lrange listkey 1 3
1) "e"
2) "d"
3) "c"
127.0.0.1:6379> lrange listkey 0 6
1) "f"
2) "e"
3) "d"
4) "c"
5) "java"
6) "b"
7) "a"
  • 獲取列表指定索引下標的元素:lindex key index(-1表示最後一個元素)
127.0.0.1:6379> lindex listkey -1
"a"
  • 獲取列表長度:llen key
127.0.0.1:6379> llen listkey
(integer) 7

  (3)刪除 lpop rpop lrem ltrim

  • 從列表左側彈出元素:lpop key
127.0.0.1:6379> lrange listkey 0 -1
1) "f"
2) "e"
3) "d"
4) "c"
5) "java"
6) "b"
7) "a"
127.0.0.1:6379> lpop listkey
"f"
127.0.0.1:6379> lrange listkey 0 -1
1) "e"
2) "d"
3) "c"
4) "java"
5) "b"
6) "a"
  • 從列表右側彈出元素:rpop key
127.0.0.1:6379> rpop listkey
"a"
127.0.0.1:6379> lrange listkey 0 -1
1) "e"
2) "d"
3) "c"
4) "java"
5) "b"
  • 刪除指定元素:lrem key count value
lrem key count value
從列表中找到等於value的元素進行刪除,根據count的不一樣分爲三種狀況:
count>0,從左到右,刪除最多count個元素
count<0,  從右到左,刪除最多count絕對值個元素
count=0,刪除全部

127.0.0.1:6379> lpush listkey a a a a a 
(integer) 10
127.0.0.1:6379> lrange listkey 0 -1
 1) "a"
 2) "a"
 3) "a"
 4) "a"
 5) "a"
 6) "e"
 7) "d"
 8) "c"
 9) "java"
10) "b"
127.0.0.1:6379> lrem listkey 4 a
(integer) 4
127.0.0.1:6379> lrange listkey 0 -1
1) "a"
2) "e"
3) "d"
4) "c"
5) "java"
6) "b"
  • 按照索引範圍修剪列表:ltrim key start end
127.0.0.1:6379> lrange listkey 0 -1
1) "a"
2) "e"
3) "d"
4) "c"
5) "java"
6) "b"
127.0.0.1:6379> ltrim listkey 3 -1
OK
127.0.0.1:6379> lrange listkey 0 -1
1) "c"
2) "java"
3) "b"

  (4)修改 lset key index newValue

127.0.0.1:6379> lset listkey 2 python
OK
127.0.0.1:6379> lrange listkey 0 -1
1) "c"
2) "java"
3) "python"

  (5)阻塞操做(經過timeout定義阻塞時間) blpop key [key ...] timeout      brpop key [key ...] timeout   

  • 列表爲空:若是timeout=3,那麼客戶端要等到3秒後返回,若是timeout=0,那麼客戶端一直阻塞下去:

  在客戶端1執行timeout=0時,會一直阻塞下去

127.0.0.1:6379> brpop list:test 3
(nil)
(3.06s)
127.0.0.1:6379> brpop list:test 0
...

  這個時候在客戶端2添加了數據element1

127.0.0.1:6379> rpush list:test element1
(integer) 1

  客戶端2添加完以後客戶端1當即有返回:

127.0.0.1:6379> brpop list:test 3
(nil)
(3.06s)
127.0.0.1:6379> brpop list:test 0
1) "list:test"
2) "element1"
(71.97s)
  • 列表不爲空,客戶端會理解返回:
127.0.0.1:6379> rpush list:test element1
(integer) 1
127.0.0.1:6379> brpop list:test 0
1) "list:test"
2) "element1"

  2.內部編碼

  • ziplist(壓縮列表):當列表的元素個數小於list-max-ziplist-entries配置(默認512個),同時列表中每一個元素的值都小於list-max-ziplist-value配置時(默認64字節),Redis會選用ziplist來做爲列表的內部實現來減小內存的使用。
  • linkedlist(鏈表):當列表類型沒法知足ziplist的條件時,Redis會使用linkedlist做爲列表的內部實現。

  (1)當元素個數較少且沒有大元素時,內部編碼爲ziplist

  (2)當元素個數超過512個,內部編碼變爲linkedlist

  (3)當某個元素超過64字節,內部編碼也會變爲linkedlist

  3.使用場景

  列表的使用場景不少,能夠記住這些公式:

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

  (1)消息隊列

  Redis的lpush+brpop命令組合便可實現阻塞隊列,生產者客戶端使用lrpush從列表左側插入元素,多個消費者客戶端使用brpop命令阻塞式的「搶」列表尾部的元素,多個消費者客戶端保證了消費的負載均衡和高可用性。

  

  (2)文章列表

  • 每篇文章使用哈希結構存儲,例如每篇文章有3個屬性title、timestamp、content:
hmset acticle:1 title xx timestamp 1476536196 content xxxx
...
hmset acticle:k title yy timestamp 1476512536 content yyyy
  • 向用戶文章列表添加文章,user:{id}:articles做爲用戶文章列表的鍵:
lpush user:1:acticles article:1 article3
...
lpush user:k:acticles article:5
...
  • 分頁獲取用戶文章列表,例以下面僞代碼獲取用戶id=1的前10篇文章:
articles = lrange user:1:articles 0 9
for article in {articles}
  hgetall {article}

  使用列表類型保存和獲取文章列表會存在兩個問題。

  • 第一,若是每次分頁獲取的文章個數較多,須要執行屢次hgetall操做,此時能夠考慮使用Pipeline批量獲取,或者考慮將文章數據序列化爲字符串類型,使用mget批量獲取。
  • 第二,分頁獲取文章列表時,lrange命令在列表兩端性能較好,可是若是列表較大,獲取列表中間範圍的元素性能會變差,此時能夠考慮將列表作二級拆分,或者使用Redis3.2的quicklist內部編碼實現,它結合ziplist和linkedlist的特色,獲取列表中間範圍的元素時也能夠高效完成。

  5、集合

  集合(set)類型也是用來保存多個的字符串元素,但和列表類型不同的是,集合中不容許有重複元素,而且集合中的元素是無序的,不能經過索引下標獲取元素。如圖2-22所示,集合user:1:follow包含
着"it"、"music"、"his"、"sports"四個元素,一個集合最多能夠存儲2 ^32 -1個元素。Redis除了支持集合內的增刪改查,同時還支持多個集合取交集、並集、差集,合理地使用好集合類型,能在實際開發中解決不少實際問題。

  

  1.命令

  (1)集合內操做

  • 添加元素:sadd key element [element ...]
127.0.0.1:6379> exists myset
(integer) 0
127.0.0.1:6379> sadd myset a b c
(integer) 3
127.0.0.1:6379> smembers myset
1) "a"
2) "b"
3) "c"
127.0.0.1:6379> sadd myset a b
(integer) 0
  • 刪除元素:srem key element [element ...]
127.0.0.1:6379> sadd myset a b
(integer) 0
127.0.0.1:6379> srem myset a b
(integer) 2
127.0.0.1:6379> srem myset a 
(integer) 0
127.0.0.1:6379> smembers myset
1) "c"
  • 計算元素個數:scard key
127.0.0.1:6379> smembers myset
1) "c"
127.0.0.1:6379> scard myset
(integer) 1
  • 判斷元素是否在集合中:sismember key element
127.0.0.1:6379> sismember myset c
(integer) 1
127.0.0.1:6379> sismember myset a
(integer) 0
  • 隨機從集合返回指定個數元素:srandmember key [count]
127.0.0.1:6379> sadd myset a b d e f g
(integer) 6
127.0.0.1:6379> smembers myset
1) "c"
2) "d"
3) "b"
4) "e"
5) "a"
6) "g"
7) "f"
127.0.0.1:6379> srandmember myset 3
1) "c"
2) "f"
3) "g"
127.0.0.1:6379> srandmember myset 3
1) "a"
2) "c"
3) "g"
  • 從集合隨機彈出元素:spop key [count]
127.0.0.1:6379> smembers myset
1) "c"
2) "d"
3) "b"
4) "e"
5) "a"
6) "g"
7) "f"
127.0.0.1:6379> spop myset 1
1) "d"
127.0.0.1:6379> spop myset 2
1) "f"
2) "e"
127.0.0.1:6379> smembers myset
1) "b"
2) "a"
3) "c"
4) "g"
  • 獲取全部元素:smembers key
127.0.0.1:6379> smembers myset
1) "b"
2) "a"
3) "c"
4) "g"

  (2)集合間操做  

  首先定義兩個集合,user:1:follow & user:2:follow

127.0.0.1:6379> sadd user:1:follow it music his sports
(integer) 4
127.0.0.1:6379> sadd user:2:follow it news ent sports
(integer) 4
  • 求多個集合的交集:sinter   key [key ...]
127.0.0.1:6379> sinter user:1:follow user:2:follow
1) "it"
2) "sports"
  • 求多個集合的並集:suinon key [key ...]
127.0.0.1:6379> sunion user:1:follow user:2:follow
1) "ent"
2) "his"
3) "it"
4) "news"
5) "music"
6) "sports"
  • 求多個集合的差集:sdiff     key [key ...]
127.0.0.1:6379> sdiff user:1:follow user:2:follow
1) "his"
2) "music"
127.0.0.1:6379> sdiff user:2:follow user:1:follow
1) "ent"
2) "news"
  • 將交集、並集、差集的結果保存 sinsterstore|suionstore|sdiffstore destination key [key ...]
127.0.0.1:6379> sinterstore user:1_2:inter user:1:follow user:2:follow
(integer) 2
127.0.0.1:6379> type user:1_2:inter
set
127.0.0.1:6379> smembers user:1_2:inter
1) "it"
2) "sports"

  2.內部編碼

  • intset(整數集合):當集合中的元素都是整數且元素個數小於set-max-intset-entries配置(默認512個)時,Redis會選用intset來做爲集合的內部實現,從而減小內存的使用。
  • hashtable(哈希表):當集合類型沒法知足intset的條件時,Redis會使用hashtable做爲集合的內部實現。

  (1)當元素個數較少且都爲整數時,內部編碼爲intset

  (2)當元素個數超過512個,內部編碼變爲hashtable

  (3)當某個元素不爲整數時,內部編碼也會變爲hashtable

  3.使用場景

  集合類型的應用場景一般分爲幾下幾種:

  • sadd = Tagging(標籤)
  • spop/srandmember = Random item(生成隨機數,好比抽獎)
  • sadd + sinter = Social Graph(社交需求)

  其中使用最多的場景是標籤(tag),主要分爲如下功能:

  (1)給用戶添加標籤

sadd user:1:tags tag1 tag2 tag5
sadd user:2:tags tag2 tag3 tag5
...
sadd user:k:tags tag1 tag2 tag4
...

  (2)給標籤添加用戶

sadd tag1:users user:1 user:3
sadd tag2:users user:1 user:2 user:3
...
sadd tagk:users user:1 user:2
...

  (3)刪除用戶下的標籤

srem user:1:tags tag1 tag5
...

  (4)刪除標籤下的用戶

srem tag1:users user:1
srem tag5:users user:1
...

  (5)計算用戶共同感興趣的標籤

sinter user:1:tags user:2:tags

  6、有序集合

  有序集合保留了集合不能有重複成員的特性,但不一樣的是,有序集合中的元素能夠排序。可是它和列表使用索引下標做爲排序依據不一樣的是,它給每一個元素設置一個分數(score)做爲排序的依據。如圖2-24所示,該有序集合包含kris、mike、frank、tim、martin、tom,它們的分數分別是一、9一、200、220、250、251,有序集合提供了獲取指定分數和元素範圍查詢、計算成員排名等功能,合理的利用有序集合,能幫助咱們在實際開發中解決不少問題。(有序集合中的元素不能重複,可是score能夠重複,就和一個班裏的同窗學號不能重複,可是考試成績能夠相同)

  

  列表、集合和有序集合三者的異同點:

  

  1.命令

  (1)集合內操做

  • 添加成員:zadd key [NX|XX] [CH] [INCR] score member [score member ...]
zadd key [NX|XX] [CH] [INCR] score member [score member ...]
nx:member必須不存在,才能夠設置成功,用於添加
xx:member必須存在,才能夠設置成功,用於更新
ch:返回這次操做後,有序集合元素和分數發生變化的個數
incr:對score作增長,至關於zincrby

127.0.0.1:6379> zadd user:ranking 251 tom
(integer) 1
127.0.0.1:6379> zadd user:ranking 1 kris 91 mike 200 frank 220 tim 250 martin
(integer) 5
  • 計算成員個數:zcard key
127.0.0.1:6379> zcard user:ranking
(integer) 6
  • 計算某個成員的分數:zscore key member
127.0.0.1:6379> zscore user:ranking tom
"251"
127.0.0.1:6379> zscore user:ranking not_exist_member
(nil)
  • 計算成員的排名:zrank key member(從低到高) | zrevrank key member(從高到低)
127.0.0.1:6379> zrank user:ranking tom
(integer) 5
127.0.0.1:6379> zrevrank user:ranking tom
(integer) 0
  • 刪除成員:zrem key member [member ...]
127.0.0.1:6379> zrem user:ranking mike
(integer) 1
  • 增長成員的分數:zincrby key increment member
127.0.0.1:6379> zscore user:ranking tom
"251"
127.0.0.1:6379> zincrby user:ranking 9 tom
"260"
  • 返回指定排名範圍的成員:zrange|zrevrange key start end [withscores]
127.0.0.1:6379> zrange user:ranking 0 -1 withscores
 1) "kris"
 2) "1"
 3) "frank"
 4) "200"
 5) "tim"
 6) "220"
 7) "martin"
 8) "250"
 9) "tom"
10) "260"
127.0.0.1:6379> zrevrange user:ranking 0 3 withscores
1) "tom"
2) "260"
3) "martin"
4) "250"
5) "tim"
6) "220"
7) "frank"
8) "200"
127.0.0.1:6379> zrevrange user:ranking 0 3 
1) "tom"
2) "martin"
3) "tim"
4) "frank"
  • 返回指定範圍分數的成員:zrangebyscore key min max [withscores] [limit offset count]    |    zrevrange key max min [withscores] [limit offset count] (+inf表明無窮大,-inf表明無窮小)
127.0.0.1:6379> zrangebyscore user:ranking 200 221 withscores
1) "frank"
2) "200"
3) "tim"
4) "220"
127.0.0.1:6379> zrangebyscore user:ranking 200 220 withscores
1) "frank"
2) "200"
3) "tim"
4) "220"
127.0.0.1:6379> zrangebyscore user:ranking 200 219 withscores
1) "frank"
2) "200"
127.0.0.1:6379> zrevrangebyscore user:ranking 260 200 withscores
1) "tom"
2) "260"
3) "martin"
4) "250"
5) "tim"
6) "220"
7) "frank"
8) "200"
127.0.0.1:6379> zrevrangebyscore user:ranking 260 201 withscores
1) "tom"
2) "260"
3) "martin"
4) "250"
5) "tim"
6) "220"
127.0.0.1:6379> zrevrangebyscore user:ranking 260 -inf withscores
 1) "tom"
 2) "260"
 3) "martin"
 4) "250"
 5) "tim"
 6) "220"
 7) "frank"
 8) "200"
 9) "kris"
10) "1"
  • 返回指定分數範圍成員個數:zcount key min max
127.0.0.1:6379> zcount user:ranking 200 220
(integer) 2
  • 刪除指定排名內的升序元素:zremrangebyrank key start end
127.0.0.1:6379> zrange user:ranking 0 -1 withscores
 1) "kris"
 2) "1"
 3) "frank"
 4) "200"
 5) "tim"
 6) "220"
 7) "martin"
 8) "250"
 9) "tom"
10) "260"
127.0.0.1:6379> zremrangebyrank user:ranking 0 2
(integer) 3
127.0.0.1:6379> zrange user:ranking 0 -1 withscores
1) "martin"
2) "250"
3) "tom"
4) "260"
  • 刪除指定分數範圍的成員:zremrangebyscore key min max
127.0.0.1:6379> zrange user:ranking 0 -1 withscores
1) "martin"
2) "250"
3) "tom"
4) "260"
127.0.0.1:6379> zremrangebyscore user:ranking 250 +inf
(integer) 2
127.0.0.1:6379> zrange user:ranking 0 -1 withscores
(empty list or set)

  (2)集合間操做

  首先定義兩個有序集合:

127.0.0.1:6379> zadd user:ranking:1 1 kris 91 mike 200 frank 220 tim 250 martin 251 tom
(integer) 6
127.0.0.1:6379> zadd user:ranking:2 8 james 77 mike 625 martin 888 tom
(integer) 4
127.0.0.1:6379> zrange user:ranking:1 0 -1 withscores
 1) "kris"
 2) "1"
 3) "mike"
 4) "91"
 5) "frank"
 6) "200"
 7) "tim"
 8) "220"
 9) "martin"
10) "250"
11) "tom"
12) "251"
127.0.0.1:6379> zrange user:ranking:2 0 -1 withscores
1) "james"
2) "8"
3) "mike"
4) "77"
5) "martin"
6) "625"
7) "tom"
8) "888"
  • 交集
zinterstore destination numkeys key [key ...] [weights weight [weight ...]][aggregate sum|min|max]
destination :交集計算結果保存到這個鍵
numkeys:須要作交集計算鍵的個數
key [key ...]:須要作交集計算的鍵
weights weight [weight ...]:每一個鍵的權重,在作交集計算時,每一個鍵中的每一個member會將本身分數乘以這個權重,每一個鍵的權重默認是1
aggregate sum|min|max:計算成員交集後,分值能夠按照sum(和)、min(最小值)、max(最大值)作彙總,默認值是sum

127.0.0.1:6379> zinterstore user:ranking:1_inter_2 2 user:ranking:1 user:ranking:2
(integer) 3
127.0.0.1:6379> zrange user:ranking:1_inter_2 0 -1 withscores
1) "mike"
2) "168"
3) "martin"
4) "875"
5) "tom"
6) "1139"

127.0.0.1:6379> zinterstore user:ranking:1_inter_2 2 user:ranking:1 user:ranking:2 weights 1 0.5 aggregate max
(integer) 3
127.0.0.1:6379> zrange user:ranking:1_inter_2 0 -1 withscores
1) "mike"
2) "91"
3) "martin"
4) "312.5"
5) "tom"
6) "444"

  • 並集(和交集全部參數同樣)
127.0.0.1:6379> zunionstore user:ranking:1_union_2 2 user:ranking:1 user:ranking:2 
(integer) 7
127.0.0.1:6379> zrange user:ranking:1_union_2 0 -1 withscores
 1) "kris"
 2) "1"
 3) "james"
 4) "8"
 5) "mike"
 6) "168"
 7) "frank"
 8) "200"
 9) "tim"
10) "220"
11) "martin"
12) "875"
13) "tom"
14) "1139"

  2.內部編碼

  • ziplist(壓縮列表):當有序集合的元素個數小於zset-max-ziplist-entries配置(默認128個),同時每一個元素的值都小於zset-max-ziplist-value配置(默認64字節)時,Redis會用ziplist來做爲有序集合的內部實現,ziplist能夠有效減小內存的使用。
  • skiplist(跳躍表):當ziplist條件不知足時,有序集合會使用skiplist做爲內部實現,由於此時ziplist的讀寫效率會降低。

  (1)當元素較少且每一個元素較小時,內部編碼爲ziplist

  (2)當元素個數超過128個,內部編碼變爲skiplist

  (3)當某個元素大於64字節時,內部編碼變爲hashtable

  3.使用場景

  有序集合比較典型的使用場景就是排行榜系統。例如視頻網站須要對用戶上傳的視頻作排行榜,榜單的維度多是多個方面的:按照上傳時間、按照播放數量、按照點贊次數、按照收藏次數等。

  以按照點贊次數記錄天天上傳視頻的排行榜,主要須要實現如下4個功能:

  (1)添加用戶贊數

  例如用戶mike上傳了一個視頻,並得到了3個贊,可使用有序集合的zadd和zincrby功能:

zadd user:ranking:2016_03_15 mike 3

  若是以後再得到一個贊,可使用zincrby:

zincrby user:ranking:2016_03_15 mike 1

  (2)取消用戶贊數

  因爲各類緣由(例如用戶註銷、用戶做弊)須要將用戶刪除,此時須要將用戶從榜單中刪除掉,可使用zrem。例如刪除成員tom:

zrem user:ranking:2016_03_15 mike

  (3)展現獲贊數最多的十個用戶

zrevrangebyrank user:ranking:2016_03_15 0 9

  (4)展現用戶信息及用戶分數

  將用戶名做爲鍵後綴,將用戶信息保存在哈希類型中,至於用戶的分數和排名可使用zscore和zrank兩個功能:

hgetall user:info:tom
zscore user:ranking:2016_03_15 mike
zrank user:ranking:2016_03_15 mike

  7、鍵管理

  1.單個鍵管理

  • 鍵重命名:rename key newkey
127.0.0.1:6379> get hello
"world"
127.0.0.1:6379> rename hello redis
OK
127.0.0.1:6379> get redis
"world"
127.0.0.1:6379> get hello
(nil)

若是在rename以前,鍵已經存在,那麼它的值將被覆蓋:
127.0.0.1:6379> get redis
"world"
127.0.0.1:6379> get java
"jedis"
127.0.0.1:6379> rename redis java
OK
127.0.0.1:6379> get java
"world"
127.0.0.1:6379> gert redis
(error) ERR unknown command 'gert'
127.0.0.1:6379> get redis
(nil)

爲了防止被強行rename,可使用renamenx來確保只有newKey不存在時才被覆蓋:
127.0.0.1:6379> get java
"world"
127.0.0.1:6379> get python
"redis-py"
127.0.0.1:6379> renamenx java python
(integer) 0
127.0.0.1:6379> get java
"world"
  • 隨機返回一個鍵:randomkey
127.0.0.1:6379> keys *
1) "python"
2) "java"
127.0.0.1:6379> randomkey
"python"
127.0.0.1:6379> randomkey
"java"
127.0.0.1:6379> randomkey
"python"
127.0.0.1:6379> randomkey
"python"
  • 鍵過時

  ①expire key seconds:鍵在seconds秒後過時

  ②expireat key timastamp:鍵在秒級時間戳timestamp後過時

  ③pexpire key milliseconds:鍵在milliseconds毫秒後過時

  ④pexpireat key milliseconds-timestamp:鍵在毫秒時間戳timestamp後過時

  • 鍵遷移

  ①move key db

  ②dump key + restore key ttl value

  ③migrate host port key| destination-db timeout [COPY] [REPLACE] [KEYS key]

  2.遍歷鍵

  • 全局遍歷鍵:keys pattern
127.0.0.1:6379> dbsize
(integer) 0
127.0.0.1:6379> mset hello world redis test jedis best hill high
OK
127.0.0.1:6379> keys *
1) "redis"
2) "hill"
3) "jedis"
4) "hello"
127.0.0.1:6379> keys [j,r]edis
1) "redis"
2) "jedis"
127.0.0.1:6379> keys h?ll*
1) "hill"
2) "hello"

  考慮到Redis的單線程架構,若是Redis包含了大量的鍵,執行keys命令極可能會形成Redis阻塞,因此通常建議不要在生產環境中使用keys命令。在一下三種狀況下,可使用:

  ①在一個不對外提供服務的Redis從節點上執行,這樣不會阻塞到客戶端的請求,可是會影響到主從複製。

  ②若是確認鍵值總數確實比較少,能夠執行該命令。

  ③可使用scan命令漸進式地遍歷全部鍵,能夠有效防止阻塞。

  • 漸進式遍歷:scan cursor [match pattern] [count number]

  Redis存儲鍵值對實際使用的是hashtable的數據結構:

  

  每次執行scan,只掃描一個字典中的一部分鍵,直到將字典中的全部鍵遍歷完畢:

  

scan cursor [match pattern] [count number]
cursor:必需參數,實際上cursor是一個遊標,第一次遍歷從0開始,每次scan遍歷完都會返回當前遊標的值,直到遊標值爲0,表示遍歷結束。
match pattern:可選參數,它的做用的是作模式的匹配,這點和keys的模式匹配很像。
count number:可選參數,它的做用是代表每次要遍歷的鍵個數,默認值是10,此參數能夠適當增大。

127.0.0.1:6379> keys *
 1) "s"
 2) "u"
 3) "d"
 4) "h"
 5) "y"
 6) "b"
 7) "q"
 8) "p"
 9) "r"
10) "i"
11) "m"
12) "z"
13) "f"
14) "w"
15) "c"
16) "n"
17) "e"
18) "a"
19) "k"
20) "v"
21) "j"
22) "x"
23) "l"
24) "o"
25) "t"
26) "g"

127.0.0.1:6379> scan 0
1) "17"
2)  1) "s"
    2) "e"
    3) "b"
    4) "q"
    5) "f"
    6) "h"
    7) "m"
    8) "t"
    9) "u"
   10) "d"
127.0.0.1:6379> scan 17
1) "7"
2)  1) "a"
    2) "v"
    3) "j"
    4) "p"
    5) "r"
    6) "l"
    7) "o"
    8) "y"
    9) "z"
   10) "x"
127.0.0.1:6379> scan 7
1) "0"
2) 1) "i"
   2) "k"
   3) "w"
   4) "c"
   5) "n"
   6) "g"

  3.數據庫管理

  • 切換數據庫:select dbIndex

  MySql這種關係型數據庫支持在一個實例下有多個數據庫存在的,可是與關係型數據庫用字符來區分不一樣數據庫名不一樣,Redis只是用數字做爲多個數據庫的實現。

  Redis默認配置中有16個數據庫,在redis.conf中有databases 16定義了默認有16個數據庫。

  當使用redis-cli -h {ip} -p {port}時默認使用的就是0號數據庫,最後一個數據庫是15號數據庫,不一樣數據庫之間的數據沒有任何關聯,甚至能夠存在相同的鍵。

127.0.0.1:6379> set hello world
OK
127.0.0.1:6379> get hello
"world"
127.0.0.1:6379> select 15
OK
127.0.0.1:6379[15]> get hello
(nil)

  爲何Redis最好是隻使用0號數據庫,不提倡使用多個數據庫:

  ①Redis是單線程的,。若是使用多個數據庫,那麼這些數據庫仍熱是使用一個CPU,彼此之間仍是會受到影響的。

  ②多數據庫的使用方式,會讓調試和運維不一樣業務的數據庫變得困難,假若有一個慢查詢存在,仍然會影響其餘數據庫,這樣會使得別的業務方定位問題很是的困難。

  ③部分Redis的客戶端根本就不支持這種方式。即便支持,在開發的時候來回切換數字形式的數據庫,很容易弄亂。

  若是要使用多個數據庫功能,徹底能夠在一臺機器上部署多個Redis實例,彼此用端口來作區分,由於現代計算機或者服務器一般是有多個CPU的,這樣既保證了業務之間不會受到影響,又合理地使用了CPU資源。

  • 清除數據庫:flushdb(清除當前數據庫)/flushall(清除全部數據庫)
127.0.0.1:6379> flushdb
OK
127.0.0.1:6379> dbsize
(integer) 0

127.0.0.1:6379> set hello world
OK
127.0.0.1:6379> select 1
OK
127.0.0.1:6379[1]> set redis jedis
OK
127.0.0.1:6379[1]> flushall
OK
127.0.0.1:6379[1]> get redis
(nil)
127.0.0.1:6379[1]> select 0
OK
127.0.0.1:6379> get hello
(nil)
相關文章
相關標籤/搜索