Redis 入門實踐

1 簡介

Redis,REmote DIctionary Server,是一個由 Salvatore Sanfilippo 寫的 Key-Value 存儲系統。html

Redis 是一個開源的使用 ANSI C 語言編寫、遵照 BSD 協議、支持網絡、可基於內存亦可持久化的日誌型、Key-Value 數據庫,並提供多種語言的API。java

它一般被稱爲數據結構服務器,由於值(Value)能夠是字符串(String), 哈希(Map), 列表(list), 集合(sets)和有序集合(sorted sets)等類型。node

2 安裝

官網地址: https://redis.iogit

下載地址: https://github.com/antirez/redis/releasesgithub

2.1 Windows

安裝完成後在安裝目錄下執行:redis

redis-server.exe redis.windows.conf

2.2 Linux

下載,解壓縮並編譯Redis最新穩定版本:算法

wget http://download.redis.io/releases/redis-5.0.3.tar.gz
tar xzf redis-5.0.3.tar.gz
cd redis-5.0.3
make

啓動Redis服務:spring

cd src
./redis-server ../redis.conf

3 配置

Redis 的配置文件,Windows 是安裝目錄的 redis.windows.conf 文件,Linux 是安裝目錄下的 redis.conf 文件。shell

在鏈接上 Redis 服務後,能夠經過 config 命令查看或者編輯配置項。數據庫

3.1 查看

redis 127.0.0.1:6379> config get ${name}

例:

127.0.0.1:6379> config get port
1) "port"
2) "6379"

3.2 編輯

redis 127.0.0.1:6379> config set ${name} ${value}

例:

127.0.0.1:6379> config set loglevel "notice"
OK
注:部分配置不能經過 config 命令動態編輯,須要直接修改配置文件對應內容,例如端口 port。

3.3 部分參數說明

3.3.1 daemonize

是否以守護線程運行,默認爲 no,使用 yes 啓用守護線程;(後臺啓動)

3.3.2 port

Redis監聽端口,默認爲 6379;

注:做者曾解釋過 6379 的來歷。6379 在手機按鍵對應的英文是 MERZ,意大利歌女 Alessia Merz 的名字。參考連接: http://oldblog.antirez.com/po...

3.3.3 bind

指定客戶端鏈接地址,默認爲 127.0.0.1,也就是隻能本地鏈接,屏蔽該參數啓用遠程鏈接;

3.3.4 timeout

客戶端空閒多長時間(秒)關閉該鏈接,指定爲 0 關閉該功能;

3.3.5 save

save <seconds> <changes>

指定在多長時間內,至少有多少次更新操做,就將數據同步到數據文件,能夠多個條件配合使用;

Redis默認提供了三個條件:

save 900 1
save 300 10
save 60 10000

說明Redis在下列三種狀況將會同步數據到文件中:

  1. 在 900 秒後至少 1 個 key 發生改變;
  2. 在 300 秒後至少 10 個key發生改變;
  3. 在 60 秒後至少 10000 個key發生改變;

3.3.6 dbfilename

本地數據庫文件名,默認是dump.rdb;

3.3.7 dir

本地數據庫文件存放路徑,默認是./(當前目錄);

3.3.8 replicaof

replicaof <masterip> <masterport>

當在主從複製中,本身做爲 slave,設置 master 的 ip 和端口,在該 slave 啓動時,會自動從 master 進行數據同步;

3.3.9 masterauth

當 master 設置了密碼後,slave 鏈接 master 的密碼;

3.3.10 requirepass

設置 Redis 鏈接密碼,默認關閉;

3.3.11 appendonly

開啓 Redis 數據持久化到日誌中(AOF),默認爲 no 未開啓;

因爲默認的數據持久化方案(RDB),存儲到 dump.rdb 文件中,在斷電或服務忽然掛掉的狀況下會丟失數據,開啓日誌持久化能夠彌補該不足;

3.3.12 appendfilename

日誌文件名,默認爲 appendonly.aof;

3.3.13 appendfsync

日誌更新頻率,有3個可選值;

  1. no,讓操做系統本身決定,速度最快;
  2. always,每次操做都會寫更新日誌,速度較慢但最安全;
  3. everysec,每秒更新一第二天志,折中方案;(默認)

3.4 淘汰策略

# maxmemory <bytes>

