Code前端首页关于Code前端联系我们

Redis原理:单点传输速率、存储类型、监控机制、网络协议...

terry 2年前 (2023-09-26) 阅读数 55 #数据库

作者:永恒的灵魂

redis单点吞吐量

单点TPS达到8万/秒,0秒/ 100秒。

Redis的5种存储类型

string、list、set、map(hash)、stored-set

redis的字符串类型

  1. ,浮点字符串可以表示3种类型。根据场景自动相互转换,根据需要选择底层传输模式
  2. 值内部存储为int和sds结构。 int 存储整数,sds 存储字节/字符串和浮点数据。最后会有一个存放“\0”(C标准目录)的地方,并且多保留一些空的空间(即自由空间)。如果append字符串的长度小于可用空间,则sds不会重新申请内存。直接利用空闲空间
  3. 进行扩容:如果字符串操作完成后字符串预期长度小于1M,则扩容后的buf数组大小=预期长度*2+1;如果大于1M,buf总是保留1M的可用空间。
  4. 值对象通常由两部分内存组成:redisObject部分和sds部分,sds部分由redisObject的ptr指向。创建值对象时,redisObject 和 sds 通常需要两次内存。对于短字符串,两者都可以连续存储,因此可以同时请求两者的内存
  5. redis 列表类型

    1. 列表类型的值对象内部由 linkedlist 或 ziplist 承载。如果列表中的元素数量和元素长度都较小,则redis使用ziplist实现来减少内存使用,否则使用链表结构
    2. ,linkedlist的内部实现是双向链表。 head和tail是元素指针,列表中定义了列表的长度,所以pop/push操作和llen操作的复杂度都是O(1)。由于这是一个链表,所以lindex类的操作复杂度仍然是O(N)
    3. ziplist的内部结构
  • 所有内容都存储在连续的内存中。其中,zlbyte表示ziplist的总长度,zltail指向最后一个元素,zllen表示元素数量,条目包含元素本身的内容,zlend是ziplist的分隔符
  • rpush,rpop, llen,复杂度为 O(1 );lpush 由于 /pop 操作涉及到移动整个元素列表,所以复杂度为 O(N)

redis 的映射类型

  1. map 也称为哈希。 Map内部的key和value不能再嵌入到map中,只能是字符串类型:整型、浮点数、字符串
  2. map主要通过两种载体方法实现:hashtable和ziplist。对于数据量较小的map,使用ziplist
  3. hashtable内部结构
  • 主要分为三层,从下到上分别是:dictEntry、dicttht、dict
  • dictEntry:处理key值。将相邻元素保留在同一个桶中。指针,同时维护哈希收集器的内部连接
  • dicttht:维护哈希表的所有桶链
  • dict:用于在dicttht需要扩容/缩容时管理dicttht的迁移哈希表基本结构维护 dicttht 和散列存储表的字段。它是一个数组,每个元素都指向桶的第一个元素(dictEntry)
  • 设置值的过程:首先通过MurmurHash算法找到key的哈希值,然后调制收集区域的数量。 key对应的bin,然后指定bin,循环遍历所有条目并判断是否已经存在相同的key。如果没有,则将新key对应的键值对插入桶头。并更新使用的dicttht的数量,其中used表示哈希表中存储了多少项。由于每次插入都要遍历哈希存储中的所有条目,因此如果桶中的条目较多,性能会线性下降
  • 扩展:使用负载因子来确定是否增加桶的数量。负载因子=哈希表中的元素与哈希立方体数量的比率。有两个阈值。如果小于1,则不扩展;如果大于5,则肯定是在膨胀。扩容时,新增收集器数量是现有收集器数量的2n倍。
  • 规模:负载因子阈值0.1
  • 通过创建新的哈希表来实现扩容/缩容。即解包时,两个哈希表会同时存在,一个是源表,一个是目标表。通过数据迁移,将源表的分组逐步迁移到目标表,从而实现扩容。迁移完成后,目标表会覆盖源表。在迁移过程中,首先访问源表。如果发现与key匹配的源表存储已经迁移,则重新访问目标表。否则,
  • redis源表中的操作是单线程处理请求,迁移和访问请求是一样的。它是在线程内部执行的,所以不会有并发问题
  1. ziplist的内部结构
  • 与list ziplist实现类似。不同的是,ziplist中属于map的条目数始终是2整数的倍数,奇数存储键,偶数存储值
  • 在ziplist实现中,哈希遍历变成了链接顺序遍历。列表,复杂度为 O(N)。

