1. 程式人生 > >Redis高級進階(一)

Redis高級進階(一)

具體類 tro 類型 長度 刪除過期數據 專用 影響 生活 設置時間

一、redis中的事務

在關系型數據庫中事務是必不可少的一個核心功能,生活中也是處處可見,比如我們去銀行轉賬,首先需要將A賬戶的錢劃走,然後存到B賬戶上,這兩個步驟必須在同一事務中,要麽都執行,要麽都不執行,不然錢憑空消失了,換了誰也無法接受。

同樣,redis中也為我們提供了事務,原理是:先把一組同一事務中的命令發送給redis,然後redis進行依次執行。

1、事務的語法:

multi
命令1
命令2
...
exec

解釋下語法:首先通過multi命令告訴redis:“下面我發給你的命令是屬於同一事務的,你呢,先不要執行,可以把它們先存儲起來”。redis回答:“okay啦”。而後我們就發送銀行轉賬的命令1和命令2,這時redis將遵從約定不會執行命令,而是返回queued,表示把這兩條命令存儲到等待執行的事務隊列中了。最後我們發送exec命令,redis開始依次執行在等待隊列中的命令,完成一個事務的操作。

redis保證事務中所有命令要麽全執行,要麽都不執行。如果在發送exec前客戶端斷線了,redis會清空等待的事務隊列,所有命令都不會執行。而一旦發送了exec,即使在執行過程中客戶端斷線了也沒有關系,因為redis早已存儲了命令。

下面我們模擬下銀行轉賬的業務,從銀行A轉賬5000到銀行B,中間企圖修改銀行A的余額,這時看看能否轉賬成功並保證金額正確?

  • 先給銀行A 和B分別初始化10000元。
技術分享
127.0.0.1:6379> set bankA 10000
OK
127.0.0.1:6379> get bankA
"10000"
127.0.0.1:6379> set bankB 10000
OK
127.0.0.1:6379> get bankB
"10000"
技術分享
  • 進行轉賬業務操作:
技術分享
127.0.0.1:6379> multi
OK
127.0.0.1:6379> decrby bankA 5000
QUEUED
127.0.0.1:6379> incrby bankB 5000
QUEUED
127.0.0.1:6379> 
技術分享
  • 重新打開一個session,模擬修改銀行A的余額,比如取10000元,這時錢被取了,余額為0
127.0.0.1:6379> decrby bankA 10000
(integer) 0
127.0.0.1:6379> get bankA
"0"
  • 到第一個session中執行事務
127.0.0.1:6379> exec
1) (integer) -5000   #這裏假設余額可以透支,哈哈。變成﹣5000,而不是原來想的5000,如果實際業務,這時是無法進行轉賬的。事務保證了余額正確
2) (integer) 15000

2、事務錯誤處理

如果在執行一個事務時,裏面某個命令出錯了,redis怎麽處理呢?在redis中,需要分析導致命令錯誤的原因,不同的原因會有不同的處理方式。

1)語法錯誤

語法錯誤是指該命令不存在或者參數個數不正確。對於這類錯誤,redis(2.6.5之後的版本)的處理方式是直接返回錯誤,全部不執行,即使裏面有正確的命令。

技術分享
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set key value
QUEUED
127.0.0.1:6379> set key
(error) ERR wrong number of arguments for ‘set‘ command
127.0.0.1:6379> iiiget key
(error) ERR unknown command ‘iiiget‘
127.0.0.1:6379> exec
(error) EXECABORT Transaction discarded because of previous errors.
127.0.0.1:6379> get key
(nil)     #事務中有語法錯誤的命令,即使有一個命令正確也不會被執行
127.0.0.1:6379> 
技術分享

2)運行錯誤

運行錯誤是指命令在執行的時候報錯,比如用散列的命令操作集合類型的鍵。這類錯誤在運行前redis是無法發現的,故事務中如出現這樣錯誤的命令,其他正確的命令會依然被執行,即使是在錯誤命令之後的。需要小心為上,避免此類錯誤。

技術分享
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set key 1
QUEUED
127.0.0.1:6379> sadd key 2
QUEUED
127.0.0.1:6379> set key 3
QUEUED
127.0.0.1:6379> exec
1) OK
2) (error) WRONGTYPE Operation against a key holding the wrong kind of value
3) OK
127.0.0.1:6379> get key
"3"
技術分享

可見sadd key 2出錯了,但是set key 3依然被執行了。

redis中的事務不像關系型數據庫有回滾機制,為此如出現這樣的問題,開發者必須自己收拾造成這樣的爛攤子了。

為了保證盡量不出現命令和數據類型不匹配的運行錯誤,事前規劃數據庫(如保證鍵名規範)是尤為重要的。

3、watch命令

watch命令可以監控一個和多個鍵,一旦被監控鍵的值被修改,阻止之後的一個事務執行(即執行exec時返回nil,但這時watch監控也會失效),還記得上面轉賬嗎?當在轉賬事務過程中,bankA被取走了10000,余額變成0,這時操作轉賬時應該提示余額不足,無法轉賬。可以使用watch命令來阻止轉賬事務的執行。下面優化一下上面的轉賬業務:

