Redis集群分片存储小结

Redis集群

Redis Cluster是Redis的分布式集群解决方案,在3.0版本推出后有效地解决了redis在分布式方面的需求,实现了数据在多个Redis节点之间的自动分片、故障自动转移、扩容机制等功能。
Redis 集群通过分区来提供一定程度的可用性,在实际环境中当某个节点宕机或者不可达的情况下继续处理命令。Redis 集群的优势:

  • 自动分割数据到不同的节点上。
  • 整个集群的部分节点失败或者不可达的情况下能够继续处理命令。

分片存储

Redis 集群没有使用一致性hash, 而是引入了哈希槽的概念。

Redis 集群有16384个哈希槽,每个key通过CRC16校验后对16384取模来决定放置哪个槽。集群的每个节点负责一部分hash槽,举个例子,比如当前集群有3个节点,那么:

  • 节点 A 包含 0 到 5500号哈希槽。
  • 节点 B 包含5501 到 11000 号哈希槽。
  • 节点 C 包含11001 到 16384号哈希槽。

这种结构很容易添加或者删除节点。比如如果我想新添加个节点D, 我需要从节点 A、 B、 C中得部分槽到D上。如果我想移除节点A,需要将A中的槽移到B和C节点上,然后将没有任何槽的A节点从集群中移除即可。由于从一个节点将哈希槽移动到另一个节点并不会停止服务,所以无论添加删除或者改变某个节点的哈希槽的数量都不会造成集群不可用的状态。

Redis 集群的主从复制模型

为了使在部分节点失败或者大部分节点无法通信的情况下集群仍然可用,所以集群使用了主从复制模型,每个节点都会有N-1个复制品。

Redis 一致性保证

Redis 并不能保证数据的强一致性, 这意味这在实际中集群在特定的条件下可能会丢失写操作。

第一个原因是因为集群是用了异步复制。写操作过程:

  • 客户端向主节点B写入一条命令。
  • 主节点B向客户端回复命令状态。
  • 主节点将写操作复制给他得从节点 B1、B2 和 B3。

主节点对命令的复制工作发生在返回命令回复之后, 因为如果每次处理命令请求都需要等待复制操作完成的话, 那么主节点处理命令请求的速度将极大地降低 —— 我们必须在性能和一致性之间做出权衡。(Redis 集群可能会在将来提供同步写的方法。)
Redis 集群另外一种可能会丢失命令的情况是集群出现了网络分区, 并且一个客户端与至少包括一个主节点在内的少数实例被孤立。

举个例子, 假设集群包含 A 、 B 、 C 、 A1 、 B1 、 C1 六个节点, 其中 A 、B 、C 为主节点, A1 、B1 、C1 为A,B,C的从节点, 还有一个客户端 Z1 假设集群中发生网络分区,那么集群可能会分为两方,大部分的一方包含节点 A 、C 、A1 、B1 和 C1,小部分的一方则包含节点 B 和客户端 Z1 。

Z1仍然能够向主节点B中写入,如果网络分区发生时间较短,那么集群将会继续正常运作,如果分区的时间足够让大部分的一方将B1选举为新的master,那么Z1写入B中得数据便丢失了。(在网络分裂出现期间, 客户端 Z1 可以向主节点 B 发送写命令的最大时间是有限制的, 这一时间限制称为节点超时时间(node timeout), 是 Redis 集群的一个重要的配置选项。)

搭建集群

要让集群正常运作至少需要三个主节点,不过在刚开始试用集群功能时, 强烈建议使用六个节点: 其中三个为主节点, 而其余三个则是各个主节点的从节点。 下面将在一台机器上通过不同端口号实现redis伪集群。

准备6个独立的redis服务

安装redis
wget http://download.redis.io/releases/redis-5.0.5.tar.gz
tar -zxvf redis-5.0.5.tar.gz
cd redis-5.0.5 
make
# 安装到 /usr/local/redis 目录中 安装的文件只有一个bin目录
make install PREFIX=/usr/local/redis/ 

# 创建配置文件和data存放目录
mkdir /usr/local/redis/conf /usr/local/redis/data
准备6个redis.conf配置文件
# 配置文件进行了精简,完整配置可自行和官方提供的完整conf文件进行对照。端口号自行对应修改
#后台启动的意思
daemonize yes 
 #端口号
