Redis高级2


1. 简介

Redis前面的基础部分此处不做记录 , 本篇记录狂神说讲解的Redis配置及高级应用的知识.

Redis官网

Redis中文网

https://redis.com.cn/

http://www.redis.cn/

https://www.redis.net.cn/

2. 特殊数据类型

2.1 geospatial 地理位置

朋友的定位, 附近的人, 打车距离计算?

Redis3.2版本新增了Geo. 这个功能可以推算地理位置的信息, 两地之间的距离, 方圆多少距离内的人等.

命令描述
Redis GEOHASH 命令返回一个或多个位置元素的 Geohash 表示
Redis GEOPOS 命令从key里返回所有给定位置元素的位置(经度和纬度)
Redis GEODIST 命令返回两个给定位置之间的距离
Redis GEORADIUS 命令以给定的经纬度为中心, 找出某一半径内的元素
Redis GEOADD 命令将指定的地理空间位置(纬度、经度、名称)添加到指定的key中
Redis GEORADIUSBYMEMBER 命令找出位于指定范围内的元素,中心点是由给定的位置元素决定

geoadd 添加地理位置

规则: 两级无法直接添加, 我们一般会下载城市数据, 直接通过java程序一次性导入

有效的经度从-180度到180度。
有效的纬度从-85.05112878度到85.05112878度。

当坐标位置超出上述指定范围时,该命令将会返回一个错误。城市经纬度查询网站

# 参数 key 值(经度,维度,名称)
127.0.0.1:6379> geoadd china:city 166.40 39.90 beijing
(integer) 1
127.0.0.1:6379> geoadd china:city 121.47 31.23 shanghai
(integer) 1
127.0.0.1:6379> geoadd china:city 106.50 29.53 chongqing
(integer) 1
127.0.0.1:6379> geoadd china:city 114.05 22.52 shenzhen
(integer) 1
127.0.0.1:6379> geoadd china:city 120.15 30.28 hangzhou
(integer) 1
127.0.0.1:6379> geoadd china:city 125.14 42.9 xian
(integer) 1

geopos 获取当前定位, 一定是一个坐标值.

127.0.0.1:6379> geopos china:city beijing
1) 1) "166.40000134706497192"
   2) "39.90000009167092543"
127.0.0.1:6379> geopos china:city beijing shanghai
1) 1) "166.40000134706497192"
   2) "39.90000009167092543"
2) 1) "121.47000163793563843"
   2) "31.22999903975783553"

geodist

返回两个给定位置之间的距离。如果两个位置之间的其中一个不存在, 那么命令返回空值。

如果用户没有显式地指定单位参数, 那么 GEODIST 默认使用米作为单位。

指定单位的参数 unit 必须是以下单位的其中一个:

  • m 表示单位为米。
  • km 表示单位为千米。
  • mi 表示单位为英里。
  • ft 表示单位为英尺。
127.0.0.1:6379> GEODIST china:city beijing shanghai # 北京到上海的距离, 单位默认m
"4132638.7228"
127.0.0.1:6379> GEODIST china:city beijing shanghai km # 指定单位km
"4132.6387"

我附近的人? (获取附近人的地址, 定位), 通过半径来查询.

georadius

以给定的经纬度为中心, 返回键包含的位置元素当中, 与中心的距离不超过给定最大距离的所有位置元素。

范围可以使用以下其中一个单位:

  • m 表示单位为米。
  • km 表示单位为千米。
  • mi 表示单位为英里。
  • ft 表示单位为英尺。

在给定以下可选项时, 命令会返回额外的信息:

  • WITHDIST: 在返回位置元素的同时, 将位置元素与中心之间的距离也一并返回。 距离的单位和用户给定的范围单位保持一致。
  • WITHCOORD: 将位置元素的经度和维度也一并返回。
  • WITHHASH: 以 52 位有符号整数的形式, 返回位置元素经过原始 geohash 编码的有序集合分值。 这个选项主要用于底层应用或者调试, 实际中的作用并不大。
  • COUNT : 在默认情况下, GEORADIUS 命令会返回所有匹配的位置元素。 使用 count 选项去获取前 N 个匹配元素.
127.0.0.1:6379> GEORADIUS china:city 110 30 1000 km # 经度110,维度30为中心, 距离该点1000km以内的城市
1) "chongqing"
2) "shenzhen"
3) "hangzhou"
127.0.0.1:6379> GEORADIUS china:city 110 30 500 km  # 经度110,维度30为中心, 距离该点500km以内的城市
1) "chongqing"
127.0.0.1:6379> GEORADIUS china:city 110 30 800 km withdist  # 经度110,维度30为中心, 距离该点1000km以内的城市及距离
1) 1) "chongqing"
   2) "341.9374"
127.0.0.1:6379> GEORADIUS china:city 110 30 1000 km withdist count 2  # 经度110,维度30为中心, 距离该点1000km以内的城市, 限定数量返回
1) 1) "chongqing"
   2) "341.9374"
2) 1) "shenzhen"
   2) "924.6408"   
127.0.0.1:6379> GEORADIUS china:city 110 30 800 km withdist withcoord  # 经度110,维度30为中心, 距离该点1000km以内的城市及位置信息
1) 1) "chongqing"
   2) "341.9374"
   3) 1) "106.49999767541885376"
      2) "29.52999957900659211"
127.0.0.1:6379> GEORADIUS china:city 110 30 800 km withdist withhash  
1) 1) "chongqing"
   2) "341.9374"
   3) (integer) 4026042091628984

georadiusbymember 这个命令和 GEORADIUS 命令一样, 都可以找出位于指定范围内的元素, 但是 GEORADIUSBYMEMBER 的中心点是由给定的位置元素决定的, 而不是像 GEORADIUS 那样, 使用输入的经度和纬度来决定中心点.

127.0.0.1:6379> GEORADIUSBYMEMBER china:city beijing 1000 km # 找出指定位置元素(北京)为中心,周围1000km内的城市
1) "beijing"

geohash 返回一个或多个位置元素的 Geohash 表示。

127.0.0.1:6379> GEOHASH china:city beijing chongqing # 将二维的经纬度转换为一维的字符串,如果两个字符串越接近, 那么距离越近.
1) "xxn6fx8f350"
2) "wm5xzrybty0"

GEO底层的实现原理其实就是Zset, 可以使用Zset的命令操作数据.

127.0.0.1:6379> ZRANGE china:city 0 -1 # 获取所有元素
1) "chongqing"
2) "shenzhen"
3) "hangzhou"
4) "shanghai"
5) "beijing"
6) "xian"
127.0.0.1:6379> zrem china:city beijing # 移除指定元素
(integer) 1
127.0.0.1:6379> ZRANGE china:city 0 -1
1) "chongqing"
2) "shenzhen"
3) "hangzhou"
4) "shanghai"
5) "xian"

2.2 Hyperloglog

Redis 2.8.9版本新增了Hyperloglog数据结构, Hyperloglog用于基数的统计.

优点: 占用的内存是固定的, 2^64个不同的元素, 只需要占用12KB内存. 如果要从内存角度来比较的话, Hyperloglog首先.

网页的UV统计(一个人访问一个网站多次, 还是算是一个人).

传统的方式, set集合保存用户的id, 然后就可以统计set集合中的元素数量作为标准判断. 这个方式如果保存大量的用户id, 就会比较麻烦, 我们的目的是为了计数, 而不是保存用户ID.

使用Hyperloglog统计UV任务, 只有0.81%误差, 可以忽略不计.

命令描述
Redis Pfmerge 命令将多个 HyperLogLog 合并为一个 HyperLogLog
Redis Pfadd 命令添加指定元素到 HyperLogLog 中。
Redis Pfcount 命令返回给定 HyperLogLog 的基数估算值。
127.0.0.1:6379> PFADD mykey a b c d e f g h i j # 创建第一组元素
(integer) 1
127.0.0.1:6379> PFCOUNT mykey # 统计mykey元素的基数数量
(integer) 10
127.0.0.1:6379> PFADD mykey2 i j z x c v b n m # 创建第二组元素
(integer) 1
127.0.0.1:6379> PFCOUNT mykey2  # 统计mykey2元素的基数数量
(integer) 9
127.0.0.1:6379> PFMERGE mykey3 mykey mykey2 # 合并两组元素到mykey3
OK
127.0.0.1:6379> PFCOUNT mykey3  # 查看并集的数量, 统计mykey3元素的基数数量
(integer) 15

2.3 Bitmaps

进行位存储.

统计用户信息, 活跃, 不活跃! 用户登录, 未登录! 365天打卡场景. >> 只有两个状态的都可以使用Bitmaps.

Bitmaps 位图数据结构, 都是操作二进制位进行记录, 只有0和1两个状态.

使用bitmaps来记录一周的打卡.

127.0.0.1:6379> SETBIT sign 0 1 # 周一打卡
(integer) 0
127.0.0.1:6379> SETBIT sign 1 0 # 周二未打卡
(integer) 0
127.0.0.1:6379> SETBIT sign 2 0 # 周三未打卡
(integer) 0
127.0.0.1:6379> SETBIT sign 3 1 # 周四打卡
(integer) 0
127.0.0.1:6379> SETBIT sign 4 1 # 周五打卡
(integer) 0
127.0.0.1:6379> SETBIT sign 5 0 # 周六未打卡
(integer) 0
127.0.0.1:6379> SETBIT sign 6 0 # 周日未打卡

查看周四, 周日是否打卡.

127.0.0.1:6379> GETBIT sign 3
(integer) 1
127.0.0.1:6379> GETBIT sign 6
(integer) 0

统计操作, 统计打卡的天数.

127.0.0.1:6379> BITCOUNT sign # 统计打开的天数, 只有三天打卡了
(integer) 3

