Redis篇

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)

  1. noeviction:当内存使用超过配置的时候会返回错误,不会驱逐任何键

  2. allkeys-lru:加入键的时候,如果过限,首先通过LRU算法驱逐最久没有使用的键

  3. volatile-lru:加入键的时候,如果过限,首先从设置了过期时间的键集合中驱逐最久没有使用的键

  4. allkeys-random:加入键的时候如果过限,从所有key随机删除

  5. volatile-random:加入键的时候如果过限,从过期键的集合中随机驱逐

  6. volatile-ttl:从配置了过期时间的键中驱逐马上就要过期的键

  7. volatile-lfu:从所有配置了过期时间的键中驱逐使用频率最少的键

  8. 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执行失败

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值