技術分享
127.0.0.1:6379> watch bankA  #監控銀行A賬號
OK
127.0.0.1:6379> decrby bankA 10000
(integer) 0
127.0.0.1:6379> multi
OK
127.0.0.1:6379> decrby bankA 5000
QUEUED
127.0.0.1:6379> incrby bankB 5000
QUEUED
127.0.0.1:6379> exec
(nil)
127.0.0.1:6379> get bankA
"0"
127.0.0.1:6379> get bankB
"10000"
127.0.0.1:6379>
技術分享

二、生存時間

在實際開發中經常會遇到一些有時效的數據,比如限時優惠活動、緩存或驗證碼等,過一段時間需要刪除這些數據。在關系數據庫中一般需要維護一個額外的字段來存儲過期時間,然後定期檢測刪除過期數據。而在redis中命令就可以搞定。

1、命令

expire key seconds:返回1表示設置成功,0表示設置失敗或該鍵不存在;

技術分享
127.0.0.1:6379> set lifecycle ‘test life cycle‘
OK
127.0.0.1:6379> get lifecycle
"test life cycle"
127.0.0.1:6379> expire lifecycle 30   #設置生存時間為30秒,最小單位是秒
(integer) 1
127.0.0.1:6379> ttl lifecycle      #ttl查看還剩多久
(integer) 13
127.0.0.1:6379> ttl lifecycle
(integer) 11
127.0.0.1:6379> ttl lifecycle
(integer) 9
127.0.0.1:6379> ttl lifecycle
(integer) 8
127.0.0.1:6379> ttl lifecycle     #當時間到了刪除後會返回-2,不存在的鍵也返回-2
(integer) -2
技術分享

取消設置時間:persist key

除了專用的取消命令,set,getset命令也會清除key的生存時間。

pexpire key mileseconds 精確到毫秒

三、排序

在我們實際的開發中,很多地方用到排序這個功能,上節咱們說過有序集合就可以實現排序功能,是通過它分數,redis除了有序集合外還有sort命令,sort命令很復雜,能用好它不是很容易,而且一不小心就可能導致性能問題,下面就說說這些排序。

1、有序集合排序

有序集合常用的場景是大數據排序,如遊戲玩家的排行榜,一般很少需要獲得鍵中的所有數據。

2、sort命令

除了上面的有序集合,redis還提供了sort命令,它可以對列表、集合、有序集合類型鍵進行排序,並且完成如關系數據庫中關聯查詢類似的任務。

  • 對集合類型排序:
技術分享
127.0.0.1:6379> sadd set_sort 5 2 1 0 7 9
(integer) 6
#不是說集合是無序的嘛,明明上面添加時無序的,這裏打印出來怎麽排序了呢?是這樣的,集合常常用來存儲對象的id,一般都是整數,對於這種情況,進行了優化,所以這裏是排序了
127.0.0.1:6379> smembers set_sort   1) "0"
2) "1"
3) "2"
4) "5"
5) "7"
6) "9"
127.0.0.1:6379> sort set_sort desc   #desc是按降序排序,當然這裏的排序並不影響原始的數據
1) "9"
2) "7"
3) "5"
4) "2"
5) "1"
6) "0"
技術分享
  • 對列表排序:
技術分享
127.0.0.1:6379> lpush mylist 2 -1 3 44 5 
(integer) 5
127.0.0.1:6379> lrange mylist 0 -1
1) "5"
2) "44"
3) "3"
4) "-1"
5) "2"
127.0.0.1:6379> sort mylist
1) "-1"
2) "2"
3) "3"
4) "5"
5) "44"
技術分享
  • 對有序集合排序
技術分享
127.0.0.1:6379> zadd myzset 0 tom 1 helen 2 allen 3 jack
(integer) 4
127.0.0.1:6379> zrange myzset 0 -1  #這是按照分數排序取得結果
1) "tom"
2) "helen"
3) "allen"
4) "jack"
127.0.0.1:6379> sort myzset
(error) ERR One or more scores can‘t be converted into double
127.0.0.1:6379> sort myzset alpha  #通過sort按照字母字典排序後的結果,這時忽略有序集合的分數了,按照元素的字典排序。
1) "allen"
2) "helen"
3) "jack"
4) "tom"
技術分享

當然,如果結果集數據太多,需要分頁顯示,這時可以用limit參數搞定:

limit offset count  表示從第offset起,共取得count個數據
127.0.0.1:6379> sort myzset alpha desc limit 2 2 #表示從第二條取2條記錄 1) "helen" 2) "allen"

3、sort命令的參數

1)By參數

有時我們經常遇到對一個散列表中某個字段進行排序,比如寫博客時按照文章的發布時間進行排序,取最新的文章放到首頁,這個時候sort命令的by參數就可以派上用場了。

By參數的語法是:“by 參考鍵”,其中參考鍵可以是字符串類型或者散列類型鍵的某個字段(散列表示為:鍵名->字段名)如果提供了by參數,sort將不會再按照元素自身值進行排序,而是對每個元素使用元素值替換參考鍵中的第一個‘*’並獲取其值,然後依據該值進行排序。

