Redis

Redis是什么?

Redis 是一个 基于内存的 key-value 存储系统,它可以用作数据库、缓存和消息中间件。

  1. 内存存储:

    Redis 将数据存储在内存中,读写速度非常快,非常适合需要高性能的应用场景。支持每秒数十万次的读写操作,是需要快速响应场景如缓存,的理想选择。

  2. 数据类型:

    • string(字符串): 基本的数据存储单元,可以存储字符串、整数或者浮点数。
    • hash(哈希): 一个键值对集合,可以存储多个字段。
    • list(列表): 一个简单的列表,可以存储一系列的字符串元素,可以添加一个元素到列表的头部(左边)或者尾部(右边)。
    • set(集合): 一个无序集合,可以存储不重复的字符串元素,是通过哈希表实现的,所以添加,删除,查找的复杂度都是 O(1)。
    • zset(sorted set:有序集合): 类似于集合,但是每个元素都会关联一个double类型的分数,redis正是通过分数来为集合中的成员进行从小到大的排序,zset的成员是唯一的,但分数(score)却可以重复,适合做排行榜。
    • 位图(Bitmaps):基于字符串类型,可以对每个位进行操作。
    • 超日志(HyperLogLogs):用于基数统计,可以估算集合中的唯一元素数量。
    • 地理空间(Geospatial):用于存储地理位置信息。
    • 发布/订阅(Pub/Sub):一种消息通信模式,允许客户端订阅消息通道,并接收发布到该通道的消息。
    • 流(Streams):用于消息队列和日志存储,支持消息的持久化和时间排序。
    • 模块(Modules):Redis 支持动态加载模块,可以扩展 Redis 的功能。
  3. 持久化:

    • Redis 支持数据持久化,可以将内存中的数据异步地写入磁盘,以确保数据不会因为服务器宕机而丢失。主要有两种持久化方式:
      • RDB(Redis Database):生成数据快照并保存到磁盘。
      • AOF(Append Only File):记录每个写操作到日志文件中。
  4. 原子性操作:

    • Redis 的所有操作都是原子性的,这意味着操作要么完全执行,要么完全不执行。这种特性对于确保数据的一致性和完整性至关重要,尤其是在高并发环境下处理事务时。
  5. 主存复制:

    • 主存复制是指将数据从一个 Redis 实例(称为主节点或 Master)复制到一个或多个其他 Redis 实例(称为从节点或 Slave)。这项功能有助于实现数据冗余、读写分离和系统的高可用性。
      1. 数据冗余
        • 通过复制,主节点的数据会实时地同步到从节点。这意味着即使主节点出现故障,从节点仍然拥有完整的数据副本,从而减少数据丢失的风险。
      2. 负载均衡
        • 在读写分离的架构中,所有写操作都发往主节点,而读操作可以由从节点处理。这样可以有效地分散负载,提高系统的读性能。
      3. 高可用性
        • 如果主节点发生故障,从节点可以迅速提升为新的主节点,以保证系统的持续可用性。这通常通过 Redis Sentinel 实现。
  6. 支持发布/订阅模式:

    • Redis 内置了发布/订阅模式(Pub/Sub),允许客户端之间通过消息传递进行通信。这使得 Redis 可以作为消息队列和实时数据传输的平台。

发布和订阅

  1. Redis发布订阅(pub/sub)是一种消息通信模式:发送者(pub)发送消息,订阅者(sub)接受消息
  2. Redis客户端可以订阅任意数量的频道
  3. 当给某个频道发布消息后,消息就会发生给订阅的客户端

消息队列

  1. 与消息队列进行交互的实体有两类,一类是生产者(producer),另一类是消费者(consumer)。生产者将需要处理的任务放入任务队列中,而消费者则不断地从任务队列中读取任务信息并执行。

  2. Subscriber: 收音机, 可以收到多个频道, 并以队列方式显示

    Publisher: 电台, 可以往不同的 FM 频道中发消息

    Channel: 不同频率的 FM 频道

    从 Pub/Sub 的机制来看, 它更像是一个广播系统, 多个订阅者(Subscriber) 可以订阅多个频道(Channel), 多个发布者(Publisher) 可以往多个频道(Channel) 中发布消息。

发布订阅模式分类

  1. 一个发布者,多个订阅者

    主要应用:通知、公告

  2. 多个发布者,一个订阅者

    主要应用:排行榜、投票、计数

  3. 多个发布者,多个订阅者

    主要应用:群聊、聊天

发布订阅操作

1
2
PUBLISH channel msg  
将信息 message 发送到指定的频道 channel
1
2
SUBSCRIBE channel [channel ...]
订阅频道, 可以同时订阅多个频道
1
2
UNSUBSCRIBE [channel ...]
取消订阅指定的频道, 如果不指定频道, 则会取消订阅所有频道
1
2
PSUBSCRIBE pattern [pattern ...]
订阅一个或多个符合给定模式的频道, 每个模式以 * 作为匹配符, 比如 it* 匹配所 有以 it 开头的频道( it.news 、 it.blog 、 it.tweets 等等), news.* 匹配所有 以 news. 开头的频道( news.it 、 news.global.today 等等), 诸如此类
1
2
PUNSUBSCRIBE [pattern [pattern ...]]
退订指定的规则, 如果没有参数则会退订所有规则

示例:

两个客户端订阅channel频道,另一个同时订阅channel频道

