🕰 一个案例带你彻底搞懂延迟双删

引言:一个事故

周五下午6点,小明正准备收拾东西下班,突然技术支持群里来了条新消息。

"用户反馈新活动送的积分显示不对!有人说自己是1000,有人说是500,但刷新几次又变成1000了!"运营小王很着急。

小明一下就紧张了起来。

积分系统正是他负责的模块,上周刚刚加了Redis缓存来提升性能。代码逻辑本身很简单:更新积分时先删除缓存,再更新数据库。而且测试环境一直很稳定,没想到上生产就出问题了。

"快看看是咋回事?"领导的消息紧接着就弹了出来。

小明赶紧登录生产环境查看,库里的积分确实是最新的没毛病,但是Redis缓存里还有没更新的!

明明是先删缓存再更新数据库的,怎么缓存里还会有旧数据?小明一脸懵逼,赶紧去找架构师老李求助。

老李正在悠闲地喝茶混加班时长,看了一眼小明的代码,非常淡定:"典型的缓存一致性问题,你遇到的是高并发下的竞态条件。"

"竞态条件?"小明一脸懵。

"来,我给你好好讲讲这个问题的根本原因,以及如何用延迟双删来彻底搞定。" 老李放下茶杯,打开了IDEA。

正文

问题的本质:并发竞态条件

老李在白板上画了个时间轴:"你现在的问题,就是在高并发情况下,删缓存和查缓存发生了竞态条件。"

sequenceDiagram participant ThreadA as 线程A(更新积分) participant Cache as Redis缓存 participant DB as 数据库 participant ThreadB as 线程B(查询积分) Note over ThreadA,ThreadB: 问题场景:先删缓存再更新数据库 ThreadA->>Cache: 1. 删除缓存 key:user:1001 Note over Cache: 缓存被删除 ThreadB->>Cache: 2. 查询缓存 key:user:1001 Note over Cache: 缓存未命中 ThreadB->>DB: 3. 查询数据库(此时还是旧数据500分) ThreadA->>DB: 4. 更新数据库(新数据1000分) ThreadB->>Cache: 5. 将旧数据写入缓存(500分) Note over Cache: 缓存中存储了脏数据!

问题就出在第3步和第4步的时序上。

当线程B读取数据库时,线程A还没有完成数据库更新,所以B读到的还是旧数据,然后把这个旧数据写入了缓存。

小明悟了:"所以需要在更新数据库后,再删除一次缓存!"

"是了!这就是延迟双删的核心思想。" 老李笑着说,"删除两次缓存,第一次是为了避免查询命中旧缓存,第二次是为了清理可能的脏数据。"

延迟双删的完整流程

老李继续在白板上画出延迟双删的流程图:

sequenceDiagram participant ThreadA as 线程A(更新积分) participant Cache as Redis缓存 participant DB as 数据库 participant ThreadB as 线程B(查询积分) participant DelayTask as 延迟任务 Note over ThreadA,DelayTask: 延迟双删完整流程 ThreadA->>Cache: 1. 第一次删除缓存 Note over Cache: 缓存被删除 ThreadB->>Cache: 2. 查询缓存(未命中) ThreadB->>DB: 3. 查询数据库(旧数据500分) ThreadA->>DB: 4. 更新数据库(新数据1000分) ThreadB->>Cache: 5. 将旧数据写入缓存(500分) ThreadA->>DelayTask: 6. 创建延迟删除任务 Note over DelayTask: 延迟300ms等待 DelayTask->>Cache: 7. 第二次删除缓存 Note over Cache: 脏数据被清理 participant ThreadC as 线程C(再次查询) ThreadC->>Cache: 8. 查询缓存(未命中) ThreadC->>DB: 9. 查询数据库(新数据1000分) ThreadC->>Cache: 10. 写入新数据到缓存 Note over Cache: 缓存数据正确

第一版:基础实现

先来看看最基础的延迟双删实现。

老李开始写代码:

java 复制代码
@Service
public class UserPointService {
    
