redis核心知识笔记整理

        本文内容主要从《redis设计与实现》一书中摘录总结成文,可以让我们以最快的速度回顾相关的核心知识点。文章成文以常见的领域模块组织。

数据结构

  1. 字符串:redis中的字符串数据类型不直接使用C语言中的字符数组,而是使用SDS(动态字符串)的结构体表示。使用SDS可以获得非常多的好处。比如可以直接获得字符串的长度,避免C中字符数组每次都重新计算长度的低效;字符串修改操作时避免内存溢出问题,避免频繁申请调整内存大小问题,因为设计有一定的缓冲机制(预分配空间)
  2. 列表:双向无环的链表。
  3. 哈希:和java的Map原理差不多,支持渐进式自动扩容/缩容。但是不支持同一个桶内的enty链表多了就变成红黑树,也不支持并发的分段锁
  4. 跳跃表:一种有序的链表结构,但是因为节点设计有多个后继指针,所以遍历的时候可以中间跳过一些节点,加快查找速度。其算法复杂度为O(logN),差不多可以和树媲美了。但是其构建过程又比树简单。因此被广泛应用。
  5. 整数集合:有序去重的整数集合。支持自动长度类型升级,但不支持降级。大大节约了存储空间
  6. 压缩列表:一种用于实现上面列表和哈希两种数据类型底层存储的数据结构之一。当列表的值或哈希的键值属于短小的整数或字符串以及值数量较少时redis底层会自动应用这一实现。主要是为了压缩空间而设计的。底层采用字节数组实现。自己定义数据结构、编码、长度以及操作指针移动读写数据,类似自定义读写文件的原理。只是一个在内存中,一个基于磁盘文件。

数据类型

  1. redis中的5中数据类型的底层具体实现就是基于以上6中数据结构实现的。
  2. 字符串类型底层实现有int、raw、embstr三种实现。raw就是SDS(动态字符串),embstr是一种专为短字符串优化设计的存储结构,相对于raw优化了内存申请及释放效率。
  3. 列表类型底层实现有ziplist(压缩列表)和linkedlist(链表)两种实现。
  4. 哈希类型底层实现有ziplist(压缩列表)和hashtable(哈希表)两种实现。
  5. 集合类型底层实现有intset(整数集合)和hashtable(哈希表)两种实现。
  6. 有序集合类型底层实现有ziplist(整数集合)和skiplist(跳跃表)两种实现。
  7. 另外一种BitMap操作,也是基于SDS结构存储实现的。因为SDS结构中使用了字节数组作为存储类型,一个字节有8位,我们对这些位进行单独操作即可实现。所以刚好也适用于这类位操作的存储,只是存储空间必须是8的整数倍,会浪费掉一点点空间。从而也有了其他编程语言的BitMap功能

存储模块

内存管理

  1. c语言本事不具备自定内存管理,所以redis自己基于引用计数方式实现了自己的对象内存管理。
  2. 为了节省内存,采用相同信息对象共享机制,对所有数据类型。
  3. 对于已经过期的key/value采用惰性删除和定期删除相结合的策略。惰性删除:每次对可以的读写请求都会判断一下是否过期,过期就删除。定期删除:定期检查过期字典,分批次检查其中一部分key,过期则删除,防止无效key积累占用空间。