port 6381
# IP绑定,redis不建议对公网开放,直接绑定0.0.0.0没毛病
bind 0.0.0.0
# redis数据文件存放的目录
dir /usr/local/redis/data
# 开启AOF
appendonly yes
 # 开启集群
cluster-enabled yes
# 会自动生成在上面配置的dir目录下
cluster-config-file nodes-6381.conf 
cluster-node-timeout 5000
# 这个文件会自动生成
pidfile /var/run/redis_6381.pid 

文件中的 cluster-enabled 选项用于开实例的集群模式, 而 cluster-conf-file 选项则设定了保存节点配置文件的路径, 默认值为 nodes.conf.节点配置文件无须人为修改, 它由 Redis 集群在启动时创建, 并在有需要时自动进行更新。

启动6个redis事例
# 一定要注意每个配置文件中的端口号哦
/usr/local/redis/bin/redis-server /usr/local/redis/conf/6381.conf
/usr/local/redis/bin/redis-server /usr/local/redis/conf/6382.conf
/usr/local/redis/bin/redis-server /usr/local/redis/conf/6383.conf
/usr/local/redis/bin/redis-server /usr/local/redis/conf/6384.conf
/usr/local/redis/bin/redis-server /usr/local/redis/conf/6385.conf
/usr/local/redis/bin/redis-server /usr/local/redis/conf/6386.conf

通过redis-cli工具创建集群

在redis 5 中可使用redis-cli工具创建集群。

# 5.0版本的方式
/usr/local/redis/bin/redis-cli --cluster create 192.168.0.106:6381 192.168.0.106:6382 \
192.168.0.106:6383 192.168.0.106:6384 192.168.0.106:6385 192.168.0.106:6386 \
--cluster-replicas 1

--cluster-replicas 1 表示我们希望为集群中的每个主节点创建一个从节点。

最后,如果一切顺利,您将看到如下消息:

[OK] All 16384 slots covered

这表示集群中的 16384 个槽都有至少一个主节点在处理, 集群运作正常。

检验集群和测试

# 检查集群,查看所有节点信息
/usr/local/redis/bin/redis-cli -c -h 192.168.0.106 -p 6381 cluster nodes

Redis 的 redis-cli 程序实现了非常基本的集群支持, 可以使用命令 redis-cli -c 来启动。

/usr/local/redis/bin/redis-cli -c -h 192.168.0.106 -p 6381
192.168.0.106:6381> set a 1
-> Redirected to slot [15495] located at 192.168.0.106:6383
OK
192.168.0.106:6383> get a
"1"

故障转移测试

测试自动故障转移

关闭6381端口的redis服务,发现集群中6385成为了主节点。

[root@localhost ~]# /usr/local/redis/bin/redis-cli -c -h 192.168.0.106 -p 6381
192.168.0.106:6381> shutdown
not connected>
[root@localhost ~]# /usr/local/redis/bin/redis-cli --cluster check 192.168.0.106:6381
Could not connect to Redis at 192.168.0.106:6381: Connection refused
[root@localhost ~]# /usr/local/redis/bin/redis-cli --cluster check 192.168.0.106:6382
Could not connect to Redis at 192.168.0.106:6381: Connection refused
192.168.0.106:6382 (dd4bb70f...) -> 0 keys | 5462 slots | 1 slaves.
192.168.0.106:6383 (b670b499...) -> 1 keys | 5461 slots | 1 slaves.
192.168.0.106:6385 (70ae2648...) -> 0 keys | 5461 slots | 0 slaves.
[OK] 1 keys in 3 masters.
0.00 keys per slot on average.
>>> Performing Cluster Check (using node 192.168.0.106:6382)
M: dd4bb70ffbc9f943996951c787bb92a13f1c7ccb 192.168.0.106:6382
   slots:[5461-10922] (5462 slots) master
   1 additional replica(s)
M: b670b499e0e16cdf7ce2c32a92b8e31977d186ce 192.168.0.106:6383
   slots:[10923-16383] (5461 slots) master
   1 additional replica(s)
S: 56e45088ed9ab6a4c72f8c7453651683fa2127de 192.168.0.106:6386
   slots: (0 slots) slave
   replicates dd4bb70ffbc9f943996951c787bb92a13f1c7ccb
