Redis-大厂程序员是怎么用的 | 豆包MarsCode AI 刷题

127 阅读7分钟

Redis 基本原理

为什么需要 Redis

  • 由于服务访问量的增加,数据从单表,演进出了分库分表。

  • MySQL 从单机演进出了集群

    • 数据量增长

    • 读写数据压力的不断增加

屏幕截图 2024-11-20 133618.png

于是一个比较自然的想法是:将热点数据缓存到内存中。

  • 数据分冷热

    • 热数据:经常被访问到的数据
  • 将热数据存储到内存中

如下图所示,在读场景下,首先从 Redis 中读取数据,如果没有,则再向 MySQL 中读取数据。在写场景下,向 MySQL 写入数据,同时 Redis 会监听 MySQL 数据修改情况,进行数据更新。

屏幕截图 2024-11-20 133838.png

Redis 基本工作原理

  • 数据从内存中读写

  • 支持持久化存储,数据保存到硬盘上防止重启数据丢失

    • 增量数据保存到 AOF 文件, AOF 文件记录了 Redis 执行的每条指令,而不是数据本身。

    • 全量数据 RDB 文件, RDB 文件以二进制流的方式记录了 Redis 中的所有数据。

    Redis 数据持久化的基本流程如下所示,每次读写内存时,都会向 AOF 文件追加写命令。当 Redis 服务重启时,首先会比较 AOF 文件和 RDB 文件,检查 RDB 文件中的数据是否完整。

屏幕截图 2024-11-20 140056.png

  • 单线程处理所有操作命令

    Redis 中所有命令都是单线程,串行执行的。

屏幕截图 2024-11-20 140220.png

Redis 应用案例

连续签到

用户每天可以进行一次签到,如果断签,连续签到计数将归0。可以通过 Incr 方法实现,该方法能够让指定 key 的 value 加一,并通过 ExpireAt 设置超时时间。

  • Key: cc_uid_1165894833

  • value: 252

  • expireAt: 后天的0点

sds 数据结构

根据 sds 指针定位数据位置,指针左移获取元信息:数据类型、预留空间大小、实际数据大小。指针右移获取数据 value.

  • 可以存储字符串、数字、二进制数据

  • 通常和 expire 配合使用

  • 场景:存储计数, Session

屏幕截图 2024-11-20 172605.png

消息通知

用 list 作为消息队列,例如当文章更新时,将更新后的文章推送到 ES, 用户就能搜索到最新的文章数据。

屏幕截图 2024-11-20 172726.png

Redis 中 List 数据结构使用 Quicklist, 它由一个双向链表和 listpack 实现。 listpack 包含了多个元素。

屏幕截图 2024-11-20 174635.png

屏幕截图 2024-11-20 174646.png

计数

在用户有多项计数需求的情况下,可以通过 hash 结构存储,例如:文章点赞数、万丈阅读数、关注数、关注者...

Redis 中是通过若干个槽位 + 链表实现 Hash 结构和解决 Hash 冲突的。并且当槽位的链表过长时,需要进行槽位的扩容,也就是 rehash. 在 Redis 中通过两个 hash table 实现 rehash, ht[0] 存储的是原数据,当需要进行 rehash 时,将 ht[0] 数据拷贝到 ht[1], 并交换指针完成槽位扩容。

  • rehash: 直接将 ht[0] 中的数据一次性迁移到 ht[1] 中。在数据量较小的情况下是较快可行的。但在大量数据的情况下,迁移过程将会明显阻塞用户请求。

  • 渐进式 rehash: 每次用户访问都会迁移少量数据。将整个迁移过程,平摊到所有的访问用户请求过程中。

屏幕截图 2024-11-20 191131.png

排行榜

排行榜功能需要记录每个用户的积分,并且当积分发生变化时,排名要实时变更。在 Redis 中可以通过 zset 数据结构实现。

zset 大致通过跳表实现,跳表由多层链表组成,上层链表元素与下层链表元素连接,上层链表元素是下层链表元素的一部分。由于链表的有序性,跳表便可以很好的支持二分查找。此外, zset 还结合一个字典,实现通过 key 即用户名查询对应的分数 score.