持久化模块

  1. 有RDB和AOF两种持久化模式。RDB每次都是全量复制当前数据实现,而AOF则是基于更新命令日志实现保存和恢复数据实现。
  2. RDB保存数据有save和bgsave两种方案。save会阻塞服务直到完成,而bgsave是通过操作系统的fork调用产生一个单独子进程实现保存,子进程操作系统会自动拷贝父进程数据,所以不影响redis继续提供服务。
  3. RDB文件保存过程中针对较长的字符串,会启用LZF压缩算法进行压缩存储。redis自定义了RDB文件的格式。
  4. AOF是异步同步的,所以系统宕机会存在丢失数据的可能。具体过程是更新命令修改内存数据后,将命令文本放入aof_buf缓冲池中,然后根据某种配置的策略缓冲池满时或定时调用操作系统的write方法写入文件,此时数据可能还处在操作系统的PageCache中,并没有真正写入磁盘。redis提供了一些策略配置(appendfsync),可以让我们定时或达到一个更新量后或每次更新一个命令后强制刷入到磁盘,通过调用操作系统的fsync方法。
  5. AOF日志文件恢复数据采用模拟客户端重放日志的方式进行。有点类似MySQL的主从复制机制
  6. 为了应对AOF文件日志膨胀问题,采用AOF文件重写机制,将最新的数据使用一条命令重写保存到文件,然后替换掉之前的AOF文件,大大降低文件磁盘占用,也加快了数据恢复操作。
  7. AOF文件重写机制实现,是通过创建一个子进程,使用操作系统fork调用将自动复制父进程的数据给子进程。这样子进程重写文件,不会阻塞父进程继续提供服务了。同时父进程将新的更新命令存入一份到aof重写缓冲区。最后一起追加到子进程创建好的AOF文件末尾再替换掉原来的文件。这样就把AOF重写操作对服务器的影响降到了最低。

网络通信

  1. 采用I/O多路复用的reactor模型,底层有select、epoll等不同系统的实现方式,会自动根据当前操作系统选择使用。采用队列方式接受客户端命令。命令处理过程采用单线程方式进行,避免多线程导致的锁问题,提高效率。命令执行完返回结果仅将数据放入网络缓冲区等待发送即可,然后立即又去执行下一个命令队列中的命令。而不是等待将数据发送回给客户端接收后再去执行下一个命令,这样效率就会很差了。所以redis的瓶颈就在于对单个命令执行的效率。
  2. 哨兵节点、主从节点之间都建立了相互通信的蛛网连接模式,集群模式下也是类似。
  3. 为了通信链接的安全性和可靠性,采用不同用处的通信隔离机制。即节点之间不是只有一个套接字链接,而是有多条,用途不同。

集群模块

  1. 主从复制采用PUSH和PULL相结合的方式,由从服务器发起同步复制请求命令。然后主服务推送更新命令日志给从服务器执行。
  2. 主从复制模块,对于新的从服务器,主服务器会生成一个RDB文件推送给从服务器进行恢复。后续主的新命令日志放入一个缓冲池中,并推送给从,从而实现数据异步最终一致性。对于短暂掉线的从服务器,重新同步主服务器的命令日志无需通过RDB文件恢复,而是主服务器直接将其对应的从服务器的本地从服务器的命令日志缓冲区中通过偏移量比较,继续推送到从服务器
  3. 每个节点记录自己处理的槽(Slot)使用BitMap方式。节约了空间,其设置获取值的时间负责度为O(1)。
  4. 服务端接受到客户端的key值操作后,会判断key所在的槽是否属于自身,如果不属于,则会返回客户端一个重定向操作,而不是直接自己转发命令到其他节点。这和许多其他分布式系统不同,比如zk是转发给leader,还有es、rabitmq等都是通过当前接受节点代理转发到目标节点,目标节点返回代理节点后再转发给客户端。这种对客户端要求重定向机制,感觉效率很低,可能绝大多数操作都需要一次重定向,命中率不高。
  5. 服务端对请求的key执行哈希算法得到一个长整数,然后对16384个槽取模得到最终落地的槽号。

选举

  1. 集群模式下,选举新主节点流程:从节点发现其主节点下线,于是广播给整个集群具有投票权的其他主节点。然后这些主节点进行下线主节点的从节点投票,通过Raft算法,最终得票过半的成为新的主节点。并负责进行故障转移操作。整个过程其实也类似于ZK的选主过程,关键点是一个选举纪元的自增整数,zk里叫“代”。避免算法的数据不一致性问题。
  2. 哨兵集群在进行故障判断和转移时,采用raft算法,选举出领头节点。由领头节点负责决断主服务器是否下线及故障转移操作。

