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的配置属性来尽量减少,但是有点耗性能,不推荐。

也就是说,我们的应用程序,是应该能够容忍极少数的缓存失败的,不要将缓存当作救命稻草,相对来讲,数据库才是数据最可靠的最终载体。