Redis | 非常重要的中间件

Redis

Redis | 非常重要的中间件_第1张图片

谈谈你对 Redis 的理解

  • redis 是一种基于内存存储的 NoSQL 开源数据库,它提供了五种基本的数据类型:String、List、Hash、Set、Zset。

  • 因为 Redis 基于内存存储,并且在数据结构上进行了大量的优化,所有它的 IO 性能比较好,因此,在实际开发中,我们会把它作为数据库和应用之间的缓存中间件。

  • 并且因为它是非关系型数据库,所以不存在表结构之间的关联,这样能够很好的提升应用程序的数据 IO 效率。

  • 在企业级开发中,它又提供了主从复制和哨兵,以及集群的方式去使用。在 Redis 集群中,它提供了 Hash 槽的方式去实现数据的分片,进一步提升了性能和可拓展性。

缓存

  1. 本地缓存:缓存在内存中。比如 LRUMap

    • 优点:访问快

    • 缺点:内存空间有限,缓存内容少

  2. 分布式缓存

    • 优点:解决了内存空间有限的问题

    • 缺点:远程交互耗时

  3. 多级缓存:本地只保存需要高频率访问的热点数据,其他数据保存在分布式缓存中。

缓存淘汰机制

  • FIFO: First-In-First-Out,删除最先缓存的。

  • LRU: Least-Recently-Used,最近最少使用。删除掉最近不太可能访问到的数据。如果缓存中有些数据最近都没有被访问过,那就认为它之后被访问的概率低。

    • 用 LinkedHashMap 实现

    • 如果自己写节点的话,

      LinkedNode

      • int key;

      • int value;

      • LinkedNode pre;

      • LinkedNode next;

  • LFU: Least-Frequency-Used:最不经常使用。删除掉最近访问频率很低的数据。如果缓存中有数据最近被访问的次数很少,那就认为它以后被访问的概率也低。

Redis 为什么快?

  1. Redis 是基于内存的,而内存的读取速度要远大于磁盘的读取速度。

  2. Redis 是单线程模型,所以没有线程切换和上下文切换的开销。

  3. Redis 采用了 IO 多路复用模型,可以处理并发连接。

Redis 内存管理

Redis 是如何判断数据是否过期的呢?

用一个过期时间字典来维护数据的过期时间。

Redis 的过期删除策略

过期删除策略

  1. 惰性删除:只有取出 key 的时候,才判断是否需要过期删除。对 CPU 友好,对内存不友好。

  2. 定期删除:定期取出一部分 key 进行过期删除。对内存友好,对 CPU 不友好。

Redis 采用的过期删除策略

  • 定期删除 + 惰性删除 + 内存淘汰机制

Redis 内存淘汰机制了解么?

相关问题:MySQL 里有 2000w 数据,Redis 中只存 20w 的数据,如何保证 Redis 中的数据都是热点数据?

Redis 提供 6 种数据淘汰策略:

  1. volatile-lru(least recently used):从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰

  2. volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰

  3. volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰

  4. allkeys-lru(least recently used):当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的 key(这个是最常用的)

  5. allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰

  6. no-eviction:禁止驱逐数据,也就是说当内存不足以容纳新写入数据时,新写入操作会报错。这个应该没人使用吧!

4.0 版本后增加以下两种:

  1. volatile-lfu(least frequently used):从已设置过期时间的数据集(server.db[i].expires)中挑选最不经常使用的数据淘汰

  2. allkeys-lfu(least frequently used):当内存不足以容纳新写入数据时,在键空间中,移除最不经常使用的 key

Redis 内存模型

Redis 单线程模型

基于 Reactor 模型开发的单线程模型, 称为文件事件处理器。它采用 IO 多路复用来监听多个套接字。

Redis | 非常重要的中间件_第2张图片

Redis | 非常重要的中间件_第3张图片

IO 多路复用

  • IO 多路复用就是一个 IO 模型,他可以实现一个线程监听多个文件句柄,只要有一个文件句柄就绪,就可以通知应用程序进行相应的读写操作。如果没有文件句柄就绪,那么就会阻塞应用程序,交出 CPU。

    文件句柄:操作系统对于打开的文件的唯一识别依据。

如何监听大量的客户端连接?

虽然文件事件处理器是单线程的,但是使用IO 多路复用来监听多个套接字。

BIO:同步阻塞。一个线程处理一个请求(连接),一个线程在处理一个连接时,其他的请求会被阻塞。

NIO:同步非阻塞。指 IO 多路复用,一个线程处理多个请求(连接)。

AIO:异步非阻塞。用户态访问内核态,内核态会立即返回一个标记,待处理完所有事件后,发送信号返回给用户进程。