# MAXMEMORY POLICY: how Redis will select what to remove when maxmemory
# is reached. You can select among five behaviors:
#
# volatile-lru -> Evict using approximated LRU among the keys with an expire set.
# allkeys-lru -> Evict any key using approximated LRU.
# volatile-lfu -> Evict using approximated LFU among the keys with an expire set.
# allkeys-lfu -> Evict any key using approximated LFU.
# volatile-random -> Remove a random key among the ones with an expire set.
# allkeys-random -> Remove a random key, any key.
# volatile-ttl -> Remove the key with the nearest expire time (minor TTL)
# noeviction -> Don't evict anything, just return an error on write operations.
#
# LRU means Least Recently Used
# LFU means Least Frequently Used
#
# Both LRU, LFU and volatile-ttl are implemented using approximated
# randomized algorithms.
#
# Note: with any of the above policies, Redis will return an error on write
#       operations, when there are no suitable keys for eviction.
#
#       At the date of writing these commands are: set setnx setex append
#       incr decr rpush lpush rpushx lpushx linsert lset rpoplpush sadd
#       sinter sinterstore sunion sunionstore sdiff sdiffstore zadd zincrby
#       zunionstore zinterstore hset hsetnx hmset hincrby incrby decrby
#       getset mset msetnx exec sort
#
# The default is:
#
# maxmemory-policy noeviction
  1. LRU least recently used 最近使用;
  2. LFU least frequently used 最少使用;
  3. noeviction 默認淘汰策略,return an error;

4 數據類型

Redis支持五種數據類型:string(字符串),hash(哈希),list(列表),set(集合)及 zset(sorted set:有序集合)。

4.1 string

最基本類型,二進制安全,也能夠包含jpg或序列化後的對象,最大支持512M;

例:

127.0.0.1:6379> SET name "caojiantao"
OK
127.0.0.1:6379> GET name
"caojiantao"

4.2 hash

Key-Value鍵值對集合,適合用來存儲簡單對象;

例:

127.0.0.1:6379> hmset user name caojiantao age 18
OK
127.0.0.1:6379> hget user age
"18"

4.3 list

簡單的字符串列表,雙向鏈表的數據結構;

例:

127.0.0.1:6379> lpush months 1
(integer) 1
127.0.0.1:6379> lpush months 2
(integer) 2
127.0.0.1:6379> rpush months 3
(integer) 3
127.0.0.1:6379> lrange months 0 10
1) "2"
2) "1"
3) "3"
127.0.0.1:6379> lpop months
"2"
127.0.0.1:6379> rpop months
"3"

4.4 set

string 類型的無序集合(惟一性),hash 結構,操做複雜度爲 O(1);

例:

127.0.0.1:6379> sadd team zhangsan lisi
(integer) 2
127.0.0.1:6379> smembers team
1) "zhangsan"
2) "lisi"
127.0.0.1:6379> sadd team lisi
(integer) 0

4.5 zset

同 set,不過每一個子元素會關聯一個 double 類型的分數 score,zset 根據 score 排序;

例:

127.0.0.1:6379> zadd days 1 one
(integer) 1
127.0.0.1:6379> zadd days 0 zero
(integer) 1
127.0.0.1:6379> zadd days 2 two
(integer) 1
127.0.0.1:6379> zrangebyscore days 0 10
1) "zero"
2) "one"
3) "two"

4.6 geo

geo 爲地理位置類型,3.2+ 版本纔開始支持,其底層實現還是 zset,因此刪除成員命令同 zrem;

重要命令一覽:

  • geoadd 增長某個地理位置座標
  • geopos 獲取某個地理位置座標
  • geodist 獲取兩個地理位置的距離
  • georadius 根據給定的地理位置座標獲取指定範圍內的地理位置集合
  • geohash 獲取某個地理位置的 geohash 值

例:

127.0.0.1:6379> geoadd positions 116.407258 39.991496 olympics 116.403909 39.915547 tiananmen 116.333374 40.009645 qinghua
(integer) 3
127.0.0.1:6379> geodist positions tiananmen qinghua
"12070.5091"
127.0.0.1:6379> georadiusbymember positions tiananmen 20 km
1) "qinghua"
2) "tiananmen"
3) "olympics"
127.0.0.1:6379> georadiusbymember positions tiananmen 10 km
1) "tiananmen"
2) "olympics"

