Redis支持两种数据持久化方式:RDB方式和AOF方式。前者会根据配置的规则定时将内存中的数据持久化到硬盘上,后者则是在每次执行写命令之后将命令记录下来。两种持久化方式可以单独使用,但是通常会将两者结合使用。
RDB方式的持久化是通过快照的方式完成的。当符合某种规则时,会将内存中的数据全量生成一份副本存储到硬盘上,这个过程称作”快照”,Redis会在以下几种情况下对数据进行快照:
根据配置规则进行自动快照;
快照执行的过程如下:
需要注意的是:
在执行fork的时候操作系统(类Unix操作系统)会使用写时复制(copy-on-write)策略,即fork函数发生的一刻,父进程和子进程共享同一块内存数据,当父进程需要修改其中的某片数据(如执行写命令)时,操作系统会将该片数据复制一份以保证子进程不受影响,所以RDB文件存储的是执行fork操作那一刻的内存数据。所以RDB方式理论上是会存在丢数据的情况的(fork之后修改的的那些没有写进RDB文件)。
在使用Redis存储非临时数据时,一般都需要打开AOF持久化来降低进程终止导致的数据丢失,AOF可以将Redis执行的每一条写命令追加到硬盘文件中,这一过程显然会降低Redis的性能,但是大部分情况下这个影响是可以接受的,另外,使用较快的硬盘能提高AOF的性能。
默认情况下,Redis没有开启AOF(append only file)持久化功能,可以通过在配置文件中作如下配置启用:
appendonly yes
开启之后,Redis每执行一条写命令就会将该命令写入硬盘中的AOF文件。AOF文件保存路径和RDB文件路径是一致的,都是通过dir参数配置,默认文件名是:appendonly.aof,可以通过配置appendonlyfilename参数修改,例如:
appendfilename “appendonly.aof”
AOF以纯文本的形式记录了Redis执行的写命令。
AOF文件是可识别的纯文本,它的内容就是一个个的Redis标准命令,
AOF日志也不是完全按客户端的请求来生成日志的,比如命令 INCRBYFLOAT 在记AOF日志时就被记成一条SET记录,因为浮点数操作可能在不同的系统上会不同,所以为了避免同一份日志在不同的系统上生成不同的数据集,所以这里只将操作后的结果通过SET来记录。
每一条写命令都生成一条日志,AOF文件会很大。
AOF重写是重新生成一份AOF文件,新的AOF文件中一条记录的操作只会有一次,而不像一份老文件那样,可能记录了对同一个值的多次操作。其生成过程和RDB类似,也是fork一个进程,直接遍历数据,写入新的AOF临时文件。在写入新文件的过程中,所有的写操作日志还是会写到原来老的AOF文件中,同时还会记录在内存缓冲区中。当重完操作完成后,会将所有缓冲区中的日志一次性写入到临时文件中。然后调用原子性的rename命令用新的 AOF文件取代老的AOF文件。
命令:BGREWRITEAOF
虽然每次执行更改数据库的内容时,AOF都会记录执行的命令,但是由于操作系统本身的硬盘缓存的缘故,AOF文件的内容并没有真正地写入硬盘,在默认情况下,操作系统会每隔30s将硬盘缓存中的数据同步到硬盘,但是为了防止系统异常退出而导致丢数据的情况发生,我们还可以在Redis的配置文件中配置这个同步的频率:
# appendfsync always
appendfsync everysec
# appendfsync no
选项:
1、appendfsync no
当设置appendfsync为no的时候,Redis不会主动调用fsync去将AOF日志内容同步到磁盘,所以这一切就完全依赖于操作系统的调试了。对大多数Linux操作系统,是每30秒进行一次fsync,将缓冲区中的数据写到磁盘上。
2、appendfsync everysec
当设置appendfsync为everysec的时候,Redis会默认每隔一秒进行一次fsync调用,将缓冲区中的数据写到磁盘。但是当这一次的fsync调用时长超过1秒时。Redis会采取延迟fsync的策略,再等一秒钟。也就是在两秒后再进行fsync,这一次的fsync就不管会执行多长时间都会进行。这时候由于在fsync时文件描述符会被阻塞,所以当前的写操作就会阻塞。所以,结论就是:在绝大多数情况下,Redis会每隔一秒进行一次fsync。在最坏的情况下,两秒钟会进行一次fsync操作。这一操作在大多数数据库系统中被称为group commit,就是组合多次写操作的数据,一次性将日志写到磁盘。
3、appednfsync always
当设置appendfsync为always时,每一次写操作都会调用一次fsync,这时数据是最安全的,当然,由于每次都会执行fsync,所以其性能也会受到影响。
建议采用 appendfsync everysec(缺省方式)
快照模式可以和AOF模式同时开启,互补影响。
RDB持久化是指在指定的时间间隔内将内存中的数据集快照写入磁盘,实际操作过程是fork一个子进程,先将数据集写入临时文件,写入成功后,再替换之前的文件,用二进制压缩存储。
AOF持久化以日志的形式记录服务器所处理的每一个写、删除操作,查询操作不会记录,以文本的方式记录,可以打开文件看到详细的操作记录。
1). 对于相同数量的数据集而言,AOF文件通常要大于RDB文件。RDB 在恢复大数据集时的速度比 AOF 的恢复速度要快。
2). 根据同步策略的不同,AOF在运行效率上往往会慢于RDB。总之,每秒同步策略的效率是比较高的,同步禁用策略的效率和RDB一样高效。
二者选择的标准,就是看系统是愿意牺牲一些性能,换取更高的缓存一致性(aof),还是愿意写操作频繁的时候,不启用备份来换取更高的性能,待手动运行save的时候,再做备份(rdb)。rdb这个就更有些 eventually consistent的意思了。
参考:
redis学习(九)——数据持久化
Redis实战和核心原理详解(9)RDB和AOF的优缺点对比以及如何选择
执行BGREWRITEAOF命令对redis的AOF进行重写,也可以启动一个定时脚本每天定时执行
参考:
解决redis aof文件过大的问题
继承linkedHashMap实现,构建指定最先访问的元素放到列表最后,覆写相关方法
class LRUCache extends LinkedHashMap {
private final int CACHE_SIZE;
/**
* 传递进来最多能缓存多少数据
*
* @param cacheSize 缓存大小
*/
public LRUCache(int cacheSize) {
// true 表示让 linkedHashMap 按照访问顺序来进行排序,最近访问的放在头部,最老访问的放在尾部。
super((int) Math.ceil(cacheSize / 0.75) + 1, 0.75f, true);
CACHE_SIZE = cacheSize;
}
@Override
protected boolean removeEldestEntry(Map.Entry eldest) {
// 当 map中的数据量大于指定的缓存个数的时候,就自动删除最老的数据。
return size() > CACHE_SIZE;
}
}
参考:
Redis 的过期策略都有哪些?内存淘汰机制都有哪些?手写一下 LRU 代码实现?
分享一个今日头条的面试题——LRU原理和Redis实现
读请求:先读缓存->无则读数据库->更新缓存
写请求:先删除缓存在更新数据库,还是先更新数据库,后删除缓存。为什么不是写完数据库后,更新缓存,请看下面的讨论。
两种方式各有优劣。但是都要解决两类问题:
先更新数据库,后删除缓存(写操作比读取慢,缓存不一致出现概率较低),解决这一问题,可以给缓存设置失效时间,或者延迟删除缓存,保证读请求发生之后再删除缓存,或者搞一个队列,把对同一条记录操作的读写请求放到同一个队列,当请求处理完毕后再删除缓存。
关于先更新数据库,后删除缓存,存在的另一大问题就是更新数据库成功,删除缓存失败了,导致缓存数据库不一致,可以
具体参考:
分布式之数据库和缓存双写一致性方案解析
下面的讨论,倾向于先删除缓存,后更新数据库,因为这种方案,可以很方便的解决删除缓存成功,但是更新数据库失败的问题,因为这样不会导致缓存不一致,缓存只是被删除了,会在读请求时重新被更新。
(1)、如果写数据库的值与更新到缓存值是一样的,不需要经过任何的计算,可以马上更新缓存,但是如果对于那种写数据频繁而读数据少的场景并不合适这种解决方案,因为也许还没有查询就被删除或修改了,这样会浪费时间和资源
(2)、如果写数据库的值与更新缓存的值不一致,写入缓存中的数据需要经过几个表的关联计算后得到的结果插入缓存中,那就没有必要马上更新缓存,只有删除缓存即可,等到查询的时候在去把计算后得到的结果插入到缓存中即可。
(3)、分布式锁保证有序
所以一般的策略是当更新数据时,先删除缓存数据,然后更新数据库,而不是更新缓存,等要查询的时候才把最新的数据更新到缓存
当更新数据时,如更新某商品的库存,当前商品的库存是100,现在要更新为99,先更新数据库更改成99,然后删除缓存,发现删除缓存失败了,这意味着数据库存的是99,而缓存是100,这导致数据库和缓存不一致。
这种情况应该是先删除缓存,然后在更新数据库,如果删除缓存失败,那就不要更新数据库,如果说删除缓存成功,而更新数据库失败,那查询的时候只是从数据库里查了旧的数据而已,这样就能保持数据库与缓存的一致性。
在高并发的情况下,如果当删除完缓存的时候,这时去更新数据库,但还没有更新完,另外一个请求来查询数据,发现缓存里没有,就去数据库里查,还是以上面商品库存为例,如果数据库中产品的库存是100,那么查询到的库存是100,然后插入缓存,插入完缓存后,原来那个更新数据库的线程把数据库更新为了99,导致数据库与缓存不一致的情况
遇到这种情况,可以用队列的去解决这个问题,创建几个队列,如20个,根据商品的ID去做hash值,然后对队列个数取摸,当有数据更新请求时,先把它丢到队列里去,当更新完后在从队列里去除,如果在更新的过程中,遇到以上场景,先去缓存里看下有没有数据,如果没有,可以先去队列里看是否有相同商品ID在做更新,如果有也把查询的请求发送到队列里去,然后同步等待缓存更新完成。
这里有一个优化点,如果发现队列里有一个查询请求了,那么就不要放新的查询操作进去了,用一个while(true)循环去查询缓存,循环个200MS左右,如果缓存里还没有则直接取数据库的旧数据,一般情况下是可以取到的。
在高并发下解决场景二要注意的问题
摘自:
Redis系列之数据库与缓存数据一致性解决方案
redis缓存与数据库一致性问题解决
我们在是使用redis过程中,通常会这样做,先读取缓存,如果缓存不存在,则读取数据库。
不管是先写库,再删除缓存;还是先删除缓存,再写库,都有可能出现数据不一致的情况。
因为写和读是并发的,没法保证顺序,如果删除了缓存,还没有来得及写库,另一个线程就来读取,发现缓存为空,则去数据库中读取数据写入缓存,此时缓存中为脏数据。如果先写了库,在删除缓存前,写库的线程宕机了,没有删除掉缓存,则也会出现数据不一致情况。
如果是redis集群,或者主从模式,写主读从,由于redis复制存在一定的时间延迟,也有可能导致数据不一致。
(1)读操作优先读取redis,不存在的话就去访问MySql,并把读到的数据写回Redis中;
(2)写操作的话,直接写MySql,成功后再写入Redis,替换掉原来的旧数据(可以在MySql端定义CRUD触发器,在触发CRUD操作后写数据到Redis,也可以在Redis端解析binlog,再做相应的操作)
(3)设定合理的超时时间,即经过超时时间,自动将redis中相应的数据删除。这样最差的情况是在超时时间内,内存存在不一致。当然这种策略要考虑redis和数据库主从同步的耗时,所以在第二次删除前最好休眠一定的时间,比如500毫秒,这样无疑又增加了写请求的耗时。
缓存穿透是指查询一个一定不存在的数据,由于缓存是不命中时被动写的,并且出于容错考虑,如果从存储层查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,失去了缓存的意义。在流量大时,可能DB就挂掉了,要是有人利用不存在的key频繁攻击我们的应用,这就是漏洞。
有很多种方法可以有效地解决缓存穿透问题,最常见的则是采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被 这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。另外也有一个更为简单粗暴的方法(我们采用的就是这种),如果一个查询返回的数据为空(不管是数 据不存在,还是系统故障),我们仍然把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分钟。
缓存雪崩是指在我们设置缓存时采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到DB,DB瞬时压力过重雪崩。
缓存失效时的雪崩效应对底层系统的冲击非常可怕。大多数系统设计者考虑用加锁或者队列的方式保证缓存的单线 程(进程)写,从而避免失效时大量的并发请求落到底层存储系统上。这里分享一个简单方案就时讲缓存失效时间分散开,比如我们可以在原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。
如果是黑客攻击,导致缓存雪崩,可采用策略如下:
事前 保证redis高可用 可恢复,使用本地缓存ehcache,限流及降级组件Hystrix
事后 进行恢复
对于一些设置了过期时间的key,如果这些key可能会在某些时间点被超高并发地访问,是一种非常“热点”的数据。这个时候,需要考虑一个问题:缓存被“击穿”的问题,这个和缓存雪崩的区别在于这里针对某一key缓存,前者则是很多key。
缓存在某个时间点过期的时候,恰好在这个时间点对这个Key有大量的并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。
缓存雪崩、缓存穿透、缓存预热、缓存更新、缓存降级等问题
缓存穿透,缓存击穿,缓存雪崩解决方案分析
如何正确访问Redis中的海量数据?服务才不会挂掉
应对缓存击穿的解决方法
20万用户同时访问一个热点Key,如何优化缓存架构
基本思想:读写分离 - 提高Redis的QPS,主从复制 - 保证高可用
原理:
Redis主从复制可以根据是否是全量分为全量同步和增量同步。
Redis全量复制一般发生在Slave初始化阶段,这时Slave需要将Master上的所有数据都复制一份。具体步骤如下:
Redis增量复制是指Slave初始化后开始正常工作时主服务器发生的写操作同步到从服务器的过程。
增量复制的过程主要是主服务器每执行一个写命令就会向从服务器发送相同的写命令,从服务器接收并执行收到的写命令。
Redis的同步策略是:主从刚刚连接的时候,进行全量同步;全量同步结束后,进行增量同步。当然,如果有需要,slave 在任何时候都可以发起全量同步。redis 策略是,无论如何,首先会尝试进行增量同步,如不成功,要求从机进行全量同步。
Redis-Sentinel也就是哨兵机制,是Redis官方推荐的高可用性(HA)解决方案,当用Redis做Master-slave的高可用方案时,假如master宕机了,Redis本身(包括它的很多客户端)都没有实现自动进行主备切换,而Redis-sentinel本身也是一个独立运行的进程,它能监控多个master-slave集群,发现master宕机后能进行自动切换。
它的主要功能有以下几点:
很显然,只使用单个sentinel进程来监控redis集群是不可靠的,当sentinel进程宕掉后(sentinel本身也有单点问题,single-point-of-failure)整个集群系统将无法按照预期的方式运行。所以有必要将sentinel集群,这样有几个好处:
另一个相关的概念:脑裂问题
指的是一个系统中由于网络分区等原因,出现了两个master,需要关注此问题
如上图所示,Redis为了保证高可用性,采用Master-slave形式部署,采用AOF或RDB进行持久化,采用集群culster机制来分布式存储。多个哨兵,不仅同时监控主从数据库,而且哨兵之间互为监控,并且哨兵不会存在单点问题。哨兵会监控主从Redis服务器是否正常,当发现有问题时,会进行通知,并进行故障转移。当监控到master节点宕机后,会进行maser选举出新的master节点,并进行主从切换,并将其他节点作为新选举出的master节点的slave。哨兵Sentinel用到的分布式一致性算法是Raft分布式算法。
redis利用比较易于实现的raft协议实现了节点宕机的自动化处理,保障了集群的高可用性。Raft协议与paxos算法类似,但比paxos算法好理解,我们都知道,paxos算法是经典的分布式一致性算法,zookeeper的ZAB协议也是在其基础上设计的。ZAB,paxos,Raft都是采用过半策略。关于相关分布式算法,我会在后边的系列,Zookeeper系列进行介绍。对于Redis的监控过程,Leader选举,主备切换,数据同步等过程,跟Zookeeper类似,我们可以把zookeeper的实现方式与其进行类比
摘自:
Redis 原理及应用(3)–内存淘汰机制、主从同步原理,HA策略(哨兵机制)分析
简单来说,一个物理节点与多个虚拟节点映射,在hash的时候,使用虚拟节点数目而不是物理节点数目。当物理节点变化的时候,虚拟节点的数目无需变化,只涉及到虚拟节点的重新分配。而且,调整每个物理节点对应的虚拟节点数目,也就相当于每个物理节点有不同的权重。
无虚拟节点:
加入虚拟节点,防止雪崩:
更多:
什么是一致性Hash算法
redis集群原理(哈希槽、主从复制等技术)
更多:
Redis集群方案及实现
Redis cluster集群:原理及搭建
三张图秒懂Redis集群设计原理
Redis详解之-集群方案:高性能(使用原生Redis Cluster)
Redis系列九:redis集群高可用
redis集群搭建
参考:
Redis 原理及应用(4)–Redis应用场景分析
分布式锁的几种使用方式(redis、zookeeper、数据库)
分布式锁简单入门以及三种实现方式介绍