Redis 数据持久化

《对线面试官》 Redis 持久化 (qq.com)

RDB — 数据快照

  • 是一个经过压缩后的二进制文件,默认开启。用于:

    • 数据备份。

    • 复制到从服务器进行主从复制。

  • redis 会启动定时任务,定时去 fork 子进程来进行 RDB 备份。

(1) 同步方式

  • bgsave 时,Redis 会 fork 一个子进程进行持久化,然后先将数据写入一个临时文件中,等数据全部写完后,再把临时文件替换掉上次持久化好的文件。

Fork

  • Fork 是指复制一个和当前进程一样的子进程。新进程所有的数据(变量,环境变量,程序计数器等)和原来进程一样。一般来说,父进程和子进程共用一段物理内存。

  • Fork 的时候,会阻塞主线程。会带来什么问题?

    • redis 是单线程的,fork 时,主线程被阻塞,可能会丢失数据,所以可以用一个缓存区来保存 redis 主线程阻塞时接受的数据。

写时复制技术

属于Linux的一种技术,即数据同步之前先创建临时文件,将数据保存进临时文件,等同步完成后,再将临时文件替换掉同步的数据内容。

  • 为什么要写时复制技术?

    • 如果不采用临时复制技术,在同步过程中,发生程序中断,那么同步的数据就不完整。

(2) 同步指令

  • save:立即同步,同时阻塞 redis,当同步完成后,再恢复 redis

  • bgsave:fork 出一个子线程来进行 RDB 同步。

AOF - 只追加文件

Append only file,默认关闭。可以在 redis.conf 文件中,通过 appendonly yes 打开。

  • AOF 以日志形式保存 Redis 每个操作(不保存读操作)。

  • 只追加文件,不修改文件

  • 通过 AOF 缓冲区,而不是 fork 子进程来实现追加数据,但是文件太大时重写是通过 fork 进程实现的。

(1) 同步方式

  • 客户端的请求写命令会被 append 到 AOF 缓冲区

  • AOF 缓冲区会根据持久层策略(always, everysec, no)将写操作追加到磁盘的 AOF 文件末尾。这里只追加文件,不修改文件。

  • 在 Redis 服务启动时,会读取这个 AOF 文件,并且执行上面的写操作。

  • 当文件大小大于 server.aof_rewrite_min_size,并且当前 AOF 文件大小和上一次重写时 AOF 文件大小差的比例达到 auto-aof-rewrite-percentage 时,则重写文件。

(2) AOF 策略

  1. alwaysappendfsync always,只要有写变化,就写入 appendonly.aof 日志

  2. everysecappendfsync everysec,每秒同步一次数据到磁盘。(推荐)

  3. noappendfsync no,让操作系统决定何时将数据写入磁盘。

重写

  • 当持久化的文件过大时,Redis 会 fork 子进程压缩并重写文件,然后把新文件替换掉原来的文件。

    Redis | 非常重要的中间件_第4张图片

RDB和AOF同时开启,Redis选择哪一个?

  • Redis默认选取AOF数据

Redis 数据类型

基本数据类型

String

  • 常用指令:

    • SET key value

    • GET key

    • INCR key:key 保存的值+1

    • DECR key:key 保存的值-1

  • 使用场景:

    • 做用户的 Session 管理。

    • 博客访问量情况

  • 底层原理:

    • SDS(Simple Dynamic String)

      struct sdshdr{
          int len; // 字符串长度
          int free; // 未使用字符数组长度
          char[] buf; // 字符数组
      }

      Redis | 非常重要的中间件_第5张图片

    • 避免缓冲区溢出:String 进行修改时,Redis会检查剩余空间是否满足要求,不满足的话会扩展到足够的空间来保存 String。

    • 空间预分配:String 扩容时,Redis会给予足够多的内存空间。

    • 惰性释放:字符串需要缩短时,所需空间不会直接缩短,而是用 free 保存剩余空间,以便再次分配。

    • String 长度不能超过 512m

List

有序列表

  • 常用指令

    • LPUSH

    • RPUSH

    • LPOP

    • RPOP

    • LRANGE key start stop

  • 使用场景:可以用来实现分页查询,或者读取用户评论数

  • 底层原理:

    3.0以前:用ziplist + linkedlist保存

    • 当列表长度小于512,且所有元素都小于64字节时,用ziplist保存

    • 否则,用LinkedList保存

      LinkedList保存了头节点head,尾节点tail,列表长度

      LinkedList:

      Redis | 非常重要的中间件_第6张图片

      3.0以后:用quicklist保存。这样可以节省双向列表保存pre和next指针的空间。

      • quicklist 是 ziplist 和 linkedlist 的组合

        • ziplist 是内存中一段连续的存储区域,元素用 entry 表示。

        • linkedlist 是一个双向链表,节点保存了 pre、ziplist、next

        Redis | 非常重要的中间件_第7张图片

        Redis | 非常重要的中间件_第8张图片