4.7 小結

類型 簡介 特性 場景
String(字符串) 二進制安全 能夠包含任何數據,好比jpg圖片或者序列化的對象,一個鍵最大能存儲512M ---
Hash(字典) 鍵值對集合,即編程語言中的Map類型 適合存儲對象,而且能夠像數據庫中update一個屬性同樣只修改某一項屬性值(Memcached中須要取出整個字符串反序列化成對象修改完再序列化存回去) 存儲、讀取、修改用戶屬性
List(列表) 鏈表(雙向鏈表) 增刪快,提供了操做某一段元素的API 1,最新消息排行等功能(好比朋友圈的時間線) 2,消息隊列
Set(集合) 哈希表實現,元素不重複 一、添加、刪除,查找的複雜度都是O(1) 二、爲集合提供了求交集、並集、差集等操做 一、共同好友 二、利用惟一性,統計訪問網站的全部獨立ip 三、好友推薦時,根據tag求交集,大於某個閾值就能夠推薦
Sorted Set(有序集合) 將Set中的元素增長一個權重參數score,元素按score有序排列 數據插入集合時,已經進行自然排序 一、排行榜
geo 經緯度座標類型 附近的人

5 特性

5.1 事務

multi
...(命令)
exec

一次執行多條命令,有如下特色:

  1. 發送exec指令前,全部的操做都會放入隊列緩存;
  2. 執行事務時,任何命令執行失敗,其餘命令正常被執行,已操做的命令不會回滾(非原子性);
  3. 執行過程當中,其餘客戶端的命令不會插入到該事務中;
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set a 1
QUEUED
127.0.0.1:6379> set b 2
QUEUED
127.0.0.1:6379> get a
QUEUED
127.0.0.1:6379> del a
QUEUED
127.0.0.1:6379> exec
1) OK
2) OK
3) "1"
4) (integer) 1

5.2 發佈訂閱

Redis 支持一個發佈訂閱的消息通訊模式,發送者 pub 發送消息,訂閱者 sub 接受消息,可訂閱任意數量的頻道 channel;

三個客戶端都訂閱了 channel 這個頻道;

一旦有消息發佈pub到channel中,以前訂閱該channel的三個客戶端都會收到這個message;

例:

客戶端訂閱talk頻道;

127.0.0.1:6379> subscribe talk
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "talk"
3) (integer) 1

另開客戶端發佈消息值talk頻道;

127.0.0.1:6379> publish talk "hello world"
(integer) 1

此時客戶端收到消息;

1) "message"
2) "talk"
3) "hello world"

5.3 腳本

Redis 使用 Lua 解釋器執行,執行命令爲eval;

eval script numkeys key [key ...] arg [arg ...]
  • script,lua腳本內容
  • numkeys,key的個數
  • key,Redis中key屬性
  • arg,自定義參數
注:key 和 arg 在 lua 腳本佔位符分別爲 KEYS[] 和 ARGV[],必須大寫,數組下標從 1 開始。

例:獲取腳本參數

127.0.0.1:6379> eval "return {KEYS[1],KEYS[2],ARGV[1]}" 2 "key1" "key2" "argv1"
1) "key1"
2) "key2"
3) "argv1"

一般會將腳本存儲到一個lua文件中,假如test.lua內容以下:

return {KEYS[1],KEYS[2],ARGV[1]}

執行這個lua腳本命令;

redis-cli.exe --eval test.lua "key1" "key2" , "argv1"
1) "key1"
2) "key2"
3) "argv1"

注意參數格式與以前有點出入,執行lua腳本文件不須要numkeys,key和arg參數用逗號相隔;

6 數據結構

6.1 sorted set

兩種編碼實現:ziplist 和 skiplist,當知足下列條件採用 ziplist 編碼方式:

  1. 有序集合保存的元素數量小於128個 ;
  2. 有序集合保存的全部元素成員的長度小於64字節 ;

同時 zset 還維護了一個字典,保存元素 member 到 分值 score 的映射,便於等值查找。

6.1.1 ziplist

壓縮列表, 2 個緊挨在一塊兒的節點組成一個元素,表明元素的實際值和分值大小。

6.1.2 skiplist

跳躍表,有利於範圍查找,相比紅黑樹實現難度較爲簡單得多。

