Redis

redis集群中间件 redis消息中间件_数据库

谈谈你对 Redis 的理解

  • redis 是一种基于内存存储的 NoSQL 开源数据库,它提供了五种基本的数据类型:String、List、Hash、Set、Zset。
  • 因为 Redis 基于内存存储,并且在数据结构上进行了大量的优化,所有它的 IO 性能比较好,因此,在实际开发中,我们会把它作为数据库和应用之间的缓存中间件。
  • 并且因为它是非关系型数据库,所以不存在表结构之间的关联,这样能够很好的提升应用程序的数据 IO 效率。
  • 在企业级开发中,它又提供了主从复制和哨兵,以及集群的方式去使用。在 Redis 集群中,它提供了 Hash 槽的方式去实现数据的分片,进一步提升了性能和可拓展性。

缓存

  1. 本地缓存:缓存在内存中。比如 LRUMap
  • 优点:访问快
  • 缺点:内存空间有限,缓存内容少
  1. 分布式缓存
  • 优点:解决了内存空间有限的问题
  • 缺点:远程交互耗时
  1. 多级缓存:本地只保存需要高频率访问的热点数据,其他数据保存在分布式缓存中。

缓存淘汰机制

  • 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集群中间件 redis消息中间件_redis集群中间件_02

redis集群中间件 redis消息中间件_java_03

IO 多路复用

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

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

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

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

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

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

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

Redis 数据持久化

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 子进程压缩并重写文件,然后把新文件替换掉原来的文件。

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; // 字符数组 }
  • 避免缓冲区溢出: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:

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

  • quicklist 是 ziplist 和 linkedlist 的组合
  • ziplist 是内存中一段连续的存储区域,元素用 entry 表示。
  • linkedlist 是一个双向链表,节点保存了 pre、ziplist、next

redis集群中间件 redis消息中间件_java_04

redis集群中间件 redis消息中间件_Redis_05

Hash

  • 常用指令:
  • HSET key field value
  • HGET key field
  • HDEL key field
  • HEXISTS key field
  • HKEYS key
  • 使用场景:保存一个对象,一个对象有多个属性。
  • 我项目中用来保存用户分布地域情况
  • 底层原理:
  • 当 Hash 中键值对少于512对,且每个键值对大小不超过64字节时,用 ziplist 保存
    ziplist:
  • 内存中连续的存储空间
  • 元素用entry表示

zlbytes: ziplist长度

zltail:ziplist尾部偏移量

zllen:entry个数

entry:元素

zlend:0xFF,标识结尾

  • 否则用 HashTable 保存。
    HashTable:
  • 一个 dictht 指向一个数组
  • 每个数组上的键值对用链表形式保存

redis集群中间件 redis消息中间件_Redis_06

  • 扩容
  • 基于原 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 来存储
  • 否则,存储类似 hashtable,只是 value 赋 null

Zset

  • 常用指令:
  • ZADD key score member
  • ZCARD key:获取成员数
  • ZCOUNT key min max: 获取指定分数区间(min到max)的成员数量
  • 使用场景:
  • 做排行榜
  • 按照浏览量
  • 按照视频播放量
  • 按照点赞量
  • 微博热搜榜,名称+热力值
  • 底层原理:
  • 当 zset 长度小于128,且每个元素大小小于64k,用 ziplist
  • 否则,用跳跃表

跳表:和红黑树一样,插入、删除、搜索的时间复杂度都为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
  1. 查看玩家分数:ZSCORE key member
  • ZSCORE lb user1 —> 89
  1. 按名次查看排行榜: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
  • 查询前三的玩家
  1. 查看玩家的排名:ZREVRANK key member
  2. 增减玩家分数:ZINCRBY key incrScore member
  3. 移除玩家:ZREM key member
  4. 删除排行榜:DEL key

相同分数问题:

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

高级数据类型

Bitmap

  • 按bit位存储。
  • 二进制数字位的 0、1 数组。数组下标用 offset 维护。
  • 可以用来保存状态信息。

