自动采集网站php源码,河南省新闻发布会最新,办宽带需要多少钱,下载app赚钱的平台Redis的设计与实现
数据结构和内部编码
type命令实际返回的就是当前键的数据结构类型#xff0c;它们分别是#xff1a;string(字符串)hash(哈希)、list(列表)、set(集合)、zset (有序集合)#xff0c;但这些只是Redis对外的数据结构。
实际上每种数据结构都有自己底层的…Redis的设计与实现
数据结构和内部编码
type命令实际返回的就是当前键的数据结构类型它们分别是string(字符串)hash(哈希)、list(列表)、set(集合)、zset (有序集合)但这些只是Redis对外的数据结构。
实际上每种数据结构都有自己底层的内部编码实现而且是多种实现这样Redis会在合适的场景选择合适的内部编码。 每种数据结构都有两种以上的内部编码实现例如list数据结构包含了linkedlist和ziplist两种内部编码。同时有些内部编码例如ziplist,可以作为多种外部数据结构的内部实现可以通过object encoding命令查询内部编码。
Redis这样设计有两个好处:
第一可以改进内部编码而对外的数据结构和命令没有影响这样一旦开发出更优秀的内部编码无需改动外部数据结构和命令例如Redis3.2提供了quicklist结合了ziplist和linkedlist两者的优势为列表类型提供了一种更为优秀的内部编码实现而对外部用户来说基本感知不到。
第二多种内部编码实现可以在不同场景下发挥各自的优势例如ziplist比较节省内存但是在列表元素比较多的情况下性能会有所下降这时候Redis会根据配置选项将列表类型的内部实现转换为linkedlist。
redisobject对象
Redis存储的所有值对象在内部定义为redisobject结构体内部结构如图所示。 Redis存储的数据都使用redis0bject来封装包括string、hash、list、set,zset在内的所有数据类型。理解redis0bject对内存优化非常有帮助下面针对每个字段做详细说明:
type字段
type字段:表示当前对象使用的数据类型Redis主要支持5种数据类型:string, hash、 list,set,zset。可以使用type { key}命令查看对象所属类型,type命令返回的是值对象类型,键都是string类型。
encoding字段
encoding 字段 :表示Redis内部编码类型,encoding在 Redis内部使用代表当前对象内部采用哪种数据结构实现。理解Redis内部编码方式对于优化内存非常重要,同一个对象采用不同的编码实现内存占用存在明显差异。
lru字段
lru字段:记录对象最后次被访问的时间,当配置了maxmemory和maxmemory-policyvolatile-lru或者allkeys-lru时用于辅助LRU算法删除键数据。可以使用object idletime {key}命令在不更新lru字段情况下查看当前键的空闲时间。 可以使用scan object idletime 命令批量查询哪些键长时间未被访问找出长时间不访问的键进行清理, 可降低内存占用。
refcount字段
refcount字段:记录当前对象被引用的次数用于通过引用次数回收内存当refcount0时可以安全回收当前对象空间。使用object refcount(key}获取当前对象引用。当对象为整数且范围在[0-9999]时Redis可以使用共享对象的方式来节省内存。
PS面试题Redis的对象垃圾回收算法-----引用计数法。
*ptr字段
*ptr字段:与对象的数据内容相关如果是整数直接存储数据;否则表示指向数据的指针。
Redis新版本字符串且长度44字节的数据字符串sds和redisobject一起分配从而只要一次内存操作即可。
PS 高并发写入场景中在条件允许的情况下建议字符串长度控制在44字节以内减少创建redisobject内存分配次数从而提高性能。 Redis中的线程和IO模型 Redis 基于 Reactor 模式开发了自己的网络事件处理器 - 文件事件处理器file event handler后文简称为 FEH而该处理器又是单线程的所以redis设计为单线程模型。
采用I/O多路复用同时监听多个socket根据socket当前执行的事件来为socket 选择对应的事件处理器。
当被监听的socket准备好执行accept、read、write、close等操作时和操作对应的文件事件就会产生这时FEH就会调用socket之前关联好的事件处理器来处理对应事件。
所以虽然FEH是单线程运行但通过I/O多路复用监听多个socket不仅实现高性能的网络通信模型又能和 Redis 服务器中其它同样单线程运行的模块交互保证了Redis内部单线程模型的简洁设计。
下面来看文件事件处理器的几个组成部分。
socket
文件事件就是对socket操作的抽象 每当一个 socket 准备好执行连接accept、read、write、close等操作时 就会产生一个文件事件。一个服务器通常会连接多个socket多个socket可能并发产生不同操作每个操作对应不同文件事件。
I/O多路复用程序
I/O 多路复用程序会负责监听多个socket。 文件事件分派器
文件事件分派器接收 I/O 多路复用程序传来的socket 并根据socket产生的事件类型 调用相应的事件处理器。
文件事件处理器
服务器会为执行不同任务的套接字关联不同的事件处理器 这些处理器是一个个函数 它们定义了某个事件发生时 服务器应该执行的动作。
Redis 为各种文件事件需求编写了多个处理器若客户端连接Redis对连接服务器的各个客户端进行应答就需要将socket映射到连接应答处理器写数据到Redis接收客户端传来的命令请求就需要映射到命令请求处理器从Redis读数据向客户端返回命令的执行结果就需要映射到命令回复处理器当主服务器和从服务器进行复制操作时 主从服务器都需要映射到特别为复制功能编写的复制处理器。
Redis6中的多线程
Redis6.0之前的版本真的是单线程吗
Redis在处理客户端的请求时包括获取 (socket 读)、解析、执行、内容返回 (socket 写) 等都由一个顺序串行的主线程处理这就是所谓的“单线程”。但如果严格来讲从Redis4.0之后并不是单线程除了主线程外它也有后台线程在处理一些较为缓慢的操作例如清理脏数据、无用连接的释放、大 key 的删除等等。
Redis6.0之前为什么一直不使用多线程
官方曾做过类似问题的回复使用Redis时几乎不存在CPU成为瓶颈的情况 Redis主要受限于内存和网络。例如在一个普通的Linux系统上Redis通过使用pipelining每秒可以处理100万个请求所以如果应用程序主要使用O(N)或O(log(N))的命令它几乎不会占用太多CPU。
使用了单线程后可维护性高。多线程模型虽然在某些方面表现优异但是它却引入了程序执行顺序的不确定性带来了并发读写的一系列问题增加了系统复杂度、同时可能存在线程切换、甚至加锁解锁、死锁造成的性能损耗。Redis通过AE事件模型以及IO多路复用等技术处理性能非常高因此没有必要使用多线程。单线程机制使得 Redis 内部实现的复杂度大大降低Hash 的惰性 Rehash、Lpush 等等,“线程不安全” 的命令都可以无锁进行。
Redis6.0为什么要引入多线程呢
Redis将所有数据放在内存中内存的响应时长大约为100纳秒对于小数据包Redis服务器可以处理80,000到100,000 QPS这也是Redis处理的极限了对于80%的公司来说单线程的Redis已经足够使用了。
但随着越来越复杂的业务场景有些公司动不动就上亿的交易量因此需要更大的QPS。常见的解决方案是在分布式架构中对数据进行分区并采用多个服务器但该方案有非常大的缺点例如要管理的Redis服务器太多维护代价大某些适用于单个Redis服务器的命令不适用于数据分区数据分区无法解决热点读/写问题数据偏斜重新分配和放大/缩小变得更加复杂等等。
所以总结起来redis支持多线程主要就是两个原因
• 可以充分利用服务器 CPU 资源目前主线程只能利用一个核
• 多线程任务可以分摊 Redis 同步 IO 读写负荷
Redis6.0默认是否开启了多线程
Redis6.0的多线程默认是禁用的只使用主线程。如需开启需要修改redis.conf配置文件io-threads-do-reads yes 开启多线程后还需要设置线程数否则是不生效的。同样修改redis.conf配置文件
关于线程数的设置官方有一个建议4核的机器建议设置为2或3个线程8核的建议设置为6个线程线程数一定要小于机器核数。还需要注意的是线程数并不是越大越好官方认为超过了8个基本就没什么意义了。
Redis6.0采用多线程后性能的提升效果如何
Redis 作者 antirez 在 RedisConf 2019分享时曾提到Redis 6 引入的多线程 IO 特性对性能提升至少是一倍以上。国内也有大牛曾使用unstable版本在阿里云esc进行过测试GET/SET 命令在4线程 IO时性能相比单线程是几乎是翻倍了。如果开启多线程至少要4核的机器且Redis实例已经占用相当大的CPU耗时的时候才建议采用否则使用多线程没有意义。
缓存淘汰算法
当 Redis 内存超出物理内存限制时内存的数据会开始和磁盘产生频繁的交换 (swap)。交换会让 Redis 的性能急剧下降对于访问量比较频繁的 Redis 来说这样龟速的存取效率基本上等于不可用。
maxmemory
在生产环境中我们是不允许 Redis 出现交换行为的为了限制最大使用内存Redis 提供了配置参数 maxmemory 来限制内存超出期望大小。
当实际内存超出 maxmemory 时Redis 提供了几种可选策略(maxmemory-policy) 来让用户自己决定该如何腾出新的空间以继续提供读写服务。 Noeviction
noeviction 不会继续服务写请求 (DEL 请求可以继续服务)读请求可以继续进行。这样可以保证不会丢失数据但是会让线上的业务不能持续进行。这是默认的淘汰策略。
volatile-lru
volatile-lru 尝试淘汰设置了过期时间的 key最少使用的 key 优先被淘汰。没有设置过期时间的 key 不会被淘汰这样可以保证需要持久化的数据不会突然丢失。
volatile-ttl
volatile-ttl 跟上面一样除了淘汰的策略不是 LRU而是 key 的剩余寿命 ttl 的值ttl 越小越优先被淘汰。
volatile-random
volatile-random 跟上面一样不过淘汰的 key 是过期 key 集合中随机的 key。
allkeys-lru
allkeys-lru 区别于volatile-lru这个策略要淘汰的 key 对象是全体的 key 集合而不只是过期的 key 集合。这意味着没有设置过期时间的 key 也会被淘汰。
allkeys-random
allkeys-random跟上面一样不过淘汰的策略是随机的 key。
volatile-xxx 策略只会针对带过期时间的key 进行淘汰allkeys-xxx 策略会对所有的 key 进行淘汰。如果你只是拿 Redis 做缓存那应该使用 allkeys-xxx客户端写缓存时不必携带过期时间。如果你还想同时使用 Redis 的持久化功能那就使用 volatile-xxx 策略这样可以保留没有设置过期时间的 key它们是永久的 key 不会被 LRU 算法淘汰。
LRU 算法
实现 LRU 算法除了需要key/value 字典外还需要附加一个链表链表中的元素按照一定的顺序进行排列。当空间满的时候会踢掉链表尾部的元素。当字典的某个元素被访问时它在链表中的位置会被移动到表头。所以链表的元素排列顺序就是元素最近被访问的时间顺序。
位于链表尾部的元素就是不被重用的元素所以会被踢掉。位于表头的元素就是最近刚刚被人用过的元素所以暂时不会被踢。 近似 LRU 算法
Redis 使用的是一种近似 LRU 算法它跟 LRU 算法还不太一样。之所以不使用 LRU 算法是因为需要消耗大量的额外的内存需要对现有的数据结构进行较大的改造。近似
LRU 算法则很简单在现有数据结构的基础上使用随机采样法来淘汰元素能达到和 LRU 算法非常近似的效果。Redis 为实现近似 LRU 算法它给每个 key 增加了一个额外的小字段这个字段的长度是 24 个 bit也就是最后一次被访问的时间戳。
当 Redis 执行写操作时发现内存超出maxmemory就会执行一次 LRU 淘汰算法。这个算法也很简单就是随机采样出 5(可以配置maxmemory-samples) 个 key然后淘汰掉最旧的 key如果淘汰后内存还是超出 maxmemory那就继续随机采样淘汰直到内存低于 maxmemory 为止。 如何采样就是看maxmemory-policy 的配置如果是 allkeys 就是从所有的 key 字典中随机如果是 volatile 就从带过期时间的 key 字典中随机。每次采样多少个 key 看的是 maxmemory_samples 的配置默认为 5。
采样数量越大近似 LRU 算法的效果越接近严格LRU 算法。
同时 Redis3.0 在算法中增加了淘汰池新算法会维护一个候选池大小为16池中的数据根据访问时间进行排序第一次随机选取的key都会放入池中随后每次随机选取的key只有在访问时间小于池中最小的时间才会放入池中直到候选池被放满。当放满后如果有新的key需要放入则将池中最后访问时间最大最近被访问的移除。进一步提升了近似 LRU 算法的效果。
Redis维护了一个24位时钟可以简单理解为当前系统的时间戳每隔一定时间会更新这个时钟。每个key对象内部同样维护了一个24位的时钟当新增key对象的时候会把系统的时钟赋值到这个内部对象时钟。比如我现在要进行LRU那么首先拿到当前的全局时钟然后再找到内部时钟与全局时钟距离时间最久的差最大进行淘汰这里值得注意的是全局时钟只有24位按秒为单位来表示才能存储194天所以可能会出现key的时钟大于全局时钟的情况如果这种情况出现那么就两个相加而不是相减来求最久的key。
LFU算法
LFU算法是Redis4.0里面新加的一种淘汰策略。它的全称是Least Frequently Used它的核心思想是根据key的最近被访问的频率进行淘汰很少被访问的优先被淘汰被访问的多的则被留下来。
LFU算法能更好的表示一个key被访问的热度。假如你使用的是LRU算法一个key很久没有被访问到只刚刚是偶尔被访问了一次那么它就被认为是热点数据不会被淘汰而有些key将来是很有可能被访问到的则被淘汰了。如果使用LFU算法则不会出现这种情况因为使用一次并不会使一个key成为热点数据。LFU原理使用计数器来对key进行排序每次key被访问的时候计数器增大。计数器越大可以约等于访问越频繁。具有相同引用计数的数据块则按照时间排序。
LFU一共有两种策略
volatile-lfu在设置了过期时间的key中使用LFU算法淘汰key
allkeys-lfu在所有的key中使用LFU算法淘汰数据
LFU把原来的key对象的内部时钟的24位分成两部分前16位ldt还代表时钟后8位logc代表一个计数器。
logc是8个 bit用来存储访问频次因为8个 bit能表示的最大整数值为255存储频次肯定远远不够所以这8个 bit存储的是频次的对数值并且这个值还会随时间衰减如果它的值比较小那么就很容易被回收。为了确保新创建的对象不被回收新对象的这8个bit会被初始化为一个大于零的值LFU INIT_VAL默认是5。
ldt是16个bit用来存储上一次 logc的更新时间。因为只有16个 bit所精度不可能很高。它取的是分钟时间戳对2的16次方进行取模。
ldt的值和LRU模式的lru字段不一样的地方是, ldt不是在对象被访问时更新的,而是在Redis 的淘汰逻辑进行时进行更新淘汰逻辑只会在内存达到 maxmemory 的设置时才会触发在每一个指令的执行之前都会触发。每次淘汰都是采用随机策略随机挑选若干个 key更新这个 key 的“热度”淘汰掉“热度”最低的key。因为Redis采用的是随机算法如果 key比较多的话那么ldt更新得可能会比较慢。不过既然它是分钟级别的精度也没有必要更新得过于频繁。
ldt更新的同时也会一同衰减logc的值。
为什么 Redis 要缓存系统时间戳
我们平时使用系统时间戳时常常是不假思索地使用System.currentTimeInMillis或者time.time()来获取系统的毫秒时间戳。Redis不能这样因为每一次获取系统时间戳都是一次系统调用系统调用相对来说是比较费时间的作为单线程的Redis承受不起所以它需要对时间进行缓存由一个定时任务每毫秒更新一次时间缓存获取时间都是从缓存中直接拿。
过期策略和惰性删除
过期
Redis 所有的数据结构都可以设置过期时间时间一到就会自动删除。但是会不会因为同一时间太多的key 过期以至于忙不过来。同时因为Redis 是单线程的删除的时间也会占用线程的处理时间如果删除的太过于繁忙会不会导致线上读写指令出现卡顿。
过期的 key 集合
redis 会将每个设置了过期时间的 key 放入到一个独立的字典中以后会定时遍历这个字典来删除到期的 key。除了定时遍历之外它还会使用惰性策略来删除过期的 key所谓惰性策略就是在客户端访问这个 key 的时候redis 对 key 的过期时间进行检查如果过期了就立即删除。定时删除是集中处理惰性删除是零散处理。
定时扫描策略
Redis 默认会每秒进行十次过期扫描过期扫描不会遍历过期字典中所有的 key而是采用了一种简单的贪心策略。
1、从过期字典中随机 20 个 key
2、删除这 20 个 key 中已经过期的 key
3、如果过期的 key 比率超过 1/4那就重复步骤 1
设想一个大型的 Redis 实例中所有的 key 在同一时间过期了会出现怎样的结果
毫无疑问Redis 会持续扫描过期字典 (循环多次)直到过期字典中过期的 key 变得稀疏才会停止 (循环次数明显下降)。这就会导致线上读写请求出现明显的卡顿现象。导致这种卡顿的另外一种原因是内存管理器需要频繁回收内存页这也会产生一定的 CPU 消耗。
所以业务开发人员一定要注意过期时间如果有大批量的 key 过期要给过期时间设置一个随机范围而不能全部在同一时间过期。
从库的过期策略
从库不会进行过期扫描从库对过期的处理是被动的。主库在 key 到期时会在 AOF 文件里增加一条 del 指令同步到所有的从库从库通过执行这条 del 指令来删除过期的 key。
因为指令同步是异步进行的所以主库过期的 key 的 del 指令没有及时同步到从库的话会出现主从数据的不一致主库没有的数据在从库里还存在比如上一节的集群环境分布式锁的算法漏洞就是因为这个同步延迟产生的。
惰性删除
所谓惰性策略就是在客户端访问这个key的时候redis对key的过期时间进行检查如果过期了就立即删除不会给你返回任何东西。
定期删除可能会导致很多过期key到了时间并没有被删除掉。所以就有了惰性删除。假如你的过期 key靠定期删除没有被删除掉还停留在内存里除非你的系统去查一下那个 key才会被redis给删除掉。这就是所谓的惰性删除即当你主动去查过期的key时,如果发现key过期了,就立即进行删除,不返回任何东西.
总结定期删除是集中处理惰性删除是零散处理。
lazyfree
使用 DEL 命令删除体积较大的键 又或者在使用 FLUSHDB 和 FLUSHALL 删除包含大量键的数据库时造成redis阻塞的情况另外redis在清理过期数据和淘汰内存超限的数据时如果碰巧撞到了大体积的键也会造成服务器阻塞。
为了解决以上问题 redis 4.0 引入了lazyfree的机制它可以将删除键或数据库的操作放在后台线程里执行 从而尽可能地避免服务器阻塞。
lazyfree的原理不难想象就是在删除对象时只是进行逻辑删除然后把对象丢给后台让后台线程去执行真正的destruct避免由于对象体积过大而造成阻塞。redis的lazyfree实现即是如此下面我们由几个命令来介绍下lazyfree的实现。
4.0 版本引入了 unlink 指令它能对删除操作进行懒处理丢给后台线程来异步回收内存。
UNLINK的实现中首先会清除过期时间然后调用dictUnlink把要删除的对象从数据库字典摘除再判断下对象的大小太小就没必要后台删除如果足够大就丢给后台线程最后清理下数据库字典的条目信息。
主线程将对象的引用从「大树」中摘除后会将这个 key 的内存回收操作包装成一个任务塞进异步任务队列后台线程会从这个异步队列中取任务。任务队列被主线程和异步线程同时操作所以必须是一个线程安全的队列。
Redis 提供了 flushdb 和 flushall 指令用来清空数据库这也是极其缓慢的操作。Redis 4.0 同样给这两个指令也带来了异步化在指令后面增加 async 参数就会进入后台删除逻辑。
Redis4.0 为这些删除点也带来了异步删除机制打开这些点需要额外的配置选项。 1、slave-lazy-flush 从库接受完 rdb 文件后的 flush 操作
2、lazyfree-lazy-eviction 内存达到 maxmemory 时进行淘汰
3、lazyfree-lazy-expire key 过期删除
4、lazyfree-lazy-server-del rename 指令删除 destKey