Redis缓存问题:穿透,击穿,雪崩,双写一致性等

2024-04-27 19:20

本文主要是介绍Redis缓存问题:穿透,击穿,雪崩,双写一致性等,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

Redis缓存问题:穿透,击穿,雪崩,双写一致性等

在高并发场景下,数据库往往是最薄弱的环节,我们通常选择使用redis来进行缓存,以起到缓冲作用,来降低数据库的压力,但是一旦缓存出现问题,也会导致数据库瞬间压力过大甚至崩溃,从而导致整个系统崩溃.今天就聊聊常见的redis缓存问题.

缓存击穿

缓存击穿一般指redis中的一个热点数据过期,导致大量请求直接访问数据库的情况,导致数据库瞬间压力过大甚至崩溃.

解决方案:

  1. 设置热点数据永不过期,这是一个不错的方案(要考虑业务特性,体量以及成本),前提是热点数据不能频繁发生改变,否则就会出现缓存污染.最好是根据一定的策略进行定时更新
  2. 重要接口限流,做好熔断和降级的准备,sentinel是个不错的选择
  3. 使用互斥锁,保证同一时刻只有一个线程可以访问数据库,这何尝不是一种限流呢

缓存穿透

缓存穿透指缓存和数据库中都没有的数据,用户不断发起请求.这种情况最可能就是有人试图恶意攻击系统

解决方案:

  1. 加校验:拦截非法请求,用户鉴权等
  2. redis缓存一个无效值,以防止对同一个key在数据库中的多次查询,但redis中可能会出现大量无效值,导致缓存污染,所以要将有效时间设置得短一些
  3. 添加布隆过滤器,在对数据库进行查询前,先通过布隆过滤器判断是否存在

一般来说这三种方案是同时使用的,第一层一般是校验,拦截部分非法用户和不合理请求(拦截不可能全部拦截而且如果攻击者通过某些方式掌握了大量合法用户呢),第二层是布隆过滤器,尽量避免对数据库的直接访问,但仍然有误判的可能性,第三层再缓存一个无效值,做到尽可能降低风险

缓存雪崩

缓存雪崩一般指reids中大批量数据在极短时间内(同时)过期,导致大量的查询数据库

解决方案:

  1. 在存储数据时,设置过期时间为一个随机值(也可以理解成给固定的过期时间加上一个随机值,类似密码学中的加盐),尽量保证不会有大量数据在同一时间过期
  2. 将热点数据尽量均匀地分布在不同的数据库中
  3. 多级缓存
  4. 设置热点数据永不过期(同缓存击穿中的)

缓存污染

缓存污染指的是缓存中一些只会被访问一次或者几次的的数据,被访问完后,再也不会被访问到,但这部分数据依然留存在缓存中,消耗缓存空间,也会在一定程度上影响redis的性能

redis缓存的maxmemory应该设置多大,这是一个关乎性能和成本的问题,需要根据实际情况进行权衡,但普遍推荐的是设置为总数据量的15%-30%(其他博客都这么写,而且范围还挺大,应该没什么问题🤔)

缓存淘汰策略

官方文档写了8种,如下图:
请添加图片描述

  • noeviction(不驱逐,即不淘汰)

    默认策略,当缓存达到maxmemory时,redis会拒绝所有写请求,并返回错误信息,此时redis已经进入只读模式,无法再进行写操作,但仍然可以进行读操作

  • allkeys-lru
    所有key采用LRU算法进行淘汰,即优先删除最近最少使用的key

  • allkeys-lfu
    所有key采用LFU算法进行淘汰,即优先删除最不常用的key

  • volatile-lru
    只淘汰设置了过期时间的key,采用LRU算法进行淘汰

  • volatile-lfu
    只淘汰设置了过期时间的key,采用LFU算法进行淘汰

  • allkeys-random
    所有key采用随机删除

  • volatile-random
    只淘汰设置了过期时间的key,采用随机删除

  • volatile-ttl
    删除过期字段设置为true和剩余最短生存时间(TTL)值的密钥。

缓存和数据库一致性

不管是先写MySQL数据库,再删除Redis缓存;还是先删除缓存,再写数据库,都有可能出现数据不一致的情况。举一个例子:如果删除了缓存Redis,还没有来得及写库MySQL,另一个线程就来读取,发现缓存为空,则去数据库中读取数据写入缓存,此时缓存中为脏数据。

更新缓存有四种设计模式: Cache aside, Read through, Write through, Write behind caching

缓存更新模式

Cache Aside(旁路缓存)
  • 失效:应用程序先从cache取数据,没有得到,则从数据库中取数据,成功后,放到缓存中。
  • 命中:应用程序从cache中取数据,取到后返回。
  • 更新:更新数据库, 更新/删除缓存

那么到底是更新缓存还是删除缓存呢?

如果读的频率高, 选择更新缓存, 这样可以提高缓存命中率

如果写的频率高, 选择删除缓存, 等到要读的时候再写入缓存, 可以减少无效的写操作和减少缓存数据量

