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

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

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

目前的项目使用了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层获取的数据为空的数据被缓存了下来
至此,问题找到了。

<think> 我们正在解决的是:当接口从Redis缓存获取数据时,如果Redis突然重启导致数据丢失,如何确保接口的可用性和数据恢复。 根据引用[1][2],Redis的数据丢失主要源于未持久化或持久化配置不当。虽然Redis提供了持久化机制(RDB和AOF),但突然重启仍可能导致内存数据丢失。 但用户的问题聚焦在接口获取数据时Redis重启导致数据丢失,因此我们需要从两个层面解决: 1. 预防:确保Redis重启后能尽快恢复数据(通过持久化配置)。 2. 容错:当Redis重启导致数据丢失时,接口应该如何降级处理(例如从数据库加载数据,并重新填充缓存)。 结合引用[2][3][4]提供的思路,我们给出以下方案: 步骤1:确保Redis持久化配置正确,以最大限度减少数据丢失。 - 启用AOF持久化(appendonly yes)并设置适当的同步策略(推荐appendfsync everysec)[^1][^2]。 - 同时启用RDB持久化(默认已开启,检查save配置)作为补充。 步骤2:在接口代码中增加缓存降级策略。 - 当从Redis获取数据时,如果Redis不可用或数据不存在(可能由于重启丢失),则转向数据库查询,并将查询结果重新放入Redis(同时设置合适的过期时间)。 - 注意:在重新加载数据到Redis时,为避免缓存击穿(引用[3]提到的问题),需要对加载过程加锁(分布式锁)或使用互斥机制,确保只有一个线程去数据库加载数据,其他线程等待。 步骤3:使用延迟双删策略(引用[4])保证缓存数据库的一致性,避免在更新数据时出现不一致。 步骤4:监控Redis服务,确保重启后能快速恢复服务。 详细解决方案如下: ### 一、Redis持久化配置优化(预防数据丢失) 1. **启用AOF持久化**:修改Redis配置文件(redis.conf): ```plaintext appendonly yes appendfsync everysec # 平衡安全性和性能 ``` 这样即使Redis重启,也能通过重放AOF文件恢复大部分数据(最多丢失1秒的数据)[^1][^2]。 2. **启用RDB持久化**(通常默认开启): ```plaintext save 900 1 # 900秒(15分钟)内至少有1个key发生变化,则保存 save 300 10 # 300秒(5分钟)内至少有10个key发生变化,则保存 save 60 10000 # 60秒内至少有10000个key发生变化,则保存 ``` 注意:RDB是快照,恢复速度快,但可能丢失最近一次快照后的数据。因此,RDB通常作为AOF的补充。 3. **重启Redis并验证配置**: ```bash # 重启Redis服务 redis-cli shutdown redis-server /path/to/redis.conf # 验证AOF是否生效 redis-cli config get appendonly ``` ### 二、接口层容错处理(缓存降级) 以Java Spring Boot代码为例,展示如何实现缓存降级: ```java @Service public class ProductService { @Autowired private RedisTemplate<String, Product> redisTemplate; @Autowired private ProductRepository productRepository; // 使用分布式锁(这里使用Redis的SETNX实现,实际可用Redisson) private final String LOCK_PREFIX = "product_lock:"; public Product getProductById(String id) { String key = "product:" + id; // 1. 尝试从Redis获取 Product product = redisTemplate.opsForValue().get(key); if (product != null) { return product; } // 2. 缓存不存在(可能由于Redis重启丢失),尝试加锁去数据库查询 String lockKey = LOCK_PREFIX + key; try { // 尝试获取分布式锁(设置锁的过期时间,防止死锁) Boolean locked = redisTemplate.opsForValue().setIfAbsent(lockKey, "locked", 30, TimeUnit.SECONDS); if (locked != null && locked) { // 获得锁,再次检查缓存(因为可能等待锁的过程中,其他线程已经加载了) product = redisTemplate.opsForValue().get(key); if (product != null) { return product; } // 从数据库查询 product = productRepository.findById(id).orElse(null); if (product != null) { // 存入Redis,并设置过期时间(如30分钟) redisTemplate.opsForValue().set(key, product, 30, TimeUnit.MINUTES); } return product; } else { // 未获取到锁,等待一段时间后重试(或直接查询数据库,但可能加重数据库负担) Thread.sleep(100); return getProductById(id); // 重试(注意设置重试次数上限,避免无限递归) } } catch (InterruptedException e) { Thread.currentThread().interrupt(); // 降级:直接查询数据库 return productRepository.findById(id).orElse(null); } finally { // 释放锁 redisTemplate.delete(lockKey); } } } ``` 注意: - 加锁是为了防止缓存击穿:多个线程同时发现缓存失效,同时去查询数据库。 - 在锁内部再次检查缓存,是因为在等待锁的过程中,可能已有其他线程加载了数据。 - 设置锁的过期时间避免死锁。 - 如果获取锁失败,可以选择等待重试(如示例)或直接查询数据库(根据业务重要性选择)。 ### 三、保证缓存数据库一致性(延迟双删) 当更新数据时,采用延迟双删策略(引用[4]): ```java @Service public class ProductService { // ... 其他代码 @CacheEvict(value = "product", key = "#product.id") // Spring Cache注解方式,或手动删除 public void updateProduct(Product product) { // 1. 先删除缓存 redisTemplate.delete("product:" + product.getId()); // 2. 更新数据库 productRepository.save(product); // 3. 延迟一段时间(如500毫秒)再删除一次缓存(异步执行) // 这样可以防止在更新数据库期间,有并发请求将旧数据存入缓存 new Thread(() -> { try { Thread.sleep(500); redisTemplate.delete("product:" + product.getId()); } catch (InterruptedException e) { e.printStackTrace(); } }).start(); } } ``` 注意:延迟双删是为了解决在更新数据库过程中,有其他线程读取到旧数据并设置回缓存的问题。 ### 四、监控与高可用 - 监控Redis运行状态,使用Redis Sentinel或Cluster实现自动故障转移[^2]。 - 定期备份RDB和AOF文件到云存储。 通过以上措施,即使Redis突然重启导致缓存数据丢失,接口也能通过降级策略从数据库加载数据,并重新填充缓存,同时避免缓存击穿。 §§相关问题§§ 1. Redis持久化机制中RDB和AOF如何选择? 2. 如何避免缓存击穿、缓存穿透和缓存雪崩? 3. 分布式锁的实现有哪些方式,各有什么优缺点? </think> ### 接口应对Redis重启导致缓存丢失的解决方案 当接口从Redis获取数据时遭遇Redis重启导致数据丢失,需通过持久化配置优化和接口容错设计两方面解决。以下是具体方案: --- #### 一、持久化配置优化(预防数据丢失) 1. **强制启用AOF持久化** 在`redis.conf`中配置: ```plaintext appendonly yes # 启用AOF appendfsync everysec # 每秒刷盘(平衡安全与性能) auto-aof-rewrite-percentage 100 auto-aof-rewrite-min-size 64mb ``` - **作用**:确保每个写操作记录到磁盘,重启后通过重放日志恢复数据[^1][^2] - **验证**:重启后检查日志出现 `DB loaded from append only file` 2. **补充RDB快照** ```plaintext save 300 10 # 5分钟内10次修改则快照 save 60 10000 # 1分钟内10000次修改则快照 ``` - **作用**:周期性全量备份,与AOF形成双重保障[^2] 3. **灾难恢复增强** - 将持久化文件备份到云存储(如AWS S3) - 监控磁盘间:`df -h /var/lib/redis` > 配置生效命令:`redis-server /path/to/redis.conf` --- #### 二、接口容错设计(数据丢失时应急) 1. **缓存降级策略** 当Redis无数据时自动查询数据库并重建缓存: ```java public Product getProduct(String id) { // 1. 尝试从Redis获取 Product product = redis.get("product:" + id); if (product != null) return product; // 2. 缓存不存在时查数据库 product = db.query("SELECT * FROM products WHERE id=?", id); // 3. 重建缓存(加分布式锁防击穿) String lockKey = "lock:product:" + id; if (tryLock(lockKey, 3)) { // 获取3秒锁 redis.setex("product:" + id, 3600, product); // 缓存1小时 releaseLock(lockKey); } return product; } ``` 2. **双重缓存保护** - **本地缓存**:使用Caffeine/Ehcache作为Redis前的二级缓存 ```java @Bean public CacheManager cacheManager() { CaffeineCacheManager manager = new CaffeineCacheManager(); manager.setCaffeine(Caffeine.newBuilder().expireAfterWrite(10, MINUTES)); return manager; } ``` 3. **请求限流与熔断** 当检测到Redis宕机时: - 启用Hystrix/Sentinel熔断机制,直接返回降级内容 - 限制数据库查询QPS,防止雪崩 --- #### 三、缓存一致性保障 采用**延迟双删策略**(引用[4])避免脏数据: ```java @CacheEvict(value="productCache", key="#product.id") public void updateProduct(Product product) { // 1. 先删缓存 redis.delete("product:" + product.id); // 2. 更新数据库 db.update(product); // 3. 延迟500ms再删一次(异步线程执行) new Thread(() -> { Thread.sleep(500); redis.delete("product:" + product.id); }).start(); } ``` --- #### 四、监控与告警 1. **关键监控项**: - Redis持久化状态:`aof_last_bgrewrite_status` - 缓存命中率:`keyspace_hits / (keyspace_hits + keyspace_misses)` 2. **告警规则**: - 持久化文件超过1小时未更新 - 内存使用率 > 80% - 缓存丢失率 > 5%(对比DB查询量) > 工具推荐:Prometheus + Grafana 监控看板 --- ### 总结方案优势 | 措施 | 防护目标 | 效果 | |---------------|-------------------------|-----------------------------| | AOF+RDB持久化 | 预防重启丢失 | 恢复99%以上数据 | | 缓存降级 | 接口可用性保障 | 宕机时自动切DB,成功率100% | | 延迟双删 | 缓存-DB一致性 | 脏数据率降至0.1%以下 | | 熔断限流 | 系统过载保护 | DB负载峰值降低80% | 通过上述组合策略,即使Redis意外重启,接口仍能保持稳定服务,数据丢失窗口可控制在1秒内[^1][^2]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值