Redis缓存更新策略、详解并发条件下数据库与缓存的一致性问题以及消息队列解决方案

2023-11-12

0、前言

        我们知道,缓存由于在内存中,数据处理速度比直接操作数据库要快很多,因此常常将数据先读到缓存中,再进行查询、更新等操作。
        但与之而来的问题就是,内存中的数据不仅没有持久化,而且需要保证redis和数据库中数据的一致性,针对这个问题,redis如何保证这样的一致性有以下几种策略。

1、Write Back(写回)策略

        实际开发中最不常用的策略,它仅针对非敏感数据、一致性要求不强的数据,才有可能采用。实际开发不采用。

        Write Back(写回)策略先把数据读入redis,在更新数据的时候,只更新缓存,同时将缓存数据设置为脏的,然后立马返回,并不会更新数据库。对于数据库的更新,会通过批量异步更新的方式进行,例如设置定时任务进行更新。

        例如,对于博客浏览量这样的数据,我们采用写回策略,即使多个用户并发访问,我们每次只要把缓存中的浏览量更新即可,这种 写回策略 非常适合发生大量写操作的场景。

        也就是说,读写都在redis中进行,然后异步地更新回数据库来保持一致性、持久化。

        明显的缺点:带来的问题是,数据不是强一致性的,而且会有数据丢失的风险。因为缓存一般使用内存,而内存是非持久化的,所以一旦缓存机器掉电,就会造成原本缓存中的脏数据丢失。

2、Read/Write Through(读穿 / 写穿)策略

 (1)Read Through 策略

        先查询缓存中数据是否存在,如果存在则直接返回,如果不存在,则由缓存组件负责从数据库查询数据,并将结果写入到缓存组件,最后缓存组件将数据返回给应用。

(2)Write Through 策略

当有数据更新的时候,先查询要写入的数据在缓存中是否已经存在:

  • 如果缓存中数据已经存在,则更新缓存中的数据,并且由缓存组件同步更新到数据库中,然后缓存组件告知应用程序更新完成。
  • 如果缓存中数据不存在,直接更新数据库,然后返回;

 3、Cache Aside 旁路缓存 策略(实际开发常用)

        实际开发中,前两种策略都用不了,而采用旁路缓存策略,只不过有一些难度和注意点。

先说正确结论:

写策略的步骤:

  • 先更新数据库中的数据,再删除缓存中的数据。

读策略的步骤:

  • 如果读取的数据命中了缓存,则直接返回数据;
  • 如果读取的数据没有命中缓存,则从数据库中读取数据,然后将数据写入到缓存,并且返回给用户。

  (1)数据库和缓存都要更新?

        如果叛逆一点,更新数据库,更新缓存,会带来怎样的并发问题呢?
        借用小林coding的时序图如下:
        假设请求A、B同时对数据更新,顺序如下,在并发情况下,有可能先更新的请求A还没有更新完的时候,请求B就把缓存都更新完事了,然后A再更新缓存。
        可见,这样会造成数据库为2、缓存为1,也就是不一致状况。而如果先更新缓存再更新数据库也是同理的,仍然有数据不一致问题

         

(2)改进:只更新数据库,不更新缓存了,直接把缓存中的数据删了

        反正就算redis里没数据,查询时也会从数据库里查出来放在redis里,那我直接不更新了!把数据删了,到时候再读不就好了!这就是Cache Aside 策略。

        但有1个问题:更新数据库 删除缓存 这两个步骤的顺序该如何呢?

         <1> 假设我们先删除缓存,引用小林的图片:线程A先删除缓存再更新数据库为“21”,但由于更新写入数据库的速度是慢很多的,很可能中间出现了请求B在做查询,从而读取到还未更新的值“20”,并把缓存更细。从而导致不一致问题,这是不允许的。

          <2> 这次改邪归正,我们先更新数据库,再删除缓存:有人会觉得请求A如果去查询数据时,如果缓存未命中,在把数据写回redis的过程中,线程B过来先更新再删除,那就会导致如下的不一致情况了吗?!
        但实际上这样的情况很少,根本原因在于update数据库的速度 比 update缓存的速度 要慢得多。

        也就是说,黄色线条中间,更新缓存的时间间隔是很短的,而更新数据库的时间相对要慢得多,因此这种并发问题很罕见,还是能保证一致性的。

(3)再改进:如果“删除缓存”这个步骤失败了怎么办?

        为了确保万无一失,我们可以给缓存数据加了过期时间,就算在这期间存在缓存数据不一致,但过期时间到了会自动清除redis的key,这样也能避免删除失败的问题,达到最终一致。
        但问题在于,如果删除失败需要等待过期时间,数据的时效性、一致性就不强了,有可能明明更新了数据,查询显示出来却要过一段时间才生效,这对敏感业务来说是有影响的!

        解决方案使用消息队列实现异步处理

        在消费者线程中,尝试删除缓存。
        如果删除失败,则根据任务是否在消息队列中进行判断,若在队列中,则继续重试;否则报错。
        如果删除成功,才将任务从消息队列中移除。示例代码如下:

import redis.clients.jedis.Jedis;
import redis.clients.jedis.exceptions.JedisException;

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

public class CacheManager {
    private Jedis redisClient;
    private BlockingQueue<String> messageQueue;

    public CacheManager() {
        // 初始化Redis连接和消息队列
        redisClient = new Jedis("localhost");
        messageQueue = new LinkedBlockingQueue<>();

        // 创建并启动消费者线程
        Thread consumerThread = new Thread(new Consumer());
        consumerThread.start();
    }

    public void deleteCache(String key) {
        // 将任务添加到消息队列中
        String task = key;
        try {
            messageQueue.put(task);
            System.out.println("Added cache delete task for key: " + key);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    private class Consumer implements Runnable {
        @Override
        public void run() {
            while (true) {
                try {
                    String task = messageQueue.take();

                    // 尝试删除缓存
                    try {
                        // 删除缓存的操作,此处为示例代码,根据实际情况进行修改
                        redisClient.del(task);
                        System.out.println("Deleted cache for key: " + task);

                    } catch (JedisException e) {
                        // 删除失败,重试或报错
                        if (messageQueue.contains(task)) {
                            // 仍在队列中,继续重试
                            System.out.println("Failed to delete cache for key: " + task + ", retrying...");
                            messageQueue.put(task);
                        } else {
                            // 不在队列中,报错
                            System.out.println("Failed to delete cache for key: " + task + ", max retries exceeded. Reporting error...");
                        }
                    }

                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public static void main(String[] args) {
        CacheManager cacheManager = new CacheManager();

        // 示例使用
        cacheManager.deleteCache("user:1");
        cacheManager.deleteCache("user:2");
    }
}

4、小结

        本文通过介绍多种缓存更新策略,以及深入理解了实际开发中常用的旁路缓存策略所遇到的问题,并通过消息队列进行改进,实现了缓存与数据库的一致性。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Redis缓存更新策略、详解并发条件下数据库与缓存的一致性问题以及消息队列解决方案 的相关文章

  • Android Studio 0.8.2 URI 有一个权限组件

    我收到 Gradle 项目同步失败 消息 当我启动 Android Studio 时 当我尝试清理项目时 我收到 无法完成 Gradle 执行原因 URI 具有权限组件 我已经尝试了几件事 但仍然陷入困境 我将配置文件从用户文件夹中移出 并
  • 切换大小写错误。用于 Mac 上 Android 开发的 Eclipse IDE:将工作区合规性更改为 JRE 1.7

    当尝试在 android 项目中使用带有 switch string 的 Switch Case 时 我在 eclipse IDE 中收到错误消息 将工作区合规性更改为 JRE1 7 对于低于 1 7 的源级别 无法打开字符串类型的值 仅允
  • Java 8 文档日期时间教程错误

    The Oracle 临时查询教程页面 https docs oracle com javase tutorial datetime iso queries html显示此示例代码 Code TemporalQueries query Te
  • 为什么java中的BigInteger被设计成不可变的?

    在 java 中 BigInteger 是不可变的 但我想了解为什么 因为很多时候它用于进行大量计算 从而产生大量对象 所以 不让它变得不可变感觉有点直观 我想到的情况类似于字符串操作 然后是 StringBuilder 的选项 是否应该有
  • Java 可以启动 Windows UAC 吗?

    正如标题所说 我想知道用 Java 编写的程序是否可能 并且只有java 以管理员权限重新启动自己 最好是 jar 以本机 Windows UAC 的方式显示 为了使其对用户更可信 我做了功课 发现可以使用来完成此操作C 和 Java 之间
  • 什么时候数据库被称为嵌入式数据库?

    术语 嵌入式数据库 与 数据库 具有不同的含义吗 我见过的嵌入式数据库有两种定义 嵌入式数据库就像专门为 嵌入式 空间 移动设备等 设计的数据库系统一样 这意味着它们在紧张的环境中 内存 CPU 方面 可以合理地执行 嵌入式数据库就像不需要
  • java中使用多个分隔符分割字符串

    我正在研究一种数据挖掘算法 我需要使用多个单词来标记字符串 我有一个单独的文件 其中包含所有停用词 我需要做的是通过任何作为分隔符的单词 停用词 来标记输入字符串 例如 如果文件包含停用词 a is and of that 输入字符串变为
  • 为什么 java.util.concurrent.FutureTask 不可序列化

    我目前正在使用 Apache Wicket 我有一些 REST 调用 每个调用需要几秒钟 Wicket 只允许同步调用 ajax 所以我尝试使用 Future 和 Callable 这是我的课程的一部分 public abstract cl
  • 如何在 JavaFX 中设置滚动窗格的单位增量?

    The 滚动条 http docs oracle com javafx 2 api javafx scene control ScrollBar htmlJavaFX 中的类包含一个用于设置单位增量的属性 这就是我所追求的 但是我找不到如何
  • 仅在文件下载完成后设置 cookie。

    我有一个场景 我想告诉用户下载完成并提示关闭按钮 为此 我使用 jquery 插件来连续监视 cookie 以了解下载何时完成 我的问题是我想设置这个cookie fileDownload true and path 下载完成后立即进行 为
  • 更改 Java 字符串中的日期格式

    I ve a String代表一个日期 String date s 2011 01 18 00 00 00 0 我想将其转换为Date并将其输出到YYYY MM DD format 2011 01 18 我怎样才能实现这个目标 好的 根据我
  • Java KeyListener:按下两个键时如何执行操作?

    请看下面的代码 import java awt event import javax swing import java awt public class KeyCheck extends JFrame private JButton ch
  • 使用 Spring Java 配置自动装配 bean

    是否可以使用Spring的 Autowired用 Java 编写的 Spring 配置中的注释 例如 Configuration public class SpringConfiguration Autowired DataSource d
  • Rmi 错误 IllegalArgumentException、MarshalException

    为所有人上课 package Task2 import java rmi RemoteException import java rmi server UnicastRemoteObject public class IdCl extend
  • FirebaseAuth.getInstance().signOut() 不注销

    我尝试从 firebase 注销用户 但在关闭应用程序并再次打开后 用户仍然处于连接状态 我尝试从 firebase 定期注销用户 但没有解决问题 我想知道是什么导致了这个问题 logout setOnClickListener new V
  • 为数组生成随机索引

    我知道对于普通整数来说这是这样 但是有索引这样的东西吗 Random dice new Random int n dice nextInt 6 System out println n 你是什 么意思 数组索引是普通数字 所以你可以轻松地做
  • ACTION_MEDIA_BUTTON 的广播接收器不起作用

    我正在为 Android 操作系统版本 4 0 3 ICS 编写 Android 应用程序 问题是我没有从 BroadcastReceiver 的 onReceive 方法中的 Log d 获得输出 这意味着我的应用程序没有正确处理广播 我
  • 将字符串从代码页 1252 转换为 1250

    我怎样才能转换一个String将代码页 1252 中的字符解码为String在代码页 1250 中解码 例如 String str1252 String str1250 convert str1252 System out print st
  • 在 Android 中创建硬链接和符号链接

    我正在创建一个应用程序 我想在其中使用 Android 外部内存文件系统中的硬链接和符号链接 我尝试过使用命令 Os link oldpath newpath Os link oldpath newpath 但是 当我尝试这样做时 我收到此
  • 为什么 pagefactory 类在从另一个类初始化时返回 null

    在我的测试课上 我有DesiredCapabilities为 Appium 测试设置 在该课程中 我初始化了 BasePage 课程pagefactory元素 当我运行测试时 它按预期工作 现在 我尝试将 DesiredCapability

随机推荐

  • 生成doc文件,并压缩进文件夹

    导出业务人员日志 SuppressWarnings null RequestMapping value exportEsComDailyList public void exportEsComDailyList RequestParam n
  • 提升开发效率的必备技能:Spring集成Mybatis和PageHelper详解

    目录 引言 一 Spring集成MyBatis 1 1 pom依赖 1 2 配置文件 1 3 Spring整合MyBatis 1 3 1 配置自动扫描JavaBean 1 3 2 配置数据源 1 3 3 配置session工厂 1 3 4
  • js 数组

    1 数组的创建 var arrayObj new Array 创建一个数组 var arrayObj new Array size 创建一个数组并指定长度 注意不是上限 是长度 var arrayObj new Array element0
  • pytorch CPU与GPU模型参数相互加载

    文章目录 1 模型保存以及加载方法 2 单 GPU 和 单 CPU 参数 模型相互加载 3 多 GPU 模型 参数 4 单 GPU or CPU 模型加载多 GPU 参数 5 单 GPU or CPU 加载 多GPU模型 参数 6 多 GP
  • linux下jdk的安装

    目录 获取文件下载地址 官网获取文件下载地址 下载文件到指定目录下并修改改文件名 卸载已经存在的JDK 查看系统是否安装JDK 卸载JDK 安装JDK 赋予权限 安装JDK 配置JDK的环境变量 在配置文件的最底部加上以下配置 重新刷新配置
  • Java实现多线程下载 URL以及URLConnection

    主线程 public class MultiThreadDown public static void main String args throws Exception 初始化Downutil对象 final DownUtil downu
  • linux中解压tar.gz或zip类型的文件到具体文件夹

    zip对应的解压缩命令为unzip 命令格式 unzip 选项 压缩包名 选项 d 指定解压缩位置 示例 unzip d tmp test zip 将tar gz文件解压到指定的目录中 tar zxvf tmp tar gz C tmp 在
  • WEB常见的扫描器具体使用方法

    常用的WEB扫描器 1 awvs Acunetix Web Vulnerability Scanner 简称AWVS 是一款知名的网络漏洞扫描工具 它通过网络爬虫测试你的网站安全 检测流行安全漏洞 现已更新到10 下载地址 链接 https
  • Cisco_路由器基础命令

    Cisco 路由器基础命令 1 接口描述 路由器F0 1 或S0 1 接口命名为ABC Router config interface fastEthernet 0 1 进入到接口fastEthernet 0 1 Router config
  • mysql基础查询

    mysql基础查询 进程的相关信息 查看information schema数据库中的PROCESSLIST表来获取正在执行的查询进程的信息 该表包含了当前连接到MySQL服务器的所有进程的相关信息 包括进程ID id 和进程名称 name
  • JavaScript 简介 及引用方式

    js的引用方式 3种 1 行内引用 通过在开标签中的事件属性引用js的函数 2 内部引用 通过在script标签中编写js代码使用 1 script标签可以写在页面任何位置 2 script标签通常使用在body中的最后 或者body的后面
  • csv怎么保存开头数字0_【EXCEL必知必会】大基本功[4]—分列以及CSV文件处理

    阅读全文大概需要4 5分钟 本文是专栏 Excel必知必会 的第四篇教程 如果想了解专栏内容规划 请参阅开篇 温馨提示 如果您已经特别熟悉Excel 大可不必再看这篇文章 或只挑选部分 文中对Excel的说明和操作基于Mac Excel20
  • Git的原理及使用

    一 简述 在Git出现之前 大部分公司还是用SVN进行项目管理的 这里来对比一下 集中式 SVN 集中式的版本控制系统都有一个单一的几种管理的服务器 保存所有文件的修订版本 而协同工作的人们都通过客户端连接到这台服务器 取出最新的文件或者提
  • linux基础课程2-----熟练使用Linux系统命令

    目录 一 系统信息类命令是对系统的各种信息进行显示和设置的命令 1 dmesg命令 2 free命令 3 cal命令 4 clock命令 二 熟练使用进程管理类命令 1 ps命令 2 pidof命令 3 kill命令 4 killall命令
  • 用xpath获取html源码

    from lxml import html import requests url http navi cnki net knavi JournalDetail GetArticleList year 2018 issue 04 pykm
  • C++设计模式_02_面向对象设计原则

    文章目录 1 面向对象设计 为什么 2 重新认识面向对象 3 面向对象设计原则 3 1 依赖倒置原则 DIP 3 2 开放封闭原则 OCP 3 3 单一职责原则 SRP 3 4 Liskov 替换原则 LSP 3 5 接口隔离原则 ISP
  • 「python」关于sympy的使用笔记

    关于sympy的使用笔记 这是一篇使用python的符号计算工具包的笔记 随本人使用情况更新 1 变量 sympy中的变量可分为两种 常数变量 一般变量 from sympy import t symbols t real True con
  • 面面俱到!涵盖Java所有核心技术,阿里新产2023版Java面试核心突击手册太全了!

    程序员面试背八股 可以说是现在互联网开发岗招聘不可逆的形式了 其中最卷的当属Java 网上动不动就是成千上百道的面试题总结 你要是都能啃下来 平时技术不是太差的话 面试基本上问题就不会太大 这时候尴尬的现象就出现了 虽然八股文背的好并不能代
  • OpenBSD 安装

    OpenBSD 被誉世上最安全的系统 OpenBSD有最前沿的安全技术 适合于做防火墙和分布式环境下的私有网络服务 OpenBSD组每6个月发布一个新的发行版 即每年的 月 日和11月1日发布 你可以在此找到关于开发周期的更多信息 Open
  • Redis缓存更新策略、详解并发条件下数据库与缓存的一致性问题以及消息队列解决方案

    0 前言 我们知道 缓存由于在内存中 数据处理速度比直接操作数据库要快很多 因此常常将数据先读到缓存中 再进行查询 更新等操作 但与之而来的问题就是 内存中的数据不仅没有持久化 而且需要保证redis和数据库中数据的一致性 针对这个问题 r