1. 程式人生 > >分散式架構-Redis 從入門到精通 完整案例 附原始碼

分散式架構-Redis 從入門到精通 完整案例 附原始碼

導讀

  篇幅較長,乾貨十足,閱讀需要花點時間,全部手打出來的字,難免出現錯別字,敬請諒解。珍惜原創,轉載請註明出處,謝謝~!

NoSql介紹與Redis介紹

什麼是Redis?

  Redis是用C語言開發的一個開源的高效能鍵值對(key-value)記憶體資料庫。

  它提供五種資料型別來儲存值:字串型別、雜湊型別、列表型別、集合型別、有序型別。

  它是一種NoSql資料庫。

什麼是NoSql?

  • NoSql,即Not-Only Sql(不僅僅是SQL),泛指非關係型的資料庫。
  • 什麼是關係型資料庫?資料結構是一種有行有列的資料庫。
  • NoSql資料庫是為了解決高併發、高可用、高可擴充套件、大資料儲存問題而產生的資料庫解決方案。
  • NoSql可以作為關係型資料庫的良好補充,但是不能替代關係型資料庫。

NoSql資料庫分類

鍵值(key-value)儲存資料庫

  • 相關產品:Tokyo Cabinet/Tyrant、Redis、Voldemort、Berkeley Db等
  • 典型應用:記憶體快取,主要用於處理大量資料的高訪問負載
  • 資料模型:一系列鍵值對
  • 優勢:快速查詢
  • 劣勢:儲存的資料缺少結構化

列儲存資料庫

  • 相關產品:Cassandra、Hbase、Riak
  • 典型應用:分散式的檔案系統
  • 資料模型:以列簇式儲存,將同一列資料存在一起
  • 優勢:查詢速度快,可擴充套件性強,更容易進行分散式擴充套件
  • 劣勢:功能相對侷限

文件型資料庫

  • 相關產品:CouchDB、MongoDB
  • 典型應用:web應用(與key-value類似,value是結構化的)
  • 資料模型:一系列鍵值對
  • 優勢:資料結構要求不嚴格
  • 劣勢

圖形(Graph)資料庫

  • 相關資料庫:Neo4J、InfoGrid、Infinite、Graph
  • 典型應用:社交網路
  • 資料模型:圖結構
  • 優勢:利用圖結構先關演算法
  • 劣勢:需要對整個圖做計算才能得出結果,不容易做分散式的叢集方案。

Redis歷史發展

  2008年,義大利的一家創業公司Merzia推出了一款給予MySql的網站實時統計系統LLOOGG,然而沒過多久該公司的創始人Salvatore Sanfilippo便對MySql的效能感到失望,於是他決定親力為LLOOGG量身定做一個數據庫,並於2009年開發完成,這個資料庫就是Redis。

  不過Salvatore Sanfilippo並不滿足只將Redis用於LLOOGG這一款產品,而是希望更多的人使用它,於是在同一年Salvatore Sanfilippo將Redis開源釋出。

  並開始和Redis的另一名主要的程式碼貢獻者Pieter Noordhuis一起繼續著Redis的開發,直到今天。

  Salvatore Sanfilippo自己也沒有想到,短短的幾年時間,Redis就擁有了龐大的使用者群體。Hacker News在2012年釋出一份資料庫的使用請款調查,結果顯示有近12%的公司在使用Redis。國內如新浪微博、街旁網、知乎網、國外如GitHub、Stack、Overflow、Flickr等都是Redis的使用者。

  VmWare公司從2010年開始贊助Redis的開發,Salvatore Sanfilippo和Pieter Noordhuis也分別在3月和5月加入VMware,全職開發Redis。

Redis的應用場景

  • 記憶體資料庫(登入資訊、購物車資訊、使用者瀏覽記錄等)
  • 快取伺服器(商品資料、廣告資料等等)(最多使用)
  • 解決分散式叢集架構中的Session分離問題(Session共享)
  • 任務佇列。(秒殺、搶購、12306等等)
  • 支援釋出訂閱的訊息模式
  • 應用排行榜
  • 網站訪問統計
  • 資料過期處理(可以精確到毫秒)

Redis安裝及配置

  • 官網地址:https://redis.io/
  • 中文官網地址:http://www.redis.cn
  • 下載地址:http://download.redis.io/releases/

Linux環境下安裝Redis

注:將下載後的Redis拖進Linux需要安裝下,VMware Tools,參考連結

將下載後的Redis拖進linux

安裝C語言需要的GCC環境

yum install gcc-c++

解壓Redis原始碼壓縮包

tar -zxf redis-4.0.11.tar.gz

編譯Redis原始碼

make

安裝Redis

make install PREFIX=/user/local/redis

格式:make install PREFIX=安裝目錄

Redis啟動

前端啟動

  • 啟動命令:redis-server,直接執行bin/redis-server將以前端模式啟動。

關閉服務

ctrl+c

啟動缺點:客戶端視窗關閉,則redis-server程式結束,不推薦使用

後端啟動(守護程序啟動)

拷貝redis

cp redis.conf /usr/local/redis/bin

格式:cp 拷貝資料夾 拷貝路徑

 修改redis.conf,將daemonize由no改為yes

vim redis.conf

 執行命令

 ./redis-server redis.conf

格式:啟動服務 指定配置檔案

 關閉服務(粗暴方式)

kill -9 42126

格式:kill -9 程序號

 正常關閉

./redis-cli shutdown

其他命令說明

redis-server :啟動redis服務
redis-cli :進入redis命令客戶端
redis-benchmark: 效能測試的工具
redis-check-aof : aof檔案進行檢查的工具
redis-check-dump :  rdb檔案進行檢查的工具
redis-sentinel :  啟動哨兵監控服務

Redis客戶端

自帶命令列客戶端

語法

./redis-cli -h 127.0.0.1 -p 6379 

修改redis.conf配置檔案(解決ip繫結問題)

#bind 127.0.0.1 繫結的ip才能訪問redis伺服器,註釋掉該配置

