Redis优势
读速度11万/s,写8W/s
单个操作是原子性的,多个操作支持事务(multi…exec)
Redis数据结构
SDS
1、用来保存字符串值: redis数据库中,包含字符串值的键值对在底层都是由SDS实现的
2、用作缓冲区
链表
字典
redis数据库的底层就是采用字典来实现的,字典也是哈希键的底层实现之一
下图是一个普通状态下的字典(没有在进行rehash的时候):
跳跃表
有序集合键(zset)底层实现,以及在集群节点中用作内部数据结构,除此之外没有其他用途
各个节点中的1.0、2.0和3.0是节点所保存的分值。在跳跃表中,节点按各自所保存的分值从小到大排列。
右边那些就是跳跃表节点,每个节点的高度是创建节点时随机生成的
跳跃表中所有节点按分值大小来排序,各个节点保存的对象是唯一的,但多个节点保存的分值可以相同
压缩列表
压缩列表(ziplist)是列表键和哈希键的底层实现之一
当一个列表键只包含少量列表项,并且每个列表项要么就是小整数值,要么就是长度比较短的字符串,那么Redis就会使用压缩列表来做列表键的底层实现。
区别于链表:
链表在Redis中的应用非常广泛,比如列表键的底层实现之一就是链表。当一个列表键包含了数量比较多的元素,又或者列表中包含的元素都是比较长的字符串时,Redis就会使用链表作为列表键的底层实现。
为节约内存而开发
对象
Redis对象系统:用引用计数进行内存回收(对象)
redis每个对象都由一个redisObject结构表示
对于Redis数据库保存的键值对来说,键总是一个字符串对象,而值则可以是字符串对象、列表对象、哈希对象、集合对象或者有序集合对象的其中一种
字符串对象
编码转换:
字符串对象的编码可以是int、raw或者embstr
1、字符串对象保存的是整数值,且可用long表示,用字符串对象的ptr属性保存,编码为int
2、保存字符串值,且长度>32字节,用SDS保存,编码为raw
3、保存字符串值,且长度<=32字节,用SDS保存,编码为embstr
列表对象
ziplist编码:使用压缩列表实现
linkedlist编码:使用双端链表实现
哈希对象
ziplist编码使用压缩列表实现
hashtable编码使用字典实现(键或值长度太大、或者键值对数量太多都会从ziplist转换成hashtable)
集合对象
intset编码:使用整数集合实现
hashtable编码:使用字典实现
有序集合对象
ziplist编码:使用压缩列表实现
skiplist编码:使用zset结构实现,包含一个字典和一个跳跃表
跳跃表节点的Object属性保存了元素成员,score属性保存了元素分值,以便进行范围性操作,如zrange、 zrank
字典中每个键保存了元素的成员,字典的值保存了元素对应的分值,因此通过O(1)可以查到该键的分值,如ZSCORE命令
这两个结构会通过指针共享相同元素的成员和分值,不会造成内存浪费
Redis持久化
-
redis是内存数据库,若无持久化,那么数据断电即失
-
redis持久化有两种方式:rdb aof
-
RDB记录所有的键值对,AOF记录所有的写指令
-
redis默认用rdb,要用aof需要修改配置文件手动开启
AOF
AOF就是记录每个写操作(读操作不记录),默认是每秒同步一次,所以文件完整性会更好,最多就丢失一秒的数据。当然这样的缺点就是aof文件会很大,修复速度慢。
AOF文件的写入和同步
redis服务器进程就是一个时间循环,结束之前就要判断是否把aof_buf缓存区的内容写入和保存到AOF文件中,至于什么时候写入和同步AOF文件,由appendfsync配置参数来决定,该参数有几个值:
1、always:每个事件循环都要将缓冲中所有内容写入并同步到AOF文件
2、everysec:所有内容写入AOF文件,若上次同步到现在超过一秒,就进行AOF文件同步
3、no: 所有内容写入AOF文件,但何时同步取决于操作系统
AOF文件的载入和数据还原
读取AOF文件的每一条命令,执行,执行完就还原了。
解决AOF文件体积太大的问题—AOF重写
原理:从数据库中读取键现在的值,然后用一条命令去记录键值对(这样就一个键对应一条命令,而不是很多条)
AOF后台重写—BGREWRITEAOF命令
为什么会有后台重写呢?因为redis是单线程的,如果服务器进程直接调用AOF重写(aof_rewrite函数),会有很多写操作,就会导致这个服务器进程阻塞,服务器将无法处理客户端的请求。
于是,就创建子进程,子进程AOF重写。
但是,在子进程AOF重写期间,如果服务进程继续处理命令,就会导致AOF文件和服务器当前数据库状态不一致。
所以,就弄了个AOF重写缓冲区和AOF缓冲区。
服务器执行完写命令后会发到这两个缓冲区。
子进程重写完AOF,就通知服务器进程,服务器进程此时阻塞一下,把AOF重写缓冲区的内容写入新的AOF文件中,对新AOF文件进行改名,覆盖现有的AOF文件
RDB
RDB是在指定的时间间隔内将内存中的数据集快照写入磁盘,恢复的时候将快照文件直接读入内存中。
RDB文件是一个经过压缩的二进制文件,保存在硬盘中
1、上面所说的指定时间间隔,是多少?
这个是在redis配置文件中的save项中进行配置的,比如你配置了save 900 1,那么只要服务器在900秒之内对数据库进行了至少一次修改,那么就会写入磁盘
2、写入磁盘的用什么命令?
生成RDB文件的命令其实有两个
(1)SAVE命令:会阻塞redis服务器直到RDB创建完毕,在此期间无法处理任何命令(此时所有命令请求都会被拒绝)
(2)BGSAVE命令:派生子进程,子进程创建RDB文件,父进程继续处理命令请求
而上面所说的配置save选项,就会让服务器每个一段时间自动执行一次BGSAVE命令
Redis数据库还原
如果开启了AOF,就用AOF还原数据库,否则才用RDB
复制
slaveof命令
旧版:同步+命令传播
同步
1、从服务器向主服务器发送SYNC命令
2、主服务器执行BGSAVE命令,生成RDB文件,BGSAVE后面执行的写命令放到缓冲区中
3、BGSAVE执行完,将RDB发给从服务器
4、从服务器载入RDB,恢复至主服务器执行BGSAVE时的状态
5、主服务器把缓冲中的写命令发给从服务器,从服务器执行这些命令
命令传播
主从服务器就会进入命令传播阶段,这时主服务器只要一直将自己执行的写命令发送给从服务器,而从服务器只要一直接收并执行主服务器发来的写命令,就可以保证主从服务器一直保持一致了。
旧版的缺点:
1、断线重连的复制效率很低:因为从服务器发送SYNC给主服务器,主服务器通过BGSAVE全量生成RDB文件,效率就比较低
2、SYNC很耗费资源:主服务器BGSAVE生成RDB,耗费CPU、磁盘IO;主服务器发送RDB给从服务器:占用网络资源;从服务器载入RDB过程阻塞:无法处理命令请求;
新版复制功能的实现(redis 2.8)
PSYNC代替SYNC
完整重同步(用于初次复制):与SYNC一样
部分重同步(用于断线后重连):主服务器向从服务器发送断线期间主服务器执行的写命令
如何实现部分重同步?
复制偏移量offset(主从都有,就是说主服务器给从服务器传播N个字节,主服务器的复制偏移量就+N,从服务器收到主服务器传来N个字节,从服务器的复制偏移量就+N)
复制积压缓冲区(固定长度的队列,保存着主服务器最近一部分传播的写命令)
服务器运行ID(初次复制时主服务器给从服务器的)
断线重连后,从服务器把复制偏移量发给主服务器,主服务器看这个复制偏移量是否还在复制积压缓存区里面,是的话,执行部分重同步,否则完整重同步;
另外,从服务器重连后还会把初始复制时保存的服务器运行ID发给主服务器,若跟主服务器运行ID一致,才可执行部分重同步
整个流程:主服务器收到PSYNC命令(PSYNC runid offset),主服务器检查运行ID是否跟自己的运行ID一致,再检查复制偏移量是否还在复制积压缓冲区里面,是的话才把复制积压缓冲区(就是队列)中该偏移量后面的写命令发给从服务器
新版复制的实现
SLAVEOF:异步命令
同步
从服务器将向主服务器发送PSYNC命令
命令传播
当完成了同步之后,主从服务器就会进入命令传播阶段,这时主服务器只要一直将自己执行的写命令发送给从服务器,而从服务器只要一直接收并执行主服务器发来的写命令,就可以保证主从服务器一直保持一致了。
集群
节点握手,组成一个集群:cluster meet ip port
启动节点:配置cluster-enabled为yes,开启服务器的集群模式,成为一个节点,否则就是一个单机的普通服务器
集群模式下才用到的数据结构:clusterNode、clusterLink、clusterState(每个节点都会有的哈)
clusterNode:记录节点状态,并为集群中所有其他节点创建一个相应的clusterNode结构,来记录其他节点的状态
clusterMeet命令实现:用这条命令跟另一个节点B握手,然后通过gossip协议传播给集群中其他节点,让其他节点也与节点B握手,最终节点B被集群中所有节点认识
槽指派:cluster addslots 0 1 2…:将一个或多个槽指派给节点负责
记录节点的槽指派信息:clusterNode结构的slots属性和numslot属性记录了节点负责处理哪些槽
slots是一个二进制位数组,一共有16384位,当前节点负责处理哪个槽,该槽对应下标就置为1,numslot是负责处理槽的数量
传播节点的槽指派信息:节点会把自己的slots数组发给其他节点,告知其他节点自己在负责处理哪些槽
每个节点是如何知道其他节点负责处理哪些槽的呢?
每个节点都维护了一个数组,这个数组中的每个元素,对应集群中每个节点的IP、端口、以及负责管理的槽数组,因此每个节点都可以知道其他节点负责哪些槽,如下图:
访问键的过程:
1、计算键属于哪个槽:CRC16(key) & 16383,假设计算出来的槽是6257
2、检查clusterState.slots[6257]是不是clusterState.myself
3、如果不是,就是访问clusterState.slots[6257]指向的clusterNode结构,得到ip和port,向客户端返回moved 6257 ip port
计算键所属槽:CRC16(key) & 16383 —cluster keyslot可以查看给定键属于哪个槽
重新分片:将任意数量已指派给某个节点的槽改为指派给另一个节点,比如在集群中新增一个节点的时候,这个操作是集群管理软件负责执行的,我理解应该是自动执行的吧。。
复制与故障转移
设置从节点:cluster replicate <node_id>:让接受命令的节点成为node_id所指定节点的从节点,并开始对主节点进行复制
复制就是向从节点发送命令slaveof master_ip master_port
故障检测
每个节点定期向其他节点发送PING消息来检测对方是否在线,若没有及时收到PONG消息,就会认为对方疑似下线(PFAIL)
半数以上主节点将某个主节点报告为疑似下线,那么这个主节点将被标记为已下线
将某个节点X标记为已下线的节点,向集群广播一条关于X的FAIL消息,所有收到这个FAIL消息的节点,立即将X节点标记为已下线
如何选举新的主节点
从节点向主节点广播消息,让他们为自己投票,先到先得原则
当一个从节点得到大于N/2+1票数时,该从节点就被选为新主节点
过期时间
expire key ttl 设置key生存时间为ttl秒
pexpire key ttl 设置key生存时间为ttl毫秒
expireat key timestamp 设置key过期时间为timestamp所指定的秒数时间戳
pexpireat key timestamp 设置key过期时间为timestamp所指定的毫秒数时间戳
实际上expire pexpire expireat 都是使用pexireat命令来实现的
redisDb里面(可以理解为每个子数据库),保存了过期字典,键是指针,指向数据库键,值为该键的过期时间(一个毫秒精度的UNIX时间戳,就是说哪个时间点过期,而不是秒数或者毫秒数啥的)
查看过期时间:TTL key
移除过期时间persist key,其实就是删除过期字典中的关联,估计那个值也会被删掉吧
过期键的判定:键是否存在于过期字典----取出过期时间戳-----当前Unix时间戳是否过期时间戳
命令:ttl pttl 看结果是否大于0
过期删除策略
定时删除:设置过期时间同时设置一个定时器,定时器时间快到时,把键给删掉
优点:内存友好:定时删除,释放内存
缺点:CPU不友好,过期键较多时,浪费太多时间在删除操作上
惰性删除:不管他,等到下次get key的时候,比较当前时间戳和key的过期时间,决定用不用删除
CPU友好,但内存不友好(过期了还占着内存,如果这些键一直没有被访问,就一直占用内存,内存泄漏)
定期删除:每隔一段时间检查一次,然后删除过期的
比较折中的方案。
Redis使用的策略:惰性删除+定期删除
惰性删除由db.c/expireIfNeeded函数实现(所有读写数据库的Redis命令在执行之前都会调用expireIfNeeded函数对输入键进行检查)
定期删除由redis.c/activeExpireCycle函数实现
大概理解,就是说redis在读写的时候会去判断键是否过期需要删除,也会定期去清理过期键
内存淘汰策略
定期删除+惰性删除,由于定期删除是随机的,惰性删除又是访问到过期键才删除,所有就有可能有些key过期了却没有删除,可能会导致内存满了。
那么就要采用内存淘汰策略:(redis.conf中的maxmemory-policy volatile-lru)
-
noeviction:当内存使用超过配置的时候会返回错误,不会驱逐任何键
-
allkeys-lru:加入键的时候,如果过限,首先通过LRU算法驱逐最久没有使用的键
-
volatile-lru:加入键的时候,如果过限,首先从设置了过期时间的键集合中驱逐最久没有使用的键
-
allkeys-random:加入键的时候如果过限,从所有key随机删除
-
volatile-random:加入键的时候如果过限,从过期键的集合中随机驱逐
-
volatile-ttl:从配置了过期时间的键中驱逐马上就要过期的键
-
volatile-lfu:从所有配置了过期时间的键中驱逐使用频率最少的键
-
allkeys-lfu:从所有键中驱逐使用频率最少的键
订阅和发布
底层采用字典实现
频道订阅关系的底层:字典(键是频道,值是客户端链表)
模式订阅关系的底层:链表(每个链表节点保存了客户端和模式)
订阅频道:subscribe
退订频道:unsubscribe
订阅模式:psubscribe
退订模式:punsubscribe
发送消息到频道订阅者和模式订阅者:publish
查看有哪些频道:pubsub channels
查看某个或某些频道的订阅者数量:pubsub numsub 频道1名称 频道2名称…
查看当前被订阅模式的数量:pubsub numpat (就是返回模式链表的长度)
事务
事务保存的状态:
执行exec时,会遍历队列,依次执行命令
事务中如果命令入队出错(比如语法错误),exec时就会保存,事务中所有命令都不会执行
但如果事务开始执行了,但在执行期间出现错误,那么整个事务也会继续执行下去的。
Watch命令的实现
redis保存了一个watched_keys的字典,键就是被监视的数据库键,值是所有监视这个数据库键的客户端
所有对数据库修改的命令如SET、LPUSH等,都会把监视被修改键的客户端的redis_dirty_cas标识打开,表示该客户端的事务安全性已经被破坏了。
然后当这个客户端在执行exec的时候,服务器发现他的事务安全性被破坏,就会拒绝执行它提交的事务。
还有一个标志REDIS_DIRTY_EXEC表示事务在命令入队中出现错误,这个标志也会导致exec执行失败