目录

MySQL实战经验

1、数据库与缓存不一致问题

1.1、问题产生

为什么用缓存?

  • 提升读性能:db受限于磁盘限制,缓存基于内存,磁盘与内存的读性能想差甚远

会产生什么问题?

  • 数据不一致:数据冗余意味着不一致

1.2、解决办法

要点:并发(时序)、异常情况 指标:缓存利用率、并发安全、一致性

1.2.1、MySQL单机版

最佳实践:先更新数据库,再删除缓存 方案一:更新数据库、更新缓存

  • 思路
    • 先更新数据库,再更新缓存
    • 先更新数据库,再更新缓存
  • 问题
    • 异常问题:第一步更新成功,第一步更新失败,必将导致不一致。
    • 并发问题:并发必将有时序问题。
      • 例子
        • A线程更新数据库(X=1),B线程更新数据库(X=2)
        • B线程更新缓存(X=2),A线程更新缓存(X=1)
      • 结果:X的值,数据库=2,缓存=1,不一致持续时长取决于缓存过期时间
  • 结论:该方案原则上不推荐使用,主要是时序问题无法保证

方案二:更新数据库、删除缓存

  • 思路

    • 先删除缓存,再更新数据库
    • 先更新数据库,再删除缓存
  • 问题

    • 异常问题:跟第一种方案同理无法避免,异常必将导致不一致
    • 并发问题
      • 先删除缓存,再更新数据库
        • 例子
          • 1、线程A要更新X=2(原值X=1)
          • 2、线程A先删除缓存
          • 3、线程B读取缓存,发现不存在,从数据库读取旧值(X=1)
          • 4、线程A将新值写入数据库(X=2)
          • 5、线程B将旧值写入缓存(X=1)
        • 结果:X的值,数据库=2,缓存=1
        • 分析:读写请求并发,A、B线程交替读写,出现概率还是可能的。
      • 先更新数据库,再删除缓存
        • 例子
          • 1、缓存中 X 不存在(数据库 X = 1)
          • 2、线程 A 读取数据库,得到旧值(X = 1)
          • 3、线程 B 更新数据库(X = 2)
          • 4、线程 B 删除缓存
          • 5、线程 A 将旧值写入缓存(X = 1)
        • 结果:X的值,数据库=2,缓存=1
        • 分析:出现时机且满足以下三个条件,现实中出现概率较少,存在理论上的可能
          • 线程A读请求时,缓存已失效
          • 读写请求并发
          • 步骤2中读时长 > 「步骤3+步骤4」中的「更新+删除」动作总时长概率较低
  • 结论:「先更新数据库,再删除缓存」从分析结果来看能绝大概率解决并发问题,是该命中的最佳实践。

根据「先更新数据库,再删除缓存」仅解决并发问题,如何解决异常问题?

  • 思路一:一致性要求不高,容忍短暂不一致,等待缓存到过期时间,不处理
  • 思路二:删缓存动作失败,尝试充实重试
    • 同步重试:仅在网络抖动丢包的情况
    • 异步重试:写MQ异步读取消费,MQ需保证可靠性直到消费成功
  • 思路三:监听MySQL的binlog,思路就跟主从原理类似,实现缓存同步。
    • 市场上现有工具可参考阿里的canal,「数据库+缓存」、「数据库 + es索引」

1.2.2、MySQL主从 + 读写分离

问题:MySQL主从 + 读写分离架构中,读请求存在延迟导致不一致?

  1. 线程 A 更新主库 X = 2(原值 X = 1)
  2. 线程 A 删除缓存
  3. 线程 B 查询缓存,没有命中,查询「从库」得到旧值(从库 X = 1)
  4. 从库「同步」完成(主从库 X = 2)
  5. 线程 B 将「旧值」写入缓存(X = 1) 结果: X 的值在缓存中是 1(旧值),在主从库中是 2(新值),也发生不一致。 分析:在有大事务或资源抢占明显时,主从延时的概率还是存在的,故发生的概率还是会存在

思路:根据经验,预估主从延时时间,将缓存再次删除

  • 方案一:更新数据库,删除缓存,休眠等待延时时间,再次删除缓存
  • 方案二(推荐):更新数据库,将删动作写入延时队列(延时时间根据经验1-5s),定期消费消息执行删除动作
  • 方案三:选择性读主,更新数据库时,记录「库 + 表 + pk」作为键记录到缓存中,读db时根据该键决定是否读主

总结:最佳实践选择「先更新数据库,再删除缓存」的方案实现数据库与缓存一致,根据业务一致性要求和MySQL主从+读写分离架构考虑是否要使用延迟删除方案。

实践:写多读少场景保db高可用,读多写少做缓存高可用 + 容灾缓存。

其他:很多时候,缓存也存在主从架构,甚至还有跨机房延迟等情况,实际从理论上提出完美方案就非常复杂了。最佳办法还是保证每一个子系统子服务保证一致,写的代码保障高质量避免极端情况出现,故才能达到全局最优的方案。

2、数据库与ES同步问题

问题起源:在用户产品研发中,经常会碰到很多复杂多样的查询,如标签、分类、人群等,而实际的元数据却存在于关系型数据库MySQL,关系型数据库对于复杂查询很难支持,同时它也不适合承担太多索引的职责。于是,引入ES充当索引角色,也就是形成复杂标签与原数据ID成映射关系。

  • 读请求:查询es索引,找到索引与元数据ID,通过元数据ID查MySQL具体数据,最终返回

  • 写请求

    • 方式一(同步写):写MySQL、写ES索引,存在写失败问题,扩展性也不好
    • 方式二(异步写)
      • 来源一:监听MySQL的binlog,将binlog写入MQ,解析binlog将数据同步到ES
      • 来源二:业务制定日志规范,打印用户日志,采集用户日志,流式计算日志,将日志导入ES

参考资料