    @Autowired
    private UserPointMapper userPointMapper;
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    /**
     * 更新用户积分
     * 
     * 核心思路:
     * 1. 先删除缓存,避免读取到旧数据
     * 2. 更新数据库
     * 3. 延迟删除缓存,清理可能的脏数据
     */
    @Transactional(rollbackFor = Exception.class)
    public void updateUserPoint(Long userId, Integer points, String reason) {
        String cacheKey = buildCacheKey(userId);
        
        try {
            // 第一步:删除缓存
            redisTemplate.delete(cacheKey);
            log.info("第一次删除缓存成功,key: {}", cacheKey);
            
            // 第二步:更新数据库
            UserPoint userPoint = new UserPoint();
            userPoint.setUserId(userId);
            userPoint.setPoints(points);
            userPoint.setUpdateReason(reason);
            userPoint.setUpdateTime(new Date());
            
            int affectedRows = userPointMapper.updateByUserId(userPoint);
            if (affectedRows == 0) {
                throw new BizException("用户不存在或积分更新失败");
            }
            
            log.info("数据库更新成功,userId: {}, newPoints: {}", userId, points);
            
            // 第三步:延迟删除缓存
            // 这里用简单的新线程来演示概念,生产环境千万不能这么搞
            new Thread(() -> {
                try {
                    // 延迟时间需要根据业务场景调整
                    // 一般设置为:数据库操作耗时 + 读取操作耗时 + 缓冲时间
                    Thread.sleep(500);
                    
                    // 第二次删除缓存
                    redisTemplate.delete(cacheKey);
                    log.info("延迟删除缓存成功,key: {}", cacheKey);
                    
                } catch (InterruptedException e) {
                    log.error("延迟删除缓存被中断,key: {}", cacheKey, e);
                    Thread.currentThread().interrupt();
                } catch (Exception e) {
                    log.error("延迟删除缓存失败,key: {}", cacheKey, e);
                    // 这里可以考虑重试或者告警
                }
            }).start();
            
        } catch (Exception e) {
            log.error("更新用户积分失败,userId: {}", userId, e);
            throw new BizException("积分更新失败:" + e.getMessage());
        }
    }
    
    /**
     * 查询用户积分
     * 
     * 查询策略:
     * 1. 先查缓存,命中直接返回
     * 2. 缓存未命中,查询数据库
     * 3. 数据库结果写入缓存,设置过期时间
     */
    public UserPoint getUserPoint(Long userId) {
        String cacheKey = buildCacheKey(userId);
        
        // 第一步:查询缓存
        UserPoint cachedPoint = (UserPoint) redisTemplate.opsForValue().get(cacheKey);
        if (cachedPoint != null) {
            log.info("缓存命中,userId: {}, points: {}", userId, cachedPoint.getPoints());
            return cachedPoint;
        }
        
        // 第二步:缓存未命中,查询数据库
        UserPoint userPoint = userPointMapper.selectByUserId(userId);
        if (userPoint == null) {
            throw new BizException("用户积分不存在");
        }
        
        // 第三步:写入缓存
        // 设置30分钟过期时间,避免数据长期不一致
        redisTemplate.opsForValue().set(cacheKey, userPoint, 30, TimeUnit.MINUTES);
        log.info("数据库查询并写入缓存,userId: {}, points: {}", userId, userPoint.getPoints());
        
        return userPoint;
    }
    
    private String buildCacheKey(Long userId) {
        return String.format("user:point:%d", userId);
    }
}

小明仔细看了看代码:"这个延迟时间500ms是怎么确定的?"

"好问题!" 老李点点头。

延迟时间的设置很关键,太短了清理不了脏数据,太长了可能影响性能。一般的计算公式是:

延迟时间 = 数据库写操作耗时 + 数据库读操作耗时 + 网络延迟 + 安全缓冲时间

比如你的数据库写操作需要50ms,读操作需要30ms,网络延迟10ms,那么延迟时间可以设置为200ms(50+30+10+110)。

