掘金 后端 ( ) • 2021-06-06 18:11
.markdown-body{word-break:break-word;line-height:1.75;font-weight:400;font-size:15px;overflow-x:hidden;color:#333}.markdown-body h1,.markdown-body h2,.markdown-body h3,.markdown-body h4,.markdown-body h5,.markdown-body h6{line-height:1.5;margin-top:35px;margin-bottom:10px;padding-bottom:5px}.markdown-body h1{font-size:30px;margin-bottom:5px}.markdown-body h2{padding-bottom:12px;font-size:24px;border-bottom:1px solid #ececec}.markdown-body h3{font-size:18px;padding-bottom:0}.markdown-body h4{font-size:16px}.markdown-body h5{font-size:15px}.markdown-body h6{margin-top:5px}.markdown-body p{line-height:inherit;margin-top:22px;margin-bottom:22px}.markdown-body img{max-width:100%}.markdown-body hr{border:none;border-top:1px solid #ddd;margin-top:32px;margin-bottom:32px}.markdown-body code{word-break:break-word;border-radius:2px;overflow-x:auto;background-color:#fff5f5;color:#ff502c;font-size:.87em;padding:.065em .4em}.markdown-body code,.markdown-body pre{font-family:Menlo,Monaco,Consolas,Courier New,monospace}.markdown-body pre{overflow:auto;position:relative;line-height:1.75}.markdown-body pre>code{font-size:12px;padding:15px 12px;margin:0;word-break:normal;display:block;overflow-x:auto;color:#333;background:#f8f8f8}.markdown-body a{text-decoration:none;color:#0269c8;border-bottom:1px solid #d1e9ff}.markdown-body a:active,.markdown-body a:hover{color:#275b8c}.markdown-body table{display:inline-block!important;font-size:12px;width:auto;max-width:100%;overflow:auto;border:1px solid #f6f6f6}.markdown-body thead{background:#f6f6f6;color:#000;text-align:left}.markdown-body tr:nth-child(2n){background-color:#fcfcfc}.markdown-body td,.markdown-body th{padding:12px 7px;line-height:24px}.markdown-body td{min-width:120px}.markdown-body blockquote{color:#666;padding:1px 23px;margin:22px 0;border-left:4px solid #cbcbcb;background-color:#f8f8f8}.markdown-body blockquote:after{display:block;content:""}.markdown-body blockquote>p{margin:10px 0}.markdown-body ol,.markdown-body ul{padding-left:28px}.markdown-body ol li,.markdown-body ul li{margin-bottom:0;list-style:inherit}.markdown-body ol li .task-list-item,.markdown-body ul li .task-list-item{list-style:none}.markdown-body ol li .task-list-item ol,.markdown-body ol li .task-list-item ul,.markdown-body ul li .task-list-item ol,.markdown-body ul li .task-list-item ul{margin-top:0}.markdown-body ol ol,.markdown-body ol ul,.markdown-body ul ol,.markdown-body ul ul{margin-top:3px}.markdown-body ol li{padding-left:6px}.markdown-body .contains-task-list{padding-left:0}.markdown-body .task-list-item{list-style:none}@media (max-width:720px){.markdown-body h1{font-size:24px}.markdown-body h2{font-size:20px}.markdown-body h3{font-size:18px}}

“这是我参与更文挑战的第5天,活动详情查看: 更文挑战

前言

推荐观看文章:redis安装并开机自启动整合springboot实战,安装Redis。

为什么要创建Redis分布式集群呢?Redis的部署有单机版,一主多从+哨兵。单机版就不用说了,节点挂了就直接不可用了。一主多从+哨兵,一般用作读写分离,并且由于哨兵的存在可以在Master不可用的时候,选择另外的Slave作为新的Master使得集群可以继续提供服务。

但是一主多从的架构,Slave只是Master的副本,一个Key在Master和所有的Slave上都是存在。在Key很多的情况下,就不够存储了,当然可以增大机器的内存。

而Redis提供Cluster的模式,可以横向扩展Key的存储。

服务器规划

服务器:

  • 192.168.79.120

  • 192.168.79.121

  • 192.168.79.122

  • 192.168.79.123

  • 192.168.79.124

  • 192.168.79.125

配置文件

在默认的之上修改的部分,每台都一样

bind 0.0.0.0
dir "/usr/local/redis/db"
requirepass "root1234"
cluster-enabled yes
cluster-config-file nodes-6379.conf
cluster-node-timeout 5000appendonly yes
复制代码

创建集群

在Redis5之前的版本,创建Redis的集群是需要Ruby脚本的,也就是需要一个Ruby的环境,但是Redis5版本之后,通过redis-cli就可以创建Redis集群了。以下命令:

redis-cli -a root1234 --cluster create 192.168.79.120:6379 192.168.79.121:6379 192.168.79.122:6379 192.168.79.123:6379 192.168.79.124:6379 192.168.79.125:6379 --cluster-replicas 1
复制代码

敲完之后,输入yes创建三主三从的集群,--cluster-replicas 1说明每台master一个slave。六台刚好。

>>> Performing hash slots allocation on 6 nodes...
Master[0] -> Slots 0 - 5460
Master[1] -> Slots 5461 - 10922
Master[2] -> Slots 10923 - 16383
Adding replica 192.168.79.124:6379 to 192.168.79.120:6379
Adding replica 192.168.79.125:6379 to 192.168.79.121:6379
Adding replica 192.168.79.123:6379 to 192.168.79.122:6379
M: fc45e8b102f1e3d689d1ca194593dc801689f28c 192.168.79.120:6379
   slots:[0-5460] (5461 slots) master
M: 6e6c3a530610c2e319e531ed6ba14a8a42ba5fe8 192.168.79.121:6379
   slots:[5461-10922] (5462 slots) master
M: 3cac41b0e8da1182f413ec2590430856fddab153 192.168.79.122:6379
   slots:[10923-16383] (5461 slots) master
S: b911d342eb181c2dee04f4bb49ead2246e58b070 192.168.79.123:6379
   replicates 3cac41b0e8da1182f413ec2590430856fddab153
S: be51a450c46fa33670f8a83e502ae5ead8d0e84b 192.168.79.124:6379
   replicates fc45e8b102f1e3d689d1ca194593dc801689f28c
S: 05724a6a68f7f24ac74e07900db269c136410fd3 192.168.79.125:6379
   replicates 6e6c3a530610c2e319e531ed6ba14a8a42ba5fe8
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 192.168.79.120:6379)
M: fc45e8b102f1e3d689d1ca194593dc801689f28c 192.168.79.120:6379
   slots:[0-5460] (5461 slots) master
   1 additional replica(s)
