缓存与数据库数据一致性:旁路缓存、读写穿透和异步写入模式解析

旁路缓存模式、读写穿透模式和异步缓存写入模式是三种常见的缓存使用模式,以下是对三种经典缓存使用模式在缓存与数据库数据一致性方面更全面的分析:

一、旁路缓存模式(Cache - Aside Pattern)

1.数据读取流程

  • 应用程序首先向缓存发送读取请求,检查所需数据是否在缓存中。
  • 如果缓存命中,直接从缓存中获取数据并返回给应用程序,这能极大提高读取速度,减少数据库的负载。
  • 若缓存未命中,应用程序接着向数据库发送读取请求,从数据库获取数据。获取到数据后,一方面将数据返回给应用程序,另一方面把数据写入缓存,同时可以设置缓存数据的过期时间,以便在数据更新后能及时从数据库重新获取最新数据。

2.数据写入流程

  • 当应用程序要更新数据时,首先更新数据库中的数据,确保数据库作为数据的可靠来源得到及时更新。
  • 在数据库更新成功后,立即删除缓存中对应的旧数据。这样做是为了让下次读取该数据时,能从数据库获取到最新数据并更新到缓存中,保证缓存数据的时效性。

3.一致性分析

  • 优点
    • 实现相对简单,在正常情况下能较好地保证数据一致性。以数据库为数据的权威来源,缓存主要用于加速读取,通过先更新数据库再删除缓存的操作顺序,多数情况下能确保缓存数据要么是最新的,要么不存在,等待下次读取时更新。
    • 读性能优化明显,缓存命中时能快速响应读取请求,减轻数据库压力,适用于读多写少的场景。
  • 缺点
    • 在高并发场景下可能出现数据不一致问题。例如,两个并发更新操作同时对同一数据进行修改,若操作 A 先更新数据库但在删除缓存前,操作 B 更新数据库并先于操作 A 删除缓存,接着操作 A 再删除缓存,此时缓存中无最新数据,读取请求可能获取到旧数据,直到下次缓存更新。
    • 缓存数据的过期时间设置较为关键,若设置过长,可能导致缓存数据长时间不一致;设置过短,则会增加数据库的读取压力。

4.代码实例

