Redis主從, 哨兵, Lettuce(二)

主從複製(Master& Slave)

作用

  1. 數據備份: 除 RDB& AOF(持久化)以外的數據備份方式
  2. 故障恢復: 當主節點(Master)出現問題時, 可以將從節點(Slave)用於主節點, 及時的恢復服務, 實現服務的高可用
  3. 讀寫分離: 主節點寫, 從節點讀, 實現讀寫分離, 提高服務器的負載能力
  • 除此之外, 主從複製也是 Redis集羣的基礎

同步過程/ 複製原理

  • 全量同步: 發生在從節點初始化階段, 將主節點的所有數據做成快照(RDB)文件, 發送給從節點過程; *當數據量較大時, 會對主從節點和網絡帶寬造成很大的開銷

  • 增量同步: Redis的增量同步是指從節點完成初始化後開始正常工作時, 主節點將新產生的寫操作命令同步到從節點, 然後在從節點執行的過程

具體過程如下:

  1. 從節點發起 SYNC命令到主節點
  2. 主節點收到 SYNC命令後, 執行 BGSAVE命令, 在異步進程內生成快照(RDB)文件, 並使用緩衝區記錄同時階段產生的所有寫操作命令
  3. 主節點生成完快照(RDB)文件後, 將該文件發送到從節點, 在此發送期間產生的所有寫操作命令繼續記錄
  4. 從節點收到快照(RDB)文件後, 丟棄所有舊數據, 並載入收到的快照
  5. 主節點發送完快照文件後, 開始向從節點發送緩衝區內記錄的(全量同步期間額外記錄的)寫操作命令
  6. 從節點完成對快照的載入後, 開始接收來自主節點的操作命令並執行
  • 完成全量同步後, 如無特殊情況一直是增量同步, 就比如從節點斷線服務中斷一段時間後重啓, Redis會盡可能首先嚐試增量同步, 否者, 全量同步. 還有主節點, 因同步時需要開闢額外的緩衝區來記錄命令等操作, 所以實際場景中得考慮好主節點的內存剩餘空間

Redis單臺服務默認是主節點, 通過命令方式或配置方式, 可以指向其它節點(被指向的節點可以是主也可以是從), 同時當前節點會變成從節點; 主節點可以有多個從節點, 但從節點只能有一個主節點; 數據的複製是單向的, 只能主節點到從節點

Redis配置

基本參數


# 外部網絡是否可以連接服務 yes啓用(默認)/ no
protected-mode yes

# Redis服務端口
port 6379

# 是否守護進程運行 yes/ no(默認)
daemonize no

# 是否通過 supervised管理守護進程 no不使用(默認)/ upstart/ systemd/ auto
supervised no

# Redis進程編號存儲文件& path
pidfile "/var/run/redis_6379.pid"

RDB持久化相關參數

  • https://blog.csdn.net/qcl108/article/details/106879002#RDB_AOF_36

# RDB持久化文件名
dbfilename "dump_6379.rdb"

# 數據庫存入目錄
dir "/Users/quanchunlin/Desktop/redis-6.0.5/conf"

REPLICATION(複製/主從同步)相關參數


# 配置形式指向節點與命令行的 slaveof相同的效果
replicaof <masterip> <masterport>

# 設置主節點的密碼
masterauth <master-password>

# 當前節點爲從節點時, 將會無法執行寫操作命令 yes(默認)/ no
replica-read-only yes

# 主從同步策略: disk與 socket(diskless/無磁盤方式 此方式還處於實驗階段). yes/ no不啓用(默認)
repl-diskless-sync no

# 從節點指定往主節點發送 Ping的時間間隔. 10秒(默認)
# repl-ping-replica-period 10

# 同步的超時時間
# repl-timeout 60

# 是否啓用 TCP_NODELAY, no不開啓(默認)/yes. 如果開啓則會使用少量的 TCP包進行數據傳輸到從節點, 速度會比較慢; 如不開啓傳輸速度會比較快, 但會佔用較多的帶寬
repl-disable-tcp-nodelay no

# 設置緩衝區大小, 在從節點失連時主節點存放要同步到從節點的數據, 設置的越大, 從節點可以失連的時間就越長
# repl-backlog-size 1mb

