redis内存k-v,支持多种数据结构,第一个重点在于如何操做更快和适当的节省内存,第二个重点在于分布式管理。本文redis基于3.0。第一部分将介绍全部内存数据结构实现,关注rehash的实现,对编写内存存储提供数据结构参考没什么框架,单线程,无内存池等复杂设计,基本不支持正规的ACID;还会介绍内存溢出淘汰策略,过时键删除,持久化等功能;第二部分将介绍分布式集群,redis自身的主从模式/哨兵模式/集群模式,经常使用的codis,公司自研的非开源集群。顺便说了些双机房中redis同步方案,redis应用中应优化的点以及常见的redis热key解决方案。前端
对象类型 | 编码方式 | 选择条件 | 编码详情 |
string | int | long类型整数 | ptr直接指向整数 |
---|---|---|---|
embstr动态字符串 | 长度<=44 | 数组形式组织sds,len/内存预分配/结尾有\0 | |
动态字符串 | 长度>44 | 链表形式组织sds | |
列表 | 压缩列表 | 长度<64&&元素数<512 | 数组形式组织ziplist |
双端链表 | 长度>=64&&元素数>=512 | 双端链表 | |
quicklist | 3.2版本后 | xx | |
哈希 | 压缩列表 | 长度<64&&元素数<512 | |
字典 | 长度>=64&&元素数>=512 | 两个table/若干桶 | |
集合 | 整数集合 | 元素数<512 | |
字典 | 元素数>=512 | ||
有序集合 | 压缩列表 | 长度<64&&元素数<128 | 分支最小元素/分值 |
跳表 | 长度>=64&&元素数>=128 | 字典+跳表 |
保持0仍然可使用部分C语言字符串的一些函数
Len 获取长度,保证二进制安全;
多出剩余空间,每次检查free预分配内存,杜绝缓冲区溢出,惰性释放,减小修改字符串带来的内存重分配次数node
struct sdshdr { len = 11; free = 0; buf = "hello world\0"; // buf 的实际长度为 len + 1 }; 分配内存,删除才释放 # 预分配空间足够,无须再进行空间分配 if (sdshdr.free >= required_len): return sdshdr # 计算新字符串的总长度 newlen = sdshdr.len + required_len # 若是新字符串的总长度小于 SDS_MAX_PREALLOC # 那么为字符串分配 2 倍于所需长度的空间 # 不然就分配所需长度加上 SDS_MAX_PREALLOC (1M)数量的空间 if newlen < SDS_MAX_PREALLOC: newlen *= 2 else: newlen += SDS_MAX_PREALLOC # 分配内存 newsh = zrelloc(sdshdr, sizeof(struct sdshdr)+newlen+1)
压缩列表使用特殊的编码来标识长度,再加上连续的内存,很是节约空间mysql
area |<----------------------------------------------- entry ----------------------->| size 5 byte 2 bit 6 bit 11 byte +-------------------------------------------+----------+--------+---------------+ component | pre_entry_length | encoding | length | content | | | | | | value | 11111110 00000000000000000010011101100110 | 00 | 001011 | hello world | +-------------------------------------------+----------+--------+---------------+
Pre_entry_length
1 字节:若是前一节点的长度小于 254 字节,便使用一个字节保存它的值。
5 字节:若是前一节点的长度大于等于 254 字节,那么将第 1 个字节的值设为 254 ,而后用接下来的 4 个字节保存实际长度。
encodinng/length/content
以 00 、 01 和 10 开头的字符数组的编码方式以下:redis
编码 | 编码长度 | content 部分保存的值 |
---|---|---|
00bbbbbb | 1 byte | 长度小于等于 63 字节的字符数组。 |
01bbbbbb xxxxxxxx | 2 byte | 长度小于等于 16383 字节的字符数组。 |
10____ aaaaaaaa bbbbbbbb cccccccc dddddddd | 5 byte | 长度小于等于 4294967295 的字符数组。 |
具体如何省内存:相好比双向,指针加sds的len,free结尾空,2*4+1+2*4(32位指针和Int都是4字节);压缩链表2/6字节。算法
添加节点在前面,要更新pre_entry_length,next 的 pre_entry_length 只有 1 字节长,但编码 new 的长度须要 5 字节的时候可能连锁更新。next 的 pre_entry_length 有 5 字节长,但编码 new 的长度只须要 1 字节不作处理。sql
这里的encoding是针对整个intset的。当某元素长度超过期要总体升级编码方式。全存Int所以不须要length。只会升级不会降级。升级过程:数据库
扩展内容。从后开始移动,将新值插入 bit 0 15 31 47 63 95 127 value | 1 | 2 | 3 | ? | 3 | ? | | ^ | | +-------------+ int16_t -> int32_t
相比于平衡二叉树,不须要严格的平衡,随机层数.插入和删除不须要调整性能很高查找略逊色
https://www.cl.cam.ac.uk/teac...后端
int zslRandomLevel(void) { int level = 1; while ((random()&0xFFFF) < (ZSKIPLIST_P * 0xFFFF)) //这里取小于0xffff的数,有0.25的几率level+1,所以level有1/4几率为2, 1/16的几率为3等等 level += 1; return (level<ZSKIPLIST_MAXLEVEL) ? level : ZSKIPLIST_MAXLEVEL; } ZSKIPLIST_MAXLEVEL=32 ZSKIPLIST_P=1/4 一个节点的平均层数 = 1/(1-p),Redis 每一个节点平均指针数为1.33 平均时间复杂度:O(logn)
若进行了rehash,先遍历小hash表的v & t0->sizemask索引指向的链表,再遍历大hash表中该索引rehash后的全部索引链表。
由于sizemask=sizehash-1所以低位全是1,索引取决于hashkey的低K位,
同一个节点的hashkey不变,若原来为8位hash,hashkey为…abcd,原索引计算为bcd,
扩展到16位hash,索引变为abcd,若要找出全部原bcd索引的链表,须要在新的hash中找0bcd,1bcd。
由于要循环高位,因此这样从高位到低位反向来,例如:
000 --> 100 --> 010 --> 110 --> 001 --> 101 --> 011 --> 111 --> 000
0000 --> 1000 --> 0100 --> 1100 --> 0010 --> 1010 --> 0110 --> 1110 --> 0001 --> 1001 --> 0101 --> 1101 --> 0011 --> 1011 --> 0111 --> 1111 --> 0000
当rehash时,可能会有重复,但不会有遗漏数组
do { /* Emit entries at cursor */ de = t1->table[v & m1]; while (de) { fn(privdata, de); de = de->next; } /*这里v从0开始,加1只取前m1-m0位,再与后m0位合并*/ v = (((v | m0) + 1) & ~m0) | (v & m0); /* Continue while bits covered by mask difference is non-zero */ } while (v & (m0 ^ m1)); //这里异或前m1-m0位全是1,直到while中的v为全1后加1变为全0这里为0退出。所以若原来v为110,8到32,rehash的表将遍历00110,01110,10110,11110 而后是下一个v的确认 v |= ~m0; //m0低位全是0,>m0全是1,将超出m0的置1,只保留低m0位 v = rev(v); //二进制翻转 v++; //加1,正常进位 v = rev(v); //二进制翻转,这步以后至关于将v从高位+1向低位进位
Redis 用来当作LRU cache的几种策略(使用内存已达到maxmemory):缓存
noeviction:无策略,直接返会异常
allkeys-lru:全部key进行LRU,先移除最久使用的(当前时间,减去最近访问的时间)
allkeys-random:随机移除
volatile-random:只随机移除有过时时间的key
volatile-tt: 优先移除最短ttl的有过时时间的key
近似的LRU。采样逐出(默认5个里淘汰一个)。https://redis.io/topics/lru-c...
4.0后引入LFU(least frequently):大概原理是次数达到一个阶段给个计数器初始值,随时间递减。采样取最小淘汰(源码LFULogIncr)
接收到SLAVEOF命令执行步骤:
设置masterhost,masterport 发送OK给客户端 建立socket connect到主服务器,主服务器accept 发送ping给主服务器,收到PONG继续不然断开重连 主服务器requirepass,从服务器masterauth 发送端口给主服务器 REPLCONF listening-pot <port-number> 同步SYNC/PSYNC 命令传播
1.SYNC
主服务器BGSAVE命令生成一个RDB文件,并使用缓冲区开始记录写命令
BGSAVE结束后后发送RDB文件给从服务器
从服务器载入
主服务器将和缓冲区中写命令发送给从服务器,从服务器执行
2.命令传播
主服务器将全部写命令传播给从服务器
每秒一次频率向主服务器发送REPLCONF ACK <replication_offset>进行心跳检测。检测网络和命令丢失
主服务器配置min-slaves-to-write n, min-slaves-max-lag m当从服务器数量少于3个,或者延迟大于等于10将拒绝执行写命令
根据replication_offset检测是否丢失命令,补发命令
3.断线后重复制的优化 PSYNC
2.8版本以上redis使用PSYNC命令代替SYNC,断线后使用部分重同步,其余使用SYNC
从服务器向主服务器发送命令:首次PSYNC ? -1 ,断线后重复制 PSYNC <runid> <offset>。主服务器返回:+FULLERSYNC <runid> <offset> ,+CONTINUE , -ERR没法识别从服务器重发SYNC命令
4.上面2/3都是2.8以上才支持,须要用到replication_offset,复制积压缓冲区,服务运行ID
主服务器每次向从服务器传播N个字节,将本身的复制偏移量加N。从服务器每次收到N个字节,将本身的复制偏移量加N 主服务器进行命令传播时,不只会将写命令发送给从服务器,还会将写命令写入复制积压缓冲区,先进先出 从服务器会记录正在复制的主服务器的运行ID,网络断开后,从服务器向主服务器发送这个ID,主服务器根据本身运行ID决定是部分重同步仍是彻底同步
哨兵系统也是一个或多个特殊的redis服务器,监视普通服务器,负责下线主服务器和故障转移
1.启动
(1)初始化服务器
sentinel不适用数据库,再也不如RDB/AOF
(2)将普通redis服务器使用的代码替换成sentinel专用代码
使用不一样端口,命令集(只有PING,SENTINEL,INFO.SUBSCRIBE,UNSUBSCRIBE,PSUBSCRIBE,PUNSUBSCRIBE)
(3)初始化sentinel状态
(4)根据给定的配置文件,初始化sentinel的监视主服务器列表
(5)建立连向主服务器的网络链接
命令链接,订阅链接(在创建后发送SUBSCRIBE __sentinel__:hello,sentinel需求经过接收其余服务器发来的频道信息发现未知的sentinel)
2.获取主服务器信息
sentinel默认10s一次向主服务器发INFO命令,获取更新sentinelRedisInstance的run_id,role,slaves的等
3.获取从服务器信息
sentinel会对主服务器的从服务器创建命令链接和订阅链接,也是10s/次发送INFO,更新slaves的sentinelRedisInstance
4.向主服务器和从服务器发送信息
sentinel默认2s/次用命令链接向主服务器和从服务器发送 PUBLISH __sentinel__:hello "<s_ip>,<s_port>,<s_runid>,<s_epoch>,<m_name>,<m_ip>,<m_port>,<m_epoch>"
对每一个与sentinel链接的服务器,即发送信息到频道又订阅频道接收信息。收到信息后提取参数检查如果本身的丢弃,不然根据信息更新主服务sentinelRedisInstance中的sentinels,建立链接向其余sentinel的命令链接
5.检测主观下线状态
sentinel默认1s/次的频率向全部主/从/sentinel服务器发送PING命令,有效回复为+PONG,-LOADING,-MASTERDOWN。当一个实例在down-after-milliseconds内,连续向sentinel返回无效回复,sentinel修改实例中flags加入|SRI_S_DOWN标识主观下线
6.检查客观下线状态
若是被sentinel判断为主观下线,sentinel当前配置纪元为0,将向其余sentinel发送命令 SENTINEL is-master-down-by-addr <ip> <port> <current_epoch> <runid>
返回
<down_state> <leader_runid> // *表明命令仅用于检测主服务器的下线状态 <leader_epoch> //前一个只为*则为0
当接收到认为下线的sentinel数量超过quorum(sentinel moniter 127.0.0.1 6379 2中2设置)则flags加|SRI_O_DOWN
7.选举领头Sentinel(raft)
也经过SENTINEL is-master-down-by-addr 看来是要分开进行,带runid。
每一个发现主服务器进入客观下线的sentinel向其余sentinal发送命令
在一个配置epoch中将先到的设为局部领头,不能再更改。
接收回复检查epoch的值和本身的相同就取出leader_runid,若是发现本身被半数以上选择,则成为领头,epoch+1
若是在规定时间内未选举成功,epoch+1从新选举
8.故障转移
领头进行故障转移
1) 选出新的主服务器
在线的,5s内回复过INFO的,与主服务器断开链接时间足够短,优先级高,复制偏移量大,runid最小 发送SLAVEOF no one 以1s/次(其余是10s/次)的频率向该服务器发送INFO。当role变为master时继续2
2) 向下线的主服务的其余从服务器发送SLAVEOF命令
3) 向旧的主服务器发送SLAVEOF命令
一个集群由多个node组成,经过分片进行数据共享,CLUSTER MEET <ip> <port>将各阶段加入到cluster
1.启动
一个node就是运行在集群模式下的redis服务器,在启动时若cluster-enabled是yes,则开启服务器的集群模式。
节点继续使用单机模式的服务器组件,只是serverCron函数会调用集群模式特有的clusterCron函数,执行集群的常规操做,例如向集群的其余节点发送Gossip消息,检查节点是否断线,或者检查是否须要对下线的节点进行故障转移操做等。节点数据库和单机彻底相同除了智能使用0号出具库这和个限制,另外除了将键值对保存在数据库里边以外,节点还会用clusterState中的slots_to_keys跳跃表来保存槽和键,方便对属于某槽全部数据库键进行批量操做
2.客户点向A发送CLASTER MEET <B.ip> <B.port>
A建立B的clusterNode加入到clusterState.nodes中
发送MEET给B
B返回PONG
A发送PING,握手完成
A将B的信息经过Gossip传播给急群众其余节点
3.槽指派,向节点发送CLUSTER ADDSLOTS <slot> [slot ...]
遍历全部输入槽,若是有已经指派的返回错误,若是都没有指派,再遍历一次:
更新当前lusterState.slots[i]设为Myself
更新本身clusterNode 的slots,numslots属性
将本身的slots数组经过消息发送给集群中其余节点,A收到B后会把本身的clusterState.nodes中查找B对应的clusterNode结构,更新其中的slots数组;更新clusterNode中的slots,numslots属性
维护总体slots目的:查某个槽被哪一个节点处理
维护单个节点slots目的:将某节点的全部槽指派信息发送给其余。
4.执行命令
在全部的槽都指派完毕以后,集群就会进入上线状态,这是客户端就能够向集群中的节点发送数据命令了。客户端向节点发送与数据库键相关的命令时,若是键所在的槽正好就指派给了当前节点,那么节点就直接执行命令;若是键所在的槽并无指派给当前节点,那么节点返回一个MOVED错误,指引客户端(redirect)至正确节点,并再次发送以前想要执行的命令。
1)计算键属于哪一个槽 CLUSTER KEYSLOT [key]
CRC16(KEY) & 16383
2) 若计算的i不对应Myself 返回MOVED <slot> <ip>:<port>
3) 客户端根据MOVED错误,转向节点从新发送命令
5.从新分片
redis集群的从新分片操做能够将任意数量已经指派给某个节点的槽改成指派给另外一个节点,而且相关的槽所属的键值对也会从源节点转移到目标节点。能够online下。
redis的从新分片操做时由redis的集群管理软件redis-trib负责执行的,redis提供了进从新分片所需的全部命令,而redis-trib则经过向源节点和目标节点发送命令来进行从新分片操做。步骤以下:
1)redis-trib对目标节点发送CLUSTER SETLOT < slot > IMPORTING < source_id> 准备好导入
2)redis-trib对源节点发送CLUSTER SETLOT < slot> MIGRATING < target_id > 准备好迁移
3)redis-trib对源节点发送CLUSTER GETKEYSINSLOT < slot > < count > 得到最多count个属于槽slot的键值对的键名
4)对3中每一个键名,redis-trib对源节点发送MIGRATE < key_name> 0 < timeout> 迁移
5)重复3和4,知道槽中的键值对迁移到目标节点
6)redis-trib向任意节点发送CLUSTER SETLOT < slot> NODE < target_id>,将槽指派给目标节点,并经过消息告知整个集群,最终全部节点都会知道槽slot已经指派给了目标节点。
6.ASK错误 处理正在迁移中槽错误
接到ASK错误的客户端会根据错误提供的IP地址和端口号,转向至正在导入槽的目标节点,而后向目标节点发送一个ASKING命令,再从新发送本来想要执行的命令。
ASKING命令加client.flags|=REDIS_ASKING。正常客户端发送一个关于槽i的命令,而槽i又没有指派给这个节点的话,会返回MOVED错误,设置了REDIS_ASKING后,则会破例执行
MOVED错误表明槽的负责权已经从一个节点转移到另外一个,每次遇到都自动发到MOVED指向的节点。而ASK只是迁移槽中临时的,下次对下次有影响
7.复制与故障转移
1)复制
redis集群中的节点分为主节点和从节点,其中主节点用于处理槽,而从节点则用于复制主节点,并在被复制的主节点下线以后代替下线的主节点继续处理命令请求。
设置从节点:CLUSTER REPLICATE < node_id> 让接收命令的节点成为node_id的从节点
接收到该命令的节点首先会在本身的clusterState.nodes字典里面找到node_id对应的节点clusterNode结构,并将本身的clusterState.myself.slaveof指针指向这个结构;
节点会修改本身clusterState.myself.flags中的属性,关闭原来的REDIS_NODE_MASTER标识,打开REDIS_NODE_SLAVE标识;
调用复制代码,至关于向从节点发送SLAVEOF <master_ip> <master_port>。
2)故障检测
集群中的每一个节点都会按期地向集群中的其余节点发送PING消息,若是规定时间内没有返回PONG,发送消息的节点就会把接受消息的节点标记为疑似下线PFAIL。clusterNode的flags标识(REDIS_NODE_PFAIL)
集群中各节点经过互相发送消息的方式交换集群中各个节点的状态信息,当A经过消息得知B认为C进入疑似下线,A在本身clusterState.nodes中找到C对应的clusterNode结构将B的下线报告添加到该clusterNode的fail_reports中
半数以上主节点都报告x意思下线,则标记为FAIL,将主节点x标记为下线的节点向集群广播FAIL消息,全部接受者都将x标记为FAIL
3)故障转移
当一个从节点发现本身复制的主节点进入了下线状态的时候,从节点将开始对下线主节点进行故障转移,步骤以下:
选举新的主节点 新的主节点执行SLAVEOF no one命令,成为新的主节点 新的主节点将下线主节点的槽指派给本身 新的主节点向集群广播PONG消息,代表本身接管了原来下线节点的槽 新的节点开始接收和本身复制处理槽有关的命令请求。
选举新的主节点
一样基于Raft实现 从节点广播CLUSTERMSG_TYPE_FAILOVER_AUTH_REQUEST,未投过票的主节点返回CLUSTERMSG_TYPE_FAILOVER_AUTH_ACK。配置纪元自增,半数以上。
8.消息
消息由消息头(header)和消息正文(data)组成。
cluster.h/clusterMsg结构表示消息头,cluster.h/clusterMsgData联合体指向消息的正文。
节点消息分为5类:
1)MEET
A接到客户端发送的CLUSTER MEET B命令后,会向B发送MEET消息,B加入到A当前所处的集群里
2)PING
每一个节点默认1s/次从已知节点随机选5个,对最长时间未发送PING的节点发送PING,或当有节点超过cluster-node-timeout的一半未收到PONG也发送PING,检查节点是否在线
3)PONG
确认MEET,PING;或主动发送让集群中其余节点当即刷新该节点信息,好比故障转移操做成功后
以上三种消息都使用Gossip协议交换各自不一样节点的信息,三种消息的正文都是由两个cluster.h/clusterMsgDataGossip结构组成
发送者从本身已知节点列表中随机选择两个节点(主、从),保存在两个clusterMsgDataGossip结构中。接收者发现节点不在已知节点列表则与节点握手,不然更新信息。注意PONG也会带两个回去
4)FAIL
主节点判断FAIL状态,广播
clusterMsgDataFail。(gossip随机会慢)
5)PUBLISH
当节点收到一个PUBLISH,会执行这个命令并向集群中广播一条PUBLISH。即向集群中某个节点发送PUBLISH <channel> <message>将致使集群中全部节点都向channel频道发送message消息。
要让集群全部节点都执行相同命令,能够广播,但还要用PUBLISH发是由于直接广播这种作法,不符合redis集群的“各个节点经过发送和接收消息来进行通讯”这一规则。
clusterMsgDataPublish
原生Gossip过程是由种子节点发起,当一个种子节点有状态须要更新到网络中的其余节点时,它会随机的选择周围几个节点散播消息,收到消息的节点也会重复该过程,直至最终网络中全部的节点都收到了消息。这个过程可能须要必定的时间,因为不能保证某个时刻全部节点都收到消息,可是理论上最终全部节点都会收到消息,所以它是一个最终一致性协议。每次散播消息都选择还没有发送过的节点进行散播(有冗余)
介绍codis的架构组件,可用性,一致性,扩展性
Codis 3.x 由如下组件组成:
1.使用自动负载均衡须要知足一个前提:全部codis-server的分组master必须配置maxmemory。
2.各组codis-server分配多少个slot是由其maxmemory决定。好比:A组maxmemory为10G, B组maxmory为1G,进行自动均衡处理后,A组分配的slot会是B组的10倍。
3.自动负载均衡并不会达到绝对意义上的均衡,其只作到maxmemory与分配的slot个数的比例均衡。没法达到操做次数的均衡。
4.自动负载均衡的处理过程当中,若是发现存在maxmemory与分配的slot个数比例不均衡时,则会进行发起slot迁移的操做。达到均衡目的的前提下,此过程当中会作到尽可能减小slot的迁移。
codis和twemproxy最大的区别有两个:
一个是codis支持动态水平扩展,对client彻底透明不影响服务的状况下能够完成增减redis实例的操做;
一个是codis是用go语言写的并支持多线程而twemproxy用C并只用单线程。
后者又意味着:codis在多核机器上的性能会好于twemproxy;codis的最坏响应时间可能会由于GC的STW而变大,不过go1.5发布后会显著下降STW的时间;若是只用一个CPU的话go语言的性能不如C,所以在一些短链接而非长链接的场景中,整个系统的瓶颈可能变成accept新tcp链接的速度,这时codis的性能可能会差于twemproxy。
数据量的限制。1024. 迁移比想象的频繁 zk依赖,zk出问题,路由错误没法发现,redis没有路由信息
方案1:
方案2:
codis收到命令后发送给两个机房的redis
方案3:
7.redis影响性能的命令:(执行时间长,传输数据多)
key*,sort(非要单独机器
smembers 控制集合的数量,分子集,srandmember,
save bgsave/afo启动时/master首次收到slave同步请求等时fork进程(fork时虽然数据写时复制,但仍是会复制页表,大页能够减小页表,但改就会复制)
appendfsync everysec 子进程持久化和主进程 IO阻塞
bgrewriteaof AOF buffer和文件合并时阻塞的
问题:请求多,部分key集中于同一机器,没法经过增长机器解决,源于redis的从都是备份恢复做用,codis等集群也是
解决方案:
1.同时刻只有一个获取锁,2.多数节点可用则能够获取锁,3.不会死锁,4.锁按期间有效1/2 :同时多个master上请求锁,超过一半则成功3:master有时间自动释放,监控每隔时间检查释放等4:释放锁须要密钥,保证不释放别人的锁步骤:一、获取当前时间(单位是毫秒)。二、轮流用相同的key和随机值在N个节点上请求锁,在这一步里,客户端在每一个master上请求锁时,会有一个和总的锁释放时间相比小的多的超时时间。好比若是锁自动释放时间是10秒钟,那每一个节点锁请求的超时时间多是5-50毫秒的范围,这个能够防止一个客户端在某个宕掉的master节点上阻塞过长时间,若是一个master节点不可用了,咱们应该尽快尝试下一个master节点。三、客户端计算第二步中获取锁所花的时间,只有当客户端在大多数master节点上成功获取了锁(在这里是3个),并且总共消耗的时间不超过锁释放时间,这个锁就认为是获取成功了。四、若是锁获取成功了,那如今锁自动释放时间就是最初的锁释放时间减去以前获取锁所消耗的时间。五、若是锁获取失败了,无论是由于获取成功的锁不超过一半(N/2+1)仍是由于总消耗时间超过了锁释放时间,客户端都会到每一个master节点上释放锁,即使是那些他认为没有获取成功的锁。