S: b911d342eb181c2dee04f4bb49ead2246e58b070 192.168.79.123:6379
   slots: (0 slots) slave
   replicates 3cac41b0e8da1182f413ec2590430856fddab153
S: 05724a6a68f7f24ac74e07900db269c136410fd3 192.168.79.125:6379
   slots: (0 slots) slave
   replicates 6e6c3a530610c2e319e531ed6ba14a8a42ba5fe8
M: 6e6c3a530610c2e319e531ed6ba14a8a42ba5fe8 192.168.79.121:6379
   slots:[5461-10922] (5462 slots) master
   1 additional replica(s)
S: be51a450c46fa33670f8a83e502ae5ead8d0e84b 192.168.79.124:6379
   slots: (0 slots) slave
   replicates fc45e8b102f1e3d689d1ca194593dc801689f28c
M: 3cac41b0e8da1182f413ec2590430856fddab153 192.168.79.122:6379
   slots:[10923-16383] (5461 slots) master
   1 additional replica(s)
[OK] All nodes agree about slots configuration.
>>> Check for open slots...
>>> Check slots coverage...
[OK] All 16384 slots covered.
复制代码

从上面的输出日子就可以看到,确实创建可三台主,三台从,一共16284跟slot被分配了。Master和Slave由Redis分配好了。

通过命令检查任意一节点:

redis-cli -a root1234 --cluster check 192.168.79.120:6379