參考: https://segmentfault.com/a/11...

7 爲何快

  1. 徹底基於內存;
  2. 數據結構簡單;
  3. 單線程避免上下文切換;
  4. 多路 I/0 複用模型,非阻塞;

8 使用(Java)

8.1 客戶端

8.1.1 Jedis

github: https://github.com/xetorthio/jedis

阻塞 I/O 模型,調用方法都是同步的,不支持異步調用,而且 Jedis 客戶端非線程安全,須要結合鏈接池使用;

maven依賴:

<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>2.9.0</version>
</dependency>

demo示例:

String host = "127.0.0.1";
int port = 6379;
// 鏈接本地的 Redis 服務
Jedis jedis = new Jedis(host, port);
// 查看服務是否運行
System.out.println("服務正在運行: " + jedis.ping());

// 基本操做
String key = "welcome";
jedis.set(key, "hello world");
System.out.println(jedis.get(key));

// 鏈接池配置
GenericObjectPoolConfig config = new GenericObjectPoolConfig();
config.setMaxTotal(1);
// 鏈接池操做
JedisPool pool = new JedisPool(config, host, port);
Jedis a = pool.getResource();
// a.close();
System.out.println(a);
Jedis b = pool.getResource();
System.out.println(b);

8.1.2 Lettuce

github: https://github.com/lettuce-io/lettuce-core

基於 Netty 框架,異步調用,線程安全;

maven依賴:

<dependency>
    <groupId>io.lettuce</groupId>
    <artifactId>lettuce-core</artifactId>
    <version>5.0.3.RELEASE</version>
</dependency>

demo示例:

// 1. 構造uri
RedisURI uri = RedisURI.builder()
    .withHost("127.0.0.1")
    .withPort(6379)
    .build();
// 2. 建立client
RedisClient client = RedisClient.create(uri);
// 3. 鏈接redis
StatefulRedisConnection<String, String> connect = client.connect();
// 4. 獲取操做命令(同步)
RedisCommands<String, String> commands = connect.sync();
String key = "welcome";
System.out.println(commands.get(key));

connect.close();

8.1.3 Redission

github: https://github.com/redisson/redisson

實現了分佈式和可擴展的 Java 數據結構;

maven依賴:

<dependency>
   <groupId>org.redisson</groupId>
   <artifactId>redisson</artifactId>
   <version>3.11.5</version>
</dependency>

demo示例:

public static void main(String[] args) {
    // 1. 建立鏈接配置
    Config config = new Config();
    config.useSingleServer().setAddress("redis://10.242.24.246:6379");
    // 2. 建立 redisson 實例
    RedissonClient client = Redisson.create(config);
    // 操做數據
    RBucket<Object> bucket = client.getBucket("name");
    bucket.set("caojiantao");
    System.out.println(bucket.get());
    // 3. 關閉鏈接實例
    client.shutdown();
}

8.2 springboot集成

8.2.1 maven 依賴

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
注:springboot 2.x 以後使用了 Lettuce 替換掉了底層 Jedis 的依賴。

8.2.2 屬性配置

在 application.yml 添加下面屬性

spring:
  redis:
    host: 127.0.0.1
    port: 6379
    password: 123456
    # 鏈接池配置(根據須要)
    lettuce:
      pool:
        max-idle: 8

8.2.3 基本使用

springboot 默認注入了 RedisTemplate 和 StringRedisTemplate 兩個實例用來操做 Redis,前者 key 和 value 都是採用 JDK 序列化,後者只能操做 String 數據類型;

可直接注入使用;

@Autowired
@Qualifier("redisTemplate")
private RedisTemplate redisTemplate;

@Autowired
@Qualifier("stringRedisTemplate")
private StringRedisTemplate stringRedisTemplate;

public void test() {
    String key = "welcome";
    Object o = redisTemplate.opsForValue().get(key);
    // 此處爲null,因爲key序列化方式爲JDK
    System.out.println(o);

    String s = stringRedisTemplate.opsForValue().get(key);
    System.out.println(s);
}
注:Redis 默認注入原理可參考 RedisAutoConfiguration 類。

8.2.4 自定義 Template

默認注入的兩種 RedisTemplate 顯然不適用全部的業務場景,自定義 Template 通常只需下列兩個步驟;

  1. 自定義 RedisSerializer;
  2. 注入自定義 Template;