"但是这个基础版本有几个问题。" 老李指着代码说:

  1. 每次更新都创建新线程,高并发下会有性能问题
  2. 如果应用重启,延迟任务就丢失了
  3. 缓存删除失败没有重试机制
  4. 缺乏监控,没法观察延迟删除的执行情况

第二版:生产级实现

来看看生产环境应该怎么实现。

老李新建了一个改进版的类:

java 复制代码
@Service
public class UserPointServiceV2 {
    
    @Autowired
    private UserPointMapper userPointMapper;
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    // 使用线程池管理延迟任务,避免频繁创建线程
    private final ScheduledExecutorService delayDeleteExecutor = 
        Executors.newScheduledThreadPool(
            10, // 核心线程数,根据业务量调整
            new ThreadFactoryBuilder()
                .setNameFormat("delay-cache-delete-%d")
                .setDaemon(true) // 设置为守护线程
                .setUncaughtExceptionHandler((t, e) -> 
                    log.error("延迟删除缓存线程异常", e))
                .build()
        );
    
    // 延迟删除配置
    @Value("${cache.delay-delete.delay-time:300}")
    private long delayTime; // 延迟时间,单位毫秒
    
    @Value("${cache.delay-delete.max-retry:3}")
    private int maxRetryTimes; // 最大重试次数
    
    /**
     * 更新用户积分
     */
    @Transactional(rollbackFor = Exception.class)
    public void updateUserPoint(Long userId, Integer points, String reason) {
        String cacheKey = buildCacheKey(userId);
        String operationId = generateOperationId(userId);
        
        try {
            // 第一步:删除缓存(同步执行,确保删除成功)
            deleteCacheWithRetry(cacheKey, "first-delete", operationId);
            
            // 第二步:更新数据库
            UserPoint userPoint = buildUserPoint(userId, points, reason);
            int affectedRows = userPointMapper.updateByUserId(userPoint);
            
            if (affectedRows == 0) {
                throw new BizException("用户不存在或积分更新失败");
            }
            
            log.info("积分更新成功 - operationId: {}, userId: {}, newPoints: {}", 
                operationId, userId, points);
            
            // 第三步:异步延迟删除缓存
            scheduleDelayDelete(cacheKey, operationId);
            
        } catch (Exception e) {
            log.error("积分更新失败 - operationId: {}, userId: {}, points: {}", 
                operationId, userId, points, e);
            throw new BizException("积分更新失败:" + e.getMessage());
        }
    }
    
    /**
     * 批量更新用户积分
     * 对于批量操作,延迟双删同样适用
     */
    @Transactional(rollbackFor = Exception.class)
    public void batchUpdateUserPoint(List<UserPointUpdateRequest> requests) {
        if (requests.isEmpty()) {
            return;
        }
        
        String operationId = UUID.randomUUID().toString();
        Set<String> cacheKeys = requests.stream()
            .map(req -> buildCacheKey(req.getUserId()))
            .collect(Collectors.toSet());
        
        try {
            // 第一步:批量删除缓存
            Long deletedCount = redisTemplate.delete(cacheKeys);
            log.info("批量删除缓存 - operationId: {}, 删除数量: {}/{}", 
                operationId, deletedCount, cacheKeys.size());
            
            // 第二步:批量更新数据库
            for (UserPointUpdateRequest request : requests) {
                UserPoint userPoint = buildUserPoint(
                    request.getUserId(), 
                    request.getPoints(), 
                    request.getReason()
                );
                userPointMapper.updateByUserId(userPoint);
            }
            
            log.info("批量积分更新成功 - operationId: {}, 更新数量: {}", 
                operationId, requests.size());
            
            // 第三步:延迟批量删除缓存
            delayDeleteExecutor.schedule(() -> {
                try {
                    Long delayDeletedCount = redisTemplate.delete(cacheKeys);
                    log.info("延迟批量删除缓存成功 - operationId: {}, 删除数量: {}", 
                        operationId, delayDeletedCount);
                } catch (Exception e) {
                    log.error("延迟批量删除缓存失败 - operationId: {}", operationId, e);
                    // 可以考虑重试或发送告警
                }
            }, delayTime, TimeUnit.MILLISECONDS);
            
        } catch (Exception e) {
            log.error("批量积分更新失败 - operationId: {}", operationId, e);
            throw new BizException("批量积分更新失败:" + e.getMessage());
        }
    }
    
