redis设计与实现笔记

简介

1.Remote Dictionary Server(Redis) 基于键值的内存数据库
2.五种数据类型
字符串(String) 列表(list) 哈希(Map) 集合(sets) 有序集合(sorted sets)
其他 基于字符串(bitMaps位图,HyperLoglog超小内存唯一值计数),基于集合(GEO地理信息定位)
3.redis组成
redis-server/redis-cli
4.特性
官方宣称单机10wOPS,单线程,支持持久化(RDB/AOF)
支持发布订阅/lua脚本/事务/pipeline/主从复制
默认有16个库 使用0号库 使用select 0-15 切换库
高可用 redis-sentinel
分布式 redis-cluster
5.应用场景
1)缓存系统 2)计数器 3)消息队列 4)排行榜 5)社交网络 6)实时系统
动静分离时,不同浏览器对单个域名同时开启的下载线程数量是有限制的,可以使用不同的域名引入js/css等文件。
一致性hash算法 环偏斜:虚拟节点
缓存
    进程内缓存 ehcache
    进程外缓存 redis/memcache
    进程内缓存是指将数据缓存在站点或进程内部,可以防止直接访问数据库,节省带宽,降低访问延迟。
    但进程内缓存数据一致性很难保证,适用于允许数据不一致的场景,在秒杀等高并发场景下,可以防止透传后端压力过大。
6.redis事务
redis事务是一组命令,redis事务保证这些命令被执行时,中间不会被任何其他操作打断。
MULTI开启事务/EXEC提交事务

乐观锁一般通过版本号机制实现
redis watch机制与乐观锁
redis可以设置watch监视一个或多个key,如果在事务exec执行之前这个key被其他命令修改了,
那么事务将被打断
7.主从复制
通过复制replication方式实现主从
哨兵Sentinel高可用方案
Sentinel会在Master故障时,将一台slave提升为Master,实现故障转移
8.redis安全
设置密码/绑定Ip/命令禁止或重命名/修改默认端口

一、redis基本数据结构

1.SDS(Simple dynamic string)简单动态字符串
主要用来保存字符串,以及用作缓冲区
SDS结构中保存有字符串长度,所以获取字符串长度的时间复杂度为O(1).
SDS会进行空间预分配,以减少内存重分配次数 如果SDS长度小于1M每次预扩充一倍空间,如果大于等于1M,每次扩充1M空间。
多分配的未使用内存保存在惰性空间内,必要时可以调用API释放
2.链表
reids提供了双端,无环,带表头指针和表尾指针的链表
3.字典
redis的字典使用哈希表作为底层实现,使用MurmurHash2算法计算hash值
负载因子 = 哈希表已保存节点数量/hash表大小
redis的字典包含连个哈希表,一个平时使用,一个rehash时使用
哈希表的实质是数组+链表
对哈希表进行扩展或者收缩时,程序需要将现有哈希表中的值rehash到新哈希表中,
rehash过程不是一次完成,而是渐进式完成。
4.跳跃表
跳跃表是一种有序数据结构,他通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的。
支持平均O(logN),最坏O(N)复杂度的节点查找,大部分情况下跳跃表的效率与平衡二叉树媲美。
跳跃表是有序集合的底层实现之一。
redis中每个跳跃表节点的层高都是1~32之间的随机数
5.整数集合(intset)
整数集合是集合键的底层实现之一,当一个集合只包含整数值元素,并且元素数量不多时,redis
会使用整数集合作为集合键的底层实现。
整数集合可以保证集合中不会出现重复元素。
整数集合的底层实现为数组,这个数组以有序、无重复的方式保存集合元素。
6.压缩列表(ziplist)
压缩列表是列表键和哈希键的底层实现之一。当一个列表键只包含少量列表项,并且列表项为
小整数或短字符串时,redis会使用压缩列表做列表键底层实现。
压缩列表可以节约内存,是一种顺序型数据结构。
向压缩列表添加或删除节点可能会引发连锁更新,但几率不大。
7.对象
字符串(String) 列表(list) 哈希(Map) 集合(sets) 有序集合(sorted sets)
其他 基于字符串(bitMaps位图,HyperLoglog超小内存唯一值计数),基于集合(GEO地理信息定位)
1)字符串对象
编码可以是int/embstr/raw
当字符串中保存的是整数值,并且可以用long表示时,字符串的编码为int
当字符串长度小于32时,编码为embstr,大于32时,为raw
embstr:embstr字符串对象的所有数据都保存在一块连续的内存中,效率更高。将内存分配/释放次数调用次数从2次简化为1次。
2)列表对象
编码可以是ziplist或linkedlist
当列表对象保存的元素数量小于512个,且元素长度都小于64字节,使用ziplist优化。
3)哈希对象
编码可以是ziplist或lhashtable
ziplist底层实现的hash表按照键-值-键-值的紧密顺序排列。
当对象保存的元素数量小于512个,且键和值长度都小于64字节,使用hashtable优化。
4)集合对象
编码可以是intset或hashtable intset使用整数集合作为底层实现
当集合对象保存的所有元素都是整数值,数量小于512个时,使用hashtable。
5)有序集合对象
编码可以是ziplist或skiplist
当对象保存的元素数量小于128个,且元素长度都小于64字节,使用ziplist优化。
6)内存回收
redis使用引用计数,进行内存回收和对象共享
redis初始化时默认配置会生成0-9999的整数字符串对象,并共享这些对象。
redis只支持对整数字符串进行共享。不支持复杂对象的共享,以节约cpu时间。
对象会记录自己的最后一次被访问时间。