參考第三方序列化框架 protostuff,序列化後體積較小,速度快;

import io.protostuff.*;
import io.protostuff.runtime.RuntimeSchema;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.SerializationException;

/**
 * @author caojiantao
 */
public class ProtoStuffSerializer<T> implements RedisSerializer<T> {

    private Class<T> clazz;

    public ProtoStuffSerializer(Class<T> clazz) {
        this.clazz = clazz;
    }

    @Override
    public byte[] serialize(T t) throws SerializationException {
        if (t == null) {
            return new byte[0];
        }
        Schema<T> schema = RuntimeSchema.getSchema(clazz);
        return ProtostuffIOUtil.toByteArray(t, schema, LinkedBuffer.allocate());
    }

    @Override
    public T deserialize(byte[] bytes) throws SerializationException {
        if (bytes == null) {
            return null;
        }
        Schema<T> schema = RuntimeSchema.getSchema(clazz);
        T t = schema.newMessage();
        ProtostuffIOUtil.mergeFrom(bytes, t, schema);
        return t;
    }
}

而後手動注入到spring容器中;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;

@Configuration
public class RedisConfig {

    @Bean("customTemplate")
    public RedisTemplate<String, Student> customTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Student> template = new RedisTemplate<>();
        // 注入redis鏈接工廠實例
        template.setConnectionFactory(factory);
        ProtoStuffSerializer<Student> serializer = new ProtoStuffSerializer<>(Student.class);
        // 設置key、value序列化方式
        template.setKeySerializer(RedisSerializer.string());
        template.setValueSerializer(serializer);
        template.afterPropertiesSet();
        return template;
    }
}

9 分佈式解決方案

9.1 主從同步

將全部數據存儲到單個 Redis 主要存在兩個問題;

  1. 數據備份;
  2. 數據量過大下降性能;

主從模式很好的解決了以上問題。一個 Redis 實例做爲主機 master,其餘的做爲從機 slave,主機主要用於數據的寫入,從機則主要提供數據的讀取。從機在啓動時會同步全量主機數據,主機也會在寫入數據的時候同步到全部的從機。

有兩種方式能夠設置主從關係;

  1. 在啓動配置文件指定 replicaof 參數;
  2. 啓動Redis實例後執行replicaof ip port命令;

簡單測試,複製 redis.conf 文件,主要配置以下:

master:

port 6379
logfile "6379.log"
dbfilename "dump-6379.rdb"

slave_1:

port 6380
logfile "6380.log"
dbfilename "dump-6380.rdb"
replicaof 127.0.0.1 6379

slave_2:

port 6381
logfile "6381.log"
dbfilename "dump-6381.rdb"
replicaof 127.0.0.1 6379

slave_3:

port 6382
logfile "6382.log"
dbfilename "dump-6382.rdb"
replicaof 127.0.0.1 6379

依次啓動上述四個Redis實例;

./redis-server 6379.conf
./redis-server 6380.conf
./redis-server 6381.conf
./redis-server 6382.conf

鏈接6379主機master,查看replication信息;

127.0.0.1:6379> info replication
# Replication
role:master
connected_slaves:3
slave0:ip=127.0.0.1,port=6380,state=online,offset=322,lag=0
slave1:ip=127.0.0.1,port=6381,state=online,offset=322,lag=1
slave2:ip=127.0.0.1,port=6382,state=online,offset=322,lag=0
master_replid:417b1e3811a2d9b3465876d65c67a36949de8f9f
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:322
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:322

說明了當前 Redis 實例爲主機,有三個從機;

在當前主機寫入數據;

127.0.0.1:6379> set msg "hello world"
OK

在其餘任意從機執行獲取操做;

127.0.0.1:6382> get msg
"hello world"

已經成功設置主從同步。

9.2 哨兵模式

主從模式存在必定的弊端,master 一旦發生宕機,主從同步過程將會中斷。

Sentinel(哨兵)做爲一個單獨的服務,用來監控 master 主機,間接監控全部 slave 從機,以下圖所示;

sentinel 主要有如下三個特色;

  1. 監控 Redis 實例是否正常運行;
  2. 節點發生故障,可以通知另外;

當master發生故障,sentinel 會採用在當前 sentinel 集羣中投票方式,從當前全部 slave 中,推舉一個做爲新的master,從而保證了 Redis 的高可用性。

