Redis 基本原理
为什么需要 Redis
-
由于服务访问量的增加,数据从单表,演进出了分库分表。
-
MySQL 从单机演进出了集群
-
数据量增长
-
读写数据压力的不断增加
-
于是一个比较自然的想法是:将热点数据缓存到内存中。
-
数据分冷热
- 热数据:经常被访问到的数据
-
将热数据存储到内存中
如下图所示,在读场景下,首先从 Redis 中读取数据,如果没有,则再向 MySQL 中读取数据。在写场景下,向 MySQL 写入数据,同时 Redis 会监听 MySQL 数据修改情况,进行数据更新。
Redis 基本工作原理
-
数据从内存中读写
-
支持持久化存储,数据保存到硬盘上防止重启数据丢失
-
增量数据保存到 AOF 文件, AOF 文件记录了 Redis 执行的每条指令,而不是数据本身。
-
全量数据 RDB 文件, RDB 文件以二进制流的方式记录了 Redis 中的所有数据。
Redis 数据持久化的基本流程如下所示,每次读写内存时,都会向 AOF 文件追加写命令。当 Redis 服务重启时,首先会比较 AOF 文件和 RDB 文件,检查 RDB 文件中的数据是否完整。
-
-
单线程处理所有操作命令
Redis 中所有命令都是单线程,串行执行的。
Redis 应用案例
连续签到
用户每天可以进行一次签到,如果断签,连续签到计数将归0。可以通过 Incr 方法实现,该方法能够让指定 key 的 value 加一,并通过 ExpireAt 设置超时时间。
-
Key: cc_uid_1165894833
-
value: 252
-
expireAt: 后天的0点
sds 数据结构
根据 sds 指针定位数据位置,指针左移获取元信息:数据类型、预留空间大小、实际数据大小。指针右移获取数据 value.
-
可以存储字符串、数字、二进制数据
-
通常和 expire 配合使用
-
场景:存储计数, Session
消息通知
用 list 作为消息队列,例如当文章更新时,将更新后的文章推送到 ES, 用户就能搜索到最新的文章数据。
Redis 中 List 数据结构使用 Quicklist, 它由一个双向链表和 listpack 实现。 listpack 包含了多个元素。
计数
在用户有多项计数需求的情况下,可以通过 hash 结构存储,例如:文章点赞数、万丈阅读数、关注数、关注者...
Redis 中是通过若干个槽位 + 链表实现 Hash 结构和解决 Hash 冲突的。并且当槽位的链表过长时,需要进行槽位的扩容,也就是 rehash. 在 Redis 中通过两个 hash table 实现 rehash, ht[0] 存储的是原数据,当需要进行 rehash 时,将 ht[0] 数据拷贝到 ht[1], 并交换指针完成槽位扩容。
-
rehash: 直接将 ht[0] 中的数据一次性迁移到 ht[1] 中。在数据量较小的情况下是较快可行的。但在大量数据的情况下,迁移过程将会明显阻塞用户请求。
-
渐进式 rehash: 每次用户访问都会迁移少量数据。将整个迁移过程,平摊到所有的访问用户请求过程中。
排行榜
排行榜功能需要记录每个用户的积分,并且当积分发生变化时,排名要实时变更。在 Redis 中可以通过 zset 数据结构实现。
zset 大致通过跳表实现,跳表由多层链表组成,上层链表元素与下层链表元素连接,上层链表元素是下层链表元素的一部分。由于链表的有序性,跳表便可以很好的支持二分查找。此外, zset 还结合一个字典,实现通过 key 即用户名查询对应的分数 score.
分布式锁
并发场景下,要求一次只能有一个协程执行。执行完成后,其它等待中的协程才能执行。在 Redis 中可以使用 setnx 实现,这里用了 Redis 的两个特性:
-
Redis 是单线程执行命令。
-
setnx 只有未设置过才能执行成功。
但 setnx 并不能保障高可用性。
-
业务超时解锁,导致并发问题。业务执行时间超过锁超时时间
-
redis主备切换临界点问题。主备切换后,A持有的锁还未同步到新的主节点时,B可在新主节点获取锁,导致并发问题。
-
redis集群脑裂,导致出现多个主节点
Redis 使用注意事项
大 Key
大 key 指的是 redis 键值对中 value 很大的数据。
| 数据类型 | 大 key 标准 |
|---|---|
| String | value的字节数大于10KB |
| Hash, Set, Zset, list | 元素个数大于5000个或者总value字节数大于10MB |
大 key 的危害:
- 读取成本高
- 容易导致慢查询(过期、删除)
- 主从复制异常,服务阻塞
- 请求 Redis 超市报错。
消除大 Key 的方法
- 拆分:将大 Key 拆分成多个小 Key, 例如一个 String 拆分成多个 String:
- 压缩:通过 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.
慢查询场景
容易导致 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 值。
-
-
缓存雪崩
-
将缓存失效时间分散开,比如在原有的失效时间基础上增加一个随机值。
-
使用缓存集群,避免单机宕机造成的缓存雪崩。
-