# 如果一段時間內沒有從節點連接主節點, 則會釋放緩衝區. 設置0則表示永不釋放緩衝區
# repl-backlog-ttl 3600

# 當主節點無法工作時哨兵(Sentinel)通過這個值來決定將哪個從(Slave)優先提升爲主節點, 值越小表示越優先提升, 但設置爲0表示, 此節點將永遠不能被提升爲主節點. 100(默認)
replica-priority 100

# 當 Master少於3個, 延時小於等於10秒的已連接 Slave, 就可以停止接收寫操作
# 1. 至少需要3個 Slave的狀態爲 oneline
# 2. 延時是以秒爲單位, 且必須小於等於指定值, 是從最後一個 Slave接收到的 ping(通常每秒發送)開始計數
# min-replicas-to-write 3
# min-replicas-max-lag 10

SECURITY(安全)相關參數


# 當前服務節點密碼
# requirepass foobared

# 命令重命名: 用於危險命令改變名字. 注: 該命令記錄到 AOF文件後被傳送到從服務器可能產生問題
# rename-command CONFIG ""

AOF(Append Only File)持久化相關參數

  • https://blog.csdn.net/qcl108/article/details/106879002#RDB_AOF_36

# yes開啓/ no關閉(默認)
appendonly no

# AOF文件名 (default: "appendonly.aof")
appendfilename "appendonly_6379.aof"

LUA腳本


# Lua腳本的最大執行時間, 毫秒爲單位
lua-time-limit 5000

SLOW LOG(慢查詢日誌)腳本


# 記錄超過多少微秒的查詢命令. 查詢的執行時間不包括客戶端的 I/O執行和網絡通信時間, 只是查詢命令執行時間
# 1000000等於1秒, 設置爲0則記錄所有命令
slowlog-log-slower-than 10000

# 記錄大小,可通過SLOWLOG RESET命令重置
slowlog-max-len 128

多服務單機簡單部署

  • 創建三個 redis.conf環境, 1個 Master, 2個 Slave. 放到同一個目錄

# 主節點 redis_6379.conf
port 6379
dbfilename dump_6379.rdb

# 從節點 redis_6380.conf
port 6380
dbfilename dump_6380.rdb
replicaof 127.0.0.1 6379 # 可以啓動後手動在 redis-cli, 通過 slaveof命令指向主節點

# 從節點 redis_6381.conf
port 6381
dbfilename dump_6381.rdb
replicaof 127.0.0.1 6379 # 可以啓動後手動在 redis-cli, 通過 slaveof命令指向主節點

  • 依次啓動: …/src/redis-server redis-6379.conf, …/src/redis-server redis-6380.conf, …/src/redis-server redis-6381.conf
  • 啓動後可以通過 info replication命令查看主從信息

哨兵模式(Reids Sentinel)

作用

  • 在多從單主架構中, 一旦主節點由於故障不能提供服務時, 通過哨兵(Sentinel)模式, 將其中一個從節點自動晉升爲主節點, 同時通知其它從節點重新指向新的主節點, 來維持高可用 Redis服務

原理& 過程

  1. 主觀下線
  • 每隔一秒, 每個 Sentinel會向主節點或從節點做心跳檢測, 當一個主節點心跳反應超過 sentinel down-after-milliseconds設的毫秒數, Sentinel將會對該節點做失敗判定並標註
  1. 客觀下線
  • 當主觀下線的節點爲主節點時, 該 Sentinel會向其它 Sentinel詢問對主節點的判斷, 當至少 sentinel monitor的數 Sentinel同意判定時, 該 Sentinel會做出客觀下線的決定
  1. 領導者 Sentinel選舉
  • Raft算法實現: 假設 s1(sentinel-1)最先完成客觀下線, 它會向其餘 Sentinel發送命令, 請求成爲領導者, 收到請求的 Sentinel如果沒有同意過其它 Sentinel的請求, 那麼就會同意 s1的請求, 成爲領導者
  1. 故障轉移
  • 領導者 Sentinel在多個從節點中選出一個節點作爲新的主節點, 選與前主節點數據相似度最高的從節點, 然後將其它從節點重新指向新的主節點, Sentinel集羣也會將原主節點改爲從節點並繼續監控, 當其恢復後命令它去複製新的主節點

