记录一次删除缓存导致的接口部分数据为空

本文介绍了一个缓存策略调整案例,包括使用TP5框架通过自定义中间件为缓存添加标签以便于管理和统一清理,以及发现并解决因双层缓存策略导致的部分数据被错误缓存为空的问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前段时间接到个需求,要求现有的数据做到实时更新。目前相关接口都是等待缓存过期才能查到最新的数据,如果想做到实时更新,就要在生成数据后去触发删除缓存数据

目前的项目使用了TP5框架,开始想的是使用框架自带的Cache类的Tag进行缓存的标记与删除,看过源码后发现TP5的tag实现并不是很理想,其把缓存key使用逗号分隔存在一个tag中,并发场景中,可能会出现tag中的key丢失的情况,最后还是计划通过给缓存加前缀,然后通过原生keys匹配到具体的缓存key列表,然后使用del方法删除缓存。

为了在最小程度上修改代码,我通过自定义中间件判断path并确定是否需要打上标签的数据,然后判断Request的cacheTag属性进行标记处理,中间件的实现如下:

<?php
namespace app\common\middleware;
use think\Request;
class CacheTag
{
    protected $IndexApiList = [
        'home/index/index',
        'home/inside/testindex',
        'home/inside/nnindex'
    ];
    
    /**
     * 缓存标记处理:对于指定接口进行目录标记,方便统一清除缓存
     * @access public
     * @return mixed
     **/
    public function handle()
    {
        $request = Request::instance();
        $path = strtolower($request->module() . '/' . $request->controller() . '/' . $request->action());
        switch ($path) {
            case in_array($path, $this->IndexApiList) :
            	$request->cacheTag = 'tagIndex';
                return;
            default :
            	$request->cacheTag = false;
                return;
        }
    }
}

上面实现了对缓存的标记,删除缓存代码就不贴出来了。

测试了几遍没什么问题就发布到线上了,谁曾想线上出现了问题,有几个接口返回接口是空数组,并且空结果被缓存了,导致前台项目长时间获取不到数据。开始以为是删除缓存的问题,排查了一番之后并不是,于是又去翻了之前的代码,真的发现了问题,由于之前的缓存策略实现是model层、Controller层的双层缓存策略,同时缓存获取也很不规范,竟然获取了两次缓存(当然这不规范的代码都是前人留下的,这种操作着实很迷)

$cacheName = "home_index_index";
$cacheClass = new \think\Cache();
if($cacheClass->get($cacheName)){
   $res =  $cacheClass->get($cacheName);
}else{
   $res = $this->where($param)->select()->toArray();
   $cacheClass->set($cacheName,$res,22000);
}
return $res;

到这里我基本断定是因为获取两次缓存的原因了。设想一下生成空结果缓存的流程:
接口存在访问量,这时的model层在第一次获取到缓存的时候缓存是有的,这时生成指数任务触发删除缓存操作,那么model层第二次获取接口缓存的时候就返回的是空结果了。那么整个controller层执行完毕之后就会有部分从model层获取的数据为空的数据被缓存了下来
至此,问题找到了。

### 如何删除指定区间的缓存缓存管理中,针对不同类型的缓存实现机制,可以采取多种方式来清除特定区间的数据。以下是几种常见的方法及其适用场景: #### 1. **MyBatis 中的手动清理缓存** 当使用 MyBatis 进行数据库操作时,如果绑定了不同的 `.xml` 文件对应不同的接口,则每次调用 `sqlSession.getMapper()` 获取的映射器可能不一致,这会导致缓存失效[^1]。在这种情况下,可以通过显式调用 `clearCache()` 方法手动清当前命名间下的缓存。 ```java // 手动清理缓存示例 mapper.someQuery(); // 查询前的状态 session.clearCache(); // 清理当前 Mapper 的本地缓存 ``` 此外,还可以通过配置 SQL 映射语句中的属性 `flushCache=true` 来自动触发缓存刷新。不过需要注意的是,只有执行了带有该属性的 `insert/delete/update` 操作后,才会真正更新缓存[^4]。 --- #### 2. **Ehcache 缓存清理策略** 对于分布式环境或者更复杂的业务需求,通常会选择像 Ehcache 这样的第三方缓存框架。其提供了丰富的 API 接口用于管理和控制缓存行为[^2]。要删除某个范围内的键值对,可采用如下两种主要手段之一: - 使用精确匹配的方式逐个移除目标项; - 或者借助批量处理函数一次性淘汰符合条件的所有记录。 下面是一个简单的 Java 实现例子展示如何按条件过滤并销毁部分数据片段: ```java import net.sf.ehcache.Cache; import net.sf.ehcache.Element; public void removeRange(Cache cache, String prefixKey) { List<String> keysToRemove = new ArrayList<>(); for (Object key : cache.getKeys()) { if (((String)key).startsWith(prefixKey)) { keysToRemove.add((String)key); } } for (String key : keysToRemove){ cache.remove(key); // 删除单个元素 } } ``` 值得注意的是,在实际部署过程中应当特别留意序列化的兼容性和一致性问题,因为任何未被妥善保存至磁盘上的临时状态都有可能导致意外丢失风险。 --- #### 3. **Bluestore 用户态 Cache 控制** Ceph 存储系统内部使用的 Bluestore 组件也具备自己的缓存子系统设计思路——即完全绕过 Linux 内核 PageCache 而自行维护基于 Direct I/O 技术的一套解决方案[^3]。尽管如此,它仍然允许开发者定义自定义规则去调整哪些内容应该优先驻留在内存当中以及何时将其驱逐出去。 假设我们希望从 LRU/2Q 队列里剔除掉某些满足特定模式的对象实例,那么就需要深入理解底层的具体工作原理并与之交互。然而目前官方文档并未公开太多关于这部分高级功能的信息,因此建议联系技术支持团队寻求进一步指导。 --- ### 总结 综上所述,无论是关系型数据库 ORM 层面还是 NoSQL 类型的服务端架构亦或是专用硬件加速设备领域内都存在着各自独特的缓存治理方案可供选择。每种技术栈均有自己独特的优势同时也伴随着相应的局限性,所以在具体实施之前务必要充分评估项目背景后再做决定。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值