首页
Redis
技术内幕
纯内存存储、IO多路复用、单线程架构。
Redis服务器是一个事件驱动程序,需要处理两类事件:文件事件和时间事件,文件事件用于处理服务器和客户端之间的网络IO,时间事件主要用来处理定时操作。
文件事件处理器分为四部分:套接字、I/O多路复用程序、文件事件分派器以及事件处理器。
- 套接字:文件事件是对套接字操作的抽象,每当一个套接字的操作准备就绪时,就会产生一个文件事件。
- I/O多路复用程序:负责监听多个套接字,会将所有产生的套接字事件都放到同一个队列里,文件事件分派器从队列中获取事件。
- 文件事件分派器:将套接字关联到对应的事件处理器。
- 事件处理器:包含连接应答处理器、命令请求处理器、命令回复处理器等等。
数据结构
- string:int(整型)、embstr(小于等于44个字节)、raw
- hash:ziplist、hashtable
- list:quicklist
- set:intset(整数集合)、hashtable。
- zset:ziplist、skiplist。
编码方式
- embstr:为连续分配的空间,查找更快,且内存释放也只需一次。
- ziplist:为连续存储的顺序结构,能节省内存,虽然其复杂度为O(n),但因为是连续的内存空间,所以可以利用CPU缓存大幅提高性能。
- quicklist:是结合了ziplist和linkedlist的优势的数据结构,即节点为ziplist的链表。
- skiplist:多层结构链表,相比于红黑树实现简单但占用更多的内存,查找效率可比拟二叉查找树,同时适合范围读取。
SDS
简单动态字符串,只有值才会使用SDS,键还是使用C语言的字符串,SDS记录了字符串长度。优势在于:不需要遍历即可以直接获取到字符串长度;杜绝了缓存区溢出;减少修改时内存重分配,包括空间预分配以及惰性空间释放;二进制安全,允许空字符(不再需要用于标记字符串的结束),支持保存任意格式的二进制数据。
其他功能
- pipeline:用于批量执行多个命令,能有效节约RTT(往返时延),相比于原生批量命令,pipeline是非原子性的。
- Bitmaps:位操作,底层为字符串。
- HyperLogLog:基数算法,底层为字符串。
- Geo:地理信息功能,使用geohash和zset数据结构实现。
- pub/sub:消息队列,适合可靠性要求低的简单场景。
事务
- MULTI:使用MULTI命令开启一个事务,之后发送的所有的命令会被加入一个事务队列中,且服务器只会返回QUEUED,表示当前命令已经入队;
- EXEC:使用EXEC命令退出当前事务,此时服务器会顺序执行事务队列中的所有命令,然后返回所有的结果,如果执行命令时出现错误,服务器仍会继续执行剩余的命令;
- DISCARD:使用DISCARD命令取消当前事务,事务队列中所有命令都会被服务器丢弃;
- WATCH:在开启一个事务前使用WATCH命令监听一个或多个键,服务器在执行事务前会检查被监听的键是否有被改动过,如果已被改动则会直接抛出异常,不会执行事务队列中的命令;
- UNWATCH:使用UNWATCH命令可以取消所有之前的WATCH命令。
ACID
- 原子性:不具备真正的原子性,如果发送命令期间,出现语法错误则整个事务都不会执行,此场景具备原子性,但如果是执行时出错(错误的操作),事务仍会继续执行下去且无法回滚,此场景无原子性;
- 事务执行过程中出现宕机,如果结果已被持久化则不会丢失,即会出现部分成功无法保证原子性。
- 一致性:由redis保证;
- 隔离性:事务为串行执行的,故多个事务之间可以保证隔离性;
- 持久性:由redis持久化机制保证。
包含逻辑处理的事务
- 方案:首先监听本次事务涉及到的键,然后查询数据并执行逻辑处理,确定出需要执行的命令,最后开启事务并执行命令。
- 分析:相当于使用了乐观锁,对键的监听能保证并发安全,缺点是不适合高并发场景,且锁粒度过大。
Lua
通过lua脚本可以执行redis命令,高效且是原子性的,同时可以被缓存以实现复用,绝大多数情况下都应该使用lua脚本替代事务。
持久化
RDB
为当前进程数据生成快照保存,适合冷备份,优点是恢复速度快,缺点是无法很好的保证高可用性和实时性。
包括save和bgsave命令,save命令会阻塞redis,不建议使用;bgsave命令会fork一个子进程,只有在fork阶段才会阻塞redis,为主流方式。
bgsave流程
- 执行bgsave,会先判断当前是否存在bgsave命令,存在则直接返回;
- 执行fork,过程中会阻塞父进程,fork完成后,不再阻塞父线程;
- 子进程使用copy-on-write机制与父进程共享内存,根据父进程内存生成临时快照文件,完成后原子替换原RDB文件;
- 替换完成后发送信号通知父进程,父进程更新统计信息。
AOF
以独立日志的方式记录每次写命令,通过重新执行AOF文件中的命令来恢复数据,优点是能保证实时性,缺点是恢复速度慢。
所有写命令以文本协议格式追加到AOF缓冲区,根据指定的策略(一般使用everysec)将缓冲区同步到磁盘的AOF文件,同时定期会对AOF文件执行重写以防止文件过大。
‘no-appendfsync-on-rewrite’配置表示AOF重写期间AOF追加操作会阻塞,即可能会导致服务器阻塞,默认开启,如果关闭则在重写期间AOF追加会被写入缓冲池内,重写完成后再追加到AOF文件,如果服务宕机则会丢失缓冲区内的数据。也可以选择在服务器压力较小时手动触发bgrewriteaof,以避免在流量高峰时触发AOF自动重写。
RDB和AOF混合
4.0版本开始支持混合持久化,AOF重写的时候会把RDB的内容写到AOF文件的开头。
内存管理
内存碎片
Redis服务器的统计值为mem_fragmentation_ratio,一般不要大于1.5。
产生内存碎片的原因有:Redis向操作系统申请的内存空间可能会大于数据实际需要的存储空间;频繁修改数据,Redis的内存策略是不轻易释放空间给操作系统。
内存回收策略
- 惰性删除:读取到过期键时才删除
- 定时删除:定时删除过期的键
- 主动删除:内存达到maxmemory会触发主动淘汰策略
内存溢出控制
- noeviction:默认策略,不删除;
- volatile-lru:使用lru算法,只删除过期的键;
- allkeys-lru:使用lru算法,可删除所有键;
- volatile-random:随机删除,只删除过期的键;
- allkeys-random:随机删除,可删除所有键;
- volatile-ttl:优先删除ttl较短的键,也包括还没过期的键;
- volatile-lfu:使用lfu算法,只删除过期的键;
- allkeys-lfu:使用lfu算法,可删除所有键。
内存优化
- 编码优化,如放宽使用ziplist编码的条件,字符串尽量在44个字节以下以使用embstr编码;
- 缩减键值对象,在能满足业务场景的前提下,尽可能的缩短键和值的长度;
- 尽量不要对字符串使用修改操作,SDS会预分配空间造成内存浪费;
- 字符串重构,string类型改为hash类型;
- 使用更节省空间的序列化方式。
主从复制
- 从节点保存主节点信息;
- 主从建立socket连接;
- 发送ping命令;
- 权限验证;
- 同步数据集;
- 持续异步复制。
同步数据集
使用psync命令执行数据同步,分为全量复制和部分复制。
- 全量复制流程:
- 从节点保存主节点的运行ID和偏移量offset;
- 主节点执行bgsave保存RDB文件到本地,之后主节点会把写命令数据保存在复制积压缓冲区内;
- 主节点发送RDB文件给从节点;
- 从节点接受完成RDB文件后清空旧数据;
- 从节点加载RDB文件;
- 主节点把缓冲区内的数据发送给从节点。
- 部分复制:如果出现故障导致宕机,恢复后会执行部分复制操作,从节点会将保存的已复制数据的偏移量offset和主节点的运行ID发送给主节点,并要求补发丢失的数据,如果主节点的复制积压缓冲区内存在这部分数据则直接发送给从节点。
部署模式
主从
使用一台主库和多台从库,可以实现读写分离,但不支持主从切换。
哨兵
在主从模式的基础上额外增加了哨兵节点集群,哨兵节点为不存储数据的redis实例,其节点个数需要为奇数个,每一个哨兵节点都会监控所有普通节点和其他哨兵节点,当发现普通节点失效会将其标记为下线,如果下线的是主节点,则哨兵集群会通过半数原则推选出一个哨兵节点来完成故障转移,并通知到应用方和整个集群。
故障发现及转移通过pub/sub功能实现,整个集群使用固定的Channel进行通信,普通节点定时发送消息以维持心跳,哨兵集群订阅频道监控普通节点,并在故障转移完成后发送消息通知到整个集群。
集群
由多个节点组成,包括主节点和对应的从节点,主节点负责读写请求和集群维护,从节点只作为主节点的备份,对主节点的数据和状态信息进行复制,并在主节点故障时替换主节点。
集群模式只允许使用0号数据库。
使用虚拟槽分区,范围为0-16383,使用bitmap标识则需要16383位,即大小为2KiB;每个主节点只负责其中一个子集,由节点自身维护槽和键的映射关系;因为解耦了数据和节点的关系,所以节点可以动态增加或删除。
节点间使用Gossip协议通信,即彼此不断的交换信息,追求的是整个集群的最终一致性。每个节点需要额外开启一个通信端口,并在固定的周期内根据一定的规则选择几个节点通信。
优化建议
- 主节点不做持久化工作,会增大数据丢失的可能;
- 从节点不建议过多,或者使用树状或单向链表结构。
可能造成阻塞的原因
- 不合理的使用命令,如keys、hgetall、flushall等,可以通过使用slowlog命令分析慢查询;
- 由于命令执行效率下降导致CPU饱和,如放宽了使用ziplist的条件而导致过度内存优化;
- 持久化阻塞,内存实例过大会导致fork阻塞,硬盘压力过大会导致fsync阻塞。
- 短连接优化,如果客户端未使用连接池,会导致Redis服务器浪费大量资源在连接处理上。