发布者向频道channel发布消息“hello”

两个客户端均接收到消息

再向频道channel2发布消息“world”

只有订阅了频道channel2的客户端收到消息

  1. 发布者发布消息后,返回的值为订阅者的数量
  2. 发布的消息没有持久化
  3. 订阅的客户端,只能收到订阅后发布的消息

Java 使用 Redis

  1. 引入依赖

    1
    2
    3
    4
    5
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
    <version>2.7.6</version>
    </dependency>
  2. 配置数据源

    在application.yml中配置:

    1
    2
    3
    4
    5
    6
    spring:
    redis:
    host: localhost
    port: 6379
    password:
    database: 0
  3. 编写配置类,创建RedisTemplate对象

    Spring Data Redis中提供了一个高度封装的类:RedisTemplate,对相关api进行了归类封装,将同一类型操作封装为operation接口,具体分类如下:

    ValueOperations:string数据操作

    SetOperations:set类型数据操作

    ZSetOperations:zset类型数据操作

    HashOperations:hash类型的数据操作

    ListOperations:list类型的数据操作

    但需要对RedisTemplat进行自定义序列化,因为Redis默认使用 JdkSerializationRedisSerializer 来序列化和反序列化数据,这种方式序列化的数据是二进制格式,不宜阅读和调试,而且是Java特有的,其他语言难以处理,影响跨平台兼容性,使用这种方式还会产生较大的数据,影响效率。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.data.redis.connection.RedisConnectionFactory;
    import org.springframework.data.redis.core.RedisTemplate;
    import org.springframework.data.redis.serializer.RedisSerializer;


    @Configuration
    public class RedisTemplateConfig {
    @Bean
    public RedisTemplate<String, Object> redisTemplate (RedisConnectionFactory connectionFactory){
    RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
    redisTemplate.setConnectionFactory(connectionFactory);
    redisTemplate.setKeySerializer(RedisSerializer.string());
    return redisTemplate;
    }
    }
  4. 使用redis

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69

    import jakarta.annotation.Resource;
    import org.junit.jupiter.api.Test;
    import org.springframework.boot.test.context.SpringBootTest;
    import org.springframework.data.redis.core.*;

    @SpringBootTest
    class RedisApplicationTests {

    @Resource
    private RedisTemplate<String, Object> redisTemplate;

    @Test
    public void redisTest01(){
    ValueOperations<String,Object> valueOperations = redisTemplate.opsForValue();
    //操作字符串类型
    valueOperations.set("aa","aa");
    System.out.println((String) valueOperations.get("aa"));
    //aa

    //操作哈希类型
    HashOperations<String,String,Object> hashOperations = redisTemplate.opsForHash();
    hashOperations.put("user","name","jack");
    hashOperations.put("user","age",11);
    System.out.println(hashOperations.get("user", "name"));
    System.out.println(hashOperations.get("user", "age"));
    System.out.println(hashOperations.keys("user"));
    System.out.println(hashOperations.values("user"));
    //jack
    //11
    //[age, name]
    //[11, jack]
    //hashOperations.delete("age");

    //操作列表
    ListOperations<String, Object> listOperations = redisTemplate.opsForList();
    listOperations.leftPushAll("mylist","a","b","c");
    System.out.println(listOperations.range("mylist", 0, -1));
    listOperations.leftPush("mylist","d");
    listOperations.rightPop("mylist"); //删除
    System.out.println(listOperations.size("mylist"));

    //集合操作
    SetOperations<String, Object> setOperations = redisTemplate.opsForSet();
    setOperations.add("set1","a","b","c","d");
    setOperations.add("set2","a","b","x","y");
    System.out.println(setOperations.members("set1"));
    System.out.println(setOperations.size("set1"));
    System.out.println(setOperations.intersect("set1", "set2"));//交集
    System.out.println(setOperations.union("set1", "set2"));//并集
    setOperations.remove("set1","a","b");
    //[b, a, c, d]
    //4
    //[b, a, c, d]
    //[b, x, a, c, y, d]

    //有序集合
    ZSetOperations<String, Object> zSetOperations = redisTemplate.opsForZSet();
    zSetOperations.add("zset1","a",10);
    zSetOperations.add("zset1","b",12);
    zSetOperations.add("zset1","c",8);
    System.out.println(zSetOperations.range("zset1", 0, -1));
    zSetOperations.incrementScore("zset1","c",10);//为c加十分
    //[c, a, b]

    }

    }

Redis持久化

RDB(Redis Database)

RDB执行流程

  1. 在指定的时间间隔内将内存中的数据集快照写入磁盘,也就是Snapshot快照,恢复时将快照文件读入内存。
  2. RDB是redis默认持久化策略。
  3. 具体流程如下:
    1. redis 客户端执行 bgsave 命令或者自动触发 bgsave 命令;
    2. 主进程判断当前是否已经存在正在执行的子进程,如果存在,那么主进程直接返回;
    3. 如果不存在正在执行的子进程,那么就 fork 一个新的子进程进行持久化数据,fork 过程是阻塞的,fork 操作完成后主进程即可执行其他操作;
    4. 子进程先将数据写入到临时的 rdb 文件中,待快照数据写入完成后再原子替换旧的 rdb文件;
    5. 同时发送信号给主进程,通知主进程 rdb 持久化完成,主进程更新相关的统计信息
  4. 优缺点
    • 整个过程中,主进程是不进行任何IO操作的,这就确保了极高的性能
    • 如果需要进行大规模数据的恢复,切对于数据恢复的完整性不是非常敏感,那RDB方式要比AOF方式更加高效
    • RDB的缺点是最后一次持久化后的数据可能丢失(如果是正常关闭redis,仍然会进行持久化,不会造成数据丢失,如果是redis异常终止/宕机,就会造成数据丢失)

