业务中我们如何更新缓存?Redis

更新缓存的步骤特别简单,总共就两步:更新数据库和更新缓存。但就这么简单的两步,我们需要考虑好几个问题。

先更新数据库还是先更新缓存?更新缓存时先删除还是直接更新?

假设第一步成功了,第二步失败了怎么办?

假设 2 个线程同时更新同一个数据,A 线程先完成第一步,B 线程先完成第二步,此时该怎么办?

组合一:先更新缓存,再更新数据库(否)

对于这个组合,会遇到这种情况:

假设第 2 步数据库更新失败了,要求回滚缓存的更新,这时该怎么办呢?

我们知道 Redis 不支持事务回滚,除非我们采用手工回滚的方式,先保存原有数据,然后再将缓存更新回原来的数据,这种解决方案就有点尴尬了。

这里我简单举个例子,比如:

原来缓存中的值是 a,两个线程同时更新库存;

线程 A 将缓存中的值更新成 b,且保存了原来的值 a,然后更新数据库;

线程 B 将缓存中的值更新成 c,且保存了原来的值 b,然后更新数据库;

线程 A 更新数据库时失败了,它必须回滚了,那现在缓存中的值更新回什么呢?

要不这样吧,我们在 A 线程更新缓存与数据库的整个过程中,先把缓存及数据库都锁上,确保别人不能更新,这种方法可不可行呢?当然是可行的,但是别人能不能读呢?

假设 A 更新数据库失败回滚缓存时,线程 C 也来参一腿,它需要先读取缓存中的值,这时又返回什么值呢?

看到这个场景,你是不是有点儿印象?不错,这就是典型的事务隔离级别场景。我们只是使用一下缓存而已,你让我自己实现事务隔离级别,这个要求会不会有点高?我们还是考虑别的吧。

组合二:先删除缓存,再更新数据库(否)

使用这种方案,就算我们更新数据库失败了也不需要回滚缓存。这种做法虽然巧妙规避了失败回滚的问题,却引来了 2 个更大的问题。

假设 A 线程先删除缓存,再更新数据库。在 A 线程完成更新数据库之前,后执行的 B 线程反而超前完成了操作,读取 key 发现没数据后,将数据库中的旧值存放到了缓存中。A 线程在 B 线程都完成后再更新数据库,这样就会出现缓存(旧值)与数据库的值(新值)不一致的问题。

为了解决一致性问题,我们可以让 A 线程给 key 加锁,因为写操作特别耗时,这种处理方法会导致大量的读请求卡在锁中。

以上描述的是典型的高可用和一致性难以两全的问题,要再加上分区容错就是 CAP 了,这里我们就不展开讨论了,继续讨论另外两种组合吧。

组合三:先更新数据库,再更新缓存(否)

对于组合三,我们同样需要考虑 2 个问题。

假设第一步成功,第二步失败了怎么办?因为缓存不是主流程,数据库才是,所以我们不会因为更新缓存失败而回滚第一步对数据库的更新。此时,我们一般采取的做法是做重试机制,但重试机制如果存在延时还是会出现数据库与缓存不一致的情况,非常不好处理啊。

假设 2 个线程同时更新同一个数据,A 线程先完成了第一步,B 线程先完成了第二步怎么办?

假设 2 个线程同时更新同一个数据,A 线程先完成了第一步,B 线程先完成了第二步怎么办?这个你先好好想一下。我们接着推演整个过程:A 线程把值更新成 a,B 线程把值更新成 b,此时数据库中的最新值是 b,因为 A 线程先完成了第一步,后完成第二步,所以缓存中的最新值是 a,数据库与缓存的值还是不一致,还是不好处理啊。

因此,我不建议采用以上这个方案。

组合四:先更新数据库,再删除缓存(OK)

针对组合四,我们看看到底会存在哪些问题。

假设第一步成功了,第二步失败了怎么办?这种情况的出现概率与上个组合相比明显少不少,因为删除比更新容易多了。此时虽然它不完美,但出现一致性的问题概率少。

假设 2 个线程同时更新同一个数据,A 线程先完成第一步,B 线程先完成第二步怎么办?这块你也先好好想一下。我们接着推演整个过程:A 线程把值更新成 a,B 线程把值更新成 b,此时数据库中的最新值是 b,因为 A 线程先完成第一步,至于第二步谁先完成已经无所谓了,反正是直接删除缓存数据。

看到这,我们发现组合四完美地解决了以上难题,所以我建议更新缓存时,先更新数据库再删除缓存。

不过,这个解决方案也会引发另外 3个问题。

删除缓存数据后变相出现缓存击穿,此时该怎么办?

删除缓存失败如何重试?

删除缓存失败,重试成功前出现脏数据。这个需要与业务商量,毕竟这种情况还是少见,我们可以根据实际业务情况判断是否需要解决这个瑕疵。毕竟任何一个方案都不是完美的,但如果剩下 1% 的问题需要我们花好几倍的代价去解决,从技术上来讲得不偿失。

经验分享 程序员 微信小程序 职场和发展