1. 添加依赖
以gradle为例:
dependencies {
compile(
"org.springframework.boot:spring-boot-starter-web:$springBootVersion",
"org.springframework.boot:spring-boot-starter-data-redis:${springBootVersion}",
"com.alibaba:fastjson:1.2.44",
)
}
2. 配置application.yml文件
server:
port: 8080
spring:
application:
name: redis-cluster-demo
#Redis Config
redis:
database: 0
timeout: 10000
pool:
maxIdle: 300
minIdle: 50
maxActive: 1000
cluster:
nodes: 172.16.0.15:8001,172.16.0.15:8002,172.16.0.15:8003,172.16.0.15:8004,172.16.0.15:8005,172.16.0.15:8006
connTimeOut: 1000 #连接server超时时间
soTimeOut: 1000 #等待response超时时间
maxAttempts: 2 #连接失败重试次数
3. 配置RedisCluster
package com.jason.redis.cluster.config;
import org.apache.commons.pool2.impl.GenericObjectPoolConfig;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Component;
import redis.clients.jedis.HostAndPort;
import redis.clients.jedis.JedisCluster;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
@Configuration
public class RedisConfig {
/**
* JedisPool相关配置
*/
@Component
@ConfigurationProperties(prefix = "spring.redis.pool")
public class JedisPoolConfigProp {
Integer maxIdle;
Integer minIdle;
Integer maxActive;
public Integer getMaxIdle() {
return maxIdle;
}
public void setMaxIdle(Integer maxIdle) {
this.maxIdle = maxIdle;
}
public Integer getMinIdle() {
return minIdle;
}
public void setMinIdle(Integer minIdle) {
this.minIdle = minIdle;
}
public Integer getMaxActive() {
return maxActive;
}
public void setMaxActive(Integer maxActive) {
this.maxActive = maxActive;
}
}
/**
* Cluster节点相关配置
*/
@Component
@ConfigurationProperties(prefix = "spring.redis.cluster")
public class ClusterConfigProp {
List<String> nodes;
public List<String> getNodes() {
return nodes;
}
public void setNodes(List<String> nodes) {
this.nodes = nodes;
}
}
/**
* Cluster相关配置
*/
@Configuration
public class RedisClusterConfig {
@Autowired
private ClusterConfigProp clusterConfigProp;
@Autowired
private JedisPoolConfigProp jedisPoolConfigProp;
@Bean
public JedisCluster jedisCluster() {
Set<HostAndPort> nodeSet = new HashSet<>();
for (String node : clusterConfigProp.getNodes()) {
String[] split = node.split(":");
nodeSet.add(new HostAndPort(split[0], Integer.valueOf(split[1])));
}
GenericObjectPoolConfig poolConfig = new GenericObjectPoolConfig();
poolConfig.setMaxTotal(jedisPoolConfigProp.getMaxActive());
poolConfig.setMaxIdle(jedisPoolConfigProp.getMaxIdle());
poolConfig.setMinIdle(jedisPoolConfigProp.getMinIdle());
JedisCluster jedisCluster = new JedisCluster(nodeSet, poolConfig);
return jedisCluster;
}
}
}
4. 编写测试controller
package com.jason.redis.cluster.controller;
import com.alibaba.fastjson.JSONObject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import redis.clients.jedis.JedisCluster;
import java.util.Date;
@RestController
@RequestMapping("test")
public class TestController {
@Autowired
private JedisCluster jedisCluster;
@GetMapping("/add/{key}")
@ResponseBody
public JSONObject addKey(@PathVariable String key) {
jedisCluster.set(key, new Date() + "");
JSONObject json = new JSONObject();
json.put("key", key);
json.put("value", jedisCluster.get(key));
return json;
}
@GetMapping("/del/{key}")
@ResponseBody
public JSONObject delKey(@PathVariable String key) {
jedisCluster.del(key);
JSONObject json = new JSONObject();
json.put(key + "是否存在?", jedisCluster.exists(key));
return json;
}
}
5. 打包部署
使用gradle将工程打成一个可运行jar包,并上传至服务器,gradle文件打包相关代码
jar {
String tmpString = ''
configurations.runtime.each { tmpString = tmpString + " lib\\" + it.name }
manifest {
attributes 'Main-Class': 'com.jason.redis.cluster.Application'
attributes 'Class-Path': tmpString
}
}
//清除上次的编译过的文件
task clearPj(type: Delete) {
delete 'build', 'target'
}
//删除临时文件
task release(type: Delete) {
delete 'build/libs/lib', 'build/tmp', 'build/classes', 'build/resources'
}
task packageJar(type: Copy, dependsOn: [build, release])
6. 启动工程
[root@VM_0_15_centos ~]# java -jar redis-cluster-demo-1.0-SNAPSHOT.jar
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v1.5.9.RELEASE)
2018-07-08 08:58:47.047 INFO 27357 --- [ main] com.jason.redis.cluster.Application : Starting Application on VM_0_15_centos with PID 27357 (/root/redis-cluster-demo-1.0-SNAPSHOT.jar started by root in /root)
部分省略...
2018-07-08 08:58:55.018 INFO 27357 --- [ main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8080 (http)
2018-07-08 08:58:55.034 INFO 27357 --- [ main] com.jason.redis.cluster.Application : Started Application in 9.84 seconds (JVM running for 11.085)
7. 向集群中添加几个key
我们调用add接口,添加几个key到redisCluster中,value为系统当前时间,这里我添加了test_key_01,test_key_02,...一直到test_key_10;
[root@VM_0_15_centos ~]# curl http://127.0.0.1:8080/test/add/test_key_01
{"value":"Sun Jul 08 09:15:43 CST 2018","key":"test_key_01"}
省略...
[root@VM_0_15_centos ~]# curl http://127.0.0.1:8080/test/add/test_key_10
{"value":"Sun Jul 08 09:17:02 CST 2018","key":"test_key_10"}
8. 验证主从同步及数据分片
当前集群的slave->master状态为: 8004->8001,8005->8002, 8006->8003
先连接3个主节点,查看当前节点key的情况
[root@VM_0_15_centos src]# ./redis-cli -h 172.16.0.15 -p 8001
172.16.0.15:8001> keys *
1) "test_key_06"
2) "test_key_02"
[root@VM_0_15_centos src]# ./redis-cli -h 172.16.0.15 -p 8002
172.16.0.15:8002> keys *
1) "test_key_10"
2) "test_key_03"
3) "test_key_07"
[root@VM_0_15_centos src]# ./redis-cli -h 172.16.0.15 -p 8003
172.16.0.15:8003> keys *
1) "test_key_05"
2) "test_key_01"
3) "test_key_04"
4) "test_key_08"
5) "test_key_09"
接下来连接3个从节点,查看key的情况
[root@VM_0_15_centos src]# ./redis-cli -h 172.16.0.15 -p 8004
172.16.0.15:8004> keys *
1) "test_key_02"
2) "test_key_06
[root@VM_0_15_centos src]# ./redis-cli -h 172.16.0.15 -p 8005
172.16.0.15:8005> keys *
1) "test_key_10"
2) "test_key_03"
3) "test_key_07"
[root@VM_0_15_centos src]# ./redis-cli -h 172.16.0.15 -p 8006
172.16.0.15:8006> keys *
1) "test_key_09"
2) "test_key_05"
3) "test_key_08"
4) "test_key_01"
5) "test_key_04"
可以看到,主从同步及数据分片都已经OK了。
我们删除一个可以试试,随便选一个test_key_02吧
[root@VM_0_15_centos ~]# curl http://127.0.0.1:8080/test/del/test_key_02
{"test_key_02是否存在?":false}
再到8001和8004查看key的情况,test_key_02都已经被移除了。
[root@VM_0_15_centos src]# ./redis-cli -h 172.16.0.15 -p 8001
172.16.0.15:8001> keys *
1) "test_key_06"
[root@VM_0_15_centos src]# ./redis-cli -h 172.16.0.15 -p 8004
172.16.0.15:8004> keys *
1) "test_key_06"
主从同步再次验证成功!
9. 验证主从切换
我们先停掉8001这个主节点,从理论上讲,8004节点将会升级为主节点,等到8001节点再次启动之后,8001将会作为8004的slave节点,并从8004同步最新的数据,我们来一起验证一下:
直接用kill -9 杀掉8001节点服务,可以看成是模拟服务器宕机的情况。
[root@VM_0_15_centos ~]# ps -ef|grep 8001
root 28063 1 0 09:10 ? 00:00:03 ./src/redis-server 172.16.0.15:8001 [cluster]
root 31305 31248 0 10:21 pts/5 00:00:00 grep --color=auto 8001
[root@VM_0_15_centos ~]# kill -9 28063
此时,进入8004节点,查看其主从状态:
[root@VM_0_15_centos src]# ./redis-cli -h 172.16.0.15 -p 8004
172.16.0.15:8004> info replication
# Replication
role:master
connected_slaves:0
master_replid:276339bcb6889ad591ee983974d10a023afdc6d4
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:6153
second_repl_offset:-1
repl_backlog_active:0
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:6153
可以发现,8004已经升级为master节点,并拥有0个slave节点。我们现在继续add一些key进来:
查看8004的key值情况
172.16.0.15:8004> keys *
1) "test_key_15"
2) "test_key_20"
3) "test_key_11"
4) "test_key_19"
5) "test_key_06"
接下来再次启动8001并查看key值:
[root@VM_0_15_centos src]# ./redis-cli -h 172.16.0.15 -p 8001
172.16.0.15:8001> info replication
# Replication
role:slave
master_host:172.16.0.15
master_port:8004
master_link_status:up
master_last_io_seconds_ago:4
master_sync_in_progress:0
slave_repl_offset:336
slave_priority:100
slave_read_only:1
connected_slaves:0
master_replid:f60e42b3eaeded6411af8a79f2c78c310ff8ca0d
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:336
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:336
172.16.0.15:8001> keys *
1) "test_key_19"
2) "test_key_06"
3) "test_key_15"
4) "test_key_20"
5) "test_key_11"
当再次启动后,8001同步了8004的key,并由之前的master节点转为slave节点。
以上可以看出,故障转移生效。
我们最后来关注一个异常情况:
我们知道,test_key_20这个名称的key,会路由到8001->8004这个分片,8004为master,8001为slave,现在我们del掉这个key,并且kill掉8004,然后再次添加这个key,看看会发生什么情况。
预想结果可能会因为8004宕机,8001升级为主节点,test_key_20将会保存到8001节点。
验证一下:
[root@VM_0_15_centos ~]# curl http://127.0.0.1:8080/test/del/test_key_20
{"test_key_20是否存在?":false}
[root@VM_0_15_centos ~]# ps -ef|grep 8004
root 31619 1 0 10:27 ? 00:00:00 ./src/redis-server 172.16.0.15:8004 [cluster]
root 32094 26365 0 10:36 pts/4 00:00:00 grep --color=auto 8004
[root@VM_0_15_centos ~]# kill -9 31619
[root@VM_0_15_centos ~]# curl http://127.0.0.1:8080/test/add/test_key_20
{"timestamp":1531017417511,"status":500,"error":"Internal Server Error","exception":"redis.clients.jedis.exceptions.JedisConnectionException","message":"Could not get a resource from the pool","path":"/test/add/test_key_20"}
[root@VM_0_15_centos ~]# curl http://127.0.0.1:8080/test/add/test_key_20
{"value":"Sun Jul 08 10:37:00 CST 2018","key":"test_key_20"}
我们发现,当kill掉8004这个主节点之后,第一次add的时候,应用程序报错了,这个时候,对于test_key_20的add操作丢失了!!!
当第二次add的时候才能成功。 这个问题是因为JedisPool中部分连接失效导致的,第一次应用程序拿到了一个失效的连接,导致操作失败,当第一次操作失败之后,jedisPool会剔除无效连接,因此第二次才可以拿到有效连接去操作redis。当然,这种情况可以通过调节jedisPool的配置属性来尽量减少,但是有点耗性能,不推荐。
也就是说,我们的应用程序,是应该能够容忍极少数的缓存失败的,不要将缓存当作救命稻草,相对来讲,数据库才是数据最可靠的最终载体。