protected-mode yes 是否開啟保護模式,由yes改為no

引數說明

  • -h:redis伺服器的ip地址
  • -p:redis例項的埠號

預設方式

如果不制定主機和埠號也可以

./redis-cli

預設的主機地址是:127.0.0.1
預設的埠號是:6379

Redis資料型別

官網命令大全網址

http://www.redis.cn/commands.html

  • String(字元型別)
  • Hash(雜湊型別)
  • List(列表型別)
  • Set(集合型別)
  • SortedSet(有序集合型別,簡稱zset)

注:命令不區分大小寫,而key是區分大小寫的。

String型別

賦值

語法:SET key value

取值

語法:GET key

取值並賦值

語法:GETSET key value

演示

 數值增減

前提條件:

  1. 當value為整數資料時,才能使用以下命令運算元值的增減。
  2. 數值增減都是原子操作。

遞增數字

語法:INCR key

 增加指定的整數

語法:INCRBY key increment

 遞減數值

語法:DECR key

減少指定的整數 

語法:DECRBY key decrement

 僅當不存在時賦值

注:該命令可以實現分散式鎖的功能,後續講解!!!!

語法:setnx key value

向尾部追加值

注:APPEND命令,向鍵值的末尾追加value。如果鍵不存在則該鍵的值設定為value,即相當於set key value。返回值是追加後字串的總長度。

 獲取字串長度

注:strlen命令,返回鍵值的長度,如果鍵不存在則返回0

 語法:STRLEN key

同時設定/獲取多個鍵值

語法:

  1. MSET key value [key value ....]
  2. MGET key [key ....]

 應用場景之自增主鍵

需求:商品編號、訂單號採用INCR命令生成。

設計:key明明要有一定的設計

實現:定義商品編號key:items:id

 Hash型別

  Hash叫雜湊型別,它提供了欄位和欄位值的對映。欄位值只能是字串型別,不支援雜湊型別、集合型別等其他型別。

賦值 

  HSET命令不區分插入和更新操作,當執行插入操作時HSET命令返回1,當執行更新操作時返回0。

一次只能設定一個欄位值

語法:HSET key field value

 一次設定多個欄位值

語法:HMSET key field value [field value ...]

 當欄位不存在時

類似HSET,區別在於如何欄位存在,該命令不執行任何操作

語法:HSETNX key field value

取值

一次只能獲取一個欄位值

語法:HGET key field

 一次可以獲取多個欄位值

語法:HMGET key field [field ....]

獲取所有欄位值

語法:HGETALL key

 刪除欄位

可以刪除一個或多個欄位,返回值是被刪除的欄位個數

語法:HDEL key field [field ...]

 增加數字

語法:HINCRBY key field increment

 判斷欄位是否存在

語法:HEXISTS key field

只獲取欄位名或欄位值

語法:

  1. HKEYS key
  2. HVALS key

 獲取欄位數量

語法:HLEN key

 獲取所有欄位

作用:獲取hash的所有資訊,包括key和value

語法:hgetall key

 應用之儲存商品資訊

注意事項:存在哪些物件資料,特別是物件屬性經常發生增刪改操作的資料。

商品資訊欄位

  【商品id,商品名稱,商品描述,商品庫存,商品好評】

定義商品資訊的key

  商品id為1001的資訊在Redis中的key為:[items.1001]

示例

 List型別

  ArrayList使用陣列方式儲存資料,所以根據索引查詢資料速度快,而新增或者刪除元素時需要涉及到位移操作,所以比較慢。

  LinkedList使用雙向連結串列方式儲存資料,每個元素都記錄前後元素的指標,所以插入、刪除資料時只是更改前後元素的指標即可,速度非常快。然後通過下標查詢元素時需要從頭開始索引,所以比較慢,但是如果查詢前幾個元素或後幾個元素速度比較快。

 List介紹

  Redis的列表型別(list)可以儲存一個有序的字串列表,常用的操作是向列表兩端新增元素,或者獲取列表的某一個片段。

  列表型別內部是使用雙向連結串列(double linked list)實現的,所以向列表兩端新增元素的時間複雜度為0/1,獲取越接近兩端的元素速度就越快。意味著即使是一個有幾千萬個元素的列表,獲取頭部或尾部的10條記錄也是極快的。

向列表兩端新增元素

向列表左邊新增元素

語法:LPUSH key value [value ...]

 向列表右邊新增元素

語法:RPUSH key value [value ....]

 檢視列表

語法:LRANGE key start stop

  LRANGE命令是列表型別最常用的命令之一,獲取列表中的某一片段,將返回start、stop之間的所有元素(包括兩端的元素),索引從0開始。索引可以是負數,“-1”代表最後一邊的一個元素

 從列表兩端彈出元素

LPOP命令從列表左邊彈出一個元素,會分兩步完成:

  1. 將列表左邊的元素從列表中移除
  2. 返回被移除的元素值

語法:

  1. LPOP key
  2. RPOP key

獲取列表中元素的個數

語法:LLEN key 

 刪除列表中指定個數的值

  LREM命令會刪除列表中前count個數為value的元素,返回實際刪除的元素個數。根據count值不同,該命令的執行方式會有所不同。

語法:LREM key count value

  1. 當count>0時,LREM會從列表左邊開始刪除
  2. 當count<0時,LREM會從列表右邊開始刪除
  3. 當count=0時,LREM會刪除所有值為value的元素

獲取/設定指定索引的元素值

獲取指定索引的元素值

語法:LINDEX key index

設定指定索引的元素值

語法:LSET key index value

向列表中插入元素 

  該命令首先會在列表中從左到右查詢值為pivot的元素,然後根據第二個引數是BEFORE還是AFTER來決定將value插入到該元素的前面還是後面。

語法:LINSERT key BEFORE|AFTER pivot value

 將元素從一個列表轉移到另一個列表中

語法:RPOPLPUSH source destination

 應用之商品評論列表