9.3 集羣模式

在哨兵模式下,每一個 Redis 實例都是存儲的全量數據。爲了最大化利用內存空間,採用集羣模式,即分佈式存儲,每臺 Redis 存儲不一樣的內容。Redis 集羣沒有使用一致性hash,而是引入了哈希槽的概念 。

數據存儲在 16384 個 slot(插槽)中,全部的數據都是根據必定算法映射到某個 slot 中;

爲何是 16384: https://github.com/antirez/re...

集羣模式至少三個Redis節點,不然會提示:

./redis-cli --cluster create 127.0.0.1:7000 127.0.0.1:7001
*** ERROR: Invalid configuration for cluster creation.
*** Redis Cluster requires at least 3 master nodes.
*** This is not possible with 2 nodes and 0 replicas per node.
*** At least 3 nodes are required.

在src目錄建立confs文件夾,複製redis.conf文件6分,三主三從;

主要配置以下;

port 7000
cluster-enabled yes
cluster-node-timeout 15000
cluster-config-file "nodes-7000.conf"
pidfile /var/run/redis-7000.pid
logfile "cluster-7000.log"
dbfilename dump-cluster-7000.rdb
appendfilename "appendonly-cluster-7000.aof"

順序啓動相關Redis示例,最後建立集羣;

./redis-server confs/7000.conf
./redis-server confs/7001.conf
./redis-server confs/7002.conf
./redis-server confs/7003.conf
./redis-server confs/7004.conf
./redis-server confs/7005.conf
./redis-cli --cluster create 127.0.0.1:7000 127.0.0.1:7001 127.0.0.1:7002 127.0.0.1:7003 127.0.0.1:7004 127.0.0.1:7005 --cluster-replicas 1

控制檯輸出建立集羣信息:

>>> Performing hash slots allocation on 6 nodes...
Master[0] -> Slots 0 - 5460
Master[1] -> Slots 5461 - 10922
Master[2] -> Slots 10923 - 16383
Adding replica 10.242.24.246:7003 to 10.242.24.246:7000
Adding replica 10.242.24.246:7004 to 10.242.24.246:7001
Adding replica 10.242.24.246:7005 to 10.242.24.246:7002
>>> Trying to optimize slaves allocation for anti-affinity
[WARNING] Some slaves are in the same host as their master
M: bbb45e488e5679b79dd077f97803304534793420 10.242.24.246:7000
   slots:[0-5460] (5461 slots) master
M: b490121213e22451a9b788755b0be0d3bf158cda 10.242.24.246:7001
   slots:[5461-10922] (5462 slots) master
M: 000f55716f8e9f2c635744999a49425bcc65595d 10.242.24.246:7002
   slots:[10923-16383] (5461 slots) master
S: 17611ff6f3dffbfab60ce4ae7b7991a9ae280bcd 10.242.24.246:7003
   replicates b490121213e22451a9b788755b0be0d3bf158cda
S: 950a5a467ccb6af3280b67a3f2ce2e3fa7510bd8 10.242.24.246:7004
   replicates 000f55716f8e9f2c635744999a49425bcc65595d
S: c15ce5e96e69a1d93c5a71953ee044af6b2bd560 10.242.24.246:7005
   replicates bbb45e488e5679b79dd077f97803304534793420
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 10.242.24.246:7000)
M: bbb45e488e5679b79dd077f97803304534793420 10.242.24.246:7000
   slots:[0-5460] (5461 slots) master
   1 additional replica(s)
S: c15ce5e96e69a1d93c5a71953ee044af6b2bd560 10.242.24.246:7005
   slots: (0 slots) slave
   replicates bbb45e488e5679b79dd077f97803304534793420
M: b490121213e22451a9b788755b0be0d3bf158cda 10.242.24.246:7001
   slots:[5461-10922] (5462 slots) master
   1 additional replica(s)
S: 950a5a467ccb6af3280b67a3f2ce2e3fa7510bd8 10.242.24.246:7004
   slots: (0 slots) slave
   replicates 000f55716f8e9f2c635744999a49425bcc65595d
M: 000f55716f8e9f2c635744999a49425bcc65595d 10.242.24.246:7002
   slots:[10923-16383] (5461 slots) master
   1 additional replica(s)
