当前位置: 首页 > news >正文

【PmHub后端篇】PmHub 中缓存与数据库一致性的实现方案及分析

在软件开发项目中,缓存的使用十分普遍。缓存作为一种存储机制,能够暂时保存数据,从而加速数据的读取和访问。然而,当数据同时存在于缓存和数据库中时,如何保证两者的数据一致性成为了一个关键问题。在 PmHub 项目中,同样面临着这样的挑战,下面将详细介绍 PmHub 中保证缓存和数据库一致性的相关内容。

1 缓存的重要性

缓存可分为本地缓存和分布式缓存。本地缓存如 JDK 自带的 HashMap 和 ConcurrentHashMap,以及 Ehcache、Guava Cache、Spring Cache、Caffeine 等常见的本地缓存框架;分布式缓存如常用的 Redis,它可以单机或者集群部署在不同的服务器上。本地缓存与应用处于同一位置,而分布式缓存独立部署在不同服务器。无论是哪种缓存,其核心目的都是用空间换时间,通过存储可能重复使用或计算的数据,减少数据的重新获取或计算时间。

2 缓存一致性问题

导致缓存和数据库数据不一致的原因主要有以下几点:

  1. 缓存过期:缓存中的数据存在生命周期,过期后若未及时更新,会出现数据不一致情况。
  2. 写操作延迟:执行写操作时,数据库更新和缓存更新时间不同步,可能导致缓存中的数据不一致。
  3. 并发操作:多个并发操作同时进行,会出现竞态条件,从而导致缓存和数据库数据不一致。
  4. 缓存失效策略不当:使用不当的缓存失效策略,会使缓存中的数据无法及时更新,造成不一致。
  5. 网络延迟或故障:网络延迟或故障会导致缓存服务器和数据库之间通信出现问题,进而导致数据不一致。

3 常见的解决不一致问题的方案

  1. Cache Aside 模式:在读写操作中使用该模式,确保在写操作后及时失效缓存中的数据。
  2. 分布式锁:在并发写操作时使用分布式锁,保证同时只有一个操作能够更新缓存和数据库,避免竞态条件。
  3. 双写一致性:在写操作时同时更新数据库和缓存,以确保数据的一致性。
  4. 延迟双删:在写操作时,先删除缓存中的数据,更新数据库后,再次删除缓存中的数据,确保缓存中数据的一致性。
  5. 版本控制:在缓存和数据库中使用版本号或时间戳,确保数据更新时的一致性检查。
  6. 监控和告警:对缓存和数据库中的数据进行监控,发现不一致时及时告警并处理。

4 常见缓存更新策略

模式名称描述优点缺点使用场景
Cache Aside Pattern (旁路缓存模式)读取数据时先检查缓存,缓存未命中则从数据库读取并更新缓存。写入数据时先更新数据库,然后使缓存失效。- 读取性能高
- 实现简单
- 首次请求数据一定不在缓存问题
- 写操作较频繁的话会导致缓存中数据被频繁删除,会影响缓存命中率
数据读取频率高,写入频率较低的场景
Read/Write Through Pattern (读写穿透模式)所有的读写操作都通过缓存进行,缓存负责同步数据库。- 数据一致性好
- 实现了读写操作的统一
- 实现复杂
- 依赖缓存的高可用性
数据读取和写入频率均较高的场景
Write Behind Pattern (异步缓存写入)写操作首先更新缓存,然后异步地将数据写入数据库。- 写操作性能高
- 减少数据库压力
- 存在数据丢失的风险
- 数据一致性较差
写操作频繁,且对实时一致性要求不高的场景

4 Cache Aside 模式

4.1 Cache Aside 模式概述

读取数据时先检查缓存,缓存未命中则从数据库读取并更新缓存;写入数据时先更新数据库,然后使缓存失效。
在这里插入图片描述

4.2 Cache Aside 模式优点

  • 读取性能高:缓存命中时可直接从缓存读取数据,显著减少数据库访问压力。
  • 实现简单:模式逻辑清晰,容易理解和实现。
  • 灵活性强:程序员可根据需要灵活控制缓存的更新和失效策略,适应不同应用场景和需求。
  • 缓存利用率高:只有在缓存未命中时才从数据库读取数据并更新缓存,避免不必要的数据冗余。

4.3 Cache Aside 模式局限

  • 写操作较慢:每次写操作都需更新数据库并使缓存失效,操作过程较长,导致写操作性能较低。
  • 数据一致性挑战:在高并发环境下,缓存和数据库之间容易出现数据不一致的情况,如缓存失效不及时或更新顺序问题。
  • 缓存预热问题:初始时缓存为空,第一次读取时会有较大延迟,因此建议将热点数据直接放入缓存。
  • 复杂的失效策略:程序员需要设计合理的缓存失效策略,增加了实现和维护的复杂性。
  • 过期数据风险:若没有合适的失效机制,缓存中可能存在过期数据,返回错误或过时的信息给用户。

5 PmHub 中的实践

5.1 PmHub 中的数据读取