    /**
     * 带重试机制的缓存删除
     */
    private void deleteCacheWithRetry(String cacheKey, String operation, String operationId) {
        int retryCount = 0;
        Exception lastException = null;
        
        while (retryCount <= maxRetryTimes) {
            try {
                Boolean deleted = redisTemplate.delete(cacheKey);
                if (Boolean.TRUE.equals(deleted) || retryCount == 0) {
                    // 删除成功或者key本来就不存在(第一次尝试)
                    log.info("缓存删除成功 - operation: {}, operationId: {}, key: {}, retry: {}", 
                        operation, operationId, cacheKey, retryCount);
                    return;
                }
            } catch (Exception e) {
                lastException = e;
                log.warn("缓存删除失败 - operation: {}, operationId: {}, key: {}, retry: {}", 
                    operation, operationId, cacheKey, retryCount, e);
            }
            
            retryCount++;
            if (retryCount <= maxRetryTimes) {
                // 指数退避策略
                long sleepTime = Math.min(1000, 100 * (1L << (retryCount - 1)));
                try {
                    Thread.sleep(sleepTime);
                } catch (InterruptedException ie) {
                    Thread.currentThread().interrupt();
                    throw new RuntimeException("缓存删除被中断", ie);
                }
            }
        }
        
        // 最终失败
        log.error("缓存删除最终失败 - operation: {}, operationId: {}, key: {}", 
            operation, operationId, cacheKey, lastException);
        
        // 这里可以发送告警或写入失败队列
        // alertService.sendAlert("缓存删除失败", cacheKey, operationId);
    }
    
    /**
     * 调度延迟删除任务
     */
    private void scheduleDelayDelete(String cacheKey, String operationId) {
        delayDeleteExecutor.schedule(() -> {
            deleteCacheWithRetry(cacheKey, "delay-delete", operationId);
        }, delayTime, TimeUnit.MILLISECONDS);
        
        log.info("延迟删除任务已调度 - operationId: {}, key: {}, delayTime: {}ms", 
            operationId, cacheKey, delayTime);
    }
    
    private UserPoint buildUserPoint(Long userId, Integer points, String reason) {
        UserPoint userPoint = new UserPoint();
        userPoint.setUserId(userId);
        userPoint.setPoints(points);
        userPoint.setUpdateReason(reason);
        userPoint.setUpdateTime(new Date());
        return userPoint;
    }
    
    /**
     * 生成操作ID,用于链路追踪
     */
    private String generateOperationId(Long userId) {
        return String.format("update_point_%d_%d", userId, System.currentTimeMillis());
    }
    
    private String buildCacheKey(Long userId) {
        return String.format("user:point:%d", userId);
    }
    
    /**
     * 应用关闭时优雅停机
     */
    @PreDestroy
    public void shutdown() {
        log.info("开始关闭延迟删除线程池");
        delayDeleteExecutor.shutdown();
        
        try {
            // 等待已提交的任务完成
            if (!delayDeleteExecutor.awaitTermination(10, TimeUnit.SECONDS)) {
                log.warn("延迟删除任务未能在10秒内完成,强制关闭");
                delayDeleteExecutor.shutdownNow();
                
                // 再等待一段时间
                if (!delayDeleteExecutor.awaitTermination(5, TimeUnit.SECONDS)) {
                    log.error("延迟删除线程池强制关闭失败");
                }
            }
        } catch (InterruptedException e) {
            log.error("等待延迟删除线程池关闭被中断", e);
            delayDeleteExecutor.shutdownNow();
            Thread.currentThread().interrupt();
        }
        
        log.info("延迟删除线程池已关闭");
    }
}

