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

发布于:2025-05-16 ⋅ 阅读:(20) ⋅ 点赞:(0)

在软件开发项目中,缓存的使用十分普遍。缓存作为一种存储机制,能够暂时保存数据,从而加速数据的读取和访问。然而,当数据同时存在于缓存和数据库中时,如何保证两者的数据一致性成为了一个关键问题。在 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 参数键值,未找到时返回空字符串
     */
    @Override
    public 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 当尝试删除内置参数时抛出
     */
    @Override
    public void deleteConfigByIds(Long[] configIds) {
        // 遍历所有待删除配置ID
        for (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 (国内访问速度更快)

网站公告

今日签到

点亮在社区的每一天
去签到