标簽 : Java與NoSQL
Redis事務(<code>transaction</code>)是一組指令的集合,同指令一樣也是Redis的最小執行機關, Redis保證一個事務内的指令執行不被其他指令影響.
事務操作
MySQL
Redis
開啟
<code>start transaction</code>
<code>MULTI</code>
語句
DML
普通指令
取消
<code>rollback</code>
<code>DISCARD</code>
執行
<code>commit</code>
<code>EXEC</code>
MySQL的<code>rollback</code>與Redis的<code>DISCARD</code>有一定的差別.
假設現在已經成功執行了事務内的前2條語句, 第3條語句出錯:
MySQL<code>rollback</code>後,前2條的語句影響消失.
Redis可以分為兩種情況:
文法錯誤: 事務中斷, 所有語句均得不到執行;
運作錯誤: (如文法正确,但适用資料類型不對: 像<code>ZADD</code>操作<code>List</code>), <code>EXEC</code>會執行前2條語句, 并跳過第3條語句.
這樣的部分成功會導緻資料不一緻, 而這一點需要由開發人員負責, 比如提前規劃好緩存<code>key</code>的設計.
悲觀鎖(Pessimistic Lock): 很悲觀,每次讀寫資料都認為别人會修改,是以每次讀資料都會上鎖,這樣如果别人也想讀寫這條資料就會阻塞, 直到加鎖的人把鎖釋放. 傳統的RDBMS中用到了很多這種鎖機制, 如行鎖、表鎖、讀鎖、寫鎖等. 樂觀鎖(Optimistic Lock): 顧名思義非常樂觀, 每次讀寫資料時候都認為别人不會修改,是以不再上鎖,但在更新資料時會判斷一下在此期間有沒有人更新了這條資料, 這個判斷過程可以使用<code>版本号</code>等機制實作, 而Redis預設就對樂觀鎖提供了支援 –<code>WATCH</code>指令.
<code>WATCH</code>指令可以監控一個/多個<code>key</code>, 一旦其中有一個被修改/删除, 則之後的事務就不會執行,如用<code>WATCH</code>指令來模拟搶票場景:
<code>WATCH</code>指令的作用隻是當被監控的<code>key</code>值修改後阻止事務執行,并不能阻止其他Client修改. 是以一旦<code>EXEC</code>執行失敗, 可以重新執行整個方法或使用<code>UNWATCH</code>指令取消監控.
樂觀鎖适用于讀多寫少情景,即沖突真的很少發生,這樣可以省去大量鎖的開銷. 但如果經常産生沖突,上層應用需要不斷的retry,反倒是降低了性能,是以這種情況悲觀鎖比較适用.
Redis可以使用<code>EXPIRE</code>指令設定<code>key</code>的過期時間, 到期後Redis會自動删除它.
指令
作用
<code>EXPIRE key seconds</code>
Set a timeout on key.
<code>TTL key</code>
Get the time to live for a key
<code>PERSIST key</code>
Remove the expiration for a key
除了<code>PERSIST</code>指令之外,<code>SET</code>/<code>GETSET</code>為<code>key</code>指派的同時也會清除<code>key</code>的過期時間.另外如果<code>WATCH</code>監控了一個擁有過期時間的<code>key</code>,<code>key</code>到期自動删除并不會被<code>WATCH</code>認為該<code>key</code>被修改.
緩存DB資料
當伺服器記憶體有限時,如果大量使用緩存而且過期時間較長會導緻Redis占滿記憶體; 另一方面為了防止占用記憶體過大而設定過期時間過短, 則有可能導緻緩存命中率過低而使系統整體性能下降.是以為緩存設計一個合理的過期時間是很糾結的, 在Redis中可以限制能夠使用的最大記憶體,并讓Redis按照一定規則的淘汰不再需要的<code>key</code>: 修改<code>maxmemory</code>參數,當超過限制會依據<code>maxmemory-policy</code>參數指定的政策來删除不需要的<code>key</code>:
<code>maxmemory-policy</code>
規則說明
<code>volatile-lru</code>
隻對設定了過期時間的key使用LRU算法删除
<code>allkey-lru</code>
使用LRU删除一個key
<code>volatile-random</code>
隻對設定了過期時間的key随機删除一個key
<code>allkey-random</code>
随機删除一個key
<code>volatile-ttl</code>
删除過期時間最近的一個key
<code>noevication</code>
不删除key, 隻傳回錯誤(預設)
Redis的<code>SORT</code>指令可以對<code>List</code>、<code>Set</code>、<code>Sorted-Set</code>類型排序, 并且可以完成與RDBMS 連接配接查詢 類似的任務:
參數
描述
<code>ALPHA</code>
<code>SORT</code>預設會将所有元素轉換成雙精度浮點數比較,無法轉換則會提示錯誤,而使用<code>ALPHA</code>參數可實作按字典序比較.
<code>DESC</code>
降序排序(<code>SORT</code>預設升序排序).
<code>LIMIT</code>
指定傳回結果範圍.
<code>STORE</code>
<code>SORT</code>預設直接傳回排序結果, <code>STORE</code>可将排序後結果儲存為<code>List</code>.
注: <code>SORT</code>在對<code>Sorted-Set</code>排序時會忽略元素分數,隻針對元素自身值排序.
很多情況下<code>key</code>實際存儲的是對象ID, 有時單純對ID自身排序意義不大,這就用到了<code>BY</code>參數, 對ID關聯的對象的某個屬性進行排序:
<code>pattern</code>可以是字元串類型<code>key</code>或<code>Hash</code>類型<code>key</code>的某個字段(表示為鍵名 -> 字段名).如果提供了<code>BY</code>參數, <code>SORT</code>将使用ID值替換參考<code>key</code>中的第一個<code>*</code>并擷取其值,然後根據該值對元素排序.
注意:
當<code>pattern</code>不包含<code>*</code>時, <code>SORT</code>将不會執行排序操作;
當ID元素的參考<code>key</code>不存在時,預設設定為0;
如果幾個ID元素的<code>pattern</code>值相同,則會再比較元素本身值排序.
<code>GET</code>參數不影響排序過程,它的作用是使<code>SORT</code>傳回結果不再是元素自身的值,而是<code>GET</code>參數指定的鍵值:
同<code>BY</code>一樣, <code>GET</code>參數也支援<code>String</code>類型和<code>Hash</code>類型, 并使用<code>*</code>作為占位符.
注: <code>GET</code>參數擷取自身值需要使用<code>#</code>: <code>GET #</code>
<code>SORT</code>的時間複雜度為<code>O(N+M*log(M))</code>:
是以開發過程中使用<code>SORT</code>需要注意:
盡可能減小待排序<code>key</code>中元素數量(減小<code>N</code>);
使用<code>LIMIT</code>參數限制結果集大小(減小<code>M</code>);
如果待排序資料量較大,盡可能使用<code>STORE</code>将結果緩存.
消息隊列就是”傳遞消息的隊列”,與消息隊列進行互動的實體有兩類, 一是生産者: 将需要處理的消息放入隊列; 一是消費者: 不斷從消息隊列中讀出消息并處理.
<dl></dl>
<dt>使用消息隊列有如下好處:</dt>
<dd></dd>
松耦合: 生産者和消費者無需知道彼此的實作細節, 隻需按照協商好的消息格式讀/寫, 即可實作不同程序間通信,這就使得生産者和消費者可以由不同的團隊使用不同的開發語言編寫.
易擴充: 消費者可以有多個,且可以分布在不同的Server中, 降低單台Server負載, 橫向擴充業務.
Redis提供了<code>BRPOP</code>/<code>BLPOP</code>指令來實作消息隊列:
<code>BRPOP key [key ...] timeout</code>
Remove and get the last element in a list, or block until one is available
<code>BLPOP key [key ...] timeout</code>
Remove and get the first element in a list, or block until one is available
<code>BRPOPLPUSH source destination timeout</code>
Pop a value from a list, push it to another list and return it; or block until one is available
注: 若Redis同時監聽多個<code>key</code>, 且每個<code>key</code>均有元素可取,則Redis按照從左到右的順序去挨個讀取<code>key</code>的第一個元素.
前面的<code>BRPOP</code>/<code>BLPOP</code>實作的消息隊列有一個限制: 如果一個隊列被多個消費者監聽, 生産者釋出一條消息隻會被其中一個消費者擷取. 是以Redis還提供了一組指令實作“釋出/訂閱”模式, 同樣可用于程序間通信:
“釋出/訂閱”模式也包含兩種角色: 釋出者與訂閱者. 訂閱者可以訂閱一個/多個頻道, 而釋出者可向指定頻道發送消息, 所有訂閱此頻道的訂閱者都會收到此消息.
<code>PUBLISH channel message</code>
Post a message to a channel
<code>SUBSCRIBE channel [channel ...]</code>
Listen for messages published to the given channels
<code>UNSUBSCRIBE [channel [channel ...]]</code>
Stop listening for messages posted to the given channels
<code>PSUBSCRIBE pattern [pattern ...]</code>
Listen for messages published to channels matching the given patterns
<code>PUNSUBSCRIBE [pattern [pattern ...]]</code>
Stop listening for messages posted to channels matching the given patterns
MessagesQueue
注: 發送的消息不會持久化,一個訂閱者隻能接收到後續釋出的消息,之前發送的消息就接收不到了.
Redis支援兩種持久化方式: RDB與AOF. RDB: Redis根據指定的規則“定時”将記憶體資料快照到硬碟; AOF:Redis在每次執行指令後将指令本身記錄下來存放到硬碟.兩種持久化方式可結合使用.
快照執行過程: Redis使用<code>fork()</code>函數複制一份目前程序副本; 父程序繼續接收并處理用戶端請求, 而子程序将所有記憶體資料寫入磁盤臨時檔案; 當子程序将所有資料寫完會用該臨時檔案替換舊的RDB檔案, 至此一次快照完成(可以看到自始至終RDB檔案都是完整的).
Redis會在以下幾種情況下對資料進行快照:
根據配置規則
配置由兩個參數構成: 時間視窗<code>M</code>和改動<code>key</code>個數<code>N</code>; 當時間<code>M</code>内被改動的<code>key</code>的個數大于<code>N</code>時, 即符合自動快照條件:
使用者執行<code>SAVE</code>/<code>BGSAVE</code>/<code>FLUSHALL</code>指令:
除了讓Redis自動快照, 當進行服務重新開機/手動遷移以及備份時也需要我們手動執行快照.
<code>SAVE</code>
<code>SAVE</code>指令會使Redis同步地執行快照操作(過程中會阻塞所有來自用戶端的請求, 是以盡量避免線上使用)
<code>BGSAVE</code>
在背景異步執行快照操作,Redis還可繼續響應請求
<code>FLUSHALL</code>
<code>FLUSHALL</code>會清空所有資料,無論是否觸發了自動快照條件(隻要有配置了),Redis都會執行一次快照
<code>LASTSAVE</code>
擷取最近一次成功執行快照時間
執行複制
當設定了主從模式, Redis會在複制初始化時執行快照,即使沒有配置自動快照條件.
通過RDB方式實作持久化, Redis在啟動後會讀取RDB快照檔案, 将資料從硬碟導入記憶體, 但如果在持久化過程中Redis異常退出, 就會丢失最後一次快照以後更改的所有資料.
AOF将Redis執行的每一條指令追加到硬碟檔案中.然後在啟動Redis時逐條執行AOF檔案中的指令将資料載入記憶體.
Redis預設沒有開啟AOF, 需要以如下參數啟用:
開啟AOF後, Redis會将每一條有可能更改資料的指令寫入AOF檔案,這樣就導緻AOF檔案越來越大,即使有可能記憶體中實際存儲的資料并沒多少. 是以Redis每當達到一定條件就自動重寫AOF檔案,這個條件可以在配置檔案中設定:
此外, 我們還可以使用<code>BGREWRITEAOF</code>指令手動執行AOF重寫.
執行AOF持久化時, 由于作業系統緩存機制, 資料并沒有真正寫入磁盤,而是進入了磁盤緩存, 預設情況下系統每30S執行一次同步操作, 将緩存内容真正寫入磁盤, 如果在這30S的系統異常退出則會導緻磁盤緩存資料丢失, 如果應用無法忍受這樣的損失, 可通過<code>appendfsync</code>參數設定同步機制:
複制(replication)中,Redis的角色可以分為兩類, Master:可以執行讀/寫操作,當寫操作導緻資料修改時會自動将資料同步給Slave; Slave:一般是隻讀的,并接受Master同步過來的資料(Slave自身也可以作為Master存在, 如圖):
replication複制時序
Slave啟動後向Master發送<code>SYNC</code>指令;Master收到後在背景儲存RDB快照, 并将快照期間接收到的所有指令緩存.
快照執行完, Master将快照檔案與所有緩存的指令發送給Slave;
Slave接收并載入快照, 然後執行所有收到的緩存指令,這一過程稱為複制初始化.
複制初始化完成後,Master每接收到寫指令就同步給Slave,進而保證主從資料一緻.
通過Redis的複制功能可以實作以下應用:
讀寫分離:
通過複制可實作讀寫分離, 以提高伺服器的負載能力, 可以通過複制建立多個Slave節點, Master隻進行寫操作, 而由Slave負責讀操作, 這種一主多從的結構很适合讀多寫少的場景.
Slave持久化
持久化是一個相對耗時的操作, 為了提高性能, 可以通過複制功能建立一個/多個Slave, 并在Salve中啟用持久化, Master禁用持久化. 當Master崩潰後:
在Slave使用<code>SLAVEOF NO ONE</code>指令将Slave提升成Master繼續服務;
啟用之前崩潰的Master, 然後使用<code>SLAVEOF</code>将其設定為新Master的Slave, 即可将資料同步回來.
注意: 當開啟複制且Master關閉持久化時, Master崩潰後一定不能直接重新開機Master, 這是因為當Master重新開機後, 因為沒有開啟持久化, 是以Redis内的所有資料都會被清空, 這時Salve從Master接受資料, 所有的Slave也會被清空, 導緻Slave持久化失去意義.
當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.
Cluster是Redis提供的另一高可用性解決方案:Redis叢集通過分片(sharding)來進行資料共享, 并提供複制與故障轉移功能.
一個 Redis 叢集通常由多個節點組成, 最初每個節點都是互相獨立的,要組建一個真正可工作的叢集, 必須将各個獨立的節點連接配接起來.連接配接各個節點的工作可以使用<code>CLUSTER MEET</code>指令完成:
向一個節點發送<code>CLUSTER MEET</code>指令,可以使其與<code>ip</code>+<code>port</code>所指定的節點進行握手,當握手成功時, 就會将目标節點添加到目前節點所在的叢集中.
案例
假設現在有三個獨立的節點 127.0.0.1:7000 、 127.0.0.1:7001 、 127.0.0.1:7002:
通過向節點 7000 發送<code>CLUSTER MEET 127.0.0.1 7001</code>指令,可将節點7001添加到節點7000所在的叢集中:
繼續向節點7000發送<code>CLUSTER MEET 127.0.0.1 7002</code>指令,同樣也可将節點7002也拉進來:
至此, 握手成功的三個節點處于同一個叢集:
通過在配置檔案中使用<code>requirepass</code>參數可為Redis設定密碼:
這樣用戶端每次連接配接都需要發送密碼,否則Redis拒絕執行用戶端指令:
Redis支援在配置檔案中将指令重命名, 以保證隻有自己的應用可以使用該指令:
如果希望禁用某個指令,可将指令重命名為空字元串.
<code>SLOWLOG</code>
當一條指令執行超過時間限制時,Redis會将其執行時間等資訊加入耗時統計日志, 逾時時間等可通過以下配置實作:
<code>MONITOR</code> : 監控Redis執行的所有指令
其他常用管理工具
<dt>參考&拓展</dt>