Redis概念+数据结构介绍+应用 | 青训营

84 阅读9分钟

Redis笔记:原理+代码

1 为什么要使用Redis?

  • 数据冷热分离,热数据存储于内存

2 Redis工作原理

2.1 基本特性

  1. 内存数据库:Redis的主要特点是将数据存储在内存中,这使得它能够快速地读写数据。它在内存中维护了一个数据集,并通过定期将数据持久化到磁盘来保证数据持久性。

  2. 单线程模型:Redis使用单线程模型来处理客户端请求。虽然它是单线程的,但是通过异步I/O、非阻塞操作和事件驱动的方式,能够在高并发情况下高效地处理请求。

  3. 数据结构多样性:Redis支持多种数据结构,如字符串、哈希表、列表、集合、有序集合等。每种数据结构都有其特定的操作,使得Redis在不同场景下都能高效地应对。

  4. 持久化:为了保证数据的持久性,Redis提供了两种持久化方式:RDB快照和AOF日志。RDB快照是将数据库的数据以快照的方式定期写入磁盘,而AOF日志是将写命令追加到日志文件中,以保证每次写操作的持久性。

  5. 发布订阅:Redis支持发布-订阅模式,允许客户端订阅一个或多个频道,当频道中有消息发布时,订阅者会收到相应的消息。

  6. 事务:Redis支持事务,客户端可以将多个命令包装成一个事务进行执行。事务在执行过程中会排队,一次性执行,保证了一系列操作的原子性。

  7. 复制:Redis支持主从复制模式,通过复制可以实现数据的备份、读写分离等功能。主节点将数据同步到从节点,从节点可以接受读取请求,从而减轻主节点的负载。

  8. 分片:Redis提供了分片功能,可以将数据分散存储在多个节点上,从而支持更大规模的数据存储和访问。

  9. 内存管理:由于Redis的数据存储在内存中,它需要有效地管理内存。Redis使用了多种策略来控制内存使用,如LRU(Least Recently Used,最近最少使用)、过期策略等。

Redis通过将数据存储在内存中、使用单线程模型、提供多样的数据结构和功能等特点,使得它成为一个高性能、多功能的缓存和数据存储解决方案。

2.2 数据结构

2.2.1 sds字符串

sds就是Redis中的字符串类型

sds指针指向头部和实际存储区的分界位置,指针左移获取元数据,右移则获取数据

image.png

2.2.2 Quicklist

Quicklist是list的底层数据结构。 image.png Quicklist和一般的链表没有太大区别,主要区别就是数据是用指针单独指向了listpack

image.png listpack支持变长存储,每个元素开头会存储4个字节的编码类型和data长度等元数据,而在末尾存储的是元素总长度:type+data,因此该数据结构同时支持正向遍历和反向遍历。

2.2.3 哈希

hash数据结构对应set命令。

image.png

Redis中的哈希表也是同一槽位以链表的形式存储冲突值,因此在哈希表的负载因子(即键值对数量与哈希槽位数量的比率)超过一定阈值,或者当哈希表需要缩小以节省内存时,Redis 就会触发 rehash 操作。在 rehash 过程中,Redis 会创建一个新的哈希表,并逐步将键值对从旧表迁移到新表。这允许 Redis 在不停机的情况下进行内存优化,例如当系统的内存需求减少时,可以通过收缩哈希表来释放内存。

2.2.4 跳表zskiplist

跳表zskiplist是zset的底层数据结构。 image.png 跳表的核心思想是通过多级索引来加速查找操作。每一级索引是一个链表,其中每个节点指向下一级索引中的节点。最底层的索引链表就是原始有序链表,而顶层的索引链表包含了所有元素,从而减少了查找的时间复杂度。通过跳过一些节点,跳表能够在 O(log n) 的平均时间复杂度内进行查找操作,因为每一级索引能够减少需要检查的节点数量。

PS:为什么Redis选用跳表而不是红黑树?

  1. 相对简单的实现:跳表的实现比红黑树要简单。红黑树是一种复杂的自平衡二叉查找树,需要更多的代码和维护,而跳表相对来说更易于理解和实现。

  2. 更好的平均性能:虽然红黑树在最坏情况下的时间复杂度比跳表要低(红黑树的最坏情况下为 O(log n),而跳表为 O(sqrt(n))),但是在实际应用中,平均性能更为重要。跳表在平均情况下的查找性能(O(log n))与红黑树相近,而插入和删除操作的性能可能在某些情况下略优于红黑树。

  3. 内存占用更低:跳表相对于红黑树来说,可能需要更少的指针和额外信息,从而在一些情况下占用更少的内存。这对于内存敏感的应用非常重要。

  4. 简单的动态更新:跳表在插入和删除操作后可以比较容易地进行动态调整,保持索引的平衡性。而红黑树的调整操作相对复杂一些。

  5. 局部性和缓存友好:跳表的节点是链表形式,而红黑树的节点在内存中可能更为分散。跳表的局部性更好,更适合缓存系统。

