一、CAP原理
CAP包含:
- C : Consistent,一致性
- A : Availability,可用性
- P : Partition tolerance,分区容忍性
CAP原理是分布式数据存储的理论基石,一个数据分布式系统不可能同时满足上面三个条件,应该有所取舍。
分布式系统的节点往往都是分布在不同的机器上进行网络隔离开的,这意味着必然会导致网络断开的风险,这个网络断开的场景叫网络分区。
当网络分区(P)发生时,两个分布式节点无法进行通信,一个节点的修改无法同步到另一个节点,数据的一致性(C)无法满足。除非我们牺牲可用性(A),暂时停掉分布式服务,不提供数据修改,等网络恢复正常后,节点可同步时,再继续对外提供服务。
补充:
一致性:更新操作后,所有的节点同一时间的数据一致。一致性可以分两个不同的视角
- 客户端角度:一致性主要指多个用户并发访问时更新的数据如何被其他用户获取的问题;
- 服务器角度:一致性则是用户进行数据更新时如何将数据复制到整个系统,以保证数据的一致。
可用性:用户访问数据时,系统是否能在正常响应时间返回结果。
分区容忍性:即分布式系统在遇到某节点或网络分区故障的时候,仍然能够对外提供满足一致性和可用性的服务。
BASE原理:
BA(basically Available):基本可用性。指分布式系统在出现故障时,系统允许损失部分可用性,即保证核心功能或者当前最重要功能可用。对于用户来说,他们当前最关注的功能或者最常用的功能的可用性将会获得保证,但是其他功能会被削弱。
S(Soft-state):软状态。软状态允许系统数据存在中间状态,但不会影响系统的整体可用性,即允许不同节点的副本之间存在暂时的不一致情况。
E(Eventually Consistent):最终一致性。最终一致性要求系统中数据副本最终能够一致,而不需要实时保证数据副本一致。
二、主从同步
redis满足CAP原理中的分区容忍性(P)和可用性(A),不保证一致性(C),但会尽量保证主从数据的最终一致性。从节点会努力追赶主节点,最终和主节点一致。如果网路断开了,主从节点数据将会大量不一致,网络恢复从节点会采用多种策略追赶主节点,尽力保持和主节点一致,如果最终还是不一致,需要人工排查。
主从节点同步策略类似于持久化策略RDB与AOF
1、增量同步
增量同步的是指令流,主节点会将写操作指令记录在本地的内存缓存中,然后异步将内存缓存中指令同步个从节点,从节点一边执行指令流,一边向主节点反馈同步进度(偏移量)
注意:内存缓存是有限的,redis的复制内存缓存类似于数组实现的环形队列,但数组内容满了,就会覆盖前面的数据。
当长时间的网络断开或波动时会导致缓冲区未被同步的指令被覆盖,此时从节点无法通过指令流进行同步(偏移量判断),就需要快照同步
2、快照同步
快照同步是一个非常耗用资源的操作,主节点bgsave一次,生成二进制文件rdb,然后将rdb内容传送到从节点,从节点接受后,清空内存数据,全量加载rdb,和RDB持久化的启动一样。
注意:快照同步时增量同步也在进行,如果快照同步期间,增量同步中内存缓冲又满了,会导致快照同步,从而陷入快照同步的死循环,所以务必配置合适的内存缓冲参数。
3、增加从节点
从节点先进行一次快照同步,同步完成后增量同步
4、无盘复制
快照同步时不生成rdb文件,而是直接利用socket套接字,将二进制字节流传送给从节点。
5、wait指令
Redis主从节点数据复制是异步进行的,使用wait指令可以将异步复制变为同步复制,确保系统的强一致性(不严格,还是不能保证完全的一致性(C))
1 wait n m //n:从节点数 m:等待时间,为0时无限等待
三、Sentinel——主从服务器集群
Sentinel是Redis抵抗主节点故障的方案,Sentinel负责持续监控主节点状态,主节点故障时会自动选择最优从节点为主节点,程序不用重启。
客户端连接redis集群时首先会向Sentinel请求主节点地址,后续直接与主节点通信,当主节点发生故障时,客户端会重新向Sentinel请求主节点地址,Sentinel会将自动选择最优从节点为主节点返回个客户端。
Sentinel主从节点自动切换也不能保证数据的一致性(C),但可尽量保证消息少丢失
1 min-slaves-to-write 1 //表示主节点必须至少有一个从节点在进行复制,否则停止对外写服务 2 min-slaves-max-lag 10 //如果10s内没有收到从节点的反馈,就认为从节点同步不正常
同一台服务器模拟实现Sentinel:
redis根目录下
复制2份redis.conf作为从节点
port 6379 //master port 6389 //spare1 port 6399 //spare //两个从节点redis.conf需要加入 slaveof 192.168.0.114 6379
分别启动redis并查询状态
修改sentinel.conf,设置监听主节点
sentinel monitor master 192.168.0.114 6379 1 //表示多少个slave认为注解点失效,sentinel就认为主节点失效, sentinel config-epoch master 0 sentinel leader-epoch master 0
复制2份分别设置监听端口和id
port 26379 port 26389 port 26399
分别启动并查询状态
关闭主节点,sentinel自动设置从节点192.168.0.114:6389为主节点,原主节点重启后,sentinel自动扫描为从节点
查看节点信息
info Replication //redis-cli中执行
测试代码:遇到的问题
①上面从节点redis.conf中host不能设置为127.0.0.1
②设置127.0.0.1启动后redis.conf/sentinel.conf中slaveof等配置会被sentinel程序覆盖,需要检查并还原redis.conf/sentinel.conf配置
1 public class RedisUtils { 2 3 /** 4 * 创建单例 5 */ 6 7 private RedisUtils() throws IllegalAccessException { 8 throw new IllegalAccessException(); 9 } 10 11 // private static Jedis JEDIS = null; 12 private static JedisPool jedisPool = null; 13 private static JedisSentinelPool jedisSentinelPool = null; 14 private static final String HOST = "192.168.0.114"; 15 static{ 16 // JEDIS = new Jedis(HOST,6379,1000); 17 JedisPoolConfig config = new JedisPoolConfig(); 18 config.setMaxTotal(100); 19 config.setMaxIdle(100); 20 config.setMaxWaitMillis(10000); 21 config.setTestOnBorrow(true); 22 jedisPool = new JedisPool(config,HOST,6379); 23 Set<String> hosts = new HashSet<String>(); 24 hosts.add(HOST + ":26379"); 25 hosts.add(HOST + ":26389"); 26 hosts.add(HOST + ":26399"); 27 jedisSentinelPool = new JedisSentinelPool("master",hosts,config); 28 29 } 30 31 public static Jedis getJedis(){ 32 // return jedis; 33 // Jedis jedis = jedisPool.getResource(); 34 Jedis jedis = jedisSentinelPool.getResource(); 35 if (jedis != null){ 36 return jedis; 37 }else { 38 jedis = jedisPool.getResource(); 39 if(jedis != null){ 40 return jedis; 41 } 42 return new Jedis(HOST,6379,1000); 43 } 44 45 } 46 47 /** 48 * 测试连接 49 */ 50 public static void main(String[] args){ 51 Jedis jedis = null; 52 try { 53 jedis = getJedis(); 54 jedis.set("linkTest2","hello World2"); 55 String back = jedis.set("linkTest","hello World"); 56 System.out.println(("OK").equals(back)); 57 Object response = RedisUtils.eval(RedisWithLock.UNLOCK_EVAL, Arrays.asList("linkTest","linkTest2"), Arrays.asList("hello World","hello World2")); 58 System.out.println(response); 59 }finally { 60 if(jedis != null){ 61 //释放jedispool的一个连接 62 jedis.close(); 63 } 64 //关闭jedispool 65 close(); 66 } 67 } 68 69 }
四、Codis——分布式集群
Codis是Redis集群方案之一,采用Go语言开发,由前豌豆荚中间件团队开发并开源的。
1、原理
Codis是一个代理中间件,客户端发送请求会经过Codis定位到具体的Redis服务器。
怎么定位,以上面3个Redis服务为例
①定义hash表(1024个槽位),Codis默认1024个槽位(可设置),将1024个槽位映射到3个Redis服务
②当客户端请求Codis时,Codis采用哈希函数中的除余法(hash(key) % 1024)定位key所对应的槽位,然后根据①中映射关系,请求对应的Redis服务
2、场景
1)多个Codis:Codis是代理中间件,可以启动多个Codis实例增加整体QPS,同时也就具备了容灾功能,此时槽位映射信息不可能存储到各个Codis实例中,会导致信息的不同步,因此,需要一个分布式配置存储数据库来持久化槽位映射信息,Codis一开始使用的是zookeeper,后来也支持etcd。另外提供一个Dashboard来观察和修改槽位映射关系,当映射关系改变时Codis Proxy会监听并同步槽位映射关系。
2)新增Redis服务:以上面3个Redis服务为例,现在需要新增一个Reids服务,槽位映射关系会改变,需要将3个Redis服务中属于新Redis服务的槽位所对应的数据,迁移到新Redis服务
Codis对Redis进行了改造,增加SLOTSSCAN指令,可以遍历相同槽位下的所有Key/Value数据,便于数据迁移。迁移单位是key,迁移成功后删除key。
迁移过程中,若果
3)Codis支持自动均衡slot
五、Cluster——分布式集群
Cluster是Redis作者提供的Redis集群化方案,采用Ruby语言开发
1、原理
Cluster采用无中心结构,客户端是直接定位并请求Redis的
怎么定位:以上面3个Redis服务为例
①定义hash表(16384个槽位),Cluster默认16382个槽位,将16382个槽位映射到3个Redis服务
②当客户端连接Redis集群时,Cluster会返回一份集群的槽位映射信息给客户端,当客户端请求时,直接根据这份映射信息请求对应的Redis服务器
2、场景
1)由于客户端缓存了槽位映射关系,所以可能导致与服务器实际映射关系不一致,需要纠正机制校验调整,当客户端向一个错误的节点发出指令后,该节点会返回一个特殊的跳转指令携带目标操作的节点地址,告诉客户端去连接正确的节点,客户端接收到此指令会更新缓存,然后请求正确的节点。
2)新增Redis服务,cluster迁移单位是槽,槽内数据迁移成功才会删除,一个槽一个槽的迁移,当一个槽位迁移时,原节点槽位处于中间过渡状态migrating,目标节点槽位处于importing状态
迁移过程时同步的,源节点的主线程会处于阻塞状态,迁移完成。
由于migrate指令是阻塞指令,当key内容很大时,会导致源节点和目的节点卡顿,影响集群稳定性
3)容错,cluster可以为每个主节点设置若干个从节点。
3、同一台服务器模拟实现cluster集群
参照这个博客
脚本部署:redis-trib.rb的所有指令都移至到redis-cli中
需要先设置节点为cluster节点
将redis.conf中的########### REDIS CLUSTER##################配置全部打开
注意前面的Sintinel中slaveof需要删除,持久化文件rdb,aof也要删除
ps:不支持域名配置有点坑
1 redis-cli --cluster create --cluster-replicas *//创建集群 2 redis-cli --cluster check 192.168.0.114 6379 //检查slot是否分配完 3 redis-cli --cluster info 192.168.0.114 6379 //查询集群信息 4 redis-cli --cluster rebalance 192.168.0.114 6379 //平均主节点slot数 5 redis-cli --cluster del-node 192.168.0.114:6279 6fb1087bd4c97bcc41f52891ab4ca11b77f1ba12 //删除节点、只能删除未分配slot的节点会直接shutdown节点 6 redis-cli --cluster add-node --cluster-slave --cluster-master-id a0da54f9e47726549f32465bae5cc038f524ddc4 192.168.0.114:6279 192.168.0.114:6379 //创建从节点,需要先清空rdb等 7 redis-cli --cluster add-node 192.168.0.114:6279 192.168.0.144:6379 //创建主节点 8 redis-cli --cluster reshard 192.168.0.144:6379 //转移slot
创建集群:不能指定主从节点,主从节点
1 redis-cli --cluster create --cluster-replicas 1 192.168.0.114:6379 192.168.0.114:6389 192.168.0.114:6399 192.168.0.114:6279 192.168.0.114:6289 192.168.0.114:6179 192.168.0.114:6189
查看slot及集群信息
删除节点
新增从节点
新增主节点,留个问题
分配slot
平均slot
最终
代码测试:
1 public class ClusterTest { 2 3 /** 4 * 创建单例 5 */ 6 7 private ClusterTest() throws IllegalAccessException { 8 throw new IllegalAccessException(); 9 } 10 11 private static JedisCluster jedisCluster = null; 12 private static final String HOST = "192.168.0.114"; 13 static{ 14 JedisPoolConfig config = new JedisPoolConfig(); 15 config.setMaxTotal(100); 16 config.setMaxIdle(100); 17 config.setMaxWaitMillis(10000); 18 config.setTestOnBorrow(true); 19 Set<String> hosts = new HashSet<String>(); 20 hosts.add(HOST + ":26279"); 21 hosts.add(HOST + ":26289"); 22 hosts.add(HOST + ":26399"); 23 Set<HostAndPort> nodes = new HashSet<>(); 24 nodes.add(new HostAndPort(HOST,6379)); 25 nodes.add(new HostAndPort(HOST,6389)); 26 nodes.add(new HostAndPort(HOST,6399)); 27 nodes.add(new HostAndPort(HOST,6279)); 28 nodes.add(new HostAndPort(HOST,6289)); 29 nodes.add(new HostAndPort(HOST,6179)); 30 nodes.add(new HostAndPort(HOST,6189)); 31 jedisCluster = new JedisCluster(nodes,config); 32 33 } 34 35 /** 36 * 测试连接 37 */ 38 public static void main(String[] args){ 39 Jedis jedis = null; 40 try { 41 jedisCluster.set("linkTest","hello World"); 42 jedisCluster.get("linkTest"); 43 System.out.println(jedisCluster.get("linkTest")); 44 }finally { 45 if(jedis != null){ 46 jedis.close(); 47 } 48 close(); 49 } 50 } 51 52 public static void close(){ 53 if(jedisCluster != null){ 54 jedisCluster.close(); 55 } 56 } 57 }
《redis深度历险》