需求1:使用者針對某一商品釋出評論,一個商品會被不同的使用者進行評論,儲存商品評論時,要按時間順序排序。

需要2:使用者在前端頁面查詢該商品的評論,需要按照時間順序降序排序。

思路:

  使用list儲存商品評論資訊,key是該商品的id,value是商品評論資訊商品編號為1001的商品評論key【items:comment:1001】 

 Set型別

set型別即集合型別,其中的資料時不重複且沒有順序。

集合型別和列表型別的對比:

   集合型別的常用操作是向集合中加入或刪除元素、判斷某個元素是否存在等,由於集合型別的Redis內部是使用值為空雜湊標實現,所有這些操作的時間複雜度都為0/1。

  Redis還提供了多個集合之間的交集、並集、差集的運算。

新增/刪除元素

語法:SADD key member [member ...]

語法:SREM key member [member ...]

獲取集合中的所有元素 

語法:SMEMBERS key

 判斷元素是否在集合中

語法:SISMEMBER key member

 集合運算命令

集合的差集運算 A-B

屬於A並且不屬於B的元素構成的集合

 語法:SDIFF key [key ...]

集合的交集運算 A∩B

屬於A且屬於B的元素構成的集合。 

 語法:SINTER key [key ...]

集合的並集運算 A ∪ B

屬於A或者屬於B的元素構成的集合

 

 

 語法:SUNION key [key ...]

獲取集合中的元素個數

語法:SCARD key

從集合中彈出一個元素 

注意:集合是無序的,所有spop命令會從集合中隨機選擇一個元素彈出

語法:SPOP key

 SortedSet型別zset

  在集合型別的基礎上,有序集合為集合中的每個元素都關聯一個分數,這使得我們不僅可以完成插入、刪除和判斷元素是否存在集合中,還能夠獲得最高或最低的前N個元素、獲取指定分數範圍內的元素等與分蘇有關的操作。

在某些方面有序集合和列表型別有些相似。

  1. 二者都是有序的。
  2. 二者都可以獲得某一範圍的元素

但是二者有著很大的區別:

  1. 列表型別是通過連結串列實現的,後去靠近兩端的資料速度極快,而當元素增多後,訪問中間資料的速度會變慢。
  2. 有序集合型別使用雜湊實現,所有即使讀取位於中間部分的資料也很快。
  3. 列表中不能簡單的調整某個元素的位置,但是有序集合可以(通過更改分數實現)。
  4. 有序集合要比列表型別更耗記憶體。

新增元素

  向有序集合中加入一個元素和該元素的分數,如果該元素已經存在則會用新的分數替換原有的分數。返回值是新加入到集合中的元素個數,不不含之前已經存在的元素。

語法:ZADD key score member [score member ...]

獲取排名在某個範圍的元素列表

按照元素分數從小到大的順序返回索引從start到stop之間的所有元素(包含兩端的元素)

語法:ZRANGE key start stop [WITHSCORES]

 如果需要獲取元素的分數的可以在命令尾部加上WITHSCORES引數

 獲取元素的分數

語法:ZSCORE key member

 刪除元素

移除有序集key中的一個或多個成員,不存在的成員將被忽略。

當key存在但不是有序集型別時,返回錯誤。

語法:ZREM key member [member ...]

 獲取指定分數範圍的元素

語法:ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT offset count]

 增加某個元素的分數

返回值是更改後的分數

語法:ZINCRBY key increment member

 獲取集合中元素的數量

語法:ZCARD key

 獲得指定分數範圍內的元素個數

語法:ZCOUNT key min max

 按照排名範圍刪除元素

語法:ZREMRANGEBYRANK key start stop

 按照分數範圍刪除元素

語法:ZREMRANGEBYSCORE key min max

 獲取元素的排名

從小到大

語法:ZRANK key member

從大到小

語法:ZREVRANK key member

 

 應用之商品銷售排行榜

需求:根據商品銷售對商品進行排序顯示

思路:定義商品銷售排行榜(sorted set集合),key為items:sellsort,分數為商品小數量。

寫入商品銷售量:

>商品編號1001的銷量是9,商品編號1002的銷量是10

>商品編號1001銷量家1

>商品銷量前10名

 

 通用命令

keys

語法:keys pattern

del

語法:DEL key

exists

作用:確認一個key是否存在

語法:exists key

expire

  Redis在實際使用過程中更多的用作快取,然後快取的資料一般都是需要設定生存時間的,即:到期後資料銷燬。

EXPIRE key seconds             設定key的生存時間(單位:秒)key在多少秒後會自動刪除

TTL key                     檢視key生於的生存時間

PERSIST key                清除生存時間 

PEXPIRE key milliseconds    生存時間設定單位為:毫秒

例子:
192.168.101.3:7002> set test 1        設定test的值為1
OK
192.168.101.3:7002> get test            獲取test的值
"1"
192.168.101.3:7002> EXPIRE test 5    設定test的生存時間為5秒
(integer) 1
192.168.101.3:7002> TTL test            檢視test的生於生成時間還有1秒刪除
(integer) 1
192.168.101.3:7002> TTL test
(integer) -2
192.168.101.3:7002> get test            獲取test的值,已經刪除
(nil)

rename

作用:重新命名key

語法:rename oldkey newkey

type

作用:顯示指定key的資料型別

語法:type key

Redis事務

事務介紹

  • Redis的事務是通過MULTI,EXEC,DISCARD和WATCH這四個命令來完成。
  • Redis的單個命令都是原子性的,所以這裡確保事務性的物件是命令集合。
  • Redis將命令集合序列化並確保處於一事務的命令集合連續且不被打斷的執行。
  • Redis不支援回滾的操作。