3. Redis事务

Redis事务的本质, 就是一组命令的集合. 一个事务中的所有命令都会被序列化, 在事务执行过程中, 会按照顺序执行! ( 一次性, 顺序性, 排他性)

---- 队列 set.. set.. set... 执行----

==Redis事务事务没有隔离级别的概念.==

所有的命令在事务中, 并没有被执行, 只有发起执行命令的时候才会执行 ! (EXEC)

==Redis单条命令是保证原子性的, 但是事务是不保证原子性的.==

事务操作命令:

命令描述
Redis Exec 命令执行所有事务块内的命令。
Redis Watch 命令监视一个(或多个) key ,如果在事务执行之前这个(或这些) key 被其他命令所改动,那么事务将被打断。
Redis Discard 命令取消事务,放弃执行事务块内的所有命令。
Redis Unwatch 命令取消 WATCH 命令对所有 key 的监视。
Redis Multi 命令标记一个事务块的开始。

开启事务并执行成功

127.0.0.1:6379> MULTI # 开启事务
OK
127.0.0.1:6379(TX)> set k1 v1
QUEUED
127.0.0.1:6379(TX)> set k2 v2
QUEUED
127.0.0.1:6379(TX)> get k2
QUEUED
127.0.0.1:6379(TX)> set k3 v3
QUEUED
127.0.0.1:6379(TX)> EXEC # 执行事务(提交)
1) OK
2) OK
3) "v2"
4) OK
127.0.0.1:6379>

取消事务

127.0.0.1:6379> MULTI # 开启事务
OK
127.0.0.1:6379(TX)> set k1 v1
QUEUED
127.0.0.1:6379(TX)> set k2 v2
QUEUED
127.0.0.1:6379(TX)> set k4 v4
QUEUED
127.0.0.1:6379(TX)> DISCARD # 取消事务, 队列中的命令都不会执行
OK

队列中存在编译型异常命令(命令错误), 事务中所有的命令都不会被执行.

127.0.0.1:6379> MULTI # 开启事务
OK
127.0.0.1:6379(TX)> set k1 v1
QUEUED
127.0.0.1:6379(TX)> set k2 v2
QUEUED
127.0.0.1:6379(TX)> set k3 v3
QUEUED
127.0.0.1:6379(TX)> getset k3 # 错误的命令
(error) ERR wrong number of arguments for 'getset' command
127.0.0.1:6379(TX)> set k4 v4
QUEUED
127.0.0.1:6379(TX)> exec # 执行事务失败, 所有的命令都不会被执行
(error) EXECABORT Transaction discarded because of previous errors.

队列中存在运行时异常命令( 例如java中的1/0 ), 如果事务队列中存在语法错误, 错误的不会被执行, 其他都会正常执行成功.

127.0.0.1:6379> MULTI # 开启事务
OK
127.0.0.1:6379(TX)> set k1 v1
QUEUED
127.0.0.1:6379(TX)> set k2 v2
QUEUED
127.0.0.1:6379(TX)> INCR k1  # 非数字字符串不能加1, 运行时异常
QUEUED
127.0.0.1:6379(TX)> set k3 v3
QUEUED
127.0.0.1:6379(TX)> get k3
QUEUED
127.0.0.1:6379(TX)> EXEC # 提交事务, 运行时异常的命令执行失败, 队列中的其他命令都正常执行
1) OK
2) OK
3) (error) ERR value is not an integer or out of range
4) OK
5) "v3"

watch监控(乐观锁)

在学习watch监控之前, 我们先来了解一下悲观锁/乐观锁相关知识.

悲观锁

顾名思义,每次去拿数据的时候都被认为别人会修改,所以每次在拿数据的时候都会被锁上,这样别人想拿这个数据就会block直到它拿到锁,传统的关系型数据库里边就用到了很多这种锁机制,比如行锁、表锁等,读锁、写锁等,都是在做操作之前先锁上。

乐观锁

每次去拿数据的时候都认为别人不会修改所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号, CAS等机制

CAS机制

CAS(Compare And Swap), 比较并替换. CAS属于乐观锁,乐观地认为程序中的并发情况不那么严重,所以让线程不断去尝试更新, 直到成功。

CAS机制当中使用了3个基本操作数:内存地址V,旧的预期值A,要修改的新值B。

更新一个变量的时候,只有当变量的预期值A和内存地址V当中的实际值相同时,才会将内存地址V对应的值修改为新值B。

CAS的缺点

(1). CPU开销较大
在并发量比较高的情况下,如果许多线程反复尝试更新某一个变量,却又一直更新不成功,循环往复,会给CPU带来很大的压力。

(2). 不能保证代码块的原子性
CAS机制所保证的只是一个变量的原子性操作,而不能保证整个代码块的原子性。比如需要保证3个变量共同进行原子性的更新,就不得不使用Synchronized了。

扣减余额操作, 无加塞篡改,先监控再开启multi,保证两笔金额变动在同一个事务内.

正常执行成功

127.0.0.1:6379> set money 100
OK
127.0.0.1:6379> set out 0
OK
127.0.0.1:6379> WATCH money # 监视money对象
OK
127.0.0.1:6379> MULTI # 开启事务
OK
127.0.0.1:6379(TX)> DECRBY money 20
QUEUED
127.0.0.1:6379(TX)> INCRBY out 20
QUEUED
127.0.0.1:6379(TX)> exec # 事务正常结束, 期间数据没有发生变动, 事务可以正常执行成功
1) (integer) 80
2) (integer) 20

多线程修改值, 使用watch可以当作redis的乐观锁操作.

# 线程1
127.0.0.1:6379> WATCH money # 监控money
OK
127.0.0.1:6379> MULTI # 开启事务
OK
127.0.0.1:6379(TX)> DECRBY money 20
QUEUED
127.0.0.1:6379(TX)> INCRBY money 20
QUEUED
127.0.0.1:6379(TX)> EXEC # 提交事务之前, 其他线程修改了被监控的money的值,导致事务提交失败.
(nil)

# 线程2
127.0.0.1:6379> GET money
"100"
127.0.0.1:6379> SET money 500
OK

使用unwatch取消监控, 相当于释放锁.

127.0.0.1:6379> WATCH money # 监控money
OK
127.0.0.1:6379> DECRBY money 10
(integer) 90
127.0.0.1:6379> INCRBY out 10
(integer) 30
127.0.0.1:6379> UNWATCH # money修改后,取消监控
OK
127.0.0.1:6379> WATCH money # 重新监控money
OK
127.0.0.1:6379> MULTI # 开启事务
OK
127.0.0.1:6379(TX)> DECRBY money 10
QUEUED
127.0.0.1:6379(TX)> INCRBY out 10
QUEUED
127.0.0.1:6379(TX)> EXEC # 提交事务,执行成功
1) (integer) 80
2) (integer) 40

4. SpringBoot整合Jedis

Jedis: 采用的直连, 多个线程操作的话, 是不安全的, 一般使用Jedis pool连接池, 更像BIO模式.

lettuce: 采用netty, 实例可以在多个线程中进行共享, 不存在线程不安全的情况, 可以减少线程数据, 更像NIO模式.

整合Jedis配置

spring:
  redis:
    host: 192.168.65.129
    password: 123456
    port: 6379
    jedis:
      pool:
        max-idle: 50 # 连接池中最大空闲数
        max-active: 100 #  连接池中最大连接数
        min-idle: 10 # 连接池中最小空闲数
        max-wait: 10000 # 连接池最大阻塞等待时间(使用负值表示没有限制)
    timeout: 2000 # 连接超时时间

整合Lettuce配置

spring:
  redis:
    #    host: 192.168.65.129 # 单机
    #    port: 6379
    password: 123456
    timeout: 2000 # 连接超时时间
    lettuce:
      pool:
        max-idle: 50 # 连接池中最大空闲数
        max-active: 100 #  连接池中最大连接数
        min-idle: 10 # 连接池中最小空闲数
        max-wait: 10000 # 连接池最大阻塞等待时间(使用负值表示没有限制)
      shutdown-timeout: 100 # 关闭超时时间
    cluster: # 集群配置
      nodes:
        - 192.168.65.129:7000
        - 192.168.65.129:7001
        - 192.168.65.129:7002
        - 192.168.65.129:7003
        - 192.168.65.129:7004
        - 192.168.65.129:7005
      max-redirects: 3

4.1 源码分析

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(RedisOperations.class)
@EnableConfigurationProperties(RedisProperties.class)
@Import({ LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class })
public class RedisAutoConfiguration {

    @Bean
    @ConditionalOnMissingBean(name = "redisTemplate") // 不存在才生效, 意味着可以自定义redisTemplate来替换默认的
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory)
            throws UnknownHostException {
      // 默认的RedisTemplate, 没有过多的设置, Redis对象都是需要序列化!
      // 两个泛型都是Object, Object类型需要我们自行强制转换
        RedisTemplate<Object, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(redisConnectionFactory);
        return template;
    }

    @Bean
    @ConditionalOnMissingBean // 由于string是redis中最常用的类型,所以说单独提取出来默认的
    public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory)
            throws UnknownHostException {
        StringRedisTemplate template = new StringRedisTemplate();
        template.setConnectionFactory(redisConnectionFactory);
        return template;
    }
}

4.2 自定义RedisTemplate