192.168.79.120:6379 (fc45e8b1...) -> 0 keys | 5461 slots | 1 slaves.
192.168.79.121:6379 (6e6c3a53...) -> 0 keys | 5462 slots | 1 slaves.
192.168.79.122:6379 (3cac41b0...) -> 0 keys | 5461 slots | 1 slaves.
[OK] 0 keys in 3 masters.
0.00 keys per slot on average.
>>> Performing Cluster Check (using node 192.168.79.120:6379)
M: fc45e8b102f1e3d689d1ca194593dc801689f28c 192.168.79.120:6379
   slots:[0-5460] (5461 slots) master
   1 additional replica(s)
S: b911d342eb181c2dee04f4bb49ead2246e58b070 192.168.79.123:6379
   slots: (0 slots) slave
   replicates 3cac41b0e8da1182f413ec2590430856fddab153
S: 05724a6a68f7f24ac74e07900db269c136410fd3 192.168.79.125:6379
   slots: (0 slots) slave
   replicates 6e6c3a530610c2e319e531ed6ba14a8a42ba5fe8
M: 6e6c3a530610c2e319e531ed6ba14a8a42ba5fe8 192.168.79.121:6379
   slots:[5461-10922] (5462 slots) master
   1 additional replica(s)
S: be51a450c46fa33670f8a83e502ae5ead8d0e84b 192.168.79.124:6379
   slots: (0 slots) slave
   replicates fc45e8b102f1e3d689d1ca194593dc801689f28c
M: 3cac41b0e8da1182f413ec2590430856fddab153 192.168.79.122:6379
   slots:[10923-16383] (5461 slots) master
   1 additional replica(s)
[OK] All nodes agree about slots configuration.
>>> Check for open slots...
>>> Check slots coverage...
[OK] All 16384 slots covered.
复制代码

通过检查可以清楚的知道Redis Cluster中各个Redis实例之间的关系了。

关于Redis Cluster中的Slot(槽)

一个key是会通过Redis中CRC算法,去计算出一个的key是属于哪一个Slot。在上面的例子中,一共有16384个slot被平均地分配到了三台Master上(Slave不分配Slot),每个Key在集群中只会存在一份。

当往Redis Cluuster中插入一个Key时,就通过CRC16(key) mod 16384算法取模,假如:执行redis命令,SET test 123。将执行crc16(test)=63534,然后mod取余,63534%16384=14382,得出14382。由于10923-16383是位于192.168.79.122:6379上,test这个key就会被存储在192.168.79.122:6379的Redis实例上。

SpringBoot整合,连接Redis Cluster

<dependencies>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-data-redisartifactId>
        dependency>
        <dependency>
            <groupId>org.springframework.bootgroupId>
            <artifactId>spring-boot-starter-webartifactId>
        dependency>
    dependencies>
复制代码

配置文件

spring:
  redis:
    cluster:
      nodes: 192.168.79.120:6379,192.168.79.121:6379,192.168.79.122:6379,192.168.79.123:6379,192.168.79.124:6379,192.168.79.125:6379
    password: root1234
复制代码

redis通用工具类

@Component
public class RedisUtil {

    private final StringRedisTemplate redisTemplate;

