Redis与Java - 实践
Redis與Java - 實踐
標簽 : Java與NoSQL
Transaction
Redis事務(transaction)是一組命令的集合,同命令一樣也是Redis的最小執行單位, Redis保證一個事務內的命令執行不被其他命令影響.
`MULTI`SADD user:1:following 2SADD user:2:follower 1 `EXEC`vs. RDBMS
| 開啟 | start transaction | MULTI |
| 語句 | DML | 普通命令 |
| 取消 | rollback | DISCARD |
| 執行 | commit | EXEC |
- MySQL的rollback與Redis的DISCARD有一定的區別.
假設現在已經成功執行了事務內的前2條語句, 第3條語句出錯:
- MySQLrollback后,前2條的語句影響消失.
- Redis可以分為兩種情況:
- 語法錯誤: 事務中斷, 所有語句均得不到執行;
- 運行錯誤: (如語法正確,但適用數據類型不對: 像ZADD操作List), EXEC會執行前2條語句, 并跳過第3條語句.
這樣的部分成功會導致數據不一致, 而這一點需要由開發人員負責, 比如提前規劃好緩存key的設計.
樂觀鎖與WATCH
悲觀鎖(Pessimistic Lock): 很悲觀,每次讀寫數據都認為別人會修改,所以每次讀數據都會上鎖,這樣如果別人也想讀寫這條數據就會阻塞, 直到加鎖的人把鎖釋放. 傳統的RDBMS中用到了很多這種鎖機制, 如行鎖、表鎖、讀鎖、寫鎖等.
樂觀鎖(Optimistic Lock): 顧名思義非常樂觀, 每次讀寫數據時候都認為別人不會修改,所以不再上鎖,但在更新數據時會判斷一下在此期間有沒有人更新了這條數據, 這個判斷過程可以使用版本號等機制實現, 而Redis默認就對樂觀鎖提供了支持 –WATCH命令.
WATCH命令可以監控一個/多個key, 一旦其中有一個被修改/刪除, 則之后的事務就不會執行,如用WATCH命令來模擬搶票場景:
SET ticket 1 # 現在假設只有一張票了 `WATCH` ticket # 監控票數變化 `MULTI`DECRBY username 400DECR ticket[DECR ticket] # 現在假設有另外一個用戶直接把這張票買走了 `EXEC`-> `(nil)` # 則這條事務執行就不會成功小結
WATCH命令的作用只是當被監控的key值修改后阻止事務執行,并不能阻止其他Client修改. 所以一旦EXEC執行失敗, 可以重新執行整個方法或使用UNWATCH命令取消監控.
樂觀鎖適用于讀多寫少情景,即沖突真的很少發生,這樣可以省去大量鎖的開銷. 但如果經常產生沖突,上層應用需要不斷的retry,反倒是降低了性能,所以這種情況悲觀鎖比較適用.
Expire & Cache
Redis可以使用EXPIRE命令設置key的過期時間, 到期后Redis會自動刪除它.
| EXPIRE key seconds | Set a timeout on key. |
| TTL key | Get the time to live for a key |
| PERSIST key | Remove the expiration for a key |
除了PERSIST命令之外,SET/GETSET為key賦值的同時也會清除key的過期時間.另外如果WATCH監控了一個擁有過期時間的key,key到期自動刪除并不會被WATCH認為該key被修改.
- 緩存DB數據
為了提高網站負載能力,常需要將一些訪問頻率較高但對CPU/IO消耗較大的操作結果緩存起來,并希望讓這些緩存過期自動刪除, 下面我們就使用Redis緩存DB數據, 場景介紹可以參考:Memcached - In Action:緩存DB查詢數據.
當服務器內存有限時,如果大量使用緩存而且過期時間較長會導致Redis占滿內存; 另一方面為了防止占用內存過大而設置過期時間過短, 則有可能導致緩存命中率過低而使系統整體性能下降.因此為緩存設計一個合理的過期時間是很糾結的, 在Redis中可以限制能夠使用的最大內存,并讓Redis按照一定規則的淘汰不再需要的key: 修改maxmemory參數,當超過限制會依據maxmemory-policy參數指定的策略來刪除不需要的key:
| volatile-lru | 只對設置了過期時間的key使用LRU算法刪除 |
| allkey-lru | 使用LRU刪除一個key |
| volatile-random | 只對設置了過期時間的key隨機刪除一個key |
| allkey-random | 隨機刪除一個key |
| volatile-ttl | 刪除過期時間最近的一個key |
| noevication | 不刪除key, 只返回錯誤(默認) |
Sort
Redis的SORT命令可以對List、Set、Sorted-Set類型排序, 并且可以完成與RDBMS 連接查詢 類似的任務:
SORT key [BY pattern] [LIMIT offset count] [GET pattern [GET pattern ...]] [ASC|DESC] [ALPHA] [STORE destination]| ALPHA | SORT默認會將所有元素轉換成雙精度浮點數比較,無法轉換則會提示錯誤,而使用ALPHA參數可實現按字典序比較. |
| DESC | 降序排序(SORT默認升序排序). |
| LIMIT | 指定返回結果范圍. |
| STORE | SORT默認直接返回排序結果, STORE可將排序后結果保存為List. |
注: SORT在對Sorted-Set排序時會忽略元素分數,只針對元素自身值排序.
BY
很多情況下key實際存儲的是對象ID, 有時單純對ID自身排序意義不大,這就用到了BY參數, 對ID關聯的對象的某個屬性進行排序:
[BY pattern]pattern可以是字符串類型key或Hash類型key的某個字段(表示為鍵名 -> 字段名).如果提供了BY參數, SORT將使用ID值替換參考key中的第一個*并獲取其值,然后根據該值對元素排序.
SORT mi.blog:1:my BY mi.blog:*:data->time DESC- 注意:
- 當pattern不包含*時, SORT將不會執行排序操作;
- 當ID元素的參考key不存在時,默認設置為0;
- 如果幾個ID元素的pattern值相同,則會再比較元素本身值排序.
GET
GET參數不影響排序過程,它的作用是使SORT返回結果不再是元素自身的值,而是GET參數指定的鍵值:
[GET pattern [GET pattern ...]]同BY一樣, GET參數也支持String類型和Hash類型, 并使用*作為占位符.
SORT mi.blog:1:my BY mi.blog:*:data->time GET mi.blog:*:data->content GET mi.blog:*:data->time注: GET參數獲取自身值需要使用#: GET #
性能
SORT的時間復雜度為O(N+M*log(M)):
where N is the number of elements in the list or set to sort, and M the number of returned elements. When the elements are not sorted, complexity is currently O(N) as there is a copy step that will be avoided in next releases.- 所以開發過程中使用SORT需要注意:
- 盡可能減小待排序key中元素數量(減小N);
- 使用LIMIT參數限制結果集大小(減小M);
- 如果待排序數據量較大,盡可能使用STORE將結果緩存.
Message
1. 消息隊列
消息隊列就是”傳遞消息的隊列”,與消息隊列進行交互的實體有兩類, 一是生產者: 將需要處理的消息放入隊列; 一是消費者: 不斷從消息隊列中讀出消息并處理.
使用消息隊列有如下好處:Redis提供了BRPOP/BLPOP命令來實現消息隊列:
| BRPOP key [key ...] timeout | Remove and get the last element in a list, or block until one is available |
| BLPOP key [key ...] timeout | Remove and get the first element in a list, or block until one is available |
| BRPOPLPUSH source destination timeout | Pop a value from a list, push it to another list and return it; or block until one is available |
注: 若Redis同時監聽多個key, 且每個key均有元素可取,則Redis按照從左到右的順序去挨個讀取key的第一個元素.
2. 消息訂閱
前面的BRPOP/BLPOP實現的消息隊列有一個限制: 如果一個隊列被多個消費者監聽, 生產者發布一條消息只會被其中一個消費者獲取. 因此Redis還提供了一組命令實現“發布/訂閱”模式, 同樣可用于進程間通信:
“發布/訂閱”模式也包含兩種角色: 發布者與訂閱者. 訂閱者可以訂閱一個/多個頻道, 而發布者可向指定頻道發送消息, 所有訂閱此頻道的訂閱者都會收到此消息.
| PUBLISH channel message | Post a message to a channel |
| SUBSCRIBE channel [channel ...] | Listen for messages published to the given channels |
| UNSUBSCRIBE [channel [channel ...]] | Stop listening for messages posted to the given channels |
| PSUBSCRIBE pattern [pattern ...] | Listen for messages published to channels matching the given patterns |
| PUNSUBSCRIBE [pattern [pattern ...]] | Stop listening for messages posted to channels matching the given patterns |
- MessagesQueue
注: 發送的消息不會持久化,一個訂閱者只能接收到后續發布的消息,之前發送的消息就接收不到了.
持久化
Redis支持兩種持久化方式: RDB與AOF. RDB: Redis根據指定的規則“定時”將內存數據快照到硬盤; AOF:Redis在每次執行命令后將命令本身記錄下來存放到硬盤.兩種持久化方式可結合使用.
RDB
- 快照執行過程:
- Redis使用fork()函數復制一份當前進程副本;
- 父進程繼續接收并處理客戶端請求, 而子進程將所有內存數據寫入磁盤臨時文件;
- 當子進程將所有數據寫完會用該臨時文件替換舊的RDB文件, 至此一次快照完成(可以看到自始至終RDB文件都是完整的).
Redis會在以下幾種情況下對數據進行快照:
- 根據配置規則
配置由兩個參數構成: 時間窗口M和改動key個數N; 當時間M內被改動的key的個數大于N時, 即符合自動快照條件:
- 用戶執行SAVE/BGSAVE/FLUSHALL命令:
除了讓Redis自動快照, 當進行服務重啟/手動遷移以及備份時也需要我們手動執行快照.
| SAVE | SAVE命令會使Redis同步地執行快照操作(過程中會阻塞所有來自客戶端的請求, 因此盡量避免線上使用) |
| BGSAVE | 在后臺異步執行快照操作,Redis還可繼續響應請求 |
| FLUSHALL | FLUSHALL會清空所有數據,無論是否觸發了自動快照條件(只要有配置了),Redis都會執行一次快照 |
| LASTSAVE | 獲取最近一次成功執行快照時間 |
- 執行復制
當設置了主從模式, Redis會在復制初始化時執行快照,即使沒有配置自動快照條件.
通過RDB方式實現持久化, Redis在啟動后會讀取RDB快照文件, 將數據從硬盤導入內存, 但如果在持久化過程中Redis異常退出, 就會丟失最后一次快照以后更改的所有數據.
RDB其他配置參數
dir ./ # 設置工作目錄,RDB文件(以及后面的AOF文件)會寫入該目錄 dbfilename dump.rdb # 設置RDB文件名 rdbcompression yes # 導出RDB是否壓縮 rdbchecksum yes # 存儲和加載RDB校驗完整性 stop-writes-on-bgsave-error yes # 后臺備份進程出錯時,主進程停止寫入.AOF
AOF將Redis執行的每一條命令追加到硬盤文件中.然后在啟動Redis時逐條執行AOF文件中的命令將數據載入內存.
Redis默認沒有開啟AOF, 需要以如下參數啟用:
appendonly yes no-appendfsync-on-rewrite yes: # 正在導出RDB快照的過程中,停止同步AOF.AOF重寫
開啟AOF后, Redis會將每一條有可能更改數據的命令寫入AOF文件,這樣就導致AOF文件越來越大,即使有可能內存中實際存儲的數據并沒多少. 因此Redis每當達到一定條件就自動重寫AOF文件,這個條件可以在配置文件中設置:
auto-aof-rewrite-percentage 100 # 比起上次重寫時的大小,AOF增長率100%時重寫 auto-aof-rewrite-min-size 64mb # AOF大小超過64M時重寫此外, 我們還可以使用BGREWRITEAOF命令手動執行AOF重寫.
硬盤數據同步
執行AOF持久化時, 由于操作系統緩存機制, 數據并沒有真正寫入磁盤,而是進入了磁盤緩存, 默認情況下系統每30S執行一次同步操作, 將緩存內容真正寫入磁盤, 如果在這30S的系統異常退出則會導致磁盤緩存數據丟失, 如果應用無法忍受這樣的損失, 可通過appendfsync參數設置同步機制:
# appendfsync always # 每次執行寫入都執行同步 appendfsync everyse # 每秒執行一次同步操作 # appendfsync no # 不主動進行同步, 而是完全由操作系統執行.集群
1. Replication
復制(replication)中,Redis的角色可以分為兩類, Master:可以執行讀/寫操作,當寫操作導致數據修改時會自動將數據同步給Slave; Slave:一般是只讀的,并接受Master同步過來的數據(Slave自身也可以作為Master存在, 如圖):
- replication復制時序
- Slave啟動后向Master發送SYNC命令;Master收到后在后臺保存RDB快照, 并將快照期間接收到的所有命令緩存.
- 快照執行完, Master將快照文件與所有緩存的命令發送給Slave;
- Slave接收并載入快照, 然后執行所有收到的緩存命令,這一過程稱為復制初始化.
- 復制初始化完成后,Master每接收到寫命令就同步給Slave,從而保證主從數據一致.
- 通過Redis的復制功能可以實現以下應用:
- 讀寫分離:
通過復制可實現讀寫分離, 以提高服務器的負載能力, 可以通過復制建立多個Slave節點, Master只進行寫操作, 而由Slave負責讀操作, 這種一主多從的結構很適合讀多寫少的場景. - Slave持久化
持久化是一個相對耗時的操作, 為了提高性能, 可以通過復制功能建立一個/多個Slave, 并在Salve中啟用持久化, Master禁用持久化. 當Master崩潰后:
- 在Slave使用SLAVEOF NO ONE命令將Slave提升成Master繼續服務;
- 啟用之前崩潰的Master, 然后使用SLAVEOF將其設置為新Master的Slave, 即可將數據同步回來.
- 讀寫分離:
注意: 當開啟復制且Master關閉持久化時, Master崩潰后一定不能直接重啟Master, 這是因為當Master重啟后, 因為沒有開啟持久化, 所以Redis內的所有數據都會被清空, 這時Salve從Master接受數據, 所有的Slave也會被清空, 導致Slave持久化失去意義.
關于Redis復制的詳細介紹以及配置方式可參考博客:Redis研究 -主從復制.
2. Sentinel
當Master遭遇異常中斷服務后, 需要手動選擇一個Slave升級為Master, 以使系統能夠繼續提供服務. 然而整個過程相對麻煩且需要人工介入, 難以實現自動化. 為此Redis提供了哨兵Sentinel.
Sentinel哨兵是Redis高可用性解決方案之一: 由一個/多個Sentinel實例組成的Sentinel系統可以監視任意多個Master以及下屬Slave, 并在監控到Master進入下線狀態時, 自動將其某個Slave提升為新的Master, 然后由新的Master代替已下線的Master繼續處理命令請求.
- 如圖: 若此時Master:server1進入下線狀態, 那么Slave: server2,server3,server4對Master的復制將被迫中止,并且Sentinel系統也會察覺到server1已下線, 當下線時長超過用戶設定的下線時長時, Sentinel系統就會對server1執行故障轉移操作:
- Sentinel會挑選server1下屬的其中一臺Slave, 將其提升為新Master;
- 然后Sentinel向server1下屬的所有Slave發送新的復制指令,讓他們成為新Master的Salve, 當所有Salve都開始復制新Master時, 故障轉移操作完成.
- 另外, Sentinel還會繼續監視已下線的server1, 并在他重新上線時, 將其設置為新Master的Slave.
關于Redis哨兵的詳細介紹以及配置方式可參考博客:Redis Sentinel(哨兵):集群解決方案.
3. Cluster
Cluster是Redis提供的另一高可用性解決方案:Redis集群通過分片(sharding)來進行數據共享, 并提供復制與故障轉移功能.
一個 Redis 集群通常由多個節點組成, 最初每個節點都是相互獨立的,要組建一個真正可工作的集群, 必須將各個獨立的節點連接起來.連接各個節點的工作可以使用CLUSTER MEET命令完成:
CLUSTER MEET <ip> <port>向一個節點發送CLUSTER MEET命令,可以使其與ip+port所指定的節點進行握手,當握手成功時, 就會將目標節點添加到當前節點所在的集群中.
- 案例
假設現在有三個獨立的節點 127.0.0.1:7000 、 127.0.0.1:7001 、 127.0.0.1:7002:
- 通過向節點 7000 發送CLUSTER MEET 127.0.0.1 7001命令,可將節點7001添加到節點7000所在的集群中:
- 繼續向節點7000發送CLUSTER MEET 127.0.0.1 7002命令,同樣也可將節點7002也拉進來:
- 至此, 握手成功的三個節點處于同一個集群:
- 通過向節點 7000 發送CLUSTER MEET 127.0.0.1 7001命令,可將節點7001添加到節點7000所在的集群中:
關于Redis-Cluster的詳細介紹以及更多配置方式可參考博客:redis-cluster研究和使用.
管理
1. 數據庫密碼
通過在配置文件中使用requirepass參數可為Redis設置密碼:
requirepass ?∑′??¥¨??π這樣客戶端每次連接都需要發送密碼,否則Redis拒絕執行客戶端命令:
AUTH ?∑′??¥¨??π2. 重命名
Redis支持在配置文件中將命令重命名, 以保證只有自己的應用可以使用該命令:
rename-command FLUSHALL qwertyuiop如果希望禁用某個命令,可將命令重命名為空字符串.
3. 工具
- SLOWLOG
當一條命令執行超過時間限制時,Redis會將其執行時間等信息加入耗時統計日志, 超時時間等可通過以下配置實現:
MONITOR : 監控Redis執行的所有命令
注意: MONITOR命令非常影響Redis性能, 一個客戶端使用MONITOR會降低Redis將近一半的負載能力. Instagram團隊開發了一個基于MONITOR命令的Redis查詢分析工具redis-faina, 可根據MONITOR的監控結果分析出最常用的命令/訪問最頻繁的key等信息, 詳細可參考博客:關于 Redis 的性能分析工具 Redis Faina.
其他常用管理工具
參考&拓展
總結
以上是生活随笔為你收集整理的Redis与Java - 实践的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Linux IPC实践(3) --具名F
- 下一篇: 简洁的Java8