先查询缓存,若有数据则直接返回,若无则去数据库查询,然后写入缓存。例如根据键名查询参数配置信息即采用此方式:com.laigeoffer.pmhub.system.service.impl.SysConfigServiceImpl#selectConfigByKey

    /*** 根据键名查询参数配置信息* 查询逻辑:* 1. 优先从Redis缓存中获取配置值* 2. 缓存未命中时查询数据库* 3. 将数据库查询结果写入缓存(缓存穿透保护)* 4. 最终未找到配置时返回空字符串** @param configKey 参数key* @return 参数键值,未找到时返回空字符串*/@Overridepublic String selectConfigByKey(String configKey) {// 从Redis获取缓存值(使用系统配置专用前缀)String configValue = Convert.toStr(redisService.getCacheObject(getCacheKey(configKey)));// 命中缓存直接返回if (StringUtils.isNotEmpty(configValue)) {return configValue;}// 构造查询条件对象SysConfig config = new SysConfig();config.setConfigKey(configKey);// 查询数据库配置信息SysConfig retConfig = configMapper.selectConfig(config);// 数据库查询结果处理if (StringUtils.isNotNull(retConfig)) {// 更新缓存(设置永不过期)redisService.setCacheObject(getCacheKey(configKey), retConfig.getConfigValue());return retConfig.getConfigValue();}// 未找到配置时返回空字符串return StringUtils.EMPTY;}

5.2 PmHub 中的数据更新

更新数据时,先更新数据库信息,然后删除缓存信息,例如在批量删除参数信息的场景中即采用此方式:com.laigeoffer.pmhub.system.service.impl.SysConfigServiceImpl#deleteConfigByIds

    /*** 批量删除参数信息* 删除逻辑:* 1. 遍历所有待删除配置ID* 2. 对每个配置执行:*    - 校验是否为内置参数(禁止删除内置配置)*    - 执行数据库删除*    - 清理对应Redis缓存* 3. 遇到内置参数时抛出业务异常终止操作** @param configIds 需要删除的参数ID数组* @throws ServiceException 当尝试删除内置参数时抛出*/@Overridepublic void deleteConfigByIds(Long[] configIds) {// 遍历所有待删除配置IDfor (Long configId : configIds) {// 获取完整配置信息(用于后续校验和缓存清理)SysConfig config = selectConfigById(configId);// 内置参数校验(UserConstants.YES 表示内置系统参数)if (StringUtils.equals(UserConstants.YES, config.getConfigType())) {throw new ServiceException(String.format("内置参数【%1$s】不能删除 ", config.getConfigKey()));}// 执行数据库删除configMapper.deleteConfigById(configId);// 清理对应的Redis缓存(避免脏数据残留)redisService.deleteObject(getCacheKey(config.getConfigKey()));}}

6 总结

本文围绕软件开发中缓存使用展开,阐述缓存重要性,分析缓存与数据库数据不一致原因,介绍常见解决不一致的方案和缓存更新策略,展示PmHub项目中数据读取和更新实践,为确保数据一致性提供参考。

7 参考链接

  1. PmHub如何保证缓存和数据库的一致性
  2. 项目仓库(GitHub):https://github.com/laigeoffer/pmhub
  3. 项目仓库(码云):https://gitee.com/laigeoffer/pmhub (国内访问速度更快)
http://www.xdnf.cn/news/481933.html

相关文章:

  • c/c++的opencv的图像预处理讲解
  • 动态IP赋能业务增效:技术解构与实战应用指南
  • 1-10 目录树
  • 东方通2024年报分析:信创国产化龙头的蓬勃发展与未来可期
  • mysql的not exists走索引吗
  • uniapp-商城-60-后台 新增商品(属性的选中和页面显示)
  • MySQL——2、库的操作和表的操作
  • 割点与其例题
  • 管理工具导入CSV文件,中文数据乱码的解决办法。(APP)
  • 从类的外部访问静态成员:深入理解C#静态特性
  • C语言编程中的时间处理
  • 【学习笔记】机器学习(Machine Learning) | 第七章|神经网络(1)
  • Vue3中setup运行时机介绍
  • MyBatis—动态 SQL
  • 网安面试经(1)
  • MySQL8.x新特性:与mysql5.x的版本区别
  • SpringBoot--Bean管理详解
  • 3D生成新突破:阶跃星辰Step1X-3D开源,可控性大幅提升
  • python + flask 做一个图床
  • ThinkPad X250电池换电池芯(理论技术储备)
  • windows系统中下载好node无法使用npm
  • STM32控制电机
  • duxapp 2025-03-29 更新 编译结束的复制逻辑等
  • MySQL的触发器
  • ubuntu 20.04 更改国内镜像源-阿里源 确保可用
  • 适合学校使用的桌面信息看板,具有倒计时、桌面时钟、课程表、天气预报、自动新闻联播、定时关机、消息通知栏、随机点名等功能。
  • CMU-15445(4)——PROJECT#1-BufferPoolManager-Task#2
  • 【软件工程】符号执行与约束求解缺陷检测方法
  • MySQL性能优化
  • RAG-MCP:基于检索增强生成的大模型工具选择优化框架