3 Redis的应用

  1. expire的应用:连续签到

需求:掘金每日连续签到:用户每日有一次签到的机会,如果断签,连续签到计数将归0。 连续签到的定义:每天必须在23:59:59前签到

只需要设置Redis key 的 expireAt 为0点即可简单地实现该功能。

  1. 消息通知

需求:例如当文章更新时,将更新后的文章推送到ES,用户就能搜索到最新的文章数据

典型的lsit作为消息队列的应用,应用了list消息队列:向列表中添加元素时,新元素会被添加到列表的末尾,而最早添加的元素则在列表的开头。

  1. 计数

需求:一个用户有多项计数需求

可通过hash结构存储

  1. 排行榜

需求:积分变化时,排名要实时变更

可以结合 zset + dict 实现。

  1. 限流

需求:要求1秒内放行的请求为N,超过N则禁止访问

方案:

使用 Redis 的计数器(Counter)和过期时间(TTL)功能。 对于每个用户或 IP 地址,维护一个计数器键,用来记录每秒的请求数。 每当有请求到达时,首先检查计数器的值。 如果计数器的值小于 N,则允许请求,并将计数器的值加 1。 如果计数器的值达到 N,则拒绝请求。 设置一个过期时间,比如 1 秒,让计数器键在每秒钟后自动过期,然后重置计数器的值为 0。

  1. 分布式锁

需求:并发场景,要求一次只能有一个协程执行。执行完成后,其它等待中的协程才能执行。

可以使用redis的setnx实现,利用了两个特性

  • Redis是单线程执行命令
  • setnx只有未设置过才能执行成功

Redis 官方提供了 一种基于 Redis 的分布式锁实现方式,称为 "Redlock" 算法:

  1. 选择多个 Redis 节点:选择 N 个 Redis 节点,分布在不同的物理服务器上,用作锁的存储。
  2. 获取锁:为了获取锁,客户端尝试在 N 个 Redis 节点上设置相同的锁键和唯一的锁值,同时设置过期时间。只有在大多数节点上成功设置了锁,才认为锁获取成功。
  3. 释放锁:释放锁时,客户端在所有节点上尝试删除锁,只有在大多数节点上成功删除了锁,才认为锁释放成功。

更多细节参考 Redlock\

4 Redis常见注意事项

4.1 大Key与热Key

类型定义危害注意事项
大Key存储在某个键对应的值非常大读取慢、易导致慢查询、易导致主从复制异常避免大Key
大Key拆分image.png
大Key压缩
区分冷热:如榜单列表场景,zset缓存前10页,后续数据走db
热Key用户访问一个Key的QPS特别高出现CPU负载突增或者不均的情况设置Localcache:在业务服务侧设置Localcacheimage.png
拆分:将key:value这一个热Key复制写入多份,例如key1:value,key2:value,访问的时候访问多个key,但value是同一个,以此将qps分散到不同实例上,降低负载。代价是,更新时需要更新多个key,存在数据短暂不一致的风险
使用代理:使用代理 程序,相当于缓存了热键,不存在的缓存才访问Redis,同时可以设置规则实现:热Key发现、LocalCache

4.2 慢查询

容易导致慢查询的操作:

  • 一次性传入过多的key/value
  • zset在大小超过5k以上时,简单的zadd/zrem也可能导致慢查询
  • 之前提到过的大Key查询
  • 大Key删除

4.3 缓存穿透与缓存雪崩

类型定义危害解决方案
缓存穿透热点数据查询绕过缓存,直接查询数据库查询一个一定不存在的数据:通常不会缓存不存在的数据,这类查询请求都会直接打到db,如果有系统bug或人为攻击,那么容易导致db响应慢甚至宕机缓存空值: 如一个不存在的userID。这个id在缓存和数据库中都不存在。则可以缓存一个空值,下次再查缓存直接反空值。
布隆过滤器: 通过bloom filter算法来存储合法Key,得益于该算法超高的压缩率,只需占用极小的空间就能存储大量key值
缓存雪崩缓存过期时:在高并发场景下,一个热key如果过期,会有大量请求同时击穿至db,容易影响db性能和稳定。同一时间有大量key集中过期时,也会导致大量请求落到db上,导致查询变慢,甚至出现db无法响应新的查询缓存失效时间分散:比如在原有的失效时间基础上增加一个随机值,例如不同Key过期时间,可以设置为 10分1秒过期,10分23秒过期,10分8秒过期。
使用缓存集群,避免单机宕机造成的缓存雪崩。