    public RedisUtil(StringRedisTemplate redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    // Key(键),简单的key-value操作

    /**
     * 实现命令:TTL key,以秒为单位,返回给定 key的剩余生存时间(TTL, time to live)。
     *
     * @param key
     * @return
     */
    public long ttl(String key) {
        return redisTemplate.getExpire(key);
    }

    /**
     * 实现命令:expire 设置过期时间,单位秒
     *
     * @param key
     * @return
     */
    public void expire(String key, long timeout) {
        redisTemplate.expire(key, timeout, TimeUnit.SECONDS);
    }

    /**
     * 实现命令:INCR key,增加key一次
     *
     * @param key
     * @return
     */
    public long incr(String key, long delta) {
        return redisTemplate.opsForValue().increment(key, delta);
    }

    /**
     * 实现命令:KEYS pattern,查找所有符合给定模式 pattern的 key
     */
    public Set keys(String pattern) {
        return redisTemplate.keys(pattern);
    }

    /**
     * 实现命令:DEL key,删除一个key
     *
     * @param key
     */
    public void del(String key) {
        redisTemplate.delete(key);
    }

    // String(字符串)

    /**
     * 实现命令:SET key value,设置一个key-value(将字符串值 value关联到 key)
     *
     * @param key
     * @param value
     */
    public void set(String key, String value) {
        redisTemplate.opsForValue().set(key, value);
    }

    /**
     * 实现命令:SET key value EX seconds,设置key-value和超时时间(秒)
     *
     * @param key
     * @param value
     * @param timeout (以秒为单位)
     */
    public void set(String key, String value, long timeout) {
        redisTemplate.opsForValue().set(key, value, timeout, TimeUnit.SECONDS);
    }

    /**
     * 实现命令:GET key,返回 key所关联的字符串值。
     *
     * @param key
     * @return value
     */
    public String get(String key) {
        return (String) redisTemplate.opsForValue().get(key);
    }

    /**
     * 批量查询,对应mget
     *
     * @param keys
     * @return
     */
    public List mget(List keys) {
        return redisTemplate.opsForValue().multiGet(keys);
    }

    /**
     * 批量查询,管道pipeline
     *
     * @param keys
     * @return
     */
    public List batchGet(List keys) {

//nginx -> keepalive
//redis -> pipeline

        List result = redisTemplate.executePipelined(new RedisCallback() {
            @Override
            public String doInRedis(RedisConnection connection) throws DataAccessException {
                StringRedisConnection src = (StringRedisConnection) connection;

                for (String k : keys) {
                    src.get(k);
                }
                return null;
            }
        });

        return result;
    }


    // Hash(哈希表)

    /**
     * 实现命令:HSET key field value,将哈希表 key中的域 field的值设为 value
     *
     * @param key
     * @param field
     * @param value
     */
    public void hset(String key, String field, Object value) {
        redisTemplate.opsForHash().put(key, field, value);
    }

    /**
     * 实现命令:HGET key field,返回哈希表 key中给定域 field的值
     *
     * @param key
     * @param field
     * @return
     */
    public String hget(String key, String field) {
        return (String) redisTemplate.opsForHash().get(key, field);
    }

    /**
     * 实现命令:HDEL key field [field ...],删除哈希表 key 中的一个或多个指定域,不存在的域将被忽略。
     *
     * @param key
     * @param fields
     */
    public void hdel(String key, Object... fields) {
        redisTemplate.opsForHash().delete(key, fields);
    }

    /**
     * 实现命令:HGETALL key,返回哈希表 key中,所有的域和值。
     *
     * @param key
     * @return
     */
    public Map hgetall(String key) {
        return redisTemplate.opsForHash().entries(key);
    }

    // List(列表)

    /**
     * 实现命令:LPUSH key value,将一个值 value插入到列表 key的表头
     *
     * @param key
     * @param value
     * @return 执行 LPUSH命令后,列表的长度。
     */
    public long lpush(String key, String value) {
        return redisTemplate.opsForList().leftPush(key, value);
    }

    /**
     * 实现命令:LPOP key,移除并返回列表 key的头元素。
     *
     * @param key
     * @return 列表key的头元素。
     */
    public String lpop(String key) {
        return (String) redisTemplate.opsForList().leftPop(key);
    }

    /**
     * 实现命令:RPUSH key value,将一个值 value插入到列表 key的表尾(最右边)。
     *
     * @param key
     * @param value
     * @return 执行 LPUSH命令后,列表的长度。
     */
    public long rpush(String key, String value) {
        return redisTemplate.opsForList().rightPush(key, value);
    }
}
复制代码

测试用的控制层

@RestController
@RequestMapping("test")
public class TestRedisController {

    @Autowired
    private RedisUtil redisUtil;

    @GetMapping("add")
    public void add(@RequestParam("key") String key, @RequestParam("value") String value) {
        redisUtil.set(key, value);
    }

    @GetMapping("list")
    public Object listAllKeys() {
        Set keys = redisUtil.keys("*");
        HashMap map = new HashMap<>();
        for (String key : keys) {
            String value = redisUtil.get(key);
            map.put(key, value);
        }
        return map;
    }
}
复制代码

启动类

@SpringBootApplication
public class RedisApplication {

    public static void main(String[] args) {
        SpringApplication.run(RedisApplication.class, args);
    }
}
复制代码

测试

运行启动类

发送GET请求

http://localhost;8080/test/add?key=age&value=18

http://localhost;8080/test/add?key=name&value=tom

http://localhost;8080/test/list