Skip to main content

Redis

分布式锁

保证加锁时设置过期时间的原子性

  • redis.setnx(key, value) 无法设置过期时间
redis.setnx(key, value);
redis.expire(key, 25);

上述操作无法保证原子性,可用下面的方案代替

redis.set(key, value, 25, "NX");

防止超时后释放锁时误删其他线程的锁

  • 假如线程 A 成功得到了锁,并且设置的超时时间是 25 秒。由于某些原因导致线程 A 执行的很慢很慢,过了 25 秒都没执行完,这时候锁过期自动释放,线程 B 得到了锁。随后,线程 A 执行完了任务,执行 del 指令来释放锁。但这时候线程 B 还没执行完,线程 A 实际上删除的是线程 B 加的锁。
  • 可以在加锁的时候把当前的线程 id 当做 value ,并在删除之前验证 key 对应的 value 是不是自己线程的 id

保证删除锁时的原子性

  • 防止超时后释放锁时误删其他线程的锁时,加入了判断逻辑,使得删除锁的操作变成了非原子操作。
  • 使用 Lua 脚本来保证删除锁操作的原子性
String luaScript = 'if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end';
redis.eval(luaScript , Collections.singletonList(key), Collections.singletonList(threadId));

跳表

  • 链表加多级索引的结构,就是跳表,是为了解决平衡二叉树复杂的问题,是平衡树的一种替代品,以一种较为简单的方式实现了平衡二叉树的功能。

skiplist 与平衡树、哈希表的比较

  • skiplist 和各种平衡树(如 AVL、红黑树等)的元素是有序排列的,而哈希表不是有序的,在哈希表上只能做单个 key 的查找,不适宜做范围查找。
  • 在做范围查找的时候,平衡树比 skiplist 操作要复杂。在平衡树上,找到指定范围的小值之后,还需要以中序遍历的顺序继续寻找其它不超过大值的节点。而在 skiplist 上进行范围查找就非常简单,只需要在找到小值之后,对第一层链表进行若干步的遍历就可以实现。
  • 平衡树的插入和删除操作可能引发子树的调整,逻辑复杂,而 skiplist 的插入和删除只需要修改相邻节点的指针,操作简单又快速。
  • 从内存占用上来说,skiplist 比平衡树更灵活一些。一般来说,平衡树每个节点包含 2 个指针(分别指向左右子树),而 skiplist 每个节点包含的指针数目平均为 1/(1-p),具体取决于参数 p 的大小。如果像 Redis 里的实现一样,取 p = 1/4,那么平均每个节点包含 1.33 个指针,比平衡树更有优势。
  • 查找单个 keyskiplist 和平衡树的时间复杂度都为 O(logn),大体相当,而哈希表在保持较低的哈希值冲突概率的前提下,查找时间复杂度接近 O(1),性能更高一些。
  • 从算法实现难度上来比较,skiplist 比平衡树要简单得多。

BigKey

  • 通常将含有较大数据或含有大量成员、列表数的 Key 称之为大 Key

常见衡量指标

  • 一个 STRING 类型的 Key,它的值为 5MB(数据过大)
  • 一个 LIST 类型的 Key,它的列表数量为 20000 个(列表数量过多)
  • 一个 ZSET 类型的 Key,它的成员数量为 10000 个(成员数量过多)
  • 一个 HASH 格式的 Key,它的成员数量虽然只有 1000 个但这些成员的 value 总大小为 100MB(成员体积过大)

带来的问题

  • Redis 实例变慢
  • Redis 实例内存不断变大引发 OOM,或达到 maxmemory 设置值引发写阻塞或重要 Key 被逐出
  • Redis Cluster 中的某个 node 内存远超其余 node,但因 Redis Cluster 的数据迁移最小粒度为 Key 而无法将 node 上的内存均衡化
  • Key 上的读请求使 Redis 实例占用服务器全部带宽,自身变慢的同时影响到该服务器上的其它服务
  • 删除一个大 Key 造成主库较长时间的阻塞并引发同步中断或主从切换

HotKey

  • 某个 Key 接收到的访问次数显著高于其它 Key 时,可以将其称之为热 Key

