美文网首页
谈谈数据库,缓存一致性

谈谈数据库,缓存一致性

作者: CoderBear | 来源:发表于2019-12-28 17:32 被阅读0次

    几年前,我在看博客的时候,看到有一篇博客的标题就是关于数据库,缓存一致性的,不以为然,直接跳过去了,心想,这么简单的问题还讨论个鬼啊。这种想法持续了很久,直到某天,我看到越来越多的人都在讨论数据库,缓存一致性的问题,才好好的看了下博客,才发现原来数据库,缓存一致性真不是一个简单的问题。今天我也来谈谈数据库,缓存一致性问题。

    科普

    考虑到有一些小伙伴可能技术不是那么好,可能没有接触过缓存,所以这里还是花上一分钟的时间,来介绍下什么是缓存,为什么要有缓存,以及数据库和缓存是如何搭配使用的。

    读取数据库是比较耗时的操作,如果每次都需要去数据库读取数据,会对数据库造成一定的压力,程序性能也会比较低下,所以需要引入缓存。

    缓存是提升程序性能的最重要、最有效、也是最简单的手段之一。

    引入缓存后,读操作会先去缓存中看下,如果没有命中缓存,才去读取数据库,然后把读取出来的数据再放到缓存中去,这样下一次读操作就可以命中缓存了,如果命中缓存,就可以直接把数据返回出去了。

    image.png

    写操作,除了修改数据库,还需要删除缓存,因为不删除缓存,读的操作读到的永远都是缓存中的旧数据。

    先删除缓存,后修改数据库

    这个方案显然是有问题的。

    两个并发的读写操作:

    1. 一个写的操作先进来,把缓存删除了;
    2. 在写操作还没有更新数据库的时候,一个读的请求又进来了,发现没有命中缓存,就去数据库把老数据取出来了;
    3. 写操作更新了数据库;
    4. 读操作把老数据放在了缓存中。

    这样,数据库中的数据和缓存中的数据就不一致了,为了更好的让大家理解这个过程,献上一张丑到无法自拔的图:


    image.png

    这个方案显然不行,但是这个方案真的一无是处吗?

    非也,让我们设想下这样的场景:一个写的请求进来,删除缓存,这个时候,Redis服务器突然出问题了,或者网络突然出问题了,导致删除缓存失败,抛出了一个异常,导致程序没有继续执行修改数据库的操作。从数据库、缓存一致性的角度来说,这里很好的保证了数据库、缓存的一致性,两者保存的数据是一样的,尽管保存的都是老数据。

    先修改数据库,后删除缓存

    相信绝大多数小伙伴都是运用的这个方案, 先前我觉得数据库,缓存一致性没有什么好讨论的,太简单了,就是因为我觉得这个方案是如此完美,但是后面我才慢慢发现这个方案也有一定的问题。

    看到第一种方案存在的问题,大家也一定想到了这个方案也有同样的问题。

    在没有缓存的情况下,两个并发的读写操作:

    1. 读操作先进来,发现没有缓存,去数据库中读数据,这个时候因为某种原因卡了,没有及时把数据放入缓存;
    2. 写的操作进来了,修改了数据库,删除了缓存;
    3. 读操作恢复,把老数据写进了缓存。
    image.png

    这样就造成了数据库、缓存不一致,不过,这个概率出现的非常低,因为这需要在没有缓存的情况下,有读写的并发操作,在一般情况下,写数据库的操作要比读数据库操作慢得多,在这种情况下,还要保证读操作写缓存晚于写操作删除缓存才会出现这个问题,所以这个问题应该可以忽略不计。

    说了这么多,并没有看到先修改数据库,后删除缓存的致命问题啊,别急,让我们继续设想这样的场景:一个写的操作进来,修改了数据库,但是删除缓存的时候 ,由于Redis服务器出现问题了,或者网络出现问题了,导致删除缓存失败,这样数据库保存的是新数据,但是缓存里面的数据还是老数据,妥妥的数据库、缓存不一致啊。

    延迟双删

    可以看到修改数据库,后删除缓存有两个问题,虽然两个问题都是低概率的,但是永远追求完美的程序员可不能允许有这样的事情发生,所以第三种方案出现了:延迟双删。

    延迟双删就是先删除缓存,后修改数据库,最后延迟一定时间,再次删除缓存。

    图片.png

    这么做就可以在一定程度上缓解上述两个问题,第一次删除缓存相当于检测下缓存服务是否可用,网络是否有问题,第二次延迟一定时间,再次删除缓存,是因为要保证读的请求在写的请求之前完成。

    但是这么做,还是有一定问题,比如第一次删除缓存是成功的,第二次删除缓存才失败,又该怎么办?

    内存队列

    上面三种方式,都有一定的问题:

    • 修改数据库、删除缓存这两个操作耦合在了一起,没有很好的做到单一职责;
    • 如果写操作比较频繁,可能会对Redis造成一定的压力;
    • 如果删除缓存失败,该怎么办?

    为了解决上面三个问题,第四种方式出现了:内存队列删除缓存:写操作只是修改数据库,然后把数据的Id放在内存队列里面,后台会有一个线程消费内存队列里面的数据,删除缓存,如果缓存删除失败,可以重试多次。

    image.png

    这样,就把修改数据库和删除缓存两个操作解耦了,如果删除缓存失败,也可以多次尝试。由于后台有一个线程去消费内存队列去删除缓存,不是直接删除缓存,所以修改数据库和删除缓存之间产生了一定的延迟,这延迟应该可以保证读操作已经执行完毕了。

    但是这么做也有不好的地方:

    • 程序复杂度成倍上升,需要维护线程、队列以及消费者;
    • 如果写操作非常频繁,队列的数据比较多,可能消费会比较慢,修改数据库后,间隔了一定的时间,缓存才被删除。

    但是这也是没有办法的事情,哪有十全十美的解决方案。

    第三方队列

    一般来说,系统分为前台系统和后台系统,前台系统主要是读操作,后台系统才有写操作。

    比如商品中心,前台是面向用户的,当用户打开商品详情页,会去缓存中拿数据,后台是面向业务人员的,业务人员可以在后台系统对商品信息进行修改。

    如果是具有一定规模的公司,前台系统和后台系统肯定不在同一个服务器上,而且是由不同的部门去负责的,所以内存队列是肯定用不了的,如果后台系统修改数据库后,直接删除缓存,一定会发生如下的故事。

    后台系统 小明:你们前台系统的产品详情缓存的key是什么格式的?发我下。
    前台系统 小花:Product:XXXXX。
    后台系统 小明:好的。

    过了几天,小花找到小明。

    前台系统 小花:不对啊。你们怎么没有把活动中的产品详情缓存给删掉啊?
    后台系统 小明:纳尼,我怎么知道你们是两个缓存啊,把活动中的产品详情缓存的key的格式发我下。
    前台系统 小花:Activity:Product:XXXX。
    后台系统 小明:好的。

    过了几天,订单系统的开发又找到小明。
    订单系统 小强:你们修改了产品详情后,还要把订单中的产品详情缓存给删除。
    后台系统 小明:。。。

    过了几天,广告系统的开发又找到小明。
    广告系统 小王:你们修改了产品详情后,还要把广告中的产品详情缓存给删除。

    后台系统 小明 卒,享年25。

    如果引用了第三方队列,如RabbitMQ,Kafka,小明就不会“卒”了,后台系统的小明修改了数据库后,不需要关心缓存的事情,只要把数据的Id丢到消息队列,前台系统、广告系统、订单系统的开发消费消息队列中的数据删除缓存。

    上面说的几种方案,都是比较常见的,也比较简单,当然不同的方案也可以搭配使用,但是没有“银弹”,没有完美的解决方案,就看你们的研发团队,你们的场景适合哪种解决方案了。

    今天的话题到这里就结束了。

    相关文章

      网友评论

          本文标题:谈谈数据库,缓存一致性

          本文链接:https://www.haomeiwen.com/subject/rsmynctx.html