Redis Cluster是Redis用於解決分散式的方案,又稱為Redis叢集
是一個讓數據再多個Node之間相互傳輸的服務,Redis Cluster的優勢主要有以下兩個點
- 自動分割數據到不同Node
- 部分Node當機或不可用情況能夠繼續維持服務
不過Redis Cluster並不支持處理多個keys的命令,在不同Node移動數據並不像Redis那樣高性能,高負載時有可能會遇到不可預期錯誤
Redis Cluster並非採用Consistent Hashing而是用Hash Slots,它其實就是代表一個Keys的集合
Redis Cluster共有16384 Hash Slots,將Key做CRC16校驗接著Mod 16384決定Key會放置於哪個Slot,Redis Cluster每個Node負責一部分的Hash Slots。例如,目前Cluster中有三個Master Node,則:
- Node A包含0到3000 Hash Slots
- Node B包含3001到9000 Hash Slots
- Node C包含9001到16383 Hash Slots
如果新增了Node D,僅需將原本Node上的Hash Slots添加至Node D上;相對的要刪除一個Node A,將Node A上的Hash Slots遷移至Node B / C / D上,再將沒有任何Hash Slots的Node A移除即可;且新增、刪除或搬遷Hash Slots時無須停止任何服務。
Redis Cluster為了使部分Node失敗或大部分的Node無法通訊時仍可以使用,Redis Cluster使用Master-Slave複製模型,則每個Master Node則最少有會1個Slave Node;以上述的例子而言,如果沒有使用該模型的情況下,Node A失效則會導致Cluster因0到3000的Hash Slots不可用而失效。
最後Redis Cluster並不保證數據的一致性,這也意味著Redis Cluster在特定條件下有可能會丟失數據。
讓我們開始來建置Redis Cluster吧
接著我們開始來建立Redis Cluster吧,根據官方文獻建置一個Redis Cluster最少需要三個Master,也意味著還需要三個Slave來確保每個Master失效時才能將角色轉移至Slave上。
所以需先準備6台Host或container,系統為CentOS 7 minimal
Name | IP | Port | Cluster BUS Port |
Master 1 | 192.168.126.135 | 6379 | 16379 |
Master 2 | 192.168.126.136 | 6380 | 16380 |
Master 3 | 192.168.126.141 | 6381 | 16381 |
Slave 1 | 192.168.126.142 | 6382 | 6382 |
Slave 2 | 192.168.126.143 | 6383 | 6383 |
Slave 3 | 192.168.126.144 | 6384 | 6384 |
安裝教學請參考「Install Redis Server on CentOS 7」
Master與Slave配置如下
# Master 1配置
bind 192.168.126.135 127.0.0.1
port 6379
protected-mode yes
daemonize yes
supervised systemd
pidfile /var/run/redis_6379.pid
masterauth password
requirepass password
cluster-enabled yes
cluster-node-timeout 15000
cluster-config-file nodes-6379.conf
cluster-slave-validity-factor 10
cluster-migration-barrier 1
cluster-require-full-coverage no
# Master 2配置
bind 192.168.126.136 127.0.0.1
port 6380
protected-mode yes
daemonize yes
supervised systemd
pidfile /var/run/redis_6380.pid
masterauth password
requirepass password
cluster-enabled yes
cluster-node-timeout 15000
cluster-config-file nodes-6380.conf
cluster-slave-validity-factor 10
cluster-migration-barrier 1
cluster-require-full-coverage no
# Master 3配置
bind 192.168.126.141 127.0.0.1
port 6381
protected-mode yes
daemonize yes
supervised systemd
pidfile /var/run/redis_6381.pid
masterauth password
requirepass password
cluster-enabled yes
cluster-node-timeout 15000
cluster-config-file nodes-6381.conf
cluster-slave-validity-factor 10
cluster-migration-barrier 1
cluster-require-full-coverage no
# Slave 1配置
bind 192.168.126.142 127.0.0.1
port 6382
protected-mode yes
daemonize yes
supervised systemd
pidfile /var/run/redis_6382.pid
masterauth password
requirepass password
cluster-enabled yes
cluster-node-timeout 15000
cluster-config-file nodes-6382.conf
cluster-slave-validity-factor 10
cluster-migration-barrier 1
cluster-require-full-coverage no
# Slave 2配置
bind 192.168.126.143 127.0.0.1
port 6383
protected-mode yes
daemonize yes
supervised systemd
pidfile /var/run/redis_6383.pid
masterauth password
requirepass password
cluster-enabled yes
cluster-node-timeout 15000
cluster-config-file nodes-6383.conf
cluster-slave-validity-factor 10
cluster-migration-barrier 1
cluster-require-full-coverage no
# Slave 3配置
bind 192.168.126.144 127.0.0.1
port 6384
protected-mode yes
daemonize yes
supervised systemd
pidfile /var/run/redis_6384.pid
masterauth password
requirepass password
cluster-enabled yes
cluster-node-timeout 15000
cluster-config-file nodes-6384.conf
cluster-slave-validity-factor 10
cluster-migration-barrier 1
cluster-require-full-coverage no
防火牆規則:
firewall-cmd --add-port=6379/tcp --permanent
firewall-cmd --add-port=6380/tcp --permanent
firewall-cmd --add-port=6381/tcp --permanent
firewall-cmd --add-port=6382/tcp --permanent
firewall-cmd --add-port=6383/tcp --permanent
firewall-cmd --add-port=6384/tcp --permanent
firewall-cmd --add-port=16379/tcp --permanent
firewall-cmd --add-port=16380/tcp --permanent
firewall-cmd --add-port=16381/tcp --permanent
firewall-cmd --add-port=16382/tcp --permanent
firewall-cmd --add-port=16383/tcp --permanent
firewall-cmd --add-port=16384/tcp --permanent
firewall-cmd --reload
透過redis-rb或redis-cli讓Node互相Handshake
不過依稀記得redis-rb輸入完就設定完成了
redis-cli則是需一個命令一個命令接著下
redis-rb:
redis-trib create --replicas 1 192.168.126.135:6379 192.168.126.142:6382 192.168.126.136:6380 192.168.126.133:6383 192.168.126.141:6381 192.168.126.144:6384
redis-cli:
redis-cli -h 192.168.126.135 -p 6379 -a password -c CLUSTER meet 192.168.126.136 6380
redis-cli -h 192.168.126.135 -p 6379 -a password -c CLUSTER meet 192.168.126.141 6381
redis-cli -h 192.168.126.135 -p 6379 -a password -c CLUSTER meet 192.168.126.142 6382
redis-cli -h 192.168.126.135 -p 6379 -a password -c CLUSTER meet 192.168.126.143 6383
redis-cli -h 192.168.126.135 -p 6379 -a password -c CLUSTER meet 192.168.126.144 6384
透過下方指令來查看目前狀態
備註:
Handshake失敗原因大部分都是Redis Port / Cluster BUS port其中一個沒有連接導致Handshake失敗
Cluster BUS port = Redis Port + 10000
redis-cli –h 192.168.126.135 -p 6379 –a password –c cluster nodes
分配Hash Slots
redis-cli -h 192.168.126.135 -p 6379 -a password -c CLUSTER ADDSLOTS {0..5461}
redis-cli -h 192.168.126.136 -p 6380 -a password -c CLUSTER ADDSLOTS {5462..10922}
redis-cli -h 192.168.126.141 -p 6381 -a password -c CLUSTER ADDSLOTS {10923..16383}
沒有分配完會如下圖
分配完成如下圖
配置都沒問題,就輸入下方指令保存至每個Node config
redis-cli -h 192.168.126.135 -p 6379 -a password -c CLUSTER SAVECONFIG
要測試角色切換最簡單的方式就是停止服務
systemctl stop redis
我將135停止服務
142就會變成Master,並將該訊息通知給其他Node
等135重啟後變成Slave角色
Java程式碼:
package cy.com;
import com.sun.istack.internal.NotNull;
import redis.clients.jedis.HostAndPort;
import redis.clients.jedis.JedisCluster;
import redis.clients.jedis.JedisPoolConfig;
import java.io.IOException;
import java.io.InputStream;
import java.util.*;
class RedisCluster {
/**Map*/
private static Map<String, Object> map;
/**
* @see JedisCluster
*/
private JedisCluster jedisCluster;
/**
* @see JedisPoolConfig
*/
private static JedisPoolConfig getJedisPoolConfig() {
JedisPoolConfig config = new JedisPoolConfig();
config.setMaxTotal(Integer.parseInt(RedisCluster.map.get(RedisInfo.REDIS_POOL_MAXACTIVE).toString()));
config.setMinIdle(Integer.parseInt(RedisCluster.map.get(RedisInfo.REDIS_POOL_MIN_IDLE).toString()));
config.setMaxIdle(Integer.parseInt(RedisCluster.map.get(RedisInfo.REDIS_POOL_MAX_IDLE).toString()));
config.setMaxWaitMillis(Long.parseLong(RedisCluster.map.get(RedisInfo.REDIS_POOL_MAX_WAIT_MILLIS).toString()));
config.setNumTestsPerEvictionRun(Integer.parseInt(RedisCluster.map.get(RedisInfo.REDIS_POOL_NUM_TESTS_PER_EVICTION_RUN).toString()));
config.setTimeBetweenEvictionRunsMillis(Long.parseLong(RedisCluster.map.get(RedisInfo.REDIS_POOL_TIME_BETWEEN_EVICTION_RUNS_MILLIS).toString()));
config.setMinEvictableIdleTimeMillis(Long.parseLong(RedisCluster.map.get(RedisInfo.REDIS_POOL_MIN_EVICTABLE_IDLE_TIME_MILLIS).toString()));
config.setSoftMinEvictableIdleTimeMillis(Long.parseLong(RedisCluster.map.get(RedisInfo.REDIS_POOL_SOFT_MIN_EVICTABLE_IDLE_TIME_MILLIS).toString()));
config.setTestOnBorrow(Boolean.parseBoolean(RedisCluster.map.get(RedisInfo.REDIS_POOL_TEST_ON_BORROW).toString()));
config.setTestWhileIdle(Boolean.parseBoolean(RedisCluster.map.get(RedisInfo.REDIS_POOL_TEST_WHILE_IDLE).toString()));
config.setTestOnReturn(Boolean.parseBoolean(RedisCluster.map.get(RedisInfo.REDIS_POOL_TEST_ON_RETURN).toString()));
config.setBlockWhenExhausted(Boolean.parseBoolean(RedisCluster.map.get(RedisInfo.REDIS_POOL_BLOCK_WHEN_EXHAUSTED).toString()));
return config;
}
/**
* get Cluster host and port
*
* @return HostAndPort
*/
private static Set<HostAndPort> getHostAndPortSet() {
Set<HostAndPort> set = new HashSet<HostAndPort>();
String[] array = RedisCluster.map.get(RedisInfo.REDIS_CLUSTERS).toString().split(";");
for (String c : array) {
String[] hostAndPort = c.split(":");
set.add(new HostAndPort(hostAndPort[0], Integer.parseInt(hostAndPort[1])));
}
return set;
}
static {
if (RedisCluster.map == null) {
final InputStream inputStream = RedisInfo.class.getClassLoader()
.getResourceAsStream("Redis.properties");
final Properties properties = new Properties();
try {
properties.load(inputStream);
inputStream.close();
RedisCluster.map = new HashMap<String, Object>();
//Master Name
RedisCluster.map.put(RedisInfo.REDIS_MASTER_NAME, properties.getProperty(RedisInfo.REDIS_MASTER_NAME));
//Host
RedisCluster.map.put(RedisInfo.REDIS_HOST, properties.getProperty(RedisInfo.REDIS_HOST));
//Port
RedisCluster.map.put(RedisInfo.REDIS_PORT, properties.getProperty(RedisInfo.REDIS_PORT));
//Sentinels
RedisCluster.map.put(RedisInfo.REDIS_SENTINELS, properties.getProperty(RedisInfo.REDIS_SENTINELS));
//Clusters
RedisCluster.map.put(RedisInfo.REDIS_CLUSTERS, properties.getProperty(RedisInfo.REDIS_CLUSTERS));
//Timeout
RedisCluster.map.put(RedisInfo.REDIS_TIMEOUT, properties.getProperty(RedisInfo.REDIS_TIMEOUT));
//So Timeout
RedisCluster.map.put(RedisInfo.REDIS_SO_TIMEOUT, properties.getProperty(RedisInfo.REDIS_SO_TIMEOUT));
//Password
RedisCluster.map.put(RedisInfo.REDIS_PASSWORD, properties.getProperty(RedisInfo.REDIS_PASSWORD));
//Max attempts
RedisCluster.map.put(RedisInfo.REDIS_MAX_ATTEMPTS, properties.getProperty(RedisInfo.REDIS_MAX_ATTEMPTS));
//Max active
RedisCluster.map.put(RedisInfo.REDIS_POOL_MAXACTIVE, properties.getProperty(RedisInfo.REDIS_POOL_MAXACTIVE));
//Max idle
RedisCluster.map.put(RedisInfo.REDIS_POOL_MAX_IDLE, properties.getProperty(RedisInfo.REDIS_POOL_MAX_IDLE));
//Min idle
RedisCluster.map.put(RedisInfo.REDIS_POOL_MIN_IDLE, properties.getProperty(RedisInfo.REDIS_POOL_MIN_IDLE));
//Max wait millis
RedisCluster.map.put(RedisInfo.REDIS_POOL_MAX_WAIT_MILLIS, properties.getProperty(RedisInfo.REDIS_POOL_MAX_WAIT_MILLIS));
//Num tests per eviction run
RedisCluster.map.put(RedisInfo.REDIS_POOL_NUM_TESTS_PER_EVICTION_RUN, properties.getProperty(RedisInfo.REDIS_POOL_NUM_TESTS_PER_EVICTION_RUN));
//Time between eviction runs millis
RedisCluster.map.put(RedisInfo.REDIS_POOL_TIME_BETWEEN_EVICTION_RUNS_MILLIS, properties.getProperty(RedisInfo.REDIS_POOL_TIME_BETWEEN_EVICTION_RUNS_MILLIS));
//Min evictable idle time millis
RedisCluster.map.put(RedisInfo.REDIS_POOL_MIN_EVICTABLE_IDLE_TIME_MILLIS, properties.getProperty(RedisInfo.REDIS_POOL_MIN_EVICTABLE_IDLE_TIME_MILLIS));
//Soft min evictable idle time millis
RedisCluster.map.put(RedisInfo.REDIS_POOL_SOFT_MIN_EVICTABLE_IDLE_TIME_MILLIS, properties.getProperty(RedisInfo.REDIS_POOL_SOFT_MIN_EVICTABLE_IDLE_TIME_MILLIS));
//Test on borrow
RedisCluster.map.put(RedisInfo.REDIS_POOL_TEST_ON_BORROW, properties.getProperty(RedisInfo.REDIS_POOL_TEST_ON_BORROW));
//Test while idle
RedisCluster.map.put(RedisInfo.REDIS_POOL_TEST_WHILE_IDLE, properties.getProperty(RedisInfo.REDIS_POOL_TEST_WHILE_IDLE));
//Test on return
RedisCluster.map.put(RedisInfo.REDIS_POOL_TEST_ON_RETURN, properties.getProperty(RedisInfo.REDIS_POOL_TEST_ON_RETURN));
//Block when exhausted
RedisCluster.map.put(RedisInfo.REDIS_POOL_BLOCK_WHEN_EXHAUSTED, properties.getProperty(RedisInfo.REDIS_POOL_BLOCK_WHEN_EXHAUSTED));
} catch (IOException e) {
}
}
}
/**
* Redis Cluster Constructor
*/
public RedisCluster() {
//Timeout
final int TIMEOUT = Integer.parseInt(RedisCluster.map.get(RedisInfo.REDIS_TIMEOUT).toString());
//So Timeout
final int SO_TIMEOUT = Integer.parseInt(RedisCluster.map.get(RedisInfo.REDIS_SO_TIMEOUT).toString());
//Password
final String PASSWORD = RedisCluster.map.get(RedisInfo.REDIS_PASSWORD).toString();
//Max Attempts
final int MAX_ATTEMPTS = Integer.parseInt(RedisCluster.map.get(RedisInfo.REDIS_MAX_ATTEMPTS).toString());
//Nodes
final Set<HostAndPort> NODES = getHostAndPortSet();
//Config
final JedisPoolConfig CONFIG = getJedisPoolConfig();
//Setting Redis Cluster
this.jedisCluster = new JedisCluster(NODES, TIMEOUT, SO_TIMEOUT, MAX_ATTEMPTS, PASSWORD, CONFIG);
}
/**
* @param key Key
* @param value Value
*/
public String set(final @NotNull String key, final @NotNull String value) {
return this.jedisCluster.set(key, value);
}
/**
* @param key Key
* @see <a href="https://redis.io/commands/get">GET</a>
*/
public String get(@NotNull final String key) {
return this.jedisCluster.get(key);
}
/**
* Close connection
*/
public void close() throws IOException {
this.jedisCluster.close();
this.jedisCluster = null;
}
}
Redis.properties:
redis.masterName=mymaster
redis.host=192.168.126.135
redis.port=6379
redis.sentinels=192.168.126.135:26379;192.168.126.136:26379
redis.clusters=192.168.126.135:6379;192.168.126.136:6380;192.168.126.141:6381;192.168.126.142:6382;192.168.126.143:6383;192.168.126.144:6384
redis.timeout=10000
redis.sotimeout=10000
redis.password=password
redis.maxAttempts=5
redis.pool.maxActive=128
redis.pool.maxIdle=10
redis.pool.minIdle=1
redis.pool.maxWaitMillis=3000
redis.pool.numTestsPerEvictionRun=50
redis.pool.timeBetweenEvictionRunsMillis=3000
redis.pool.minEvictableIdleTimeMillis=1800000
redis.pool.softMinEvictableIdleTimeMillis=10000
redis.pool.testOnBorrow=true
redis.pool.testWhileIdle=true
redis.pool.testOnReturn=true
redis.pool.blockWhenExhausted=true