相關命令

  • MULTI

    注:用於標記事務塊的開始。

    Redis會將後續的命令逐個放入佇列中,然後使用EXEC命令原子化地執行這個命令序列。

    語法:MULTI

  • EXEC

    在一個事務中執行所有先前放入佇列的命令,然後恢復正常的連線狀態。

    語法:EXEC

  • DISCARD

    清楚所有先前在一個事務中放入佇列的命令,然後恢復正常的連線狀態。

    語法:DISCARD

  • WATCH

    當某個事務需要按條件執行時,就要使用這個命令將給定的鍵設定為受監控的狀態。

    語法:WATCH key [key ....]

    注:該命令可以實現redis的樂觀鎖

  • UNWATCH

    清除所有先前為一個事務監控的鍵。

    語法:UNWATCH

 

 事務失敗處理

  • Redis語法錯誤(編譯器錯誤)

 

  •  Redis型別錯誤(執行期錯誤)

 

為什麼redis不支援事務回滾?

  1. 大多數事務失敗是因為語法錯誤或者型別錯誤,這兩種錯誤,再開發階段都是可以避免的
  2. Redis為了效能方面就忽略了事務回滾

Redis實現分散式鎖

鎖的處理

  單應用中使用鎖:單執行緒多執行緒

    synchronize、Lock

  分散式應用中使用鎖:多程序

分散式鎖的實現方式

  1. 資料庫的樂觀鎖
  2. 給予zookeeper的分散式鎖
  3. 給予redis的分散式鎖

分散式鎖的注意事項

  1. 互斥性:在任意時刻,只有一個客戶端能持有鎖
  2. 同一性:加鎖和解鎖必須是同一個客戶端,客戶端自己不能把別人加的鎖給解了。
  3. 避免死鎖:即使有一個客戶端在持有鎖的期間崩潰而沒有主動解鎖,也能保證後續其他客戶端能加鎖。

實現分散式鎖

獲取鎖

方式一(使用set命令實現)

方式二(使用setnx命令實現)

package com.cyb.redis.utils;

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;

public class jedisUtils {
    private static String ip = "192.168.31.200";
    private static int port = 6379;
    private static JedisPool pool;
    static {
        pool = new JedisPool(ip, port);
    }
    public static Jedis getJedis() {
        return pool.getResource();
    }
    public static boolean getLock(String lockKey, String requestId, int timeout) {
        //獲取jedis物件,負責和遠端redis伺服器進行連線
        Jedis je=getJedis();
        //引數3:NX和XX
        //引數4:EX和PX
        String result = je.set(lockKey, requestId, "NX", "EX", timeout);
        if (result=="ok") {
            return true;
        }
        return false;
    }

    public static synchronized boolean getLock2(String lockKey, String requestId, int timeout) {
        //獲取jedis物件,負責和遠端redis伺服器進行連線
        Jedis je=getJedis();
        //引數3:NX和XX
        //引數4:EX和PX
        Long result = je.setnx(lockKey, requestId);
        if (result==1) {
            je.expire(lockKey, timeout); //設定有效期
            return true;
        }
        return false;
    }
}

釋放鎖

package com.cyb.redis.utils;

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;

public class jedisUtils {
    private static String ip = "192.168.31.200";
    private static int port = 6379;
    private static JedisPool pool;
    static {
        pool = new JedisPool(ip, port);
    }
    public static Jedis getJedis() {
        return pool.getResource();
    }
    /**
     * 釋放分散式鎖
     * @param lockKey
     * @param requestId
     */
    public static void releaseLock(String lockKey, String requestId) {
        Jedis je=getJedis();
        if (requestId.equals(je.get(lockKey))) {
            je.del(lockKey);
        }
    }
}

Redis持久化方案

導讀

  Redis是一個記憶體資料庫,為了保證資料的永續性,它提供了兩種持久化方案。

  1. RDB方式(預設)
  2. AOF方式

RDB方式

  RDB是Redis預設採用的持久化方式。

  RDB方式是通過快照(snapshotting)完成的,當符合一定條件時Redis會自動將記憶體中的資料進行快照並持久化到硬碟。

RDB觸發條件

  1. 符合自定義配置的快照規則
  2. 執行save或者bgsave命令
  3. 執行flushall命令
  4. 執行主從複製操作

在redis.conf中設定自定義快照規則

1、RDB持久化條件

  格式:save <seconds> <changes>

示例:

  save 900 1:表示15分鐘(900秒)內至少1個鍵更改則進行快照。

  save 300 10:表示5分鐘(300秒)內至少10個鍵被更改則進行快照。

  save 60 10000:表示1分鐘內至少10000個鍵被更改則進行快照。

2、配置dir指定rdb快照檔案的位置

# Note that you must specify a directory here, not a file name.
dir ./

3、配置dbfilename指定rdb快照檔案的名稱

# The filename where to dump the DB
dbfilename dump.rdb

說明

  1. Redis啟動後會讀取RDB快照檔案,將資料從硬碟載入到記憶體
  2. 根據資料量大小與結構和伺服器效能不同,這個時間也不同。通常將記錄1千萬個字串型別鍵,大小為1GB的快照檔案載入到記憶體中需要花費20-30秒鐘。

快照的實現原理

快照過程

  1. redis使用fork函式複製一份當前程序的副本(子程序)
  2. 父程序繼續接受並處理客戶端發來的命令,而子程序開始將記憶體中的資料寫入到硬碟中的臨時檔案。
  3. 當子程序寫入完所有資料後會用該臨時檔案替換舊的RDB檔案,至此,一次快照操作完成。

注意

  1. redis在進行快照的過程中不會修改RDB檔案,只有快照結束後才會將舊的檔案替換成新的,也就是說任何時候RDB檔案都是完整的。
  2. 這就使得我們可以通過定時備份RDB檔案來實現redis資料庫的備份,RDB檔案是經過壓縮的二進位制檔案,佔用的空間會小於記憶體中的資料,更加利於傳輸。

RDB優缺點

缺點

  使用RDB方式實現持久化,一旦redis異常退出,就會丟失最後一次快照以後更改的所有資料。這個時候我們就需要根據具體的應用場景,通過組合設定自動快照條件的方式將可能發生的資料損失控制在能夠接受範圍。如果資料相對來說比較重要,希望將損失降到最小,則可以使用AOF方式進行持久化