S: b4ee8ebdfd74f6c979235b919261dc2176817bc5 192.168.0.106:6384
   slots: (0 slots) slave
   replicates b670b499e0e16cdf7ce2c32a92b8e31977d186ce
M: 70ae264862333262eab15610d46b3528c9a35bf9 192.168.0.106:6385
   slots:[0-5460] (5461 slots) master
[OK] All nodes agree about slots configuration.
>>> Check for open slots...
>>> Check slots coverage...
[OK] All 16384 slots covered.
手动故障转移

有的时候在主节点没有任何问题的情况下强制手动故障转移也是很有必要的,比如想要升级主节点的Redis进程,我们可以通过故障转移将其转为slave再进行升级操作来避免对集群的可用性造成很大的影响。

Redis集群使用 CLUSTER FAILOVER命令来进行故障转移,不过要被转移的主节点的从节点上执行该命令 手动故障转移比主节点失败自动故障转移更加安全,因为手动故障转移时客户端的切换是在确保新的主节点完全复制了失败的旧的主节点数据的前提下下发生的,所以避免了数据的丢失。

启动6381端口的redis服务,在6381节点上,执行CLUSTER FAILOVER,使从节点升级到主节点。测试如下:

/usr/local/redis/bin/redis-server /usr/local/redis/conf/6381.conf
[root@localhost ~]# /usr/local/redis/bin/redis-cli -c -h 192.168.0.106 -p 6381
192.168.0.106:6381> CLUSTER FAILOVER

集群扩容

# 1、 启动新节点
/usr/local/redis/bin/redis-server /usr/local/redis/conf/6387.conf

# 2、 加入到已经存在的集群作为master
/usr/local/redis/bin/redis-cli --cluster add-node 192.168.0.106:6387 192.168.0.106:6382
# 本质就是发送一个新节点通过 CLUSTER MEET命令加入集群
# 新节点没有分配hash槽

# 3、 加入到已经存在的集群作为slave
/usr/local/redis/bin/redis-cli --cluster add-node 192.168.0.106:6387 192.168.0.106:6382 --cluster-slave
# 可以手工指定master,否则就是选择一个slave数量较少的master 
/usr/local/redis/bin/redis-cli --cluster add-node 192.168.0.106:6387 192.168.0.106:6382 --cluster-slave --cluster-master-id <node-id>
# 还可以将空master,转换为slave
cluster replicate <master-node-id>

# 4、 检查集群
/usr/local/redis/bin/redis-cli --cluster check 192.168.0.106:6382

集群节点删除

# 注意:删除master的时候要把数据清空或者分配给其他主节点
/usr/local/redis/bin/redis-cli --cluster del-node 192.168.0.106:6381 <node-id>

集群需要注意的问题

  • 增加了slot槽的计算,是不是比单机性能差?

    共16384个槽,slots槽计算方式公开的,java客户端中就使用了:HASH_SLOT = CRC16(key) mod 16384。
    为了避免每次都需要服务器计算重定向,优秀的java客户端都实现了本地计算,和服务器slots分配进行映射,有变动时再更新本地内容。

  • redis集群大小,到底可以装多少数据?

    理论是可以做到16384个槽,但是redis官方建议是最大1000个实例。

  • 集群节点是怎么通信的?

    每个Redis集群节点都有一个额外的TCP端口,每个节点使用TCP连接与每个其他节点连接。

  • ask和moved重定向的区别是什么?

    重定向包括两种情况:
    如果是确定slot不属于当前节点,redis会返回moved。
    如果当前redis节点正在处理slot迁移,则代表此处请求对应的key暂时不在此节点,返回ask,告诉客户端本次请求重定向。

  • 数据倾斜和访问倾斜的问题怎么处理?

    倾斜导致集群中部分节点数据多,压力大。解决方案分为前期和后期:

    前期是业务层面提前预测。哪些key是热点,在设计的过程中规避。

    后期是slot迁移,尽量将压力分摊。

  • slot手动迁移怎么做?

    迁移过程如下,完整的迁移流程:
    在迁移目的节点执行cluster setslot IMPORTING 命令,指明需要迁移的slot和迁移源节点。
    在迁移源节点执行cluster setslot MIGRATING 命令,指明需要迁移的slot和迁移目的节点。
    在迁移源节点执行cluster getkeysinslot获取该slot的key列表。
    在迁移源节点执行对每个key执行migrate命令,该命令会同步把该key迁移到目的节点。
    在迁移源节点反复执行cluster getkeysinslot命令,直到该slot的列表为空。
    在迁移源节点和目的节点执行cluster setslot NODE ,完成迁移操作。

  • 节点之间会交换信息,传递的消息包括槽的信息,带来带宽消耗。

    应避免使用大的一个集群,可以分为多个集群。

  • 发布/订阅

    在一个 Redis 集群中,客户端能订阅任何一个节点,也能发布消息给任何一个节点。集群会确保发布的消息都会按需进行转发。 目前的实现方式是单纯地向所有节点广播所有的发布消息,在将来的实现中会用 bloom filters 或其他算法来优化。

  • 如何在redis cluster中实现读写分离?

    redis cluster默认所以从节点上的读写,都会重定向到key对接槽的主节点上。

    可以通过readonly设置当前连接可读,通过readwrite取消当前连接的可读状态。