HyperLogLog

  • 基数统计。
  • 用于去重(合并的时候,相同的数据只计算一次。)

Geospatial

提供地理位置信息。

其他

pub/sub

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

redis集群中间件 redis消息中间件_redis_07

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

redis集群中间件 redis消息中间件_java_08

  • 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集群中间件 redis消息中间件_redis集群中间件_09

  • 加锁机制:
  • 线程去获取锁,获取成功:执行 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

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


优点:

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

缺点:

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

2. 不能删除数据。

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

缓存击穿

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

解决办法:

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

缓存雪崩

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

解决办法:

  1. 给热点数据设置成永不过期。
  2. 可以考虑给缓存的时间设置波动过期值,避免大量数据一起过期。
  3. 也可以考虑使用双缓存模式,A缓存中热点数据永不过期,B缓存中热点数据可以过期,当数据过期后,去A缓冲中读取数据。

主从复制

一主多从

redis集群中间件 redis消息中间件_redis集群中间件_10

特点

  • 写读分离:主服务器写,从服务器读,从服务器不能写
  • 从服务器挂掉
  • 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集群中间件 redis消息中间件_redis_11

  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
  1. 建立 socket 连接:开启主从复制后,master 和 slave 之间建立 socket 连接。
  2. 检查连接:slave 向 master 发送一个 ping 请求,来检查两者的连接状态。
  • 如果 slave 收到 pong,证明连接正常。
  • 如果 slave 没有收到 pong,或者收到错误信息,则 m 和 s 断开socket连接,并重连。

redis集群中间件 redis消息中间件_redis_12

  1. 身份验证:如果 master 和 slave 都没有设置密码或者密码相同,则可以进行同步。否则,断开socket,并且重连。
  2. 同步:连接正常并且完成身份验证后,slave 向 master 发送 psync 命令,然后 master 和 slave 之间同步数据。slave 把数据库状态同步到和 master 相同。
  • 全量复制
  • 部分复制
  1. 命令传播:同步完成后,master 进行的新操作都要传播给 slave。
  • 延迟传播:master 积攒多个 tcp 包后,再发送给 slave,通常是 40ms 发一次。提高了性能,损失了一致性。
  • 立即传播:master 每进行一次操作,立即把新操作发送给 slave。保证了一致性,降低了性能。

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

全量复制和部分复制

全量复制

redis集群中间件 redis消息中间件_redis_13

  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集群中间件 redis消息中间件_redis_14

部分复制过程

redis集群中间件 redis消息中间件_redis集群中间件_15

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

哨兵模式

redis集群中间件 redis消息中间件_redis_16

  • 哨兵每一秒都会向 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
  1. 数据集中过期。
  • 数据集中过期,会导致 redis 大量执行删除过期 key 的操作,影响速度。
  1. 内存不够了,所以每次写入内存时,redis 要先进行内存的清理。
  2. 在 Redis 上执行 INFO 命令,查看 latest_fork_usec 项,单位微秒:这是进行持久化操作时,fork 一个子进程的花费时间。
  3. AOF 的刷盘机制选取问题:
  • appendfsync always:主线程每次执行写操作后立即刷盘,此方案会占用比较大的磁盘 IO 资源,但数据安全性最高
  • appendfsync no:主线程每次写操作只写内存就返回,内存数据什么时候刷到磁盘,交由操作系统决定,此方案对性能影响最小,但数据安全性也最低,Redis 宕机时丢失的数据取决于操作系统刷盘时机
  • appendfsync everysec:主线程每次写操作只写内存就返回,然后由后台线程每隔 1 秒执行一次刷盘操作(触发fsync系统调用),此方案对性能影响相对较小,但当 Redis 宕机时会丢失 1 秒的数据
  1. 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集群中间件 redis消息中间件_Redis_17

哨兵模式

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

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集群中间件 redis消息中间件_redis_18

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

用虚拟节点改进

redis集群中间件 redis消息中间件_数据库_19

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 删除即可。