Redis Sentinel配置


# 外部網絡是否可以連接服務 yes啓用/ no(默認)
protected-mode no

# Redis Sentinel服務端口
port 26379

# 是否守護進程運行 yes/ no(默認)
daemonize no

# Redis Sentinel進程編號存儲文件& path
pidfile "/var/run/redis-sentinel-26379.pid"

# 日誌文件
logfile "/var/log/sentinel-26379.log"

# 工作目錄
dir "/Users/quanchunlin/Desktop/redis-6.0.5/conf"

# 設置<master-name>標識集羣名稱可以自定義, 設置<password>連接主從節點時的密碼; 注: Sentinel無法爲主從分別設置密碼, 所以需將密碼都設置一樣的
# sentinel auth-pass <master-name> <password>

# 監聽地址爲 <ip> <redis-port>的主節點, <quorum>數是有多少個 Sentinel同意當前主節點失效時, 纔會被當前 Sentinel集羣認爲是客觀下線
sentinel monitor <master-name> <ip> <redis-port> <quorum>

# 當主備切換時, 某臺從節點晉升爲新的主節點時, 同時其它從節點被重新指向新的主節點做主從同步, <numreplicas>值越小同步完成時間越長, 但如果值設的過大, 可能會導致主節點阻塞
# sentinel parallel-syncs <master-name> <numreplicas>

# Specifies the failover timeout in milliseconds. It is used in many ways:
#
# - The time needed to re-start a failover after a previous failover was
#   already tried against the same master by a given Sentinel, is two
#   times the failover timeout.
#
# - The time needed for a replica replicating to a wrong master according
#   to a Sentinel current configuration, to be forced to replicate
#   with the right master, is exactly the failover timeout (counting since
#   the moment a Sentinel detected the misconfiguration).
#
# - The time needed to cancel a failover that is already in progress but
#   did not produced any configuration change (SLAVEOF NO ONE yet not
#   acknowledged by the promoted replica).
#
# - The maximum time a failover in progress waits for all the replicas to be
#   reconfigured as replicas of the new master. However even after this time
#   the replicas will be reconfigured by the Sentinels anyway, but not with
#   the exact parallel-syncs progression as specified.
# sentinel failover-timeout <master-name> <milliseconds>

# 心跳反應需要超過多少失效時間, Sentinel纔將一個主節點主觀的地判定是不可用的. 30000毫秒(默認)
sentinel down-after-milliseconds <master-name> <milliseconds>

# 通知腳本: 當有警告級別的事件發生時(比如說 Redis實例的主觀失效和客觀失效等)將會調用此腳本, 一般用於發送郵件或 SMS等方式通知系統管理員. 調用該腳本時, 將傳給腳本兩個參數, 一個是事件的類型, 一個是事件的描述
# sentinel notification-script <master-name> <script-path>

# 當主備切換時, 將會自動調用此腳本. 調用該腳本時, 將傳給腳本以下參數 <master-name> <role> <state> <from-ip> <from-port> <to-ip> <to-port>
# sentinel client-reconfig-script <master-name> <script-path>

多服務單機簡單部署

  • 創建三個 sentinel.conf環境

# sentinel-26379.conf
port 26379
sentinel monitor mymaster 127.0.0.1 6379 2
sentinel down-after-milliseconds mymaster 10000

# sentinel-26380.conf
port 26380
sentinel monitor mymaster 127.0.0.1 6379 2
sentinel down-after-milliseconds mymaster 20000

# sentinel-26381.conf
port 26381
sentinel monitor mymaster 127.0.0.1 6379 2
sentinel down-after-milliseconds mymaster 30000

  • 依次啓動: …/src/redis-sentinel sentinel-26379.conf, …/src/redis-sentinel sentinel-26380.conf, …/src/redis-sentinel sentinel-26381.conf

Spring boot Lettuce使用

Jar包


        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>

配置