Fork&Copy-On-Write

  1. Fork的作用是复制一个与当前进程一样的进程,新进程的所有数据(变量、环境变量、程序计数器等)数值都和原进程一致,但是是一个全新的进程,并作为原进程的子进程。
  2. 在Linux程序中,fork()会产生一个和父进程完全相同的子进程,但子进程在此后多会exec系统调用,出于效率考虑,Linux引入了“写时复制技术 即:Copy-on-Write”
  3. 一般情况父进程和子进程会同用一段物理内存,只有进程空间的各段的内容要发生变化时,才会将父进程的内容复制一份给子进程。

RDB配置

dump.rdb文件

  1. 快照文件默认为dump.rdb,在redis.conf中可以配置

  2. dump.rdb保存位置默认为:redis启动命令行所在的目录下

    在哪个目录下启动,dump.rdb就保存在哪,如果每次启动位置不一致,会导致数据恢复时缺失,所以最好讲rdb文件的保存路径进行统一设置,不论在哪启动都将数据保存在一个rdb文件中。

默认快照配置

如果没有开启save的注释,那么在退出redis时,也会进行备份,更新dump.rdb

save & bgsave

  1. save:用save命令时,只进行保存,其他不管,会导致客户端其他命令阻塞,直到 RDB 过程完成为止,对于内存比较大的实例造成长时间阻塞,基本不采用;
  2. bgsave:Redis 会在后台异步进行快照操作, 快照同时还可以响应客户端请求。 阻塞只发生在 fork 创建进程阶段,这个时间很短;
  3. 可以通过 lastsave 命令获取最后一次成功执行快照的时间(unix 时间戳) , 可以使用工具转换
  4. 禁用save:给save传入空字符串 动态停止 RDB: redis-cli config set save “”

flushall

  1. 清空整个redis 服务器的数据(删除所有数据库的所有数据)
  2. 同时也会产生dump.rdb,只不过数据为空

stop-write-on-bgsave-error

当redis无法写入磁盘的话(比如磁盘满了),直接关掉redis的写操作

rdbcompression

  1. 对于存储在磁盘中的快照,可以设置是否进行压缩存储。如果是的话,redis会采用LZF算法进行压缩
  2. 如果不想消耗CPU来进行压缩的话,可以设置为关闭此功能,默认yes

RDB备份&恢复

重要/敏感的数据建议在MySQL保存一份

  1. 查询rdb文件目录

  2. 将dump.rdb进行备份,如果有必要可以写shell脚本来定时备份。

  3. 清空数据

  4. 删除dump.rdb

  5. 将备份文件改名并重启redis

  6. 数据恢复

RDB优势&劣势

优势:

  1. 适合大规模的数据恢复
  2. 对数据完整性和一致性要求不高更适合使用
  3. 节省磁盘空间
  4. 恢复速度快

劣势:

  1. 虽然redis在fork时使用了写时拷贝技术,但是如果数据庞大时还是比较消耗性能
  2. 在备份周期在一定时间间隔做一次备份,如果redis意外down掉(如果正常关闭redis,仍然会进行RDB备份,不会丢失数据),就会丢失最后一次快照后的所有改动。

AOF(Append Of File)

AOF是什么?

  1. 以日志的形式来记录每个写操作(增量保存),将redis执行过的都所有写指令记录下来(比如set/del 操作会被记录,读操作get不会被记录)
  2. 只许追加文件但不可以改写文件
  3. redis启动之初会读取该文件重构数据
  4. redis重启动的话就根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作

AOF执行流程

  1. 客户端的请求写命令会被append追加到AOF缓冲区内
  2. AOF缓冲区根据AOF持久化策略【always、everysec、no】将操作sync同步到磁盘的AOF文件中
  3. AOF文件大小超过重写策略或手动重写时,会对AOF文件rewrite重写,压缩AOF文件容量
  4. redis服务重启时,会重新load加载AOF文件中的写操作达到恢复数据的目的

AOF开启

  1. 在redis.conf文件中配置

  2. AOF文件的保存路径,和RDB的路径一致

  3. AOF和RDB同时开启时,系统默认取AOF的数据

AOF演示

  1. 开启AOF

  2. 重启redis服务,查看AOF文件,大小为0

  3. 对redis进行操作

  4. 查看AOF文件

AOF备份&恢复

AOF 的备份机制和性能虽然和 RDB 不同, 但是备份和恢复的操作同 RDB 一样, 都是拷贝备
份文件, 需要恢复时再拷贝到 Redis 工作目录下, 启动系统即加载

正常恢复:

  1. 修改默认的 appendonly no, 改为 yes
  2. 将有数据的 aof 文件定时备份, 需要恢复时, 复制一份保存到对应目录(查看目录:config
    get dir)
  3. 恢复:重启 redis 然后重新加载

异常恢复:

遇到AOF文件损坏,通过/usr/local/bin/redis-check-aof --fix appendonly.aof 进行恢复

