架构
一主多从,主用来处理写请求,从用来处理读请求。
主从复制,读写分离。最低一主二从,因为后面的哨兵模式会进行选举,如果只有一个从就无法进行选举。
默认情况下,每个服务器都是Master,除非进行配置。
主从复制的作用
- 故障恢复:当主节点出现问题时,从结点提供服务,实现快速的故障恢复;
- 负载均衡:分担服务器压力,大大提高redis的并发量;
- 高可用基石:是哨兵和集群能够实施的基础,所以主从复制是Redis高可用的基础;
如何在一台机器上模拟
环境配置
连接上Redis后,通过info replication
查看当前库的信息:
pidfile
pidfile /var/run/redis_xxxx.pid
logfile
logfile “xxxxx.log”
dump文件
dbfilename dumpxxx.rdb
启动的时候,通过加载不同的配置文件来启动多个redis服务器
<a name="OKl6t"></a>
## 配置主从关系
在不配置的条件下,都是Master。<br />方式就是认老大,即让从机找老大即可。命令是SLAVEOF + IP + 端口
```bash
# 在连接Redis以后,运行以下命令,就可以认127.0.0.1的6379端口对应的Redis为Master
SLAVEOF 127.0.0.1 6379
slave-read-only yes # 开启从结点只能只读
# 或者新版本
REPLICAOF 127.0.0.1 6379
replica-read-only yes
PS:通过命令配置主从关系是暂时的,应该去配置文件里进行配置,每次启动的时候就自动形成主从关系了。在配置文件里有:
slaveof <masterip> <masterport>
slave-append-only yes
# 或者
replicaof <masterip> <masterport>
replica-append-only yes
取决于具体版本的配置文件,可以去看注释。
如果主机有密码,则配置:
masterauth <master-password>
细节
- Master可以写和读,但是从机只能读。在Master里写入时,会自动同步到从机。
从机不能写!
- 如果主机断开连接后又回来了,主从关系仍然可以正常运作。
- 如果从机断了以后又连接回来了,采用的是配置文件,那么从机能拿到主机的所有数据。
原因:
Slave启动成功连接到Master后会发送一个sync同步命令。
Master收到命令后,启动后台的存盘进程,保存一个rdb文件,在后台执行完毕之后,master将传送整个数据文件到slave,完成一次同步。
既做Master,也做Slave
A - B - C的模式
B既是主节点,也是从结点,但是主要A - B之间没断开,那么B优先作为从结点,不能写。
如果A - B 之间断开了,在B中使用:
SLAVEOF no one
就可以让B自己成为主机,但是该过程是手动的。因此我们需要哨兵模式来自动选择Master。
主从结构具体流程
- Slave想Master发送psync同步数据,发送命令之前会跟master建立socket长连接;
- 主节点收到psync命令,执行bgsave生成rdb快照数据;
- 注意,在生成快照文件的过程中,可能会有一些新数据过来,rdb会把这些新数据缓存起来,缓存的是写命令,而非rdb数据,有点像aof;
- 将rdb数据发送给slave;
- slave会把之前的老数据清空,加载主节点发送过来的rdb;
- Master将主节点缓存的写命令发到slave;
- Slave执行缓存的命令,完成所有的同步。
以上过程称为全量同步;
在全量同步以后:
Master通过Socket长连接把写命令发送给Slave,保证主从数据一致性。
主从复制风暴
如果一个主节点有很多从结点,那么多个从节点同时从一个主节点进行复制,导致主节点压力过大。为了缓解主从复制风暴,通常可以做以下架构优化。
管道
即一次性发送多条命令到redis, 让redis进行批量运行。
管道的优点
如果不采用管道,那么10条命令要进行10次网络传输;
采用管道,那么10条命令只需要1次网络传输,大大提升了效率。
// ********测试管道********
Pipeline pl = jedis.pipelined();
for (int i = 0; i < 10; i++) {
pl.incr("pipelineKey");
pl.set("lang" + i, "lang");
}
List<Object> results = pl.syncAndReturnAll();
System.out.println(results);
管道没有原子性
管道只是把所有命令一次性传过去,然后一个一个执行,如果有某一条命令执行失败,其他命令照常执行。
Redis Lua脚本
为什么需要Lua脚本
- Redis自带的事务很鸡肋,不能保证原子性,但是Lua脚本可以保证原子性;
- 减少网络开销,类似管道,一次传输多条命令到redis进行执行。
Lua脚本实例
jedis.set("product_stock", "15");
String script = "local count = redis.call('get', KEYS[1])" +
"local a = tonumber(count)" +
"local b = tonumber(ARGV[1])" +
"if a >= b then " +
" redis.call ('set', KEYS[1], a - b)" +
//模拟语法回滚 "bb == 0" +
" return 1" +
" end " +
" return 0";
Object obj = jedis.eval(script, Arrays.asList("product_stock"), Arrays.asList("10"));
System.out.println(obj);
可以看到,product_stock的值为5, 因为初始化时为15, 在Lua脚本里成功执行后,减掉了10,所以等于5.
如果我们让Lua脚本报错
jedis.set("product_stock", "15");
String script = "local count = redis.call('get', KEYS[1])" +
"local a = tonumber(count)" +
"local b = tonumber(ARGV[1])" +
"if a >= b then " +
" redis.call ('set', KEYS[1], a - b)" +
//模拟语法回滚
"bb == 0" +
" return 1" +
" end " +
" return 0";
Object obj = jedis.eval(script, Arrays.asList("product_stock"), Arrays.asList("10"));
System.out.println(obj);
可以看到,整个Lua脚本都没有执行。
使用Lua脚本的注意事项
- 由于Redis的数据写入单线程的,所以千万不要在Lua脚本里出现死循环或者耗时的运算,否则会阻塞掉后面的命令。
哨兵模式(重点)
自动选举老大的模式
原理,Redis启动一个独立的哨兵进程,它发送命令给Redis服务器,等待服务器响应,从而监控运行多个运行实例。
多哨兵模式
单哨兵模式有个问题,就是有可能单个哨兵也会死亡。因此我们可以采用多哨兵模式:
假设哨兵1检测到Master挂了,系统不会马上认为Master挂了去做failover过程,该现象称为主观下线。
只有当后面的哨兵也检测到主服务器不可用,并且数量达到一定值时,才认为Master真的挂了,从而由哨兵进行投票选举新的Master,进行failover操作。
切换成功后,通过发布订阅模式,让各个哨兵把自己监控的从服务器实现切换主机,这个过程称为客观下线。
配置哨兵
安装
redis sentinel
:sudo apt install redis-sentinel
安装成功后,通过
sudo vim /etc/redis/sentinel.conf
可以查看配置文件。复制一份配置文件并修改; ```bash cp sentinel.conf sentinel-26379.conf
修改配置
sudo vim sentinel-26379.conf port 26379 deamonize yes pidfile “/var/run/redis-sentinel-26379.pid” logfile “26379.log”
可能需要提前建立文件夹
dir “/usr/local/redis-26379”
sentinel monitor
sentinel monitor mymaster 192.168.31.27 6379 2
quorum表示有多少个sentinel认为一个master失效时,才算真正的失效,这里我们只配置3个sentinel
所以设置成2表示有半数认为失效就是真正的失效。
3. 运行sentinel
```bash
sudo redis-sentinel sentinel-26379.conf
- 检测sentinel是否搭建成功 ```bash vim /etc/redis/sentinel-26379.conf
拉到最地下可以看到,检测出了slave结点的信息。
![image.png](https://cdn.nlark.com/yuque/0/2021/png/12689050/1630227720999-bf0057e0-2eb5-422e-b0a2-9b5bd900db2c.png#clientId=ube72c344-ec9f-4&from=paste&height=147&id=u3eee11b5&margin=%5Bobject%20Object%5D&name=image.png&originHeight=102&originWidth=539&originalType=binary&ratio=1&size=17388&status=done&style=none&taskId=u5698a596-4941-4075-9238-9eb1a678847&width=774.5)
<a name="sRsav"></a>
# Jedis访问哨兵
搭建哨兵以后,哨兵会将主节点信息推送给客户端。
1. 首先,关闭掉配置里的protectedmode,以及注释掉`bind 127.0.0.1`
1. 以下测试代码。
```java
package com.lang.jedis;
import redis.clients.jedis.*;
import java.util.HashSet;
import java.util.Set;
public class JedisSentinelTest {
public static void main(String[] args) {
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
jedisPoolConfig.setMaxTotal(20);
jedisPoolConfig.setMaxIdle(10);
jedisPoolConfig.setMinIdle(5);
String masterName = "mymaster";
Set<String> sentinels = new HashSet<>();
sentinels.add(new HostAndPort("192.168.31.27", 26379).toString());
sentinels.add(new HostAndPort("192.168.31.27", 26380).toString());
sentinels.add(new HostAndPort("192.168.31.27", 26381).toString());
JedisSentinelPool jedisSentinelPool = new JedisSentinelPool(masterName, sentinels, jedisPoolConfig, 3000, null);
Jedis jedis = null;
try {
jedis = jedisSentinelPool.getResource();
System.out.println(jedis.set("sentinel-888", "lang"));
System.out.println(jedis.get("sentinel-888"));
} catch (Exception e) {
e.printStackTrace();
} finally {
// 注意这里不是关闭连接,在JedisPool模式下,Jedis会把资源归还给连接池。
if (jedis != null) {
jedis.close();
}
}
}
}
哨兵自动选举
此处用spring boot访问哨兵
- 配置applications.yml ```java server: port: 8080
spring: redis: database: 0 timeout: 3000 sentinel: master: mymaster nodes: 192.168.31.27:26379, 192.168.31.27:26380, 192.168.31.27:26381 lettuce: pool: max-idle: 50 min-idle: 10 max-active: 100 max-wait: 1000
2. Controller
```java
package com.example.redisbootsentineltest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class IndexController {
private static final Logger logger = LoggerFactory.getLogger(IndexController.class);
@Autowired
private StringRedisTemplate stringRedisTemplate;
@RequestMapping("/test_sentinel")
public void testSentinel() {
int i = 1;
while (true) {
try {
stringRedisTemplate.opsForValue().set("sentinel_test" + i, i + "");
System.out.println("设置key: " + i);
i++;
Thread.sleep(1000);
} catch (Exception e) {
logger.error("错误", e);
}
}
}
}
- 启动项目 ```java package com.example.redisbootsentineltest;
import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication public class RedisBootSentinelTestApplication {
public static void main(String[] args) {
SpringApplication.run(RedisBootSentinelTestApplication.class, args);
}
}
记得添加依赖
```java
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>2.4.3</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-pool2 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>2.9.0</version>
</dependency>
- 访问
localhost:8080/test_sentinel
- 关闭master结点,等待一段时间后观察,某个slave被选举成新的master。
这里出现了一个问题,当我配置quorum为2时,没有进行选举,改成1才选举了。一开始不明白原因,后来发现是sentinel之间都设置的是同样的ID(因为配置文件是复制粘贴的),没有互相感知到,导致无法进行大于2的投票。
# sentinel monitor <master-name> <ip> <redis-port> <quorum>
sentinel monitor mymaster 192.168.31.27 6379 2
解决方式是:
将设置的ID注释掉。
# sentinel myid ffd6a08d341ddb001ed705c93ab8977be23799c2