S: 17611ff6f3dffbfab60ce4ae7b7991a9ae280bcd 10.242.24.246:7003
   slots: (0 slots) slave
   replicates b490121213e22451a9b788755b0be0d3bf158cda
[OK] All nodes agree about slots configuration.
>>> Check for open slots...
>>> Check slots coverage...
[OK] All 16384 slots covered.

集羣部署成功後,鏈接7000這個節點,注意鏈接命令:

./redis-cli -c -p 7000
127.0.0.1:7000> get name
-> Redirected to slot [5798] located at 127.0.0.1:7001
(nil)

9.3.1 添加節點

假如當前集羣爲 7000, 7001, 7002 三個節點,正確配置啓動新節點 7003 後執行命令:

[root@localhost redis-conf]# redis-cli --cluster add-node 127.0.0.1:7003 127.0.0.1:7000
>>> Adding node 127.0.0.1:7003 to cluster 127.0.0.1:7000
>>> Performing Cluster Check (using node 127.0.0.1:7000)
M: 9de886a23be8bc92bbe51a4e73ad27d2fb96df8d 127.0.0.1:7000
   slots:[0-5460] (5461 slots) master
M: cba883e361f23f1415e4d94148c7c26900c28111 127.0.0.1:7001
   slots:[5461-10922] (5462 slots) master
M: 3855e27b1ec68d6481d6d308101fb28dd6ed21df 127.0.0.1:7002
   slots:[10923-16383] (5461 slots) master
[OK] All nodes agree about slots configuration.
>>> Check for open slots...
>>> Check slots coverage...
[OK] All 16384 slots covered.
>>> Send CLUSTER MEET to node 127.0.0.1:7003 to make it join the cluster.
[OK] New node added correctly.

添加的新節點沒有分配 slots,須要手動分配:

[root@localhost redis-conf]# redis-cli --cluster reshard 127.0.0.1:7000
>>> Performing Cluster Check (using node 127.0.0.1:7000)
M: 9de886a23be8bc92bbe51a4e73ad27d2fb96df8d 127.0.0.1:7000
   slots:[0-5460] (5461 slots) master
M: cba883e361f23f1415e4d94148c7c26900c28111 127.0.0.1:7001
   slots:[5461-10922] (5462 slots) master
M: 28eedd55e0fd8e35d36766055b720418c14fa04a 127.0.0.1:7003
   slots: (0 slots) master
M: 3855e27b1ec68d6481d6d308101fb28dd6ed21df 127.0.0.1:7002
   slots:[10923-16383] (5461 slots) master
[OK] All nodes agree about slots configuration.
>>> Check for open slots...
>>> Check slots coverage...
[OK] All 16384 slots covered.
How many slots do you want to move (from 1 to 16384)? 100
What is the receiving node ID? 28eedd55e0fd8e35d36766055b720418c14fa04a 
Please enter all the source node IDs.
  Type 'all' to use all the nodes as source nodes for the hash slots.
  Type 'done' once you entered all the source nodes IDs.
Source node #1: 9de886a23be8bc92bbe51a4e73ad27d2fb96df8d 
Source node #2: done

Ready to move 100 slots.
  Source nodes:
    M: 9de886a23be8bc92bbe51a4e73ad27d2fb96df8d 127.0.0.1:7000
       slots:[0-5460] (5461 slots) master
  Destination node:
    M: 28eedd55e0fd8e35d36766055b720418c14fa04a 127.0.0.1:7003
       slots: (0 slots) master
  Resharding plan:
    ...
    ...
Do you want to proceed with the proposed reshard plan (yes/no)? yes
  ...
  ...

節點檢查:

[root@localhost redis-conf]# redis-cli --cluster info 127.0.0.1:7000
127.0.0.1:7000 (9de886a2...) -> 0 keys | 5361 slots | 0 slaves.
127.0.0.1:7001 (cba883e3...) -> 0 keys | 5462 slots | 0 slaves.
127.0.0.1:7003 (28eedd55...) -> 0 keys | 100 slots | 0 slaves.
127.0.0.1:7002 (3855e27b...) -> 0 keys | 5461 slots | 0 slaves.

9.3.2 刪除節點

首先將該節點的 slots 轉移(同新增),而後執行刪除節點操做:

[root@localhost redis-conf]# redis-cli --cluster del-node 127.0.0.1:7003 28eedd55e0fd8e35d36766055b720418c14fa04a
>>> Removing node 28eedd55e0fd8e35d36766055b720418c14fa04a from cluster 127.0.0.1:7003
>>> Sending CLUSTER FORGET messages to the cluster...
>>> SHUTDOWN the node.

9.4 分佈式鎖

場景:定時任務集羣部署,Job 須要加鎖單次執行;

方案:基於 Redis 實現分佈式鎖,以 Job 惟一標識爲 key,設置 expiration,在 Job 執行前獲取鎖斷定;

優勢:實現較爲簡單,過時策略防止死鎖,效率較高;

基於 springboot 2.x 項目,參考代碼以下;

加鎖:

/**
  * 嘗試加鎖
  *
  * @param lockKey    加鎖的KEY
  * @param requestId  加鎖客戶端惟一ID標識
  * @param expireTime 過時時間
  * @param timeUnit   時間單位
  * @return 是否加鎖成功
  */
public Boolean tryLock(String lockKey, String requestId, long expireTime, TimeUnit timeUnit) {
    RedisConnection connection = connectionFactory.getConnection();
    Boolean result = connection.set(lockKey.getBytes(StandardCharsets.UTF_8), requestId.getBytes(StandardCharsets.UTF_8), Expiration.from(expireTime, timeUnit), RedisStringCommands.SetOption.SET_IF_ABSENT);
    connection.close();
    return result;
}

requestId 一般用做標識加鎖請求的惟一性,只有對應的加鎖請求,才能成功解鎖。防止某個客戶端操做阻塞好久,鎖超時自動釋放被另外客戶端拿到,而後本身又執行釋放鎖釋放掉其餘客戶端當前持有的鎖。

解鎖:

/**
  * 釋放鎖
  *
  * @param lockKey   加鎖的KEY
  * @param requestId 加鎖客戶端惟一ID標識
  * @return 是否釋放成功
  */
public boolean releaseLock(String lockKey, String requestId) {
    // Lua代碼,一次性執行保證原子性,避免併發問題
    String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
    RedisConnection connection = connectionFactory.getConnection();
    byte[][] keysAndArgs = new byte[2][];
    keysAndArgs[0] = lockKey.getBytes(StandardCharsets.UTF_8);
    keysAndArgs[1] = requestId.getBytes(StandardCharsets.UTF_8);
    Long result = connection.scriptingCommands().eval(script.getBytes(StandardCharsets.UTF_8), ReturnType.INTEGER, 1, keysAndArgs);
    connection.close();
    return result != null && result > 0;
}

注意解鎖姿式,保證操做原子性。

9.4.1 鎖超時

當鎖的持有時間沒法估算,存在鎖超時致使被自動釋放掉的可能。能夠在獲取鎖成功時,開啓一個定時線程詢問持有鎖情況,若當前仍持有鎖狀態,則刷新過時時間。

參考 Redisson 實現:https://github.com/redisson/redisson/blob/master/redisson/src/main/java/org/redisson/RedissonLock.java (renewExpiration)

9.4.2 RedLock

主從複製時,獲取鎖成功還未同步 slave 時,master 宕機會出現數據不一致狀況。

官方提供名爲 RedLock 的算法思想:

  1. 獲取當前時間;
  2. 嘗試按順序在 N 個節點獲取鎖;
  3. 在大多數節點獲取鎖成功,則認爲成功;
  4. 若是鎖獲取成功了,鎖有效時間就是最初的鎖有效時間減去以前獲取鎖所消耗的時間;
  5. 若是鎖獲取失敗了,將會嘗試釋放全部節點的鎖;

Redlock 算法: https://redis.io/topics/distlock

10 緩存雪崩、緩存穿透和緩存擊穿

10.1 緩存雪崩

描述:同一時間緩存大面積失效,數量級的請求直接打到數據庫。

方案:給緩存失效時間加上一個隨機數。

10.2 緩存穿透

描述:請求不符合緩存條件,直接打到數據庫。

方案:參數作好校驗,null 值也可緩存。

10.3 緩存擊穿

描述:熱點數據失效瞬間,大量對該熱點數據的請求直接打到數據庫。

方案:設置緩存永不過時,或者查詢引入互斥鎖。

相關文章
相關標籤/搜索