對於生產環境,高可用是避免不了要面對的問題,不管什麼環境、服務,只要用於生產,就須要知足高可用;此文針對的是redis的高可用。html
接下來會有系列文章,該系列是對spring-session實現分佈式集羣session的共享的完整闡述,同時也引申出緩存的實現;而此篇是該系列的第一篇。java
github地址:https://github.com/youzhibing/redislinux
redis版本:redis-3.0.0git
linux:centos6.7github
ip:192.168.11.202, 一臺服務器上搭建搭建所有redis實例,包括數據節點實例以及哨兵(sentinel)實例redis
客戶端jedis,基於spring-bootspring
搭建一主二從的主從環境apache
安裝很簡單,網上資料不少,redis官網也有說明;主要就是3步:解壓,make,make installcentos
redis解壓後,redis home目錄下有redis配置的樣例文件,咱們不直接在此文件上就行修改,在redis home目錄下新建文件夾master_slave,將配置文件都放於此目錄下緩存
master配置文件:redis-6379.conf
port 6379 bind 192.168.11.202 requirepass "myredis" daemonize yes logfile "6379.log" dbfilename "dump-6379.rdb" dir "/opt/soft/redis/data" #如若master設置了認證密碼,那麼全部redis數據節點都配置上masterauth屬性 masterauth "myredis"
slave-1配置文件:redis-6380.conf
port 6380 bind 192.168.11.202 requirepass "myredis" daemonize yes logfile "6380.log" dbfilename "dump-6380.rdb" dir "/opt/soft/redis/data" #如若master設置了認證密碼,那麼全部redis數據節點都配置上masterauth屬性 masterauth "myredis" slaveof 192.168.11.202 6379
slave-2配置文件:redis-6381.conf
port 6381 bind 192.168.11.202 requirepass "myredis" daemonize yes logfile "6381.log" dbfilename "dump-6381.rdb" dir "/opt/soft/redis/data" #如若master設置了認證密碼,那麼全部redis數據節點都配置上masterauth屬性 masterauth "myredis" slaveof 192.168.11.202 6379
以下相關路徑須要根據本身的狀況進行改動,可能和個人不同
[root@slave1 master_slave]# cd /opt/redis-3.0.0/master_slave/ [root@slave1 master_slave]# ./../src/redis-server redis-6379.conf [root@slave1 master_slave]# ./../src/redis-server redis-6380.conf [root@slave1 master_slave]# ./../src/redis-server redis-6381.conf
確認主從關係
[root@slave1 master_slave]# ./../src/redis-cli -h 192.168.11.202 -p 6379 -a myredis info replication # Replication role:master connected_slaves:2 slave0:ip=192.168.11.202,port=6380,state=online,offset=393,lag=0 slave1:ip=192.168.11.202,port=6381,state=online,offset=393,lag=0 master_repl_offset:393 repl_backlog_active:1 repl_backlog_size:1048576 repl_backlog_first_byte_offset:2 repl_backlog_histlen:392
還能夠從從節點視角來看:[root@slave1 master_slave]# ./../src/redis-cli -h 192.168.11.202 -p 6380 -a myredis info replication
如若進行順利,按如上配置,一主二從環境搭建完畢
主要兩個做用:一、做爲主節點的一個備份,一旦主節點出現故障,從節點能夠做爲後備"頂"上來,而且保證數據儘可能不丟失(主從複製是最終一致性);二、從節點能夠拓展主節點的能力,一旦主節點不能支撐大併發的讀操做,從節點能夠在必定程度上幫助主節點分擔讀壓力
一、一旦主節點出現故障,須要手動將一個從節點晉升爲主節點,同時須要修改應用方的主節點地址,還須要命令其餘從節點去複製新的主節點,整個過程須要人工干預
二、主節點的寫能力受到單機的限制
三、主節點的存儲能力受到單機的限制
此片主要講經過sentinel解決上述問題1,問題二、3則在下篇博客進行說明
sentinel-26379.conf
port 26379 daemonize yes logfile "26379.log" dir "/opt/soft/redis/data" sentinel monitor mymaster 192.168.11.202 6380 2 #redis數據master節點設置了認證,則須要以下配置 sentinel auth-pass mymaster myredis sentinel down-after-milliseconds mymaster 30000 sentinel parallel-syncs mymaster 1 sentinel failover-timeout mymaster 180000
sentinel-26380.conf
port 26380 daemonize yes logfile "26379.log" dir "/opt/soft/redis/data" sentinel monitor mymaster 192.168.11.202 6380 2 #redis數據master節點設置了認證,則須要以下配置 sentinel auth-pass mymaster myredis sentinel down-after-milliseconds mymaster 30000 sentinel parallel-syncs mymaster 1 sentinel failover-timeout mymaster 180000
sentinel-26381.conf
port 26381 daemonize yes logfile "26379.log" dir "/opt/soft/redis/data" sentinel monitor mymaster 192.168.11.202 6380 2 #redis數據master節點設置了認證,則須要以下配置 sentinel auth-pass mymaster myredis sentinel down-after-milliseconds mymaster 30000 sentinel parallel-syncs mymaster 1 sentinel failover-timeout mymaster 180000
[root@slave1 master_slave]# ./../src/redis-sentinel sentinel-26379.conf [root@slave1 master_slave]# ./../src/redis-sentinel sentinel-26380.conf [root@slave1 master_slave]# ./../src/redis-sentinel sentinel-26381.conf
[root@slave1 master_slave]# ./../src/redis-cli -h 192.168.11.202 -p 26379 info Sentinel # Sentinel sentinel_masters:1 sentinel_tilt:0 sentinel_running_scripts:0 sentinel_scripts_queue_length:0 master0:name=mymaster,status=ok,address=192.168.11.202:6379,slaves=2,sentinels=3
Redis Sentinel 最終拓撲結構
手動停掉6379實例或者kill掉6379實例
[root@slave1 master_slave]# ./../src/redis-cli -h 192.168.11.202 -p 6379 -a myredis
192.168.11.202:6379> shutdown
查看26379.log
master節點已經自動切換到192.168.11.202:6380了,重啓6379,6379則是6378的從節點了。
[root@slave1 master_slave]# ./../src/redis-cli -h 192.168.11.202 -p 6380 -a myredis 192.168.11.202:6380> info replication # Replication role:master connected_slaves:2 slave0:ip=192.168.11.202,port=6381,state=online,offset=80802,lag=1 slave1:ip=192.168.11.202,port=6379,state=online,offset=80802,lag=1 master_repl_offset:80945 repl_backlog_active:1 repl_backlog_size:1048576 repl_backlog_first_byte_offset:2 repl_backlog_histlen:80944
基於spring-boot開發,spring-boot-test測試, 這二者本文不作說明,網上資料不少,不熟悉的自行去補充; 工程結構以下圖
pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.lee</groupId> <artifactId>redis</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>jar</packaging> <name>redis</name> <url>http://maven.apache.org</url> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>1.5.9.RELEASE</version> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <!-- 日誌過濾器 --> <dependency> <groupId>org.codehaus.janino</groupId> <artifactId>janino</artifactId> </dependency> </dependencies> </project>
redis-sentinel.properties
redis.masterName=mymaster redis.sentinels=192.168.11.202:26379,192.168.11.202:26380,192.168.11.202:26381 redis.timeout=10000 #鏈接master須要用到的密碼,若是redis數據節點開啓了鏈接認證 redis.password=myredis # 鏈接池 # 鏈接池最大鏈接數(使用負值表示沒有限制) redis.pool.maxActive=150 # 鏈接池中的最大空閒鏈接 redis.pool.maxIdle=10 # 鏈接池中的最小空閒鏈接 redis.pool.minIdle=1 # 獲取鏈接時的最大等待毫秒數,小於零:阻塞不肯定的時間,默認-1 redis.pool.maxWaitMillis=3000 # 每次釋放鏈接的最大數目 redis.pool.numTestsPerEvictionRun=50 # 釋放鏈接的掃描間隔(毫秒) redis.pool.timeBetweenEvictionRunsMillis=3000 # 鏈接最小空閒時間(毫秒) redis.pool.minEvictableIdleTimeMillis=1800000 # 鏈接空閒多久後釋放, 當空閒時間>該值 且 空閒鏈接>最大空閒鏈接數 時直接釋放(毫秒) redis.pool.softMinEvictableIdleTimeMillis=10000 # 在獲取鏈接的時候檢查有效性, 默認false redis.pool.testOnBorrow=true # 在空閒時檢查有效性, 默認false redis.pool.testWhileIdle=true # 在歸還給pool時,是否提早進行validate操做 redis.pool.testOnReturn=true # 鏈接耗盡時是否阻塞, false報異常,ture阻塞直到超時, 默認true redis.pool.blockWhenExhausted=true
RedisConfig.java
package com.lee.redis.config; import java.util.Arrays; import java.util.HashSet; import java.util.Set; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.PropertySource; import redis.clients.jedis.JedisPoolConfig; import redis.clients.jedis.JedisSentinelPool; @Configuration @PropertySource("redis/redis-sentinel.properties") public class RedisConfig { private static final Logger LOGGER = LoggerFactory.getLogger(RedisConfig.class); @Value("${redis.masterName}") private String masterName; @Value("${redis.sentinels}") private String sentinels; @Value("${redis.timeout}") private int timeout; @Value("${redis.password}") private String password; @Value("${redis.pool.maxActive}") private int maxTotal; @Value("${redis.pool.maxIdle}") private int maxIdle; @Value("${redis.pool.minIdle}") private int minIdle; @Value("${redis.pool.maxWaitMillis}") private long maxWaitMillis; @Value("${redis.pool.numTestsPerEvictionRun}") private int numTestsPerEvictionRun; @Value("${redis.pool.timeBetweenEvictionRunsMillis}") private long timeBetweenEvictionRunsMillis; @Value("${redis.pool.minEvictableIdleTimeMillis}") private long minEvictableIdleTimeMillis; @Value("${redis.pool.softMinEvictableIdleTimeMillis}") private long softMinEvictableIdleTimeMillis; @Value("${redis.pool.testOnBorrow}") private boolean testOnBorrow; @Value("${redis.pool.testWhileIdle}") private boolean testWhileIdle; @Value("${redis.pool.testOnReturn}") private boolean testOnReturn; @Value("${redis.pool.blockWhenExhausted}") private boolean blockWhenExhausted; @SuppressWarnings({ "unchecked", "rawtypes" }) @Bean public JedisSentinelPool jedisSentinelPool(JedisPoolConfig poolConfig) { LOGGER.info("sentinel set : {}", sentinels); Set sentinelSet = new HashSet(Arrays.asList(sentinels.split(","))); JedisSentinelPool jedisPool = new JedisSentinelPool(masterName, sentinelSet, poolConfig, timeout, password); return jedisPool; } @Bean public JedisPoolConfig jedisPoolConfig() { JedisPoolConfig jedisPoolConfig = new JedisPoolConfig(); jedisPoolConfig.setMaxTotal(maxTotal); jedisPoolConfig.setMaxIdle(maxIdle); jedisPoolConfig.setMinIdle(minIdle); jedisPoolConfig.setMaxWaitMillis(maxWaitMillis); jedisPoolConfig.setNumTestsPerEvictionRun(numTestsPerEvictionRun); jedisPoolConfig .setTimeBetweenEvictionRunsMillis(timeBetweenEvictionRunsMillis); jedisPoolConfig .setMinEvictableIdleTimeMillis(minEvictableIdleTimeMillis); jedisPoolConfig .setSoftMinEvictableIdleTimeMillis(softMinEvictableIdleTimeMillis); jedisPoolConfig.setTestOnBorrow(testOnBorrow); jedisPoolConfig.setTestWhileIdle(testWhileIdle); jedisPoolConfig.setTestOnReturn(testOnReturn); jedisPoolConfig.setBlockWhenExhausted(blockWhenExhausted); return jedisPoolConfig; } }
Application.java
package com.lee.redis; import org.springframework.boot.Banner; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; @Configuration @EnableAutoConfiguration @ComponentScan public class Application { public static void main(String[] args) { SpringApplication app = new SpringApplication(Application.class); app.setBannerMode(Banner.Mode.OFF); // 是否打印banner // app.setApplicationContextClass(); // 指定spring應用上下文啓動類 app.setWebEnvironment(false); app.run(args); } }
RedisTest.java
package com.lee.redis; import org.junit.Test; import org.junit.runner.RunWith; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.junit4.SpringRunner; import redis.clients.jedis.Jedis; import redis.clients.jedis.JedisSentinelPool; @RunWith(SpringRunner.class) @SpringBootTest(classes = Application.class) public class RedisTest { private static final Logger LOGGER = LoggerFactory.getLogger(RedisTest.class); @Autowired private JedisSentinelPool sentinelPool; @Test public void getNameTest() { Jedis jedis = null; try { jedis = sentinelPool.getResource(); String name = jedis.get("name"); System.out.println("name is " + name); } catch(Exception e) { LOGGER.error(e.getMessage(), e); } finally { if (jedis != null) { jedis.close(); } } } }
更多詳情請上個人github
運行RedisTest.java的getNameTest方法(name屬性已經在redis中設置,沒設置的須要提早設置),獲得結果:
一、有人可能會有這樣的疑問:爲何經過sentinel來獲取redis的鏈接,而不是直接鏈接master來獲取redis鏈接呢?
試想一下,客戶端直接經過master節點獲取redis鏈接,若是master節點掛掉了,雖然Redis Sentinel能夠完成故障轉移,可是客戶端沒法獲取這個變化,那麼客戶端就沒法獲取redis鏈接了;
最瞭解master節點信息的就是Sentinel節點集合,因此經過sentinel來獲取redis鏈接就能知足高可用的要求了。
二、redis master的故障轉移不影響客戶端鏈接代碼, 可是轉移期間內,經過sentinel是獲取不到主節點的鏈接的, 由於轉移期間內master節點還沒被選舉出來;
《Redis開發與運維》
http://www.redis.cn/topics/sentinel.html