redis

  1. set的集合类型存储在intset或hashtable中。哈希表值始终为空。如果集合只包含整数元素,则使用intset
  2. intset的内部结构
  • 种子元素是一个字节数组,按照从小到大的顺序存储集合的元素。
  • 由于元素是按顺序排列的,因此集合集合操作是通过二分查找来进行的,复杂度为O(log(N))。插入时,首先通过二分查找得到插入位置,然后展开元素,然后将所有元素在预期插入位置后向右移动一位,最后插入元素。插入的复杂度是O(N)。删除一个类似于

redis

  1. 的ordered-set类型,类似于map,是一个键值对,但是是按顺序的。 value是一个浮点数,即所谓的score,内部是从小到大排序的
  2. 内部结构采用ziplist或者skiplist+hashtable实现

redis客户端与服务端的交互方式

  1. 串行模式请求/响应
  • 每个请求的发送取决于前一个请求对应结果的完整接收。同一连接每秒的传输速率较低
  • 单个请求的 Redis 处理时间通常比 LAN 延迟小一个数量级。因此,在串行模式下,单个连接大部分时间都在等待网络。
  1. 双工请求/响应模式(管道)
  • 适合批量独立写入操作。可以将请求数据批量发送到服务器,然后一次性从连接到服务器的字节流中读取所有响应数据,减少网络延迟,这样单链路传输速率将为幅度高于串行。
  1. Atomic 一种特殊的批量请求/响应模式(事务)
  • 客户端通过与redis服务器的两步交互来实现批量命令原子执行的事务效果:与入队操作(即服务器先进行)暂时传递发送的连接对象)存在于请求队列中)和执行阶段(按顺序执行请求队列中的所有请求)
  • 批量请求执行过程中,链接请求不会执行其他客户端请求
  • Redis事务不一致,没有回滚机制。如果中途失败,会显示错误信息,但不会恢复成功执行的命令
  • 事务的条件可能是读操作。由于批量请求是先排队,然后批量执行,所以读操作通常不会与批量写请求一起执行。目前批量写入前后读取的数据可能不一致。这可以通过乐观锁的可串行性来解决。 Redis通过时钟机制实现乐观关闭。具体实现过程请看下一个问题
  1. 发布/订阅模型
  • 发布者和订阅者通过通道redisDB键值系统进行关联。所有频道都通过一个map来维护,key是频道名称,value是所有订阅者客户端的指针列表
  1. 批量执行脚本(脚本模式)

Redis使用乐观锁流程作为监控机制

  1. 此事务中的寄存器影响了所有处于监控模式的key
  2. 执行只读操作
  3. 根据只读操作的结果,编译写操作命令并发送给服务器入队♽发送原子批量执行命令 EXEC 尝试执行所连接的请求线上的命令
  4. 如果有一个或多个先前注册到监视模式的键在 EXEC 之前发生了更改,则 EXEC 将直接失败并拒绝执行;否则,按顺序执行请求队列。来自
  5. redis 的所有请求都没有本机悲观锁定或快照实现,但可以通过乐观锁定绕过。如果两次读操作不同,则触发watch机制,并拒绝后续的EXEC执行。

Redis网络协议

redis协议位于TCP层之上,即客户端和redis实例保持双工连接。只处理串行协议数据

