乐趣区

关于java:不懂缓存一致性易把代码写成Bug

哈喽哈喽大家猴,我是把代码写成 bug 的大头菜。公众号:大头菜技术(bigheadit)。原创不易,但欢送转载。

本文次要分享一下对于缓存一致性问题和其解决方案。上面是本文的次要目录,大家能够挑着看。

目录
  1. 什么是缓存一致性
  2. 为什么要保障缓存一致性
  3. 如何保障缓存一致性
  4. 如何做到强一致性
  5. 总结

01 什么是缓存一致性

就是缓存和数据库的数据不统一导致的问题,缓存一致性分为强一致性和最终一致性。

  • 强一致性,这个比拟损耗性能,比较复杂,退出之后,可能会比没加缓存更慢。
  • 最终一致性,是容许缓存数据和数据库数据一段时间内不统一,但数据最终会保持一致。这个性能较高。

02 为什么要保障缓存一致性

因为业务中存在一些写操作导致的,是要先写缓存,还是先写数据库。二者程序的不同会导致不同的问题。

单纯的读操作,是不会导致缓存一致性问题的,因为读是幂等的哈。读无数次都是不会变的,因而就不存在读操作引起缓存一致性问题。

因而导致缓存不统一的就是写操作了。写操作是导致缓存不统一的起因。但这不是要保障缓存一致性的理由,
归根结底都是业务须要,如果业务需要容许缓存和数据库的不统一,那就不须要保障缓存一致性了。

03 如何保障缓存一致性(解决方案)

置信很多人都晓得经典计划:cache aside pattern。

首先明确的是,读不会产生缓存一致性问题。是写操作,才会产生缓存一致性问题。

第一点,生效:申请过去时,先拜访缓存,缓存不存在,再去拜访数据库,更新缓存。

第二点,读:申请过去时,缓存中有数据,间接返回数据。

第三点,写:先更新数据库,后删除缓存。

要害在第三点,前提,数据库必定是更新的。剩下的问题就是:是要更新缓存?还是要删除缓存?是先对数据库操作?还是先对缓存操作?

俩俩组合有 4 种可能性:

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

1. 先更新缓存,后更新数据库

首先咱们要明确,更新数据库或者更新缓存,都面临着更新失败的危险。但在互联网高并发的环境中和依据墨菲定律,这个事是肯定会产生的。

  1. 先更新缓存,胜利了
  2. 后更新数据库,失败了,当然你会说重试,好,那我就重试 N 次,但如果数据库彻底挂了,复原不了了,重试也没用

导致问题:数据失落,数据库外面的数据还是老数据

2. 先更新数据库,后更新缓存

假如有两个申请, A 申请是更新,B 申请是更新,A 先 B 后,但二者距离很短

  1. 线程 A 更新了数据库
  2. 线程 B 更新了数据库
  3. 线程 B 更新了缓存
  4. 线程 A 更新了缓存

导致问题:缓存中是旧数据,数据库中是新数据,这就不统一了。还有就是更新后的缓存,真的会被再读取吗?如果缓存数据不再被读取,那就白白操作了一次缓存更新操作。并且还占用内存空间。

依据这个例子,能够看出,更新缓存是不可取的,那就间接删除缓存吧。接着看

3. 先删除缓存,后更新数据库

假如有两个申请, A 申请是更新,B 申请是读,可能呈现的问题

  1. 线程 A 删除缓存
  2. 线程 B 查问不到缓存,间接去数据库查旧值
  3. 线程 A 将新值写入数据库
  4. 线程 B 更新缓存

导致问题:缓存中的是旧值,数据库中的是新值,二者不统一。进一步,如果数据库存在读写拆散,那么缓存和数据库数据不统一的状况进一步加剧。

  1. 线程 A 删除缓存
  2. 线程 A 将新值写入主数据库,但未同步数据到从数据库
  3. 线程 B 查问不到缓存,间接去从数据库查,查到旧值
  4. 线程 B 更新缓存
  5. 新数据同步到从数据库

导致问题:缓存是旧值,数据库是新值,二者数据不统一

4. 先更新数据库,后删除缓存

假如有两个申请, A 申请是读,B 申请是更新,可能会呈现的问题

  1. 缓存刚好生效
  2. 线程 A 查数据库,失去旧值
  3. 线程 B 更新数据库
  4. 线程 B 删除缓存
  5. 线程 A 更新缓存

导致问题:缓存是旧值,数据库是新值,二者不统一。但这种状况的可能性相对来说比拟小,因为须要缓存刚好生效,并且此时有一个线程去读,且刚好又有一个写的线程。而且写的线程实践上是比读的线程慢的,因为写的线程,须要加锁。而查问不必加锁,不包含简单的查问。

在数据库读写拆散的状况下,这种状况会更加显著:

  1. 线程 B 更新主库
  2. 线程 B 删除缓存
  3. 线程 A 查问缓存,没有命中,查问从库失去旧值
  4. 数据同步到从库
  5. 线程 A 更新缓存

导致问题:缓存数据和数据库数据不统一

如果思考更新数据库或者更新缓存失败的话,那么更新数据库失败的话,其实数据库和缓存都是旧数据,因而不存在数据不统一的状况。

如果更新缓存失败,那么有过期工夫来保障最终一致性。如果非要较真的话,能够退出重试机制。

重试机制能够用线程池,也能够用 MQ。MQ 更加牢靠。能够间接订阅 MySQL 的 binlog,来触发缓存的删除。当然,其实 MQ 也会挂。然而 MQ 和缓存都一起挂的几率,应该很小吧。

综上所述四种状况

尽管每种计划都有各自的问题,但呈现几率较小的是 先更新数据库,后删除缓存 计划。为什么先更新数据库?因为数据库的长久化能力比缓存好。上述四种状况,还可能呈现缓存并发,缓存穿透,缓存雪崩的问题。这些问题,这里就不探讨了。感兴趣的话,本人去看我的相干文章。

04 如何做到强一致性

计划一:分布式事务

能够用分布式事务,分布式事务,具体的实现有 2PC、3PC、音讯队列等。如果要采纳这个计划,架构设计中要引入很多容错、回退、兜底的措施。业务代码就减少复杂性了。还有人说用分布式一致性算法 paxos 和 raft,这就更简单了。

计划二:分布式读写锁

首先,咱们回到 先更新数据库,后删除缓存,要明确什么时候会呈现脏数据?

呈现脏数据:更新数据库后,删除缓存之前。这时候二者数据是不统一的。

如果实现更新数据库时,所有读申请都被阻塞。这就解决了数据不统一的问题,这其实是串行化思路。但结果,当然是性能下滑。

总结

其实抉择数据的强一致性和数据的最终一致性。得看具体需要,我如同说了一句废话。然而放弃强一致性,意味着咱们零碎的性能失去肯定水平的晋升。相同,如果咱们谋求强一致性,那就会巨简单,而且可能得失相当,可能性能比不加缓存时还低。缓存这货色,想要用得好,就须要好好推敲,比方过期工夫的设置,长久化,故障复原,空间和工夫的均衡,一致性的抉择,这些都要好好斟酌,没有最好的计划,只有适合的计划。

退出移动版