彻底搞明白redis和mysql数据一致性方案分析与选择

2024年 3月 25日 113.5k 0

1. 业务背景

本篇文章主要来说说redis和mysql中的数据一致性问题。首先给出结论:不存在强一致性的完美解决方案,选择"先更新mysql在删除redis"方案是产生数据不一致的概率最低,数据丢失风险最小,把控度最高的方案。

我们在日常的项目里面,通常都会涉及把一些不怎么经常变化但是又经常访问的数据放在redis缓存中以提高读取数据的性能,如果从redis查询到数据则返回,没有查询到则从mysql中查询,然后回写到redis。这是很常规的操作,大家是再熟悉不过了。例如自己所在的智能硬件项目,经常会根据固件版本获取硬件固件信息,以此来判断此版本是否存在,这个固件信息一般上传之后很久不会变动,但是会经常的查询。如下:

/**
 * 根据版本获取固件信息,来判断此版本是否存在
 */
@Test
public void testFirmware() {
    FirmwareInfo firmwareInfo = firmwareService.get("0.0.1");
    log.info("firmwareInfo: {}", JSON.toJSONString(firmwareInfo));
}


/**
 * 根据版本号获取设备固件信息
 * @param version
 * @return
 */
@Override
public FirmwareInfo get(String version) {
    // 从redis中获取固件信息
    Object firmwareInfo  = redisUtil.get(getVersionKey(version));

    // 如果没有则从mysql中查询
    if(Objects.isNull(firmwareInfo)) {
        // 从DB查询
        firmwareInfo = firmwareInfoMapper.selectByVersion(version);
        // 最后回写到redis
        redisUtil.set(getVersionKey(version), Objects.isNull(firmwareInfo) ? 0 : firmwareInfo, 72);
    }
    // 最后返回固件信息
    return (Objects.isNull(firmwareInfo) ||  firmwareInfo.equals(0)) ? null : (FirmwareInfo)firmwareInfo;
}

但是固件信息有时候在某种情况下也是需要更新的,例如:我们的固件的下载url更新了,我们需要根据version获取新的url信息。那我们必然要更新这个新的url,更新策略是怎样的???

2. 几种更新策略分析

2.1 先更新mysql再更新redis

