Redis主从, 哨兵, Lettuce(二)

主从复制(Master& Slave)

作用

  1. 数据备份: 除 RDB& AOF(持久化)以外的数据备份方式
  2. 故障恢复: 当主节点(Master)出现问题时, 可以将从节点(Slave)用于主节点, 及时的恢复服务, 实现服务的高可用
  3. 读写分离: 主节点写, 从节点读, 实现读写分离, 提高服务器的负载能力
  • 除此之外, 主从复制也是 Redis集群的基础

同步过程/ 复制原理

  • 全量同步: 发生在从节点初始化阶段, 将主节点的所有数据做成快照(RDB)文件, 发送给从节点过程; *当数据量较大时, 会对主从节点和网络带宽造成很大的开销

  • 增量同步: Redis的增量同步是指从节点完成初始化后开始正常工作时, 主节点将新产生的写操作命令同步到从节点, 然后在从节点执行的过程

具体过程如下:

  1. 从节点发起 SYNC命令到主节点
  2. 主节点收到 SYNC命令后, 执行 BGSAVE命令, 在异步进程内生成快照(RDB)文件, 并使用缓冲区记录同时阶段产生的所有写操作命令
  3. 主节点生成完快照(RDB)文件后, 将该文件发送到从节点, 在此发送期间产生的所有写操作命令继续记录
  4. 从节点收到快照(RDB)文件后, 丢弃所有旧数据, 并载入收到的快照
  5. 主节点发送完快照文件后, 开始向从节点发送缓冲区内记录的(全量同步期间额外记录的)写操作命令
  6. 从节点完成对快照的载入后, 开始接收来自主节点的操作命令并执行
  • 完成全量同步后, 如无特殊情况一直是增量同步, 就比如从节点断线服务中断一段时间后重启, Redis会尽可能首先尝试增量同步, 否者, 全量同步. 还有主节点, 因同步时需要开辟额外的缓冲区来记录命令等操作, 所以实际场景中得考虑好主节点的内存剩余空间

Redis单台服务默认是主节点, 通过命令方式或配置方式, 可以指向其它节点(被指向的节点可以是主也可以是从), 同时当前节点会变成从节点; 主节点可以有多个从节点, 但从节点只能有一个主节点; 数据的复制是单向的, 只能主节点到从节点

Redis配置

基本参数


# 外部网络是否可以连接服务 yes启用(默认)/ no
protected-mode yes

# Redis服务端口
port 6379

# 是否守护进程运行 yes/ no(默认)
daemonize no

# 是否通过 supervised管理守护进程 no不使用(默认)/ upstart/ systemd/ auto
supervised no

# Redis进程编号存储文件& path
pidfile "/var/run/redis_6379.pid"

RDB持久化相关参数

  • https://blog.csdn.net/qcl108/article/details/106879002#RDB_AOF_36

# RDB持久化文件名
dbfilename "dump_6379.rdb"

# 数据库存入目录
dir "/Users/quanchunlin/Desktop/redis-6.0.5/conf"

REPLICATION(复制/主从同步)相关参数


# 配置形式指向节点与命令行的 slaveof相同的效果
replicaof <masterip> <masterport>

# 设置主节点的密码
masterauth <master-password>

# 当前节点为从节点时, 将会无法执行写操作命令 yes(默认)/ no
replica-read-only yes

# 主从同步策略: disk与 socket(diskless/无磁盘方式 此方式还处于实验阶段). yes/ no不启用(默认)
repl-diskless-sync no

# 从节点指定往主节点发送 Ping的时间间隔. 10秒(默认)
# repl-ping-replica-period 10

# 同步的超时时间
# repl-timeout 60

# 是否启用 TCP_NODELAY, no不开启(默认)/yes. 如果开启则会使用少量的 TCP包进行数据传输到从节点, 速度会比较慢; 如不开启传输速度会比较快, 但会占用较多的带宽
repl-disable-tcp-nodelay no

# 设置缓冲区大小, 在从节点失连时主节点存放要同步到从节点的数据, 设置的越大, 从节点可以失连的时间就越长
# repl-backlog-size 1mb

