如何保证Redis与Mysql双写一致性?

https://www.cnblogs.com/coderacademy/p/18137480

延迟双删

对于上面链接的文章,里面的延迟双删没有给出具体的例子,也没有直接指出具体解决的问题是针对那种缓存策略,这里补充一下,延时双删缓存针对的是Cache aside pattern(缓存旁路策略),处理的是在高并发读写同时存在的情况下可能会出现的问题,详细如下。

什么是延迟双删:

延时双删策略能够有效解决缓存和数据库之间的数据不一致问题。它的核心思想是在更新数据库之后,先删除缓存中的数据,延迟一段时间后再次删除缓存中的数据。其具体步骤如下:

  1. 更新数据库:先将数据更新到数据库中。
  2. 删除缓存:立即删除缓存中对应的旧数据。
  3. 延迟一段时间:等待一段时间(通常是足够长以保证并发写入完成的时间)。
  4. 再次删除缓存:再次删除缓存中的数据,以防止并发操作在缓存中留下旧数据。

tips:延迟双删需要由更新数据的那个程序去处理。

延时双删策略的应用场景

延时双删策略的应用场景通常涉及**高并发写操作**和**读取操作**同时发生的情况。以下是一个典型的具体场景,展示如何出现缓存和数据库数据不一致的问题。

场景描述

假设有一个电商网站,使用缓存(如 Redis)来加速商品信息的读取。例如,一个商品的库存信息存储在数据库中,同时也缓存到 Redis 中,以便快速读取。

具体流程

  1. **用户A请求读取商品库存信息:**
  • 用户A请求读取某个商品的库存信息,系统会优先从缓存(Redis)中读取该商品的库存。如果缓存中存在,则直接返回;如果不存在,则从数据库读取并将结果缓存起来。
  1. **用户B请求更新商品库存信息:**
  • 在用户A读取缓存中的商品库存时,用户B执行了一个购买操作,该操作会更新商品的库存信息。系统会先更新数据库中的商品库存,然后删除缓存中的商品库存信息,以确保下一次读取时会从数据库中获取最新数据。

问题出现

在用户B更新商品库存后,但**在删除缓存之后、更新数据库完成之前的时间窗口**内,用户A再次读取商品库存信息。这会出现以下问题:

  • 用户B更新库存后,缓存被删除,但是用户A此时发起读取请求,因为缓存已经被删除,系统会去数据库中读取库存数据。

  • 但数据库还没有完成更新操作(可能因为写入操作较慢,或者在执行事务),用户A读取到的仍然是旧的库存信息。

  • 最后,数据库更新完成,数据正确,但用户A刚刚读取到了错误(旧)的库存数据,导致**数据不一致**。

延时双删策略如何解决这个问题

为了防止上述数据不一致情况的发生,可以使用延时双删策略:

  1. **用户B更新商品库存信息:**
  • 更新数据库中的库存信息。

  • **立即删除缓存**中的商品库存信息。

  1. **用户A读取商品库存信息:**
  • 如果在缓存被删除之后读取,系统会从数据库中读取。

  • 用户A读取时可能得到旧数据(数据库尚未更新完成),但接下来的延时操作将解决这个问题。

  1. **延迟删除缓存:**
  • 设置一个延迟(例如 500 毫秒),在此延迟之后,再次尝试删除缓存中的商品库存信息。

  • 这段延迟时间应足够长,以确保数据库更新操作已经完成。

具体应用中的时间点

  • **T1:用户B请求更新库存,系统开始更新数据库。**

  • **T2:用户B更新数据库后,立即删除缓存。**

  • **T3:数据库更新操作未完成,用户A读取库存,发现缓存不存在,转向读取数据库。**

  • **T4:用户A读取到旧的库存数据(数据库写操作未完成)。**

  • **T5:延迟一段时间(如500毫秒),再次删除缓存。**

  • **T6:用户A再次请求,发现缓存不存在,此时数据库已更新,读取到最新库存信息。**

为什么这个策略有效

  1. **避免读取旧数据**:通过在更新数据库后立即删除缓存,避免缓存中存在旧数据。

  2. **降低不一致的窗口期**:延迟删除缓存提供了一个补偿机制,以防止在数据库写入完成前的缓存穿透现象。

  3. **高效读取最新数据**:第二次删除缓存确保了之后的读取操作总能获取最新数据,减少了缓存和数据库之间的数据不一致的风险。

总结

延时双删策略主要解决的是在缓存删除和数据库更新之间的短时间窗口内出现数据不一致的问题,特别适用于**高并发写操作和读操作混合**的场景,如电商库存更新、金融交易系统的账户余额更新等。通过这种策略,系统能够更好地保障缓存和数据库之间的数据一致性。

延迟双删除golang代码示例

Go 复制代码
package main

import (
    "context"
    "fmt"
    "time"

    "go.mongodb.org/mongo-driver/bson"
    "go.mongodb.org/mongo-driver/mongo"
    "go.mongodb.org/mongo-driver/mongo/options"
)

func main() {
    // 示例:更新数据库并执行延时双删策略

    // 示例的数据库操作
    ctx := context.Background()

    // 第一步:更新数据库
    updateDatabase(ctx)

    // 第二步:立即删除缓存
    deleteCache()

    // 第三步:设置一个延迟任务(如 500 毫秒)
    time.AfterFunc(500*time.Millisecond, func() {
        deleteCache() // 再次删除缓存
    })

    fmt.Println("Database update and delayed cache deletion scheduled.")
}

