如何保证数据库和缓存双写一致???

缓存

我们先说一下我们会使用到缓存,也就是为什么要使用缓存:
1.为了高性能,假如说我们需要需要复杂的操作才能从数据库查出来结果,而且这个结果在后期不会怎么发生变化,主要是读请求那么直接将数据放在缓存中,再有读请求进来,直接从缓存中拿就可以了。
2.为了高并发,在系统请求高峰期时,我们可以将大量数据放在缓存中,这样就可以减少对MySql的访问,我们都知道,缓存是直接基于内存的,内存是天然支持高并发的,单机并发量就可以达到几万甚至几十万。

问题

如果说我们用了缓存,肯定会出现双写的问题,双写肯定会带来数据的不一致性。

解决问题:

1.将读和写串行到一个内存队列中,这样就不会出现数据不一致的情况了,但是吞吐量就会下降,需要更多的机器去支持线上的请求,实际生产环境大多数情况下不一定有多余的机器来支持。所以我们就来看一下最经典的方案。
2.缓存+数据库读写

  • 读的时候从缓存中读取数据,缓存有返回,如果缓存没有数据的话就去从数据库读,读出的数据先放到缓存,然后再返回响应
  • 我们需要更新数据的时候,先更新数据库,再删掉缓存

这样的话会出现什么问题呢?假如说我们在删除缓存的时候删除失败了,但是数据库已经更新为了新的数据,缓存中还是就数据,这时候有请求过来读取了缓存中的数据,当然读的是以前的旧数据。
解决:我们先删去缓存,然后再更新数据库,及时更新数据库失败,这个时候数据库还是以前的值,当有请求进来的时候,读取缓存没有,读数据库读到的是以前的数据,虽然读到的是旧的数据,但是不会出现数据不一致。
难道这样做就万无一失了吗???
假如说我们在数据库更新数据的过程中,这个时候还没有更新完又有新的请求进来了,读的是以前的旧数据,响应请求后数据库更新操作完成了。巧不巧????
这可怎么办?
解决:我们在更新数据的时候,根据数据的唯一标识(例如主键等),将操作路由之后,发送到一个 jvm 内部队列中。读取数据的时候,如果发现数据不在缓存中,那么将重新读取数据+更新缓存的操作,根据唯一标识路由之后,发送到同一个 jvm 内部队列中。一个队列对应一个工作线程,每个工作线程串行拿到对应的操作,然后一条一条的执行。这样的话,一个数据变更的操作,先删除缓存,然后再去更新数据库,但是还没完成更新。此时如果一个读请求过来,没有读到缓存,那么可以先将缓存更新的请求发送到队列中,此时会在队列中积压,然后同步等待缓存更新完成。
我们可以做个优化:在一个队列中,多个更新缓存请求串在一起是没意义的,我们可以做个过滤,如果说队列中已经有一个更新缓存的请求了,就不必再放个更新请求操作进去了,直接等待前面的更新操作请求完成即可。
等到那个队列对应的工作线程完成了上一个操作的数据库的修改之后,才会去执行下一个操作,也就是缓存更新的操作,此时会从数据库中读取最新的值,然后写入缓存中。如果请求还在等待时间范围内,不断轮询发现可以取到值了,那么就直接返回;如果请求等待的时间超过一定时长,那么这一次直接从数据库中读取当前的旧值。
警告⚠️⚠️⚠️
在高并发的场景下,我们使用此解决方法需要注意一下几个问题:

  • 读请求超时阻塞

出现原因:数据更新很频繁,导致队列中积压了大量更新操作在里面,然后读请求会发生大量的超时,最后导致大量的请求直接走数据库
解决:这个就要根据实际业务系统的运行情况,去做一些压力测试,和模拟线上环境,观察在系统请求高峰期的时候,内存队列会挤压多少更新操作,估算一下导致最后一个更新操作对应的读请求,需要多少时间多少时间,如果读请求在 200ms 返回,ok没问题。如果说超过了怎么解决呢?
如果一个内存队列中可能积压的更新操作特别多,只能加机器了,让每个机器上部署的服务实例处理更少的数据,每个内存队列中积压的更新操作就会越少。

  • 读请求并发量过高
  • 多服务实例部署的请求路由
  • 热点数据问题,导致请求倾斜
    好啦问题都该解决了,那么最后一个问题就是为什么在有更新操作的时候不去更新缓存,而是直接删除缓存,不去更新数据库呢???
    假如说我们只需要更改某条数据的某一个字段,我们更新了缓存,数据库中还是旧数据,有个请求进来需要查询其他表的数据来进行运算,才能计算出缓存最新的值的。所以直接删除缓存。

你可能感兴趣的:(高并发)