这个版本就比较完善了。

有线程池、还加了重试机制、支持批量操作,还有优雅停机。

小明认真地看着代码:"这个指数退避策略是什么意思?"

"简单来说,就是重试的间隔时间越来越长。"老李解释道,"第一次重试等100ms,第二次等200ms,第三次等400ms。这样可以避免在Redis出现短暂问题时过于频繁的重试。"

第三版:基于消息队列

虽然线程池方案已经比较完善了,但其实还有一个更加可靠的方案。

就是基于消息队列的延迟删除。

小明不解。

老李画了个对比图:

graph TB subgraph "线程池方案" A1[业务请求] --> B1[删缓存] B1 --> C1[更新DB] C1 --> D1[提交延迟任务到内存队列] D1 --> E1[线程池执行] E1 --> F1[删缓存] G1[应用重启] -.->|任务丢失| D1 end subgraph "MQ方案" A2[业务请求] --> B2[删缓存] B2 --> C2[更新DB] C2 --> D2[发送延迟消息到MQ] D2 --> E2[MQ延迟投递] E2 --> F2[消费者删缓存] G2[应用重启] -.->|消息持久化,不丢失| D2 end

MQ方案的优势很明显:

  1. 消息存储在MQ中,应用重启也不会丢失
  2. MQ如果有集群,能保证消息不丢失,达到了高可用
  3. 自带重试和死信队列
  4. 可以通过MQ控制台看消息状态,监控很方便
  5. 多个消费者实例可以并行处理

来看看基于RabbitMQ的实现:

java 复制代码
@Service
public class UserPointServiceV3 {
    
    @Autowired
    private UserPointMapper userPointMapper;
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    @Autowired
    private RabbitTemplate rabbitTemplate;
    
    // 延迟删除相关配置
    private static final String DELAY_DELETE_EXCHANGE = "cache.delay.delete.exchange";
    private static final String DELAY_DELETE_ROUTING_KEY = "cache.delay.delete";
    
    /**
     * 更新用户积分
     */
    @Transactional(rollbackFor = Exception.class)
    public void updateUserPoint(Long userId, Integer points, String reason) {
        String cacheKey = buildCacheKey(userId);
        String operationId = generateOperationId(userId);
        
        try {
            // 第一步:删除缓存
            deleteCacheWithRetry(cacheKey, "first-delete", operationId);
            
            // 第二步:更新数据库
            UserPoint userPoint = buildUserPoint(userId, points, reason);
            int affectedRows = userPointMapper.updateByUserId(userPoint);
            
            if (affectedRows == 0) {
                throw new BizException("用户不存在或积分更新失败");
            }
            
            log.info("积分更新成功 - operationId: {}, userId: {}, newPoints: {}", 
                operationId, userId, points);
            
            // 第三步:发送延迟删除消息
            sendDelayDeleteMessage(cacheKey, operationId);
            
        } catch (Exception e) {
            log.error("积分更新失败 - operationId: {}, userId: {}, points: {}", 
                operationId, userId, points, e);
            throw new BizException("积分更新失败:" + e.getMessage());
        }
    }
    
    /**
     * 发送延迟删除消息
     */
    private void sendDelayDeleteMessage(String cacheKey, String operationId) {
        CacheDeleteMessage message = CacheDeleteMessage.builder()
            .cacheKey(cacheKey)
            .operationId(operationId)
            .createTime(System.currentTimeMillis())
            .retryCount(0)
            .build();
        
        try {
            rabbitTemplate.convertAndSend(
                DELAY_DELETE_EXCHANGE,
                DELAY_DELETE_ROUTING_KEY,
                message,
                // 设置消息延迟时间
                msg -> {
                    msg.getMessageProperties().setDelay(300); // 延迟300ms
                    msg.getMessageProperties().setMessageId(operationId);
                    return msg;
                }
            );
            
            log.info("延迟删除消息发送成功 - operationId: {}, cacheKey: {}", 
                operationId, cacheKey);
            
        } catch (Exception e) {
            log.error("延迟删除消息发送失败 - operationId: {}, cacheKey: {}", 
                operationId, cacheKey, e);
            // 消息发送失败,可以考虑回退到线程池方案
            fallbackToThreadPool(cacheKey, operationId);
        }
    }
    
