文章目录
隐藏
在订单系统的缓存策略中,如果采用“先删缓存再存数据库”的方式,的确存在缓存并发不一致问题。为了解决这一问题,需要深入分析可能出现的并发情况,并使用适当的方案来保证数据的一致性。
1. 缓存策略:“先删缓存,再存库”
1.1 该策略的流程
- Step 1:在更新订单数据时,首先删除缓存中的旧数据。
- Step 2:然后将更新后的数据写入数据库。
- Step 3:下次查询时,如果缓存为空,则从数据库中查询最新数据,并重新加载到缓存中。
1.2 并发问题
- 假设有两个并发请求:请求 A 进行写操作,请求 B 进行读操作。
- 请求 A 先删除了缓存。
- 请求 B 在数据库更新完成之前进行读操作,发现缓存已删除,于是从数据库中读取旧数据,并将旧数据写回缓存。
- 请求 A 完成数据库更新,此时缓存中存的仍是旧数据,导致脏数据回写的问题。
2. 改进策略:“先存库,再删缓存”
2.1 该策略的流程
- Step 1:先将订单数据写入数据库。
- Step 2:删除缓存中与之对应的旧数据。
- Step 3:下次查询时,如果缓存为空,则从数据库中查询最新数据并写回缓存。
2.2 优点
- 该策略可以有效避免上面提到的并发问题。即使有并发读写请求,由于数据库已经完成了数据的更新,读请求从数据库中读取的将是最新数据,缓存中不可能存入旧数据。
2.3 并发问题的进一步分析
- 在高并发场景下,多个写请求可能会频繁删除缓存,导致缓存频繁失效,并带来数据库的缓存穿透风险。
- 需要配合延迟双删策略来进一步提高一致性。
3. 延迟双删策略
3.1 延迟双删策略的流程
- Step 1:写入数据库后,立即删除缓存。
- Step 2:等待一定的时间(如 100ms),再次删除缓存,以保证缓存中不会有旧数据。
- 延迟删除的时间间隔要根据业务场景和写入数据库的延迟来进行设置。
- Step 3:如果缓存重建时被旧数据污染,延迟的二次删除可以清理这部分脏数据。
3.2 延迟双删策略的优点
- 通过延迟删除,可以减少因数据库写入延迟导致的缓存不一致问题,确保在高并发下数据的一致性。
- 即使存在并发读操作,延迟的二次删除可以清除可能出现的旧缓存,确保数据的最终一致性。
4. 进一步的优化:引入分布式锁
- 为了解决高并发情况下缓存重建的竞争,可以在写入数据库前对关键数据的缓存重建操作加锁。
- 例如,在缓存中存储一个标记,表明当前缓存正在重建,这样其他读请求在此期间不再写入旧缓存。
4.1 使用分布式锁的流程
- Step 1:写请求获得锁后,先写数据库,再删除缓存。
- Step 2:释放锁,允许缓存重建。
- Step 3:在缓存重建过程中,其他请求因锁的存在而无法访问该缓存,避免并发写入不一致。
4.2 分布式锁的实现
- 可以使用 Redis 的
SETNX
或者基于 Redisson 的分布式锁实现。 - 注意,锁的超时时间要设置得足够长,以防止锁释放前数据库操作尚未完成。
5. 总结与对比
缓存策略 | 优点 | 缺点 | 场景适用 |
---|---|---|---|
先删缓存后存库 | 实现简单,适用于并发较低的场景 | 存在脏数据回写问题,数据不一致 | 并发较低、更新操作不频繁的场景 |
先存库后删缓存 | 避免并发下的脏数据问题,数据一致性较高 | 存在频繁缓存失效的问题,带来缓存穿透 | 并发高、数据更新频繁的场景 |
延迟双删策略 | 进一步提升数据一致性,减少脏数据的存在 | 引入延迟时间参数,增加实现复杂度 | 高并发场景,要求数据最终一致性 |
分布式锁 | 确保缓存重建的安全,解决高并发下的缓存写入冲突 | 增加实现和运维复杂性,影响性能 | 高并发、高一致性要求的业务场景 |
小结
在订单系统的缓存策略中,“先存库再删缓存”是基础方案,而“延迟双删策略”和“分布式锁”则是应对高并发的进一步优化措施。根据业务场景的具体需求和并发压力,可以灵活选择合适的方案。
4o
You said:
存库后再基于 mq 的删除缓存 key 是不是更好
ChatGPT said:
ChatGPT
是的,使用 消息队列(MQ)来删除缓存是一种更可靠的方法,可以提升一致性和性能。通过消息队列进行异步删除缓存,能够确保数据持久化后再进行缓存删除,从而解决并发下的缓存一致性问题。以下是这种方案的优势及其具体实现方式:
1. 存库后基于 MQ 删除缓存的优点
1.1 异步处理,提高系统吞吐量
- 数据库写入完成后,直接发送一个消息到消息队列,将缓存删除的逻辑异步化,可以降低主流程的耗时,提升系统的整体吞吐量和响应速度。
1.2 更高的缓存一致性
- 通过消息队列,确保缓存的删除操作是在数据库写入成功后进行。这样可以确保缓存中不会有旧数据,避免脏数据的产生。
- 由于缓存删除操作与写库操作解耦,即使写库和删除缓存之间有短暂的延迟,仍能保证最终一致性。
1.3 容错和重试机制
- 消息队列通常具备消息重试机制,在删除缓存失败时,可以重新发送删除请求,确保缓存被成功清理。
- 如果某个服务实例或数据库节点发生故障,MQ 也可以通过重试和消息持久化,保证删除缓存的操作最终被执行。
2. 实现流程
- 存库后发送消息:
- 在更新数据库成功后,立即发送一个删除缓存的消息到消息队列(如 RabbitMQ、Kafka、RocketMQ 等)。
- 消息的内容可以是需要删除的缓存键(或键的列表),也可以包括删除操作的元数据信息。
- 消费者订阅并处理消息:
- 另一个独立的缓存管理服务或模块作为消费者,监听 MQ 中的消息,收到消息后根据键值删除缓存。
- 可以使用异步的方式进行删除操作,避免删除操作阻塞主线程。
- 设置重试与幂等性:
- 消息队列通常支持重试机制,当删除缓存失败时,可以自动重试。
- 消费者在执行缓存删除时,需保证幂等性(即多次删除相同缓存不会导致错误)。
3. 使用 MQ 删除缓存的典型示例
示例伪代码
javaCopy code// 数据库更新逻辑
void updateOrder(Order order) {
// 1. 更新数据库
orderRepository.save(order);
// 2. 发送缓存删除消息
mqProducer.send("cache-delete-topic", order.getId());
}
// 消息消费者逻辑
void cacheDeleteConsumer(String orderId) {
// 3. 删除缓存
String cacheKey = "order:" + orderId;
cacheService.delete(cacheKey);
}
4. 方案对比:基于 MQ 的缓存删除 vs 延迟双删
4.1 基于 MQ 的缓存删除
- 优点:
- 提高了主流程的吞吐量,因为删除缓存是异步的。
- 使用 MQ 进行异步缓存删除,具有更好的容错能力,且支持消息重试和持久化。
- 整体架构清晰,耦合度低,适合高并发、高可用的场景。
- 缺点:
- 需要引入消息队列,增加系统的复杂度和运维成本。
- 需要考虑消息丢失、消费失败等问题,需要完善的监控和补偿机制。
4.2 延迟双删策略
- 优点:
- 实现相对简单,不依赖外部的消息队列,只需引入延迟删除逻辑即可。
- 适合并发较低、缓存一致性要求较高的场景。
- 缺点:
- 无法解决极端情况下的脏数据问题(如缓存删除后的短暂时间内读取旧数据)。
- 延迟删除时间的设置不易把握,可能影响系统的实时性和性能。
5. 推荐场景
- 高并发的订单系统:采用“存库后基于 MQ 删除缓存”更合适,能够确保在高并发场景下,缓存的一致性和系统的吞吐量。
- 中低并发的系统:可以使用“延迟双删”来保证一致性,并且实现相对简单。
6. 小结
- 基于 MQ 的缓存删除方案提供了一种更可靠的缓存一致性保证,特别适合高并发、高可用的场景。
- 在具体实现时,需要确保消息队列的可靠性和幂等性处理,同时监控消息的发送和消费状态。
4o
You said:
这里的 mq 消息是不是用事务消息更合适?存库后服务被 kill 呢
ChatGPT said:
ChatGPT
是的,在这种场景下,使用事务消息(如 RocketMQ 的事务消息)更合适,因为事务消息可以确保存库和发送消息这两个操作的原子性。这样,即使在数据库写入成功后,服务被意外杀掉,也能保证消息最终能够正确发送,从而保证缓存的一致性。
1. 事务消息的流程
事务消息通过确保数据库操作和消息发送在一个事务中完成,能够有效解决数据库写入后服务被 kill 的问题。
1.1 事务消息的基本流程
- Step 1:应用服务先向消息队列发送一个半消息,这是一种特殊的消息,暂时对消费者不可见。
- Step 2:在发送半消息后,执行数据库写入操作。
- 如果数据库写入成功,则向 MQ 确认提交事务,使得半消息转变为可消费的消息。
- 如果数据库写入失败,则向 MQ 发送回滚指令,删除半消息。
- Step 3:消费者在收到消息后,进行缓存的删除操作。
- Step 4:如果在提交消息之前服务被 kill,消息队列会自动触发事务回查,确认数据库事务是否提交,确保消息的最终一致性。
2. 事务消息的优势
- 强一致性:使用事务消息可以确保数据库写入和消息发送的原子性,消除服务被 kill 后可能导致的缓存不一致问题。
- 可靠性:即使在服务异常中断的情况下,事务消息也能通过回查机制来补偿未完成的消息发送,确保最终一致性。
- 重试机制:在消费缓存删除消息时,如果删除失败,消费者可以通过重试机制继续执行删除操作,避免缓存数据的持久性不一致。
3. 事务消息的实现(以 RocketMQ 为例)
RocketMQ 提供了事务消息,以下是具体实现逻辑:
3.1 生产者端
javaCopy code// 1. 发送半消息
TransactionSendResult sendResult = producer.sendMessageInTransaction(
"cache-delete-topic",
message,
localTransactionExecutor // 本地事务执行器
);
// 2. 实现本地事务执行器
public class LocalTransactionExecutor implements LocalTransactionExecuter {
@Override
public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {
try {
// 3. 执行数据库操作
orderRepository.save((Order) arg);
// 4. 本地事务成功,提交事务消息
return LocalTransactionState.COMMIT_MESSAGE;
} catch (Exception e) {
// 5. 本地事务失败,回滚事务消息
return LocalTransactionState.ROLLBACK_MESSAGE;
}
}
}
3.2 消费者端
javaCopy code// 消费者订阅消息
consumer.subscribe("cache-delete-topic", "*");
consumer.registerMessageListener((List<MessageExt> msgs, ConsumeConcurrentlyContext context) -> {
for (MessageExt msg : msgs) {
// 处理消息,删除缓存
cacheService.delete(msg.getBody());
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
});
4. 事务消息的回查机制
- 当半消息被发送,但事务消息的提交或回滚状态不明确时,RocketMQ 会触发事务回查机制,检查数据库的本地事务是否已提交。
- 通过事务回查机制,可以在数据库操作和消息发送未完成时进行补偿操作,确保消息的可靠投递。
5. 事务消息的优缺点
优点
- 保证原子性:数据库写入和缓存删除的消息发送保证原子性,确保不一致的情况不会发生。
- 高可靠性:通过事务回查机制和消息持久化,确保消息在异常情况下不丢失。
- 最终一致性:即使服务在写库后被 kill,也可以通过事务消息的回查机制实现最终一致性。
缺点
- 实现复杂:事务消息的使用和实现需要额外的开发和维护成本。
- 性能开销:事务消息的回查机制可能会带来一定的延迟和开销,适合对一致性要求高的场景,而不适合极端低延迟场景。
6. 总结
在实现事务消息时,需要注意事务消息的回查机制和消息的幂等性,以确保整体一致性。
事务消息适合需要强一致性的场景,例如订单系统中的缓存删除机制,可以避免缓存不一致。