在很多高并发的场景如秒杀系统,QPS会瞬时暴增,如果采用直接读写数据库(如MySQL)的方式,很可能会将数据库打垮。因此这种场景需要引入Redis做缓存,应对高并发的访问。但同时也会引入新的风险,最常见的就是缓存与数据库的一致性问题。下面我们来看一下一些常见的方案。
1.Cache Aside(懒加载)模式
这是最常用的缓存模式之一。在这个模式下,数据更新的主流程发生在数据库中,缓存会根据需要来更新或失效。
流程:
- 查询时,应用首先查询缓存,如果缓存中有数据(缓存命中),则直接返回缓存数据;如果缓存没有数据(缓存未命中),则查询数据库,得到数据后返回并更新缓存。
- 更新时,应用首先更新数据库,然后将对应的缓存删除或更新(通常删除缓存让下一次查询时重新加载)。
优点:
- 数据一致性较好,因为写操作直接作用在数据库,缓存仅在需要时更新。
- 缓存可以有效减少数据库的压力。
缺点:
- 第一次查询时,如果缓存中没有数据,会引发数据库查询,可能导致缓存未命中的延迟问题。
- 如果删除缓存失败,可能会导致缓存与数据库数据不一致。
2.同步更新模式(Write Through)
在这个策略下,每次对数据库进行写操作时,缓存会同步更新,也就是写缓存和同步数据库是原子操作,其中一步失败均认为写入失败。
流程:
- 写入时,应用首先将数据写入缓存,然后通过缓存同步到数据库。
- 查询时,直接从缓存中读取数据。
优点:
- 查询时性能更高,因为数据总是直接从缓存中获取。
- 数据一致性较好,因为每次写操作都会同时更新缓存和数据库。
缺点:
- 每次写操作都要更新缓存和数据库,写入性能可能较低。
- 如果数据库写入失败(但是写入者没有感知到,或原子性策略不成功),缓存和数据库可能出现不一致的情况。
3.异步更新模式(Write Behind)
这个模式下,写操作只作用于缓存,缓存中的数据异步写入到数据库中。
流程:
- 写入时,应用先将数据写入缓存,然后异步地将数据写入数据库。
- 查询时,直接从缓存中读取数据。
优点:
- 写操作性能较高,因为不需要立即更新数据库。
- 查询效率也高,因为数据总是从缓存中获取。
缺点:
- 存在数据一致性风险,因为数据可能还未同步到数据库就被读取。
- 如果系统崩溃,可能会导致数据丢失。
同步更新和 异步更新模式的关键区别:
同步更新模式
- 强一致性:只有当缓存和数据库都成功写入时,才认为写入操作成功。
- 失败处理:如果任一写入失败,整个操作会被视为失败,通常需要进行错误处理或重试。
- 保证数据一致性:确保数据在缓存和数据库中始终保持一致性。
异步更新模式
- 弱一致性:只要缓存写入成功,操作就被视为成功,即使数据库写入失败。
- 异步处理:数据库的写入在后台进行,可能导致短时间内的数据不一致。
- 性能优先:提升了写入性能,适合对实时性要求不高的场景。
这种设计选择取决于具体的业务需求和对一致性的要求。
4.提前刷新模式(Refresh Ahead)
Refresh Ahead 主要是针对读场景的优化策略,其核心目的是在数据即将过期时提前刷新缓存,以提高查询性能。在写入操作方面,通常仍然是直接写入数据库。
- 读场景:通过预先刷新,确保缓存中的数据在高频访问时始终可用。
- 写场景:依赖于数据库的更新,可能会使用其他策略(如 Write Through 或 Write Behind)来处理缓存和数据库之间的一致性。
写入流程
- 直接写数据库:当应用需要更新数据时,首先将数据写入数据库。
- 更新缓存(可选):可以选择立即更新缓存(如 Write Through),或在后台刷新(如 Write Behind)。
- 缓存刷新:在 Refresh Ahead 策略下,缓存会在接近过期时被异步刷新,以确保数据的可用性。
优点:
- 查询时减少缓存未命中的概率,避免查询时去读取数据库,提高系统性能。
- 对频繁访问的数据非常有效。
缺点:
- 如果频繁刷新但实际查询量不大,可能会带来不必要的开销。
- 可能会引发数据不一致,特别是在缓存刷新期间,数据库中的数据发生了变化。
以上是非常常用的一Redis缓存更新方式,我们看到除了同步更新模式的一致性更好之外,其他的都存在明显的数据一致性问题。那么有没有其他的尽量保证一致性的方案呢?答案是有的。
5.延时双删
在更新数据时,应用在删除缓存和更新数据库之间引入延迟,执行两次缓存删除。
流程:
- 更新数据库。
- 删除缓存。
- 等待一定时间(例如几毫秒)。
- 再次删除缓存(即使它已经被更新)。
优点:
- 大幅降低了缓存数据未同步到数据库时的读取不一致性。
- 对于高并发的场景,双重删除可以显著减少缓存不一致的机会。
缺点:
- 增加了写操作的延迟。
- 需要合理控制延迟时间,以防止引入过多的延迟或复杂性。
6.分布式锁
在执行更新操作时,通过分布式锁来确保在同一时间只有一个实例能对数据进行更新。
流程:
- 获取分布式锁。
- 更新数据库。
- 更新或删除缓存。
- 释放锁。
优点:
- 保证在高并发场景下数据一致性。
- 避免多个实例同时对同一数据进行写入,导致数据冲突。
缺点:
- 引入了额外的复杂性和性能开销。
- 锁的管理不当可能导致死锁或性能瓶颈。
7.事件驱动架构
时间驱动架构如下:
使用消息队列或事件总线,在数据库更新时触发事件,异步地更新缓存。
流程:
- 更新数据库。
- 发布数据更新事件到消息队列。
- 监听消息的缓存更新服务消费事件,并更新缓存。
优点:
- 通过异步处理降低了数据库写入的延迟。
- 系统扩展性好,能够处理高并发情况。
缺点:
- 实现复杂,需处理事件丢失或重复消费的情况。
- 事件消费延迟可能导致短期内数据不一致。
8.版本号控制
为每个缓存数据增加版本号,每次更新数据时,检查版本号来决定是否更新缓存。
流程:
- 查询当前数据版本。
- 更新数据库时,版本号加1。
- 如果缓存中存储的版本号与数据库不一致,则更新缓存。
优点:
- 通过版本号控制,可以有效降低缓存不一致的问题。
- 适合高并发场景,可以减少无效的缓存更新。
缺点:
- 需要额外的版本控制逻辑,增加了实现复杂性。
- 在高并发情况下,可能导致版本号冲突。
9.双写
这个方案和【2.同步更新模式(Write Through)】中的模式是一样的,同时更新数据库和缓存,并确保二者在同一时刻的一致性,只不过这里强调两个动作组成的原子性,即通过事务来保证。
流程:
- 在事务中同时写入缓存和数据库。
- 只有在二者都成功时,提交事务。
优点:
- 能够保证数据的一致性。
- 对于少量数据写入操作,这种方法非常有效。
缺点:
- 复杂性高,特别是在分布式系统中。
- 事务处理可能引入性能瓶颈。
10.最终一致性
在不要求严格一致性的情况下,采用最终一致性策略,允许短期内的不一致,系统会通过后续的同步过程恢复一致性。
优点:
- 可以提高系统的可用性和性能。
- 对于一些业务场景,最终一致性是可以接受的。
缺点:
- 需要合理设计一致性恢复的逻辑。
- 业务逻辑复杂度增加。
总结
选择适合的策略取决于你的具体场景、业务需求和系统架构。结合这些策略,可以实现更高效的数据更新和一致性维护。