# 如果一段时间内没有从节点连接主节点, 则会释放缓冲区. 设置0则表示永不释放缓冲区
# repl-backlog-ttl 3600

# 当主节点无法工作时哨兵(Sentinel)通过这个值来决定将哪个从(Slave)优先提升为主节点, 值越小表示越优先提升, 但设置为0表示, 此节点将永远不能被提升为主节点. 100(默认)
replica-priority 100

# 当 Master少于3个, 延时小于等于10秒的已连接 Slave, 就可以停止接收写操作
# 1. 至少需要3个 Slave的状态为 oneline
# 2. 延时是以秒为单位, 且必须小于等于指定值, 是从最后一个 Slave接收到的 ping(通常每秒发送)开始计数
# min-replicas-to-write 3
# min-replicas-max-lag 10

SECURITY(安全)相关参数


# 当前服务节点密码
# requirepass foobared

# 命令重命名: 用于危险命令改变名字. 注: 该命令记录到 AOF文件后被传送到从服务器可能产生问题
# rename-command CONFIG ""

AOF(Append Only File)持久化相关参数

  • https://blog.csdn.net/qcl108/article/details/106879002#RDB_AOF_36

# yes开启/ no关闭(默认)
appendonly no

# AOF文件名 (default: "appendonly.aof")
appendfilename "appendonly_6379.aof"

LUA脚本


# Lua脚本的最大执行时间, 毫秒为单位
lua-time-limit 5000

SLOW LOG(慢查询日志)脚本


# 记录超过多少微秒的查询命令. 查询的执行时间不包括客户端的 I/O执行和网络通信时间, 只是查询命令执行时间
# 1000000等于1秒, 设置为0则记录所有命令
slowlog-log-slower-than 10000

# 记录大小,可通过SLOWLOG RESET命令重置
slowlog-max-len 128

多服务单机简单部署

  • 创建三个 redis.conf环境, 1个 Master, 2个 Slave. 放到同一个目录

# 主节点 redis_6379.conf
port 6379
dbfilename dump_6379.rdb

# 从节点 redis_6380.conf
port 6380
dbfilename dump_6380.rdb
replicaof 127.0.0.1 6379 # 可以启动后手动在 redis-cli, 通过 slaveof命令指向主节点

# 从节点 redis_6381.conf
port 6381
dbfilename dump_6381.rdb
replicaof 127.0.0.1 6379 # 可以启动后手动在 redis-cli, 通过 slaveof命令指向主节点

  • 依次启动: …/src/redis-server redis-6379.conf, …/src/redis-server redis-6380.conf, …/src/redis-server redis-6381.conf
  • 启动后可以通过 info replication命令查看主从信息

哨兵模式(Reids Sentinel)

作用

  • 在多从单主架构中, 一旦主节点由于故障不能提供服务时, 通过哨兵(Sentinel)模式, 将其中一个从节点自动晋升为主节点, 同时通知其它从节点重新指向新的主节点, 来维持高可用 Redis服务

原理& 过程

  1. 主观下线
  • 每隔一秒, 每个 Sentinel会向主节点或从节点做心跳检测, 当一个主节点心跳反应超过 sentinel down-after-milliseconds设的毫秒数, Sentinel将会对该节点做失败判定并标注
  1. 客观下线
  • 当主观下线的节点为主节点时, 该 Sentinel会向其它 Sentinel询问对主节点的判断, 当至少 sentinel monitor的数 Sentinel同意判定时, 该 Sentinel会做出客观下线的决定
  1. 领导者 Sentinel选举
  • Raft算法实现: 假设 s1(sentinel-1)最先完成客观下线, 它会向其余 Sentinel发送命令, 请求成为领导者, 收到请求的 Sentinel如果没有同意过其它 Sentinel的请求, 那么就会同意 s1的请求, 成为领导者
  1. 故障转移
  • 领导者 Sentinel在多个从节点中选出一个节点作为新的主节点, 选与前主节点数据相似度最高的从节点, 然后将其它从节点重新指向新的主节点, Sentinel集群也会将原主节点改为从节点并继续监控, 当其恢复后命令它去复制新的主节点

