面试题:缓存与数据库双写不一致解决方案

面试 Redis About 1,815 words

前提

保证最终一致性的解决方案是缓存设置过期时间。以下方案讨论的是不依赖于给缓存设置过期时间的情况。

方案一:先更新缓存,再更新数据库

不推荐。

先更新缓存若更新数据库失败,还需再更新缓存。

方案二:先更新数据库,再更新缓存

不推荐。

同时有请求A和请求B进行更新操作,请求A与B在不同线程,可能会出现:

  1. 请求A更新了数据库
  2. 请求B更新了数据库
  3. 请求B更新了缓存
  4. 请求A更新了缓存

这就出现请求A更新缓存应该比请求B更新缓存早才对,但是因为网络等原因,B却比A更早更新了缓存。这就导致了脏数据,因此不考虑。

方案三:先删除缓存,再更新数据库

有点问题。

有一个请求A进行更新操作,另一个请求B进行查询操作,可能会出现:

单个数据库

  1. 请求A进行写操作,删除缓存
  2. 请求B查询发现缓存不存在
  3. 请求B去数据库查询得到旧值
  4. 请求B将旧值写入缓存
  5. 请求A将新值写入数据库

读写分离架构

  1. 请求A进行写操作,删除缓存
  2. 请求A将数据写入数据库了,
  3. 请求B查询缓存发现,缓存没有值
  4. 请求B去从库查询,这时,还没有完成主从同步,因此查询到的是旧值
  5. 请求B将旧值写入缓存

数据库完成主从同步,从库变为新值

上述情况就会导致不一致的情形出现。而且,如果不采用给缓存设置过期时间策略,该数据永远都是脏数据。

解决方案

延时双删策略

public void write(String key,Object data){
    redis.delKey(key);
    db.updateData(data);
    Thread.sleep(1000);
    redis.delKey(key);
}

翻译翻译

  1. 先淘汰缓存
  2. 再写数据库(这两步和原来一样)
  3. 休眠1
  4. 再次淘汰缓存

休眠时间

自行评估自己的项目的读数据业务逻辑的耗时。然后写数据的休眠时间则在读数据业务逻辑的耗时基础上,加几百ms即可。这么做的目的,就是确保读请求结束,写请求可以删除读请求造成的缓存脏数据。

对于MySQL读写分离架构,只是睡眠时间修改为在主从同步的延时时间基础上,加几百ms

方案四:先更新数据库,再删除缓存

极端情况有问题。

有一个请求A进行更新操作,另一个请求B进行查询操作,可能会出现:

  1. 请求A查询数据库得到一个旧值
  2. 请求B将新值写入数据库
  3. 请求B删除缓存
  4. 请求A将查到的旧值写入缓存

发生这种情况的概率

步骤2的写数据库操作比步骤1的读数据库操作耗时更短,才有可能使得步骤3先于步骤4。可是,大家想想,数据库的读操作的速度远快于写操作的,因此步骤2耗时比步骤1更短,这一情形很难出现。

解决方案

延时双删策略

public void write(String key,Object data){
    db.updateData(data);
    redis.delKey(key);
    Thread.sleep(1000);
    redis.delKey(key);
}

翻译翻译

  1. 先写数据库
  2. 再淘汰缓存
  3. 休眠1
  4. 再次淘汰缓存

方案三与方案四还存在问题

问题

  1. 同步双删导致并发降低
  2. 比如一个写数据请求,然后写入数据库了,删缓存失败了,这会就出现不一致的情况。

问题一解决方案

异步。

问题二解决方案

提供一个保障的重试机制。

方案一:消息队列方式

pic1.png

  1. 更新数据库数据
  2. 缓存因为种种问题删除失败
  3. 将需要删除的key发送至消息队列
  4. 自己消费消息,获得需要删除的key
  5. 继续重试删除操作,直到成功

业务线代码侵入较大。

方案二:订阅binlong方式

pic2.png

  1. 更新数据库数据
  2. 数据库会将操作信息写入binlog日志当中
  3. 订阅程序提取出所需要的数据以及key
  4. 另起一段非业务代码,获得该信息
  5. 尝试删除缓存操作,发现删除失败
  6. 将这些信息发送至消息队列
  7. 重新从消息队列中获得该数据,重试操作。

订阅binlog程序在MySQL中有阿里开源的中间件叫canal

备注

如果对一致性要求不是很高,直接在程序中另起一个线程,每隔一段时间去重试也可。

总结

根据数据实时性要求,以及系统并发量考虑。

实时性不强,则可以选择设定缓存过期时间,先删缓存再更新数据库或先更新数据库再删缓存方案都可行。

实时性较强的,又有大并发量可以考虑延迟双删策略。

至于其他如请求串行化,放入同一个队列中依次执行的,复杂没必要。

Views: 5,319 · Posted: 2019-11-16

————        END        ————

Give me a Star, Thanks:)

https://github.com/fendoudebb/LiteNote

扫描下方二维码关注公众号和小程序↓↓↓

扫描下方二维码关注公众号和小程序↓↓↓


Today On History
Browsing Refresh