有點繞啊,下面舉例說明:假如java類別下有4篇博客文章,可以按照下圖建立文章類別和文章的數據模型。具體類別和文章的結構圖如下:

技術分享

Tag:java:posts:表示文章的類別java,我們用集合類型表示,裏面只存儲文章的ID

Post:id:表示文章,有三個字段,我們用散列類型存儲,一個id對應一個散列

這樣模型可以這樣建立了:

技術分享
127.0.0.1:6379> hmset Post:1 title ‘java study‘ content ‘java is a good programming language‘ date ‘201509122110‘
OK
127.0.0.1:6379> hmset Post:22 title ‘java study2‘ content ‘java is a good programming language‘ date ‘201409121221‘
OK
127.0.0.1:6379> hmset Post:26 title ‘java study3‘ content ‘java is a good programming language‘ date ‘201709121221‘
OK
127.0.0.1:6379> hmset Post:60 title ‘java study4‘ content ‘java is a good programming language‘ date ‘201510221221‘
OK
127.0.0.1:6379> sadd Tag:java:posts 1 22 26 60
(integer) 4
技術分享

這個時候我想對Tag:java:posts類別下的文章按照發布日期進行排序,可以這樣來:

技術分享
127.0.0.1:6379> smembers Tag:java:posts     #排序之前直接取得的文章
1) "1"
2) "22"
3) "26"
4) "60"
127.0.0.1:6379> sort Tag:java:posts by Post:*->date desc    #按照日期排序後的結果,這裏by後面參數是散列類型的
1) "26"
2) "60"
3) "1"
4) "22"
技術分享

當然了,by後面還可以跟上字符串類型,如下:

技術分享
127.0.0.1:6379> lpush sortbylist 2 1 3
(integer) 3
127.0.0.1:6379> set item:1 50
OK
127.0.0.1:6379> set item:2 90
OK
127.0.0.1:6379> set item:3 20
OK
127.0.0.1:6379> sort sortbylist by item:* desc
1) "2"
2) "1"
3) "3"
技術分享

2)get參數

上面事例中的文章排序後,如果我想獲取文章標題,需要針對每個文章id進行hget,有沒有覺得很麻煩呢?其實sort還提供了get參數,可以很輕松的獲得文章的標題

get參數不影響排序,它的作用是返回get參數指定的鍵值。get參數和by參數一樣,也支持散列和字符串類型的鍵,並使用*作為占位符,要實現排序後直接返回文章的標題,可以這樣做:

127.0.0.1:6379> sort Tag:java:posts by Post:*->date desc get Post:*->title
1) "java study3"
2) "java study4"
3) "java study"
4) "java study2"
127.0.0.1:6379> 

在一個sort中可以有多個get參數,但是by只能有一個,get #表示返回元素本身的值。還可以這樣:

技術分享

127.0.0.1:6379> sort Tag:java:posts by Post:*->date desc get Post:*->title get Post:*->date get #
1) "java study3"
2) "201709121221"
3) "26"
4) "java study4"
5) "201510221221"
6) "60"
7) "java study"
8) "201509122110"
9) "1"
10) "java study2"
11) "201409121221"
12) "22"

技術分享

3)store參數

如果希望保存排序後的結果集,可以使用store參數,默認保存後的數據類型是列表類型,如果該鍵已存在,則覆蓋它。

技術分享
127.0.0.1:6379> sort Tag:java:posts by Post:*->date desc get Post:*->title get Post:*->date get # store sort.result
(integer) 12
127.0.0.1:6379> lrange sort.result 0 -1
 1) "java study3"
 2) "201709121221"
 3) "26"
 4) "java study4"
 5) "201510221221"
 6) "60"
 7) "java study"
 8) "201509122110"
 9) "1"
10) "java study2"
11) "201409121221"
12) "22"
127.0.0.1:6379> 
技術分享

store參數常常用來跟exprie命令實現排序結果的緩存功能,如上面提到的遊戲排行榜數據,實現的偽代碼如下

技術分享
#判斷是否存在排序結果的緩存
$isExistsCache = Exists cache.sort
if ($isExistsCache = 1)
    #如果存在緩存,直接返回值
    return lrange cache.sort 0 -1
else
    #如果不存在緩存,使用sort命令排序並將結果存入cache.sort緩存中
    $sortResult=sort some.list store cache.sort
    #設置緩存的生存時間為10分鐘
    expire cache.sort 600
    return  $sortResult
技術分享

4、性能優化

sort命令是redis中最強大復雜的命令之一,如果用不好很容易出現性能問題,sort命令的時間復雜度為:O(N+MlogM),其中N表示需要排序的元素個數,M表示返回的元素個數,當N數值很大時sort排序性能較低,並且redis在排序前會建立一個長度為N的容器來存儲排序的元素,雖然是臨時的,但是如果遇到大數據量的排序則會成為性能瓶頸。所以在開發中需要註意以下幾個方面:

  • 盡可能減少待排序鍵中的元素數量(降低N值)
  • 使用limit參數只獲得需要的值(減少M值)
  • 如果排序的數據量較大,盡可能使用store將結果緩存

Redis高級進階(一)