func updateDatabase(ctx context.Context) {
    // 示例数据库更新操作
    fmt.Println("Updating database...")
    // 在此处执行数据库的更新操作...
}

func deleteCache() {
    // 示例缓存删除操作
    fmt.Println("Deleting cache...")
    // 在此处执行缓存删除操作...
}

Read-Through 和Cache Aside Pattern的读有什么区别

**Read-Through** 和 **Cache Aside Pattern**(又称为 **Lazy Loading** 或 **Lazy Caching**)是两种常见的缓存策略,它们在缓存读取(读)操作的实现上有一些关键区别。

Read-Through 缓存策略

**Read-Through** 是一种由缓存层自动管理数据加载的策略,读操作首先检查缓存,如果缓存中没有数据,缓存层会自动从后端数据源(例如数据库)加载数据并将其存储到缓存中,然后返回给调用者。

工作原理

  1. **应用程序请求数据**:
  • 应用程序向缓存层请求数据。
  1. **缓存层检查数据**:
  • 如果缓存中有数据,直接返回。

  • 如果缓存中没有数据,缓存层会自动从后端数据源(如数据库)加载数据。

  1. **缓存层更新缓存**:
  • 将从后端数据源获取的数据存储到缓存中,以便下次快速访问。
  1. **返回数据**:
  • 缓存层返回数据给应用程序。

特点

  • **自动加载数据**:缓存层自动处理缓存未命中的情况,将数据从后端数据源加载到缓存中。

  • **透明性**:应用程序不需要关心数据从哪里来,缓存层会自动管理。

  • **常用于缓存代理中**:例如,使用特定的缓存中间件或服务来管理缓存。

例子

如使用 AWS ElastiCache 或 Memcached 的集成模式,其中缓存中间件自动处理数据加载和缓存更新。

Cache Aside Pattern 缓存策略

**Cache Aside Pattern**(Lazy Loading)是一种由应用程序主动管理缓存的数据加载的策略。应用程序在读取数据时首先检查缓存,如果缓存未命中,应用程序会主动从后端数据源加载数据并手动将数据写入缓存。

工作原理

  1. **应用程序请求数据**:
  • 应用程序首先检查缓存中是否有数据。
  1. **缓存未命中时应用程序加载数据**:
  • 如果缓存中没有数据,应用程序从后端数据源(如数据库)加载数据。
  1. **应用程序更新缓存**:
  • 应用程序将从后端数据源获取的数据写入缓存。
  1. **返回数据**:
  • 应用程序返回数据给调用者。

特点

  • **主动管理缓存**:应用程序负责检查缓存、加载数据和更新缓存。

  • **灵活性**:应用程序可以决定何时加载和更新数据,缓存的逻辑在应用程序中控制。

  • **常用于手动控制缓存的场景**:例如,通过代码来管理缓存操作。

例子

在 Go、Java 或 Python 应用中,程序员在业务逻辑中手动管理从缓存中读取数据,未命中时从数据库加载数据,并将其写入缓存的操作。

区别

| 特性 | Read-Through | Cache Aside Pattern |

|-------------------------------|-------------------------------------------------------|-----------------------------------------------------|

| **数据加载责任** | 缓存层自动负责从后端加载数据 | 应用程序负责加载数据和更新缓存 |

| **实现难度** | 较低,缓存层自动管理数据加载 | 较高,应用程序需要管理缓存逻辑 |

| **灵活性** | 较低,缓存策略由缓存层定义 | 较高,应用程序可以控制何时加载和更新数据 |

| **常见使用场景** | 通常用于缓存中间件或代理(如 Memcached, AWS ElastiCache)| 手动管理缓存的应用程序 |

| **缓存未命中后的开销** | 缓存层负责处理加载,应用程序不感知 | 应用程序处理加载逻辑,有可能影响性能 |

| **读写操作的复杂度** | 读操作简单,缓存层透明处理 | 读操作复杂,需要在应用中显式处理缓存和数据库访问 |

总结

  • **Read-Through** 更适合希望透明缓存管理的场景,使用缓存中间件或代理自动处理数据加载,简化应用逻辑。

  • **Cache Aside Pattern** 更适合需要灵活控制缓存逻辑的场景,应用程序可以根据业务需求主动决定何时加载和更新缓存。

相关推荐
新法国菜9 分钟前
MySql知识梳理之DDL语句
数据库·mysql
DarkAthena1 小时前
【GaussDB】全密态等值查询功能测试及全密态技术介绍
数据库·gaussdb
ShawnLeiLei1 小时前
2.3 Flink的核心概念解析
数据库·python·flink
小花鱼20252 小时前
redis在Spring中应用相关
redis·spring
郭京京2 小时前
redis基本操作
redis·go
似水流年流不尽思念2 小时前
Redis 分布式锁和 Zookeeper 进行比对的优缺点?
redis·后端
郭京京2 小时前
go操作redis
redis·后端·go
石皮幼鸟2 小时前
数据完整性在所有场景下都很重要吗?
数据库·后端
nightunderblackcat4 小时前
新手向:异步编程入门asyncio最佳实践
前端·数据库·python
DarkAthena4 小时前
【GaussDB】使用MySQL客户端连接到GaussDB的M-Compatibility数据库
数据库·mysql·gaussdb