Read Through(读穿)

Read Through 套路就是在查询操作中更新缓存,也就是说,当缓存失效的时候,Cache Aside是由调用方负责把数据加载入缓存,而Read Through则用缓存服务自己来加载,从而对应用方是透明的。

Write Through(写穿)

Write Through 套路和Read Through相仿,不过是在更新数据时发生。当有数据更新的时候,如果没有命中缓存,直接更新数据库,然后返回。如果命中了缓存,则更新缓存,然后再由Cache自己更新数据库(这是一个同步操作)

Write Behind Caching(写回)

在更新数据的时候,只更新缓存,不更新数据库,而我们的缓存会异步地批量更新数据库。这个设计的好处就是让数据的I/O操作飞快无比(因为直接操作内存嘛 ),因为异步,还可以合并对同一个数据的多次操作,所以性能的提高是相当可观的。但是,其带来的问题是,数据不是强一致性的,而且可能会丢失

一般而言(即不是特殊的业务场景), 我们选用Cache Aside(旁路缓存)模式, 下面我们的讨论也基于此模式.

操作缓存 & 操作数据库,哪个先?

针对删除缓存的情况:

如果采用先删除缓存,在更新数据库,会有什么后果?

  1. 线程A先把缓存的A数据删除,发生上下文切换
  2. 线程B想要读取A数据,发现缓存失效,从数据库读取A数据,并且更新缓存,
  3. 切换回线程A,线程A更新数据库数据。

至此,数据不一致的情况已经出现,数据库的写入较慢,容易出现此现象

先更新数据库,再删除缓存 怎么样?

  1. 线程A读取缓存A数据,发现缓存失效,所以去数据库读取,此时还未写入缓存,发生上下文切换
  2. 线程B更新数据库,并且删除对应的缓存,
  3. 切换回线程A,线程A将读取到的旧值,写入缓存

又出现了数据不一致的情况,但是相比先删除缓存,发生概率小很多,必须同时满足:

  • 读请求的时候,缓存刚好失效
  • 读请求 与 写请求 并发

所以在实际中很难出现请求 B 已经更新了数据库并且删除了缓存,请求 A 才更新完缓存的情况。所以推荐先更新数据库,再删除缓存

还是存在不一致的可能,怎么实现最终的一致性呢?

  1. 延迟双删策略
    延迟双删策略是分布式系统中数据库存储和缓存数据保持一致性的常用策略,但它不是强一致。其实不管哪种方案,都避免不了Redis存在脏数据的问题,只能减轻这个问题,要想彻底解决,得要用到同步锁和对应的业务逻辑层面解决。

不采用先更新数据库,在删除缓存的方案, 而是用先删除缓存,再更新数据库,里面的不一致问题,用延迟双删解决
1、先删除缓存 2、再写数据库 3、休眠500ms(统计线程读取数据和写缓存的时间) (休眠的作用是当前线程等其他线程读完了数据后写入缓存后,删除缓存) 4、再删除缓存

  1. 设置较短的缓存过期时间
    沿用先更新数据库,再删除缓存,就给缓存设置一个较短的过期时间(只要它够短,就不容易读到错误数据)
针对更新缓存的情况:(面试说上面那种即可)

如果采用先更新缓存,再更新数据库

  1. 线程A更新了缓存,此时发生上下文切换
  2. 线程B更新缓存,更新数据库,此时切换回线程A
  3. 线程A更新数据库

此时就出现了数据不一致的现象,数据库的写入较慢,容易出现此现象。

反过来,先更新数据库,再更新缓存呢?

  1. 线程A更新了数据库,还未写入缓存,发生上下文切换
  2. 线程B更新了数据库,并且写入缓存,此时切换到线程A
  3. 线程A把缓存写入,覆盖了之前线程B的缓存

至此,数据不一致的情况,又出现了。缓存的写入通常要远远快于数据库的写入,所以在实际中很难出现请求 B 已经更新了数据库并且更新了缓存,请求 A 才更新完缓存的情况。所以推荐先更新数据库,再更新缓存

保证操作原子性

操作数据库和操作缓存其实是两个操作,前面的所有分析都是建立在这两个操作都能同时执行成功,只要出现一个执行成功,一个执行失败的情况,就会造成数据库和缓存数据不一致的问题,会对敏感业务造成影响。下面就是两种解决方案.

队列+重试机制

请添加图片描述

  • 更新数据库数据;
  • 缓存因为种种问题删除失败
  • 将需要删除的key发送至消息队列
  • 自己消费消息,获得需要删除的key
  • 继续重试删除操作,直到成功

该方案有一个缺点,会对业务线代码造成大量的侵入。

基于订阅binlog的同步机制

请添加图片描述

本方案启动一个订阅程序去订阅数据库的binlog,获得需要操作的数据。在应用程序中,另起一段程序,获得这个订阅程序传来的信息,进行删除缓存操作。关键是使用canal框架订阅binlog

  • 要开启mysql的binlog,需要设置binlog_format为ROW模式,并且设置server_id,保证唯一性。修改my.cnf配置文件,重启mysql服务。
