主从复制
概念
主从复制,是指将一台Redis服务器的数据,复制到其他的Redis服务器。前者称为主节点(Master),后者称为从节点(Slave),数据的复制是单向的,只能由主节点到从节点。Master以写为主,Slave 以读为主(只读模式),当主节点关闭后,从节点依然可以读取数据,但是会报错
优点:
-
实现了读写分离,提高了性能
-
在写少读多的场景下,我们甚至可以安排很多个从节点,这样就能够大幅度的分担压力,并且就算挂掉一个,其他的也能使用
实验
打开两个redis服务器,修改配置文件redis.conf
的端口
一个服务器的端口设定为6001,复制一份,另一个的端口为6002,再分别指定配置文件进行启动
redis-server.exe redis.conf
输入info replication
命令来查看当前的主从状态,可以看到默认的角色为:master,也就是说所有的服务器在启动之后都是主节点的状态,在6002的客户端输入slaveof/replicaof 127.0.0.1 6001
命令后,就会将6001服务器作为主节点,而当前节点作为6001的从节点,并且角色也会变成:slave,而且从节点信息中已经出现了6002服务器,也就是说现在6001和6002就形成了主从关系(还包含一个偏移量,这个偏移量反应的是从节点的同步情况)
主服务器和从服务器都会维护一个复制偏移量,主服务器每次向从服务器中传递 N 个字节的时候,会将自己的复制偏移量加上 N。从服务器中收到主服务器的 N 个字节的数据,就会将自己额复制偏移量加上 N,通过主从服务器的偏移量对比可以很清楚的知道主从服务器的数据是否处于一致,如果不一致就需要进行增量同步了
测试:在主节点新增数据,同步到从节点
通过输入replicaof/slaveof no one
,即可变回Master角色
新增从节点同步流程:
-
从节点执行replicaof ip port命令后,从节点会保存主节点相关的地址信息。
-
从节点通过每秒运行的定时任务发现配置了新的主节点后,会尝试与该节点建立网络连接,专门用于接收主节点发送的复制命令。
-
连接成功后,第一次会将主节点的数据进行全量复制,之后采用增量复制,持续将新来的写命令同步给从节点。
配置文件配置主节点:
replicaof 127.0.0.1 6001 #填到从节点的配置文件中
还可以使某节点作为从节点的从节点
replicaof 127.0.0.1 6002 #填到从节点6003的配置文件中
从节点分布
第二个分布的缺点:整个传播链路一旦中途出现问题,那么就会导致后面的从节点无法及时同步
哨兵模式
这里的哨兵是是专用于Redis的。哨兵会对所有的节点进行监控,如果发现主节点出现问题,那么会立即从从节点选举一个新的主节点出来,这样就不会由于主节点的故障导致整个系统不可写
类似于服务治理模式,比如Nacos和Eureka,所有的服务都会被实时监控,那么只要出现问题,肯定是可以及时发现的,并且能够采取响应的补救措施
如何添加哨兵:直接删除配置文件的全部内容并添加如下内容
#第一个和第二个是固定,第三个是为监控对象名称,随意,后面就是主节点的相关信息,包括IP地址和端口 sentinel monitor lbwnb 127.0.0.1 6001 1
选举规则:
-
首先会根据优先级进行选择,可以在配置文件中进行配置,添加
replica-priority
配置项(默认是100),越小表示优先级越高 -
如果优先级一样,那就选择偏移量最大的
-
要是还选不出来,那就选择pid(启动时随机生成的)最小的
可以修改配置中的端口实现多个哨兵
与Java互动:
在哨兵重新选举新的主节点之后,Java中的Redis的客户端怎么感知到呢
<dependencies> <dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> <version>4.2.1</version> </dependency> </dependencies>
public class Main { public static void main(String[] args) { //直接使用JedisSentinelPool来获取Master节点 //需要把三个哨兵的地址都填入 try (JedisSentinelPool pool = new JedisSentinelPool("lbwnb", new HashSet<>(Arrays.asList("127.0.0.1:26741", "127.0.0.1:26740", "127.0.0.1:26739")))) { Jedis jedis = pool.getResource(); //直接询问并得到Jedis对象,这就是连接的Master节点 jedis.set("test", "114514"); //直接写入即可,实际上就是向Master节点写入 Jedis jedis2 = pool.getResource(); //再次获取 System.out.println(jedis2.get("test")); //读取操作 } catch (Exception e) { e.printStackTrace(); } } }
集群搭建
因为单机的内存容量有限,已经没办法再继续扩展时,但又需要存储更多的内容,这时就可以让N台机器上的Redis来分别存储各个部分的数据(每个Redis可以存储1/N的数据量),实现了容量的横向扩展。同时每台Redis还可以配一个从节点,这样就可以更好地保证数据的安全性
集群的机制:
插槽就是键的Hash计算后的一个结果,采用CRC16,能得到16个bit位的数据,也就是说算出来之后结果是0-65535之间,再进行取模
Redis key的路由计算公式:s = CRC16(key) % 16384
一个Redis集群包含16384个插槽,集群中的每个Redis 实例负责维护一部分插槽以及插槽所映射的键值数据
哈希结果的值是多少,就应该存放到对应维护的Redis下,比如Redis节点1负责0-25565的插槽,而这时客户端插入了一个新的数据a=10,a在Hash计算后结果为666,那么a就应该存放到1号Redis节点中。本质上就是通过哈希算法将插入的数据分摊到各个节点的
集群搭建:
配置中开启集群模式
# cluster node enable the cluster support uncommenting the following: # cluster-enabled yes
接着把所有的持久化文件全部删除,所有的节点内容必须是空的
输入
redis-cli.exe --cluster create --cluster-replicas 1 127.0.0.1:6001 127.0.0.1:6002 127.0.0.1:6003 127.0.0.1:7001 127.0.0.1:7002 127.0.0.1:7003 #--cluster-replicas 1指的是每个节点配一个从节点
默认分配的方案是6001/6002/6003都被选为主节点,其他的为从节点,直接输入yes同意方案即可
插入数据时,键计算出来的哈希值(插槽),若不归当前节点管,则无法插入,需要去管这个插槽的节点执行
也可以使用集群方式连接,这样无论在哪个节点都可以插入,只需要添加-c
表示以集群模式访问
输入cluster nodes
命令来查看当前所有节点的信息
当一个主节点挂了,其从节点就可以晋升为主节点,再次启用原主节点,发现其变为现主节点的从节点,当两个节点都挂了,也就是说集群存在不可用的节点,会无法插入新的数据
Java连接到集群模式下的Redis:
使用JedisCluster对象即可
public class Main { public static void main(String[] args) { //和客户端一样,随便连一个就行,也可以多写几个,构造方法有很多种可以选择 try(JedisCluster cluster = new JedisCluster(new HostAndPort("127.0.0.1", 6003))){ System.out.println("集群实例数量:"+cluster.getClusterNodes().size()); cluster.set("a", "yyds"); System.out.println(cluster.get("a")); } } }
分布式锁
setnx key value #只有当指定的key不存在的时候,才能进行插入 #当客户端1设定a之后,客户端2使用setnx会直接失败,但使用set可以成功 #当客户端1删除a之后,客户端2使用setnx就会成功
某个服务加了锁但服务本身卡顿了或是直接崩溃了,那这把锁将无法释放了,因此还可以考虑加个过期时间
set a 666 EX 5 NX #最后加一个NX表示是使用setnx的模式
问题1:一个服务放了一个键值对,由于资源紧张服务时间过长,键值对的过期时间先到了,而在第一个服务未结束时,第二个服务开启并存放了与第一个服务键值相同的数据,然后第一个服务完事了,就把第二个服务存放的键值对删了
只是添加过期时间,会出现这种把别人加的锁谁卸了的情况
解决方案:现在想保证任务只能删除自己加的锁,如果是别人加的锁是没有资格删的,所以可以吧a的值指定为任务专属的值,如果在主动删除锁的时候发现值不是当前任务指定的,那么说明可能是因为超时,其他任务已经加锁了
问题2:如果在超时之前那一刹那进入到释放锁的阶段,获取到值还是自己,但是在即将执行删除之前,由于超时机制导致被删除并且其他任务也加锁了,那么这时再进行删除,仍然会导致删除其他任务加的锁
本质还是因为锁的超时时间不太好衡量,如果超时时间能够设定地比较恰当,就可以避免这种问题了
解决方案:可以使用Redisson框架,它是Redis官方推荐的Java版的Redis客户端。Redisson内部提供了一个监控锁的看门狗,作用是在Redisson实例被关闭前,不断的延长锁的有效期,它提供了很多种分布式锁的实现
<dependency> <groupId>org.redisson</groupId> <artifactId>redisson</artifactId> <version>3.17.0</version> </dependency> <dependency> <groupId>io.netty</groupId> <artifactId>netty-all</artifactId> <version>4.1.75.Final</version> </dependency>
// 注:这个锁是基于Redis的,不仅仅只可以用于当前应用,是能够垮系统的 public static void main(String[] args) { Config config = new Config(); config.useSingleServer().setAddress("redis://127.0.0.1:6379"); //配置连接的Redis服务器,也可以指定集群 RedissonClient client = Redisson.create(config); //创建RedissonClient客户端 for (int i = 0; i < 10; i++) { new Thread(() -> { try(Jedis jedis = new Jedis("127.0.0.1", 6379)){ RLock lock = client.getLock("testLock"); //指定锁的名称,拿到锁对象 for (int j = 0; j < 100; j++) { lock.lock(); //加锁 int a = Integer.parseInt(jedis.get("a")) + 1; jedis.set("a", a+""); lock.unlock(); //解锁 } } System.out.println("结束!"); }).start(); } }
如果用于存放锁的Redis服务器挂了,还是是会出问题的,这个时就可以使用RedLock,它的思路是,在多个Redis服务器上保存锁,只需要超过半数的Redis服务器获取到锁,那么就真的获取到锁了,这样就算挂掉一部分节点,也能保证正常运行