bgsave 通过fork一个子进程,专门用于写入RDB文件,避免了主线程的阻塞,这也是Redis RDB文件生成的默认配,fork子进程负责持久化过程,阻塞只会发生在fork子进程的时候。
会直接拷贝数据(内存使用率超过50%,直接拷贝不就gg了!!)
COW(Copy on Write):
fork一个子进程,只有在父进程发生写操作修改内存数据时,才会真正去分配内存空间,并复制内存数据,而且也只是复制被修改的内存页中的数据,并不是全部内存数据;
fork采用操作系统提供的写实复制(Copy On Write)机制,就是为了避免一次性拷贝大量内存数据给子进程造成的长时间阻塞问题,但fork子进程需要拷贝进程必要的数据结构,其中有一项就是拷贝内存页表(虚拟内存和物理内存的映射索引表),这个拷贝过程会消耗大量CPU资源,拷贝完成之前整个进程是会阻塞的,阻塞时间取决于整个实例的内存大小,实例 越大,内存页表越大,fork阻塞时间越久。拷贝内存页表完成后,子进程与父进程指向相同的内存地址空间,也就是说此时虽然产生了子进程,但是并没有申请与父进程相同的内存大小。那什么时候父子进程才会真正内存分离呢?“写实复制”顾名思义,就是在写发生时,才真正拷贝内存真正的数据。
AOF 日志是写后日志,“写后”的意思是 Redis 是先执行命令,把数据写入内存,然后才记录日志,这种方式,就是先让系统执行命令,只有命令能执行成功,才会被记录到日志中,否则,系统就会直接向客户端报错。所以,Redis 使用写后日志这一方式的一大好处是,可以避免出现记录错误命令的情况;另外还有个好处是:不会阻塞当前的写操作(但可能阻塞下一个操作)。
AOF 机制给我们提供了三个选择,也就是 AOF 配置项 appendfsync 的三个可选值。
Always
,同步写回:每个写命令执行完,立马同步地将日志写回磁盘;
Everysec
,每秒写回:每个写命令执行完,只是先把日志写到 AOF 文件的内存缓冲区,每隔一秒把缓冲区中的内容写入磁盘;
No
,操作系统控制的写回:每个写命令执行完,只是先把日志写到 AOF 文件的内存缓冲区,由操作系统决定何时将缓冲区内容写回磁盘。
AOF文件过大怎么办?
重写
AOF 重写机制就是在重写时,Redis 根据数据库的现状创建一个新的 AOF 文件,也就是说,读取数据库中的所有键值对,然后对每一个键值对用一条命令记录它的写入。比如说,当读取了键值对“testkey”: “testvalue” 之后,重写机制会记录 set testkey testvalue 这条命令。这样,当需要恢复时,可以重新执行该命令,实现“testkey”:“testvalue”的写入。
AOF 文件是以追加的方式,逐一记录接收到的写命令的。当一个键值对被多条写命令反复修改时,AOF 文件会记录相应的多条命令。但是,在重写的时候,是根据这个键值对当前的最新状态,为它生成对应的写入命令。这样一来,一个键值对在重写日志中只用一条命令就行了,而且,在日志恢复时,只用执行这条命令,就可以直接完成这个键值对的写入了。
原理
AOF 文件重写并不需要对现有的 AOF 文件进行任何读取、分析或者写入操作,而是通过读取服务器当前的数据库状态来实现的。首先从数据库中读取键现在的值,然后用一条命令去记录键值对,代替之前记录这个键值对的多条命令,这就是 AOF 重写功能的实现原理。
一个拷贝,两处日志
“一个拷贝”就是指,每次执行重写时,主线程 fork 出后台的 bgrewriteaof 子进程。此时,fork 会把主线程的内存拷贝一份给 bgrewriteaof 子进程(也是用到COW技术),这里面就包含了数据库的最新数据。然后,bgrewriteaof 子进程就可以在不影响主线程的情况下,逐一把拷贝的数据写成操作,记入重写日志。
“两处日志”又是什么呢?
因为主线程未阻塞,仍然可以处理新来的操作。此时,如果有写操作,第一处日志就是指正在使用的 AOF 日志,Redis 会把这个操作写到它的缓冲区。这样一来,即使宕机了,这个 AOF 日志的操作仍然是齐全的,可以用于恢复。而第二处日志,就是指新的 AOF 重写日志。这个操作也会被写到重写日志的缓冲区。这样,重写日志也不会丢失最新的操作。等到拷贝数据的所有操作记录重写完成后,重写日志记录的这些最新操作也会写入新的 AOF 文件,以保证数据库最新状态的记录。此时,我们就可以用新的 AOF 文件替代旧文件了。
AOF非阻塞的重写过程总结来说,每次 AOF 重写时,Redis 会先执行一个内存拷贝,用于重写;然后,使用两个日志保证在重写过程中,新写入的数据不会丢失。而且,因为 Redis 采用额外的线程进行数据重写,所以,这个过程并不会阻塞主线程。
先谈数据恢复策略,如果一台服务器上有既有RDB文件,又有AOF文件, RDB 和 AOF 对比。
- | RDB | AOF |
---|---|---|
启动优先级 | 低 | 高 |
体积 | 小 | 大 |
恢复速度 | 快 | 慢 |
数据安全性 | 丢失若干时间内的数据 | 根据策略决定 |
快照多久做一次?
时间太长,出现问题丢失的数据太多,时间太短,频繁写磁盘会带来压力,频繁fork子进程在fork时会阻塞主进程。
最好的方式:增量快照,一次全量快照后,记录增量的数据。
Redis 4.0 中提出了一个混合使用 AOF 日志和内存快照的方法。简单来说,内存快照以一定的频率执行,在两次快照之间,使用 AOF 日志记录这期间的所有命令操作。这样一来,快照不用很频繁地执行,这就避免了频繁 fork 对主线程的影响。而且,AOF 日志也只用记录两次快照间的操作,也就是说,不需要记录所有操作了,因此,就不会出现文件过大的情况了,也可以避免重写开销。
在Redis内部,每当我们设置一个键的过期时间时,Redis就会将该键带上过期时间存放到一个过期字典
中,对应三种策略:
定时删除
在设置某个key 的过期时间同时,我们创建一个定时器,让定时器在该过期时间到来时,立即执行对其进行删除的操作。由于淘汰策略也是在主线程执行的,此种方案不推荐。
惰性删除
设置该key 过期时间后,我们不去管它,当需要该key时,我们在检查其是否过期,如果过期,我们就删掉它,反之返回该key。Redis的惰性删除策略由 db.c/expireIfNeeded 函数实现,所有键读写命令执行之前都会调用expireIfNeeded函数对其进行检查。
定期删除
每隔一段时间,我们就对一些key进行检查,删除里面过期的key。由 redis.c/activeExpireCycle 函数实现,函数以一定的频率运行,每次运行时,都从一定数量的数据库中取出一定数量的随机键进行检查,并删除其中的过期键。
注意:并不是一次运行就检查所有的库,所有的键,而是随机检查一定数量的键。
定期删除函数的运行频率,在Redis2.6版本中,规定每秒运行10次,大概100ms运行一次。在Redis2.8版本后,可以通过修改配置文件redis.conf 的
hz
选项来调整这个次数。
最佳实践: 定期删除+惰性删除
由于定期删除每次只会随机删除一部分,会滞留很多过期key在内存中,所以需要配合惰性删除在使用该key的时候去检测如果过期则删除。
虽然定期+惰性,但是如果定期删除漏掉了很多过期 key,然后你也没及时去查询,也就没走惰性删除,此时会怎么样?如果大量过期 key 堆积在内存 里,导致 redis 内存块耗尽了, 所以此时需要走内存淘汰机制!
有如下几种淘汰方式:
volatile-lru:设置了过期时间的key使用LRU算法淘汰;allkeys-lru:所有key使用LRU算法淘汰;
volatile-lfu:设置了过期时间的key使用LFU算法淘汰;allkeys-lfu:所有key使用LFU算法淘汰;
volatile-random:设置了过期时间的key使用随机淘汰;allkeys-random:所有key使用随机淘汰;
volatile-ttl:设置了过期时间的key根据过期时间淘汰,越早过期越早淘汰;
noeviction:默认策略,当内存达到设置的最大值时,所有申请内存的操作都会报错(如set,lpush等),只读操作如get命令可以正常执行;
表示最近最少使用,常见实现方式为链表:
新数据放在链表头部 ,链表中的数据被访问就移动到链头,链表满的时候从链表尾部移出数据。
而在Redis中使用的是近似LRU算法,为什么说是近似呢?Redis中是随机采样5个(可以修改参数maxmemory-samples配置)key,然后从中选择访问时间最早的key进行淘汰,因此当采样key的数量与Redis库中key的数量越接近,淘汰的规则就越接近LRU算法。但官方推荐5个就足够了,最多不超过10个,越大就越消耗CPU的资源。
redis中对LRU并没有采用链表实现,因为它觉得太占用空间了!
具体参f考:https://www.php.cn/redis/459068.html
但在LRU算法下,如果一个热点数据最近很少访问,而非热点数据近期访问了,就会误把热点数据淘汰而留下了非热点数据,因此在Redis4.x中新增了LFU算法。
LFU(Least Frequently Used)表示最不经常使用,它是根据数据的历史访问频率来淘汰数据,其核心思想是“如果数据过去被访问多次,那么将来被访问的频率也更高”。
LFU算法反映了一个key的热度情况,不会因LRU算法的偶尔一次被访问被误认为是热点数据。
LFU算法的常见实现方式为链表:新数据放在链表尾部 ,链表中的数据按照被访问次数降序排列,访问次数相同的按最近访问时间降序排列,链表满的时候从链表尾部移出数据。
当我们启动多个 Redis 实例的时候,它们相互之间就可以通过replicaof(Redis 5.0 之前使用 slaveof)命令形成主库和从库的关系,之后会按照三个阶段完成数据的第一次同步
一旦主从库完成了全量复制,它们之间就会一直维护一个网络连接,主库会通过这个连接将后续陆续收到的命令操作再同步给从库,这个过程也称为基于长连接的命令传播
,可以避免频繁建立连接的开销。
主从之间网络断开后怎么办?
如果主从库在命令传播时出现了网络闪断,那么,从库就会和主库重新进行一次全量复制,开销非常大。从 Redis 2.8 开始,网络断了之后,主从库会采用增量复制的方式继续同步。全量复制是同步所有数据,而增量复制只会把主从库网络断连期间主库收到的命令,同步给从库。
当主从库断连后,主库会把断连期间收到的写操作命令,写入 replication buffer,同时也会把这些操作命令也写入 repl_backlog_buffer 这个缓冲区。 repl_backlog_buffer 是一个环形缓冲区,主库会记录自己写到的位置,从库则会记录自己已经读到的位置。
主从库的连接恢复之后,从库首先会给主库发送 psync 命令,并把自己当前的 slave_repl_offset 发给主库,主库会判断自己的 master_repl_offset 和 slave_repl_offset 之间的差距。在网络断连阶段,主库可能会收到新的写操作命令,所以,一般来说,master_repl_offset 会大于 slave_repl_offset。此时,主库只用把 master_repl_offset 和 slave_repl_offset 之间的命令操作同步给从库就行。
一个 Redis 集群是由多个 Redis 节点构成的,节点间可以相互通信。而每个节点又可以是多个 redis 服务器构成的主从模式。比如,我们可以将 九个节点,三三的构成主从模式(一主两从),然后将三个主节点构成集群(三主六从)。
故障检测
集群中的每个节点都会保存一份集群里所有节点的状态表,集群中的每个节点都会定期的向其他节点发送PING
消息,以此来检测对方是否下线。如果再规定的时间内没有收到PONG
消息的响应,就会将该节点标记为疑似下线(PFAIL)
,集群中的各个节点会通过消息,来交换
自己维护的节点状态表。比如某个节点是在线、下线还是疑似下线。
如果集群里半数以上的主节点,都将某个主节点标记为疑似下线,那么这个主节点将被标记为下线(FAIL)
状态,将主节点标记为下线状态的节点将会向集群广播一条消息,所有收到消息的节点,都会将节点标记为下线状态。
leader选举
当从节点发现自己复制的主节点进入下线状态后,就会触发进入选举过程,redis 集群将会在下线的主节点的从节点中选出一个节点作为新的主节点。
选举过程与哨兵选举过程非常类似。都是基于 Raft 算法做的。
首先,集群有一个配置纪元
的计数器。初始值为 0,每进行一次故障转移就会自增+1。集群中只有主节点有投票的权利,且每个主节点在一次故障转移期间只有一次投票的机会。
当集群中的从节点发现自己复制的主节点下线后,就会广播一条消息,要求所有收到这条消息且具有投票权利的节点向自己投票。
如果主节点还没有投票,则会返回 ACK 消息,表示支持该从节点成为主节点。如果某个从节点收到的票数超过主节点个数的一半,那么这个从节点将成为新的主节点。
如果集群中除下线节点外剩余的节点刚好能够被从节点瓜分,在极端情况下就会出现所有从节点的票数都不会超过一半,就会造成本次选举失败,然后配置纪元+1,进行重新选举。比如上图中的例子,如果两个从节点同时发起投票,那么就有可能每个从节点都收到一票,导致本次选举失败。
redis 集群为例尽量避免出现这种情况做了一下优化,在发现主节点下线后,从节点并不会立即发送消息,而是延迟一定的时间再发送选举消息,这个时间是随机的。计算公式如下:
DELAY = 500 milliseconds
+ random delay between 0 and 500 milliseconds
+ SLAVE_RANK * 1000 milliseconds
500ms 加上一个随机时间,SLAVE_RANK 是复制偏移量的差值,即与主节点的数据差距越小的从节点,等待的时间就会越小,越有可能成为新的主节点。
新选举出的主节点会首先会执行SLAVEOF no one
,停止对旧主节点的复制动作。然后会将旧主节点的槽位指派给自己。最后向集群发送广播消息,让集群中的其他节点知道新的主节点已经接管所有的槽位,旧主节点已经完成下线动作。
这样就完成了整个的故障转移过程,新的主节点将开始接受与自己负责的槽位有关的数据。
MySQL与MongoDB都是开源的常用数据库,但是MySQL是传统的关系型数据库,MongoDB则是非关系型数据库,也叫文档型数据库,是一种NoSQL的数据库。它们各有各的优点,关键是看用在什么地方。所以我们所熟知的那些SQL语句就不适用于MongoDB了,因为SQL语句是关系型数据库的标准语言。
MySQL特点
在不同的引擎上有不同的存储方式。
查询语句是使用传统的sql语句,拥有较为成熟的体系,成熟度很高。
开源数据库的份额在不断增加,mysql的份额也在持续增长。
缺点就是在海量数据处理的时候效率会显著变慢。
MongoDB
非关系型数据库(nosql ),属于文档型数据库。先解释一下文档的数据库,即可以存放xml、json、bson类型系那个的数据。这些数据具备自述性,呈现分层的树状数据结构。数据结构由键值(key=>value)对组成。
存储方式:虚拟内存+持久化。
查询语句:是独特的MongoDB的查询方式。
适合场景:事件的记录,内容管理或者博客平台等等。
架构特点:可以通过副本集,以及分片来实现高可用。
数据处理:数据是存储在硬盘上的,只不过需要经常读取的数据会被加载到内存中,将数据存储在物理内存中,从而达到高速读写。
mongodb 会比mysql快
首先是内存映射机制,数据不是持久化到存储设备中的,而是暂时存储在内存中,这就提高了在IO上效率以及操作系统对存储介质之间的性能损耗。(毕竟内存读取最快)
其次,NoSQL并不是不使用sql,只是不使用关系。没有关系的存在,就比如每个数据都好比是拥有一个单独的存储空间,然后一个聚集索引来指向。搜索性能一定会提高的。
mongodb 天生支持分布式部署,如果数据量足够大可以使用mongodb的分片集群,这样也是mongodb比较快的一个原因。
使用场景区别
MongoDB 是一种文档型数据库,由于它不限制数据量和数据类型,它是高容量环境下最合适的解决方案。由于 MongoDB 具备云服务需要的水平可伸缩性和灵活性,它非常适合云计算服务的开发。另外,它降低了负载,简化了业务或项目内部的扩展,实现了高可用和数据的快速恢复。
尽管 MongoDB 有那么多优点,但 MySQL 也在某些方面优于MongoDB,例如可靠性和数据一致性。另外,如果优先考虑安全性,MySQL就是安全性最高的 DBMS 之一。
而且,当应用程序需要把多个操作视为一个事务(比如会计或银行系统)时,关系数据库是最合适的选择。除了安全性,MySQL 的事务率也很高。实际上,MongoDB 支持快速插入数据,而 MySQL 相反,它支持事务操作,并关注事务安全性。
总体上看,如果项目的数据模式是固定的,而且不需要频繁变更,推荐使用 MySQL,因此项目维护容易,而且确保了数据的完整性和可靠性。
另一方面,如果项目中的数据持续增加,而且数据模式不固定,MongoDB 是最合适的选择。由于它属于非关系数据库,数据可以自由使用,不需要定义统一的数据结构,所以对数据进行更新和查询也很方便。MongoDB通常用于需要对内容进行管理、处理物联网相关业务、进行实时分析等功能的项目中。
想理解倒排索引,首先先思考一个问题,获取某个文件夹下所有文件名中 包含Spring的文件
1)确定要搜索的文件夹
2)遍历文件夹下所有文件
3)判断文件名中是否包含Spring
这种思维可以理解为是一种正向思维的方式,从外往内,根据key找value
。这种方式可以理解为正向索引
。