# 使用的數據庫索引編號
spring.redis.database=0
# 連接池最大連接數(-1表示不限制), 默認爲8
spring.redis.lettuce.pool.max-active=1000
# 連接池最大阻塞等待時間(-1表示不限制, 單位毫秒ms), 默認爲-1
spring.redis.lettuce.pool.max-wait=-1
# 連接池中的最大空閒連接, 默認爲8
spring.redis.lettuce.pool.max-idle=200
# 連接池中的最小空閒連接, 默認爲0
spring.redis.lettuce.pool.min-idle=100
# 關閉連接前等待任務處理完成的最長時間, 默認爲100ms
spring.redis.lettuce.shutdown-timeout=100ms
# 哨兵設定的主節點名稱
spring.redis.sentinel.master=mymaster
# 哨兵節點, 多節點按(,)號分割
spring.redis.sentinel.nodes=127.0.0.1:26379,127.0.0.1:26380,127.0.0.1:26381


實例


@Configuration
public class RedisConfig {
    @Bean
    public RedisTemplate redisTemplate(RedisConnectionFactory factory) {
        final RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(factory);

        /** 將默認 Jdk序列化替換成 StringRedisSerializer*/
        final StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
        template.setKeySerializer(stringRedisSerializer);
        template.setHashKeySerializer(stringRedisSerializer);

        /** 將默認 Jdk序列化替換成 Jackson2JsonRedisSerialize*/
        final Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        final ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);
        template.setValueSerializer(jackson2JsonRedisSerializer);
        template.setHashValueSerializer(jackson2JsonRedisSerializer);
        template.afterPropertiesSet();
        return template;
    }

}

@Component
public class RedisUtil {
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    /** 設置過期時間(秒)*/
    public boolean expire(String key, long time) {
        if (time > 0) {
            return redisTemplate.expire(key, time, TimeUnit.SECONDS);
        }
        return true;
    }

    /** 獲取過期時間(秒)
     * @return 返回0表示永久有效
     * */
    public long getExpire(String key) {
        return redisTemplate.getExpire(key, TimeUnit.SECONDS);
    }

    /** 判斷 key是否存在*/
    public boolean hasKey(String key) {
        return redisTemplate.hasKey(key);
    }

    /** 刪除緩存*/
    @SuppressWarnings("unchecked")
    public void del(String... key) {
        if (key != null && key.length > 0) {
            if (key.length == 1) {
                redisTemplate.delete(key[0]);
            } else {
                redisTemplate.delete(CollectionUtils.arrayToList(key));
            }
        }
    }

    /**
     * String(字符串)
     * */

    /** 獲取指定緩存*/
    public Object get(String key) {
        return key == null ? null : redisTemplate.opsForValue().get(key);
    }

    /** 設置緩存*/
    public void set(String key, Object value) {
        redisTemplate.opsForValue().set(key, value);
    }

    /** 設置緩存, 附加過期時間(秒)*/
    public void set(String key, Object value, long time) {
        if (time > 0) {
            redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
        } else {
            set(key, value);
        }
    }

    /**
     * Hash(哈希)
     * */

    /** 獲取 hash裏面指定字段的值*/
    public Object hmget(String key, String field) {
        return redisTemplate.opsForHash().get(key, field);
    }

    /** 從 hash中讀取全部的域和值*/
    public Map<Object, Object> hgetall(String key) {
        return redisTemplate.opsForHash().entries(key);
    }

    /** 設置一個 hash字段值, 如不存在將會創建*/
    public void hset(String key, String field, Object value) {
        redisTemplate.opsForHash().put(key, field, value);
    }

    /** 設置一個 hash字段值, 如不存在將會創建, 附加過期時間(秒)*/
    public void hset(String key, String field, Object value, long time) {
        redisTemplate.opsForHash().put(key, field, value);
        if (time > 0) {
            expire(key, time);
        }
    }

    /** 設置多個 hash字段值*/
    public void hmset(String key, Map<String, Object> map) {
        redisTemplate.opsForHash().putAll(key, map);
    }

    /** 設置多個 hash字段值, 附加過期時間(秒)*/
    public void hmset(String key, Map<String, Object> map, long time) {
        redisTemplate.opsForHash().putAll(key, map);
        if (time > 0) {
            expire(key, time);
        }
    }

    /** 刪除一個或多個 hash的 field*/
    public void hdel(String key, Object... field) {
        redisTemplate.opsForHash().delete(key, field);
    }