@Bean
public RedisTemplate<String, Object> redisTemplate(LettuceConnectionFactory factory) {
    // 为了开发方便, 一般使用<String,Object>
    RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
    redisTemplate.setConnectionFactory(factory);
    // 序列化配置
    Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
    ObjectMapper om = new ObjectMapper();
    om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
//        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); // enableDefaultTyping已过期
    om.activateDefaultTyping(om.getPolymorphicTypeValidator(), ObjectMapper.DefaultTyping.NON_FINAL);
    jackson2JsonRedisSerializer.setObjectMapper(om);

    // string序列化
    StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();

    // 在使用注解@Bean返回RedisTemplate时, 同时配置hashKey和hashValue的序列化方式
    // key采用String的序列化方式
    redisTemplate.setKeySerializer(stringRedisSerializer);
    // value序列化方式采用jackson
    // 使用increment方法,需要使用StringRedisSerializer或GenericToStringSerializer序列化器
    redisTemplate.setValueSerializer(new GenericToStringSerializer<>(Object.class));
    // hash的key也采用String的序列化方式
    redisTemplate.setHashKeySerializer(stringRedisSerializer);
    // hash的value采用jackson的序列化方式
    redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
    redisTemplate.afterPropertiesSet();

    return redisTemplate;
}

4.3 封装Redis工具类

package com.crys.bootluttuce.util;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;

import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;

/**
 * 描述:Redis工具类
 * @author crysw
 * @date 2022/4/11 20:54
 * @version 1.0
 */
