redis缓存更新策略
- 先删除缓存,后修改数据库
- 先修改数据库,后删除缓存
- 延迟双删
- 内存队列
- 第三方队列
先删除缓存,后修改数据库
这个方案显然是有问题的,不推荐使用。
两个并发的读写操作:
- 一个写的操作先进来,把缓存删除了;
- 在写操作还没有更新数据库的时候,一个读的请求又进来了,发现没有命中缓存,就去数据库把老数据取出来了;
- 写操作更新了数据库;
- 读操作把老数据放在了缓存中。
这样,数据库中的数据和缓存中的数据就不一致了
这个方案显然不行,在此场景下能保持数据一致?
让我们设想下这样的场景:一个写的请求进来,删除缓存,这个时候,Redis服务器突然出问题了,或者网络突然出问题了,导致删除缓存失败,抛出了一个异常,导致程序没有继续执行修改数据库的操作。从数据库、缓存一致性的角度来说,这里很好的保证了数据库、缓存的一致性,两者保存的数据是一样的,尽管保存的都是老数据。
先修改数据库,后删除缓存
(推荐使用)
在没有缓存的情况下,两个并发的读写操作:
- 读操作先进来,发现没有缓存,去数据库中读数据,这个时候因为某种原因卡了,没有及时把数据放入缓存;
- 写的操作进来了,修改了数据库,删除了缓存;
- 读操作恢复,把老数据写进了缓存。
这样就造成了数据库、缓存不一致,不过,这个概率出现的非常低,因为这需要在没有缓存的情况下,有读写的并发操作,在一般情况下,写数据库的操作要比读数据库操作慢得多,在这种情况下,还要保证读操作写缓存晚于写操作删除缓存才会出现这个问题,所以这个问题应该可以忽略不计。
说了这么多,并没有看到先修改数据库,后删除缓存的致命问题啊,别急,让我们继续设想这样的场景:一个写的操作进来,修改了数据库,但是删除缓存的时候 ,由于Redis服务器出现问题了,或者网络出现问题了,导致删除缓存失败,这样数据库保存的是新数据,但是缓存里面的数据还是老数据,妥妥的数据库、缓存不一致啊。
延迟双删
可以看到修改数据库,后删除缓存有两个问题,虽然两个问题都是低概率的,所以第三种方案出现了:延迟双删。
延迟双删就是先删除缓存,后修改数据库,最后延迟一定时间,再次删除缓存。
这么做就可以在一定程度上缓解上述两个问题,第一次删除缓存相当于检测下缓存服务是否可用,网络是否有问题,第二次延迟一定时间,再次删除缓存,是因为要保证读的请求在写的请求之前完成。
但是这么做,还是有一定问题,比如第一次删除缓存是成功的,第二次删除缓存才失败,又该怎么办?
内存队列
上面三种方式,都有一定的问题:
- 修改数据库、删除缓存这两个操作耦合在了一起,没有很好的做到单一职责;
- 如果写操作比较频繁,可能会对Redis造成一定的压力;
- 如果删除缓存失败,该怎么办?
为了解决上面三个问题,第四种方式出现了:内存队列删除缓存:写操作只是修改数据库,然后把数据的Id放在内存队列里面,后台会有一个线程消费内存队列里面的数据,删除缓存,如果缓存删除失败,可以重试多次。
这样,就把修改数据库和删除缓存两个操作解耦了,如果删除缓存失败,也可以多次尝试。由于后台有一个线程去消费内存队列去删除缓存,不是直接删除缓存,所以修改数据库和删除缓存之间产生了一定的延迟,这延迟应该可以保证读操作已经执行完毕了。
但是这么做也有不好的地方:
- 程序复杂度成倍上升,需要维护线程、队列以及消费者;
- 如果写操作非常频繁,队列的数据比较多,可能消费会比较慢,修改数据库后,间隔了一定的时间,缓存才被删除。
第三方队列
如RabbitMQ,Kafka
方案一:
流程如下所示:
(1)更新数据库数据;
(2)缓存因为种种问题删除失败
(3)将需要删除的key发送至消息队列
(4)自己消费消息,获得需要删除的key
(5)继续重试删除操作,直到成功该方案有一个缺点,对业务线代码造成大量的侵入。于是有了方案二,在方案二中,启动一个订阅程序去订阅数据库的binlog,获得需要操作的数据。在应用程序中,另起一段程序,获得这个订阅程序传来的信息,进行删除缓存操作。
流程如下图所示:
(1)更新数据库数据
(2)数据库会将操作信息写入binlog日志当中
(3)订阅程序提取出所需要的数据以及key
(4)另起一段非业务代码,获得该信息
(5)尝试删除缓存操作,发现删除失败
(6)将这些信息发送至消息队列
(7)重新从消息队列中获得该数据,重试操作。
备注说明:上述的订阅binlog程序在mysql中有现成的中间件叫canal,可以完成订阅binlog日志的功能。另外,重试机制,采用的是消息队列的方式。如果对一致性要求不是很高,直接在程序中另起一个线程,每隔一段时间去重试即可。