P109
自動補全在平常業務中隨處可見,應該算一種最多見最通用的功能。實際業務場景確定要包括包含子串的狀況,其實這在必定程度上轉換成了搜索功能,即包含某個子串的串,且優先展現前綴匹配的串。若是僅包含前綴,那麼可使用 Trie
樹,但在包含其餘的狀況下,使用數據庫/ ES
自己自帶查詢就足夠了。能夠按照四種狀況(精確匹配、前綴、後綴、包含(也可將後兩種融合成包含)),分別查詢結果,直至達到數據條數上限或者所有查詢完畢。但這種使用方法有缺點:查詢次數多、難以分頁。不過實際場景中須要補全的狀況都只要第一頁的數據便可。git
P110
需求: 記錄最近聯繫過的 100 我的名,並支持對輸入的串進行自動補全。 P110
github
數據量很小,因此能夠在 Redis
中用列表維護最近聯繫人,而後在內存中進行過濾可自動補全的串。redis
步驟: P111
數據庫
LREM
)LPUSH
)LTRIM
)P112
需求: 有不少通信錄,每一個通信錄中有幾千我的(僅包含小寫英文字母),儘可能減小 Redis
傳輸給客戶端的數據量,實現前綴自動補全。 P112
服務器
思路: 使用有序集合存儲人名,利用有序集合的特性:當成員的分值相同時,將根據成員字符串的二進制順序進行排序。若是要查找 abc
前綴的字符串,那麼實際上就是查找介於 abbz...
以後和 abd
以前的字符串。因此問題轉化爲:如何找到第一個排在 abc
以前的元素的排名 和 第一個排在 abd
以前的元素的排名。咱們能夠構造兩個不在有序集合中的字符串 (abb{
, abc{
) 輔助定位,由於 {
是排在 z
後第一個不適用的字符,這樣能夠保證這兩個字符串不存在與有序集合中,且知足轉化後的問題的限制。 P113
markdown
綜上: 經過將給定前綴的最後一個字符替換爲第一個排在該字符前的字符,再再在末尾拼接上左花括號,能夠獲得前綴的前驅 (predecessor) ,經過給前綴的末尾拼接上左花括號,能夠獲得前綴的後繼 (successor) 。併發
a~z
範圍,那麼要處理好如下三個問題: P113
UTF-8
、 UTF-16
或者 UTF-32
字符編碼(注意: UTF-16
和 UTF-32
只有大端版本可用於上述方法)`
和左花括號 {
步驟: P114
分佈式
UUID
)經過向有序集合添加元素來建立查找範圍,並在取得範圍內的元素以後移除以前添加的元素,這種技術還能夠應用在任何已排序索引 (sorted index) 上,而且能經過改善(第七章介紹)應用於幾種不一樣類型的範圍查詢,且不須要經過添加元素來建立範圍。 P115
ide
P115
分佈式鎖在業務中也很是常見,可以避免在分佈式環境中同時對同一個數據進行操做,進而能夠避免併發問題。oop
P119
// 在 conn 上獲取 key 的鎖,鎖超時時間爲 expiryTime 毫秒,等待時間最長爲 timeout 毫秒
func acquireLock(conn redis.Conn, key string, expiryTime int, timeout int) (token *int) {
// 爲了簡化,用 納秒時間戳 當 token ,實際應該用 UUID
value := int(time.Now().UnixNano())
for ; timeout >= 0; {
// 嘗試加鎖
_, err := redis.String(conn.Do("SET", key, value, "PX", expiryTime, "NX"))
// 若是獲取鎖成功,則直接返回 token 指針
if err == nil {
return &value
}
// 睡 1ms
time.Sleep(time.Millisecond)
timeout --
}
// timeout 內仍未成功獲取鎖,則獲取失敗,返回 nil
return nil
}
// 在 conn 上釋放 key 的鎖,且鎖與 token 對應
func releaseLock(conn redis.Conn, key string, token int) error {
// 用 lua 腳本保證原子性,只有 token 和值相等是才釋放
releaseLua := "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"
script := redis.NewScript(1, releaseLua)
result, err := redis.Int(script.Do(conn, key, token))
if err != nil {
return err
}
if result == 0 {
return errors.New("release failure")
}
return nil
}
複製代碼
P126
計數信號量是一種鎖,它可讓用戶限制一項資源最多能同時被多少個進程訪問,一般用於限定可以同時使用的資源數量。 P126
P126
將多個信號量的持有者的信息存儲到同一個有序集合中,即爲每一個嘗試獲取的請求生成一個 UUID
,並將這個 UUID
做爲有序集合的成員,而成員對應的分值則是嘗試獲取時的時間戳。 P127
獲取信號量步驟: P127
UUID
(時間戳 <= 當時時間戳 - 過時時間)UUID
,使用當時時間戳做爲分值,將 UUID
添加到有序集合裏面UUID
的排名
UUID
移除釋放信號量時直接從有序集合中刪除 UUID
便可。若返回值爲 1 ,則代表成功手動釋放;若返回值爲 0 ,則代表已經因爲過時而自動釋放。 P128
缺點:
UUID
A
的系統時間比 B
的系統時間快 10ms ,那麼當 A
取得了最後一個信號量的時候, B
只要在 10ms 內嘗試獲取信號量,那麼就會形成 B
獲取了不存在的信號量,致使獲取的信號量超過了信號量的總數。 P128
P128
爲了實現公平的計數信號量,即先發出獲取請求的客戶端可以獲取到信號量。咱們須要在 Redis
中維護一個自增的計數器,每次發出獲取請求前先對其自增,並使用自增後的值做爲分值將對應的 UUID
插入到另外一個有序集合中。即本來的有序集合僅用來查找並刪除過時的 UUID
,新的有序集合用來獲取排名判斷請求是否成功獲取到信號量。同時爲了保持新的有序集合及時刪過時的 UUID
,在本來的有序集合執行完刪除操做後,還要使用 ZINTERSTORE
命令,保留僅在本來有序集合中出現的 UUID
(ZINTERSTORE count_set 2 count_set time_set WEIGHTS 1 0
)。注意: 若信號量獲取失敗,則須要及時刪除本次插入的無用數據。
上述方法能在必定程度上解決信號量獲取數超過信號量總數的問題,但刪除過時 UUID
的地方仍是依賴本地時間,因此儘可能保證各個主機的系統時間差距要足夠小。 P131
去除本來的有序集合,僅留下計數器和計數值做爲分值的有序集合,並對於每一個 UUID
都設置一個有過時時間的鍵,每次移除前,遍歷有序集合,並查詢其是否過時,並從有序集合中刪除全部已過時的 UUID
。
這樣作不只能徹底達到與系統時間無關,還不會存在信號量獲取數超過信號量總數的問題,且可以實現單個獲取的信號量能有不一樣的過時時間,也必定程度上下降了時間複雜度,不過會增長客戶端與 Redis
服務器之間的交互次數。
P131
信號量使用者可能在過時時間內沒法處理完請求,此時就須要續約,延長過時時間。因爲公平的計數信號量已將時間有序集合和計數有序集合分開,因此只須要在時間有序集合中對 UUID
執行 ZADD
便可,若執行失敗,則已過時自動釋放。 P131
對於我剛剛提出的那種方法,有兩種方法能夠續約:
lua
腳本保證原子性XX
選項的 SET
命令設置新的過時時間(須要加上原有的過時時間),返回成功則續約成功,不然續約失敗P132
兩個進程 A
和 B
都在嘗試獲取剩餘的一個信號量時,即便 A
首先對計數器執行了自增操做,但只要 B
可以搶先將本身的 UUID
添加到計數有序集合中,並檢查 UUID
的排名,那麼 B
就能夠成功獲取信號量。以後 A
再將本身的 UUID
添加到有序集合裏,並檢查 UUID
排名,那麼 A
也能夠成功獲取信號量,最終致使獲取的信號量多餘信號量總數。 P132
爲了消除獲取信號量時全部可能出現的競爭條件,構建一個正確的計數信號量,咱們須要用到前面完成的帶有超時功能的分佈式鎖。在想要獲取信號量時,首先嚐試獲取分佈式鎖,若獲取鎖成功,則繼續執行獲取信號量的操做;若獲取鎖失敗,那麼獲取信號量也失敗。 P132
P133
本文首發於公衆號:滿賦諸機(點擊查看原文) 開源在 GitHub :reading-notes/redis-in-action