Hash

  • 常用指令:

    • HSET key field value

    • HGET key field

    • HDEL key field

    • HEXISTS key field

    • HKEYS key

  • 使用场景:保存一个对象,一个对象有多个属性。

    • 我项目中用来保存用户分布地域情况

  • 底层原理:

    • 当 Hash 中键值对少于512对,且每个键值对大小不超过64字节时,用 ziplist 保存

      ziplist:

      • 内存中连续的存储空间

      • 元素用entry表示

        Redis | 非常重要的中间件_第9张图片

        zlbytes: ziplist长度

        zltail:ziplist尾部偏移量

        zllen:entry个数

        entry:元素

        zlend:0xFF,标识结尾

    • 否则用 HashTable 保存。

      HashTable:

      • 一个 dictht 指向一个数组

      • 每个数组上的键值对用链表形式保存

      Redis | 非常重要的中间件_第10张图片

  • 扩容

    • 基于原 Hash 表的2倍创建一个新的哈希表,然后把旧哈希表的内容转移到新哈希表里。

    • 渐进式 rehash:

      • 旧哈希表的内容不会一下子转移到新哈希表,而是渐进式转移过去,否则一下子转移过去,可能会导致服务器在一段时间内停止服务。

        1. 用一个rehashidx字段来记录,rehashidx为0表示开始迁移。

        2. 在rehash期间,每次对字典执行增删改查操作是,程序除了执行指定的操作以外,还会顺带将 ht[0] 哈希表在rehashindex 索引上的所有键值对 rehash 到 新哈希表,当 rehash 工作完成以后,rehashindex 的值 +1

        3. 当所有数据都从旧哈希表里转移过去后,释放旧哈希表的空间。rehashidx置为-1。

  • 收缩

    • 类似于扩容,但是收缩成原哈希表1/2倍的空间。

  • 负载因子

    Redis中,loader_factor哈希表中键值对数量 / 哈希表长度

    HashMap中, loader_factor哈希 table 已用的数量 / 哈希表长度

    • 当redis没有执行持久化操作(BGSAVE or BGREWAITEAOF),负载因子是1

    • 当redis执行持久化操作(BGSAVE or BGREWAITEAOF),负载因子是5

Hash 底层和 Java HashMap 的区别?

  1. 渐进式 hash

  2. 负载因子不同