示例:

  1. 当前数据

  2. 在AOF文件后添加内容,破坏AOF文件

  3. 重启redis,发现启动失败

  4. 使用/usr/local/bin/redis-check-aof --fix appendonly.aof修复

  5. 重启,可以使用了

  6. 修复文件可能会造成数据丢失,但总比完全不能用好

AOF同步频率

  1. **appendfsync always :**始终同步,每次redis都写入都会立刻写入日志;性能较差,但数据完整性较好
  2. appendfsync everysec:默认】每秒同步,每秒记入日志一次,如果宕机,本秒的数据可能会丢失
  3. **appendfsync no:**redis不主动同步,把同步时机交给操作系统

Rewrite压缩

  1. AOF 文件越来越大,需要定期对 AOF 文件进行重写达到压缩
  2. 旧的 AOF 文件含有无效命令会被忽略,保留最新的数据命令 , 比如 set a a1 ; set a b1 ;set a c1; 保留最后一条指令就可以了
  3. 多条写命令可以合并为一个 , 比如 set a c1 b b1 c c1
  4. AOF 重写降低了文件占用空间
  5. 更小的 AOF 文件可以更快的被 redis 加载

手动触发:

直接调用bgrewriteaof命令

自动触发:

  1. auto-aof-rewrite-min-size: AOF 文件最小重写大小, 只有当 AOF 文件大小大于该值时候才能重写, 默认配置 64MB
  2. auto-aof-rewrite-percentage: 当前 AOF 文件大小和最后一次重写后的大小之间的比率等于或者大于指定的增长百分比,如 100 代表当前 AOF 文件是上次重写的两倍时候才重写
  3. 系统载入时或者上次重写完毕时,Redis 会记录此时 AOF 大小,设为base_size,如果 Redis 的 AOF 当前大小>= base_size +base_size*100% (默认)且当前大小>=64mb(默认)的情况下,Redis 会对 AOF 进行重写

AOF优势&劣势

优势:
1、 备份机制更稳健, 丢失数据概率更低。
2、可读的日志文本,通过操作 AOF 稳健,可以处理误操作
劣势:
1、 比起 RDB 占用更多的磁盘空间
2、恢复备份速度要慢
3、每次读写都同步的话,有一定的性能压力

RDB or AOF ?

官方建议:两个都用

如果只做缓存:如果你只希望你的数据在服务器运行的时候存在, 你也可以不使用任何持久化方式

Redis事务

Redis 的事务是什么?

  1. Redis 事务是一个单独的隔离操作: 事务中的所有命令都会序列化、 按顺序地执行
  2. 事务在执行的过程中,不会被其他客户端发送来的命令请求所打断
  3. Redis 事务的主要作用就是串联多个命令防止别的命令插队

Redis 事务三特性

  1. 单独的隔离操作:
    1、 事务中的所有命令都会序列化、 按顺序地执行
    2、 事务在执行的过程中, 不会被其他客户端发送来的命令请求所打断
  2. 没有隔离级别的概念
    队列中的命令(指令), 在没有提交前都不会实际被执行
  3. 不保证原子性
    事务执行过程中, 如果有指令执行失败, 其它的指令仍然会被执行, 没有回滚

Redis事务相关命令

Multi、 Exec、 discard

示意图:

  1. 从输入 Multi 命令开始,输入的命令都会依次进入命令队列中,但不会执行
  2. 输入 Exec 后,Redis 会将之前的命令队列中的命令依次执行
  3. 组队的过程中可以通过 discard 来放弃组队
  4. 示例:

【特殊情况一】如果在组队阶段报错(命令本身出现错误), 会导致 exec 失败, 那么事务的所有指令都不会被执行

【说明】:因为在组队阶段出现错误,导致exec执行事务时,事务失败,所有指令都不会被执行,此时具有原子性

【特殊情况二】如果组队成功, 但是指令有不能正常执行的, 那么 exec 提交, 会出现有成功有失败情况,也就是事务得到部分执行, 这种情况下, Redis 事务不具备原子性.

【说明】组队阶段的命令本身没有出现错误,但执行时出现错误,导致部分指令成功,此时事务不具有原子性

事务冲突

问题引入

抢票问题,一共有十张票,有三个人要买票,分别购买6,5,1张票

如果没有控制,会出现超卖现象,最终票数为-2

悲观锁

  1. 悲观锁(Pessimistic Lock), 顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修
    改,所以每次在拿数据的时候都会上锁 。
  2. 这样别人/其它请求想拿这个数据就会 block 直到它拿到锁
  3. 悲观锁是锁设计理念, 传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,
    表锁等,读锁,写锁等,都是在做操作之前先上锁.

乐观锁

  1. 乐观锁(Optimistic Lock), 顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁
  2. 但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。
  3. 乐观锁适用于多读的应用类型,这样可以提高吞吐量。Redis 就是利用这种 check-and-set机制实现事务的

主从复制

主从复制介绍

单个redis实际生产过程中可能出现的问题:

  1. 机器宕机,redis直接停用,影响业务
  2. 容量瓶颈,内存大小不够
  3. QPS瓶颈,读的并发量很高很高,一个服务不够

主从复制简单来说,就是将主机的数据同步到从机上,主机主要完成写操作,从机完成读操作,从而减轻主机压力

