一网打尽缓存问题

213 阅读5分钟

在应用开发中,凡是涉及到加快查询速度的问题,都可以考虑使用缓存。缓存在真实应用项目开发中非常重要。今天让我们一起来聊聊缓存。

1. 缓存雪崩

缓存雪崩指的是,大量的key在同一时间点失效,此时所有的请求直接打到数据上,导致数据压力剧增,甚至崩溃。

我们可以通过对key的过期时间设置一个随机值,不至于大家同时到期

2. 缓存击穿

首先缓存击穿是一个正常现象,它指的是某一个时间点,某个缓存到期了,由于在并发情况下,同一时刻请求会有多个请求都到数据库中查询数据。

本质上,我们其实只需要一个线程去数据库做查询就行,其它线程等这个线程更新完缓存后再去读取缓存即可。我们可以通过锁的方式来解决

3. 缓存穿透

缓存穿透的关键在于“透”这个字上,它指的是缓存中没有数据,数据库中也没有数据(数据库都被透掉了)。由于数据库中没有数据,所以即使某个线程到数据库查询后仍然没有数据,缓存中也一直没有数据;所以所有的请求都相当于直接打到数据库上了。

我们可以在数据库中没有查到数据的时候,也要到缓存中存一个空值,保证后续的请求可以到缓存中查询值。

4. 缓存一致性

缓存一致指的是数据库和缓存中的数据保持一致,它的情况相对复杂,我们分别讨论。

4.1 先删缓存再更新数据库

假设有两个线程A和B

--- 1. 线程A删掉缓存 ----
--- 2. 线程B来从缓存中读取数据(缓存中无数据) ----
--- 3. 线程B从数据库中读取数据 ----
--- 4. 线程A到数据库中更新数据(正确的数据) ----
--- 5. 线程B将此前读到的数据(脏数据)更新到缓存中 ----

缓存和数据库中的数据也不一致

我们可以发现,之所以出现上面的不一致是因为,在删掉缓存后到更新数据库中前这个空档期内,有线程读取到了数据库中的还未来更新的数据导致。

解决方案: 我们可以在,线程A更新数据库后再做一次删除缓存,这样删掉后,再次触发,其它线程到数据库中获取然后更新到缓存。另外,我们需要保证此前的线程B已经成功写入缓存的情况下,再做缓存删除,避免它覆盖正确的数据。所以线程A更新数据库后,需要sleep一点时间,然后删除缓存。

所以正确的操作如下:

--- 1. 线程A删掉缓存 ----
--- 2. 线程B来从缓存中读取数据(缓存中无数据) ----
--- 3. 线程B从数据库中读取数据 ----
--- 4. 线程A到数据库中更新数据(正确的数据) ----
--- 5. 线程B将此前读到的数据(脏数据)更新到缓存中 ----
--- 6. 线程A sleep一段时间,再此删除缓存 ----

4.2 先更新数据库再删除缓存

由于我们先更新数据库,在删除缓存;理论上,在数据库更新完后还未写入缓存中这个空档期内,其它线程都从缓存中获取数据,此时缓存中的数据都是不准确的。

但是由于这个间歇时间非常短,并发情况不是特别高的情况下,影响其实比较有限,这种方式也比较加单,因此推荐先更新数据库再删除缓存

需要注意的是,前面两种方式,都是依靠删除缓存来保证数据的一致性。

4.3 并发写

假设有两个线程同时去写数据库,然后去更新缓存。

--- 1. 线程A更新数据库 ----
--- 2. 线程B更新数据库 ----
--- 3. 线程B更新缓存 ----
--- 4. 线程A更新缓存 ----

由于线程A比线程B先更新数据库,但是却晚于线程B更新缓存,所以缓存中的数据是不一致的。

解决方案: 更新数据库的时候先加锁,只有在数据库更新成功、且缓存写入成功后之后,再释放锁

4.4 并发读写

假设有两个线程,一个去写一个去读(此时缓存中无数据)

--- 1. 线程A从缓存读取数据(缓存此时中无数据) ----
--- 2. 线程A从数据库中读取数据 ----
--- 3. 线程B更新数据库中的数据 ----
--- 4. 线程B更新缓存中的数据 ----
--- 5. 线程A将此前读到的数据(脏数据)更新到缓存中 ----

此时缓存和数据库中的数据也不一致

我们可以看出,这个问题还是由于两者在并发情况下,更新缓存的先后导致的。

解决方案: 在读自动更新缓存和写主动更新缓存的时候,我们提高写更新缓存的优先级,如果缓存中已经有数据,则不做更新,redis中采用setnx方式。**

5. 缓存一致的总结

前面我们讨论了4种情况,有些情况您看起来可能会感觉差不多,实际上他们是不同的;

对于前两种(先删除缓存还是先更新数据库),他们的核心点是,依靠删除缓存后,自动更新缓存机制保持缓存一致;对于后两种(并发写、并发读写)他们依靠的是主动去更新缓存