二、数据库结构

1.默认有16个库 使用0号库 使用select 0-15 切换库
2.redis数据库主要由dict和expires两个字典构成,dict负责保存键值对,
expires负责保存键的过期时间,过期字典的键是对象的指针,值是过期时间
3.读写键时的数据库操作
1)读取一个键后,服务器会更新键空间命中(hit)/不命中(miss)次数。
2)读取一个键后,服务器会更新键的LRU,最后一次访问时间。
3)会判断键是否过期,如果过期会删除这个键。
4)判断键上是否有watch事件,有的话会将标记位设为脏(dirty)
5)每次修改一个键后,都会对脏(dirty)键计数器+1,这个计数器会触发持久化和复制操作。
6)如果服务器开启了服务器通知,会发送服务器通知。
4.过期策略
1)可以使用EXPIRE PEXPIRE EXPIREAT PEXPIREAT命令设置过期时间。
使用PERSIST删除过期时间。
使用TTL/PTTL返回剩余秒/毫秒数。
2)过期删除策略
惰性删除 当从键空间获取键时,都检查取得的键是否过期,过期就删除。
定期删除 
从数据库的expires字典中随机检查一部分键的过期时间,删除过期的键。
3)AOF/RDB中的过期键策略
a.已过期的键不会保存到RDB文件中。
主从架构中的主服务器载入RDB文件时,不会载入过期键。
主从架构中的从服务器载入RDB文件时,会载入过期键。
b.AOF文件会记录命令,过期键被删除后,会记载DEL命令。
AOF重写时,过期键不会写入到新文件中。
c.复制 主从复制架构中,从服务器不会删除过期键,由主服务同步控制删除。
5.数据库通知
redis支持键空间通知(键执行了什么命令)和键事件通知(某个命令被什么键执行了)
6.RDB持久化
redis 执行save/bgsave命令生成RDB文件。
save命令会阻塞Redis服务器,直到RDB文件创建完毕。
bgsave命令会派生出子进程,由子进程异步创建RDB文件。
RDB文件会在服务器启动时自动载入,但如果服务器开启了AOF持久化功能,服务器会优先使用AOF文件
还原数据库状态。
RDB文件可以通过设置save配置选项,自动间隔性保存。
RDB文件开头是"REDIS"字符串魔数,之后是版本号。
7.AOF持久化
AOF文件会记录redis服务器所执行的写命令,追加到文件末尾
AOF文件支持always/everysec/no持久化行为,类似于mysql
AOF重写 redis会分析内存中的数据,并生成新的aof文件,AOF重写与旧的AOF文件无关。
reids通过子进程进行AOF重写,重写期间的新操作会记录到AOF重写缓冲,完成重写后,
redis会调用信号处理函数,这个操作会阻塞主线程,信号处理函数会将AOF重写缓冲追加到
新的AOF文件末尾,并通过文件重命名方式完成新旧AOF文件的切换。

三、HA实现