主从复制示意图:

  1. 上图描述了主机数据更新后, 自动同步到备机的 master/slaver 机制
  2. Master 以写为主,Slaver 以读为主
  3. 好处: 读写分离, 提升效率 (理解: 读写分离后, 将读和写操作分布到不同的Reids, 减少单个 Redis 的压力, 提升效率)
  4. 好处: 容灾快速恢复 (理解: 如果某个 slaver , 不能正常工作, 可以切换到另一个 slaver)
  5. 主从复制, 要求是 1 主多从, 不能有多个 Master( 理解: 如果有多个主服务器 Master,那么 slaver 不能确定和哪个 Master 进行同步, 出现数据紊乱)
  6. 要解决主服务器的高可用性, 可以使用 Redis 集群

搭建一主多从

在此演示一主二从,将三个redis服务端口区分即可

  1. 创建一个目录,并拷贝 redis.conf 到 该目录

  2. 对该目录下的 redis.conf 修改一下配置

  3. 创建3个文件,分别为端口6379,6380,6381的配置文件,并编辑如下

    1
    2
    3
    4
    include /myredis/redis.conf
    pidfile /var/run/redis_6379.pid
    port 6379
    dbfilename dump6379.rdb

    (其余两个同理)

  4. 启动三台服务器 连接三个redis服务,info replication查看主从复制信息

    此时三台redis都是Master

  5. 将 6380 和 6381 配置成 slaver, 6379 作为主机,在 slaver 执行如下命令
    指令说明: slaveof

  6. 测试,主机写数据,从机读数据,但从机不能写

  7. 至此,一个一主二从的主从复制就完成了

主从复制原理

示意图:

主从复制流程:

  1. Slave 启动成功连接到 master 后会发送一个 sync 命令
  2. Master 接到命令启动后台的存盘进程,同时收集所有接收到的用于修改数据集命令, 在后台进程执行完毕之后, master 将传送整个数据文件到 slave,以完成一次完全同步
  3. slave 服务在接收到数据库文件数据后,将其存盘并加载到内存中, 即 全量复制
  4. Master 数据变化了, 会将新的收集到的修改命令依次传给 slave, 完成同步, 即 增量复制
  5. 但是只要是重新连接 master,一次完全同步(全量复制)将被自动执行

【注意】:

  1. 如果从服务器宕机了, 重新启动, 仍然可以获取 Master 的最新数据
  2. 如果主服务器 宕机了, 从服务器并不会抢占为主服务器, 当主服务器恢复后, 从服务器仍然指向原来的主服务器.

薪火相传

示意图:

  1. 上一个 Slave 可以是下一个 slave 的 Master,Slave 同样可以接收其他 slaves 的连接和同步请求,那么该 slave 作为了链条中下一个的 master, 可以有效减轻 master 的写压力,去中心化降低风险
  2. 用 slaveof
  3. 风险是一旦某个 slave 宕机,后面的 slave 都没法同步
  4. 主机挂了,从机还是从机,无法写数据了

反客为主

  1. 在薪火相传的结构下, 当一个 master 宕机后, 指向 Master 的 slave 可以升为 master, 其后面的 slave 不用做任何修改
  2. 用 slaveof no one 将从机变为主机 (老韩说明: 后面可以使用哨兵模式, 自动完成切换.)

哨兵模式

示意图:

哨兵模式: 反客为主的自动版,能够后台监控主机是否故障,如果故障了根据投票数自动将从库转换为主库

演示:

  1. 创建 /myredis/sentinel.conf (指定的名字),并编辑如下

    sentinel monitor redis_master 127.0.0.1 6379 1

    说明 :sentinel monitor为关键字,redis_master为监控对象起的服务器名称,1 表示至少有多少个哨兵同意迁移到数量

  2. 启动哨兵 /usr/local/bin/redis-sentinel /myredis/sentinel.conf 哨兵端口为26379

  3. 将6379主机shutdown,哨兵显示将6379主机更换为6381

  4. 查看6381信息,可知6381现在为主机,且下面连着一个从机6380

  5. 将6379重启,哨兵将6379添加为6381的从机

细节说明:

在哨兵模式下,主机宕机之后的执行流程分析:

哨兵如何在从机中, 推选新的 Master 主机, 选择的条件依次为:

  1. 优先级在 redis.conf 中默认:replica-priority 100,值越小优先级越高
  2. 偏移量是指获得原主机数据的量, 数据量最全的优先级高
  3. 每个 redis 实例启动后都会随机生成一个 40 位的 runid, 值越小优先级越高

Redis集群

为什么需要集群?

生产环境的实际需求和问题:

  1. 容量不够,redis如何进行扩容?
  2. 并发写操作,redis如何分摊?
  3. 主从模式,薪火相传模式,主机宕机,会导致ip地址发生变化,应用程序中需要配置需要修改对应主机地址、端口等信息

传统解决方案:代理主机

  1. 客户端请求先到代理服务器
  2. 由代理服务器进行请求转发到对应的业务处理服务器
  3. 为了高可用性, 代理服务、A 服务、B 服务、C 服务都需要搭建主从结构(至少是一主一从),这样就需求搭建至少 8 台服务器
  4. 这种方案的缺点是: 成本高,维护困难, 如果是一主多从, 成本就会更高.