Set

  • 常用指令:

    • SADD:添加

    • SCARD:获取集合成员数

    • SDIFF key1 [key2]:差集

    • SINTER key1 [key2]:交集

  • 使用场景:Set可以做并集、交集、差集。可以用来查看好友的共同列表。

    • 我在项目中用于文章点赞的信息

      • articleListKey 中保存了每个用户点过赞的文章(set 结构,articleListKey, articleId

      • ARTICLE_LIKE_COUNT 保存了每个文章的点赞量(hash 结构,ARTICLE_LIKE_COUNT, articleId,赞数

          @Transactional(rollbackFor = Exception.class)
          @Override
          public void saveArticleLike(Integer articleId) {
              // 判断是否点赞
              String articleLikeKey = ARTICLE_USER_LIKE + UserUtils.getLoginUser().getUserInfoId();
              // 已经点过赞了, 删除文章id
              if (redisService.sIsMember(articleLikeKey, articleId)) {
                  // 取消点赞
                  redisService.sRemove(articleLikeKey, articleId);
                  // 文章点赞数-1
                  redisService.hDecr(ARTICLE_LIKE_COUNT, articleId.toString(), 1L);
              } else {
                  // 未点赞则增加文章id
                  redisService.sAdd(articleLikeKey, articleId);
                  // 文章点赞量+1
                  redisService.hIncr(ARTICLE_LIKE_COUNT, articleId.toString(), 1L);
              }
          }

  • 底层原理:

    • set元素少于512个,用 intset 来存储

      Redis | 非常重要的中间件_第11张图片

    • 否则,存储类似 hashtable,只是 value 赋 null

Zset

  • 常用指令:

    • ZADD key score member

    • ZCARD key:获取成员数

    • ZCOUNT key min max: 获取指定分数区间(min到max)的成员数量

  • 使用场景:

    • 做排行榜

      • 按照浏览量

      • 按照视频播放量

      • 按照点赞量

    • 微博热搜榜,名称+热力值

  • 底层原理:

    • 当 zset 长度小于128,且每个元素大小小于64k,用 ziplist

    • 否则,用跳跃表

      Redis | 非常重要的中间件_第12张图片

      跳表:和红黑树一样,插入、删除、搜索的时间复杂度都为O(logN)

(1) 查询

  • 查询:时间复杂度是 跳表高度 h * 遍历的元素数量

  • 那么跳表的每层都是上一层拿一半出来当索引,所以 L_1 = n,L_2 = n/2,L_3 = n/2^2,...,L_h = n/2^h

    最后一层节点只有2个,于是 L_h = 2 = n/2^h,h=log_2n-1

  • 又每层节点遍历次数不超过3,所以时间复杂度都为 O(3*(log_2N-1))=O(log_2n)

(2) 插入( 跳表何时增加高度?)

  • ZSET 底层维护了一 randomLevel() 函数,该方法有 1/2 的概率返回 1、1/4 的概率返回 2、1/8的概率返回 3,以此类推

    • randomLevel() 方法返回 1 表示当前插入的该元素不需要建索引,只需要存储数据到原始链表即可(概率 1/2)

    • randomLevel() 方法返回 2 表示当前插入的该元素需要建一级索引(概率 1/4)

    • randomLevel() 方法返回 3 表示当前插入的该元素需要建二级索引(概率 1/8)

    • randomLevel() 方法返回 4 表示当前插入的该元素需要建三级索引(概率 1/16)

    • 并且,建立二级索引的时候,同时也会建立一级索引....以此类推

怎么保证每层索引节点个数都是前一层索引的一半呢?

  • 凡是建索引,无论几级索引必然有一级索引,所以一级索引中元素个数占原始数据个数的比率为 randomLevel() 方法返回值 > 1 的概率。那 randomLevel() 方法返回值 > 1 的概率是多少呢?因为 randomLevel() 方法随机生成 1~MAX_LEVEL 的数字,且 randomLevel() 方法返回值 1 的概率为 1/2,则 randomLevel() 方法返回值 > 1 的概率为 1 - 1/2 = 1/2。即通过上述流程实现了一级索引中元素个数占原始数据个数的 1/2

  • 同理,当 randomLevel() 方法返回值 > 2 时,会建立二级或二级以上索引,都会在二级索引中增加元素,因此二级索引中元素个数占原始数据的比率为 randomLevel() 方法返回值 > 2 的概率。 randomLevel() 方法返回值 > 2 的概率为 1 减去 randomLevel() = 1 或 =2 的概率,即 1 - 1/2 - 1/4 = 1/4。OK,达到了我们设计的目标:二级索引中元素个数占原始数据的 1/4

(3) 删除

  • 跳表删除数据时,要把索引中对应节点也要删掉

(4) 使用 ZSET 实现游戏排行榜

设 lb 是排行榜名,user 有 user1,user2

  1. 设置玩家分数:ZADD key score member

    • ZADD lb 89 user1

    • ZADD lb 91 user2

    • ZADD lb 70 user3

    • ZADD lb 98 user4

  2. 查看玩家分数:ZSCORE key member

    • ZSCORE lb user1 —> 89

  3. 按名次查看排行榜:ZREVRANGE key start end [WITHSCORES]

    • ZREVRANGE lb 0 -1 WITHSCORES

      • -1 表示整个排行榜

      (1) user4
      (2) 98 
      (3) user2
      (4) 91
      (5) user1
      (6) 89
      (7) user3
      (8) 70
    • ZREVRANGE lb 0 2 WITHSCORES

      • 查询前三的玩家

  4. 查看玩家的排名:ZREVRANK key member

  5. 增减玩家分数:ZINCRBY key incrScore member

  6. 移除玩家:ZREM key member

  7. 删除排行榜:DEL key

相同分数问题:

  • 如果两个分数相同的用户想按照加入 zset 的先后顺序排序,可以考虑用时间戳

高级数据类型

Bitmap

  • 按bit位存储。

  • 二进制数字位的 0、1 数组。数组下标用 offset 维护。

  • 可以用来保存状态信息。

    • 比如做用户画像,用 bitmap 来做标签:Bitmap的巧用 - 无敌小阿没 - 博客园 (cnblogs.com)

HyperLogLog

  • 基数统计。

  • 用于去重(合并的时候,相同的数据只计算一次。),可以用于统计登录的人数:hyperLoglog的使用 - 简书 (jianshu.com)

Geospatial

提供地理位置信息。

其他

pub/sub

发布/订阅通信模式。可以用作消息队列(但性能不如 RabbitMQ、Kafka)

Redis | 非常重要的中间件_第13张图片

  • client1,client2,client5都订阅了channel1。

Redis | 非常重要的中间件_第14张图片

  • channel1广播message,订阅了它的client都能收到。

Pipeline

可以批量执行一系列操作,再一起返回结果。避免频繁请求响应。

Lua

redis可以使用Lua解释器来执行脚本。

Redis 事务

事务操作

  1. multi 开启事务

  2. 执行一系列操作,每个操作都会进入队列。

  3. exec 按顺序执行事务

事务特性

  • Redis 的事务不支持回滚

    • 入队前的错误会使事务失败

    • 入队后,如果发生错误,只有发生错误的事务会失败,其他事务仍然成功

      • 可以利用 Lua 脚本来批量执行多条 Redis 命令,这些 Redis 命令会被提交到 Redis 服务器一次性执行完成,大幅减小了网络开销。

分布式锁

基于 Redis 分布式锁

setnx(lock, uuid, 30, TimeUnit.second)

基于 Redisson 看门狗的分布式锁

前面说了,如果某些原因导致持有锁的线程在锁过期时间内,还没执行完任务,而锁因为还没超时被自动释放了,那么就会导致多个线程同时持有锁的现象出现,而为了解决这个问题,可以进行“锁续期”。其实,在 JAVA 的 Redisson 包中有一个"看门狗"机制,已经帮我们实现了这个功能。

  • redisson原理:

    • 在获取锁之后,redisson 会维护一个看门狗线程,当锁即将过期还没有释放时,会不断的延长锁 key 的生存时间(默认 30 秒)

    • redisson 分布式锁是一个可重入排他锁。

Redis | 非常重要的中间件_第15张图片

  • 加锁机制:

    • 线程去获取锁,获取成功:执行 lua 脚本,保存数据到 redis 数据库。

  • 线程去获取锁,获取失败:一直通过 while 循环尝试获取锁,获取成功后,执行 lua 脚本,保存数据到 redis 数据库。

  • watch dog 自动延期机制:

    • 看门狗启动后,对整体性能也会有一定影响,默认情况下看门狗线程是不启动的。如果使用 redisson 进行加锁的同时设置了锁的过期时间,也会导致看门狗机制失效。

    • redisson 在获取锁之后,会维护一个看门狗线程,在每一个锁设置的过期时间的 1/3 处(默认 10 秒),如果线程还没执行完任务,则不断延长锁的有效期。看门狗的检查锁超时时间默认是 30 秒,可以通过 lockWactchdogTimeout 参数来改变。

    • 加锁的时间默认是30秒,如果加锁的业务没有执行完,那么每隔 30 ÷ 3 = 10秒,就会进行一次续期,把锁重置成30秒,保证解锁前锁不会自动失效。

    • 那万一业务的机器宕机了呢?如果宕机了,那看门狗线程就执行不了了,就续不了期,那自然30秒之后锁就解开了。

  • redisson 分布式锁的关键点:

    • 对 key 不设置过期时间,由 Redisson 在加锁成功后给维护一个 watchdog 看门狗,watchdog 负责定时监听并处理,在锁没有被释放且快要过期的时候自动对锁进行续期,保证解锁前锁不会自动失效

    • 通过 Lua 脚本实现了加锁和解锁的原子操作

    • 通过记录获取锁的客户端 id,每次加锁时判断是否是当前客户端已经获得锁,实现了可重入锁

  • Redisson的使用:

    • 在方案三中,我们已经演示了基于Redisson的RedLock的使用案例,其实 Redisson 也封装可重入锁(Reentrant Lock)、公平锁(Fair Lock)、联锁(MultiLock)、红锁(RedLock)、读写锁(ReadWriteLock)、 信号量(Semaphore)、可过期性信号量(PermitExpirableSemaphore)、 闭锁(CountDownLatch)等,具体使用说明可以参考官方文档:Redisson的分布式锁和同步器

分布式缓存的问题

缓存穿透

当缓存和数据库里都没有一个数据时,用户恶意请求该数据,导致 redis 频繁请求数据库,给数据库造成极大的压力。比如,产品id不可能为-1,但用户频繁请求产品id为 -1 的数据,导致缓存穿透问题。

解决办法:

  1. 业务层增加filter接口,对请求进行合法性检查,过滤不合法的请求,比如请求 id 为 -1的数据。

  2. 缓存中设置一个 null 值,并设置较小的过期时间。当请求没有的数据时,就返回空。当数据库存入该数据后,及时更新缓存。

  3. 布隆过滤器

    • bitmaps

      Redis | 非常重要的中间件_第16张图片

    1. 对于一个数key,通过多个hash函数,算出多个值,每个值都在布隆过滤器对应的位置上置1。
    2. 当进来一个数,通过多个hash计算,去找对应位置上的值,如果该位置上有0,则该数一定不存在
   - 如果位置上都是1,该数==不一定==存在。

优点:

1. 能说明一个数一定不存在

缺点:

1. 不能说明一个数一定存在

2. 不能删除数据。

3. 数据量大的时候,会出现误判

缓存击穿

数据库中有数据,缓存中的该数据过期了。大量用户请求该数据,导致redis频繁读取数据库,给数据库造成极大的压力,甚至崩溃。

解决办法:

  1. 给热点数据设置成永不过期。

  2. 对于缓存中没有的数据,如果收到大量请求,则进行加锁,只放一条请求进来,然后去读取数据库,并加载到内存中。接着,再放开这个锁。其他请求需要等待解锁,并且要设置一个阈值,如果等待时间过长,则直接返回空。

缓存雪崩

数据库里有数据,而缓存中正好有大量的数据过期。这时,大量用户请求这些数据,导致 redis 频繁读取数据库,给数据库造成极大的压力,甚至奔溃。

解决办法:

  1. 给热点数据设置成永不过期。

  2. 可以考虑给缓存的时间设置波动过期值,避免大量数据一起过期。

  3. 也可以考虑使用双缓存模式,A缓存中热点数据永不过期,B缓存中热点数据可以过期,当数据过期后,去A缓冲中读取数据。

主从复制

一主多从

Redis | 非常重要的中间件_第17张图片

特点

  • 写读分离:主服务器写,从服务器读,从服务器不能写

  • 从服务器挂掉

    • master服务器下的一个 slave1 服务器 s1 挂掉,s1 重启后,会自动变成一个 master 服务器。

      把挂掉的服务器重新赋成 master 服务器的 slave,slave 仍然能读到 master 中的数据。

  • 主服务器挂掉

    • master 服务器挂掉后,它的 slave 服务器仍然是它的 slave 服务器。

      master 重启后,仍然是原来的 master。

主从复制原理

CAP原理:

  • C - Consistent ,一致性

  • A - Availability ,可用性

  • P - Partition tolerance ,分区容忍性 分布式系统的节点往往都是分布在不同的机器上进行网络隔离开的,这意味着必然会有网络断开的风险,这个网络断开的场景的专业词汇叫着「网络分区」。

在网络分区发生时,两个分布式节点之间无法进行通信,我们对一个节点进行的修改操作将无法同步到另外一个节点,所以数据的「一致性」将无法满足,因为两个分布式节点的数据不再保持一致。除非我们牺牲「可用性」,也就是暂停分布式节点服务,在网络分区发生时,不再提供修改数据的功能,直到网络状况完全恢复正常再继续对外提供服务。

一句话概括 CAP 原理就是——网络分区发生时,一致性和可用性两难全。

  • Redis 保证「最终一致性」,从节点会努力追赶主节点,最终从节点的状态会和主节点的状态将保持一致。如果网络断开了,主从节点的数据将会出现大量不一致,一旦网络恢复,从节点会采用多种策略努力追赶上落后的数据,继续尽力保持和主节点一致。

主从复制过程

Redis | 非常重要的中间件_第18张图片

  1. slave 开启主从复制:设置主服务器 master 的 ip 和 port。主从复制开启全是由从服务器 slave 发起。

    可以通过 slaveof 命令来设置从服务器的主服务器。

    有三种实现方式:

    1. conf文件中写入:slaveof master_ip master _port

    2. redis-server --slaveof master_ip master_port

    3. redis-cli后,输入slaveof master_ip master _port

  2. 建立 socket 连接:开启主从复制后,master 和 slave 之间建立 socket 连接。

  3. 检查连接:slave 向 master 发送一个 ping 请求,来检查两者的连接状态。

    • 如果 slave 收到 pong,证明连接正常。

    • 如果 slave 没有收到 pong,或者收到错误信息,则 m 和 s 断开socket连接,并重连。

    Redis | 非常重要的中间件_第19张图片

  4. 身份验证:如果 master 和 slave 都没有设置密码或者密码相同,则可以进行同步。否则,断开socket,并且重连。

  5. 同步:连接正常并且完成身份验证后,slave 向 master 发送 psync 命令,然后 master 和 slave 之间同步数据。slave 把数据库状态同步到和 master 相同。

    • 全量复制

    • 部分复制

  6. 命令传播:同步完成后,master 进行的新操作都要传播给 slave。

    • 延迟传播:master 积攒多个 tcp 包后,再发送给 slave,通常是 40ms 发一次。提高了性能,损失了一致性。

    • 立即传播:master 每进行一次操作,立即把新操作发送给 slave。保证了一致性,降低了性能。

    以上通过 master 配置中的 repl-disable-tcp-nodelay 来设置。yes为延迟传播,no为立即传播。

全量复制和部分复制

全量复制

Redis | 非常重要的中间件_第20张图片

  1. slave 给 master 发送 sync 命令,请求全量复制

  2. master 将执行 bgsave,fork 一个子进程来写 RDB 文件,并且使用一个缓存,缓存放入写完 RDB 之后的新操作。

  3. master 给 slave 发送 RDB 文件,slave 收到以后,删除旧数据,执行 RDB,与 master 进行同步。

  4. master 给 slave 发送缓存区的写操作,slave 执行并保持和 master 同步的状态。

  5. 如果 slave 开启了 AOF,则会执行 bgrewriteaof,更新AOF至最新的状态。

部分复制

Redis 2.8 以后,引入部分复制。slave 给 master 发送 psync 命令。然后决定是使用全量复制还是部分复制。

offset + runId

  • offset

    • master 和 slave 都维护了一个 offset 字段,保存的是现在数据的偏移量。如果 slave 的 offset 和 master 的 offset 一致,则不需要进行数据复制。如果 slave 的 offset 和 master 的 offset 不一致,则需要进行数据同步。

  • runid

    • master 和 slave 会各自维护一个runid,来决定是进行全量复制还是部分复制。slave 断线重连后,会将自己的 runid 和master 的 runid 进行比较,如果两者的 runid 一致,说明现在的 master 是 slave 断线重连前的 master,则可以进行部分复制。如果 runid 不一致,说明 slave 断线之前连接的 master 和现在的 master 不是一个,那应该进行全量复制。

  • 复制挤压缓冲区

    • master 维护了一个复制积压缓冲区,这个复制积压缓冲区是一个先进先出的队列。master 在进行命令传播的时候,会把新的写操作写入这个复制挤压缓冲区。旧的写操作会在对头弹出。当 slave 要求进行部分复制的时候,如果需要复制的内容存在于复制积压缓冲区,那么 master 会进行部分复制,如果要复制的内容已经不在复制积压缓冲区,或者已经不完整,那么 master 会进行全量复制。

    Redis | 非常重要的中间件_第21张图片

部分复制过程

Redis | 非常重要的中间件_第22张图片

如果 master 返回 -err,说明 master 版本是2.8之前,无法识别 psync 命令。

哨兵模式

Redis | 非常重要的中间件_第23张图片

  • 哨兵每一秒都会向 master、slaves、其他哨兵发送 ping 命令。

  • 如果节点回复时间超过设置的时间,则会被标记为主观下线。

    如果 master 被标记为主观下线。那么其他的哨兵都会给 master 发送 ping。

  • 如果多数哨兵都认为 master 主观下线,那么 master 会被标记为客观下线

  • 哨兵就要重新选举新的 master。新的 master 产生后,所有 slave 的配置文件进行修改,都变成新 master 的slave。

Redis 调优

定位 redis 变慢的原因

# 创建连接,可以用PING-PONG来检查连接是否OK
redis-cli -h localhost -p 6379
# 监控Redis的连接和读写操作
redis-cli -h localhost -p 6379 monitor
# Redis服务器的统计信息
redis-cli -h localhost -p 6379 info
# 查找 bigkey
redis-cli -h localhost -p 6379 --bigkeys
​
# 阅读 redis 提供的 slowlog(慢日志),慢日志会记录运行较慢的指令。 

可能变慢的原因

  1. 使用了时间复杂度较高的命令,比如 SORT 之类的。

  2. redis 返回数据时,有网络问题。

  3. value 太大导致的 ”bigkey“ 问题,这样 redis 会花费很多时间在分配内存和删除内存上。

    • redis-cli -h localhost -p 6379 --bigkeys 来查找 bigkey

  4. 数据集中过期。

    • 数据集中过期,会导致 redis 大量执行删除过期 key 的操作,影响速度。

  5. 内存不够了,所以每次写入内存时,redis 要先进行内存的清理。

  6. 在 Redis 上执行 INFO 命令,查看 latest_fork_usec 项,单位微秒:这是进行持久化操作时,fork 一个子进程的花费时间。

  7. AOF 的刷盘机制选取问题:

    • appendfsync always:主线程每次执行写操作后立即刷盘,此方案会占用比较大的磁盘 IO 资源,但数据安全性最高

    • appendfsync no:主线程每次写操作只写内存就返回,内存数据什么时候刷到磁盘,交由操作系统决定,此方案对性能影响最小,但数据安全性也最低,Redis 宕机时丢失的数据取决于操作系统刷盘时机

    • appendfsync everysec:主线程每次写操作只写内存就返回,然后由后台线程每隔 1 秒执行一次刷盘操作(触发fsync系统调用),此方案对性能影响相对较小,但当 Redis 宕机时会丢失 1 秒的数据

  8. redis 内存碎片太多。执行 INFO 命令,查看内存情况。

调优

  1. 避免使用太多时间复杂度较高的命令。有些操作可以在客户端完成后,再写入 redis。

  2. 避免 "bigkey"。

  3. 给 key 设置随机过期时间。

  4. 开启 lazy-free(惰性删除/延迟释放)。lazy-free 特性是 Redis 4.0 开始引入的,指的是让 Redis 采用异步方式延迟释放 key 使用的内存,将该操作交给单独的子线程处理,避免阻塞主线程。

  5. 合理配置数据持久化策略。

  6. 降低主从库全量同步的概率。

  7. 选择合适的 AOF 的刷盘机制:一般来说,appendfsync everysec 比较合适。

  8. 适当地进行 redis 内存碎片整理。(内存整理也会影响性能,要评估后进行)

  9. 优化网络情况。

Hash 槽

  • Redis Cluster 包含了 16384 个哈希槽,每个 Key 通过计算后都会落在具体一个槽位上,而这个槽位是属于哪个存储节点的,则由用户自己定义分配。例如机器硬盘小的,可以分配少一点槽位,硬盘大的可以分配多一点。如果节点硬盘都差不多则可以平均分配。所以哈希槽这种概念很好地解决了一致性哈希的弊端。

Redis 集群部署

主从模式

Redis | 非常重要的中间件_第24张图片

哨兵模式

主节点只有一个,主节点写,从节点读

cluster 模式

  • redis cluster是Redis的分布式解决方案,在3.0版本推出后有效地解决了redis分布式方面的需求

  • 自动将数据进行分片,每个master上放一部分数据

  • 提供内置的高可用支持,部分master不可用时,还是可以继续工作的

数据分布算法

hash算法

比如你有 N 个 redis实例,那么如何将一个key映射到redis上呢,你很可能会采用类似下面的通用方法计算 key的 hash 值,然后均匀的映射到到 N 个 redis上:

hash(key)%N

如果增加一个redis,映射公式变成了 hash(key)%(N+1)

如果一个redis宕机了,映射公式变成了 hash(key)%(N-1)

在这两种情况下,几乎所有的缓存都失效了。会导致数据库访问的压力陡增,严重情况,还可能导致数据库宕机。

一致性hash算法

Redis | 非常重要的中间件_第25张图片

一个master宕机不会导致大部分缓存失效,可能存在缓存热点问题

用虚拟节点改进

Redis | 非常重要的中间件_第26张图片

redis cluster 的 hash slot 算法

redis cluster 有固定的 16384 个 hash slot,对每个 key 计算 CRC16 值,然后对 16384 取模,可以获取 key 对应的 hash slot

redis cluster 中每个master都会持有部分 slot,比如有 3 个 master,那么可能每个 master 持有 5000 多个 hash slot

hash slot 让 node 的增加和移除很简单,增加一个 master,就将其他 master 的hash slot 移动部分过去,减少一个 master,就将它的 hash slot 移动到其他master 上去

移动hash slot的成本是非常低的

客户端的api,可以对指定的数据,让他们走同一个hash slot,通过hash tag来实现

127.0.0.1:7000>CLUSTER ADDSLOTS 0 1 2 3 4 ... 5000 可以将槽0-5000指派给节点7000负责。

每个节点都会记录哪些槽指派给了自己,哪些槽指派给了其他节点。

客户端向节点发送键命令,节点要计算这个键属于哪个槽。

如果是自己负责这个槽,那么直接执行命令,如果不是,向客户端返回一个MOVED错误,指引客户端转向正确的节点。

Redis 和数据库一致性问题

Cache Aside Pattern(旁路缓存模式)

Cache Aside Pattern 中遇到写请求是这样的:更新 DB,然后直接删除 cache 。

如果更新数据库成功,而删除缓存这一步失败的情况的话,简单说两个解决方案:

  1. 缓存失效时间变短(不推荐,治标不治本) :我们让缓存数据的过期时间变短,这样的话缓存就会从数据库中加载数据。另外,这种解决办法对于先操作缓存后操作数据库的场景不适用。

  2. 增加 cache 更新重试机制(常用): 如果 cache 服务当前不可用导致缓存删除失败的话,我们就隔一段时间进行重试,重试次数可以自己定。如果多次重试还是失败的话,我们可以把当前更新失败的 key 存入队列中,等缓存服务可用之后,再将缓存中对应的 key 删除即可。

你可能感兴趣的:(redis,redis,java,数据库)