1.复制
1)用户可以通过slaveof命令或选项,设置主从服务器,让一个服务器去复制另一个服务器。
主从服务器互为对方的客户端。
2)redis的复制功能分完整重同步,部分重同步,命令传播。
完整重同步
a.从服务器向主服务器发送PSYNC命令;
b.主服务器执行BGSAVE命令,生成RDB文件,并将此期间的所有写命令记录到缓冲区中;
c.主服务器将RDB文件发送给从服务器,从服务器载入RDB文件;
命令传播
d.主服务器将缓冲区内的写命令发送给从服务器,从服务器重放这些命令。
部分重同步
e.从服务器断线重连后,如果条件允许,主服务器只将断线期间的写命令发送给从服务器,提高性能。
命令补发
f.从服务器向主服务器发送replconf ACK命令时,如果从服务器偏移量少于主服务器的偏移量,主服务器会从
复制积压缓冲区中找到从服务器缺少的数据,重新发送给从服务器。
3)部分重同步原理
a.三要素:
    主服务器复制偏移量和从服务器复制偏移量
    主服务器的复制积压缓冲区
    服务器的运行ID
b.复制积压缓冲区
复制积压缓冲区默认大小1M,主服务器进行命令传播时,不仅会将写命令发送给从服务器,
还会写入到复制积压缓冲区里,从服务器重连后,根据偏移量,如果可以从复制积压缓冲区里获取到
断线期间的完整数据,则使用部分重同步,否则使用完整重同步。
复制积压缓冲区的大小可以根据以下公式推算:
2*second(平均断线重连时间)*write_size_per_second
c.服务器运行ID
重连后,从服务器会发送自己的保存的主服务器ID给主服务器,如果该主服务器ID,不是当前主服务器的,
则说明断线期间,主服务器发生了切换,这时会进行完整重同步。
4)复制过程
a.设置主服务器的地址和端口 slaveof <master_ip> <master_port>
b.建立套接字连接
c.发送ping命令 ping pong
d.身份验证
    需要在主服务器设置requirepass选项,同时在从服务器配置masterauth选项
    认证时从服务器通过AUTH命令,向主服务发送密码,进行验证。
e.发送端口信息
f.同步
g.命令传播
5)心跳检测
命令传播阶段,从服务器默认每隔1s,向主服务器发送如下命令
    replconf ack <从服务器当前复制偏移量>

2.哨兵Sentinel高可用方案
1)Sentinel会在Master故障时,将一台slave提升为Master,实现故障转移,已下线的主服务器重新上线后,
哨兵会自动将它设为新master的从服务器。
2)Sentinel本质上是一个特殊的redis服务器,Sentinel启动时不载入RDB/AOF文件,加载不同的命令集等。
Sentinel会和集群中的每一台redis服务器建立联系,Sentinel之间也会建立联系。
Sentinel和主服务器之间会创建一条命令连接(用于向主服务器发送命令,并接收命令回复)
和一个订阅连接(用于订阅主服务器的__sentinel__:hello频道)
Sentinel默认会每10s向主服务发送info命令,获取主服务器及所属从服务器的当前信息。
Sentinel默认会每10s向从服务发送info命令,获取从服务器及其主服务器的当前信息。
Sentinel默认会每2s,向所有主从服务器的__sentinel__:hello频道发送信息。
一个Sentinel发送的信息会被其他Sentinel接收到,Sentinel间只会建立命令连接。
3)主观下线状态检测
Sentinel默认每1s向所有连接对象发送ping命令,根据回复判断对象是否在线。
超过阈值时间未回复的对象,可认为主观下线。
4)客观下线状态检测
当一个Sentinel认为某个主服务器主观下线后,会向其他Sentinel询问意见,一定数量的Sentinel认为
主观下线,则可以认为是客观下线,并执行故障转移。
每个Sentinel设置的主观下线时长阈值可能不同,判断结果不同。
5)主Sentinel选举(Raft算法)
当一个主服务被判定客观下线后,会选举一个主Sentinel,进行故障转移。
选举规则:
a.每个发现主服务器下线的Sentinel都会要求其他Sentinel将自己设置为主Sentinel,它会将自己的runid,
发送给其他Sentinel。
b.按照先到先得的规则,目标Sentinel会向源Sentinel发送回复,回复中包含了
第一个源Sentinel的runid。
c.源Sentinel会判断回复信息中的runid是否是给自己的。
d.当某个Sentinel获得了半数以上的投票,它会成为主Sentinel。
e.如果规定时间内没有选出主Sentinel,会重新进行选举。
6)新的主服务器的选举
按照以下顺序,最后剩下的就被选为主服务器
a.排除下线或断线的从服务器
b.排除最近5s内没有回复过主Sentinel的info命令的从服务器
c.排除过早与主服务器断开连接的从服务器
d.根据设置的从服务器优先级排序
e.如果有多个最高优先级的从服务器,选出复制偏移量最大的从服务器
f.如果仍有多个从服务器,按照运行ID,取运行ID最小的从服务器。
7)sentinel failover <masterName> 下线主节点
8)sentinel节点数应该大于3,最好为奇数。
9)客户端初始化时连接的时sentinel节点集合,而不再是具体的redis节点,但sentinal只是配置中心不是代理。
10)三个定时任务
每10s对master和slave执行info 发现slave 确认主从关系
每2s每个sentinel通过master节点的channel交换信息 交互对节点和看法和各自信息
每1s每个sentinel对其他sentinel和redis执行ping 确认对方活着

