架构

一主多从,主用来处理写请求,从用来处理读请求。

主从复制,读写分离。最低一主二从,因为后面的哨兵模式会进行选举,如果只有一个从就无法进行选举。

默认情况下,每个服务器都是Master,除非进行配置。
image.png

主从复制的作用

  1. 故障恢复:当主节点出现问题时,从结点提供服务,实现快速的故障恢复;
  2. 负载均衡:分担服务器压力,大大提高redis的并发量;
  3. 高可用基石:是哨兵和集群能够实施的基础,所以主从复制是Redis高可用的基础;

如何在一台机器上模拟

环境配置

连接上Redis后,通过info replication查看当前库的信息:
image.png

  1. 需要复制出多个配置文件;
  2. 修改配置文件中会重名的地方,例如: ```bash

    端口号

    port 6379

pidfile

pidfile /var/run/redis_xxxx.pid

logfile

logfile “xxxxx.log”

dump文件

dbfilename dumpxxx.rdb

  1. 启动的时候,通过加载不同的配置文件来启动多个redis服务器
  2. <a name="OKl6t"></a>
  3. ## 配置主从关系
  4. 在不配置的条件下,都是Master。<br />方式就是认老大,即让从机找老大即可。命令是SLAVEOF + IP + 端口
  5. ```bash
  6. # 在连接Redis以后,运行以下命令,就可以认127.0.0.1的6379端口对应的Redis为Master
  7. SLAVEOF 127.0.0.1 6379
  8. slave-read-only yes # 开启从结点只能只读
  9. # 或者新版本
  10. REPLICAOF 127.0.0.1 6379
  11. replica-read-only yes

PS:通过命令配置主从关系是暂时的,应该去配置文件里进行配置,每次启动的时候就自动形成主从关系了。在配置文件里有:

slaveof <masterip> <masterport>
slave-append-only yes


# 或者
replicaof <masterip> <masterport>
replica-append-only yes

取决于具体版本的配置文件,可以去看注释。

如果主机有密码,则配置:

masterauth <master-password>

细节

  1. Master可以写和读,但是从机只能读。在Master里写入时,会自动同步到从机。

从机不能写!
image.png

  1. 如果主机断开连接后又回来了,主从关系仍然可以正常运作。
  2. 如果从机断了以后又连接回来了,采用的是配置文件,那么从机能拿到主机的所有数据。

原因:
Slave启动成功连接到Master后会发送一个sync同步命令。
Master收到命令后,启动后台的存盘进程,保存一个rdb文件,在后台执行完毕之后,master将传送整个数据文件到slave,完成一次同步。

既做Master,也做Slave

A - B - C的模式
B既是主节点,也是从结点,但是主要A - B之间没断开,那么B优先作为从结点,不能写。

如果A - B 之间断开了,在B中使用:

SLAVEOF no one

就可以让B自己成为主机,但是该过程是手动的。因此我们需要哨兵模式来自动选择Master。

主从结构具体流程

  1. Slave想Master发送psync同步数据,发送命令之前会跟master建立socket长连接;
  2. 主节点收到psync命令,执行bgsave生成rdb快照数据;
    1. 注意,在生成快照文件的过程中,可能会有一些新数据过来,rdb会把这些新数据缓存起来,缓存的是写命令,而非rdb数据,有点像aof;
  3. 将rdb数据发送给slave;
  4. slave会把之前的老数据清空,加载主节点发送过来的rdb;
  5. Master将主节点缓存的写命令发到slave;
  6. Slave执行缓存的命令,完成所有的同步。

image.png
以上过程称为全量同步;

在全量同步以后:
Master通过Socket长连接把写命令发送给Slave,保证主从数据一致性。

主从复制风暴

如果一个主节点有很多从结点,那么多个从节点同时从一个主节点进行复制,导致主节点压力过大。为了缓解主从复制风暴,通常可以做以下架构优化。

image.png

管道

即一次性发送多条命令到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脚本

  1. Redis自带的事务很鸡肋,不能保证原子性,但是Lua脚本可以保证原子性;
  2. 减少网络开销,类似管道,一次传输多条命令到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);

image.png

可以看到,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);

image.png
image.png
可以看到,整个Lua脚本都没有执行。

使用Lua脚本的注意事项

  1. 由于Redis的数据写入单线程的,所以千万不要在Lua脚本里出现死循环或者耗时的运算,否则会阻塞掉后面的命令。

哨兵模式(重点)

自动选举老大的模式

原理,Redis启动一个独立的哨兵进程,它发送命令给Redis服务器,等待服务器响应,从而监控运行多个运行实例。

image.png

多哨兵模式

单哨兵模式有个问题,就是有可能单个哨兵也会死亡。因此我们可以采用多哨兵模式:
image.png

假设哨兵1检测到Master挂了,系统不会马上认为Master挂了去做failover过程,该现象称为主观下线

只有当后面的哨兵也检测到主服务器不可用,并且数量达到一定值时,才认为Master真的挂了,从而由哨兵进行投票选举新的Master,进行failover操作。

切换成功后,通过发布订阅模式,让各个哨兵把自己监控的从服务器实现切换主机,这个过程称为客观下线

配置哨兵

  1. 安装redis sentinel

    sudo apt install redis-sentinel
    

    安装成功后,通过sudo vim /etc/redis/sentinel.conf可以查看配置文件。

  2. 复制一份配置文件并修改; ```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
  1. 检测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访问哨兵

  1. 配置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);
            }
        }
    }
}
  1. 启动项目 ```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>
  1. 访问localhost:8080/test_sentinel
  2. 关闭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