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

1. 业务背景

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

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

java 复制代码
/**
 * 根据版本获取固件信息,来判断此版本是否存在
 */
@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

主要存在的问题:

  1. 并发更新问题

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

  1. 第二步操作失败问题

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

  1. 并发更新读取问题

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

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

  1. 缓存刚好失效&&并发更新读取

这是一种特殊的情况,缓存刚好失效,读请求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"类似,不再画图说明。主要存在的问题:

  1. 并发更新问题

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

  1. 第二步操作失败

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

  1. 并发更新读取不会有问题

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

  1. 缓存刚好失效&&并发更新读取

这是一种特殊的情况,读请求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

  1. 并发更新不会有问题

redis和mysql中数据不会产生不一致问题,因为redis中的数据会被删除,只有mysql中有数据。

  1. 第二步操作失败

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

  1. 并发更新读取

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

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

这是一种特殊的情况,缓存刚好失效,读请求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

  1. 并发更新不会有问题

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

  1. 第二步操作失败

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

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

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

  1. 并发更新读取

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

  1. 缓存刚好失效&&并发更新读取

这是一种特殊的情况,缓存刚好失效,读请求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 "方案是产生数据不一致的概率最低 ,数据丢失风险最小把控度最高的方案。

相关推荐
momo小菜pa5 小时前
【MySQL 06】表的增删查改
数据库·mysql
BergerLee7 小时前
对不经常变动的数据集合添加Redis缓存
数据库·redis·缓存
程序员大金7 小时前
基于SpringBoot+Vue+MySQL的装修公司管理系统
vue.js·spring boot·mysql
gorgor在码农8 小时前
Mysql 索引底层数据结构和算法
数据结构·数据库·mysql
-seventy-8 小时前
SQL语句 (MySQL)
sql·mysql
huapiaoy8 小时前
Redis中数据类型的使用(hash和list)
redis·算法·哈希算法
一般路过糸.8 小时前
MySQL数据库——索引
数据库·mysql
【D'accumulation】9 小时前
令牌主动失效机制范例(利用redis)注释分析
java·spring boot·redis·后端
Cikiss9 小时前
微服务实战——SpringCache 整合 Redis
java·redis·后端·微服务
无敌少年小旋风10 小时前
MySQL 内部优化特性:索引下推
数据库·mysql