咱们知道Redis自己的QPS已经很高了,可是在一些并发量很是高的状况下,性能仍是会受到影响的。这个时候咱们但愿更多的Redis服务来分摊压力,实现负载均衡。java
若是只有一个Redis服务,一但服务发生了宕机,那么全部的客户端都没法访问,会对业务形成很大的影响。另外,若是硬件损坏了,那上面的全部数据也是没法恢复的,咱们须要个备份。node
第三点是出于存储的考虑,由于redis全部的数据都放在内存中,若是数据量大,很容易收到硬件的限制。好比一台Redis只能存4G的容量,可是有8G的数据要存,因此只能放两台机器,这个就是横向扩展,水平分片。redis
跟Kafka、RocketMQ、MySQL、ZooKeeper同样,Redis支持集群的架构,集群的节点有主节点和从节点之分。主节点叫master,从节点叫slave。slave会经过复制的技术,自动同步master的数据。算法
Redis主从复制解决了数据备份和一部分性能的问题。可是没有解决高可用的问题,在一主一从或者一主多从的状况下,若是主服务器挂了,对外提供的服务就不可用了,须要手动把从服务器切换成主服务器,而后再把剩余节点设置为它的从节点,这个比较费时,还会形成必定时间的服务不可用。数据库
从Redis2.8版本起,提供了一个稳定版本的Sentinel哨兵来解决高可用的问题,它的思路是启动奇数个Sentinel的服务来监控Redis服务器来保证服务的可用性。服务器
启动Sentinel可用用脚本启动,它本质上只是一个运行在特殊模式之下的Redis。Sentinel经过info命令获得被监听Redis机器的master,slave等信息。数据结构
./redis-sentinel ../sentinel.conf # 或者 ./redis-server ../sentinel.conf --sentinel
为了保证监控服务器的可用性,咱们会对Sentinel作集群部署,Sentinel既监控全部的Redis服务,Sentinel之间也相互监控。 Sentinel自己没有主从之分,地位是平等的,只有Redis服务节点有主从之分。架构
Sentinel经过Raft共识算法,实现Sentinel选举,选举出一个leader来,由leader完成故障转移。Raft算法的应用很普遍,好比加密货币BTB,Spring Cloud注册中心Consul也用到了Raft算法。Raft算法的核心思想是:先到先得,少数服从多数。Sentinel的Raft实现跟原生的算法是有所区别的,可是大致思想一致。 Raft算法演示:thesecretlivesofdata.com/raft/ 。并发
不管Jedis仍是Spring Boot(2.x版本默认是Lettuce),都只须要配置所有的哨兵地址,由哨兵返回当前的master节点地址。负载均衡
哨兵的不足:主从切换的过程当中会丢失数据,由于只有一个master;只能单点写,没有解决水平扩容的问题。
Redis Cluster是在Redis 3.0的版本正式推出的,用来解决分布式的需求,同时也能够实现高可用,它是去中心化的,客户端能够链接到任意一个可用的节点。Redis Cluster能够当作是由多个Redis实例组成的数据集合。客户端不须要关注数据的子集到底存储在哪一个节点,只须要关注这个集合总体。
下面就是一个三主三从 Redis Cluster架构:
Redis建立了16384个槽(slot),每一个节点负责必定区间的slot。好比Node1负责0-5460,Node2负责5461-10922,Node3负责10923-16383。
对象分布到Redis节点的时候,首先是对Key用CRC16算法计算再%16384,获得一个slot的值,数据落到负责这个slot的Redis节点上。 查看Key属于哪一个slot:
redis>cluster keyslot jack
key与slot的关系是永远不会变的,会变的只有slot和Redis节点的关系。
咱们知道key经过CRC16算法取模后会分布在不一样的节点,若是想让不少个key同时落在同一个节点怎么办呢,只须要在key里面加入{hash tag}便可。Redis在计算槽编号的时候只会获取{}之间的字符串进行槽编号计算,以下所示:
user{666}base=... user{666}fin=...
主从切换过程:
当slave发现本身的master变成FAIL状态时,便尝试进行Failover,以期成为新的master。因为挂掉的master可能会有多个slave,从而存在多个slave竞争成为master节点的过程,其过程以下:
好比三个小的主从A,B,C组成的集群,A的master挂了,A的两个小弟发起选举,结果B的master投给A的小弟A1,C的master投给了A的小弟A2,这样就会发起第二次选举,选举轮次标记+1继续上面的流程。事实上从节点并非在主节点一进入 FAIL 状态就立刻尝试发起选举,而是有必定延迟,必定的延迟确保咱们等待FAIL状态在集群中传播,slave若是当即尝试选举,其它masters或许还没有意识到FAIL状态,可能会拒绝投票。
Redis Cluster特色
至此,三种Redis的分布式方案介绍完了,Redis Cluster既能实现主从的角色分配,又可以实现主从切换,至关于集成了Replication和Sentinel的功能。
一共有三种方案,第一种是在客户端实现相关的逻辑,例如用取模或者一致性哈希对key进行分片,查询和修改都先判断key的路由。
第二种是把分片处理的逻辑抽取出来,运行一个独立的代理服务,客户端链接到这个代理服务,代理服务作请求转发。
第三种是基于服务端实现的,就是上面介绍的Redis Cluster。
客户端咱们以Jedis为例,Jedis有几种链接池,其中有一种支持分片,就是ShardedJedisPool。如今咱们来作个实验,有两个Redis的节点,经过JedisShardInfo往里面set 100个key。
public class ShardingTest { public static void main(String[] args) { JedisPoolConfig poolConfig = new JedisPoolConfig(); // Redis服务器 JedisShardInfo shardInfo1 = new JedisShardInfo("127.0.0.1", 6379); JedisShardInfo shardInfo2 = new JedisShardInfo("192.168.8.205", 6379); // 链接池 List<JedisShardInfo> infoList = Arrays.asList(shardInfo1, shardInfo2); ShardedJedisPool jedisPool = new ShardedJedisPool(poolConfig, infoList); ShardedJedis jedis = null; try { jedis = jedisPool.getResource(); for (int i = 0; i < 100; i++) { jedis.set("k" + i, "" + i); } for (int i = 0; i < 100; i++) { Client client = jedis.getShard("k" + i).getClient(); System.out.println("取到值:" + jedis.get("k" + i) + "," + "当前key位于:" + client.getHost() + ":" + client.getPort()); } } finally { if (jedis != null) { jedis.close(); } } } }
源码在:com/xhj/jedis/shard/ShardingTest.java
最后的结果经过dbsize命令发现,一台服务器有44个key,一台服务器有56个key。从结果能够发现确实是作到了负载均衡,那具体是怎么作到的呢?咱们猜测是经过哈希取模,hash(key)%N,根据余数,决定映射到哪个节点。这种方式比较简单,属于静态的分片规则,可是一但节点数量发生了变化(新增或者减小),因为取模N发生了变化,数据须要从新分布。为了解决这个问题,咱们又有了一致性哈希算法,ShardedJedisPool实际上用的就是一致性哈希算法。
接下来介绍一致性哈希算法,咱们把全部的哈希值空间组织成一个虚拟的圆环(哈希环),整个空间按顺时针方向组织。由于是环形空间,0和2^32-1是重叠的。
假设咱们有四台机器,咱们先根据机器的名称或者IP计算哈希值,而后分布到哈希环中(粉色圆圈
),以下图所示:
如今有四个请求要set或者get,咱们对key进行哈希计算,获得哈希环中的位置(蓝色圆圈
),沿哈希环顺时针找到的第一个Node,就是数据存储的节点。
新增节点5,只影响一部分数据
删除节点1,只影响一部分数据
一致性哈希算法解决了动态增减节点时,全部数据都须要从新分布的问题,它只会影响到下一个相邻的节点,对其余节点没有影响。可是这样的一致性算法仍是有缺点,就是节点不必定是均匀的分布的,特别是在节点数比较少的状况下,这是节点1的压力很大,解决这个问题还须要引入虚拟节点。
Node1引入了两个虚拟节点,Node2引入了两个虚拟节点,这时候的数据分布将是很均匀的。
一致性哈希算法在分布式系统中,负载均衡、分库分表都有所应用,跟LRU同样,是一个很基础的算法。那么在Java代码中咱们是如何实现的,哈希环是一个什么数据结构?虚拟节点又怎么实现?
咱们点开Jedis的源码,在redis.clients.util.Sharded.initialize()方法中,Redis的节点被放到了一颗红黑树TreeMap中。
private void initialize(List<S> shards) { //建立一颗红黑树 nodes = new TreeMap<Long, S>(); //for循环Redis节点 for (int i = 0; i != shards.size(); ++i) { final S shardInfo = shards.get(i); //为每一个节点建立160个虚拟节点,放入红黑树中 if (shardInfo.getName() == null) for (int n = 0; n < 160 * shardInfo.getWeight(); n++) { //按名字hash nodes.put(this.algo.hash("SHARD-" + i + "-NODE-" + n), shardInfo); } else for (int n = 0; n < 160 * shardInfo.getWeight(); n++) { //按名字hash nodes.put(this.algo.hash(shardInfo.getName() + "*" + shardInfo.getWeight() + n), shardInfo); } //把redis节点信息放到map中 resources.put(shardInfo, shardInfo.createResource()); } }
当咱们有个key须要get或者set的时候,咱们须要知道具体落在哪一个节点。
public R getShard(String key) { //从resources里面拿出具体的节点 return resources.get(getShardInfo(key)); } public S getShardInfo(byte[] key) { //这里把key进行hash,而后从红黑树上摘下比该值大的第一个节点信息 SortedMap<Long, S> tail = nodes.tailMap(algo.hash(key)); if (tail.isEmpty()) { //没有比它大的了,直接从node中取出 return nodes.get(nodes.firstKey()); } //不然返回第一个比它大的节点信息 return tail.get(tail.firstKey()); }
这里以Jedis的源码介绍一致性哈希算法,在别的使用场景中代码的写法也是大同小异的,在数据结构的选取上:
咱们不能简单地使用二叉查找树,由于可能出现不平衡的状况。平衡二叉查找树有AVL树、红黑树等,这里使用红黑树,选用红黑树的缘由有两点:
使用ShardedJedisPool之类的客户端分片代码的优点是配置简单,不依赖其余中间件,分区的逻辑能够本身定,比较灵活,缺点就是不能实现动态的服务增减,每一个客户端须要自行维护分片策略,存在重复代码。因此这时候咱们的思路就是把分片的代码抽取出来,作成一个公共服务,全部的客户端都链接到这个代理层,由代理层来进行转发。
架构图以下所示,跟数据库分表分库中间件的Mycat的工做层次是同样的。典型的代理分区方案有Twitter开源的Twemproxy和国内的豌豆荚开源的Codis。
可是会有一些问题,出现故障不能自动转移,架构复杂,须要借助其余组件Zookeeper(或者etcd/本地文件),如今已经不多使用了,能够说是在Redis Cluster出现以前的一个过渡方案,因此这里不详细介绍了。
Redis Cluster上文已经介绍过了,天生的集成了数据分片功能,能够将数据分配到不一样的实例上。这是最完美的Redis分布式方案。
由于key和slot的关系是永远不会变的,当新增了节点的时候,须要把原有的slot分配给新的节点负责,而且把相关的数据迁移过来。
添加一个新节点192.168.10.219:6378
redis-cli --cluster add-node 192.168.10.219:6378 192.168.10.219:6379
新增的节点没有哈希槽,不能分布数据,在原来的任意一个节点上执行:
redis-cli --cluster reshard 192.168.10.219:6379
输入须要分配的哈希槽的数量(好比500),和哈希槽的来源节点(能够输入all或者id)。