客户端对Redis集群的支持

Jedis对Redis集群提供了支持,下面是使用Jedis访问Redis集群的代码。

package com.study.cache.redis.a7_cluster;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.data.redis.connection.RedisClusterConfiguration;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;

import java.util.Arrays;

@Configuration
// 在cluster环境下生效
@Profile("a7_cluster")
class ClusterAppConfig {
    @Bean
    public JedisConnectionFactory redisConnectionFactory() {
        System.out.println("加载cluster环境下的redis client配置");
        RedisClusterConfiguration redisClusterConfiguration = new RedisClusterConfiguration(Arrays.asList(
                "192.168.0.106:6381",
                "192.168.0.106:6382",
                "192.168.0.106:6383",
                "192.168.0.106:6384",
                "192.168.0.106:6385",
                "192.168.0.106:6386"
        ));
        // 自适应集群变化
        return new JedisConnectionFactory(redisClusterConfiguration);
    }
}
package com.study.cache.redis.a7_cluster;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Profile;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

@Service
@Profile("a7_cluster")
public class ClusterService {
    @Autowired
    private StringRedisTemplate template;

    public void set(String userId, String userInfo) {
        template.opsForValue().set(userId, userInfo);
    }
}

package com.study.cache.redis.a7_cluster;

import com.study.cache.redis.a0_example.SingleExampleService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import java.util.concurrent.TimeUnit;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContext.xml")
@ActiveProfiles("a7_cluster") // 设置profile
// 集群对于客户端而言,基本是无感知的
public class ClusterServiceTests {
    @Autowired
    ClusterService clusterService;

    @Test
    public void setTest() {
        clusterService.set("haha", "hahhhhh");
        clusterService.set("a", "1");
        clusterService.set("foo", "bar");
    }

    // 测试cluster集群故障时的反应
    @Test
    public void failoverTest() {
        while (true) {
            try {
                long i = System.currentTimeMillis();
                clusterService.set("haha", i + "");
                // delay 10ms
                TimeUnit.MILLISECONDS.sleep(10);
            } catch (Exception e) {
                System.out.println(e.getMessage());
            }
        }
    }
}

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Redis集群分片的原理是为了解决容量问题,因为每台Redis机器的内存设置较小。通过将数据分散存储在多个节点上,可以增加整个集群的存储容量和性能。分片是根据数据的键进行的,每个键会被映射到一个特定的槽位,而每个槽位又会被分配给集群中的某个节点。这样,当客户端发送数据请求时,根据键的哈希值,可以确定数据所在的槽位,从而找到对应的节点进行数据的读取或写入操作。通过这种方式,可以实现数据在不同节点之间的分布,提高整个集群的性能和扩展性。 在Redis集群中,使用了哈希槽(hash slot)的概念来进行数据分片。集群中共有16384个哈希槽,每个槽可以存储一部分数据。当集群中新增或删除节点时,槽位会重新分配给新的节点,实现数据的平衡迁移。同时,Redis集群还使用了Gossip协议来实现节点间的信息交换和管理,保证集群的高可用性和可靠性。通过使用Redis集群分片的机制,可以实现数据的水平扩展和高性能的分布式存储。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* [Redis Cluster集群数据分片机制原理](https://download.csdn.net/download/weixin_38651165/12822944)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] - *2* *3* [redis集群分片的原理详解](https://blog.csdn.net/nandao158/article/details/123715287)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值