優點

  RDB可以最大化redis的效能:父程序在儲存RDB檔案時唯一要做的就是fork出一個字程序,然後這個子程序就會處理接下來的所有儲存工作,父程序無需執行任何磁碟I/O操作。同時這個也是一個缺點,如果資料集比較大的時候,fork可能比較耗時,造成伺服器在一段時間內停止處理客戶端的請求。

AOF方式

介紹

  預設情況下Redis沒有開啟AOF(append only file)方式的持久化

  開啟AOF持久化後每執行一條會更改Redis中的資料命令,Redis就會將該命令寫入硬碟中的AOF檔案,這一過程顯示會降低Redis的效能,但大部分下這個影響是能夠接受的,另外使用較快的硬碟可以提高AOF的效能。

配置redis.conf

設定appendonly引數為yes

appendonly yes

AOF檔案的儲存位置和RDB檔案的位置相同,都是通過dir引數設定的

dir ./

預設的檔名是appendonly.aof,可以通過appendfilename引數修改

appendfilename appendonly.aof

AOF重寫原理(優化AOF檔案)

  1. Redis可以在AOF檔案體積變得過大時,自動地後臺對AOF進行重寫
  2. 重寫後的新AOF檔案包含了恢復當前資料集所需的最小命令集合。
  3. 整個重寫操作是絕對安全的,因為Redis在建立新的AOF檔案的過程中,會繼續將命令追加到現有的AOF檔案裡面,即使重寫過程中發生停機,現有的AOF檔案也不會丟失。而一旦新AOF檔案建立完畢,Redis就會從舊AOF檔案切換到新AOF檔案,並開始對新AOF檔案進行追加操作。
  4. AOF檔案有序地儲存了對資料庫執行的所有寫入操作,這些寫入操作以Redis協議的格式儲存,因此AOF檔案的內容非常容易被人讀懂,對檔案進行分析(parse)也很輕鬆。

引數說明

  1. #auto-aof-rewrite-percentage 100:表示當前aof檔案大小超過上次aof檔案大小的百分之多少的時候會進行重寫。如果之前沒有重寫過,以啟動時aof檔案大小為基準。
  2. #auto-aof-rewrite-min-size 64mb:表示限制允許重寫最小aof檔案大小,也就是檔案大小小於64mb的時候,不需要進行優化

同步磁碟資料

  Redis每次更改資料的時候,aof機制都會將命令記錄到aof檔案,但是實際上由於作業系統的快取機制,資料並沒有實時寫入到硬碟,而是進入硬碟快取。再通過硬碟快取機制去重新整理到儲存檔案中。

引數說明

  1. appendfsync always:每次執行寫入都會進行同步,這個是最安全但是效率比較低
  2. appendfsync everysec:每一秒執行
  3. appendfsync no:不主動進行同步操作,由於作業系統去執行,這個是最快但是最不安全的方式

AOF檔案損壞以後如何修復

  伺服器可能在程式正在對AOF檔案進行寫入時停機,如果停機造成AOF檔案出錯(corrupt),那麼Redis在重啟時會拒絕載入這個AOF檔案,從而確保資料的一致性不會被破壞。

  當發生這種情況時,可以以以下方式來修復出錯的AOF檔案:

    1、為現有的AOF檔案建立一個備份。

    2、使用Redis附帶的redis-check-aof程式,對原來的AOF檔案進行修復。

    3、重啟Redis伺服器,等待伺服器字啊如修復後的AOF檔案,並進行資料恢復。

如何選擇RDB和AOF

  1. 一般來說,如果對資料的安全性要求非常高的話,應該同時使用兩種持久化功能。
  2. 如果可以承受數分鐘以內的資料丟失,那麼可以只使用RDB持久化。
  3. 有很多使用者都只使用AOF持久化,但並不推薦這種方式:因為定時生成RDB快照(snapshot)非常便於進行資料庫備份,並且RDB恢復資料集的速度也要比AOF恢復的速度要快。
  4. 兩種持久化策略可以同時使用,也可以使用其中一種。如果同時使用的話,那麼Redis啟動時,會優先使用AOF檔案來還原資料。

Redis的主從複製

什麼是主從複製

  永續性保證了即使redis服務重啟也不會丟失資料,因為redis服務重啟後將硬碟上持久化的資料恢復到記憶體中,但是當redis伺服器的硬碟損壞了可能導致資料丟失,不過通過redis的主從複製機制舊可以避免這種單點故障,如下圖:

 

 說明:

  1. 主redis中的資料有兩個副本(replication)即從redis1和從redis2,即使一臺redis伺服器宕機其他兩臺redis服務也可以繼續提供服務。
  2. 主redis中的資料和從redis上的資料保持實時同步,當主redis寫入資料時通過主從複製機制會複製到兩個從redis服務上。
  3. 只有一個主redis,可以有多個從redis。
  4. 主從複製不會阻塞master,在同步資料時,master可以繼續處理client請求
  5. 一個redis可以即是主從,如下圖:

 

主從配置

主redis配置

  無需特殊配置

從redis配置

  修改從伺服器上的redis.conf檔案

# slaveof <masterip> <masterport>
slaveof 192.168.31.200 6379

  上邊的配置說明當前【從伺服器】對應的【主伺服器】的ip是192.168.31.200,埠是6379.

實現原理

  1. slave第一次或者重連到master傳送一個SYNC的命令。
  2. master收到SYNC的時候,會做兩件事
    1. 執行bgsave(rdb的快照檔案)
    2. master會把新收到的修改命令存入到緩衝區

缺點:沒有辦法對master進行動態選舉

Redis Sentinel哨兵機制

簡介

  Sentinel(哨兵)程序是用於監控redis叢集中Master主伺服器工作的狀態,在Master主伺服器發生故障的時候,可以實現Master和Slave伺服器的切換,保證系統的高可用,其已經被整合在redis2.6+的版本中,Redis的哨兵模式到2.8版本之後就穩定了下來。