redis3.0提供无中心化集群解决方案

  1. 各个 Redis 服务仍然采用主从结构
  2. 各个 Redis 服务是连通的, 任何一台服务器, 都可以作为请求入口
  3. 各个 Redis 服务器因为是连通的, 可以进行请求转发
  4. 这种方式, 就无中心化集群配置, 可以看到,只需要 6 台服务器即可搞定
  5. 无中心化集群配置, 还会根据 key 值, 计算 slot , 把数据分散到不同的主机, 从而缓解单个主机的存取压力.[后面老师会演示和再说明]
  6. Redis 推荐使用无中心化集群配置
  7. 在实际生成环境 各个 Redis 服务器, 应当部署到不同的机器(防止机器宕机, 主从复制失效

集群介绍

  1. Redis 集群实现了对 Redis 的水平扩容, 即启动 N 个 redis 节点, 将整个数据库分布存储在这 N 个节点中, 每个节点存储总数据的 1/N。
  2. Redis 集群通过分区(partition) 来提供一定程度的可用性(availability): 即使集群中有一部分节点失效或者无法进行通讯, 集群也可以继续处理命令请求

Redis集群搭建

  1. 首先将rdb、aof文件删掉,打造一个干净的化境

  2. 修改配置文件,其他端口同理

    1
    2
    3
    4
    5
    6
    7
    include /myredis/redis.conf
    pidfile "/var/run/redis_6379.pid"
    port 6379
    dbfilename "dump6379.rdb"
    cluster-enabled yes //打开集群模式
    cluster-config-file nodes-6379.conf //设置节点配置文件名
    cluster-node-timeout 15000 //设置节点失联时间(毫秒),超过该时间,集群自动进行主从切换

  3. 启动6个redis服务,查看nodes节点文件是否生成

  4. 将六个节点合成一个集群

    1
    redis-cli --cluster create --cluster-replicas 1 192.168.211.130:6379 192.168.211.130:6380 192.168.211.130:6381 192.168.211.130:6389 192.168.211.130:6390 192.168.211.130:6391

    分析主从对应关系如下:

  5. 集群方式登录

    指令:redis-cli -c -p 6379

    指令:cluster nodes 查看集群信息,主从对应关系

【细节说明】:

  1. 一个集群至少要有三个主节点
  2. 选项 –cluster-replicas 1 表示我们希望为集群中的每个主节点创建一个从节点。
  3. 分配原则: 尽量保证主服务器和从服务器各自运行在不同的 IP 地址(机器), 防止机器故障导致主从机制失效, 高可用性得不到保障

Redis集群的使用

什么是slots?

  1. 集群启动成功,如下提示:

  2. 一个 Redis 集群包含 16384 个插槽(hash slot),编号从 0-16383, Reids 中的每个键都属于这 16384 个插槽的其中一个

  3. 集群使用公式 CRC16(key) % 16384 来计算键 key 属于哪个槽, 其中 CRC16(key) 语句用于计算键 key 的 CRC16 校验和

  4. 集群中的每个节点负责处理一部分插槽。 举个例子, 如果一个集群可以有主节点, 其中:
    -节点 A 负责处理 0 号至 5460 号插槽。
    -节点 B 负责处理 5461 号至 10922 号插槽。
    -节点 C 负责处理 10923 号至 16383 号插槽

在集群中录入值

  1. 在 redis 每次录入、查询键值,redis 都会计算出该 key 应该送往的插槽,如果不是该客户端对应服务器的插槽,redis 会告知应前往的 redis 实例地址和端口。

  2. redis-cli 客户端提供了 –c 参数实现自动重定向。

  3. 如 redis-cli -c –p 6379 登入后,再录入、查询键值对可以自动重定向

  4. k1经过计算,需要录入12706这个插槽,而这个插槽在6381主机,所以自动切换到6381

  5. 不在一个 slot 下的键值,是不能使用 mget,mset 等多键操作

    可以通过{}来定义组的概念,从而使 key 中{}内相同内容的键值对放到一个 slot 中去

查询集群中的值

  1. 指令: CLUSTER KEYSLOT <key> 返回 key 对应的 slot 值
  2. 指令: CLUSTER COUNTKEYSINSLOT <slot> 返回 slot 有多少个 key
  3. 指令: CLUSTER GETKEYSINSLOT <slot><count> 返回 count 个 slot 槽中的键

Redis集群故障恢复

  1. 如果主节点下线, 从节点会自动升为主节点(注意 15 秒超时, 再观察比较准确)

    将 6379 shutdown 6390自动升为主机

  2. 主节点恢复后,主节点回来变成从机

  3. 如果所有某一段插槽的主从节点都宕掉,Redis 服务是否还能继续, 要根据不同的配置而言:

    如果某一段插槽的主从都挂掉,而 cluster-require-full-coverage 为 yes ,那么 ,整个集群都挂掉
    如果某一段插槽的主从都挂掉,而 cluster-require-full-coverage 为 no , 那么, 只是该插槽数据不能使用,也无法存储
    redis.conf 中的参数 cluster-require-full-coverage

Redis集群的优缺点

优点
1、 实现扩容
2、分摊压力
3、无中心配置相对简单
缺点:
1、 多键操作是不被支持的
2、 多键的 Redis 事务是不被支持的。 lua 脚本不被支持
3、 由于集群方案出现较晚, 很多公司已经采用了其他的集群方案, 而其它方案想要迁移至 redis cluster, 需要整体迁移而不是逐步过渡, 复杂度较大

Redis应用问题及解决方案

缓存穿透

  1. 缓存穿透是指查询一个不存在的数据,由于缓存中肯定不存在,导致每次请求都直接访问数据库,增加数据库负载
  2. 攻击者可以通过构造不存在的key发起大量请求,对数据库造成很大的压力,可能会造成系统宕机。

缓存穿透的现象:

  1. 应用服务器压力变大
  2. redis命中率降低
  3. 一直查数据库

解决方案:

  1. 对空值缓存

    如果一个查询返回的数据为空,将这个空值也缓存起来,避免频繁查询数据库,设置空结果的过期时间应该短些,不超过五分钟。

  2. 设置可访问白名单

    定义一个可以访问的名单,每次访问和白名单的 id 进行比较,如果访问 id 不在白名单里面,进行拦截,不允许访问, 比如使用 bitmaps 实现 。

  3. 使用布隆过滤器

    使用布隆过滤器来快速判断一个请求的数据是否存在,如果布隆过滤器判断数据不存在,则直接返回,避免查询数据库。

  4. 实时监控

    当发现 Redis 的命中率开始急速降低, 需要排查访问对象和访问的数据, 和运维人员配合,可以设置黑名单限制服务 。

缓存击穿

  1. 缓存击穿是指某一热点数据缓存失效,使得大量请求直接打到数据库,增加数据库负载。
  2. 比如大家都在抢茅台,但某一时刻茅台的缓存失效了,大家都请求打到了数据库中,这就是缓存击穿。

缓存击穿现象:

  1. 数据库访问压力瞬时增加
  2. redis里面没有大量的key过期
  3. redis正常运行,但是数据库可能已经瘫痪了

解决方案:

  1. 设置热门数据

    在redis高峰访问之前,把一些热门数据提前存入redis里面,加大这些热门数据key的时长。

    或者不要给热门数据设置过期时间,在后台异步更新缓存

  2. 使用锁

    保证同一时间只有一个请求来构建缓存

缓存雪崩

  1. 缓存雪崩是指在某个时间点,大量缓存同时失效或被清空,导致大量请求直接打到数据库或后端系统,造成系统负载激增,甚至引发系统崩溃。
  2. 这通常是由于缓存中的大量数据在同一时间失效引起的。
  3. 比如一个电商系统,用户量很大,假设某一系列商品缓存突然同一时间失效,那就会造成缓存雪崩,导致服务全部打到数据库,引发严重后果

缓存雪崩的现象:

  1. 数据库访问压力变大,服务器崩溃
  2. 在极短时间内,访问大量key,而这些 key 集中过期

解决方案:

缓存键同时失效:

  1. 构建多级缓存架构

    引入多级缓存机制,如本地缓存加分布式缓存相结合,减少单点故障风险。

  2. 过期时间随机化

    设置缓存过期时间时,在原油过期时间上加一个随机值,比如1-5分钟随机,这样每个缓存过期时间重复率降低,避免同一时间大量缓存失效

  3. 缓存预热

    系统启动前提前加载缓存数据,避免大量请求落到冷启动状态下的数据库

  4. 加互斥锁

    使得没缓存或缓存失效的情况下,同一时间只有一个请求来构建缓存,防止数据库压力过大。

缓存中间件故障:

  1. 服务熔断:暂停业务的返回数据,直接返回错误
  2. 构建集群:构建多个redis集群保证其高可用

分布式锁

问题引入

  1. 单体单机部署的系统被演化成分布式集群系统后
  2. 由于分布式系统多线程、多进程并且分布在不同机器上,这将使原单机部署情况下的并发控制锁策略失效
  3. 单纯的 Java API 并不能提供分布式锁的能力
  4. 为了解决这个问题就需要一种跨 JVM 的互斥机制来控制共享资源的访问,这就是分布式锁要解决的问题

分布式锁主流实现方案

  1. 基于数据库实现分布式锁
  2. 基于缓存(Redis等) 性能好
  3. 基于Zookeeper 可靠性高

分布式锁的实现

  1. 指令:setnx key value

    - setnx 可以理解是上锁/加锁指令
    - key 是锁的键
    - value 是锁的值
    - 在这个 key 没有删除前, 不能执行相同 key 的上锁指令.

  2. 指令:del key

    删除key,这里可以理解为释放锁

  3. 指令:expire key seconds

    给锁key设置过期时间,防止死锁

  4. 指令:ttl key

    查看某个锁key的过期时间,-1代表永不过期 -2代表已过期

  5. 指令:set key value nx ex seconds

    设置锁的同时,指定该锁的过期时间

    这个指令是原子性的,防止 setnx key value / expire key seconds 两条指令, 中间执行被打断

Java简单实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@RequestMapping("testLock")
public void testLock() throws Exception {
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "ok");
if (lock){
Object value = redisTemplate.opsForValue().get("num");
if (StringUtils.isEmpty(value.toString())){
return;
}
int num = Integer.parseInt(value+"");
redisTemplate.opsForValue().set("num",++num);
redisTemplate.delete("lock");
}else{
try {
Thread.sleep(100);
testLock();
} catch (EOFException e) {
e.printStackTrace();
}
}

}

