数据库和缓存一致性问题
目录
一、前言
当我们要做一些并发量较大、查询较多的场景项目时,就需要加一层缓存(以下缓存使用redis),避免请求直接打到数据库,防止数据库扛不住宕机。
一般我们可能会使用写数据库,读缓存的策略,那么就需要保证数据库和缓存一致的问题,以下对是从浅入深的一些方案的思考以及会出现的一些问题。
二、使用缓存需要注意的问题
使用缓存需要注意以下几个方面:
- 缓存利用率:缓存中的数据有多少被命中了,若缓存利用率较低,那么就会占用资源
- 数据一致性:使用缓存之后,一定要保证数据库和缓存的数据是一致的
方案一:数据全部放入缓存,不设置过期时间
写请求操作数据库,读请求访问缓存,定时任务将数据从数据库刷新到redis。
1. 方案具体操作
- 将数据全部放入缓存中
- 写请求只更新数据库,不更新缓存
- 启动一个定时任务,定时把数据库的数据,更新到缓存中
2. 方案优缺点
优点
- 缓存命中率高:所有的读请求都可以落在缓存中,不用访问数据库,大大提高效率
缺点
- 缓存利用率低:热点数据key总是一小部分,将所有的数据全部缓存在redis中,不经常访问的数据一直存在redis中,造成资源浪费
- 数据不一致:使用定时器刷新缓存,缓存数据和数据库数据是否存在数据一直问题取决于定时任务的执行频率
3. 适用场景
这种方案适用于小项目中,并且对数据一致性要求不严格的业务场景中。
方案二:部分数据放入缓存,设置过期时间
写操作访问数据库,读请求访问缓存。
对上一个方案的缓存利用率问题进行优化,那么就可以将部分数据放入缓存,当访问的数据不在缓存中时,访问数据库,并且将数据放入缓存中设置过期时间
1. 方案具体操作
- 项目启动将一部分数据缓存到redis
- 写请求访问数据库,读请求访问redis,redis中没有数据则访问数据库,并将数据放入redis,设置过期时间
2. 方案优缺点
优点
- 缓存利用率高:存放在redis中的数据基本都是热点数据,命中率高
缺点
- 缓存一致性问题:使用这种方式就需要注意若缓存中无数据,如何从数据库中向缓存中同步数据
3. 适用场景
这种方案适用于一些较大业务场景
三、解决方案二中缓存一致性问题
1. 先更新数据库,后更新缓存(不采用)
先不考虑并发情况,只考虑第二步操作失败的情况
若更新数据库成功,更新缓存失败,导致数据库中是新值,缓存中是旧值,只有当缓存过期之后才会从数据中更新数据。
不考虑第二步失败的情况,只考虑并发情况
如上图,线程A先更新数据库x=2,然后线程B更新数据库x=3,并且更新缓存x=3,接下来线程A更新缓存x=2,造成数据库值x=3,缓存值x=2的情况,造成数据库和缓存数据不一致。
2. 先更新缓存,后更新数据库(不采用)
先不考虑并发情况,只考虑第二步操作失败的情况
当更新缓存成功,更新数据库失败,虽然缓存中是新值,但数据库中是旧值,若缓存失效,则从数据库中更新的数据是旧值,会导致用户数据前后不一致。
不考虑第二步失败的情况,只考虑并发情况
如上图,线程A先更新缓存x=2,然后线程B更新缓存x=3,并且更新数据库x=3,线程A再更新数据库x=2,此时数据库值x=2,缓存值x=3,导致数据库和缓存数据不一致。
3. 先删除缓存,后更新数据库(不采用)
先不考虑第二步失败的情况,只考虑并发情况
- 线程A删除缓存
- 线程B读取数据,缓存中没有
- 线程B访问数据库读取数据 x=1
- 线程A修改数据库 x=2,并将数据写入缓存 x=2
- 线程B写入缓存 x=1
4. 先更新数据库,后删除缓存(采用)
先不考虑第二步失败的情况,只考虑并发情况
- 线程B访问缓存中数据,此时数据刚好过期
- 线程B访问数据库,读取数据 x=1
- 线程A修改数据库数据 x=2
- 线程A删除缓存数据
- 线程B写入缓存数据 x=1
虽然这种情况是有可能发生的,但是发生的概率极低,有以下几点原因:
- 修改数据时,此数据刚好过期
- 更新数据库+删除缓存所用时间 < 读取数据库数据+写入缓存所用时间,这种情况是很难发生的,当写入数据时,会加锁,所以写数据通常比读数据库花费的时间是要长的
如何保证两步操作都执行成功
若第二步操作失败,则可以进行重新操作,但并不是无脑一直重新操作,而是应该异步重试,其实就是将重试操作放入消息队列,让其进行异步消费,当消费失败,就可以重入队列,重新消费,直到消费成功。
四、主从库延时和延时双删问题
1.先更新数据库,后更新缓存在主从数据库下还可以实现吗?
上面提到先更新数据库,后删除缓存在单个数据库中是没有什么大问题的,但如果在主从数据库架构下会不会出现什么问题?
如上图,若有主从库,则可能在主库删除缓存之后,读请求进来,发现缓存中没有,然后去从库读取旧数据,读完之后主从数据才同步,然后线程B将旧数据写入缓存,造成数据库和缓存数据不一致。
2. 解决方案
无论是主从库的同步延时或是先删除缓存,后更新数据库,都会出现一个问题,就是可能会有一个线程在修改数据之前读取到旧数据,然后在删除缓存之后将旧数据写入到缓存中,那么如何解决?
肯定也是删缓存,但不是立即删除,而是延迟删除。
2.1 解决先删除缓存,后更新的问题
上面说了,需要删除缓存,并且也需要延迟删除缓存,就是”延迟双删“,先删除缓存,然后再延时删除缓存,那么需要延迟多长时间呢?
延迟时间要大于线程B读取数据库数据+写入缓存的时间总和,这样才能在线程B写入错误数据之后,然后再次删除。
2.1 解决主从库同步数据的问题
同样,也需要延时删除,那延时多长时间呢?
延时时间要大于主从复制的时间,因为只有从库数据一致了,缓存中没有数据时,访问从库数据才会和线程A修改后的数据一致。