常见衡量指标

  • Redis 实例的每秒总访问量为 10000,而其中一个 Key 的每秒访问量达到了 7000(访问次数显著高于其它 Key
  • 对一个拥有上千个成员且总大小为 1MBHASH Key 每秒发送大量的 HGETALL(带宽占用显著高于其它 Key
  • 对一个拥有数万个成员的 ZSET Key 每秒发送大量的 ZRANGECPU 时间占用显著高于其它 Key

带来的问题

  • Key 占用大量的 Redis CPU 时间使其性能变差并影响其它请求
  • Redis Cluster 中各 node 流量不均衡造成 Redis Cluster 的分布式优势无法被利用,一个分片负载很高而其它分片十分空闲从而产生读/写热点问题
  • 在抢购、秒杀活动中,由于商品对应库存 Key 的请求量过大超出 Redis 处理能力造成超卖
  • Key 的请求压力数量超出 Redis 的承受能力造成缓存击穿,此时大量请求将直接指向后端存储将其打挂并影响到其它业务

Pipeline

  • Redis 客户端执行一条命令分为四个过程:发送命令 -> 命令排队 -> 命令执行 -> 返回结果,这整个过程称为 Round trip time(简称 RTT, 往返时间),批量命令(mgetmset)有效节约了 RTT,但大部分命令(如 hgetall)不支持批量操作,需要消耗 NRTT,这个时候需要 Pipeline 来解决这个问题。
  • 使用 Pipeline 组装的命令个数不宜太多,不然数据量过大,增加客户端的等待时间,还可能造成网络阻塞,可以将大量命令的拆分多个小的 Pipeline 命令完成。

原生批量命令(mset, mget)与 Pipeline 对比

  • 原生批量命令是原子的,Pipeline 是非原子的
  • 原生批量命令是一个命令对应多个 keyPipeline 支持多个命令
  • 原生批量命令是 Redis 服务端支持实现的,而 Pipeline 需要服务端和客户端的共同配合

hash 扩容

  • 采用的是渐进式 rehash 的方式,两个 dictht 表,rehashidx

渐进式 rehash

  • ht[1] 分配空间,让字典同时持有 ht[0]ht[1] 两个哈希表
  • rehashindex 的值设置为 0,表示 rehash 工作正式开始
  • rehash 期间,在对 hash 执行增删改查操作时,同时还会将 ht[0] 哈希表在 rehashindex 索引上的所有键值对 rehashht[1],当 rehash 工作完成以后,rehashindex 的值 +1
  • 随着字典操作的不断执行,最终会在某一时间段上 ht[0] 的所有键值对都会被 rehashht[1],这时将 rehashindex的值设置为 -1,表示 rehash 操作结束

集群

  • Redis 支持三种集群方案
    • 主从复制模式
    • Sentinel(哨兵)模式
    • Cluster 模式

主从复制模式

  • 主从复制模式中包含一个主实例(master)与一个或多个从实例(slave),客户端可对主数据库进行读写操作,对从数据库进行读操作,主数据库写入的数据会实时自动同步给从数据库。

工作机制

  1. slave 启动后,向 master发送 SYNC 命令,master 接收到 SYNC 命令后通过 bgsave 保存快照(即上文所介绍的 RDB 持久化),并使用缓冲区记录保存快照这段时间内执行的写命令
  2. master 将保存的快照文件发送给 slave,并继续记录执行的写命令
  3. slave 接收到快照文件后,加载快照文件,载入数据
  4. master 快照发送完后开始向 slave 发送缓冲区的写命令,slave 接收命令并执行,完成复制初始化
  5. 此后 master 每次执行一个写命令都会同步发送给 slave,保持 masterslave 之间数据的一致性

优点

  • master 能自动将数据同步到 slave,可以进行读写分离,分担 master 的读压力
  • masterslave 之间的同步是以非阻塞的方式进行的,同步期间,客户端仍然可以提交查询或更新请求

缺点

  • 不具备自动容错与恢复功能,masterslave 的宕机都可能导致客户端请求失败,需要等待机器重启或手动切换客户端 IP 才能恢复
  • master 宕机,如果宕机前数据没有同步完,则切换 IP 后会存在数据不一致的问题
  • 难以支持在线扩容,Redis 的容量受限于单机配置

Sentinel(哨兵)模式

  • 哨兵模式基于主从复制模式,只是引入了哨兵来监控与自动处理故障。

优点

  • 哨兵模式基于主从复制模式,所以主从复制模式有的优点,哨兵模式也有
  • 哨兵模式下,master 挂掉可以自动进行切换,系统可用性更高

缺点

  • 同样也继承了主从模式难以在线扩容的缺点,Redis 的容量受限于单机配置
  • 需要额外的资源来启动 sentinel 进程,实现相对复杂一点,同时 slave 节点作为备份节点不提供服务

Cluster 模式

  • Cluster 模式实现了 Redis 的分布式存储,即每台节点存储不同的内容,来解决在线扩容的问题。Cluster 采用无中心结构,它的特点如下:
    • 所有的 redis 节点彼此互联(PING-PONG 机制),内部使用二进制协议优化传输速度和带宽
    • 节点的 fail 是通过集群中超过半数的节点检测失效时才生效
    • 客户端与 redis 节点直连,不需要中间代理层,客户端不需要连接集群所有节点,连接集群中任何一个可用节点即可
  • Cluster 模式集群节点最小配置 6 个节点(33 从,因为需要半数以上),其中主节点提供读写操作,从节点作为备用节点,不提供请求,只作为故障转移使用。

工作机制

  1. Redis 的每个节点上,都有一个插槽(slot),取值范围为 0-16383
  2. 当存取 key 的时候,Redis 会根据 CRC16 的算法得出一个结果,然后把结果对 16384求余数,这样每个 key 都会对应一个编号在 0-16383 之间的哈希槽,通过这个值,去找到对应的插槽所对应的节点,然后直接自动跳转到这个对应的节点上进行存取操作
  3. 为了保证高可用,Cluster 模式也引入主从复制模式,一个主节点对应一个或者多个从节点,当主节点宕机的时候,就会启用从节点
  4. 当其它主节点 ping 一个主节点 A 时,如果半数以上的主节点与 A 通信超时,那么认为主节点 A 宕机了。如果主节点 A 和它的从节点都宕机了,那么该集群就无法再提供服务了

Redis 事件模型

  • Redis 服务器是一个事件驱动程序,主要处理文件事件(file event)和时间事件(time event

文件事件

  • Redis 服务器通过套接字与客户端进行连接,而文件事件就是对套接字操作的抽象,可以将其理解为 IO事件
  • RedisIO事件 放入一个就绪队列中(redisServer.aeEventLoop.fired 数组),然后在 aeProcessEvents 会依次分派给文件事件处理器处理
  • Redis 中文件事件包括:客户端的连接、命令请求、数据回复、连接断开等,当上述事件发生时,会使得相应的描述符可读可写,再调用相应类型的文件事件处理器。
  • Redis 文件事件处理器有:连接应答处理器(networking.c/acceptTcpHandler)、命令请求处理器(networking.c/readQueryFromClinet)、命令回复处理器(networking.c/sendReplyToClient

时间事件

  • 时间事件包含定时事件周期性事件,Redis 将其放入一个单向无序链表中,每当时间事件执行器运行时,就遍历链表,查找已经到达的时间事件,调用相应的处理器。
    • 定时事件:让一段程序在指定的时间之后执行一次,比如让程序X在当前时间 30ms 之后执行一次
    • 周期事件:让一段程序每隔指定时间就执行一次,比如让程序 Y 每隔 30ms 就执行一次

文件事件的处理

  • 事件分派器(Initiation Dispatcher
  • IO多路复用(Synchronous Event Demultiplexer
  • 事件循环调度

Redis key 删除策略

  • Redis 是一种内存型数据库,数据都是存放在内存中的,内存中的数据可以通过 TTL 指令获取其状态
    • XX:具有时效性的数据
    • -1:永久有效的数据
    • -2:已经过期的数据 / 被删除的数据 / 未定义的数据

删除 key 的方式

  • 被动删除:当读/写一个已经过期的 key 时,会触发惰性删除策略,直接删除掉这个过期 key
  • 主动删除:由于惰性删除策略无法保证冷数据被及时删掉,所以 Redis 会定期主动淘汰一批已过期的 key
  • 当前已用内存超过 maxmemory 限定时,触发主动清理策略

定时删除

  • 创建一个定时器,当 key 设置有过期时间,且过期时间到达时,由定时器任务立即执行对键的删除操作
  • 优点:节约内存,到时就删除,快速释放掉不必要的内存占用
  • 缺点:CPU 压力很大,无论 CPU 此时负载量多高,均占用 CPU,会影响 redis 服务器响应时间和指令吞吐量
  • 总结:用处理器性能换取存储空间(拿时间换空间)

惰性删除

  • 数据到达过期时间,不做处理,等下次访问该数据时,如果未过期,返回数据,反之则删除并返回不存在
  • 优点:节约 CPU 性能,发现必须删除的时候才删除
  • 缺点:内存压力很大,出现长期占用内存的数据
  • 总结:用存储空间换取处理器性能(拿空间换时间)

定期删除

  • 周期性轮询 redis 库中的时效性数据,采用随机抽取的策略,利用过期数据占比的方式控制删除频度
  • 特点1:CPU 性能占用设置有峰值,检测频度可自定义设置
  • 特点2:内存压力不是很大,长期占用内存的冷数据会被持续清理
  • 总结:周期性抽查存储空间(随机抽查,重点抽查)

逐出算法

  • Redis 清理数据的策略称为逐出算法

内存淘汰机制

  • 如果 Redis 服务器打开了 maxmemory 选项,并且服务器占用的内存数超过了 maxmemory 选项所设置的上限值时,会进行内存淘汰,常见的淘汰策略如下:
    • volatile-lru:从已设置过期时间的数据集中挑选最近最少使用的数据淘汰
    • volatile-ttl:从已设置过期时间的数据集中挑选将要过期的数据淘汰
    • volatile-random:从已设置过期时间的数据集中任意选择数据淘汰
    • volatile-lfu:从已设置过期时间的数据集挑选使用频率最低的数据淘汰
    • allkeys-lru:从数据集(server.db[i].dict)中挑选最近最少使用的数据淘汰
    • allkeys-lfu:从数据集(server.db[i].dict)中挑选使用频率最低的数据淘汰
    • allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰
    • no-enviction:禁止驱逐数据,这也是默认策略。当内存不足以容纳新入数据时,新写入操作就会报错,请求可以继续进行。采用 no-enviction 策略可以保证数据不被丢失。

AOF vs. RDB

RDB

  • RDBRedis DataBase)是 Redis 默认的持久化方案。在指定的时间间隔内,执行指定次数的写操作,则会将内存中的数据写入到磁盘中,即在指定目录下生成一个 dump.rdb 文件。Redis 重启后会通过加载 dump.rdb 文件恢复数据。save(阻塞) / bgsave(异步) / flushall / flushall 命令也会触发生成快照。
  • 优点
    • 适合大规模的数据恢复
    • 如果业务对数据完整性和一致性要求不高,RDB 是很好的选择
  • 缺点
    • 数据的完整性和一致性不高,因为可能在备份时宕机
    • 备份时占用内存,因为在备份时会独立创建一个子进程,将数据写入到一个临时文件,最后再将临时文件替换之前的备份文件

AOF

  • 默认不开启。是为了弥补RDB的不足(数据的不一致性),采用日志的形式来记录每个写操作,并追加到文件中。Redis 重启后会根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作。根据配置文件触发,可以是每次执行触发,可以是每秒触发,可以是不同步。
  • 重写机制
    • AOF 文件的大小超过所设定的阈值时,Redis 就会对 AOF 文件的内容压缩。
    • Redisfork 出一个新进程,读取内存中的数据,并重新写到一个临时文件中,最后替换旧的 AOF 文件。
    • AOF 文件大小是上次 rewrite 后大小的一倍且文件大于 64M 时触发。这里的 一倍64M 可以通过配置文件修改。
  • 优点
    • 数据的完整性和一致性更高
  • 缺点
    • 因为 AOF 记录的内容多,文件会越来越大,数据恢复也会越来越慢

缓冲区

  • 缓冲区的功能是用一块内存来暂存命令数据,避免出现因为数据和命令的处理速度慢于发送速度而导致的数据丢失和性能问题。但缓冲区的内存空间有限,如果发生溢出,就会丢失数据。

客户端输入缓冲区(请求)

  • 会先把客户端发送过来的命令暂存起来,Redis 主线程再从输入缓冲区中读取命令,进行处理
  • 溢出场景(会导致关闭客户端连接)
    • 请求写入大量数据(BigKey
    • 生产速度大于消费速度,慢慢被填满
  • 优化(缓冲区大小无法调整,服务端默认为每个客户端输入缓冲区分配的大小是 1GB
    • 拆分请求数据,避免写入 BigKey
    • 定位处理服务端阻塞问题

客户端输出缓冲区(响应)

  • 会先把客户端发送过来的命令的响应暂存起来
  • 溢出场景(会导致关闭客户端连接)
    • 服务器端返回了大量数据
    • 返回数据的速度太快,比如执行 MONITOR 命令,它会持续输出监测到的各个命令操作
    • 缓冲区大小设置得不合理
  • 优化
    • 避免 BigKey 操作返回大量数据,避免在线上持续执行 MONITOR 命令
    • 使用 client-output-buffer-limit 设置合理的缓冲区大小上限,或是缓冲区连续写入时间和写入量上限

复制缓冲区

  • 暂存全量同步数据
  • 溢出场景(会导致关闭同步连接)
    • 从节点接收和加载 RDB 较慢,同时主节点接收到了大量的写命令,写命令在复制缓冲区中就会越积越多
  • 优化
    • 控制主节点保存的数据量大小,通常控制在 2 ~ 4GB
    • 通过 client-output-buffer-limit 配置项来设置合理的复制缓冲区大小
    • 复制缓冲区是在主节点上的,如果集群中的从节点非常多,主节点的内存开销就会非常大,必须得控制和主节点连接的从节点个数

复制积压缓冲区

  • 暂存增量同步数据
  • 溢出场景(会导致增量同步失败转为全量同步)
    • 是一个大小有限的环形缓冲区,当写入速度比读取速度要快,会导入新写入的命令数据覆盖缓冲区中的旧命令数据
  • 优化
    • 通过 repl_backlog_size 配置项来调整复制积压缓冲区的大小

Redis 事务

https://www.cnblogs.com/yidengjiagou/p/17373712.html