redis处理命令的主要逻辑

  1. redis服务器在单线程中处理命令,但I/O层同时为多个客户端提供服务。从并行到内部单线程的转换是通过多路复用框架完成的。
  2. 首先从复用框架(epoll、evport、kqueue)内核(kernel)中选择准备好的文件描述符(fileDescriptor)就可以写入了
  3. 上一步准备好的fd的情况下,redis在每个 fd 完成事件上单独处理 fd。处理完同一个fd上的所有事件后,再处理下一个就绪的fd。事件类型有 3 种
  • acceptTcpHandler:连接请求事件
  • readQueryFromClient:客户端请求命令事件
  • sendReplyToClient:执行下一条命令后将临时执行的结果写回客户端 。
  • Redis 持久化机制

  1. redis 主要提供两种持久化机制:RDB 和 AOF;
  2. RDB
  • 默认开启,会根据指定时间将内存中数据快照到磁盘,创建dump.rdb文件,并在redis启动时恢复到内存。
  • redis创建一个单独的fork()子进程,将当前父进程的数据库数据复制到子进程的内存中,然后将子进程写入临时文件。 persist进程结束后,用这个临时文件替换上一个快照文件,然后子进程退出并释放内存。
  • 注意,每个快照持久化都会复制主进程的数据库数据,这会导致内存开销增加一倍。如果当前内存不足,服务器将被阻塞,直到复制后释放内存;内存数据一次性全部写入磁盘,因此如果数据量较大且写入操作频繁,必然会进行大量的磁盘I/O操作,这会严重影响性能以及最后持久化后的数据。可能会丢失;
  1. AOF
  • 以日志的形式记录所有写操作(读操作不记录)。您只需附加该文件,但无法重写它。 redis启动时,根据日志,从头到尾执行,完成数据恢复工作。还执行了flushDB。
  • 触发方式主要有两种:写操作的情况下,每秒定时写入(数据也会丢失)。
  • 由于AOF使用append方法,文件变得越来越大。添加了新的重写机制来解决此问题。当日志文件达到一定大小时,会fork出一个新进程来遍历进程内存。每条记录对应一个 set 语句,该语句被写入临时文件,然后替换为旧的日志文件(类似于 RDB 的工作方式)。默认触发是当AOF文件大小是上次重写后大小的两倍且文件大于64M时;
  1. 如果同时启用两种方式,数据恢复redis会优先进行AOF恢复。一般情况下,应该只使用RDB,默认是开启的,因为相比AOF,RDB方便数据库备份,而且恢复数据集的速度也快很多。
  2. 启用持久化缓存机制会对性能产生一定的影响,特别是当配置的内存已满时,会下降到数百个reqs/s。因此,如果您仅将其用于缓存,则可以关闭持久性。

redis内存分析的设计思想

  1. 执行
  • keys命令有三种方式:获取所有key,然后根据key获取所有内容。缺点是如果key数量特别多,会导致redis陷入困境,影响业务。
  • aof:所有数据都是通过aof文件传输的。缺点是有些redis实例写入频繁,不适合打开aof,而且文件可能很大,传输和解析效率较差
  • rdb:使用bgsave获取rdb文件,然后分析。 。缺点是当子进程分叉时,bgsave 可能会卡在主进程中。对于另外两种类型,bgsave用于在非高峰时段从从节点检索rdb文件,相对安全可靠。
  1. 设计思路:
  • 从redis中获取低访问次数的Rdb文件
  • rdb文件分析
  • 根据合适的数据结构和内容,内容消耗估算等❙统计。 开源框架:https://github.com/xueqiu/rdr
  • redis内存估算

  1. 基本数据类型:sds、dict、intset、zipmap、adlist、quicklist、keylist、skip❀嗨,值是 world ,其类型为 string,其内存占用:
  • 消耗一个 dictEntry (有 2 个指针,消耗一个 int64 的内存),RedisDB 是一个大 dict,每个 kv 对都是一个条目;
  • 一个robj的消耗(有1个指针,一个int和几个使用位字段的字段,总共消耗4个字节)。 robj用于在同一个dict中存储不同类型的值。通用数据结构,全称RedisObject;
  • SDS用于存储key(存储头和字符串长度的空间+1,头长度根据字符串长度而变化),sds是Redis数据结构中字符串使用的存储;
  • 存储过期时间消耗(也存储为dictEntry,时间戳为int64);
  • SDS存储值的使用,根据数据结构的不同而不同;
  • 前四个要素是基本的。它耗尽了任何密钥的存储空间。最后一个元素根据值的数据结构而变化;

redis-cluster(redis-cluster)

  1. redis3之后,节点之间发生了完全的分片。复制(主备关注)和故障转移(故障转移)的特点
  2. 配置一致性:每个节点(Node)保存集群的配置信息,并存储在clusterState中。这是通过引入自动递增的纪元变量来实现的。使集群配置在各个节点之间保持一致
  3. 数据分区sharding
  • 将所有数据划分为16384块(槽),每个节点对应一部分槽,每个key会按照分布映射到16384个。算法 一个slot,分配算法slotId=crc16(key)%16384
  • 如果客户端访问的key不在对应节点的slot中,redis会将其返回给客户端。tMoved 命令通知您正确的路由信息​​以重新启动请求。客户端为每个请求缓存本地路由缓存信息,以便下一个请求可以直接路由到合适的节点。迁移过程中需要原始支持。主要包括两种:一种是设置节点的迁移状态,即迁移钱指示源节点和目的节点;另一个是关键迁移原子命令
  1. failover故障转移
  • 调试:两个节点切换TCP。保持联系并定期进行 PING 和 PONG 互动。如果对方的PONG响应过期没有收到,则进入PFAIL状态,转发给其他节点。
  • 错误确认:如果集群中有一半以上的节点响应某项。如果PFAIL状态被确认,则会转为FAIL状态,以确认其失败。
  • 从机选择:当主机死亡时,从机会重新选择新的主机。主要是根据每个slave最后一次同步master信息的时间。从站的数据越新,选择优先级越高,被选择的可能性就越大。选择成功后,将消息转发给其他节点。
  1. 集群不可用时的情况:
  • 集群任意master挂掉,当前master没有slave。
  • 超过一半的集群主节点已经死亡。