主要存在的问题:

  • 并发更新问题
  • image.png

    多个请求并发例如请求A、B同时更新url,当请求B更新mysql出现在请求A之后,更新redis出现在请求A之前。会出现mysql中的url和redis中的url不一致。如上橙色框中所示。

  • 第二步操作失败问题
  • 如果第二步更新redis失败,会导致redis中的是旧的数据,mysql中是新的数据,mysql中的数据与redis中的数据不一致。

  • 并发更新读取问题
  • image.png

    当A请求更新的同时,B请求同时读取固件信息。当B请求读取固件信息发生在A更新redis之前,B读取到redis中旧的url,然后A更新reids中的新的url。此时会出现一次数据不一致,但是下次B再次读取时,会读取到redis中新的url。

    这个概率很大,因为请求A有更新mysql+更新redis两步操作,而请求B只有读取redis缓存一步操作。

  • 缓存刚好失效&&并发更新读取
  • image.png

    这是一种特殊的情况,缓存刚好失效,读请求B在A请求更新mysql之前读取到mysql中的旧数据,然后A更新成功,最后B回写redis旧数据0.0.1。此时出现redis和mysql中数据不一致。但是这种情况的发生条件比较严格,需要同时满足下面的条件:

    • 并发更新读取的时候,刚好redis失效
    • B请求读取mysql发生在A更新mysql之前
    • B请求回写redis旧数据发生在A请求更新redis之后

    这3者同时满足的概率很低,几乎可以忽略不计了。

    2.2 先更新redis再更新mysql

    跟上面"先更新mysql再更新redis"类似,不再画图说明。主要存在的问题:

  • 并发更新问题
  • 请求A、B同时更新url,当请求B的更新redis发生在请求A之后,当请求B更新mysql出现在请求A之前。会出现mysql中的url和redis中的url不一致。

  • 第二步操作失败
  • 如果第二步更新mysql失败,会导致mysql中的是旧的数据,redis中是新的数据,mysql中的数据与redis中的数据不一致。当redis失效之后,会读取到mysql中的旧数据。这种数据没有落库到mysql,只是存在redis中,数据丢失风险大。所以"先更新redis再更新mysql"这种方案几乎不考虑。

  • 并发更新读取不会有问题
  • image.png

    当A请求更新的同时,B请求同时读取固件信息。当B请求读取固件信息不管是发生在A更新mysql之前还是之后,B读取到redis中始终是新的url。此时不会出现数据不一致。

  • 缓存刚好失效&&并发更新读取
  • image.png

    这是一种特殊的情况,读请求B在请求A更新redis之前缓存刚好失效,读请求B在A请求更新mysql之前读取到mysql中的旧数据,然后A更新成功,最后B回写redis旧数据0.0.1。此时出现redis和mysql中数据不一致。但是这种情况的发生条件比较严格,需要同时满足下面的条件:

    • 读请求B在请求A更新redis之前缓存刚好失效
    • B请求读取mysql发生在A更新mysql之前
    • B请求回写redis旧数据发生在A请求更新mysql之后

    这3者同时满足的概率很低,几乎可以忽略不计了。

    2.3 先删除redis再更新mysql

  • 并发更新不会有问题
  • image.png
    redis和mysql中数据不会产生不一致问题,因为redis中的数据会被删除,只有mysql中有数据。

  • 第二步操作失败
  • 这个比较简单,直接说明,不再画图。更新mysql失败,则mysql中是旧的数据,由于redis中数据已经删除,所以不会产生数据不一致。但是mysql中始终是旧的数据,所以下次请求读取数据,获取到的都是旧的数据,需要要加上重试更新mysql机制。并且mysql中的数据一般来说应该要保证准确性权威性的。这种方案"先删除redis再更新mysql"一般来说也不可取。

  • 并发更新读取
  • image.png

    如上图,当读请求B在写请求A执行更新mysql之前,读取到了mysql中的旧数据url=0.0.1,然后回写redis,此时就会出现mysql中的数据和redis中的数据不一致。这个不一致需要额外采取补救措施。此时的补救措施是大家常常听到的"延迟双删"。就是写请求A在更新完mysql之后,延迟一段时间在删除redis缓存,下次读请求会从mysql中读取,然后回写redis,达到最终的一致性。如果删除失败,需要重试。整个实现比较复杂,可控性比较差。
    这个方案有下面一些缺点:

    • 延迟的时间不好把控,时间设置短了,删除redis操作发生在请求B把旧数据回写到redis之前,这样redis还是会缓存旧的数据;时间设置长了,数据不一致的时间窗口会很长。
    • 这个延迟处理,需要借助MQ,发送MQ异步延迟消息,然后消费端延迟消费删除缓存
  • 缓存刚好失效&&并发更新读取
  • image.png

    这是一种特殊的情况,缓存刚好失效,读请求B在A请求更新mysql之前读取到mysql中的旧数据,然后A更新成功,最后B回写redis旧数据0.0.1。此时出现redis和mysql中数据不一致。但是这种情况的发生条件比较严格,需要同时满足下面的条件:

    • 并发更新读取的时候,刚好redis失效
    • B请求读取mysql发生在A更新mysql之前
    • B请求回写redis旧数据发生在A请求删除redis之后

    这3者同时满足的概率很低,几乎可以忽略不计了。

    2.4 先更新mysql再删除redis

  • 并发更新不会有问题
  • image.png
    并发更新时跟上面的"先删除redis再更新mysql"类似,redis和mysql中数据不会产生不一致问题,因为redis中的数据最终都会被删除,只有mysql中有数据。

  • 第二步操作失败
  • 这个比较简单,直接说明,不再画图。删除redis失败,则redis中是旧的数据,所以会产生数据不一致。可以采用处理方式:

    • 异步发送MQ消息重试删除
    • redis的key设置过期时间,时间到期后自动删除,达到最终一致性

    对比上面的"先删除redis再更新mysql"方案,此方案只需重试删除。

  • 并发更新读取
  • image.png

    当读请求B读取redis发生在写请求A删除redis之前时,B读取到redis中的旧数据,仅仅存在这1次数据不一致。后面由于A删除了redis,B读取数据从mysql读取然后回写redis,数据达到一致。同时这个数据不一致的时间窗口存在于请求A更新mysql和删除redis之间的短暂间隙时间,非常短暂,不一致的时间窗口很短。

  • 缓存刚好失效&&并发更新读取
  • image.png

    这是一种特殊的情况,缓存刚好失效,读请求B在A请求更新mysql之前读取到mysql中的旧数据,然后A更新成功,删除redis,最后B回写redis旧数据0.0.1。此时出现redis和mysql中数据不一致。但是这种情况的发生条件比较严格,需要同时满足下面的条件:

    • 并发更新读取的时候,刚好redis失效
    • B请求读取mysql发生在A更新mysql之前
    • B请求回写redis旧数据发生在A请求删除redis之后

    这3者同时满足的概率很低,几乎可以忽略不计了。

    3. 更新策略对比

    更新策略 并发更新导致不一致 第二步操作失败导致不一致 并发更新读取导致不一致 缓存刚好失效&&并发更新读取导致不一致
    先更新mysql再更新redis 存在 存在 存在一次,概率大 存在,概率很低
    先更新redis再更新mysql 存在 存在,且数据丢失风险大 不存在 存在,概率很低
    先删除redis再更新mysql 不存在 不存在,但数据丢失风险大,数据权威性破坏 存在,延迟双删时间难把控 存在,概率很低
    先更新mysql再删除redis 不存在 存在,重试删除即可 只存在1次不一致,产生不一致的时间窗口很短 存在,概率很低

    4. 最后总结

    • 先更新mysql再更新redis,在并发情况下,出现数据不一致的概率很大,此方案不考虑。
    • 先更新redis再更新mysql ,在并发情况下,出现数据不一致的概率很大。并且如果更新mysql失败,需要引入重试机制,重试次数等都不好把控。并且新的数据存在redis缓存,mysql没有落库,数据丢失风险高。此方案也不考虑。
    • 先删除redis再更新mysql,如果更新mysql失败,虽然不会导致数据不一致,但是新的数据更新没有安全落库到mysql,同时redis缓存此时也删除了,会有数据丢失的风险,mysql的数据安全权威性遭到破坏。同时并发更新读取时,会导致数据不一致,需要引入"延迟双删"机制,此机制的延迟时间也不好把控
    • 所以只剩下最后的"先更新mysql再删除redis"方案。首先并发更新不会导致数据不一致,其次第二步删除redis失败重试即可实现简单好把控,最后并发更新读取时,只会存在一次数据不一致并且这个不一致的时间窗口期很短。

    所以综上所述,综合考虑,选择"先更新mysql在删除redis"方案是产生数据不一致的概率最低,数据丢失风险最小,把控度最高的方案。

    相关文章

    Oracle如何使用授予和撤销权限的语法和示例
    Awesome Project: 探索 MatrixOrigin 云原生分布式数据库
    下载丨66页PDF,云和恩墨技术通讯(2024年7月刊)
    社区版oceanbase安装
    Oracle 导出CSV工具-sqluldr2
    ETL数据集成丨快速将MySQL数据迁移至Doris数据库

    发布评论