3.集群
1)redis服务器在启动时会根据cluster-enabled配置是否为yes,开启集群模式。
客户端向服务器发送cluster meet <ip> <port>命令,将指定ip的节点添加到集群中。
通过cluster addslots <slot>命令,给节点分配槽。
在集群模式下服务器会维持clusterLink(clusterNode)链表,记录集群中的所有节点。
2)集群通过分片的方式保存数据库中的键值对,集群只能使用db0数据库,默认分为16384个槽(slot)。
当16384个槽都有节点在处理时,集群处于上线状态,有一个槽没得到处理,集群就会处于下线状态。
redis通过一个长度为16384/8个字节的数组判断自己所分配的槽。同时也会记录其他节点的槽设置。
3)命令执行
接收命令的节点会判断要处理的键位于哪个槽,如果是自己负责的槽就执行命令;如果不是,就向客户端
返回MOVED错误,指引客户端转向正确的节点。
MOVED格式 moved <键所在的槽> <正确的节点ip> <正确的节点port>
集群模式下客户端会自动进行转向。
4)重新分片
重新分片可以将已分派给一个节点(源)的槽分派给另一个节点(目的)。
重新分片过程中节点可以继续处理请求。
重新分片由redis-trib负责执行。
5)ASK错误
在重新分片过程中,可能出现一部分值保存在源节点中,一部分保存在目标节点中,客户端向源节点发送的键命令,
已经迁移到了目的节点。则源节点会返回ASK错误,引导客户端重新指向目的节点,类似MOVED。
6)集群故障转移
a.redis集群中主节点用于处理槽,从节点用于复制某个主节点,并在主节点下线时接替主节点。
通过 cluster replicate <node_id> 将节点设为node_id的从节点。
b.故障检测
1-1.集群中的节点会定期向其他节点发送ping消息,如果在规定时间内没有回复过pong消息,则故障节点
会被标记未疑似下线PFAIL。
1-2.集群中的各个节点会互相交换各自认为的集群节点状态信息(在线,疑似下线,已下线).
1-3.当A节点得知B节点认为C节点疑似下线,会记录下这一消息。
       当半数的主节点都报告C节点疑似下线,那么C节点会被标记为已下线,同时向集群广播C节点的FAIL消息。
       所有收到消息的节点,都将C标记为已下线。
c.选举新的主节点
1-1.集群的投票轮数计数器+1.
1-2.从服务器发现自己的主节点已下线后,向集群广播消息,要求成为主节点。
1-3.收到消息的主节点向该从节点投票,消息先到的先投。
1-4.当一个从节点收到半数以上的投票时,选举结束。
1-5.如果没有从节点获得足够票数,进行新一轮投票。
d.故障转移
1-1.选举出新的主节点。
1-2.新的主节点将已下线的主节点的槽指派给自己。
1-3.新的主节点向集群广播pong消息,告知自己成为新的主节点。
7)消息
a.MEET消息 请求接收者加入到当前集群中。
b.ping消息 通过ping消息检测其他节点是否在线。
集群中的每个节点,每隔1s从已知节点中随机选择5个节点,向其中最长时间未发送给ping消息
的节点发送ping消息。如果节点超过设定时间未与某个节点通信过,也会发送ping消息。
c.pong消息 响应meet/ping命令。或向集群广播自己的pong消息,更新自己的信息。
d.fail消息 当一个主节点A判断主节点B已经进入fail状态,会向集群广播B下线的fail消息,收到消息的节点,
会将B标记为已下线。
e.publish消息 当一个节点收到publish命令时,节点会执行该publish命令,并向集群广播一条publish消息,
所有收到消息的节点也会执行相同的publish命令。