此时没有设置过期时间,容易造成死锁,所以需要加上过期时间

1
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "ok", 3, TimeUnit.SECONDS);

过期时间为三秒,但此时又产生了新的问题,如果业务操作出现了卡顿,导致锁超时,自动释放,此时其他用户将上锁进行业务操作,而当第一个用户业务操作结束后,要删除锁,此时删除的并不是自己上的锁,而是后面用户上的锁,就出现了误删锁问题,要解决该问题,可以采用UUID防误删锁

思路分析:

  1. 在获取锁的时候, 给锁设置的值是唯一的 uuid
  2. 在释放锁时,判断释放的锁是不是同一把锁.
  3. 造成这个问题的本质原因, 是因为删除操作缺乏原子性
1
2
3
4
5
6
7
8
9
//得到一个 uuid 值,作为锁的值
String uuid = UUID.randomUUID().toString();
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid, 3, TimeUnit.SECONDS);
..........
//为了防止误删锁, 进行判断
//判断当前这个锁是不是前面获取到的锁, 相同才进行删除/释放
if (uuid.equals((String) redisTemplate.opsForValue().get("lock"))) {
redisTemplate.delete("lock");
}

此时虽然能保证大部分不会误删锁,但是考虑到一个极端问题,如果当刚判断完当前锁是前面获取到的锁,此时正好该锁过期了,然后第二个用户已经又上锁了,此时第一个用户进行删除锁操作,就会把第二个用户的锁删除。要解决该问题就得用lua脚本保证删除原子性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//=====使用 lua 脚本来锁, 控制删除原子性========
// 定义 lua 脚本
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
// 使用 redis 执行 lua 执行
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
redisScript.setScriptText(script);
// 设置一下返回值类型 为 Long
// 因为删除判断的时候,返回的 0,给其封装为数据类型。如果不封装那么默认返回 String 类
型,
// 那么返回字符串与 0 会有发生错误。
redisScript.setResultType(Long.class);
// 第一个是 script 脚本 ,第二个需要判断的 key,第三个就是 key 所对应的值
// Arrays.asList("lock") 会 传 递 给 script 的 KEYS[1] , uuid 会 传 递 给ARGV[1]
redisTemplate.execute(redisScript, Arrays.asList("lock"), uuid);

