Redis
持久化
AOF日志的实现
保存写操作命令到日志,默认是不开启的。
先执行写命令,再将当前命令写入硬盘中。
好处:
-
避免额外的检查开销
写命令可能会出现语法问题,如果不执行就直接保存的话,可能会无法检查语法的正确性。
-
不会阻塞当前写操作命令的执行
缺点:
- 执行写命令结束了,但是还未将命令写入硬盘,服务器宕机
- 写操作执行成功后才记录到AOF日志中,不会阻塞当前命令,但可能会阻塞一条命令
- Redis执行完写操作,会以命令追加的方式到server.aof_buf
- 通过write()系统调用,将aof_buf缓冲区的数据写入到AOF文件,拷贝到内核缓冲区page cache,等待内核将数据写入硬盘
三种写回硬盘策略:
-
Always:
- 每次执行完写操作命令,同步将AOF日志数据写回硬盘
- 会影响主进程的性能
-
Everysec:
- 写操作命令执行完,现将命令保存到AOF文件的内核缓冲区,每隔一秒再将缓冲区中的内容写回硬盘
- 两种方式的折中,避免Always策略性能的开销,也可以避免No策略数据的丢失
-
No:
- 写操作命令执行完,先写入AOF文件的内核缓冲区,再由OS决定什么时候写入硬盘
- 写回时机不可预知,若AOF日志内容没有写回硬盘,会造成不定数量数据的丢失
三种策略的实现,是调用fsync()函数的时机不同:
- Always策略是在每次写入AOF文件数据后,就执行fsync()函数
- 当写入是一个大key时,主线程在执行fsync()函数时,阻塞的时间会比较久
- Everysec策略是创建一个异步任务来执行fsync()函数
- 大key的持久化过程不会影响主线程
- No策略用不执行fsync()函数,不会影响主线程
AOF重写机制
AOF日志是一个文件,随着写操作命令的增多,文件的大小会越来越大,但日志文件过大会导致重启Redis,读AOF文件的内容恢复数据耗费时间过长。
为解决提出重写机制,当AOF文件的大小超过所设定的阈值,会启用AOF重写机制,压缩AOF文件。具体是读取当前数据库中的所有键值对,将每一个键值对用一条命令记录到新的AOF文件,等到全部记录完后,将新的AOF文件替换掉现有的AOF文件。
- 之前一条记录可能对应多个写操作,但许多历史记录可不保存。最需要保持键值对当前最新的状态,用一条命令去记录即可。
- 不直接复用现有的AOF文件,是因为如果在AOF重写过程中失败了,现有的AOF文件就会造成污染,不能恢复使用。
AOF后台重写
写入日志操作是在主进程中完成的,写入内容不多,所以一般不太影响命令操作。
但触发AOF重写较耗时,不能再放在主进程中完成。重写是由后台子进程bgrewriteaof完成,好处在于:
-
子进程进行AOF重写期间,主进程可以继续处理命令请求,不会造成主进程阻塞
-
子进程带主进程的数据副本
使用子进程而不是线程,因为如果使用线程,多线程之间会共享内存,在修改共享内存的时候,需要加锁来保证数据的安全,这样会导致性能降低。
如果使用子进程,创建子进程时,父子进程之前是共享内存数据的,但是共享内存只能以只读的方式完成,当任意一方修改了共享内存,会发生写时复制,此时父子进程之间就有了独立的数据副本,不需要加锁来保证数据安全。
实现:是OS会把主进程的页表复制一份给子进程,页表中记录着虚拟地址和物理地址的映射关系,但不会复制物理内存。
补充:
-
写时复制:
当子或父进程向这个内存发起写操作时,CPU会触发写保护中断,对相应的物理内存进行复制,重新设置内存映射关系,并将其权限设置为可读写。
-
可能存在的问题:
- 创建子进程的时候,OS复制父进程页表时,父进程阻塞的时间与其页表大小有关,页表越大阻塞时间越长。
- 创建完子进程后,如果其中有的修改了共享数据,发生了写时复制,会导致拷贝物理内存,内存越大,阻塞的时间越长。
当创建重写AOF的子进程是,父子进程共享物理内存,重写子进程只会对这个内存进行只读,如果主进程修改了已经存在的key-value,就会发生写时复制。
但是父和子进程之间,可能会存在数据不一致的问题,为解决Redis设置了一个AOF重写缓冲区,在重写AOF期间,当Redis执行完一个写命令,会同时将这个写命令写入到AOF缓冲区和AOF重写缓冲区。
当子进程完成AOF重写工作后,会向主进程发送一条信号,主进程收到信号之后,会调用信号处理函数,主要完成以下工作:
- 将AOF重写缓冲中的所有内容追加到新的AOF文件中,保持新旧两个AOF文件保存的数据库状态一致
- 新的AOF文件覆盖旧的AOF文件
RDB快照的实现
AOF和RDB都会各用一个日志文件来记录信息,但记录的内容不同:
- AOF文件的内容是操作命令
- RDB文件的内容是二进程数据
RDB快照记录某一瞬间的内存数据,记录的是实际数据,所以RDB的数据恢复效率会比AOF更高,只需将RDB文件读入内存即可,不需要额外执行操作命令的步骤来恢复数据。
快照的使用:
Redis提供了两个命令来生成RDB文件,分别是save和bgsave,区别如下:
- 执行save命令,会在主线程生成RDB文件,但如果写入RDB文件时间过长,会阻塞主线程
- 执行bgsave命令,会创建一个子进程来生成RDB文件,可避免主线程的阻塞
Redis会通过配置文件的选项来实现RDB文件的加载工作,Redis的快照是全量快照,每次执行快照会把内存中的所有数据都记录到磁盘中,所以执行快照操作不能太频繁,否则会对Redis的性能产生影响,但如果间隔时间太长,可能会导致丢失更多的数据。
执行快照的时候,数据如何修改
执行快照的时候,Redis依然可以继续处理操作命令,主要依靠写时复制技术完成(与AOF类似)。
在快照的过程中,如果主线程修改了内存数据,无论是否是共享的内存数据,RDB快照都无法写入主线程刚修改的数据,只能保存原本的内存数据。
所以如果系统在RDB快照文件创建完毕后崩溃,Redis会丢失主线程在快照期间修改的数据,如果当所有的共享内存都被修改,此时内存占用是原来的2倍。
AOF和RDB的合体
混合使用AOF日志和内存快照,也叫作混合持久化。
# 开启混合持久化功能
aof-use-rdb-preamble yes
在AOF日志重写时,子进程会先将与主进程共享内存数据,使用RDB快照的方式写入AOF文件中,之后主线程处理的操作命令会被记录在重写缓冲区中,其中的命令会以AOF方式写入AOF文件中,写完会通知主进程将新的含有的RDB格式和AOF格式的AOF文件替换旧的AOF文件。
前半部分是RDB格式的全量数据,后半部分是AOF格式的增量数据。这样可以实现加载的速度快,并且数据更少的丢失。
补充:
- 大key影响:
- 影响持久化
- 客户端超时阻塞
- 引发网络阻塞
- 阻塞工作线程
- 内存分布不均
- 避免大key:
- 在设计阶段最好将大key拆分为一个一个小key
- 定时检查Redis中是否存在大key,如果大key可以删除,使用unlink命令(不要使用del命令,删除时会阻塞主线程)
更新
过期删除策略
设置过期时间:
# 设置key在n秒后过期
expire <key> <n>
# 设置key在n毫秒后过期
pexpire <key> <n>
# 设置key在某个时间戳之后过期 (秒)
expireat <key> <n>
# 设置key在某个时间戳之后过期 (毫秒)
pexpireat <key> <n>
# 也可在设置字符串的时候,对key设置过期时间
# 精确到秒
set <key> <value> ex <n>
# 精确到毫秒
set <key> <value> px <n>
# 精确到秒
setex <key> <n> <value>
# 查看某个key存活的时间
ttl <key>
# 取消 key过期的时间
persist <key>
如何判断key已过期
过期字典中保存了数据库中所有key的过期时间
redisDb的结构
typedef struct redisDb {
dict *dict; /* 数据库键空间,存放着所有的键值对 */
dict *expires; /* 键的过期时间 */
....
} redisDb;
过期字典的数据结构如下:
- key是一个指针,指向某个键对象
- value是一个long long 类型的整数,保存了key过期的时间
在过期字典中查询key时:
- 检查key是否存在于过期字典中
- 不存在时,正常读取键值
- 存在时,获取key的过期时间,与当前系统时间对比,比系统时间大则没有过期,否则过期。
过期删除策略:
- 定时删除
- 在设置key的过期时间时,同时创建一个定时事件,当时间到达时,由事件处理器自动执行key的删除操作
- 优点:
- 保证过期的key尽快被删除,内存可以被尽快释放
- 缺点:
- 在过期key较多时,删除过期key可能会占用一部分CPU的时间
- 惰性删除
- 不主动删除过期键,当从数据库中访问key时,先检查key是否过期,如果过期,则删除
- 优点:
- 每次访问时,才会进行判断key是否过期,只占用很少的系统资源,对CPU友好
- 缺点:
- key可能过期,但是还保留在数据库中,占用一定内存空间
- 定期删除
- 每间隔一段时间随机从数据库中取出一定数量的key进行检查,并删除其中的过期key
- 优点:
- 限制删除操作执行的时长和频率,减少删除操作对CPU的影响的同时,删除了一定数量的过期键
- 缺点:
- 内存清理不如定时删除,系统资源使用比惰性删除多
- 难以确定删除操作执行的时长和频率
Redis过期删除策略:
惰性删除 + 定期删除
-
惰性删除
int expireIfNeeded(redisDb *db, robj *key) { // 判断 key 是否过期 if (!keyIsExpired(db,key)) return 0; .... /* 删除过期键 */ .... // 如果 server.lazyfree_lazy_expire 为 1 表示异步删除,反之同步删除; return server.lazyfree_lazy_expire ? dbAsyncDelete(db,key) : dbSyncDelete(db,key); }
访问或者修改key之前,调用expireIfNeeded函数对其进行检查,检查key是否过期。过期根据参数配置决定是异步还是同步删除。
-
定期删除
Redis默认每秒进行10次过期检查,抽取一定数量的key进行检查。随机抽取20个key判断是否过期
过期删除流程:
- 从过期字典中随机抽取20个key
- 检查20个key是否过期,并删除已过期的key
- 检查已过期的key的数量,超过检查的25%时,继续重复1,否则停止继续删除过期key,等待下一轮检查
内存淘汰策略
当Redis的运行内存超过Redis设置的最大内存后,会使用内存淘汰策略删除符合条件的key。
设置Redis的最大运行内存:
maxmemory <bytes>
设定最大运行内存。
内存淘汰策略:
- 不进行数据淘汰策略
- noevivtion:表示当运行内存超过最大设置内存时,不淘汰任何数据,但不允许写入数据,只能查询或者删除操作。
- 进行数据淘汰策略
- volatile-random:随机淘汰设置了过期时间的任意键值
- volatile-ttl:优先淘汰更早过期的键值
- volatile-lru:淘汰所有设置了过期时间的键值中,最久未使用的键值
- volatile-lfu:淘汰所有设置过期时间的键值中,最少使用的键值
- 在所有数据范围内进行淘汰
- allkeys-random:随机淘汰任意键值
- allkeys-lru:淘汰整个键值中最久未使用的键值
- allkeys-lfu:淘汰整个键值中最少使用的键值
Redis使用noeviction类型的淘汰策略,也可以通过参数设置内存淘汰策略:
config set maxmemory-policy <策略>
命令设置,设置之后立即生效,不需要重启Redis服务,但是重启Redis,配置会失效maxmemory-policy <策略>
,必须重启才能生效
LRU 和 LFU算法
- LRU算法:最近最少使用,淘汰最近最少使用的数据
- 在Redis的对象结构体中添加一个额外字段,记录此数据的最后一次访问时间
- 随机采样的方式来淘汰数据,选择随机值中最久没有使用的那个
- 但是不能解决缓存污染的问题
- LFU算法:最近最不常用,根据数据访问次数来淘汰数据
- 如果数据过去被多次访问,将来被访问的频率也更高
- 会记录每一个数据访问的次数,当一个数据被再次访问时,会增加该数据的访问次数
- 在Redis中有lru字段,其中一部分log_c用来记录key的访问频次,值越小,越容易被淘汰。
- key被访问时,会先对logc进行衰减,如果两次访问的时间差距较大,衰减的值就越大
- 之后根据概率对logc增加,logc越大的key,其值更难增加
高可用
主从复制的实现
所有数据都部署在一台服务器上,当这台服务器出现问题会导致在解决期间无法处理其他请求。所以将数据备份在其他服务器上,让这些服务器可以对外提供服务。
多台服务器之间的数据同步如下:
-
所有数据的修改都只在主服务器上
-
主服务器再将最新的数据同步给从服务器,保证主从服务器的数据一致。
-
主从服务器之间采用读写分离方式
确定主从服务器,并同步数据:
使用replicaof命令形成主服务器和从服务器的关系 replicaof <服务器 A 的 IP 地址> <服务器 A 的 Redis 端口号>
B服务器执行完命令,会成为A服务器端的从服务器,与主服务器进行第一次同步,具体过程如下:
- 第一个阶段:建立链接、协商同步
- 第二个阶段:主服务器同步数据给从服务器
- 第三个阶段:主服务器发送新写操作命令给从服务器
第一阶段:建立链接、协商同步
执行了replicaof命令后,从服务器会给主服务器发送psync命令,表示要进行数据同步。
psync命令包含两个参数,分别是主服务器的runID和复制进度offset
- runID:每个服务器在启动时会自动产生一个随机的ID来唯一标识自己,第一次同步时,不知道对方是谁,使用"?"来进行标记
- offset:表示复制进度,第一次同步时,设置为-1
主服务器收到psync命令后,会用FULLRESYNC作为响应命令返回给对方,其中会含有两个参数:主服务器的runID和主服务器目前的复制进度offset,从服务器收到之后,会进行记录。
FULLRESYNC是采用全量复制的方式,会将所有主服务器的数据都同步给从服务器。
第二阶段: 主服务器同步数据给从服务器
主服务器会执行bgsave命令来执行生成RDB文件,将文件发送给从服务器。
从服务器接收到RDB文件,会先清空当前的数据,然后载入RDB文件
主服务生成RDB文件是不会阻塞当前的主进程,会生成一个子进程来生成RDB文件,异步工作。
但是在生成RDB文件的过程中,并没有记录最新的写操作,此时主从服务器的数据就不一致。
为解决主服务器在下面这三个时间间隙中将受到的写操作命令,写入到replication buffer缓存区中:
- 主服务器生成RDB文件期间
- 主服务器发送RDB文件给从服务器期间
- 从服务器加载RDB文件期间
第三阶段:主服务器发送新写操作命令给从服务器
从服务器完成RDB的载入后,会回复一个确认消息给主服务器。主服务器会将replication buffer缓冲区中所有的记录的写操作命令发送给从服务器,保持主从服务器之间的数据一致性。
命令传播:
主从服务器完成第一次同步之后,双方之间会维护一个TCP长连接,后续主服务器所有写操作命令会传播给从服务器。
长连接可以避免频繁的TCP连接和断开带来的性能开销
分摊主服务器的压力:
主从服务器在第一次数据同步过程中,主服务器要生成RDB文件,传输RDB文件会占用主服务器的网络带宽,这些会对主服务器响应命令请求产生影响。
解决方法:将主服务器生成、传输RDB文件的压力,分摊给从服务器,从服务器再分摊给从服务器的从服务器。replicaof <目标服务器的IP> 6379
增量复制:
主从服务器之间在完成第一次同步之后,会基于长连接进行命令传播,但是网络可能延迟或者断开,导致此时主从服务器之间的数据不一致。
解决方式:网络断开后又恢复,主服务器只会将网络断开期间接收到的写操作的数据同步给从服务器。
过程步骤:
- 从服务器恢复网络之后,会发送psync命令给主服务器,此时命令中的offset参数不是-1。
- 主服务器收到命令后,用continue响应命令告诉从服务器接下来采用增量复制的方式同步数据。
- 主服务器将主从服务断线期间所执行的写命令,发送给从服务器,从服务器执行完成数据同步。
如何确定数据增量:
主服务器在进行命令传播时,会将写命令发送给从服务器,也会写入到repl_backlog_buffer缓冲区中,当断开的网络重新连接上时,从服务器会通过psync命令将自己的复制偏移量slave-repl_offset发送给主服务器,主服务器根据自己的master_repl_offset和slave_repl_offset之间的差距,来决定对从服务器执行哪种同步操作。
- 如果从服务器要读取的数据还在repl_backlog_buffer缓冲区中,则会采用增量同步的方式
- 如果不在缓冲区中,那么主服务器会采用全量同步的方式
repl_backlog_buffer:是一个环形缓冲区,用于主从服务器断连后,从中找到差异的数据
replication offset:标记上述缓冲区的同步进度,主从服务器有各自的偏移量,主服务器使用master_repl_offset来记录自己写的位置,从服务器使用slave_repl_offset来记录自己读到的位置
在网络恢复时,如果从服务器想读的数据已经被全部覆盖,主服务器会采用全量同步,为避免主服务器频繁地使用全量同步方式,环形缓冲区应该尽可能大一些,减小被覆盖掉的频率。
可以进行估算:second*write_size_per_second (second:从服务器断线后重新连接上主服务器所需的平均时间、write_size_per_second :主服务器平均每秒产生的写命令数据量大小)
补充:
-
怎么判断Redis某个节点是否正常工作
通过互相的ping-pong心态检测机制,如果一半以上的节点在ping一个节点的时候没有pong回应,集群会认为这个节点挂掉了,断开与这个节点的连接。
Redis主从节点发送的心态间隔不同,作用也有区别:
- Redis主节点默认间隔10秒对从节点发送ping命令,判断从节点的存活性和连接状态,可通过参数repl-ping-slave-period控制发送频率
- Redis从节点每隔1秒发送replconf ack{offset}命令,给主节点上报自身当前的复制偏移量:
- 实时监测主从节点网络状态
- 上报自身复制偏移量,检查复制数据是否丢失,如果从节点数据丢失,再从主节点的复制缓冲区中拉取丢失的数据。
-
主从复制架构中,过期key如何处理
主节点处理了一个key,或者通过淘汰算法淘汰了一个key,时间主节点模拟一条del命令发送给从节点,从节点收到命令后,删除key。
-
主从复制中两个buffer(replication buffer、repl backlog buffer)有什么区别:
- 出现的阶段不同:
- repl backlog buffer 是在增量复制阶段出现,一个主节点只分配一个repl backlog buffer;
- replication buffer是在全量复制阶段和增量复制阶段都会出现,主节点会给每个新连接的从节点,分配一个replication buffer
- 两个Buffer都有大小限制,缓冲区满后,发生的事情不同:
- repl backlog buffer满了,因为是环形结构,会直接覆盖起始位置数据
- replication buffer满了,会导致连接断开,删除缓存,从节点重新连接,重新开始全量复制
- 出现的阶段不同:
哨兵机制
监控主节点状态,当主节点挂了,自动将一个从节点切换为主节点,实现主从节点故障转移。
哨兵机制工作过程:
监控、选主、通知
-
判断主节点故障
哨兵每间隔1秒给所有主从节点发送ping命令,当主从节点收到ping命令后,会发送一个响应命令给哨兵,此时正常工作。
主节点或者从节点没有在规定时间内响应哨兵的PING命令,哨兵会将其标记为主观下线。
客观下线:只适用于主节点
原因:主节点可能并没有故障,可能只是因为主节点的系统压力比较大或者网络发送了拥塞,导致主节点没有在规定时间内响应哨兵的PING命令。
故为减少误判,用多个节点部署成哨兵集群,通过多个哨兵节点一起判断,可避免单个哨兵因为自身网络状况不好,误判主节点下线的情况。
当一个哨兵判断主节点为主观下线后,会向其他哨兵发起命令,其他哨兵根据自身和主节点的网络状况,做出赞同或者拒绝投票响应。当赞同的票数到达配置文件中的quorum配置项设定的值后,主节点会被哨兵标记为客观下线。
-
由哪个哨兵进行主从故障转移
哪个哨兵判断主节点为客观下线,这个哨兵就是候选者,成为候选者后会向其他哨兵发送命令,表明希望成为Leader执行主从切换,并让其他哨兵进行投票(只有一次投票机会),候选者要同时满足:
- 拿到半数以上的赞成票
- 拿到票数的同时大于等于哨兵配置文件中的quorum值
哨兵节点为什么至少有3个:
当只有两个哨兵时,其中一个出现故障,剩下的无法达到指定投票个数,无法选举成功。
quorum的值建议设置为哨兵个数的二分之一+1,哨兵节点的数量应该是奇数
-
主从故障转移的过程
- 在已经下线主节点属下的所有从节点中,挑选出一个从节点,并将其转换为主节点
- 让已下线主节点属下的所有从节点修改复制目标,修改为复制新主节点
- 将新主节点的IP地址和信息,通过发布者/订阅者机制通知给客户端
- 继续监视旧主节点,当旧主节点重新上线时,将其设置为新主节点的从节点
-
选出新主节点
在已下线主节点属下的所有从节点中,挑选一个状态良好、数据完整的从节点,向从节点发送命令将从节点转换为主节点。
首先要把已经下线的从节点过滤掉,将以往网络连接状态不好的从节点也过滤掉,进行三轮考察选择优胜者:
- 优先级最高的从节点胜出
- 复制进度最靠前的从节点胜出
- ID号小的从节点胜出
-
将从节点指向新主节点
将已下线主节点属下的所有从节点指向新主节点,可以通过向从节点发送slaveof命令来实现。
-
通知客户的主节点以更换
每个哨兵节点提供发布者/订阅者机制,客户端可以从哨兵订阅消息。客户端和哨兵建立连接后,客户端会订阅哨兵提供的频道。主从切换完后,哨兵会向+switch-master频道发布新主节点的IP地址和端口的消息
-
将旧主节点变为从节点
继续监视旧主节点,当旧主节点重新上线时,哨兵集群会向它发送slaveof命令,让其成为新主节点的从节点
-
哨兵集群如何组成
哨兵节点之间通过Redis的发布者/订阅者机制来相互发现
当哨兵A将自己的IP地址和端口信息发布到_sentinel_:hello频道上,哨兵B和C订阅了该频道,就直接可以从这个频道直接获取哨兵A的IP地址和端口号,哨兵A、B、C之间就可以建立网络连接。
哨兵集群如何知道从节点的信息:
主节点知道所有从节点的信息,哨兵会每10秒一次的频率向主节点发送INFO命令来获取所有的从节点的信息,获取从节点的信息后,会和每个从节点建立连接,并持续的对从节点进行监控。
缓存
缓存雪崩
因为Redis是内存数据库,将数据缓存在Redis里,相当于数据缓存在内存,内存的读写速度比磁盘块好几个数量级,可以提升系统性能。
缓存雪崩:
缓存雪崩:当大量缓存数据在同一时间过期或者Redis故障宕机时,此时有大量用户请求,无法在Redis中处理,全部请求都直接访问数据库,导致数据库压力骤增,造成数据库宕机,导致整个系统崩溃。
也就是:
- 大量数据同时过期
- Redis故障宕机
大量数据同时过期:
- 均匀设置过期时间
- 对缓存数据设置过期时间时可以加上一个随机数,可以保证数据不会在同一时间过期
- 互斥锁
- 当处理用户请求时,如果发现访问的数据不在Redis里,可以加一个互斥锁,保证同一时间只有一个请求来构建缓存,缓存构建完后,再释放锁。未能获取互斥锁的请求,可以等待锁释放后重新读取缓存,返回空值或者默认值。
- 实现互斥锁时,设置超时时间,防止请求拿到锁一直不释放。
- 后台更新缓存
- 缓存设置“永久有效,并将更新缓存的工作交由后台线程定时更新”。(当系统内存紧张的时候,有些缓存数据还是会被淘汰)
- 淘汰后,读取数据失败会返回空值,可能会认为数据丢失
- 后台线程负责定时更新缓存,也负责频繁地检测缓存是否有效
- 业务线程发现缓存数据失效后,通过消息队列发送一条消息通知后台线程更新缓存
- 缓存预热:将数据提前缓存,不会等待用户访问才触发。
Redis故障宕机
-
服务熔断或请求限流机制
暂停业务应用对缓存服务的访问,直接返回错误,不用再继续访问数据库,降低对数据库的访问压力,保证数据库系统的正常运行。
请求限流:只将少部分请求发送到数据库进行处理,再多的请求就在入口直接拒绝服务。
-
构建Redis缓存高可靠集群
通过主从节点的方式构建Redis缓存高可靠集群
缓存击穿
定义:如果缓存中的某个热点数据过期了,此时大量的请求访问热点数据,就无法从缓存中读取,直接访问数据库,很容易被高并发的请求冲垮。
应对方案:
- 互斥锁方案,保证同一时间只有一个业务线程更新缓存,未能获取互斥锁的请求,要么等待锁释放后重新读取缓存,要么就返回空值或者默认值。
- 不给热点数据设置过期时间,由后台异步更新缓存,或在热点数据准备过期前,提前通知后台线程更新缓存以及重新设置过期时间。
缓存穿透
缓存穿透:当用户访问数据时,既不在缓存中,也不在数据库中,导致请求在访问缓存时,发现缓存缺失。此时如果有大量的服务请求,数据库压力骤增。
存在两种情况:
- 业务误操作,缓存中的数据和数据库中的数据都被误删除,导致缓存和数据库中没有数据
- 黑客恶意攻击,故意大量访问某些读取不存在的数据的业务。
解决方案:
-
非法请求的限制
- 在API入口处判断请求参数是否合理,请求参数是否含有非法值、请求字段是否存在,如果是恶意请求,直接返回错误,避免进一步访问缓存和数据库。
-
缓存空值或者默认值
- 发现缓存穿透的现象,可以针对查询的数据,在缓存中设置一个空值或者默认值,直接返回给应用,不会继续查询数据库。
-
使用布隆过滤器快速判断数据是否存在,避免通过查询数据库来判断数据是否存在
-
在写入数据库数据时,使用布隆过滤器做个标记,在用户请求来到时,业务线程确认缓存失效后,通过查询布隆过滤器判断数据是否存在,不存在则不通过查询数据来判断数据是否存在。
-
布隆过滤器工作流程:
-
布隆过滤器由初始值都为0的位图数组和N个哈希函数两部分组成。在写入数据库数据时,在布隆过滤器中做个标记,下次查询数据是否在数据库时,只需要查询布隆过滤器。
-
布隆过滤器通过3个操作来完成标记:
- 使用N个哈希函数分别对数据做哈希计算,得到N个哈希值
- 将第一步得到的N个哈希值对位图数组的长度取模,得到每个哈希值在位图数组的对应位置
- 将每个哈希值在位图数组的对应位置的值设置为1
如果查询数据x,得到数据x的哈希值,判断位图数据组相应位置的值是否全为1,只要有一个为0则认为数据不在数据库中。
但是由于存在哈希冲突的可能性,所以查询布隆过滤器说明数据存在,并不一定证明数据库中真的存在这个数据,但查询的数据不存在,数据库中就一定不存在。
-
-
数据库和缓存如何保持一致性
无论是先更新缓存还是数据库,两个方案都存在并发问题,导致出现数据库和缓存的数据不一致的现象。
解决方法:
-
旁路缓存策略
-
更新数据的时候,不更新缓存,而是删除缓存中的数据,等到读取数据时,发现缓存中没有数据,再从数据库中读取数据,更新到缓存中。
-
可分为写策略和读策略
- 写策略:
- 更新数据库中的数据
- 删除缓存中的数据
- 读策略:
- 读取的数据命中了缓存,直接返回数据
- 读取的数据没有命中缓存,则从数据库中读取数据,然后将数据写入到缓存,并返回给用户
- 写策略:
-
先更新数据库,再删除缓存
- 会保存数据一致性,但是每次更新数据时,缓存的数据都会被删除,导致缓存的命中率下降
-
先删除缓存,再更新数据库
- 会造成缓存不一致,可以加上延迟双删
-
采用更新数据库 + 更新缓存的方案
-
实现方案:
-
再更新缓存前先加分布式锁,保证同一时间只运行一个请求更新缓存
-
更新完缓存后,给缓存加上较短的过期的时间,即使出现缓存不一致,缓存的数据也会很快过期
-
-
更新缓存有时比删除缓存代价更大 懒加载
-
-
解决方案:
-
重试机制 异步操作缓存
引入消息队列,将删除缓存要操作的数据加入到消息队列,由消费者来操作数据。
- 应用删除缓存失败,可以从消息队列帧重新读取数据,再次删除缓存,如果重试超过一定次数,还是没有成功,向业务层发送报错信息
- 删除缓存成功,把数据从消息队列中移除,避免重复操作。
-
订阅MySQL binlog,再操作缓存 异步操作缓存
- 更新数据库成功,会产生一条变更日志,记录在binlog中。
- 订阅binlog日志,拿到具体要操作的数据,再执行缓存删除
-
-