Redis Sentinel配置


# 外部网络是否可以连接服务 yes启用/ no(默认)
protected-mode no

# Redis Sentinel服务端口
port 26379

# 是否守护进程运行 yes/ no(默认)
daemonize no

# Redis Sentinel进程编号存储文件& path
pidfile "/var/run/redis-sentinel-26379.pid"

# 日志文件
logfile "/var/log/sentinel-26379.log"

# 工作目录
dir "/Users/quanchunlin/Desktop/redis-6.0.5/conf"

# 设置<master-name>标识集群名称可以自定义, 设置<password>连接主从节点时的密码; 注: Sentinel无法为主从分别设置密码, 所以需将密码都设置一样的
# sentinel auth-pass <master-name> <password>

# 监听地址为 <ip> <redis-port>的主节点, <quorum>数是有多少个 Sentinel同意当前主节点失效时, 才会被当前 Sentinel集群认为是客观下线
sentinel monitor <master-name> <ip> <redis-port> <quorum>

# 当主备切换时, 某台从节点晋升为新的主节点时, 同时其它从节点被重新指向新的主节点做主从同步, <numreplicas>值越小同步完成时间越长, 但如果值设的过大, 可能会导致主节点阻塞
# sentinel parallel-syncs <master-name> <numreplicas>

# Specifies the failover timeout in milliseconds. It is used in many ways:
#
# - The time needed to re-start a failover after a previous failover was
#   already tried against the same master by a given Sentinel, is two
#   times the failover timeout.
#
# - The time needed for a replica replicating to a wrong master according
#   to a Sentinel current configuration, to be forced to replicate
#   with the right master, is exactly the failover timeout (counting since
#   the moment a Sentinel detected the misconfiguration).
#
# - The time needed to cancel a failover that is already in progress but
#   did not produced any configuration change (SLAVEOF NO ONE yet not
#   acknowledged by the promoted replica).
#
# - The maximum time a failover in progress waits for all the replicas to be
#   reconfigured as replicas of the new master. However even after this time
#   the replicas will be reconfigured by the Sentinels anyway, but not with
#   the exact parallel-syncs progression as specified.
# sentinel failover-timeout <master-name> <milliseconds>

# 心跳反应需要超过多少失效时间, Sentinel才将一个主节点主观的地判定是不可用的. 30000毫秒(默认)
sentinel down-after-milliseconds <master-name> <milliseconds>

# 通知脚本: 当有警告级别的事件发生时(比如说 Redis实例的主观失效和客观失效等)将会调用此脚本, 一般用于发送邮件或 SMS等方式通知系统管理员. 调用该脚本时, 将传给脚本两个参数, 一个是事件的类型, 一个是事件的描述
# sentinel notification-script <master-name> <script-path>

# 当主备切换时, 将会自动调用此脚本. 调用该脚本时, 将传给脚本以下参数 <master-name> <role> <state> <from-ip> <from-port> <to-ip> <to-port>
# sentinel client-reconfig-script <master-name> <script-path>

多服务单机简单部署

  • 创建三个 sentinel.conf环境

# sentinel-26379.conf
port 26379
sentinel monitor mymaster 127.0.0.1 6379 2
sentinel down-after-milliseconds mymaster 10000

# sentinel-26380.conf
port 26380
sentinel monitor mymaster 127.0.0.1 6379 2
sentinel down-after-milliseconds mymaster 20000

# sentinel-26381.conf
port 26381
sentinel monitor mymaster 127.0.0.1 6379 2
sentinel down-after-milliseconds mymaster 30000

  • 依次启动: …/src/redis-sentinel sentinel-26379.conf, …/src/redis-sentinel sentinel-26380.conf, …/src/redis-sentinel sentinel-26381.conf

Spring boot Lettuce使用

Jar包


        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>

配置


