SpringCloud进阶--Redis与分布式
Redis与分布式
Redis是一个基于内存的高性能数据库!
主从复制

主从复制:将一台Redis服务器的数据复制到其他Redis服务器,前者是主节点(Master),后者是从节点(Slave),数据的复制是单向的,只能从主节点到从节点。Master以写为主,Slave以读为主。
这样的好处有:
- 实现读写分离,提高性能
- 在写少读多的情况下,可以安排多个从节点,这样能大幅度分担压力,就算挂掉一个,其他的也能用
具体的实施步骤如下:
- 先修改master的redis.windows.conf文件,把端口为6001,slave的端口为6002
# Accept connections on the specified port, default is 6379 (IANA #815344).
# If port 0 is specified Redis will not listen on a TCP socket.
port 6001
- 启动redis:命令如下:redis-server.exe redis.windows.conf
- 使用info replication 命令可以查看主从状态。启动客户端后,可以使用命令查看
- 将6002设置为从节点:先使用客户端连上6002 ,再输入命令 replicaof 127.0.0.1 6001 命令后,就会将6001服务器作为主节点,当前节点就是从节点

可以看到6002的角色是slave。从新连接6001再查看状态

此时,6001和6002已经形成了主从关系,这里有一个偏移量,反应的是从节点的同步情况
- 主节点和从节点都会维护一个复制偏移量,主节点每次向从节点传递N个字节的时候,会将自己的复制偏移量+N.从节点收到N个字节的数据后,就会将自己的复制偏移量+N.通过2个偏移量的对比可以知道主从节点的数据是否一致,如果不一致就需要进行增量同步!
主节点可以读写。从节点只能读!当有新的从节点加入时,会马上同步主节点的数据!
当节点关闭后,从节点也可以读取数据,毕竟从节点没关闭!
整个同步流程如下:
- 从节点执行replicaof 【ip】 [port] 命令后,从节点会保存主节点的地址信息
- 从节点通过每秒允许的定时任务发现新的主节点后,会尝试与该节点建立网络连接,专门接收主节点发送的复制命令
- 连接成功后,第一次会将主节点的数据进行全量复制,之后采用增量复制,持续将新来的写命令同步给从节点
怎么解除主从模式?
客户端连接6002 ,输入指令:replicaof no one 就可以了!
也可以直接在配置文件中配置主从模式:
在从节点的redis.windows.conf 文件中加入replicaof 127.0.0.1 6001 内容,就可以了,
除了作为主节点的从节点外,也可以作为从节点的从节点,比如增加一个6003 作为6002 的从节点,操作方式和上面一样,
这两种主从方式的区别如下:

但是第二种方式,一旦传播链路中出现问题,就会导致后面的从节点无法及时同步数据!
哨兵模式
上面的主从复制模式,一旦主节点故障,就会导致整个系统崩溃!要是能够监控到主节点的允许状况,就能够采取补救措施,这时就要用到哨兵模式。

哨兵会对所有的节点进行监控,如果发现主节点故障,会立即让从节点进行投票,选举一个新的主节点出来,这样就不会由于主节点故障而导致系统崩溃(要实现这样的功能,最少必须时一主一从模式,再小就没有意义了)。
如何启动一个哨兵?
- 修改一下配置文件:先删除全部内容,然后添加:sentinel monitor ali 127.0.0.1 6001 1
第一个和第二个时固定,第三个时监控对象名称,可以随意命名,后面就是主节点的信息(ip和端口),最后的1 后面说明,然后启动服务器:redis-server.exe redis.windows.conf --sentinel

哨兵模式启动后,会字段监控主节点,还会显示哪些节点是从节点。
此时关闭主节点后,过一段时间,哨兵会将一个从节点选举为主节点,而挂掉的主节点会设置为从节点,当挂掉的节点启动后就当成从节点使用了。
那么从节点见的选举规则是什么呢?
- 首先会根据优先级进行选择,可以在配置文件中设置,添加replica-priority配置项(默认100),值越小优先级越高
- 如果优先级一样,选择偏移量大的
- 要是还选不出来,那就选择runid(启动时随机生成的)最小的。
那要是哨兵挂了怎么办?
可以多设置几个哨兵。只需要将哨兵的配置复制一下,然后修改端口号。就可以同时启动多个哨兵,只不过要把最后一个值改为2,比如:
sentinel monitor ali 127.0.0.1 6001 2
port 2002
这个值代表的额是,当有几个哨兵认为主节点挂掉时,就判断主节点真的挂掉了。
那再哨兵重新选举新的主节点后,Java中的redis客户端怎么感知到呢?
- 先引入依赖:
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>4.2.1</version>
</dependency>
public class Main {
public static void main(String[] args) {
// 直接使用JedisSentinelPool获取Master节点
// 填入三个哨兵地址,注意,如果连不上,就把哨兵的配置文件中的protected-mode属性改为no
JedisSentinelPool pool = new JedisSentinelPool("ali", new HashSet<>(Arrays.asList(
"127.0.0.1:2001", "127.0.0.1:2002", "127.0.0.1:2003"
)));
// 询问并得到Jedis对象,就是master节点的连接
Jedis jedis = pool.getResource();
// 向master节点写入数据
jedis.set("test", "114514");
// 再次获取Jedis对象,得到的也是master节点的连接
Jedis jedis2 = pool.getResource();
// 读取数据,验证是否成功
System.out.println(jedis2.get("test"));
}
}
集群搭建
如果服务器的内存不够用,但是redis需要继续存储内容,这时候就需要使用集群来实现扩容。