四、其他功能

1.发布与订阅
1)客户端可以通过subscribe命令订阅1个或多个频道,当有其他客户端向被订阅的频道发送消息时,
该频道的所有的订阅者都会收到消息。
2)客户端可以通过psubscribe命令订阅1个或多个模式,当有其他客户端向被订阅的频道发送消息时,
不仅该频道的所有的订阅者都会收到消息,还会发送给所有与这个频道相匹配的模式订阅者,类似于
rabbitmq的topic交换器。
3)客户端可以通过unsubscribe/punsubscribe进行退定,redis服务器中会为每个频道维持一个频道订阅者链表,
以及一个全局的模式订阅链表,订阅/退订的实质是向链表插入或删除客户端值。
4)客户端执行 publish <channel> <message>命令,向频道发送消息。
5)客户端执行 pubsub channels [模式(可选)] 查看服务器上有哪些(与模式匹配的)频道
执行 pubsub numsub [频道名] 获取频道的订阅者数量
执行 pubsub numpat 返回当前服务器上被订阅的模式数量
2.事务
1)redis通过multi开启事务/exec提交事务/watch实现乐观锁机制
2)redis的事务是指,过multi开启事务后,后续的命令会被放在队列中,当执行exec命令时,
一起不间断的执行,不是传统的数据库事务,如果中间步骤执行出错,不会回滚,redis不支持回滚。
3)watch乐观锁
eg:watch "name" --> multi --> set "name" "wdw" -->exec
 当watch后数据被修改了,会设置键的redis_dirty_cas标志位,这个事务不会被执行。
4)redis的事务满足原子性、一致性、隔离性,当服务器运行在AOF,always配置下时,也满足持久性。
3.排序
1)sort命令默认以数字值的方式进行排序,如果制定了alpha选项,会以字符串方式排序。
2)采用快速排序算法。
3)sort命令支持by desc limit get store选项
默认排序结果不会保存,store命令可以将结果保存在指定的键里面。
4.redis支持通过setbit/getbit/bitcount/bitop命令操作二进制位数组。
bitcount命令通过查表法和swar算法来获取汉明重量。
5.redis支持慢查询日志

五、其他

1.哈希分布
节点取余分区
    哈希+根据余数分区 分区数不好修改
一致性哈希分区
    哈希+顺时针分区(优化取余),但数据可能会倾斜
1)构造一致性哈希环
一致性哈希算法中首先有一个哈希函数,哈希函数产生hash值,所有可能的哈希值构成一个哈希空间,
哈希空间为[0,2^32-1],这是一个“线性”的空间,但是在算法中通过恰当逻辑控制,使其首尾相衔接,
也即是0=2^32,这样就构造一个逻辑上的环形空间。
2)节点映射
将集群中的各分区映射到环上的某个一位置。比如集群中有三个分区,那么可以大致均匀的将其分布在环上。
3)确定位置
透过计算hash值在环上的位置(比如ip),顺时针找到最近的分区点。
虚拟槽改进一致性哈希分区
    解决数据偏移问题 将每个分区虚拟为n个分区,通过设置合理的n值,使分区间隔均匀,防止数据倾斜。
2.java客户端
jedisPool/jedisSentinelPool/jedisCluster
3.考虑到网络带宽,官方建议不要超过1000个节点
4.集群中发布订阅的带宽压力极大
5.集群限制
mget、mset必须在同一个slot
事务和lua支持有限,操作的key必须在一个节点
key是数据分区的最小力度
不支持多个数据库,集群模式下只有db0
复制只支持一层主从,不支持树形。

转载于:https://my.oschina.net/dajianguo/blog/3026982

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值