# 使用的数据库索引编号
spring.redis.database=0
# 连接池最大连接数(-1表示不限制), 默认为8
spring.redis.lettuce.pool.max-active=1000
# 连接池最大阻塞等待时间(-1表示不限制, 单位毫秒ms), 默认为-1
spring.redis.lettuce.pool.max-wait=-1
# 连接池中的最大空闲连接, 默认为8
spring.redis.lettuce.pool.max-idle=200
# 连接池中的最小空闲连接, 默认为0
spring.redis.lettuce.pool.min-idle=100
# 关闭连接前等待任务处理完成的最长时间, 默认为100ms
spring.redis.lettuce.shutdown-timeout=100ms
# 哨兵设定的主节点名称
spring.redis.sentinel.master=mymaster
# 哨兵节点, 多节点按(,)号分割
spring.redis.sentinel.nodes=127.0.0.1:26379,127.0.0.1:26380,127.0.0.1:26381


实例


@Configuration
public class RedisConfig {
    @Bean
    public RedisTemplate redisTemplate(RedisConnectionFactory factory) {
        final RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(factory);

        /** 将默认 Jdk序列化替换成 StringRedisSerializer*/
        final StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
        template.setKeySerializer(stringRedisSerializer);
        template.setHashKeySerializer(stringRedisSerializer);

        /** 将默认 Jdk序列化替换成 Jackson2JsonRedisSerialize*/
        final Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        final ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);
        template.setValueSerializer(jackson2JsonRedisSerializer);
        template.setHashValueSerializer(jackson2JsonRedisSerializer);
        template.afterPropertiesSet();
        return template;
    }

}

@Component
public class RedisUtil {
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    /** 设置过期时间(秒)*/
    public boolean expire(String key, long time) {
        if (time > 0) {
            return redisTemplate.expire(key, time, TimeUnit.SECONDS);
        }
        return true;
    }

    /** 获取过期时间(秒)
     * @return 返回0表示永久有效
     * */
    public long getExpire(String key) {
        return redisTemplate.getExpire(key, TimeUnit.SECONDS);
    }

    /** 判断 key是否存在*/
    public boolean hasKey(String key) {
        return redisTemplate.hasKey(key);
    }

    /** 删除缓存*/
    @SuppressWarnings("unchecked")
    public void del(String... key) {
        if (key != null && key.length > 0) {
            if (key.length == 1) {
                redisTemplate.delete(key[0]);
            } else {
                redisTemplate.delete(CollectionUtils.arrayToList(key));
            }
        }
    }

    /**
     * String(字符串)
     * */

    /** 获取指定缓存*/
    public Object get(String key) {
        return key == null ? null : redisTemplate.opsForValue().get(key);
    }

    /** 设置缓存*/
    public void set(String key, Object value) {
        redisTemplate.opsForValue().set(key, value);
    }

    /** 设置缓存, 附加过期时间(秒)*/
    public void set(String key, Object value, long time) {
        if (time > 0) {
            redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
        } else {
            set(key, value);
        }
    }

    /**
     * Hash(哈希)
     * */

    /** 获取 hash里面指定字段的值*/
    public Object hmget(String key, String field) {
        return redisTemplate.opsForHash().get(key, field);
    }

    /** 从 hash中读取全部的域和值*/
    public Map<Object, Object> hgetall(String key) {
        return redisTemplate.opsForHash().entries(key);
    }

    /** 设置一个 hash字段值, 如不存在将会创建*/
    public void hset(String key, String field, Object value) {
        redisTemplate.opsForHash().put(key, field, value);
    }

    /** 设置一个 hash字段值, 如不存在将会创建, 附加过期时间(秒)*/
    public void hset(String key, String field, Object value, long time) {
        redisTemplate.opsForHash().put(key, field, value);
        if (time > 0) {
            expire(key, time);
        }
    }

    /** 设置多个 hash字段值*/
    public void hmset(String key, Map<String, Object> map) {
        redisTemplate.opsForHash().putAll(key, map);
    }

    /** 设置多个 hash字段值, 附加过期时间(秒)*/
    public void hmset(String key, Map<String, Object> map, long time) {
        redisTemplate.opsForHash().putAll(key, map);
        if (time > 0) {
            expire(key, time);
        }
    }

    /** 删除一个或多个 hash的 field*/
    public void hdel(String key, Object... field) {
        redisTemplate.opsForHash().delete(key, field);
    }