哨兵程序的作用

  1. 監控(Monitoring):哨兵(Sentinel)會不斷地檢查你的Master和Slave是否運作正常。
  2. 提醒(Notification):當被監控的某個Redis節點出現問題時,哨兵(Sentinel)可以通過API向管理員或者其他應用程式傳送通知。
  3. 自動故障遷移(Automatic failover):當一個Master不能正常工作時,哨兵(Sentinel)會開始一次自動故障遷移操作。
    1. 它會將失效Master的其中一個Slave升級為新的Master,並讓失效Master的其他Slave改為複製新的Master;
    2. 當客戶端檢視連線失效的Master時,叢集也會向客戶端返回新Master的地址,使得叢集可以使用現在的Master替換失效的Master。
    3. Master和Slave伺服器切換後,Master的redis.conf、Slave的redis.conf和sentinel.conf的配置檔案的內容都會發生相應的改變,即Master主伺服器的redis.conf配置檔案中會多一行Slave的配置,sentinel.conf的監控目標會隨之調換。

哨兵程序的工作方式

  1. 每個Sentinel(哨兵)程序以每秒鐘一次的頻率向整個叢集中的Master主伺服器,Slave從伺服器以及其他Sentinel(哨兵)程序傳送一個PING命令。
  2. 如果一個例項(instance)距離最後一次有效回覆PING命令的時間超過down-after-milliseconds選項所指定的值,則這個例項會被Sentinel(哨兵)程序標記為主觀下線(SDOWN)。
  3. 如果一個Master主伺服器被標記為主觀下線(SDOWN),則正在監視這個Master主伺服器的所有Sentinel(哨兵)程序要以每秒一次的頻率確認Master主伺服器確實進入了主觀下線狀態。
  4. 當有足夠數量的Sentinel(哨兵)程序(大於等於配置檔案指定的值)在指定的時間範圍內確認Master主伺服器進入了主觀下線狀態(SDOWN),則Master主伺服器會被標記為客觀下線(ODOWN)。
  5. 在一般情況下,每個Sentinel(哨兵)程序會以每10秒一次的頻率向叢集中的所有Master主伺服器、Slave從伺服器傳送INFO命令。
  6. 當Master主伺服器被Sentinel(哨兵)程序標記為客觀下線(ODOWN)時,Sentinel(哨兵)程序向下線的Master主伺服器的所有Slave從伺服器傳送INFO命令的頻率會從10秒一次改為每秒一次。
  7. 若沒有足夠數量的Sentinel(哨兵)程序同意Master主伺服器下線,Master主伺服器的客觀下線狀態就會被移除。若Master主伺服器重新向Sentinel(哨兵)程序傳送PING命令返回有效回覆,Master主伺服器的主觀下線狀態就會被移除。

實現

修改從機的sentinel.conf

sentinel monitor mymaster  192.168.127.129 6379 1

啟動哨兵伺服器

redis-sentinel

Redis Cluster叢集

redis-cluster架構圖

 

 架構細節

  1. 所有的redis節點彼此互聯(PING-PING機制),內部使用二進位制協議優化傳輸速度和頻寬。
  2. 節點的fail是通過叢集中超過半數的節點檢測失效時才生效。
  3. 客戶端與redis節點直連,不需要中間proxy層,客戶端不需要連線叢集所有節點,連線叢集中任何一個可用節點即可。
  4. redis-cluster把所有的物理節點對映到[0-16383]slot上,cluster負責維護node<->slot<->value
    Redis叢集中內建了16384個雜湊槽,當需要在Redis叢集中放置一個key-value時,redis先對key使用crc16演算法算出一個結果,然後把結果對16384求餘數,這樣每個key都會對應一個編號在0-16384之間的雜湊槽,redis會根據節點數量大致均等的將雜湊槽對映到不同節點。

 

redis-cluster投票:容錯 

 

  1.  叢集中所有master參與投票,如果半數以上master節點與其中一個master節點通訊超過(cluster-node-timeout),認為該master節點掛掉。
  2. 什麼時候整個叢集不可用(cluster_state:fail)?
    1. 如果叢集任意master掛掉,且當前master沒有slave,則叢集進入fail狀態。也可以理解成叢集的[0-16384]slot對映不完全時進入fail狀態。
    2. 如果叢集超過半數以上master掛掉,無論是否有slave,叢集進入fail狀態。

安裝Ruby環境

導讀

  redis叢集需要使用叢集管理指令碼redis-trib.rb,它的執行相應依賴ruby環境。

安裝

安裝ruby

yum install ruby
yum install rubygems

將redis-3.2.9.gen拖近Linux系統

安裝ruby和redis的介面程式redis-3.2.9.gem

gem install redis-3.2.9.gem

複製redis-3.2.9/src/redis-trib.rb 檔案到/usr/local/redis目錄

cp redis-3.2.9/src/redis-trib.rb /usr/local/redis/ -r

安裝Redis叢集(RedisCluster)

  Redis叢集最少需要三臺主伺服器,三臺從伺服器,埠號分別為7001~7006。

建立7001例項,並編輯redis.conf檔案,修改port為7001。

 

修改redis.conf配置檔案,開啟Cluster-enable yes 

 

 重複以上2個步驟,完成7002~7006例項的建立,注意埠修改

啟動所有的例項

建立Redis叢集

./redis-trib.rb create --replicas 1 192.168.242.129:7001 192.168.242.129:7002 192.168.242.129:7003 192.168.242.129:7004 192.168.242.129:7005  192.168.242.129:7006
>>> Creating cluster
Connecting to node 192.168.242.129:7001: OK
Connecting to node 192.168.242.129:7002: OK
Connecting to node 192.168.242.129:7003: OK
Connecting to node 192.168.242.129:7004: OK
Connecting to node 192.168.242.129:7005: OK
Connecting to node 192.168.242.129:7006: OK
>>> Performing hash slots allocation on 6 nodes...
Using 3 masters:
192.168.242.129:7001
192.168.242.129:7002
192.168.242.129:7003
Adding replica 192.168.242.129:7004 to 192.168.242.129:7001
Adding replica 192.168.242.129:7005 to 192.168.242.129:7002
Adding replica 192.168.242.129:7006 to 192.168.242.129:7003
M: d8f6a0e3192c905f0aad411946f3ef9305350420 192.168.242.129:7001
   slots:0-5460 (5461 slots) master