屏幕截图 2024-11-20 192617.png

屏幕截图 2024-11-20 192629.png

分布式锁

并发场景下,要求一次只能有一个协程执行。执行完成后,其它等待中的协程才能执行。在 Redis 中可以使用 setnx 实现,这里用了 Redis 的两个特性:

  • Redis 是单线程执行命令。

  • setnx 只有未设置过才能执行成功。

但 setnx 并不能保障高可用性。

  • 业务超时解锁,导致并发问题。业务执行时间超过锁超时时间

  • redis主备切换临界点问题。主备切换后,A持有的锁还未同步到新的主节点时,B可在新主节点获取锁,导致并发问题。

  • redis集群脑裂,导致出现多个主节点

Redis 使用注意事项

大 Key

大 key 指的是 redis 键值对中 value 很大的数据。

数据类型大 key 标准
Stringvalue的字节数大于10KB
Hash, Set, Zset, list元素个数大于5000个或者总value字节数大于10MB

大 key 的危害:

  • 读取成本高
  • 容易导致慢查询(过期、删除)
  • 主从复制异常,服务阻塞
  • 请求 Redis 超市报错。

消除大 Key 的方法

  • 拆分:将大 Key 拆分成多个小 Key, 例如一个 String 拆分成多个 String:

屏幕截图 2024-11-20 200342.png

  • 压缩:通过 gzip, snappy, lz4 等压缩算法,将 value 压缩后写入 Redis, 读取时解压后再使用。

热 Key

热 Key 指的是那些用户访问 QPS 特别高的键值对,导致 Server 实例出现 CPU 负载突增或者不均的情况。

热 Key 的解决方法:

  • 设置 Localcache: 在访问 Redis 前,在业务服务端设置 Localcache, 降低访问 redis 的 QPS. LocalCache 中缓存过期或者未命中,则从 Redis 中将数据更新到 LocalCache. 例如: Java Guava, Golang Bigcache.

  • 拆分:将 key:value 这一个热 key 复制写入多份,访问时,访问多个 key, 但 value 是同一个,以此将 QPS 分散到不同实例上。但这样涉及到多个实例数据同步的问题。

  • 使用 Redis 代理:本质上是结合了“热 key 发现”、“ LocalCache ” 两个功能。代理首先统计发现热 Key, 然后使用 Localcache 缓存热 Key.

屏幕截图 2024-11-20 202720.png

慢查询场景

容易导致 redis 慢查询的操作:

  • 批量操作一次性传入过多的 key/value, 如 mset/hmset/sadd/zadd 等 O(n) 操作。

  • zset 大部分命令都是 O(log(n)), 当大小超过 5k 以上时,简单的 zadd 也可能导致慢查询。

  • 操作的单个 value 过大。

  • 对于大 key 的 delete/expire 操作也可能导致慢查询。 Redis 4.0 之前不支持异步删除 unlink, 大 key 删除会阻塞 Redis.

缓存穿透、缓存雪崩

缓存穿透:热点数据查询绕过缓存,直接查询数据库。

缓存雪崩:大量缓存同时过期。

危害

  • 缓存穿透:通常不会缓存一个不存在的数据,这类查询请求都会直接访问数据库,如果有系统 bug 或人为攻击,那么容易导致数据库响应慢甚至宕机。

  • 缓存雪崩:在高并发场景下,一个热 Key 如果过期,会有大量请求同时击穿至数据库,容易影响数据库性能和稳定性。同一时间有大量 key 集中过期时,也会导致大量请求落到数据库上,导致查询变慢,甚至出现数据库无法响应的情况。

解决方法

  • 缓存穿透

    • 缓存空值:例如对于不存在的 userID, 这个 id 在缓存和数据库中都不存在,则可以缓存一个空值,下次再查询缓存直接返回空值。

    • 布隆过滤器:通过 bloom filter 算法来存储合法 key , 得益于该算法超高的压缩率,只需要占用极小的空间就能存储大量 key 值。

  • 缓存雪崩

    • 将缓存失效时间分散开,比如在原有的失效时间基础上增加一个随机值。

    • 使用缓存集群,避免单机宕机造成的缓存雪崩。