注意事项

  1. 定义锁的 key, key 可以根据业务, 分别设置, 比如操作某商品, key 应该是为每个 sku 定义的, 也就是每个 sku 有一把锁
  2. 为了确保分布式锁可用,要确保锁的实现同时满足以下四个条件:
    • 互斥性。在任意时刻,只有一个客户端能持有锁。
    • 不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。
    • 加锁和解锁必须是同一个客户端,A 客户端不能把 B 客户端加的锁给解了
    • 加锁和解锁必须具有原子性

Redis新功能

ACL

基本介绍

  1. Redis ACL 是 Access Control List(访问控制列表) 的缩写, 该功能根据可以执行的命令和可以访问的键来限制某些连接

  2. 在 Redis 5 版本之前, Redis 安全规则只有密码控制 还有通过 rename 来调整高危命令比如 flushdb , KEYS* , shutdown 等

  3. Redis 6 则提供 ACL 的功能对用户进行更细粒度的权限控制 :

    接入权限:用户名和密码

    可以执行的命令

    可以操作的 KEY

常用命令

  1. acl list 展现用户权限列表
  2. acl cat 查看添加全新指令类别
  3. acl cat string 带上参数类型名,可以查看该类型可以执行的指令
  4. acl whoami 查看当前用户
  5. acl setuser 创建和编辑用户ACL

IO多线程

基本介绍

  1. Redis6 支撑多线程了,告别单线程了吗?
  2. IO 多线程指和客户端交互使用的是, 网络 IO 交互处理模块多线程,而非执行命令多线程
  3. Redis6 执行命令依然是单线程
  4. 也就是说, Redis 和客户端的交互是多线程, 在执行指令的时候, 仍然是单线程+IO 多路复用

原理架构
Redis 6 加入多线程, Redis 的多线程部分只是用来处理网络数据的读写和协议解析,执行命令仍然是单线程。之所以这么设计是不想因为多线程而变得复杂,需要去控制 key、lua、事务,LPUSH/LPOP 等等的并发问题。整体的设计大体如下:

另外,多线程 IO 默认也是不开启的,需要再配置文件 redis.conf 中配置
io-threads-do-reads yes
io-threads 4

工具支持 Cluster

基础介绍

  1. 之前老版 Redis 想要搭集群需要单独安装 ruby 环境
  2. Redis 5 将 redis-trib.rb 的功能集成到 redis-cli
  3. 另外官方 redis-benchmark 工具开始支持 cluster 模式了, 通过多线程的方式对多个分片进行压测

其它新功能-介绍

  1. RESP3 新的 Redis 通信协议: 优化服务端与客户端之间通信
  2. Client side caching 客户端缓存:基于 RESP3 协议实现的客户端缓存功能。为了进一步提升缓存的性能,将客户端经常访问的数据 cache 到客户端。减少 TCP 网络交互
  3. Proxy 集群代理模式:Proxy 功能,让 Cluster 拥有像单实例一样的接入方式,降低大家使用 cluster 的门槛。不过需要注意的是代理不改变 Cluster 的功能限制,不支持的命令还是不会支持,比如跨 slot 的多 Key 操作。
  4. Modules API:Redis 6 中模块 API 开发进展非常大,Redis 可以变成一个框架,利用 Modules 来构建不同系统,而不需要从头开始写。Redis 一开始就是一个面向编写各种系统开放的平台。