    /**
     * 消费延迟删除消息
     */
    @RabbitListener(queues = "cache.delay.delete.queue")
    public void handleDelayDeleteMessage(
        @Payload CacheDeleteMessage message,
        Channel channel,
        @Header(AmqpHeaders.DELIVERY_TAG) long deliveryTag
    ) {
        String operationId = message.getOperationId();
        String cacheKey = message.getCacheKey();
        
        try {
            // 执行缓存删除
            Boolean deleted = redisTemplate.delete(cacheKey);
            
            if (Boolean.TRUE.equals(deleted)) {
                log.info("延迟删除缓存成功 - operationId: {}, cacheKey: {}", 
                    operationId, cacheKey);
            } else {
                log.info("缓存key不存在,删除成功 - operationId: {}, cacheKey: {}", 
                    operationId, cacheKey);
            }
            
            // 手动确认消息
            channel.basicAck(deliveryTag, false);
            
        } catch (Exception e) {
            log.error("延迟删除缓存失败 - operationId: {}, cacheKey: {}, retryCount: {}", 
                operationId, cacheKey, message.getRetryCount(), e);
            
            try {
                // 增加重试次数
                message.setRetryCount(message.getRetryCount() + 1);
                
                if (message.getRetryCount() >= 3) {
                    // 超过最大重试次数,发送到死信队列
                    log.error("延迟删除缓存达到最大重试次数 - operationId: {}, cacheKey: {}", 
                        operationId, cacheKey);
                    channel.basicNack(deliveryTag, false, false);
                    
                    // 发送告警
                    // alertService.sendAlert("缓存删除失败", operationId, cacheKey);
                } else {
                    // 拒绝消息,让MQ重新投递
                    channel.basicNack(deliveryTag, false, true);
                }
            } catch (IOException ioException) {
                log.error("消息确认失败 - operationId: {}", operationId, ioException);
            }
        }
    }
    
    /**
     * MQ发送失败时的回退方案
     */
    private void fallbackToThreadPool(String cacheKey, String operationId) {
        // 使用单独的线程池作为回退方案
        CompletableFuture.runAsync(() -> {
            try {
                Thread.sleep(300);
                deleteCacheWithRetry(cacheKey, "fallback-delete", operationId);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                log.error("回退删除缓存被中断 - operationId: {}", operationId, e);
            }
        });
    }
    
    // 其他方法保持不变...
    private void deleteCacheWithRetry(String cacheKey, String operation, String operationId) {
        // ... (与V2版本相同的实现)
    }
    
    private UserPoint buildUserPoint(Long userId, Integer points, String reason) {
        // ... (与V2版本相同的实现)
    }
    
    private String generateOperationId(Long userId) {
        return String.format("update_point_%d_%d", userId, System.currentTimeMillis());
    }
    
    private String buildCacheKey(Long userId) {
        return String.format("user:point:%d", userId);
    }
}

/**
 * 缓存删除消息实体
 */
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class CacheDeleteMessage implements Serializable {
    
    private static final long serialVersionUID = 1L;
    
    /**
     * 缓存键
     */
    private String cacheKey;
    
    /**
     * 操作ID,用于链路追踪
     */
    private String operationId;
    
    /**
     * 创建时间
     */
    private Long createTime;
    
    /**
     * 重试次数
     */
    private Integer retryCount;
}

这里省略相关的配置,具体代码就由你自行去发挥了。

方案对比

老李在白板上画了个完整的对比表:

特性对比 基础版 线程池版 MQ版
实现复杂度 ⭐⭐ ⭐⭐⭐
可靠性 ⭐⭐⭐ ⭐⭐⭐⭐⭐
性能开销 ⭐⭐ ⭐⭐⭐ ⭐⭐⭐⭐
监控能力 ⭐⭐ ⭐⭐⭐⭐⭐
运维成本 ⭐⭐ ⭐⭐⭐⭐
横向扩展 ⭐⭐ ⭐⭐⭐⭐⭐

"选择哪种方案,还是要看你的具体业务场景,不能人云亦云。" 老李总结道:

  • 简单业务:基础版足够了,简单快速
  • 中型项目/对可靠性有要求:线程池版,平衡复杂度和可靠性
  • 大型项目/核心业务:MQ版,最高的可靠性和可扩展性
  • 项目已有MQ:必然优先考虑MQ版

如何计算延迟时间

小明问:"还有个问题,延迟时间到底应该怎么设置才恰当?"

确实,如果延迟时间设置不当,要么清理不了脏数据,要么影响性能。

于是老李写了个计算公式:

diff 复制代码
延迟时间 = Max(数据库写操作时间, 数据库读操作时间) + 网络往返时间 + 安全边界

具体计算示例:
- 数据库写操作:平均50ms,99分位100ms  
- 数据库读操作:平均30ms,99分位80ms
- 网络往返:平均5ms,99分位20ms  
- 安全边界:建议100ms

延迟时间 = Max(100, 80) + 20 + 100 = 220ms

为了保险起见,一般设置为300ms

如果业务对实时性要求很高,可以设置为200ms;如果对一致性要求极高,可以设置为500ms

适用场景

小明又问了:"是不是所有的缓存更新都要用延迟双删呢?"

当然不是。

延迟双删有自己的适用场景:

适合的

  1. 读多写少
  2. 有强一致性要求
  3. 更新不频繁
  4. 可以容忍短暂延迟,只要保证最终一致性

不适合的

  1. 秒级多次更新的热点数据
  2. 弱一致性要求,能接受数据不一致
  3. 实时性要求特别高
  4. 写多读少

其他方案

当然除了延迟双删,业内也有一些其他的方案,这里简单列举:

方案 优点 缺点 适用场景
延迟双删 一致性较好,实现相对简单 有延迟,可能误删 读多写少,强一致性
先更新DB再删缓存 实现简单,性能好 短暂不一致 弱一致性要求
读时加锁 一致性最强 性能差,可能死锁 极强一致性要求
Canal同步 实时性好,解耦 架构复杂,依赖多 大数据量,高实时性

写在最后

这么一趟走下来,是不是感觉延迟双删其实并不复杂。

只要掌握原理,很多难题都能迎刃而解。

技术的本质是服务业务并解决问题。

延迟双删只是手段,真正的目标是保证系统的稳定性和用户体验。

在追求技术完美的同时,也得考虑实现成本和维护复杂度。

这时候小明又冒了出来:"如果在延迟删除执行前,又有新的更新操作怎么办?"

诸君,你怎么看?

相关推荐
bobz96522 分钟前
小语言模型是真正的未来
后端
一叶飘零_sweeeet1 小时前
从繁琐到优雅:Java Lambda 表达式全解析与实战指南
java·lambda·java8
DevYK1 小时前
企业级 Agent 开发实战(一) LangGraph 快速入门
后端·llm·agent
艾伦~耶格尔1 小时前
【集合框架LinkedList底层添加元素机制】
java·开发语言·学习·面试
最初的↘那颗心2 小时前
Flink Stream API 源码走读 - print()
java·大数据·hadoop·flink·实时计算
冒泡的肥皂2 小时前
MVCC初学demo(一
数据库·后端·mysql
颜如玉3 小时前
ElasticSearch关键参数备忘
后端·elasticsearch·搜索引擎
JH30733 小时前
Maven的三种项目打包方式——pom,jar,war的区别
java·maven·jar
带刺的坐椅4 小时前
轻量级流程编排框架,Solon Flow v3.5.0 发布
java·solon·workflow·flow·solon-flow