元数据

  1. 对于新加入的节点或下线的节点信息通过像病毒一样相互传染的方式感知到,并更新自己的集群元数据信息。也就是说,它没有一个像其他分布式系统那样的中心节点去集中管理集群元数据和心跳。redis的集群元数据会存在于任何一个节点上。通过与已知节点相互通信的“传染”方式达到信息的最终一致性。缺点可能也比较明显,时间效率比较低,数据一致性算法实现也更复杂些。这种数据一致性算法显然不是Paxos,那么它是如何解决“拜占庭将军问题”的呢?
  2. 每个节点都记录所有Slot的指派信息,Slot的改变后,目标节点会通知其他所有节点,更新自己的Slot元数据信息。

容错机制

  1. 支持集群模式和哨兵模式的高可用性。主节点宕机了,会自动被从节点顶替,旧主节点恢复后成为从节点

事务

  1. 事务实现机制:它是通过将事务开始和结束命令之间的操作命令服务端接收后暂时存放在一个缓冲池中,等到事务执行命令接收到后再一次性执行。此外使用lua脚本打包一系列命令也可以实现,这样就可以不用使用事务开始结束命令了。
  2. 一致性:所谓数据一致性,并不是简单理解为事务执行成功后数据不会丢失。而应该理解为总是一致的,包括更新失败,宕机后数据被丢失或清空,那么也是一致的。
  3. 隔离性:主要是通过利用redis单线程处理模型来实现的。当然在服务端事务命令队列正式打包执行前,也会存在其他客户端请求修改数据,这样也会破坏隔离性,所以提供watch命令可以事先将要更新的key关注起来,如果在服务端事务执行前被其他线程修改了,则直接返回事务运行失败。
  4. 原子性:事务之间的命令要么都执行,要么就不执行。事务命令入队时,如果判定为错误的命令(命令不存在或格式不正确等),则事务不执行。
  5. 持久性:即便开启持久化RDB或AOF模式,也很难具有此属性。因为持久化都是异步实现的。只有AOF的appendfsync选项配置为always时,才可以满足这一性质。因为服务端执行一个命令后就会同步刷新到磁盘,如果刷入失败,则内存中的值也是失败的,具有原子操作性质。但是这种效率非常低下,所以很少会使用这个方案。
  6. 回滚:不支持事务回滚,如果事务命令组在执行期间,有出错的命令,则不会影响后续命令的执行。主要是因为太过复杂了

数据一致性

  1. redis主从之间数据满足最终一致性特征,但是会存在数据丢失风险。更新某主节点key值返回客户端成功,此时主节点和从节点数据短暂不一致,但是稍后异步同步后,数据最终一致了。
  2. 存在数据丢失风险,比如刚写入一个key到某主节点上,然后从节点来没来得及同步主节点就宕机了,此时某个从节点代替主节点后,该key就不存在了。因为主从同步是异步进行的。
  3. 即便开启持久化能力RDB或AOF也是会存在数据丢失风险的,因为都是异步进行的。

其他模块

  1. 对于内部定时任务,将任务包装成时间事件,用链表连接起来。然后使用专用的一个线程不断的遍历轮询这个事件链表,判断这个任务事件是否当前已经到达了需要执行一次的时候,如果到达了则调用事件对象里的任务函数执行这个定时任务,否则就略过。任务执行完后,更新一下事件对象的下次执行时间即可。这种设计避免了对每个定时任务启动专用线程来维护,线程执行玩任务后进入休眠固定时间状态,直到下次唤醒后继续执行。redis这里只用了一个线程就搞定了,任务到了执行时间,就丢入线程池去执行好了。这个方案有点类似于我的easytask机制,不过它的不是环形队列设计,而且它必须确保1秒内轮询完全部的任务事件,否则任务执行就会出现超过秒级的延迟。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值