缓存是什么
- 通常我们会使用更快的介质(比如内存)作为缓存,来解决较慢介质(比如磁盘)读取数据慢的问题,缓存是用来解决性能问题的一种结构模式,本质就是空间换时间。
- 缓存系统一般设计简单,功能相对单一,所以诸如Redis这种缓存系统的整体吞吐量,能达到关系型数据库的几倍甚至几十倍,因此缓存特别适用于互联网应用的高并发场景。
使用缓存需要注意什么
- 从客户端的角度来说,缓存数据的特点一定是有原始数据来源的,且允许丢失。当数据丢失后,我们需要从原始数据重新加载数据,不能认为缓存系统是绝对可靠的,更不能认为缓存系统不会删除没有过期的数据。
- 从 Redis 服务端的角度来说,缓存系统可以保存的数据量一定是小于原始数据的,那么我们就应该限制 Redis 对内存的使用量,并且明确以怎么样的策略来维护数据,常用的算法有 LRU、TTL 和 LFU 三种。
缓存同步策略
- 先更新数据库,再更新缓存
如果更新数据库成功,但缓存更新失败,此时数据库中是最新值,但缓存中是旧值,后续的读请求会直接命中缓存,得到的是旧值,对业务产生影响。 - 先更新缓存,再更新数据库
如果更新缓存成功,但数据库更新失败,此时缓存中是最新值,数据库中是旧值,后续读请求会直接命中缓存,但得到的是最新值,短期对业务影响不大。但是,一旦缓存过期或者满容后被淘汰,读请求就会从数据库中重新加载旧值到缓存中,之后的读请求会从缓存中得到旧值,对业务产生影响。 - 先删除缓存,再更新数据库
如果线程A删除缓存成功,还没来得及更新数据库(比如网络延迟),有另一个线程B读取缓存,发现缓存不存在,就回去数据库读取数据,这时读到的值是还没更新的值。然后线程A更新完数据库之后就会出现线程B读到的值和数据库的值不一致的情况。 - 先更新数据库,再删除缓存
如果线程 A 更新了数据库中的值,但还没来得及删除缓存值,线程 B 就开始读取数据了,那么此时,线程 B 查询缓存时,发现缓存命中,就会直接从缓存中读取旧值。不过,在这种情况下,如果其他线程并发读缓存的请求不多,那么,就不会有很多请求读取到旧值。而且,线程 A 一般也会很快删除缓存值,这样一来,其他线程再次读取时,就会发生缓存缺失,进而从数据库中读取最新值。
综上,先更新数据库再删除缓存是较好的一种策略。
如何解决数据不一致问题
- 缓存设置过期时间
给缓存设置一个合理的过期时间,这样能保证在一定时间内,缓存可以和数据库的数据保持一致。 - 重试机制
把要删除的缓存值或者是要更新的数据库值暂存到消息队列中,当应用没有能够成功地删除缓存值或者是更新数据库值时,可以从消息队列中重新读取这些值,然后再次进行删除或更新。
如果能够成功地删除或更新,我们就要把这些值从消息队列中去除,以免重复操作,此时,我们也可以保证数据库和缓存的数据一致了。否则的话,我们还需要再次进行重试。如果重试超过的一定次数,还是没有成功,我们就需要向业务层发送报错信息了。 - 分布式锁
高并发的情况下,可以考虑加入分布式锁,保证同一时间只有一个线程可以对同一条数据进行操作。 - 延时双删
线程A先删除缓存,然后线程 A 更新完数据库值之后,我们可以让它先 sleep 一小段时间,再进行一次缓存删除操作。