缓存和数据库一致性问题分析

目录

1、数据不一致的原因

1.1 并发操作

1.2 非原子操作

1.3 数据库主从同步延迟

2、数据不一致的解决方案

2.1 并发操作

2.2 非原子操作

2.3 主从同步延迟

2.4 最终方案

3、不同场景下的特殊考虑

3.1 读多写少的场景

3.2 读少写多的场景


1、数据不一致的原因

导致缓存和数据库数据不一致的原因有三个

  • 并发操作
  • 非原子操作
  • 数据库主从同步延迟

1.1 并发操作

假设数据库的原数据为x=0,考虑两种并发情况

  • 两个线程同时写入
  • 一个线程读,一个线程写

有以下4种方案

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

(1)先更新缓存,后更新数据库

两个线程同时写入

  • A更新缓存,x=1
  • B更新缓存,x=2
  • B更新数据库,x=2
  • A更新数据库,x=1

最终缓存为2,数据库为1

一个线程读,一个线程写

  • A读取缓存发现没有命中,于是读取数据库,得到x=0
  • B更新缓存,x=1
  • B更新数据库,x=1
  • A更新缓存,x=0

最终缓存为0,数据库为1

这种情况发生的概率极小,需要同时满足几个条件

  • 第一,缓存不存在
  • 第二,A的更新缓存命令 后于 B的更新缓存命令(基本不可能发生)
  • 第三,A读取数据库+更新缓存的时间 > B更新缓存+B更新数据库的时间(基本不可能发生,因为写数据库的耗时大概率是比读数据库慢)

(2)先更新数据库,后更新缓存

两个线程同时写入

  • A更新数据库,x=2
  • B更新数据库,x=1
  • B更新缓存,x=1
  • A更新缓存,x=2

最终缓存为2,数据库为1

一个线程读,一个线程写

  • A读取缓存发现没有命中,于是读取数据库,得到x=0
  • B更新数据库,x=1
  • B更新缓存,x=1
  • A更新缓存,x=0

最终缓存为0,数据库为1

这种情况发生的概率也是极小,跟方案2的发生条件一样。

(3)先删除缓存,再更新数据库

两个线程同时写入

由于是删除缓存,多线程同时更新的话,缓存都是会被删除,没有讨论意义

一个线程读,一个线程写

  • A读取缓存发现没有命中,于是读取数据库,得到x=0
  • B删除缓存
  • A更新缓存,x=0
  • B更新数据库,x=1

最终,缓存是0,数据库是1

(4)先更新数据库,再删除缓存

两个线程同时写入

由于是删除缓存,多线程同时更新的话,缓存都是会被删除,没有讨论意义

一个线程读,一个线程写

  • A读取缓存发现没有命中,于是读取数据库,得到x=0
  • B更新数据库,x=1
  • B删除缓存
  • A更新缓存,x=0

最终,缓存是0,数据库是1

这种情况发生的概率也是极小,需要同时满足几个条件

  • 第一,缓存不存在
  • 第二,B更新数据库+删除缓存的时间 < A读取数据库+更新缓存的时间 (基本不可能发生,因为写数据库的耗时大概率是比读数据库慢)

1.2 非原子操作

非原子操作很好理解,就是因为操作缓存和操作数据库是两步操作,所以当第二步操作失败时,就会导致数据不一致问题。

1.3 数据库主从同步延迟

以上都只考虑了单机数据库的情况,对于主从数据库还有另一个问题,就是主从同步延迟

考虑以下情况

一个往主库写入,一个从从库读取

  • A更新主库,x=1
  • A删除缓存
  • B查询缓存没有命中,查询从库,得到x=0
  • 从库同步主库,更新为x=1
  • B更新缓存,x=0

最终,缓存是0,数据库是1

2、数据不一致的解决方案

2.1 并发操作

根据1.1的几种方案,『先更新数据库,再删除缓存』是最好的。

2.2 非原子操作

最简单的解决办法就是重试,但是重试也要考虑一些问题:

  • 立即重试的话大概率会失败;
  • 重试次数设置多少比较合适;
  • 同步重试会一直占用资源;
  • 重试的过程中服务重启,会导致重试的操作消失,数据会一直不一致

所以就能想到异步重试方法,也就是把重试的这个操作写入到消息队列里,由另一个线程专门来处理重试的操作,而且消息队列本身可以保证消息不丢失(不丢失有两个方面,一是消息持久化,二是只有正确被消费掉了才会删除)

除了消息队列这中异步重试方案外,业界还有一种监听数据库bin log的方式,监听到变化后再去操作缓存,但是这种会额外引入其他的中间件而且实现复杂,综合而言没有消息队列的方案好用。

2.3 主从同步延迟

主从同步延迟的解决方案也很明显,就是延迟删除缓存,但是延迟多久再执行删除呢?这个就要靠经验了,一般来说1~5秒不等,看具体业务

所以在之前方案的基础上,引入延迟删除可以解决主从同步延迟的问题。

2.4 最终方案

1)更新数据库

2)删除缓存

3)如果删除失败则额外写入一条重试删除命令到消息队列

4)最后写入一个延迟删除命令到消息队列

3、不同场景下的特殊考虑

3.1 读多写少的场景

这种场景下,redis的作用是为了减轻数据库读取压力、加速读取,往往能接受一定的数据延迟,即保证最终一致性即可。

  • 读多,所以删除缓存操作会导致缓存穿透问题(key不存在,大量请求命中数据库)
  • 写少,所以基本不存在并发写入的问题,但是会存在并发读取和写入的问题

所以,读多写少的场景下,方案3和4是容易出现大问题的。

方案1和方案2是相对能接受的,但是也会有一定概率存在并发写入导致数据不一致的问题。

此时我们可以通过给缓存设置过期时间,使得数据保证最终一致性。

3.2 读少写多的场景

这种场景下,redis的作用是为了减轻数据库写入压力,对数据的一致性要求较高。

  • 读少,所以并发读取和写入的情况比较少
  • 写多,所以并发写入的情况比较多

由于并发写入情况比较多,此时方案3和4比较好,根据上文的分析,方案4比方案3更好。

你可能感兴趣的:(数据库,缓存)