    /** 判断 field是否存在于 hash中*/
    public boolean hexists(String key, String field) {
        return redisTemplate.opsForHash().hasKey(key, field);
    }

    /**
     * List(列表)
     * */

    /** 从列表中获取指定返回的元素*/
    public List<Object> lrange(String key, long start, long end) {
        return redisTemplate.opsForList().range(key, start, end);
    }

    /** 获取 key对应的 list的长度*/
    public long llen(String key) {
        return redisTemplate.opsForList().size(key);
    }

    /** 获取一个元素, 通过其索引列表*/
    public Object lindex(String key, long index) {
        return redisTemplate.opsForList().index(key, index);
    }

    /** 从队列的右边入队一个元素*/
    public long rpush(String key, Object value) {
        return redisTemplate.opsForList().rightPush(key, value);
    }

    /** 从队列的右边入队一个元素, 附加过期时间(秒)*/
    public long rpush(String key, Object value, long time) {
        Long count = redisTemplate.opsForList().rightPush(key, value);
        if (time > 0)
            expire(key, time);
        return count;
    }

    /** 从队列的右边入队多个元素*/
    public long rpush(String key, List<Object> value) {
        return redisTemplate.opsForList().rightPushAll(key, value);
    }

    /** 从队列的右边入队多个元素, 附加过期时间(秒)*/
    public long rpush(String key, List<Object> value, long time) {
        Long count = redisTemplate.opsForList().rightPushAll(key, value);
        if (time > 0)
            expire(key, time);
        return count;
    }

    /** 从队列的右边出队一个元*/
    public Object rpop(String key) {
        return redisTemplate.opsForList().rightPop(key);
    }

    /** 从队列的左边入队一个元素*/
    public long lpush(String key, Object value) {
        return redisTemplate.opsForList().leftPush(key, value);
    }

    /** 从队列的左边入队一个元素, 附加过期时间(秒)*/
    public long lpush(String key, Object value, long time) {
        Long count = redisTemplate.opsForList().leftPush(key, value);
        if (time > 0)
            expire(key, time);
        return count;
    }

    /** 从队列的左边入队多个元素*/
    public long lpush(String key, List<Object> value) {
        return redisTemplate.opsForList().leftPushAll(key, value);
    }

    /** 从队列的左边入队多个元素, 附加过期时间(秒)*/
    public long lpush(String key, List<Object> value, long time) {
        Long count = redisTemplate.opsForList().leftPushAll(key, value);
        if (time > 0)
            expire(key, time);
        return count;
    }

    /** 从队列的左边出队一个元素*/
    public Object lpop(String key) {
        return redisTemplate.opsForList().leftPop(key);
    }

    /**
     * Sorted Set(有序集合)
     * */

    /** 添加到有序set的一个成员, 或更新的分数, 如果它已经存在*/
    public boolean zadd(String key, Object value, double scoure) {
        return redisTemplate.opsForZSet().add(key, value, scoure);
    }

    /** 添加到有序set的一个成员, 或更新的分数, 如果它已经存在, 附加过期时间(秒)*/
    public boolean zadd(String key, Object value, double scoure, long time) {
        final Boolean flag = redisTemplate.opsForZSet().add(key, value, scoure);
        if (time > 0)
            expire(key, time);
        return flag;
    }

    /** 获取一个排序的集合中的成员数量*/
    public long zcard(String key) {
        return redisTemplate.opsForZSet().zCard(key);
    }

    /** 根据指定的index返回,返回sorted set的成员列表*/
    public Set<Object> zrange(String key, long start, long stop) {
        return redisTemplate.opsForZSet().range(key, start, stop);
    }

    /** 从排序的集合中删除一个或多个成员*/
    public long zrem(String key, Object...value) {
        return redisTemplate.opsForZSet().remove(key, value);
    }

}

    @Autowired
    private RedisUtil redisUtil;

    redisUtil.set("string", "普通字符串值!");
    log.info("string: {}", redisUtil.get("string"));


如果您觉得有帮助,欢迎点赞哦 ~ 谢谢!!

猜你喜欢

转载自blog.csdn.net/qcl108/article/details/107052239
今日推荐