深入理解Redis,Redis 深度历险:核心原理与应用实践
摘要数据结构应用场景Redis和Memcached数据处置策略切片原理持久化原理事务事件复制原理欢迎使用Sentinel。 优秀的文章在此恭候您的光临。
Redis是一个非常快速的非关系( NoSQL )内存键值数据库,可以存储键到五个值的映射。
密钥类型只能是字符串,而值支持五种数据类型:字符串、列表、集合、哈希表和有序集合。
Redis支持许多特性,包括将内存中的数据持久化到硬盘上,通过复制扩展读取性能,以及使用分片扩展写入性能。
二.数据结构词典
dictht是哈希表结构,使用拉链方法保存散列冲突。
/* thisisourhashtablestructure.everydictionaryhastwoofthisaswe * implementincrementalrehashing,fortheoldtothenewtable.*。 unsigned long sizemask; 未锁定的使用状态; ( } dictht; typedefstructdictentry { void * key; union { void *val; uint64_t u64; int64_t s64; 双精度d; ) v; 直接条目* next; ( } dictEntry; Redis的词典dict包含两个哈希表dictht。 这是为了使rehash操作变得容易。
扩展时,将一个dictht的键值重置在另一个dictht上,完成后释放空间以交换两个dictht的角色。
typedefstructdict { dict type * type; void *privdata; dictht ht[2]; long rehashidx;/* rehashingnotinprogressifrehashidx==-1 */unsigned long iterators;/* numberofiteratorscurrentlyrunning */} dict; rehash操作采用渐进式方法,而不是一次性进行。 这是为了避免一次执行许多rehash操作给服务器带来过大的负担。
渐进式rehash是通过记录dict的rehashidx来完成的。 这从0开始,并在每次运行rehash时增加。
例如,在一次rehash中,将dict[0] rehash到dict[1],这次是从rehash将dict[0]的表[ rehash idx ]的键值转换到dict[1]的dict [0]的
每次对词典执行添加、删除、搜索或更新操作时,rehash都会执行渐进式rehash。
使用渐进式rehash时,词典中的数据会分布在两个dictht上,因此也必须在相应的dictht上执行词典搜索操作。
/* performsnstepsofincrementalrehashing.return S1 iftherearestill * keystomovefromtheoldtothenewhashtable, otherwise0is returned.* * notethatarehashingstepconsistsinmovingabucket ( thatmayhavemore * Thanonekeyasweusechaining ) ) ) ) 652 from the old to the new hash table,however * sincepartofthehashtablemaybech ) itis not * guaranteedthatthisfunctionwillrehashevenasinglebucket,since it * willvisitatmaxn * 10 emptybucketsintotal, otherwisetheamountof * workitdoeswouldbeunboundandthefunctionmayblockforalongtime.*/intdictrehash { dict * d,int n/* mand dictisrehashing(d ) )返回0; while(n–d-ht[0].used!=0) { dictEntry *de,*nextde;/* notethatrehashidxcan ‘ toverflowaswearesuretherearemore * elementsbecauseht [0].used!=0*/assert(d-ht[0].size ) unsignedlong ) d-rehashidx ); while(d-ht(0).table ) { { d-rehashidx }==null ) d-rehash idx; if (–empty_visits==0)返回1; } de=d-ht[0].table[d-rehashidx];/* moveallthekeysinthisbucketfromtheoldtothenewhashht */while ( de ) { uint64_t h; nextde=de-next;/* gettheindexinthenewhashtable */h=dict hash key ( d,de-key ) d-ht[1].sizemask; de-next=d-ht[1].table[h]; d-ht[1].table[h]=de; d-ht[0].used–; d-ht[1].used; de=nextde; } d-ht [0].table [ d-rehash idx ]=null; d-rehashidx; }/* checkifwealreadyrehashedthewholetable . */if ( d-ht [0].used==0) ) zfree ) d-ht[0].table; d-ht[0]=d-ht[1]; _dictreset(d-ht[1]; d-rehashidx=-1; 返回0; } /* More to rehash. */return 1; }跳转表
是有序集合的基础性实现之一。
可基于多个指针的有序链表来实现跳转表,并可视为多个有序链表。
要找的时候,从上位的指针开始找,找到对应的区间后再到下一个阶层。
下图显示了查找22的过程。
与红黑树等平衡木相比,跳表有以下优点。
由于不需要为了保持平衡而进行旋转等操作,所以插入速度非常快; 更容易实现; 支持无锁定操作。
三.应用场景计数器
通过对String进行自增加自减少运算,可以实现计数器功能。
像Redis这样的内存数据库读写性能非常好,适合存储经常读写的计数。
高速缓存
将热点数据放入内存,设置最大内存使用量和处置策略以保证缓存命中率。
搜索表格
例如,DNS记录适合使用Redis保存。
查找表与缓存相似,利用了Redis的快速检索特性。
但是,不能禁用查找表的内容。 因为缓存不是可靠的数据源,所以可以禁用缓存内容。
消息队列
List是一个双向链表,可以通过lpush和rpop写入和读取消息
但是,最好使用Kafka、RabbitMQ等消息中间件。
会话缓存
可以使用Redis统一存储多个APP应用程序服务器的会话信息。
如果APP应用服务器不再保存用户的会话信息,则状态将消失,单个用户可以向任何APP应用服务器请求,从而更容易实现高可用性和可扩展性。
分布式锁定的实现
在分布式方案中,不能使用独立环境中的锁定来同步多个节点上的进程。
您可以使用Redis附带的SETNX命令实现分布式锁定,也可以使用官方提供的RedLock分布式锁定实现。
其他
Set可以实现交叉、并集等操作,实现共同的朋友等功能。
ZSet实现有序操作,实现排名等功能。
四. Redis和Memcached都是非关系型存储键值数据库,主要有以下差异:
数据类型
虽然Memcached只支持字符串类型,但Redis支持五种不同的数据类型,可以更灵活地解决问题。
数据永久化
Redis支持两种持久化策略: RDB快照和AOF日志,但Memcached不支持持久化。
方差
Memcached不支持分布式,只是在客户端使用一致的散列实现分布式存储。 此方法要求客户端计算存储和查询中数据所在的节点一次。
Redis Cluster实现了分布式支持。
内存管理机制
在Redis中,并非所有数据都存储在内存中,并且可以用很久没有使用的值替换磁盘,但Memcached中的数据仍然存储在内存中。
Memcached将内存划分为特定长度的块来存储数据,从而全面解决内存碎片问题。
但是,这种方式的内存利用率不高。 例如,如果块大小为128字节,且只存储了100字节的数据,则剩下的28字节将被浪费。
五.密钥过期Redis可以为每个密钥设置过期时间,如果密钥过期,该密钥将自动删除。
对于散列表这样的容器,只能对整个密钥和整个散列表设置过期时间,而不能对密钥中的各个元素设置过期时间。
六.数据处置策略可设置最大内存使用量,当内存使用量超过时,实施数据处置策略。
Redis具体有六种处置策略。
策略说明从设置了volatile-lru过期日期的数据集中选择最近使用的数据。 从设置了volatile-ttl过期日期的数据集中选择一个设置了过期日期的数据集。 从设置了volatile-random过期日期的数据集中选择任意最近使用的数据。
作为内存数据库,从性能和内存消耗的角度来看,Redis的淘汰算法实际上并不是对所有的key,而是抽样一小部分,从中选择淘汰的key。
使用Redis缓存数据时,必须确保所有缓存数据都是热点数据,以提高缓存命中率。
您可以将最大内存使用量设置为热点数据使用的内存量,然后启用allkeys-lru销毁策略以销毁最近使用的最小数据。
Redis 4.0引入了volatile-LFU和allkeys-lfu处置策略。 lfu策略通过统计访问频率来丢弃访问频率最低的键值对。
八.持久化Redis是一种内存型数据库,为了避免关机后数据丢失,需要将内存中的数据持久化到硬盘中。
RDB持久性将某个时间点的所有数据存储在硬盘上。
您可以将快照复制到另一台服务器,以创建具有相同数据的服务器的副本。
如果系统发生故障,上次创建快照后的数据将会丢失。
数据量越大,保存快照的时间就越长。
AOF持久化在AOF文件( Append Only File )的末尾添加写入命令。
要使用AOF持久性,必须设置同步选项,以确保写入命令何时同步到磁盘文件。
这是因为,写入文件时,内容并不会立即同步到磁盘,而是由操作系统决定在同步到磁盘之前将其存储在缓冲区中的时间。
这些同步选项包括:
可选同步频率always每个写入命令同步everysec。 通过每秒同步no,操作系统可以决定何时同步
always选项会显着降低服务器性能。everysec选项是正确的,可确保在系统崩溃时只丢失1秒钟左右的数据,即使Redis每秒执行一次同步,服务器性能也很少会受到影响
随着服务器写入请求的增加,AOF文件会越来越大。
Redis提供了重写AOF的特性,并可以从AOF文件中移除冗馀写入命令。
九.事务在一个事务中包含多个命令,服务器在事务执行过程中不更改来自其他客户端的命令请求。
事务中的多个命令一次发送到服务器,而不是一个。 这种方法称为流水线,通过减少客户端和服务器之间的网络通信次数来提高性能。
Redis最简单的事务实现方法是使用MULTI和EXEC命令包围事务操作。
十.事件Redis服务器是事件驱动程序。
文件事件
服务器通过套接字与客户端或其他服务器进行通信。 文件事件是套接字操作的抽象。
Redis基于Reactor模式开发自己的网络事件处理器,使用I/O复用器同时接收多个套接字,并将到达的事件转发给文件事件调度程序。 调度程序根据套接字生成的事件类型调用相应的事件处理器。
时间事件
服务具有必须在给定时刻执行的操作。 时间事件是这种计时操作的抽象形式。
时间事件分为以下几类
计时器事件:使程序在指定的时间内运行。 定期事件:让程序每隔指定的时间运行一次。
Redis将所有时间事件放置在无序的链表中,遍历整个链表以检测到达的时间事件,然后调用相应的事件处理器。
安排和执行事件
服务器必须不断侦听文件事件套接字以获取要处理的文件事件,但不应始终侦听。 否则,时间事件无法在规定的时间内执行。 因此,拦截时间应基于最接近当前的时间事件来确定。
事件的调度和执行由aeProcessEvents函数负责,伪代码如下:
defaeprocessevents(#获取到达时间最接近当前时间的时间事件的time _ event=aesearchnearesttimer ) #最近的时间事件到达后几毫秒,remaind_ms 0 ifremaind _ ms0 :基于remaind _ ms=0# remaind _ ms的值,确定timeval timeval=create _ timeval _ with _ ms ( remaind _ ms ) 最大阻塞时间由传递的timeval决定。 aeapipoll(timeval )处理发生的所有文件事件。 procesFileEvents ) )处理所有到达的时间事件。 processTimeEvents ) )将aeProcessEvents函数放入循环中进行初始化
def main ( )初始化服务器init_server ) )处理事件,直到服务器关闭( cleaain_is_not_shutdown ) )。 aepape
十一.复制使用slaveof host port命令将一个服务变为另一个服务的从服务。
从属服务器只有一个主服务器,并且不支持主主复制。
连接进程
主服务器创建快照文件并将其发送到从服务器,记录发送过程中使用缓冲区执行的写入命令。
快照文件发送完成后,开始向从服务器发送缓冲区中存储的写入命令。从服务器中丢弃所有旧数据,加载主服务器传来的快照文件,然后从服务器中
主从链条
如果负载增加,主服务器可能无法立即更新所有从属服务器,或者重新连接从属服务器并重新同步可能会导致系统过载。
要解决此问题,请创建一个中间层来分担主服务器的复制工作。
中间层的服务器是最上层服务器的从服务器,也是最下层服务器的主服务器。
十二、SentinelSentinel (哨兵)可以监听集群中的服务器,当主服务器离线时,自动从服务器中选择新的主服务器。
十三、切片是将数据分为多个部分的方法,可以将数据存储在多台机器上。 该方法在解决某些问题时可以获得线性水平的性能提高。
假设有四个Redis实例R0、R1、R2、R3和表示用户的密钥user:1、user:2、 选择指定密钥存储在哪个实例中的方法不同。
最简单的方法是范围分片,例如,从用户id为0到0~1000的存储在实例R0中,从用户id为1001到2000的存储在实例R1中。
然而,需要这样维持映射范围表,维持操作的成本高。
另一种方式是散列片( hash tile ),可以使用CRC32散列函数将密钥转换成单个数字,通过对实例的数量进行建模来得知要存储的实例。
根据执行切片的位置,可以分为以下三种切片方法:
客户端划分:客户端使用一致性散列等算法来确定将密钥分发到哪个节点。
代理分片:将客户端请求发送到代理,代理将请求转发到正确的节点。
服务器分片: redis群集。
资料来源: https://github.com/cyc 2018/cs-notes /
您的转发关注是对笔者的最大支持,欢迎关注。
我对大厂商的架构设计、BAT等厂商的面试问题的解读、编程语言理论和网络圈的逸闻等很感兴趣。 请关注笔者。 不是这样的。 晾衣服的文章在这里。