@Component
public class RedisUtil {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    /**
     * 指定缓存失效时间
     * @param key 键
     * @param time 时间(秒)
     * @return
     */
    public boolean expire(String key, long time) {
        try {
            if (time > 0) {
                redisTemplate.expire(key, time, TimeUnit.SECONDS);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 根据key获取过期时间
     * @param key 键, 不能为null
     * @return 时间(秒), 返回0代表为永久有效
     */
    public long getExpire(String key) {
        return redisTemplate.getExpire(key, TimeUnit.SECONDS);
    }

    /**
     * 判断key是否存在
     * @param key 键
     * @return true 存在, false 不存在
     */
    public boolean hashKey(String key) {
        try {
            return redisTemplate.hasKey(key);
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }


    /**
     * 删除缓存
     * @param key 可以传一个值或多个
     */
    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===============

    /**
     * 普通缓存获取
     * @param key 键
     * @return 值
     */
    public Object get(String key) {

        return key == null ? null : redisTemplate.opsForValue().get(key);
    }

    /**
     * 普通缓存放入
     * @param key 键
     * @param value 值
     * @return true成功  false失败
     */
    public boolean set(String key, Object value) {

        try {
            redisTemplate.opsForValue().set(key, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 普通缓存存入并设置时间
     * @param key
     * @param value
     * @param time
     * @return
     */
    public boolean set(String key, Object value, long time) {
        try {
            if (time > 0) {
                redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
            } else {
                set(key, value);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 递增
     * @param key 键
     * @param increment 要增加的值(大于0)
     * @return
     */
    public long incr(String key, long increment) {
        if ((increment < 0)) {
            throw new RuntimeException("递增因子必须大于0");
        }
        return redisTemplate.opsForValue().increment(key, increment);
    }

    /**
     * 递减
     * @param key 键
     * @param decrement 要减少的值(大于0)
     * @return
     */
    public long decr(String key, long decrement) {

        if (decrement < 0) {
            throw new RuntimeException("递减因子必须大于0");
        }
        return redisTemplate.opsForValue().increment(key, -decrement);
    }

    // ================Map===============

    /**
     * HashGet
     * @param key 键, 不能为null
     * @param item 项, 不能为null
     * @return
     */
    public Object hget(String key, String item) {
        return redisTemplate.opsForHash().get(key, item);
    }

    /**
     * 获取hashkey对应的所有键值
     * @param key 键
     * @return 对应的多个键值
     */
    public Map<Object, Object> hmget(String key) {
        return redisTemplate.opsForHash().entries(key);
    }

    /**
     * HashSet
     * @param key 键
     * @param map 对应多个键值
     * @return
     */
    public boolean hmset(String key, Map<String, Object> map) {
        try {
            redisTemplate.opsForHash().putAll(key, map);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * HashSet 并设置时间
     * @param key 键
     * @param map 对应多个键值
     * @param time 时间(秒)
     * @return true成功 false失败
     */
    public boolean hmset(String key, Map<String, Object> map, long time) {
        try {
            redisTemplate.opsForHash().putAll(key, map);
            if (time > 0) {
                expire(key, time);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 向一张hash表中放入数据, 如果不存在将创建
     * @param key  键
     * @param item  项
     * @param value  值
     * @return true成功 false失败
     */
    public boolean hset(String key, String item, Object value) {
        try {
            redisTemplate.opsForHash().put(key, item, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 向一张hash表中放入数据, 如果不存在将创建, 并设置过期时间
     * @param key 键
     * @param item 项
     * @param value 值
     * @param time 时间(秒) 注意: 如果已存在的hash表有时间, 这里将会替换原有的时间
     * @return true成功   false失败
     */
    public boolean hset(String key, String item, Object value, long time) {
        try {
            redisTemplate.opsForHash().put(key, item, value);
            if (time > 0) {
                expire(key, time);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 删除hash表中的值
     * @param key 键, 不能为null
     * @param item 项, 可以是多个, 不能为null
     */
    public void hdel(String key, Object... item) {
        redisTemplate.opsForHash().delete(key, item);
    }

    /**
     * 判断hash表中是否有该项的值
     * @param key 键, 不能为null
     * @param item 项, 不能为null
     * @return true存在, false不存在
     */
    public boolean hHashkey(String key, String item) {
        return redisTemplate.opsForHash().hasKey(key, item);
    }

    /**
     * hash递增, 如果不存在, 就会创建一个,并把新增后的值返回
     * @param key 键
     * @param item 项
     * @param increment 要增加的值(大于0)
     * @return
     */
    public double hincr(String key, String item, double increment) {
        return redisTemplate.opsForHash().increment(key, item, increment);
    }

    /**
     * hash递减
     * @param key 键
     * @param item 项
     * @param decrement 要减少的值
     * @return
     */
    public double hdecr(String key, String item, double decrement) {
        return redisTemplate.opsForHash().increment(key, item, -decrement);
    }

    // ================set===============

    /**
     * 根据key获取set中的所有值
     * @param key 键
     * @return
     */
    public Set<Object> sGet(String key) {
        try {
            return redisTemplate.opsForSet().members(key);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    /**
     * 根据value从一个set中查询, 是否存在
     * @param key 键
     * @param value 值
     * @return true存在, false不存在
     */
    public boolean sHasKey(String key, Object value) {
        try {
            return redisTemplate.opsForSet().isMember(key, value);
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 将数据放入set缓存
     * @param key 键
     * @param values 值, 可以是多个
     * @return 成功个数
     */
    public long sSet(String key, Object... values) {
        try {
            return redisTemplate.opsForSet().add(key, values);
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }

    /**
     * 将set数据放入缓存
     * @param key 键
     * @param time 时间(秒)
     * @param values 值, 可以是多个
     * @return 成功个数
     */
    public long sSetAndTime(String key, long time, Object... values) {
        try {
            Long count = redisTemplate.opsForSet().add(key, values);
            if (time > 0) {
                expire(key, time);
            }
            return count;
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }

    /**
     * 获取set缓存的长度
     * @param key
     * @return
     */
    public long sGetSetSize(String key) {
        try {
            return redisTemplate.opsForSet().size(key);
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }

    /**
     * 移除值为value的数据
     * @param key 键
     * @param values 值, 可以是多个
     * @return 移除的个数
     */
    public long setRemove(String key, Object... values) {
        try {
            Long count = redisTemplate.opsForSet().remove(key, values);
            return count;
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }

    // ================list===============

    /**
     * 获取list缓存的内容
     * @param key 键
     * @param start 开始
     * @param end 结束 0 到 -1 代表所有值
     * @return
     */
    public List<Object> lGet(String key, long start, long end) {
        try {
            return redisTemplate.opsForList().range(key, start, end);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    /**
     * 获取list缓存的长度
     * @param key 键
     * @return
     */
    public long lGetListSize(String key) {
        try {
            return redisTemplate.opsForList().size(key);
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }

    /**
     * 通过索引获取list中的值
     * @param key 键
     * @param index 索引, index>=0时, 0表示头, 1表示第二个元素, 依次类推; index<0时, -1表示尾, -2表示倒数第二个元素, 依次类推
     * @return
     */
    public Object lGetIndex(String key, long index) {
        try {
            return redisTemplate.opsForList().index(key, index);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    /**
     * 将list放入缓存
     * @param key 键
     * @param value 值
     * @return
     */
    public boolean lSet(String key, Object value) {
        try {
            redisTemplate.opsForList().rightPush(key, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 将list放入缓存, 并设置过期时间
     * @param key 键
     * @param value 值
     * @param time 时间(秒)
     * @return
     */
    public boolean lSet(String key, Object value, long time) {
        try {
            redisTemplate.opsForList().rightPush(key, value);
            if (time > 0) expire(key, time);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 将list放入缓存
     * @param key 键
     * @param value 值
     * @return
     */
    public boolean lSet(String key, List<Object> value) {
        try {
            redisTemplate.opsForList().rightPushAll(key, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 将list放入缓存
     * @param key 键
     * @param value 值
     * @param time 时间(秒)
     * @return
     */
    public boolean lSet(String key, List<Object> value, long time) {
        try {
            redisTemplate.opsForList().rightPushAll(key, value);
            if (time > 0) expire(key, time);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 根据索引修改list中的某条数据
     * @param key 键
     * @param index 索引
     * @param value 值
     * @return
     */
    public boolean lUpdateIndex(String key, long index, Object value) {
        try {
            redisTemplate.opsForList().set(key, index, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 移除N个值为value的数据
     * @param key 键
     * @param count 移除多少个
     * @param value 值
     * @return 移除的个数
     */
    public long lRemove(String key, long count, Object value) {
        try {
            Long remove = redisTemplate.opsForList().remove(key, count, value);
            return remove;
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }
}

测试此处省略……

5. Redis配置文件详解

启动的时候, 通过读取配置文件来启动.

单位 units, 对大小写不敏感.

# Redis configuration file example.
#
# Note that in order to read the configuration file, Redis must be
# started with the file path as first argument:
#
# ./redis-server /path/to/redis.conf

# Note on units: when memory size is needed, it is possible to specify
# it in the usual form of 1k 5GB 4M and so forth:
#
# 1k => 1000 bytes
# 1kb => 1024 bytes
# 1m => 1000000 bytes
# 1mb => 1024*1024 bytes
# 1g => 1000000000 bytes
# 1gb => 1024*1024*1024 bytes
#
# units are case insensitive so 1GB 1Gb 1gB are all the same.

包含 INCLUDES

include /path/to/local.conf
include /path/to/other.conf

网络 NETWORK

# 绑定的ip
bind 127.0.0.1 -::1
# 保护模式
protected-mode no
# 端口设置
port 6379

通用 GENERAL

# 以守护进程的方式运行,默认是no,我们需要自己开启为yes
daemonize yes
# 如果以后台方式运行,需要指定一个pid文件
pidfile /var/run/redis_6379.pid
# 日志级别
# debug (a lot of information, useful for development/testing)
# verbose (many rarely useful info, but not a mess like the debug level)
# notice (moderately verbose, what you want in production probably) 生产环境使用该级别
# warning (only very important / critical messages are logged)
loglevel notice
# 日志文件位置及文件名
logfile ""
# 数据库的数量,默认是16个
databases 16
# 是否总是线上LOGO
always-show-logo no

快照 SNAPSHOTTING, 持久化, 在规定的时间内执行了多少次操作, 则会持久化到.rdb或.aof文件中. redis是内存数据库, 如果没有持久化, 断电后会丢失数据.

# save <seconds> <changes>
# 如果指定时间内,key的修改超过了changes,就会持久化
# 如果900s内, 如果至少有1个key进行了修改, 就进行持久化操作
save 900 1 
# 如果300s内, 如果至少有10个key进行了修改, 就进行持久化操作
save 300 10
# 如果60s内, 如果至少有10000个key进行了修改, 就进行持久化操作
save 60 10000
# 持久化如果发生错误,是否还需要继续工作
stop-writes-on-bgsave-error yes
# 是否压缩rdb文件, 需要消耗一些cpu资源
rdbcompression yes
# 保存rdb文件,进行错误的检查校验
rdbchecksum yes
# rdb文件名称
dbfilename dump.rdb
# rdb文件保存目录
dir ./

主从复制

安全 SECURITY, 可以设置redis的密码, 默认是没有密码.

requirepass 123456

客户端修改密码操作

127.0.0.1:6379> ping
PONG
127.0.0.1:6379> config get requirepass
1) "requirepass"
2) "123456"
127.0.0.1:6379> config set requirepass "password123"
OK
127.0.0.1:6379> config get requirepass
1) "requirepass"
2) "password123"
127.0.0.1:6379> exit
[root@centos7-01 redis-6.x]# ./bin/redis-cli
127.0.0.1:6379> ping
(error) NOAUTH Authentication required. # 修改密码后ping失败,需要授权
127.0.0.1:6379> auth password123 # 密码授权
OK
127.0.0.1:6379> ping
PONG

限制CLIENTS

# 设置能连接上redis的最大客户端的数量
maxclients 10000
# redis配置最大的内存容量
maxmemory <bytes>
# 内存到达上限之后的处理策略
# volatile-lru -> Evict using approximated LRU, only keys with an expire set. # 从设置了过期时间的key中删除最近很少使用的key
# allkeys-lru -> Evict any key using approximated LRU. 从所有key中删除最近很少使用的key
# volatile-lfu -> Evict using approximated LFU, only keys with an expire set. 从设置了过期时间的key中删除最近使用次数最少的key
# allkeys-lfu -> Evict any key using approximated LFU. 从所有key中删除最近使用次数最少的key
# volatile-random -> Remove a random key having an expire set. 从设置了过期时间的key中随机删除
# allkeys-random -> Remove a random key, any key. 从所有key中随机删除
# volatile-ttl -> Remove the key with the nearest expire time (minor TTL) 剔除即将过期的
# noeviction -> Don't evict anything, just return an error on write operations.  永不剔除
maxmemory-policy noeviction

APPEND ONLY MODE aof模式

appendonly no
appendfilename "appendonly.aof"
#appendfsync always # 每次修改都会sync , 消耗性能
# 每秒执行一次sync, 可能会丢失1s的数据
appendfsync everysec 
# appendfsync no # 不执行同步
# 重写时是否可以运行Appendfsync,用默认no即可,保证数据安全性
no-appendfsync-on-rewrite no
# 重写规则, 如果aof文件大于64MB, 会触发aof文件重写
auto-aof-rewrite-percentage 100
# AOF文件大小达到上次rewrite后大小的一倍, 会触发aof文件重写
auto-aof-rewrite-min-size 64mb

6. Redis持久化

Redis是内存数据库, 如果不将内存中的数据库状态保存到磁盘, 那么一旦服务器进程退出, 服务器中的数据库状态也会消失, 所以Redis提供了持久化功能.

6.1 RDB (Redis DataBase)

在指定的时间间隔内将内存中的数据集快照写入磁盘, 也就是snapshot, 它恢复时是将快照文件读到内存中.

Redis会单独创建(fork)一个子进程来进行持久化, 会先将数据写入到一个临时文件中, 待持久化过后, 再用这个临时文件替换上次持久化好的文件. 整个过程中, 主进程是不进行任何IO操作的, 确保了极高的性能. 如果需要进行大规模的数据恢复, 且对于数据恢复的完整性不是非常敏感, 那RDB方式要比AOF方式更加的高效.

RDB的缺点是最后一次持久化后的数据可能丢失. 默认RDB, 一般情况下不需要修改.
在这里插入图片描述

==rdb保存的文件就是dump.rdb==

触发机制

  • 满足save规则的情况下, 会自动触发rdb规则.
  • 执行flushall命令, 也会触发rdb持久化.
  • 退出redis, 也会产生rdb文件.

如何恢复rdb文件的数据

  • 只需要将rdb文件放在redis启动目录即可, redis启动的时候会自动检查dump.rdb文件,恢复其中的数据.
  • 查看需要存在的位置
# 如果dir配置的目录下存在dump.rdb文件,启动就会自动恢复其中的数据.
127.0.0.1:6379> CONFIG GET dir
1) "dir"
2) "/usr/local/redis-6.x"

优点:

  • 适合大规模的数据恢复.
  • 对数据的完整性要求不高.

缺点:

  • 需要一定的时间间隔进行操作. 如果Redis意外宕机了, 最后一次修改数据就没有了.
  • fork进程的时候, 会占用一定的内存空间.

6.2 AOF (Append Only File)

将我们的所有命令都记录下来, 恢复数据的时候就把aof文件中的命令全部再执行一遍.

以日志的形式记录每个写操作, 将Redis执行过的所有指令记录下来(读操作不记录), 只许追加文件但不可用改写文件, Redis启动之初会读取该文件重新构建数据, 换言之, Redis重启时会根据日志文件的内容将写指令从前到后执行一次, 以完成数据的恢复工作.

==aof保存的是appendonly.aof文件==
在这里插入图片描述

默认是不打开的, 需要手动开启配置. 重启Redis就可以生效.

appendonly yes

如果aof文件有错误, redis重启会失败, 我们可以通过工具redis-check-aof修复aof文件.

./bin/redis-check-aof appendonly.aof --fix

优点

appendfsync always
#appendfsync everysec
# appendfsync no
  • 每次修改都同步,文件的完整性会更好.
  • 每秒同步一次, 可能会丢失一秒的数据
  • 从不同步, 效率最高.

缺点

  • 相对于数据文件来说, aof远远大于rdb文件, 修复的速度也比rdb慢.
  • aof运行效率也要比RDB慢, 所以redis默认的配置是rdb.

6.3 扩展

1). RDB持久化方式能够在指定的时间间隔内对你的数据进行快照存储.

2). AOF持久化方式记录每次对服务器写的操作, 当服务器重启的时候会重新执行这些命令来恢复原始的数据, AOF命令以Redis协议追加保存每次写的操作到文件末尾, Redis还能对AOF文件进行后台重写, 使得AOF文件的体积不至于过大.

3). 只做缓存, 如果你只希望你的数据在服务器运行的时候存在, 你也可以不做任何持久化.

4). 同时开启两种持久化方式

  • 在这种情况下, 当redis重启的时候会优先载入AOF文件来恢复原始的数据, 因为在通常情况下AOF文件保存的数据集要比RDB文件保存的数据集更完整.
  • RDB的数据不实时, 同时使用两者时服务器重启也会找AOF文件, 那要不要只使用AOF呢? 建议不这样操作, 因为RDB更适合用于备份数据库(AOF在不断变化, 不适合备份), 快速重启, 而且不会有AOF潜在的BUG.

5). 性能建议

  • 因为RDB文件只用作后备用途, 建议只在slave上持久化RDB文件, 而且配置15min备份一次就够了, 只保留save 900 1这条规则.
  • 如果开启 AOF, 好处是在最恶劣情况下也只会丢失不超过两秒的数据, 启动脚本比较简单, 只要load自己的aof文件即可, 代价是带来了持久了IO操作, 并且AOF rewrite的最后将rewrite过程中产生的新数据写到新文件造成的阻塞几乎不可避免. 只要硬盘许可, 应该尽量减少AOF rewrite的频率, AOF重写的基础值默认64MB太小了, 可以设置到5G以上, 默认超过原来aof文件大小的1倍(100%)可以改到更加适当的倍数.
  • 如果不开启AOF, 仅靠master-slave replication实现高可用性也可以, 能省掉一大笔IO, 也减少了rewrite时带来的系统波动. 代价是如果master-slave同时宕机, 会丢失十几分钟的数据, 启动脚本也要比较master-slave中的RDB文件, 载入较新的那个RDB文件.

7. Redis发布订阅

Redis发布订阅(pub/sub)是一种消息通信模式: 发送者(pub)发送消息, 订阅者(sub)接收消息.

Redis客户端可以订阅任意数量的频道.

发布/订阅消息图:
在这里插入图片描述

下图展示了频道channel1, 以及订阅这个频道的三个客户端: client1, client2 和 client5 之间的关系.

发布订阅

当有新消息通过publish命令发送给频道channel1时, 这个消息就会被发送给订阅它的三个客户端.

发布订阅

这些命令被广泛用于构建即时通信应用, 比如网络聊天室和实时广播, 实时提醒等.

# 订阅给定的一个或多个频道的信息
subscribe channel [channel2 ...]
# 订阅一个或多个符合给定模式的频道
psubscribe pattern [pattern2 ...]
# 将信息发送到指定的频道
publish channel message
# 退订给指定的频道
unsubscribe channel [channel2 ...]
# 退订所有给定模式的频道
punsubscribe pattern [pattern2 ...]

原理

Redis是使用C实现的, 通过分析Redis源码里的pubsub.c文件, 了解发布和订阅机制的底层实现, 加深对Redis的理解, Redis通过PUBLISH, SUBSCRIBE和PSUBSCRIBE等命令实现发布和订阅功能.

通过SUBSCRIBE命令订阅某频道后, redis-server里维护了一个字典, 字典的键就是一个个channel, 而字典的值则是一个链表, 链表中保存了所有订阅这个channel的客户端, SUBSCRIBE命令的关键, 就是将客户端添加到给定的channel的订阅链表中.

通过PUBLISH命令向订阅者发送消息, redis-server会使用给定的频道作为键, 在它所维护的channel字段中查找记录了订阅这个频道的所有客户端的链表, 遍历这个链表, 将消息发布给所有订阅者.

pub/sub从字面上理解就是发布(publish)与订阅(subscribe), 在redis中,你可以设定对某一个key值进行消费发布及消息订阅, 当一个key值上进行了消息发布后, 所有订阅它的客户端都会收到相应的消息. 这一功能最明显的用法就是用作实时消息系统, 比如普通的即时聊天, 群聊等功能.

8. Redis主从复制

8.1 概念

主从复制, 是指将一台Redis服务器的数据复制到其他的Redis服务器, 前者称为主节点(master/leader), 后者称为从节点(slave/follower); 数据的复制是单向的, 只能由主节点到从节点, master以写为主, slave以读为主.

默认情况下, 每台Redis服务器都是主节点, 且一个主节点可以有多个从节点(或没有从节点), 但一个从节点只能有一个主节点.

主从复制的作用主要包括:

  • 数据冗余: 主从复制实现了数据的热备份, 是持久化之外的一种数据冗余方式.
  • 故障恢复: 当主节点出现问题时, 可以由从节点提供服务, 实现快速的故障恢复, 实际上是一种服务的冗余.
  • 负载均衡: 在主从复制的基础上, 配合读写分离, 可以由主节点提供写服务, 由从节点提供读服务(即写Redis数据时应用连接主节点, 读Redis数据时应用连接从节点), 分担服务器负载; 尤其是在写少读多的场景下, 通过多个从节点分担读负载, 可以大大提高Redis服务器的并发量.
  • 高可用基石: 除了上述作用外, 主从复制还是哨兵和集群能够实施的基础, 因此说主从复制是Redis高可用的基础.

一般来说, 要将Redis运用于工程项目中, 只使用一台Redis是万万不能的, 原因如下:

  • 从结构上, 单个Redis服务器会发生单点故障, 并且一台服务器需要处理所有的请求负载, 压力较大.
  • 从容量上, 单个Redis服务器内存容量有限, 就算一台Redis服务器内存容量为256G, 也不能将所有内存用作Redis存储内存. ==一般单台Redis最大使用内存不应该超过20G.==

电商网站上的商品, 一般都是一次上传, 无数次浏览的, 说专业点也就是”多读少写”.

环境配置

只配置从库, 不配置主库.

127.0.0.1:6379> info replication # 查看当前库的信息
# Replication
role:master # 角色
connected_slaves:0 # 连接的从机数
master_failover_state:no-failover
master_replid:16021c9d9e112e153d4201fbed78b95ae5c500ae
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:0
second_repl_offset:-1
repl_backlog_active:0
repl_backlog_size:1048576
repl_backlog_first_byte_offset:0
repl_backlog_histlen:0

主从复制配置文件修改

# 关闭保护模式,用于公网访问
protected-mode no
# 修改端口
port 6380
# 后台启动
daemonize yes
pidfile /var/run/redis_6380.pid
# 防止在其他目录启动,最好写绝对路径的文件名 /usr/local/redis-6.x/data/6380.log
logfile "./data/6380.log"
# 此处绑定ip可以是内网ip和本机ip, 也可以直接注释掉该项
# bind 127.0.0.1
# 用于连接主节点密码
masterauth 123456
# 设置redis密码, 各个节点请保持密码一致
requirepass 123456
# 修改rdb文件名称
dbfilename dump_6380.rdb
# 数据备份文件的目录; 日志文件的默认目录等; 防止在其他目录启动,最好写绝对路径
dir /usr/local/redis-6.x/data

8.2 一主二从

默认情况下, 每台redis服务器都是主节点, 我们一般只需要配置从机就可以了. 配置从机:

# 关闭保护模式,用于公网访问
protected-mode no
# 修改端口
port 6380
# 后台启动
daemonize yes
pidfile /var/run/redis_6380.pid
# 防止在其他目录启动,最好写绝对路径的文件名 /usr/local/redis-6.x/data/6380.log
logfile "./data/6380.log"
# 此处绑定ip可以是内网ip和本机ip, 也可以直接注释掉该项
# bind 127.0.0.1
# 用于连接主节点密码
masterauth 123456
# 设置redis密码, 各个节点请保持密码一致
requirepass 123456
# 修改rdb文件名称
dbfilename dump_6380.rdb
# 数据备份文件的目录; 日志文件的默认目录等; 防止在其他目录启动,最好写绝对路径
dir /usr/local/redis-6.x/data

首先分别启动三台Redis服务, 6379, 6380 , 6381

[root@centos7-01 ~]# cd /usr/local/redis-6.x/
[root@centos7-01 redis-6.x]# ll
total 384
-rwxr-xr-x. 1 root root  4514 Apr  7 22:41 appendonly.aof
drwxr-xr-x. 2 root root   150 Jan 17 21:49 bin
drwxr-xr-x. 2 root root   139 Apr  6 21:58 data
-rw-r--r--. 1 root root   142 Apr 14 22:23 dump.rdb
-rwxr-xr-x. 1 root root 93825 Apr  4 18:00 redis6379.conf
-rwxr-xr-x. 1 root root 93825 Apr  4 18:01 redis6380.conf
-rwxr-xr-x. 1 root root 93825 Apr  4 18:01 redis6381.conf
-rwxr-xr-x. 1 root root 93745 Apr  3 21:06 redis.conf
-rw-r--r--. 1 root root   315 Apr  5 16:01 sentinel.conf
[root@centos7-01 redis-6.x]# ./bin/redis-server redis6379.conf
[root@centos7-01 redis-6.x]# ./bin/redis-server redis6380.conf
[root@centos7-01 redis-6.x]# ./bin/redis-server redis6381.conf
[root@centos7-01 redis-6.x]#
[root@centos7-01 redis-6.x]# ps -ef | grep redis-server
root       1587      1  0 21:31 ?        00:00:00 ./bin/redis-server *:6379
root       1594      1  0 21:31 ?        00:00:00 ./bin/redis-server *:6380
root       1600      1  0 21:31 ?        00:00:00 ./bin/redis-server *:6381

分别连接上三台redis服务的客户端, 通过 info replication 查看集群信息.

# 6379 
[root@centos7-01 redis-6.x]# ./bin/redis-cli -p 6379 -a 123456
Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe.
127.0.0.1:6379> INFO replication
# Replication
role:master
connected_slaves:0
master_failover_state:no-failover
master_replid:27d733e907c3f00821f7373b605225bf8315a171
master_replid2:0000000000000000000000000000000000000000
// 省略......

# 6380 
[root@centos7-01 redis-6.x]# ./bin/redis-cli -p 6380 -a 123456
Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe.
127.0.0.1:6380> INFO replication
# Replication
role:master
connected_slaves:0
master_failover_state:no-failover
master_replid:da1daf77cf23159d911a33fc1f4f82ed2d3dd374
master_replid2:0000000000000000000000000000000000000000
// 省略......

# 6381
[root@centos7-01 redis-6.x]# ./bin/redis-cli -p 6381 -a 123456
Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe.
127.0.0.1:6381> INFO replication
# Replication
role:master
connected_slaves:0
master_failover_state:no-failover
master_replid:e8ed1c0ea76f7e022327acdc56207b9aafd75bb4
master_replid2:0000000000000000000000000000000000000000
// 省略......

从上面的打印信息可以看出, 刚启动的三台Redis服务都是独立的master, 之间没有建立主从关系. 下面我们以6379为master, 与6380, 6381建立主从关系.

# slave-6380
127.0.0.1:6380> SLAVEOF 127.0.0.1 6379
OK
127.0.0.1:6380> INFO replication
# Replication
role:slave # 角色
master_host:127.0.0.1 # 主机
master_port:6379 # 端口
master_link_status:up
master_last_io_seconds_ago:0
master_sync_in_progress:0
slave_read_repl_offset:14
slave_repl_offset:14
slave_priority:100
slave_read_only:1
replica_announced:1
connected_slaves:0
master_failover_state:no-failover
master_replid:eeb823aa531ef123e24f83f918a680f2d481e5b5
master_replid2:0000000000000000000000000000000000000000
// 省略......

# slave-6381
127.0.0.1:6381> SLAVEOF 127.0.0.1 6379
OK
127.0.0.1:6381> INFO replication
# Replication
role:slave
master_host:127.0.0.1
master_port:6379
master_link_status:up
master_last_io_seconds_ago:1
master_sync_in_progress:0
slave_read_repl_offset:56
slave_repl_offset:56
slave_priority:100
slave_read_only:1
replica_announced:1
connected_slaves:0
// 省略......

# master-6379
127.0.0.1:6379> INFO replication
# Replication
role:master
connected_slaves:2 # 可以看到两台slave连接
slave0:ip=127.0.0.1,port=6380,state=online,offset=56,lag=1
slave1:ip=127.0.0.1,port=6381,state=online,offset=56,lag=1
// 省略......

主机可以写, 从机不能写, 从机只能读, 主机的所有信息和数据都会同步到从机.

# 6379 master
127.0.0.1:6379> keys *
1) "class"
2) "k2"
3) "k1"
127.0.0.1:6379> set k1 v1 # master可以写
OK
127.0.0.1:6379> get k1
"v1"

# 6380 slave
127.0.0.1:6380> keys *
1) "k2"
2) "class"
3) "k1"
127.0.0.1:6380> set k1 v1 # slave不能写操作
(error) READONLY You can't write against a read only replica.
127.0.0.1:6380> get k1 # slave可以读
"v1"

注意 :

(1) 主机断开连接, 从机依然连接到主机的, 但是没有写操作, 如果主机重新启动恢复, 从机依然可以直接获取主机写的数据.

(2) 如果从机宕机后, 从机重启后会变成独立的master, 如果要获取之前主机的数据, 需要重新与之前的主机建立主从关系.(客户端命令方式才会这样, 配置文件配置了主从关系不会出现该情况)

==复制原理==

slave启动成功后, 连接到master会发送一个sync同步命令, Master接收到命令, 会启动后台的存盘进程, 同时收集所有接收到的用于修改数据集的命令, 在后台进程执行完毕后, master将传送整个数据文件到slave, 并完成一次完整的同步.

全量复制: slave服务在接收到数据文件后, 将其存盘并加载到内存中.

增量复制: Master继续将新的所有收集到的修改命令依次传给slave, 完成同步.\

只要是重新连接到master, 会触发一次全量复制的完整同步.

真实的主从配置应该是在配置文件中, 这样才是永久的, 上面的方式只是暂时的.

################################## REPLICATION #################################
replicaof <masterip> <masterport>
masterauth "123456"

8.3 层层链路

层层链路模型也可以完成数据的主从复制, 例如: 6379是6380的master, 6380是6381的master, 如果6379宕机了, 6380依然是slave, 不能写操作. 我们平时并不使用这种模式.

如果master宕机了, 没有主机了, 此时6380和6381都是从机, 可以命令slaveof no one手动设置来让自己成为master. 其他slave就可以自动连接到这个新的master.

# 将6381服务关闭, 然后重启,建立与6380的主从关系. 6379是6380的master, 6380是6381的master.
127.0.0.1:6381> SHUTDOWN
not connected> exit
[root@centos7-01 redis-6.x]# ./bin/redis-server redis6381.conf
[root@centos7-01 redis-6.x]# ./bin/redis-cli -p 6381 -a 123456
Warning: Using a password with '-a' or '-u' option on the command line interface may not be safe.
127.0.0.1:6381> SLAVEOF 127.0.0.1 6380
OK
127.0.0.1:6381> info replication
# Replication
role:slave
master_host:127.0.0.1
master_port:6380 # 6380是6381的master.
master_link_status:up
master_last_io_seconds_ago:4
master_sync_in_progress:0
slave_read_repl_offset:4519
slave_repl_offset:4519
slave_priority:100
slave_read_only:1
replica_announced:1
connected_slaves:0
// 省略......

此时将master-6379宕机, 没有了master, 6380依然是slave, 无法写操作.

# 6379宕机
127.0.0.1:6379> SHUTDOWN
not connected>
# 6380-slave 写操作报错
127.0.0.1:6380> set k4 v4 # slave不能写操作
(error) READONLY You can't write against a read only replica.

使用命令slaveof no one 手动设置6380为master主机, 完成写操作.

# 6380变为master
127.0.0.1:6380> SLAVEOF no one
OK
127.0.0.1:6380> INFO replication
# Replication
role:master
connected_slaves:1
slave0:ip=127.0.0.1,port=6381,state=online,offset=4589,lag=0
master_failover_state:no-failover
master_replid:2074cd7c6402435e472b2bd2d0b11ee0b6b3abf1
master_replid2:eeb823aa531ef123e24f83f918a680f2d481e5b5
// 省略......
127.0.0.1:6380>
127.0.0.1:6380> set k4 v4 # 6380变成master后,完成了写操作
OK

# 6381读取到6380新写的数据
127.0.0.1:6381> get k4
"v4"

8.4 哨兵模式

上面的一主二从和层层链路都不是我们实际工作中使用的, 工作中是搭建的高可用的哨兵+集群模式, 当master宕机后, 哨兵会进行选举, 自动从slave中选出一个服务作为新的master, 而且哨兵也是集群的.

概述

主从切换: 当主服务器宕机后, 需要手动把一台从服务器切换为主服务器, 这样人工干预费时费力, 还会造成一段时间内的服务不可用, 不推荐. 更多时候, 我们会推荐使用哨兵模式, Redis2.8版本开始正式提供了Sentinel(哨兵)架构来解决这个问题.

哨兵模式, 首先Redis提供了哨兵的命令, 哨兵是一个独立的进程, 会独立运行, 原理是哨兵通过发送命令, 等待redis服务器响应, 从而达到监控多个运行的Redis实例.
在这里插入图片描述

哨兵的两个作用:

  • 通过发送命令, 让redis服务器返回监控的redis实例运行状态, 包括主服务器和从服务器.
  • 当哨兵检测到master宕机, 会自动将slave切换成master, 然后通过发布订阅模式通知其他的从服务器, 修改配置文件, 让它们切换主机.

然而一个哨兵进程对Redis服务进行监控, 也可能出现问题. 为此, 可以使用多个哨兵进行监控, 各个哨兵之间还会进行监控, 这样就形成了多哨兵模式.
在这里插入图片描述

假设主服务器宕机, 哨兵1线检测到这个结果, 系统并不会马上进行failover过程, 仅仅是哨兵1主观的认为master不可用, 这个现象称为主观下线, 当后面的哨兵也检测到主服务器master不可用, 并且数量达到一定数值时, 那么哨兵之间就会进行一次投票, 投票的结果由一个哨兵发起, 进行failover[故障转移]操作, 选举新master成功后, 就会同步发布订阅模式, 让各个哨兵把自己监控的slave从服务器切换它们的主master, 这个过程称为客观下线.

一主二从的分配,来搭建哨兵.

配置哨兵配置文件 sentinel.conf

# sentinel monitor 监控的服务名称 host port 数值
sentinel monitor redis6379 127.0.0.1 6379 1
# 数值1, 表示如果master主机宕机, 哨兵进行投票,master是否真的宕机了 票数最多的(达到配置的数值),就会重新选举出新的master.

启动哨兵进程

[root@centos7-01 redis-6.x]# ps -ef|grep redis-server 
root       1594      1  0 21:31 ?        00:00:24 ./bin/redis-server *:6380 # slave
root       2074      1  0 22:34 ?        00:00:08 ./bin/redis-server *:6381 # slave
root       2095      1  0 22:45 ?        00:00:08 ./bin/redis-server *:6379 # master
root       2156   1908  0 23:10 pts/3    00:00:00 grep --color=auto redis-server
[root@centos7-01 redis-6.x]#
[root@centos7-01 redis-6.x]# ./bin/redis-sentinel sentinel.conf # 启动哨兵进程
2159:X 20 Apr 2022 23:10:55.521 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
2159:X 20 Apr 2022 23:10:55.521 # Redis version=6.2.6, bits=64, commit=00000000, modified=0, pid=2159, just started
2159:X 20 Apr 2022 23:10:55.521 # Configuration loaded
2159:X 20 Apr 2022 23:10:55.521 * Increased maximum number of open files to 10032 (it was originally set to 1024).
2159:X 20 Apr 2022 23:10:55.521 * monotonic clock: POSIX clock_gettime
                _._
           _.-``__ ''-._
      _.-``    `.  `_.  ''-._           Redis 6.2.6 (00000000/0) 64 bit
  .-`` .-```.  ```\/    _.,_ ''-._
 (    '      ,       .-`  | `,    )     Running in sentinel mode
 |`-._`-...-` __...-.``-._|'` _.-'|     Port: 26379
 |    `-._   `._    /     _.-'    |     PID: 2159
  `-._    `-._  `-./  _.-'    _.-'
 |`-._`-._    `-.__.-'    _.-'_.-'|
 |    `-._`-._        _.-'_.-'    |           https://redis.io
  `-._    `-._`-.__.-'_.-'    _.-'
 |`-._`-._    `-.__.-'    _.-'_.-'|
 |    `-._`-._        _.-'_.-'    |
  `-._    `-._`-.__.-'_.-'    _.-'
      `-._    `-.__.-'    _.-'
          `-._        _.-'
              `-.__.-'

2159:X 20 Apr 2022 23:10:55.522 # WARNING: The TCP backlog setting of 511 cannot be enforced because /proc/sys/net/core/somaxconn is set to the lower value of 128.
2159:X 20 Apr 2022 23:10:55.522 # Sentinel ID is c34ca410b69366f5ec74d2e0b9e93e0213c94245
2159:X 20 Apr 2022 23:10:55.522 # +monitor master redis6379 127.0.0.1 6379 quorum 1 # 哨兵监控master 6379 

如果此时, Master节点宕机, 哨兵会从slave中重新选举出一个新的master.

# 6379 master
127.0.0.1:6379> INFO replication
# Replication
role:master # master
connected_slaves:2 # 2个slave连接 6380 6381
slave0:ip=127.0.0.1,port=6380,state=online,offset=2086,lag=0
slave1:ip=127.0.0.1,port=6381,state=online,offset=2086,lag=0
master_failover_state:no-failover
master_replid:20b15024cdde0f00205909954c6324972cc470fe
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:2086
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:2086
# 6379 宕机
127.0.0.1:6379> SHUTDOWN 

等待一段时间, 查看哨兵日志, 进行了选举操作.

2159:X 20 Apr 2022 23:11:25.601 # +sdown master redis6379 127.0.0.1 6379
2159:X 20 Apr 2022 23:11:25.601 # +odown master redis6379 127.0.0.1 6379 #quorum 1/1
2159:X 20 Apr 2022 23:11:25.601 # +new-epoch 50
2159:X 20 Apr 2022 23:11:25.601 # +try-failover master redis6379 127.0.0.1 6379
2159:X 20 Apr 2022 23:11:25.606 # +vote-for-leader c34ca410b69366f5ec74d2e0b9e93e0213c94245 50
2159:X 20 Apr 2022 23:11:25.606 # +elected-leader master redis6379 127.0.0.1 6379
2159:X 20 Apr 2022 23:11:25.606 # +failover-state-select-slave master redis6379 127.0.0.1 6379
2159:X 20 Apr 2022 23:11:25.700 # -failover-abort-no-good-slave master redis6379 127.0.0.1 6379
2159:X 20 Apr 2022 23:11:25.769 # Next failover delay: I will not start a failover before Wed Apr 20 23:17:26 2022
???? 选举没有成功, 不知道咋回事啊, 正常应该会从6380 , 6381中选举一个新的master

上面哨兵模式下, 选举master失败, 经过查阅资料发现了问题 , 原来是我主从服务器都设置了密码, 哨兵的配置文件也要进行密码授权, 才能正常监听到redis服务的状态.

# 再次修改sentinel.conf配置文件
sentinel monitor redis6379 127.0.0.1 6379 1
# 和master的密码保持一致
sentinel auth-pass redis6379 123456 

再次启动哨兵进程, 启动redis6379

[root@centos7-01 redis-6.x]# ./bin/redis-sentinel sentinel.conf
2220:X 20 Apr 2022 23:23:24.469 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
2220:X 20 Apr 2022 23:23:24.469 # Redis version=6.2.6, bits=64, commit=00000000, modified=0, pid=2220, just started
2220:X 20 Apr 2022 23:23:24.469 # Configuration loaded
2220:X 20 Apr 2022 23:23:24.469 * Increased maximum number of open files to 10032 (it was originally set to 1024).
2220:X 20 Apr 2022 23:23:24.469 * monotonic clock: POSIX clock_gettime
                _._
           _.-``__ ''-._
      _.-``    `.  `_.  ''-._           Redis 6.2.6 (00000000/0) 64 bit
  .-`` .-```.  ```\/    _.,_ ''-._
 (    '      ,       .-`  | `,    )     Running in sentinel mode
 |`-._`-...-` __...-.``-._|'` _.-'|     Port: 26379
 |    `-._   `._    /     _.-'    |     PID: 2220
  `-._    `-._  `-./  _.-'    _.-'
 |`-._`-._    `-.__.-'    _.-'_.-'|
 |    `-._`-._        _.-'_.-'    |           https://redis.io
  `-._    `-._`-.__.-'_.-'    _.-'
 |`-._`-._    `-.__.-'    _.-'_.-'|
 |    `-._`-._        _.-'_.-'    |
  `-._    `-._`-.__.-'_.-'    _.-'
      `-._    `-.__.-'    _.-'
          `-._        _.-'
              `-.__.-'

2220:X 20 Apr 2022 23:23:24.470 # WARNING: The TCP backlog setting of 511 cannot be enforced because /proc/sys/net/core/somaxconn is set to the lower value of 128.
2220:X 20 Apr 2022 23:23:24.470 # Sentinel ID is 0c300a1bceb96652a7ac63e821ea187f1d283e0e
2220:X 20 Apr 2022 23:23:24.470 # +monitor master redis6379 127.0.0.1 6379 quorum 1 # 哨兵监控6379 master
2220:X 20 Apr 2022 23:23:45.009 * +slave slave 127.0.0.1:6380 127.0.0.1 6380 @ redis6379 127.0.0.1 6379 # 6380 salve
2220:X 20 Apr 2022 23:23:45.015 * +slave slave 127.0.0.1:6381 127.0.0.1 6381 @ redis6379 127.0.0.1 6379 # 6381 salve

然后再次重现上面的步骤, master6379宕机, 等待一会儿, 查看哨兵日志, 已经完成了自动选举, 6380被选为新的master.

 # 6379 master宕机
2220:X 20 Apr 2022 23:24:45.998 # +sdown master redis6379 127.0.0.1 6379
2220:X 20 Apr 2022 23:24:45.999 # +odown master redis6379 127.0.0.1 6379 #quorum 1/1
# 哨兵发起命令, 2个slave之间进行选举
2220:X 20 Apr 2022 23:24:45.999 # +new-epoch 2
2220:X 20 Apr 2022 23:24:45.999 # +try-failover master redis6379 127.0.0.1 6379
2220:X 20 Apr 2022 23:24:46.006 # +vote-for-leader 0c300a1bceb96652a7ac63e821ea187f1d283e0e 2
2220:X 20 Apr 2022 23:24:46.006 # +elected-leader master redis6379 127.0.0.1 6379
2220:X 20 Apr 2022 23:24:46.006 # +failover-state-select-slave master redis6379 127.0.0.1 6379
# 选举6380
2220:X 20 Apr 2022 23:24:46.107 # +selected-slave slave 127.0.0.1:6380 127.0.0.1 6380 @ redis6379 127.0.0.1 6379
2220:X 20 Apr 2022 23:24:46.107 * +failover-state-send-slaveof-noone slave 127.0.0.1:6380 127.0.0.1 6380 @ redis6379 127.0.0.1 6379
2220:X 20 Apr 2022 23:24:46.181 * +failover-state-wait-promotion slave 127.0.0.1:6380 127.0.0.1 6380 @ redis6379 127.0.0.1 6379
2220:X 20 Apr 2022 23:24:47.149 # +promoted-slave slave 127.0.0.1:6380 127.0.0.1 6380 @ redis6379 127.0.0.1 6379
2220:X 20 Apr 2022 23:24:47.149 # +failover-state-reconf-slaves master redis6379 127.0.0.1 6379
2220:X 20 Apr 2022 23:24:47.215 * +slave-reconf-sent slave 127.0.0.1:6381 127.0.0.1 6381 @ redis6379 127.0.0.1 6379
2220:X 20 Apr 2022 23:24:48.154 * +slave-reconf-inprog slave 127.0.0.1:6381 127.0.0.1 6381 @ redis6379 127.0.0.1 6379
2220:X 20 Apr 2022 23:24:48.155 * +slave-reconf-done slave 127.0.0.1:6381 127.0.0.1 6381 @ redis6379 127.0.0.1 6379
2220:X 20 Apr 2022 23:24:48.207 # +failover-end master redis6379 127.0.0.1 6379
# 最终6380替换6379成为新的master
2220:X 20 Apr 2022 23:24:48.207 # +switch-master redis6379 127.0.0.1 6379 127.0.0.1 6380
2220:X 20 Apr 2022 23:24:48.208 * +slave slave 127.0.0.1:6381 127.0.0.1 6381 @ redis6379 127.0.0.1 6380
2220:X 20 Apr 2022 23:24:48.208 * +slave slave 127.0.0.1:6379 127.0.0.1 6379 @ redis6379 127.0.0.1 6380
2220:X 20 Apr 2022 23:25:18.282 # +sdown slave 127.0.0.1:6379 127.0.0.1 6379 @ redis6379 127.0.0.1 6380 # 检测6379还是down

我们通过查看6380, 已经是master. 并且6381的配置改写, 认6380为master.

# 6380 master
127.0.0.1:6380> info replication
# Replication
role:master
connected_slaves:1
slave0:ip=127.0.0.1,port=6381,state=online,offset=4968,lag=1
master_failover_state:no-failover
master_replid:8f83123fd97417dec1455dbc558f79d9caf654ae
master_replid2:5961664a5c896ed322afd82caac43210c677fb35
// ... 

### 6381 slave , 认6380为master
127.0.0.1:6381> INFO replication
# Replication
role:slave
master_host:127.0.0.1
master_port:6380
master_link_status:up
master_last_io_seconds_ago:2
master_sync_in_progress:0
slave_read_repl_offset:6068
slave_repl_offset:6068
slave_priority:100
slave_read_only:1
replica_announced:1
connected_slaves:0
master_failover_state:no-failover
master_replid:8f83123fd97417dec1455dbc558f79d9caf654ae
master_replid2:5961664a5c896ed322afd82caac43210c677fb35
// ...

注意: 如果此时6379重新启动后, 也不会成为master, 会以6380为master.

# 6379复活
[root@centos7-01 redis-6.x]# ./bin/redis-server redis6379.conf
[root@centos7-01 redis-6.x]# ./bin/redis-cli -p 6379 -a 123456
127.0.0.1:6379> INFO replication
# Replication
role:slave
master_host:127.0.0.1
master_port:6380
master_link_status:up
master_last_io_seconds_ago:0
master_sync_in_progress:0
slave_read_repl_offset:64499
slave_repl_offset:64499
slave_priority:100
slave_read_only:1
replica_announced:1
connected_slaves:0
master_failover_state:no-failover
master_replid:8f83123fd97417dec1455dbc558f79d9caf654ae
master_replid2:0000000000000000000000000000000000000000

# 查看6380, 连接了2个salve, 其中一个是复活的6379
127.0.0.1:6380> info replication
# Replication
role:master
connected_slaves:2
slave0:ip=127.0.0.1,port=6381,state=online,offset=67637,lag=0
slave1:ip=127.0.0.1,port=6379,state=online,offset=67637,lag=0 # slave 6379
master_failover_state:no-failover
master_replid:8f83123fd97417dec1455dbc558f79d9caf654ae
master_replid2:5961664a5c896ed322afd82caac43210c677fb35
master_repl_offset:67637
second_repl_offset:2760
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:67637

# 6379复活了, 只能是slave, 认6380为master
2220:X 20 Apr 2022 23:38:10.334 # -sdown slave 127.0.0.1:6379 127.0.0.1 6379 @ redis6379 127.0.0.1 6380
2220:X 20 Apr 2022 23:38:20.299 * +convert-to-slave slave 127.0.0.1:6379 127.0.0.1 6379 @ redis6379 127.0.0.1 6380

优点:

  • 哨兵集群, 基于主从复制模式, 所有的主从配置优点, 它全有.
  • 主从可以切换, 故障可以转移, 系统的可用性就更好.
  • 哨兵模式就是主从模式的升级, 手动到自动, 更加健壮.

缺点:

  • Redis不方便在线扩容, 集群容量一旦达到上限, 在线扩容十分麻烦.
  • 实现哨兵模式的配置有很多选择, 麻烦.

哨兵模式的全部配置

# 哨兵实例运行的端口, 默认26379
port 26379
# 哨兵的工作目录
dir /tmp
# 哨兵监控的Redis主节点的ip,port
# master-name 可以自己命名的主节点名字, 只能由字母A-z,数字0-9, 以及这三个字符".-_"组成
# quorum 配置的多少个哨兵, 统一认为master主节点失联, 就认为master真的失联.
# sentinel monitor <master-name> <ip> <redis-port> <quorum>
sentinel monitor mymaster 127.0.0.1 6379
# 当在Redis实例中开启了requirepass foobared 授权密码, 那么所有连接Redis实例的客户端都要提供密码.
# 设置哨兵连接主从的密码, 必须和主从设置一致的密码.
sentinel auth-pass mymaster passwd
# 指定多少毫秒后, 主节点没有应答哨兵sentinel, 哨兵就主观认为主节点下线, 默认30s.
# sentinel down-after-milliseconds <master-name> <milliseconds>
sentinel down-after-milliseconds mymaster 30000
# 指定在发生failover主备切换时, 最多可以有多少个slave同时对新的master进行同步.
# 配置的数字越小, 完成failover的时间就越长.
# 如果数字越大, 意味着多的slave因为replication而不可用.
# 可以通过将这个值设置为1来保证每次只有一个slave处于不能处理命令请求的状态.
# sentinel parallel-syncs <master-name> <numslaves>
sentinel parallel-syncs mymaster 1
# 故障转移的超时时间 failover-timeout可以用在以下这些方面.
# 1.同一个sentinel对同一个master两次failover之间的时间间隔.
# 2.当一个slave从一个错误的master同步数据开始计算时间, 直到slave被纠正为向正确的master同步数据为止.
# 3.想要取消一个正在进行的failover所需的时间.
# 4.当进行failover时, 配置所有slaves指向新的master所需的最大时间, 即时过了这个时间, slaves依然会被正确配置为指向master, 但是就不按照
# parallel-syncs的规则来了.
# sentinel failover-timeout <master-name> <milliseconds>
sentinel failover-timeout mymaster 180000
# SCRIPTS EXECUTION
# 配置当某一事件发生时所需要执行的脚本, 可以通过脚本来通知管理员, 例如当系统运行不正确时发送邮件通知相关人员.
# 对于脚本的运行结果有如下规则:
# 若脚本执行后返回1, 那么该脚本稍后将会被再次执行, 重复次数目前默认为10
# 若脚本执行后返回2, 或者比2更高的一个返回值, 脚本将不会重复执行.
# 如果脚本在执行过程中, 由于收到系统中断信号被终止了, 则同返回值为1时的行为相同.
# 一个脚本的最大执行时间为60s, 如果超过这个时间, 脚本将会被一个sigkill信号终止, 之后重新执行.

# 通知型脚本: 当sentinel有任何警告级别的事件发生时(比如说redis实例的主观失效和客观失效等等), 将会去调用这个脚本,这时这个脚本应该通过邮件, SMS等方式去通知系统管理员关于系统不正常运行的信息, 调用该脚本时, 将传给脚本两个参数, 一个是事件的类型, 一个是事件的描述. 如果sentinel.conf配置文件中配置了这个脚本路径, 那么必须保证这个脚本存在于这个路径并且是可执行的,否则sentile无法正常启动成功.
# 通知脚本
# sentinel notification-script <master-name> <script-path>
sentinel notification-script mymaster /var/redis/notify.sh

# 客户端重新配置主节点参数脚本
# sentinel client-reconfig-script <master-name> <script-path>
 sentinel client-reconfig-script mymaster /var/redis/reconfig.sh

9. Redis缓存穿透和雪崩

Redis缓存的使用极大的提升了应用程序的性能和效率, 特别是数据查询方面, 但同时, 它也带来了一些问题, 其中最重要的问题就是数据一致性问题, 如果对数据一致性的要求很高, 那么就不能使用缓存,而是使用关系型数据库.

另外的一些典型的问题, 缓存穿透, 缓存雪崩和缓存击穿. 目前业界也有比较好的解决方案.

9.1 缓存穿透(查不到)

概念

用户查询一个key的数据, 发现redis缓存中没有, 持久层数据库查询也没有导致本次查询失败, 当这种请求很多时, 缓存都没有命中, 查询请求全部打在了持久层数据库上, 会给我们的持久层数据库带来很大的压力, 这种现象就是缓存穿透.

解决方案

==布隆过滤器==

布隆过滤器是一种数据结构, 对所有可能查询的参数以hash形式存储, 在控制层进行校验, 不符合则丢弃, 从而避免了对底层存储系统的查询压力.
在这里插入图片描述

==缓存空对象==

当存储层不命中后, 即使返回的空对象也将其缓存起来, 同时会设置一个过期时间 , 之后再访问这个数据将会从缓存中获取, 保护了后端的数据源.
在这里插入图片描述

但是这种方法会存在两个问题:

  • 如果空值能够被缓存起来, 这意味着缓存需要更多的空间存储更多的键, 因为可能会有大量的空值的键.
  • 即使对空值设置了过期时间, 还是会存在缓存层和存储层的数据会有一段时间窗口的不一致, 这对需要保持一致性的业务会有影响.

9.2 缓存击穿(高并发,缓存过期)

概述

一个非常热点的key, 并发集中对这一个点进行访问, 当这个key在失效的瞬间, 持续的并发就会击穿缓存, 直接请求到数据库, 就像在一个屏障上凿开了一个洞一样.

当某个key在过期的瞬间, 有大量的请求并发访问, 这类数据一般是热点数据, 由于缓存过期, 会同时访问数据库来查询最新数据, 并且回写缓存, 会导致数据库瞬间压力过大而崩溃.

解决方案

==设置热点数据永不过期==

从缓存层面来看, 没有设置过期时间, 所以不会出现热点key过期后产生的问题.

==加互斥锁==

使用分布式锁, 保证对每个key同时只有一个线程去查询后端数据库服务, 其他线程没有获取到分布式锁的权限, 就会阻塞等待. 这样会把高并发的压力转移到分布式锁上, 因此对分布式锁的考验很大.
在这里插入图片描述

9.3 缓存雪崩

概念

是指在某一个时间段, 缓存集中过期失效或Redis宕机. 此时高并发的查询请求过来, 缓存无法命中, 所有查询请求打在了持久层数据库上, 造成持久层数据库的崩溃.
在这里插入图片描述

集中过期并不是非常致命的, 更致命的缓存雪崩是当缓存服务器某个节点宕机或断网. 因为自然形成的缓存雪崩, 一定是在某个时间段集中创建缓存, 这个时候数据库也是可以顶住压力的, 无非就是对数据库产生周期性的压力而已. 而缓存服务器节点的宕机, 对数据库服务器造成的压力是不可预知的, 很有可能瞬间就把数据库压崩塌.

解决方案

==Redis高可用==

Redis存在宕机风险, 可以横向扩展, 多加几台Redis节点, 这样一台Redis宕机后, 其他的节点还是可以继续工作, 也就是搭建高可用集群.(异地多活)

==限流降级==

在缓存失效后, 通过加锁或者队列来控制读数据库写缓存的线程数量, 比如: 对某个key只允许一个线程查询数据和写缓存, 其他线程等待.

==数据预热==

在服务正式部署之前, 把可能命中的数据预先访问一遍, 这样部分可能高并发请求的数据会先被加载到缓存中.

在即将发生大并发访问之前,手动触发加载不同的key到缓存中, 并设置不同的过期时间, 让缓存失效的时间点尽量均匀分布.


文章作者: 王子
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 王子 !
评论
  目录