普通哈希算法与一致性哈希算法对比

  1. 普通哈希:又称硬哈希,采用简单的取模方法对机器进行哈希,可以在缓存环境不变的情况下进行。获得了满意的结果,但是当缓存环境动态变化时,这种静态取模方法显然不能满足单调性要求(当添加或删除一台机器时,几乎所有缓存内容都必须重新分配到另一个缓冲区)。
  2. 一致哈希:按照相同的哈希算法将机器节点和键值分配到0-2^32个环上。当接收到对缓存的写请求时,计算键值k对应的哈希值Hash(k)。如果该值与前一个机器节点的哈希值完全匹配,则直接将其写入该机器节点。如果没有匹配,则机器节点,然后顺时针找到下一个节点并写入。如果超过 2^32 后仍未找到匹配节点,则从 0 开始搜索(因为这是环形结构)。为了更有可能满足平衡,可以引入虚拟节点,即将一个物理节点映射到多个虚拟节点。
  3. 参考:http://blog.huanghao.me/?p=14

缓存雪崩、缓存入侵、缓存并发、缓存预热、缓存算法

  1. 可能会发生缓存雪崩,因为数据:没有加载到缓存中,或者缓存一次大面积故障,导致每次请求都去检查数据库,导致数据库CPU和内存过载,甚至关机。解决方案:
  • 增加锁数量(即限制并发数,可以使用semphore)或者设置一定数量的队列,避免缓存失效时向数据库发起大量并发请求。但这种方法会降低性能。
  • 分析用户行为,然后均匀分配错误时间。或者在过期时间上添加一个1-5分钟的随机数。
  • 如果缓存服务器出现故障,请考虑将其用作主服务器和备份服务器。
  1. 缓存穿透:指用户查询的数据在数据库中找不到,缓存中肯定也找不到。这就导致用户查询时在缓存中找不到,而不得不每次查询数据库。解决方案:
  • 如果查询数据库也为空,则直接设置一个默认值,存入缓存中,这样该值就会第二次出现在缓冲区中,而无需继续访问数据库。只需设置一个过期时间或者如果有值则替换缓存中的值。
  • 可以为key设置一些格式规则,然后在查询前过滤掉不符合规则的key。
  1. 缓存并发:如果网站并发量较高,如果缓存失效,多个进程可以同时查询DB和设置缓存。如果并发量非常高,也会给DB和缓存带来过大的压力。常见更新问题。解决方案:
  • 锁定缓存查询。如果KEY不存在,则锁定它,然后检查缓存中的DB,然后解锁;如果其他进程发现锁,则等待锁释放后返回数据或发出DB查询。
  1. 缓存预热:目标是在系统上线之前将数据加载到缓存中。解决方案:
  • 如果数据量不大,系统启动时直接加载即可。
  • 自己写一个简单的缓存预热程序。
  1. 缓存算法:
  • FIFO 算法:先进先出。基本原理:先进入缓存的数据必须先被移除。换句话说,当缓存变满时,首先进入缓存的数据必须被逐出。
  • LFU算法:LeastFrequentlyUsed,最不常用的算法。
  • LRU算法:最少使用、最近使用的算法。
  • LRU 和 LFU 的区别。 LFU算法根据数据元素在给定时间段内被使用的次数,即根据使用次数的差异来选择最少使用的数据元素。LRU是根据使用时间的差异来确定的。

使用redis实现分布式锁

  1. 主要使用命令:
  • setnx key val。当且仅当键不存在时,设置键值为val的字符串并返回1;如果 key 存在,则不执行任何操作并返回 0。
  • 到期时间限制。设置密钥的超时时间(以秒为单位)。过了这个时间锁就会自动解锁,避免死锁。
  • 删除键。删除锁
  1. 实现思路:
  • 使用setnx进行锁定。如果返回1,说明加锁成功,并设置超时,防止系统冻结并释放锁。最后擦拭即可解锁。
  • 如果需要设置超时等待时间,可以添加while循环。如果无法获取锁,则循环获取锁,超时后退出。

版权声明

本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。

发表评论:

◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。

热门