M: 7a12bc730ddc939c84a156f276c446c28acf798c 192.168.242.129:7002
   slots:5461-10922 (5462 slots) master
M: 93f73d2424a796657948c660928b71edd3db881f 192.168.242.129:7003
   slots:10923-16383 (5461 slots) master
S: f79802d3da6b58ef6f9f30c903db7b2f79664e61 192.168.242.129:7004
   replicates d8f6a0e3192c905f0aad411946f3ef9305350420
S: 0bc78702413eb88eb6d7982833a6e040c6af05be 192.168.242.129:7005
   replicates 7a12bc730ddc939c84a156f276c446c28acf798c
S: 4170a68ba6b7757e914056e2857bb84c5e10950e 192.168.242.129:7006
   replicates 93f73d2424a796657948c660928b71edd3db881f
Can I set the above configuration? (type 'yes' to accept): yes
>>> Nodes configuration updated
>>> Assign a different config epoch to each node
>>> Sending CLUSTER MEET messages to join the cluster
Waiting for the cluster to join....
>>> Performing Cluster Check (using node 192.168.242.129:7001)
M: d8f6a0e3192c905f0aad411946f3ef9305350420 192.168.242.129:7001
   slots:0-5460 (5461 slots) master
M: 7a12bc730ddc939c84a156f276c446c28acf798c 192.168.242.129:7002
   slots:5461-10922 (5462 slots) master
M: 93f73d2424a796657948c660928b71edd3db881f 192.168.242.129:7003
   slots:10923-16383 (5461 slots) master
M: f79802d3da6b58ef6f9f30c903db7b2f79664e61 192.168.242.129:7004
   slots: (0 slots) master
   replicates d8f6a0e3192c905f0aad411946f3ef9305350420
M: 0bc78702413eb88eb6d7982833a6e040c6af05be 192.168.242.129:7005
   slots: (0 slots) master
   replicates 7a12bc730ddc939c84a156f276c446c28acf798c
M: 4170a68ba6b7757e914056e2857bb84c5e10950e 192.168.242.129:7006
   slots: (0 slots) master
   replicates 93f73d2424a796657948c660928b71edd3db881f
[OK] All nodes agree about slots configuration.
>>> Check for open slots...
>>> Check slots coverage...
[OK] All 16384 slots covered.
[root@localhost-0723 redis]#

命令客戶端連線叢集

命令:

./redis-cli -h 127.0.0.1 -p 7001 -c


注:-c表示是以redis叢集方式進行連線
./redis-cli -p 7006 -c
127.0.0.1:7006> set key1 123
-> Redirected to slot [9189] located at 127.0.0.1:7002
OK
127.0.0.1:7002>

檢視叢集的命令

檢視叢集狀態

127.0.0.1:7003> cluster info
cluster_state:ok
cluster_slots_assigned:16384
cluster_slots_ok:16384
cluster_slots_pfail:0
cluster_slots_fail:0
cluster_known_nodes:6
cluster_size:3
cluster_current_epoch:6
cluster_my_epoch:3
cluster_stats_messages_sent:926
cluster_stats_messages_received:926

檢視叢集中的節點

127.0.0.1:7003> cluster nodes
7a12bc730ddc939c84a156f276c446c28acf798c 127.0.0.1:7002 master - 0 1443601739754 2 connected 5461-10922
93f73d2424a796657948c660928b71edd3db881f 127.0.0.1:7003 myself,master - 0 0 3 connected 10923-16383
d8f6a0e3192c905f0aad411946f3ef9305350420 127.0.0.1:7001 master - 0 1443601741267 1 connected 0-5460
4170a68ba6b7757e914056e2857bb84c5e10950e 127.0.0.1:7006 slave 93f73d2424a796657948c660928b71edd3db881f 0 1443601739250 6 connected
f79802d3da6b58ef6f9f30c903db7b2f79664e61 127.0.0.1:7004 slave d8f6a0e3192c905f0aad411946f3ef9305350420 0 1443601742277 4 connected
0bc78702413eb88eb6d7982833a6e040c6af05be 127.0.0.1:7005 slave 7a12bc730ddc939c84a156f276c446c28acf798c 0 1443601740259 5 connected
127.0.0.1:7003>

維護節點

  叢集建立完成後可以繼續向叢集中新增節點。

新增主節點

新增7007節點作為新節點

命令:./redis-trib.rb add-node 127.0.0.1:7007 127.0.0.1:7001

 

檢視叢集節點發現7007已加到叢集中 

 

 hash槽重新分配

  新增完主節點需要對主節點進行hash槽分配,這樣該主節才可以儲存資料。

檢視叢集中槽佔用情況

  redis叢集有16384個槽,叢集中的每個節點分配自己槽,通過檢視叢集節點可以看到槽佔用情況。

 

 給剛新增的7007節點分配槽

第一步:連上叢集(連線叢集中任意一個可用節點都行)

./redis-trib.rb reshard 192.168.101.3:7001

第二步:輸入要分配的槽數量

 

 輸入500,表示要分配500個槽

第三步:輸入接收槽的節點id

 

輸入:15b809eadae88955e36bcdbb8144f61bbbaf38fb

ps:這裡準備給7007分配槽,通過cluster node檢視7007節點id為:

15b809eadae88955e36bcdbb8144f61bbbaf38fb

第四步:輸入源節點id

 

 輸入:all

第五步:輸入yes開始移動槽到目標節點id

 

 輸入:yes

新增從節點

  新增7008從節點,將7008作為7007的從節點