    /** 判斷 field是否存在於 hash中*/
    public boolean hexists(String key, String field) {
        return redisTemplate.opsForHash().hasKey(key, field);
    }

    /**
     * List(列表)
     * */

    /** 從列表中獲取指定返回的元素*/
    public List<Object> lrange(String key, long start, long end) {
        return redisTemplate.opsForList().range(key, start, end);
    }

    /** 獲取 key對應的 list的長度*/
    public long llen(String key) {
        return redisTemplate.opsForList().size(key);
    }

    /** 獲取一個元素, 通過其索引列表*/
    public Object lindex(String key, long index) {
        return redisTemplate.opsForList().index(key, index);
    }

    /** 從隊列的右邊入隊一個元素*/
    public long rpush(String key, Object value) {
        return redisTemplate.opsForList().rightPush(key, value);
    }

    /** 從隊列的右邊入隊一個元素, 附加過期時間(秒)*/
    public long rpush(String key, Object value, long time) {
        Long count = redisTemplate.opsForList().rightPush(key, value);
        if (time > 0)
            expire(key, time);
        return count;
    }

    /** 從隊列的右邊入隊多個元素*/
    public long rpush(String key, List<Object> value) {
        return redisTemplate.opsForList().rightPushAll(key, value);
    }

    /** 從隊列的右邊入隊多個元素, 附加過期時間(秒)*/
    public long rpush(String key, List<Object> value, long time) {
        Long count = redisTemplate.opsForList().rightPushAll(key, value);
        if (time > 0)
            expire(key, time);
        return count;
    }

    /** 從隊列的右邊出隊一個元*/
    public Object rpop(String key) {
        return redisTemplate.opsForList().rightPop(key);
    }

    /** 從隊列的左邊入隊一個元素*/
    public long lpush(String key, Object value) {
        return redisTemplate.opsForList().leftPush(key, value);
    }

    /** 從隊列的左邊入隊一個元素, 附加過期時間(秒)*/
    public long lpush(String key, Object value, long time) {
        Long count = redisTemplate.opsForList().leftPush(key, value);
        if (time > 0)
            expire(key, time);
        return count;
    }

    /** 從隊列的左邊入隊多個元素*/
    public long lpush(String key, List<Object> value) {
        return redisTemplate.opsForList().leftPushAll(key, value);
    }

    /** 從隊列的左邊入隊多個元素, 附加過期時間(秒)*/
    public long lpush(String key, List<Object> value, long time) {
        Long count = redisTemplate.opsForList().leftPushAll(key, value);
        if (time > 0)
            expire(key, time);
        return count;
    }

    /** 從隊列的左邊出隊一個元素*/
    public Object lpop(String key) {
        return redisTemplate.opsForList().leftPop(key);
    }

    /**
     * Sorted Set(有序集合)
     * */

    /** 添加到有序set的一個成員, 或更新的分數, 如果它已經存在*/
    public boolean zadd(String key, Object value, double scoure) {
        return redisTemplate.opsForZSet().add(key, value, scoure);
    }

    /** 添加到有序set的一個成員, 或更新的分數, 如果它已經存在, 附加過期時間(秒)*/
    public boolean zadd(String key, Object value, double scoure, long time) {
        final Boolean flag = redisTemplate.opsForZSet().add(key, value, scoure);
        if (time > 0)
            expire(key, time);
        return flag;
    }

    /** 獲取一個排序的集合中的成員數量*/
    public long zcard(String key) {
        return redisTemplate.opsForZSet().zCard(key);
    }

    /** 根據指定的index返回,返回sorted set的成員列表*/
    public Set<Object> zrange(String key, long start, long stop) {
        return redisTemplate.opsForZSet().range(key, start, stop);
    }

    /** 從排序的集合中刪除一個或多個成員*/
    public long zrem(String key, Object...value) {
        return redisTemplate.opsForZSet().remove(key, value);
    }

}

    @Autowired
    private RedisUtil redisUtil;

    redisUtil.set("string", "普通字符串值!");
    log.info("string: {}", redisUtil.get("string"));


如果您覺得有幫助,歡迎點贊哦 ~ 謝謝!!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章