[mysqld]
log-bin=mysql-bin # 开启 binlog
binlog-format=ROW # 选择 ROW 模式
server_id=1 # 配置 MySQL replaction 需要定义,不要和 canal 的 slaveId 重复
  • 查看是否修改 Binlog 成功。
# 查看 binlog 日志是否开启
show variables like 'log_%';
  • MySQL 执行 SQL 语句创建 canal 单独使用的账号,用来进行 Binlog 的同步和监听
CREATE USER canal IDENTIFIED BY 'canal';  
GRANT SELECT, REPLICATION SLAVE, REPLICATION CLIENT ON *.* TO 'canal'@'%';
FLUSH PRIVILEGES;
Reference:
  1. 缓存更新的套路
  2. Java全栈知识体系

欢迎访问我的个人博客www.levitategu.cn

这篇关于Redis缓存问题:穿透,击穿,雪崩,双写一致性等的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/941321

相关文章

Redis客户端连接机制的实现方案

《Redis客户端连接机制的实现方案》本文主要介绍了Redis客户端连接机制的实现方案,包括事件驱动模型、非阻塞I/O处理、连接池应用及配置优化,具有一定的参考价值,感兴趣的可以了解一下... 目录1. Redis连接模型概述2. 连接建立过程详解2.1 连php接初始化流程2.2 关键配置参数3. 最大连

解决pandas无法读取csv文件数据的问题

《解决pandas无法读取csv文件数据的问题》本文讲述作者用Pandas读取CSV文件时因参数设置不当导致数据错位,通过调整delimiter和on_bad_lines参数最终解决问题,并强调正确参... 目录一、前言二、问题复现1. 问题2. 通过 on_bad_lines=‘warn’ 跳过异常数据3

解决RocketMQ的幂等性问题

《解决RocketMQ的幂等性问题》重复消费因调用链路长、消息发送超时或消费者故障导致,通过生产者消息查询、Redis缓存及消费者唯一主键可以确保幂等性,避免重复处理,本文主要介绍了解决RocketM... 目录造成重复消费的原因解决方法生产者端消费者端代码实现造成重复消费的原因当系统的调用链路比较长的时

深度解析Nginx日志分析与499状态码问题解决

《深度解析Nginx日志分析与499状态码问题解决》在Web服务器运维和性能优化过程中,Nginx日志是排查问题的重要依据,本文将围绕Nginx日志分析、499状态码的成因、排查方法及解决方案展开讨论... 目录前言1. Nginx日志基础1.1 Nginx日志存放位置1.2 Nginx日志格式2. 499

kkFileView启动报错:报错2003端口占用的问题及解决

《kkFileView启动报错:报错2003端口占用的问题及解决》kkFileView启动报错因office组件2003端口未关闭,解决:查杀占用端口的进程,终止Java进程,使用shutdown.s... 目录原因解决总结kkFileViewjavascript启动报错启动office组件失败,请检查of

Redis MCP 安装与配置指南

《RedisMCP安装与配置指南》本文将详细介绍如何安装和配置RedisMCP,包括快速启动、源码安装、Docker安装、以及相关的配置参数和环境变量设置,感兴趣的朋友一起看看吧... 目录一、Redis MCP 简介二、安www.chinasem.cn装 Redis MCP 服务2.1 快速启动(推荐)2.

java如何实现高并发场景下三级缓存的数据一致性

《java如何实现高并发场景下三级缓存的数据一致性》这篇文章主要为大家详细介绍了java如何实现高并发场景下三级缓存的数据一致性,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 下面代码是一个使用Java和Redisson实现的三级缓存服务,主要功能包括:1.缓存结构:本地缓存:使

SpringBoot 异常处理/自定义格式校验的问题实例详解

《SpringBoot异常处理/自定义格式校验的问题实例详解》文章探讨SpringBoot中自定义注解校验问题,区分参数级与类级约束触发的异常类型,建议通过@RestControllerAdvice... 目录1. 问题简要描述2. 异常触发1) 参数级别约束2) 类级别约束3. 异常处理1) 字段级别约束

Apache Ignite缓存基本操作实例详解

《ApacheIgnite缓存基本操作实例详解》文章介绍了ApacheIgnite中IgniteCache的基本操作,涵盖缓存获取、动态创建、销毁、原子及条件更新、异步执行,强调线程池注意事项,避免... 目录一、获取缓存实例(Getting an Instance of a Cache)示例代码:二、动态

Python错误AttributeError: 'NoneType' object has no attribute问题的彻底解决方法

《Python错误AttributeError:NoneTypeobjecthasnoattribute问题的彻底解决方法》在Python项目开发和调试过程中,经常会碰到这样一个异常信息... 目录问题背景与概述错误解读:AttributeError: 'NoneType' object has no at