import redis.clients.jedis.Jedis;
import java.sql.*;public class CacheAsidePattern {private static final String REDIS_HOST = "localhost";private static final int REDIS_PORT = 6379;private static final String DB_URL = "jdbc:mysql://localhost:3306/testdb";private static final String DB_USER = "root";private static final String DB_PASSWORD = "password";public static void main(String[] args) {try (Jedis jedis = new Jedis(REDIS_HOST, REDIS_PORT);Connection conn = DriverManager.getConnection(DB_URL, DB_USER, DB_PASSWORD)) {// 创建表createTable(conn);// 插入示例数据insertSampleData(conn);// 测试读取String userName = getUser(jedis, conn, 1);System.out.println("用户姓名: " + userName);// 测试更新updateUser(jedis, conn, 1, "Bob");userName = getUser(jedis, conn, 1);System.out.println("更新后用户姓名: " + userName);} catch (SQLException e) {e.printStackTrace();}}private static void createTable(Connection conn) throws SQLException {String createTableSQL = "CREATE TABLE IF NOT EXISTS users (" +"id INT PRIMARY KEY, " +"name VARCHAR(255))";try (Statement stmt = conn.createStatement()) {stmt.executeUpdate(createTableSQL);}}private static void insertSampleData(Connection conn) throws SQLException {String insertSQL = "INSERT IGNORE INTO users (id, name) VALUES (1, 'Alice')";try (PreparedStatement pstmt = conn.prepareStatement(insertSQL)) {pstmt.executeUpdate();}}private static String getUser(Jedis jedis, Connection conn, int userId) {String key = "user:" + userId;String user = jedis.get(key);if (user != null) {System.out.println("从缓存中获取数据");return user;} else {try (PreparedStatement pstmt = conn.prepareStatement("SELECT name FROM users WHERE id = ?")) {pstmt.setInt(1, userId);ResultSet rs = pstmt.executeQuery();if (rs.next()) {user = rs.getString("name");jedis.set(key, user);System.out.println("从数据库中获取数据并写入缓存");return user;}} catch (SQLException e) {e.printStackTrace();}return null;}}private static void updateUser(Jedis jedis, Connection conn, int userId, String newName) {String key = "user:" + userId;try (PreparedStatement pstmt = conn.prepareStatement("UPDATE users SET name = ? WHERE id = ?")) {pstmt.setString(1, newName);pstmt.setInt(2, userId);pstmt.executeUpdate();jedis.del(key);System.out.println("数据库更新并删除缓存");} catch (SQLException e) {e.printStackTrace();}}
}    

二、读写穿透模式(Write - Through Pattern)

1.数据读取流程

  • 应用程序向缓存发送读取请求,尝试从缓存中获取数据。
  • 如果缓存命中,直接从缓存中返回数据给应用程序。
  • 若缓存未命中,应用程序从数据库读取数据,读取到数据后,将数据返回给应用程序,同时将数据写入缓存,且写入缓存操作是同步进行的,确保数据在缓存和数据库中同时更新。

2.数据写入流程

  • 当应用程序执行写操作时,会同时向缓存和数据库发送更新请求。先将数据写入缓存,然后由缓存负责将数据同步到数据库,通常通过缓存的写入操作触发对数据库的写入,保证缓存和数据库数据的实时同步。

3.一致性分析

  • 优点
    • 能严格保证数据一致性,每次读写操作都确保缓存和数据库的数据同步更新,两者就像一个整体,任何一方的更新立即反映到另一方,不存在数据延迟或不一致的情况。
    • 对于读写操作较为均衡的场景,该模式能较好地适应,不会出现因写操作频繁导致缓存与数据库数据不一致的问题。
  • 缺点
    • 由于缓存和数据库是不同存储系统,其写入性能和可靠性存在差异,可能出现缓存写入成功但数据库写入失败的情况,导致数据不一致。
    • 为保证一致性引入的补偿机制,如重试机制或事务机制,会增加系统复杂性和开发成本。同时,同步写入操作可能会降低写操作的性能,因为需要等待数据库写入完成才能返回结果。

4.代码实例

import redis.clients.jedis.Jedis;
import java.sql.*;public class WriteThroughPattern {private static final String REDIS_HOST = "localhost";private static final int REDIS_PORT = 6379;private static final String DB_URL = "jdbc:mysql://localhost:3306/testdb";private static final String DB_USER = "root";private static final String DB_PASSWORD = "password";public static void main(String[] args) {try (Jedis jedis = new Jedis(REDIS_HOST, REDIS_PORT);Connection conn = DriverManager.getConnection(DB_URL, DB_USER, DB_PASSWORD)) {// 创建表createTable(conn);// 插入示例数据insertSampleData(conn);// 测试读取String userName = getUser(jedis, conn, 1);System.out.println("用户姓名: " + userName);// 测试更新updateUser(jedis, conn, 1, "Bob");userName = getUser(jedis, conn, 1);System.out.println("更新后用户姓名: " + userName);} catch (SQLException e) {e.printStackTrace();}}private static void createTable(Connection conn) throws SQLException {String createTableSQL = "CREATE TABLE IF NOT EXISTS users (" +"id INT PRIMARY KEY, " +"name VARCHAR(255))";try (Statement stmt = conn.createStatement()) {stmt.executeUpdate(createTableSQL);}}private static void insertSampleData(Connection conn) throws SQLException {String insertSQL = "INSERT IGNORE INTO users (id, name) VALUES (1, 'Alice')";try (PreparedStatement pstmt = conn.prepareStatement(insertSQL)) {pstmt.executeUpdate();}}private static String getUser(Jedis jedis, Connection conn, int userId) {String key = "user:" + userId;String user = jedis.get(key);if (user != null) {System.out.println("从缓存中获取数据");return user;} else {try (PreparedStatement pstmt = conn.prepareStatement("SELECT name FROM users WHERE id = ?")) {pstmt.setInt(1, userId);ResultSet rs = pstmt.executeQuery();if (rs.next()) {user = rs.getString("name");jedis.set(key, user);System.out.println("从数据库中获取数据并写入缓存");return user;}} catch (SQLException e) {e.printStackTrace();}return null;}}private static void updateUser(Jedis jedis, Connection conn, int userId, String newName) {String key = "user:" + userId;try {conn.setAutoCommit(false);jedis.set(key, newName);try (PreparedStatement pstmt = conn.prepareStatement("UPDATE users SET name = ? WHERE id = ?")) {pstmt.setString(1, newName);pstmt.setInt(2, userId);pstmt.executeUpdate();}conn.commit();System.out.println("缓存和数据库同时更新");} catch (SQLException e) {try {conn.rollback();} catch (SQLException rollbackEx) {rollbackEx.printStackTrace();}System.out.println("更新失败: " + e.getMessage());}}
}    

三、异步缓存写入模式(Write - Behind Caching Pattern)

1.数据读取流程

  • 与前两种模式类似,应用程序首先从缓存中读取数据。
  • 若缓存命中,直接返回数据。
  • 缓存未命中时,从数据库读取数据并返回给应用程序,同时将数据写入缓存。

2.数据写入流程

  • 写操作发生时,应用程序只将数据写入缓存,然后由缓存负责在后台异步地将数据批量写入数据库。可以根据一定的策略,如达到一定的写入次数或经过一定的时间间隔,将缓存中的数据批量刷写到数据库。

3.一致性分析

  • 优点
    • 写性能极高,应用程序无需等待数据库写入完成即可快速响应写请求,能显著提高系统吞吐量,适用于写操作频繁的场景,如日志记录、实时数据采集等。
    • 通过批量写入数据库,减少了数据库的写入次数,降低了数据库的 I/O 压力,有助于提高数据库的性能和稳定性。
  • 缺点
    • 数据一致性问题较为严重。由于数据是异步写入数据库的,在写入缓存后到写入数据库之前的时间段内,若发生系统故障、缓存数据丢失或缓存服务崩溃等情况,可能导致数据丢失,破坏数据一致性。
    • 为保证数据一致性采取的措施,如持久化缓存、合理设置缓存刷写策略、系统恢复时的数据恢复操作等,增加了系统的复杂性和运维成本。同时,还需考虑数据库写入的并发控制,避免数据冲突和不一致。

4.代码实例

import redis.clients.jedis.Jedis;
import java.sql.*;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;public class WriteBehindCachingPattern {private static final String REDIS_HOST = "localhost";private static final int REDIS_PORT = 6379;private static final String DB_URL = "jdbc:mysql://localhost:3306/testdb";private static final String DB_USER = "root";private static final String DB_PASSWORD = "password";private static final int FLUSH_INTERVAL = 5; // 每 5 秒刷写一次public static void main(String[] args) {try (Jedis jedis = new Jedis(REDIS_HOST, REDIS_PORT);Connection conn = DriverManager.getConnection(DB_URL, DB_USER, DB_PASSWORD)) {// 创建表createTable(conn);// 插入示例数据insertSampleData(conn);// 启动异步刷写任务ScheduledExecutorService executor = Executors.newScheduledThreadPool(1);executor.scheduleAtFixedRate(() -> flushCacheToDB(jedis, conn), 0, FLUSH_INTERVAL, TimeUnit.SECONDS);// 测试读取String userName = getUser(jedis, conn, 1);System.out.println("用户姓名: " + userName);// 测试更新updateUser(jedis, conn, 1, "Bob");userName = getUser(jedis, conn, 1);System.out.println("更新后用户姓名: " + userName);// 关闭线程池executor.shutdown();} catch (SQLException e) {e.printStackTrace();}}private static void createTable(Connection conn) throws SQLException {String createTableSQL = "CREATE TABLE IF NOT EXISTS users (" +"id INT PRIMARY KEY, " +"name VARCHAR(255))";try (Statement stmt = conn.createStatement()) {stmt.executeUpdate(createTableSQL);}}private static void insertSampleData(Connection conn) throws SQLException {String insertSQL = "INSERT IGNORE INTO users (id, name) VALUES (1, 'Alice')";try (PreparedStatement pstmt = conn.prepareStatement(insertSQL)) {pstmt.executeUpdate();}}private static String getUser(Jedis jedis, Connection conn, int userId) {String key = "user:" + userId;String user = jedis.get(key);if (user != null) {System.out.println("从缓存中获取数据");return user;} else {try (PreparedStatement pstmt = conn.prepareStatement("SELECT name FROM users WHERE id = ?")) {pstmt.setInt(1, userId);ResultSet rs = pstmt.executeQuery();if (rs.next()) {user = rs.getString("name");jedis.set(key, user);System.out.println("从数据库中获取数据并写入缓存");return user;}} catch (SQLException e) {e.printStackTrace();}return null;}}private static void updateUser(Jedis jedis, Connection conn, int userId, String newName) {String key = "user:" + userId;jedis.set(key, newName);System.out.println("数据写入缓存,等待异步刷写数据库");}private static void flushCacheToDB(Jedis jedis, Connection conn) {try {conn.setAutoCommit(false);// 模拟获取所有用户缓存数据// 实际应用中需要根据业务逻辑获取待刷写的数据String keyPattern = "user:*";for (String key : jedis.keys(keyPattern)) {int userId = Integer.parseInt(key.split(":")[1]);String userName = jedis.get(key);try (PreparedStatement pstmt = conn.prepareStatement("UPDATE users SET name = ? WHERE id = ?")) {pstmt.setString(1, userName);pstmt.setInt(2, userId);pstmt.executeUpdate();}}conn.commit();System.out.println("缓存数据刷写到数据库");} catch (SQLException e) {try {conn.rollback();} catch (SQLException rollbackEx) {rollbackEx.printStackTrace();}System.out.println("刷写失败: " + e.getMessage());}}
}    

三种经典缓存使用模式在缓存与数据库数据一致性方面各有优劣。在实际应用中,需要根据业务对数据一致性的严格程度、读写操作的频率和性能要求等因素,综合权衡选择合适的缓存模式,并通过相应的技术手段和策略来最大程度地保障数据一致性。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/bicheng/78397.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【ESP32S3】 下载时遇到 libusb_open() failed 解决方案

之前写过一篇 《VSCode 开发环境搭建》 的文章,很多小伙伴反馈说在下载固件或者配置的时候会报错,提示大多是 libusb_open() failed ...... : 这其实是由于 USB 驱动不正确导致的,准确来说应该是与 ESP-IDF 中内置的 OpenOCD 需要…

ISCTF2024-misc(部分)

前言 之前写的,一直没发,留个记录吧,万一哪天记录掉了起码在csdn有个念想 1.少女的秘密花园 打开是个图片 随波逐流binwalk一下分离得到一个zip,解压得到base_misc发现是zip 爆破得到密码 解压得到一个txt,将里面的…

word内容使用python替换

拥有一个固定的word文件,类似模板 比如写一个测试计划,大多数内容都是通用,只需要改改软件名称,人员等等,数量多起来的情况下就可以使用代码 # 导入 Document 类,用于处理 Word 文档 from docx import Do…

py语法基础理解

条件判断 只有if-else等我语句,Python不支持switch语句 单if语句 if 条件语句: 条件为真时执行的内容 if-else语句 if 条件语句: 条件为真时执行的内容 else: 条件为假时执行的内容 if-elif语句 else if if 条件语句1: 条件语句1为真时执行的内容 elif 条件语句…

SQL进阶知识:九、高级数据类型

今天介绍下关于高级数据类型的详细介绍,并结合MySQL数据库提供实际例子。 在MySQL中,高级数据类型主要用于处理复杂的数据结构,如JSON、XML和空间数据。这些数据类型提供了更强大的功能,可以满足现代应用程序对数据存储和处理的多…

Linux软硬链接和动静态库(20)

文章目录 前言一、软硬链接基本认知实现原理应用场景取消链接ACM时间 二、动静态库认识库库的作用 三、制作静态库静态库的打包静态库的使用 四、制作动态库动态区的打包动态库的链接与使用动态库的链接原理 总结 前言 我有款非常喜欢玩的游戏,叫做《饥荒》&#xf…

【鸿蒙HarmonyOS】深入理解router与Navigation

5. 路由 1.页面路由(router模式) 1.概述 页面路由指的是在应用程序中实现不同页面之间的跳转,以及数据传递。 我们先明确自定义组件和页面的关系: 自定义组件:Component 装饰的UI单元,页面:即应用的UI…

Apache SeaTunnel:新一代开源、高性能数据集成工具

Apache SeaTunnel 是一款开源、分布式、高性能的数据集成工具,可以通过配置快速搭建数据管道,支持实时海量数据同步。 Apache SeaTunnel 专注于数据集成和数据同步,主要旨在解决数据集成领域的常见问题: 数据源多样性&#xff1a…

CF-Hero:自动绕过CDN找真实ip地址

CF-Hero:自动绕过CDN找真实ip地址 CF-Hero 是一个全面的侦察工具,用于发现受 Cloudflare 保护的 Web 应用程序的真实 IP 地址。它通过各种方法执行多源情报收集。目前仅支持Cloudflare的cdn服务查找真实ip,但从原理上来说查找方法都是通用的…

React-组件和props

1、类组件 import React from react; class ClassApp extends React.Component {constructor(props) {super(props);this.state{};}render() {return (<div><h1>这是一个类组件</h1><p>接收父组件传过来的值&#xff1a;{this.props.name}</p>&…

谈谈接口和抽象类有什么区别?

接口&#xff08;interface&#xff09;和抽象类&#xff08;abstract class&#xff09;都是 Java 中常用的“抽象”工具&#xff0c;用来定义类的规范和结构&#xff0c;但它们有一些本质的区别。下面我用一个简单明了的表格 说明来帮你理解&#xff1a; 对比点抽象类&…

使用Nacos 打造微服务配置中心

一、背景介绍 Nacos 作为服务注册中心的使用方式&#xff0c;同时 Nacos 还可以作为服务配置中心&#xff0c;用于集中式维护各个业务微服务的配置资源。 作为服务配置中心的交互流程图如下。 这样设计的目的&#xff0c;有一个明显的好处就是&#xff1a;有利于对各个微服务…

OpenCv高阶(十一)——物体跟踪

文章目录 前言一、OpenCV 中的物体跟踪算法1、均值漂移&#xff08;Mean Shift&#xff09;&#xff1a;2、CamShift&#xff1a;3、KCF&#xff08;Kernelized Correlation Filters&#xff09;&#xff1a;4、MIL&#xff08;Multiple Instance Learning&#xff09;&#xf…

声音分离人声和配乐base,vocals,drums -从头设计数字生命第6课, demucs——仙盟创梦IDE

demucs -n htdemucs --two-stemsvocals 未来之窗.mp3 demucs -n htdemucs --shifts5 之.mp3demucs -n htdemucs --shifts5 -o wlzcoutspl 未来之窗.mp3 伴奏提取人声分离技术具有多方面的重大意义&#xff0c;主要体现在以下几个领域&#xff1a; 音乐创作与制作 创作便利…

使用若依二次开发商城系统-4:商品属性

功能3&#xff1a;商品分类 功能2&#xff1a;商品品牌 功能1&#xff1a;搭建若依运行环境前言 商品属性功能类似若依自带的字典管理&#xff0c;分两步&#xff0c;先设置属性名&#xff0c;再设置对应的属性值。 一.操作步骤 1&#xff09;数据库表product_property和pro…

操作指南:vLLM 部署开源大语言模型(LLM)

vLLM 是一个专为高效部署大语言模型&#xff08;LLM&#xff09;设计的开源推理框架&#xff0c;其核心优势在于显存优化、高吞吐量及云原生支持。 vLLM 部署开源大模型的详细步骤及优化策略&#xff1a; 一、环境准备与安装 安装 vLLM 基础安装&#xff1a;通过 pip 直接安装…

32.768kHz晶振详解:作用、特性及与其他晶振的区别

一、32.768kHz晶振的核心作用 实时时钟&#xff08;RTC&#xff09;驱动&#xff1a; 提供精确的1Hz时钟信号&#xff0c;用于计时功能&#xff08;如电子表、计算机CMOS时钟&#xff09;。 分频公式&#xff1a; 1Hz 32.768kHz / 2^15&#xff08;通过15级二分频实现&#x…

第3讲、大模型如何理解和表示单词:词嵌入向量原理详解

1. 引言 大型语言模型&#xff08;Large Language Models&#xff0c;简称LLM&#xff09;如GPT-4、Claude和LLaMA等近年来取得了突破性进展&#xff0c;能够生成流畅自然的文本、回答复杂问题、甚至编写代码。但这些模型究竟是如何理解人类语言的&#xff1f;它们如何表示和处…

【Java面试笔记:进阶】19.Java并发包提供了哪些并发工具类?

Java 并发包(java.util.concurrent)提供了一系列强大的工具类,用于简化多线程编程、提升并发性能并确保线程安全。 1. Java 并发包的核心内容 并发包概述:java.util.concurrent 包及其子包提供了丰富的并发工具类,用于简化多线程编程。主要组成部分: 高级同步结构:如 C…

Matlab数字信号处理——小波阈值法去噪分析系统

&#x1f527; 系统简介 本系统通过 MATLAB GUI 图形界面&#xff0c;集成了 小波阈值去噪算法 的各个核心模块&#xff0c;可以实现以下功能&#xff1a; 打开语音文件&#xff1a;支持常见音频格式读取&#xff1b; 模拟加噪&#xff1a;系统内置白噪声模拟功能&#xff0…