命令:

./redis-trib.rb add-node --slave --master-id  主節點id   新節點的ip和埠   舊節點ip和埠

執行如下命令:

./redis-trib.rb add-node --slave --master-id cad9f7413ec6842c971dbcc2c48b4ca959eb5db4  192.168.101.3:7008 192.168.101.3:7001

cad9f7413ec6842c971dbcc2c48b4ca959eb5db4  是7007結點的id,可通過cluster nodes檢視。

nodes檢視

 

注意:如果原來該節點在叢集中的配置資訊已經生成到cluster-config-file指定的配置檔案中(如果cluster-config-file沒有指定則預設為nodes.conf),這時可能會報錯 

[ERR] Node XXXXXX is not empty. Either the node already knows other nodes (check with CLUSTER NODES) or contains some key in database 0

解決辦法是刪除生成的配置檔案nodes.conf,刪除後再執行./redis-trib.rb add-node指令

檢視叢集中的節點,剛新增7008為7007的從節點

 

 刪除節點

命令:

./redis-trib.rb del-node 127.0.0.1:7005 4b45eb75c8b428fbd77ab979b85080146a9bc017

刪除已經佔用hash槽的節點會失敗,報錯如下

[ERR] Node 127.0.0.1:7005 is not empty! Reshard data away and try again.

需要將該節點佔用的hash槽分配出去

Jedis連線叢集

建立JedisCluster類連線Redis叢集

@Test
public void testJedisCluster() throws Exception {
    //建立一連線,JedisCluster物件,在系統中是單例存在
    Set<HostAndPort> nodes = new HashSet<>();
    nodes.add(new HostAndPort("192.168.242.129", 7001));
    nodes.add(new HostAndPort("192.168.242.129", 7002));
    nodes.add(new HostAndPort("192.168.242.129", 7003));
    nodes.add(new HostAndPort("192.168.242.129", 7004));
    nodes.add(new HostAndPort("192.168.242.129", 7005));
    nodes.add(new HostAndPort("192.168.242.129", 7006));
    JedisCluster cluster = new JedisCluster(nodes);
    //執行JedisCluster物件中的方法,方法和redis一一對應。
    cluster.set("cluster-test", "my jedis cluster test");
    String result = cluster.get("cluster-test");
    System.out.println(result);
    //程式結束時需要關閉JedisCluster物件
    cluster.close();
}

使用Spring

配置applicationContext.xml

<!-- 連線池配置 -->
<bean id="jedisPoolConfig" class="redis.clients.jedis.JedisPoolConfig">
    <!-- 最大連線數 -->
    <property name="maxTotal" value="30" />
    <!-- 最大空閒連線數 -->
    <property name="maxIdle" value="10" />
    <!-- 每次釋放連線的最大數目 -->
    <property name="numTestsPerEvictionRun" value="1024" />
    <!-- 釋放連線的掃描間隔(毫秒) -->
    <property name="timeBetweenEvictionRunsMillis" value="30000" />
    <!-- 連線最小空閒時間 -->
    <property name="minEvictableIdleTimeMillis" value="1800000" />
    <!-- 連線空閒多久後釋放, 當空閒時間>該值 且 空閒連線>最大空閒連線數 時直接釋放 -->
    <property name="softMinEvictableIdleTimeMillis" value="10000" />
    <!-- 獲取連線時的最大等待毫秒數,小於零:阻塞不確定的時間,預設-1 -->
    <property name="maxWaitMillis" value="1500" />
    <!-- 在獲取連線的時候檢查有效性, 預設false -->
    <property name="testOnBorrow" value="true" />
    <!-- 在空閒時檢查有效性, 預設false -->
    <property name="testWhileIdle" value="true" />
    <!-- 連線耗盡時是否阻塞, false報異常,ture阻塞直到超時, 預設true -->
    <property name="blockWhenExhausted" value="false" />
</bean>
<!-- redis叢集 -->
<bean id="jedisCluster" class="redis.clients.jedis.JedisCluster">
    <constructor-arg index="0">
        <set>
            <bean class="redis.clients.jedis.HostAndPort">
                <constructor-arg index="0" value="192.168.101.3"></constructor-arg>
                <constructor-arg index="1" value="7001"></constructor-arg>
            </bean>
            <bean class="redis.clients.jedis.HostAndPort">
                <constructor-arg index="0" value="192.168.101.3"></constructor-arg>
                <constructor-arg index="1" value="7002"></constructor-arg>
            </bean>
            <bean class="redis.clients.jedis.HostAndPort">
                <constructor-arg index="0" value="192.168.101.3"></constructor-arg>
                <constructor-arg index="1" value="7003"></constructor-arg>
            </bean>
            <bean class="redis.clients.jedis.HostAndPort">
                <constructor-arg index="0" value="192.168.101.3"></constructor-arg>
                <constructor-arg index="1" value="7004"></constructor-arg>
            </bean>
            <bean class="redis.clients.jedis.HostAndPort">
                <constructor-arg index="0" value="192.168.101.3"></constructor-arg>
                <constructor-arg index="1" value="7005"></constructor-arg>
            </bean>
            <bean class="redis.clients.jedis.HostAndPort">
                <constructor-arg index="0" value="192.168.101.3"></constructor-arg>
                <constructor-arg index="1" value="7006"></constructor-arg>
            </bean>
        </set>
    </constructor-arg>
    <constructor-arg index="1" ref="jedisPoolConfig"></constructor-arg>
</bean>

測試程式碼

private ApplicationContext applicationContext;
    @Before
    public void init() {
        applicationContext = new ClassPathXmlApplicationContext(
                "classpath:applicationContext.xml");
    }

    // redis叢集
    @Test
    public void testJedisCluster() {
        JedisCluster jedisCluster = (JedisCluster) applicationContext
                .getBean("jedisCluster");

        jedisCluster.set("name", "zhangsan");
        String value = jedisCluster.get("name");
        System.out.println(value);
    }