老规矩开局一张图:

认真写文章,用心做分享。 




redisTemplate获取有效期 redistemplate set存取数据_缓存


科普

这里还是花上一分钟的时间,来介绍下什么是缓存,为什么要有缓存,以及数据库和缓存是如何搭配使用的。

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

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

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


redisTemplate获取有效期 redistemplate set存取数据_数据_02


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

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

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

两个并发的读写操作:

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

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


redisTemplate获取有效期 redistemplate set存取数据_redistemplate怎么修改数据_03


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

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

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

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

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

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

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


redisTemplate获取有效期 redistemplate set存取数据_数据库_04


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

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

延迟双删

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

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


redisTemplate获取有效期 redistemplate set存取数据_数据库_05


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

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

内存队列

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

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

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


redisTemplate获取有效期 redistemplate set存取数据_缓存_06


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

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

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

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

第三方队列

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

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

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

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

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

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

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

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

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

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

最后:观众老爷们,点个赞呗!