此时,面对一个写入请求,数据该写到哪个节点上呢?我们需要先明白集群的机制!
一个redis集群包含16384个插槽,集群中的每个热覅是维护一部分插槽以及插槽送映射的键值对数据,那这个插槽到底是什么呢?
插槽就是Hash计算后的一个结果,这里采用CRC16循环冗余校验,得到16个bit位的数据,就是说算出来的结果是0-65535之间,再进行取模,得到最终结果:
Redis key 的路由计算公式:slot = CRC(Key) % 16384
结果是多少就存放在对应的redis下,比如redis节点1负责0-25565的插槽,这时客户端插入了一个数据a=10.a在hash计算后结果为666.那么a就应该存到节点1。
本质上,就是通过哈希算法将如数分摊到各个节点!
搭建redis集群步骤如下:
- 这里创建6个配置,先在配置文件中开启集群模式,注意修改端口号:
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
6001 6002 6003 这三个是三个主节点。7001 7002 7003 分别是三个从节点
这里的--cluster-replicas 1 指的是每个节点配一个从节点

输入yes执行
此时,集群搭建成功!
此时在保存数据时,可以使用集群方式连接,这样无论在哪个节点插入数据都可以成功!,只需要添加 -c 表示以集群模式连接即可:
redis-cli.exe -p 6001 -c
set a 888
就可以保存成功。
此时,可以输入cluster nodes 来查看当前所有节点信息
当某一个主节点发生故障后,这个主节点的从节点就会升级为新的主节点!之后,当故障节点恢复后会变为当前主节点的从节点!
如果某个节点的主从redis都挂掉了呢?
那当前节点就不可用了,无法在此节点插入数据!
当他们都恢复后,就可以正常使用了。
如何使用Java连接到集群模式下的redis,需要用到JedisCluster对象:
// 和客户端一样,随便连一个就行,也可以多写几个!
JedisCluster cluster = new JedisCluster(new HostAndPort("127.0.0.1",6001));
System.out.println("集群实例数量:"+cluster.getClusterNodes().size());
cluster.set("test", "114514");
System.out.println(cluster.get("test"));
分布式锁
redis实现分布式锁,可以使用setnx key value 命令。
意思是,只有当指定的key不存在的时候,才能进行插入,实际上就是set if not exists
比如:setnx a 999 ,然后再输入setnx a 100 就会报错,因为已经存在key是a 的数据了!
利用这种特定,就可以在不同的服务中实现分布式锁,但是,当某个服务加了锁,但是卡顿或者崩溃了,那这个锁岂不是永远无法释放了?
此时可以加上过期时间:set a 666 EX 5 NX
这里使用的set命令。最后的NX表示使用setnx模式,和上面一样的效果。通过EX设定过期时间,这里设为5秒,表明超过5秒还没释放,就会自动删除锁!
虽然添加过期时间解决了上面的问题,但是也带来了很多麻烦,比如下面这种情况:

因此,单纯的加过期时间,会把别人加的锁给删除,要解决这种问题很简单。
现在的目标是保证任务只能删除自己加的锁,所以可以把a的值设定为任务专属值,比如使用uuid。如果在主动删除锁的时候发现值不是我们当前任务指定的,那么说明可能是因为超时,其他任务已经加锁了,此时就不能删除锁了。

此时还会有一个问题,如果在超时之前那一刹那进入到释放锁阶段,获取的值还是自己的,但是在即将执行删除之前,由于超时机制导致被删除并且其他任务也枷锁了,这时再进行删除,仍会导致删除其他任务加的锁!

本质还是因为锁的超时时间不好衡量,如果超时时间能够设定恰当,就可以避免这种问题了。
这时就需要Redisson框架,它是Redis官方推荐的Java版的Redis客户端。
Redisson内部提供了一个监控锁的看门狗,作用是再Redisson实例被关闭前,不断的延长锁的有效期!
使用步骤如下:
- 先引入依赖:
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.5.0</version>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
</dependency>
- 编写代码:
public static void main(String[] args) {
Config config = new Config();
// 连接单机模式的Redis服务器,也可以指定集群
config.useSingleServer().setAddress("redis://127.0.0.1:6379");
// 创建RedissonClient实例,内部会创建连接池
RedissonClient redisson = Redisson.create(config);
for (int i = 0; i < 10; i++) {
new Thread(()-> {
Jedis jedis = new Jedis("127.0.0.1", 6379);
// 指定锁名称,拿到锁对象
RLock lock = redisson.getLock("testLock");
for (int j = 0; j < 100; j++) {
lock.lock(); // 加锁
int a = Integer.parseInt(jedis.get("a")) +1;
jedis.set("a", String.valueOf(a));
lock.unlock(); // 解锁
}
}).start();
}
}
注意:如果存放锁的Redis服务器挂了,那么肯定会出问题,这时候,可以使用RedLock(RLock 类),它的思路是,在多个Redis服务器上保存锁,只要超过半数的Redis服务器获取到锁,那就真的获取到锁了,这样就算挂掉一部分节点,也能正常运行。
本文来自博客园,作者:NE_STOP,转载请注明原文链接:https://www.cnblogs.com/alineverstop/p/19815522
浙公网安备 33010602011771号