Redis 概述
以上图片来自极客时间的《Redis核心技术与实战》。
对原理的分析都是基于Redis3.0版本的代码,现在最新的版本应该是6.0,很多功能都是后面的版本引入的,因此这篇里不会描述多线程这些功能。
为什么使用 Redis
Redis 的缺点 & 优点
特性及优势:
- 内存数据库,吞吐率不受磁盘影响;
- 每秒可以处理超过 10 万次读写操作;
- 多数据结构支持,包括 string、hash、list、set、zset、Bitmaps、Hyperloglog、Geo、Pub/Sub、Redis Module、BloomFilter、RedisSearch、Redis-ML 等,支持绝大多数应用场景;
- Replication(复制);
- lua(支持服务端执行复杂的业务逻辑);
- LRU eviction(缓存淘汰);
- Transactions;
- Persistence(持久化),包括 rdb(快照)和 AOF 两种;
- Sentinel(哨兵);
- Cluster(分区)。
但是也不能忽略 Redis 本身的一些缺点:
- 数据库容量受到物理内存的限制,不能用作海量数据的高性能读写,因此 Redis 适合的场景主要局限在较小数据量的高性能操作和运算上;
- 缓存和数据库双写一致性问题;
- 缓存雪崩问题;
- 缓存击穿问题;
- 缓存的并发竞争问题。
Redis & Memcached
Redis 相对 Memcached 来说有以下优点:
- memcached 所有的值均是简单的字符串,redis 作为其替代者,支持更为丰富的数据类型
- redis 的速度比 memcached 快很多
- redis 可以持久化其数据
Redis 和 Memcached 之间存在以下区别:
- 存储方式 Memecache 把数据全部存在内存之中,断电后会挂掉,数据不能超过内存大小。 Redis 有部份存在硬盘上,这样能保证数据的持久性。
- 数据支持类型 Memcache 对数据类型支持相对简单。 Redis 有复杂的数据类型。
- 使用底层模型不同 它们之间底层实现方式 以及与客户端之间通信的应用协议不一样。 Redis 直接自己构建了 VM 机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求。
应用场景
- 会话缓存(Session Cache)
最常用的一种使用 Redis 的情景是会话缓存(session cache)。用 Redis 缓存会话比其他存储(如 Memcached)的优势在于:Redis 提供持久化。当维护一个不是严格要求一致性的缓存时,如果用户的购物车信息全部丢失,大部分人都会不高兴的,现在,他们还会这样吗?
幸运的是,随着 Redis 这些年的改进,很容易找到怎么恰当的使用 Redis 来缓存会话的文档。甚至广为人知的商业平台 Magento 也提供 Redis 的插件。 - 全页缓存(FPC)
除基本的会话 token 之外,Redis 还提供很简便的 FPC 平台。回到一致性问题,即使重启了 Redis 实例,因为有磁盘的持久化,用户也不会看到页面加载速度的下降,这是一个极大改进,类似 PHP 本地 FPC。
再次以 Magento 为例,Magento 提供一个插件来使用 Redis 作为全页缓存后端。
此外,对 WordPress 的用户来说,Pantheon 有一个非常好的插件 wp-redis,这个插件能帮助你以最快速度加载你曾浏览过的页面。 - 队列
Reids 在内存存储引擎领域的一大优点是提供 list 和 set 操作,这使得 Redis 能作为一个很好的消息队列平台来使用。Redis 作为队列使用的操作,就类似于本地程序语言(如 Python)对 list 的 push/pop 操作。
当然要将 Redis 作为消息队列投入生产环境还有很多设计要点,比如采用 sleep 一段时间重试还是 blpop 阻塞、主题订阅、如何应对消费者下线导致的消息丢失问题(如何保证消息一定能被消费)等。
Redis 作为消息队列坑比较多,如果希望少点麻烦且对服务质量有一定要求,最好还是采用 RocketMQ 这些比较成熟的方案。 - 延时队列
使用 zset,时间戳作为 score,消费者用 zrangebyscore 指令获取 N 秒之前的数据轮询进行处理,这种思路和 JDK 中的 ScheduledThreadPoolExecutor 有点像。 - 排行榜/计数器
Redis 在内存中对数字进行递增或递减的操作实现的非常好。集合(Set)和有序集合(Sorted Set)也使得我们在执行这些操作的时候变的非常简单,Redis 只是正好提供了这两种数据结构。所以,我们要从排序集合中获取到排名最靠前的 10 个用户–我们
称之为“user_scores”,我们只需要像下面一样执行即可:
当然,这是假定你是根据你用户的分数做递增的排序。如果你想返回用户及用户的分数,你需要这样执行:
ZRANGE user_scores 0 10 WITHSCORES
Agora Games 就是一个很好的例子,用 Ruby 实现的,它的排行榜就是使用 Redis 来存储数据的,你可以在这里看到。 - 发布/订阅
最后(但肯定不是最不重要的)是 Redis 的发布/订阅功能。发布/订阅的使用场景确实非常多。我已看见人们在社交网络连接中使用,还可作为基于发布/订阅的脚本触发器,甚至用 Redis 的发布/订阅功能来建立聊天系统!(不,这是真的,你可以去核实)。
Redis 提供的所有特性中,我感觉这个是喜欢的人最少的一个,虽然它为用户提供如果此多功能。 - 分布式锁
不要用 setnx+expire,因为如果进程 crash 或重启这个锁就直接失效了。实际上 set 命令本身就包含超时和 cas 的设置。 - 扫描
如果 Redis 中有 1 亿多个 key,其中有 10W+个 key 有固定的前缀(这种场景非常常见),如何将它们全部找出来?
由于 Redis 的单线程特性,使用 keys 可能会阻塞 Redis 进程,最好换成 scan 命令,不过可能提取出的部分 key 是重复的,需要客户端做去重。
QA
Redis与其他KV存储有何不同?
- 更多复杂的数据结构,支持更多特殊的场景;
- Redis是内存数据库,但是支持持久化到磁盘。
有人说 Redis 只适合用来做缓存,当数据库来用并不合适,为什么?
Redis 的事务并不严格:
* A(原子性):Redis 支持事务,所有命令会被保存到一个事务队列中,服务器接收到 exec 时才会被真正执行,注意如果中间出错,事务不会回滚,后面的指令还会继续执行;而且如果涉及到复杂的逻辑判断,则只能通过lua 脚本实现“伪原子性”,说它是“伪原子性”是因为虽然脚本可以一次性执行多条命令,如果中间某个命令出错还是会无法保证“要么全部执行,要么都不执行”的要求。
* I(隔离性):Redis 是单线程模型,因此可以保证隔离性。
* D(持久性):Redis 是内存数据库,服务器意外崩溃会导致内存中的数据丢失,除非开启 AOF,并且配置成每次写入都记日志,但是这样又会极大地影响效率,所以一般会配置成混合模式的持久化。
Redis会占用多少内存空间?
- An empty instance uses ~ 3MB of memory.
- 1 Million small Keys -> String Value pairs use ~ 85MB of memory.
那么10亿个kv,大概就会占用85G的内存了。 - 1 Million Keys -> Hash value, representing an object with 5 fields, use ~ 160 MB of memory.
64位机器会占用比32位机器更多的内存,因为指针在64位机器上占用更多空间,但同时64位机器也可以有更大的内存空间。
Redis 的底层数据结构有哪些
sds:string 使用,变长字符串,不够的情况下重新分配空间并将老字符串数据拷贝过去;
dict:字典应用很多,包括 Redis 数据库中保存所有 key-value、hash、set、zset。dict 类似 Java 中的 HashMap,将 key 散列到哈希桶数组中,每个哈希桶都是一个链表,插入就是插入到链表头部,当元素超过了容量的一半后会启动渐进式 rehash 进行扩容。
ziplist:相当于一个数组,查询时需要遍历一次,每次插入都需要 realloc 重新分配一个新的更大的数组,然后把老数组内容拷贝过去。
quicklist:由于 linkedlist 附加空间成本高且容易产生碎片,因此 Redis 里的 quicklist 设计成了 linkedlist 和 ziplist 的结合,它将 linkedlist 按段切分,每一段使用 ziplist 存储;
skiplist:skiplist 用于实现 zset 中按 score 排序的要求,插入时先自顶向下查位置,然后按概率计算该节点应该分配到几层。
存储数据选择 string 还是 hash?
从业务层面来看,如果要存好多字段的对象、而且这个对象的每个字段都会单独拿出来用,则可以考虑使用 hash,否则没有太多限制条件。
从性能角度来看,如果存的字段少,hash 会使用 ziplist 结构存储,性能多少受点影响,而且还要考虑转换结构和渐进式扩容对性能的损耗。
从节约空间的角度来看,string 的 key 一般都会加个前缀,一般会比 hash 占用更多的空间,不过差距不大。
设计 redis 排序,数据结构是金额+花钱的时间,金额越大,时间越早越靠前
用 zset 存,score 是金额拼上时间,金额放高位,MAX_INT 和时间作差放低位,查询时使用ZREVRANGE
命令查询。
hash 中哈希冲突怎么解决的
分两种情况:hash 在数据量小时结构是 ziplist,这时插入不会做冲突检测,插入到目标位置后就向后统一移动数据,给新插入的数据项流出空间;在数据量大时结构是 dict,这种结构和 Java 中的 HashMap 类似,使用链表来处理冲突。
- 说说 Redis 为什么那么快。
单线程模型->减少了线程间上下文切换的开销。
多路复用的 IO 模型->单线程监控多个连接。 - 为什么 Redis 记录 AOF 日志是先执行指令然后再记录 AOF 日志?而不是像其他存储引擎一样反过来?
主要是因为 Redis 本身是缓存而不是 db,侧重点不同,db 先写日志是为了失败回滚,而 Redis 持久化是一个附加功能,只能保证数据不会完全丢失。
Redis 淘汰时,如果读取,会不会数据不完整
redis 的淘汰分两种:
- 一种是过期,这种不会导致这种问题,因为查询时会判断下过期时间,过期了就不返回;
- 另一种是超过内存容量淘汰,比如 LRU,这种也不会导致这种问题,因为执行每个命令时都会检查下缓存是否超出了阈值,可见代码
server.c/processCommand
:
Redis 的持久化原理是什么
Redis 有两种持久化方式:RDB 和 AOF
RDB 是快照,AOF 记录了写操作,效率起见,一般 RDB 作为 checkpoint,checkpoint 后的数据通过 AOF 恢复。
RDB 和 AOF 之间的区别
RDB 二进制文件可以直接加载到内存速度较快;AOF 要重放命令,所以速度比较慢。
RDB 需要全量备份,AOF 可以增量备份,二者的应用场景不同。
Redis的复制原理是什么?
master 会启动一个后台进程进行持久化(RDB or AOF),第一次连接时会将 RDB 文件发给 slave,slave 先保存到磁盘,之后加载到内存中;如果不是第一次连接,slave 连接 master 后通过 PSYNC 命令告知自己同步的起始位置,master 将增量部分 AOF 文件发送给 slave。
Redis 持久化期间,主进程还能对外提供服务吗?为什么
能。
因为 Redis 的复制是通过 fork 子进程实现的,父进程仍然可以接收请求。
持久化期间,Redis如何处理新写入的数据呢,这个数据也会直接进行持久化吗?
不会。
因为 Redis 复制是通过 fork 子进程实现的,由于 COW 机制,子进程只能看到老数据。
主从复制为什么会发生延迟?怎么解决
延迟无法避免,比如主从之间的网络抖动、slave 发生阻塞(如 IO)等情况。
解决办法有两种:
min-slave-to-write N
和min-slave-max-lag M
,控制 Master,只有在至少有 N 个 slave 正在工作,并且滞后时间均小于 M 秒的情况下,Master 将不接受写入请求;slave-serve-stale-data
,控制从库对主库失去响应或复制进行过程中从库的表现,为 yes 则从库会继续响应客户端的请求,为 no 则除去 INFO 和 SLAVOF 命令之外的任何请求都会返回一个错误SYNC with master in progress
;- 编写外部监控程序,如果某个 slave 延迟较大,则通知 client 不要读这个 slave。
Redis 怎么实现高可用
从复制、Sentinel 到 Cluster
sentinel 中,使用客户端是怎么连接服务器的?(Redisson 配置)
见《Redis 客户端》。
哈希槽原理?和一致性哈希的区别?怎么落点
redis cluster 默认分配了 16384 个 slot,当我们 set 一个 key 时,会用CRC16算法来取模得到所属的 slot,然后将这个 key 分到哈希槽区间的节点上,具体算法就是:CRC16(key) % 16384
。所以我们在测试的时候看到 set 和 get 的时候,直接跳转到了 7000 端口的节点。
哈希槽与一致性哈希的区别:哈希槽由客户端来重定向到目标 slot 所在节点,一致性哈希需要由服务器端重定向到目标节点,而且需要按顺时针方向一个一个节点递归地找。
Redis雪崩、击穿、穿透等现象是怎么出现的?怎么解决
- 缓存穿透
缓存穿透指查询一个不存在的数据,出于容错考虑这个查询会穿透到 DB 层,如果这种带穿透的查询特别多可能会把 DB 打挂掉。
解决办法:使用布隆过滤器,保存所有可能存在的数据到一个足够大的 bitmap 中,由于布隆过滤器的特性,一定不存在的数据在 bitmap 中一定找不到,从而可以很大程度上避免对底层存储系统的查询压力;还有一种更简单的方法,就是在查询返回结果为空时也把这个空结果缓存起来,但是它的过期时间会短一些,最长时间不超过 5 分钟。 - 缓存雪崩
缓存雪崩指的是设置缓存时采用了相同的过期时间,导致缓存在同一时间同时失效,请求全部打到 DB,DB 瞬时压力过大导致雪崩。
解决办法:缓存失效时间随机化,在原有失效时间基础上加上一个随机值,可以使得过期时间的重复率降低;加锁并令请求排队,使得请求串行化,避免所有请求都查询数据库,不过这样会导致性能的降低。 - 缓存击穿
缓存击穿指的是某个 key 在过期时正好有大量请求访问该 key,这些请求会同时回表,可能会瞬间将后端 DB 打挂。
解决办法:使用互斥锁,缓存失效时先加锁,避免并发回表;一些长时间不变的数据完全可以不设置过期时间,或者过期时间特别长。
主从复制的流程?传的是文件吗?
流程见《主从同步》。
如果是全量同步,同步时会先同步 RDB 文件,再同步增量写命令;
如果是部分重同步,则只同步增量写命令。
中间传输失败怎么办?中间传输不一致怎么办
如果上次传输中断,则下次同步时从中断位置开始执行部分重同步。
参考
应用
数据结构
- Redis 源码涉及 C 语言
- Redis 内部数据结构详解(1)——dict
- Redis 内部数据结构详解(2)——sds
- Redis 内部数据结构详解(3)——robj
- Redis 内部数据结构详解(4)——ziplist
- Redis 内部数据结构详解(5)——quicklist
- Redis 为什么用跳表而不用平衡树?
- Redis 中的集合类型是怎么实现的?
Persistence
客户端
主从复制
Sentinel & Cluster
架构迁移
- Redis 集群迁移案例
- redis-migrate-tool
- redis-port
- redis-migration
redis-migration
redis-migration:独创的 redis 在线数据迁移工具