背景:
缓存一致性的产生原因是为了提高系统的吞吐量,通常会把一些即时性、数据强一致性要求不高的,或者访问量大且更新频率不高(读多写少) 的数据从数据库中读取并放到缓存中,提高系统的IO速度,从而提高吞吐量。
这样就会产生一些问题:
- 缓存穿透。
- 缓存雪崩。
- 缓存击穿。
- 缓存一致性问题。
缓存穿透
只查询一个一定不存在的数据,由于缓存是不命中的,将会去查询数据库,但是数据库也没有此记录,返回null值,并且我们没有将这次查询的null值存入缓存中,这将导致这个不存在的数据每次请求都要去存储层去查询,失去了缓存的意义。
风险:如果有人恶意利用这个不存在的数据穿过缓存进行大并发地攻击脆弱的数据库,那将可能导致数据库崩溃。
解决方法:
null结果缓存,就算数据库查出的是null数据,也把他缓存进缓存中,下次如果再进行查询,就直接走缓存了。可以给缓存加个相对短暂的过期时间。
缓存雪崩
缓存雪崩指的在我们给缓存的key设置过期时间时,采用了相同的过期时间,导致缓存在某一时刻大量失效,请求全部转发到数据库,数据库可能会因为压力过大导致崩溃。
解决方法:
在原有时间基础上加上一个随机值,把缓存过期时间进行一定时间内的分布,这样的缓存的失效不会都在同一时间,就避免了同一时间缓存大量失效。
缓存击穿
对于一些设置了过期时间的缓存key,如果这些key可能在某个时间点被超高并发的访问,是一种非常热点的数据,如果这些高并发的访问刚好在缓存过期的时候进行访问,那么请求压力将会都落到数据库上,可能导致数据库崩溃,这个就是缓存击穿。
解决方法:
加锁,大量并发只让一个请求去查数据库,然后加入到缓存中,其他并发请求在外面等待,一旦锁释放了,就代表数据已经被那个请求线程加入到缓存中了,其他请求就可以直接去查缓存了。
可以使用分布式锁。确保整个服务集群只有一个请求落到数据库。
也可以使用本地锁,使用本地锁只能锁住当前进程的线程,如果对于应用集群的其他节点,则是锁不住,也就是只能锁住当前进程。但是有时也是可以接受的,比如就算你有100个集群节点,那最多也就只有100个请求落入了数据库。
缓存一致性问题
缓存一致性问题指的是缓存中的数据与数据库中的数据不一致时的情况,比如缓存对数据库中的数据进行了缓存,然后数据库中该数据被修改了,但是没有及时得把缓存中相应数据进行更新。就出现了一致性问题。
解决方法:
双写模式
说白了就是在数据库该数据更新时同时更新缓存。
请求—》修改数据库该数据—》更新缓存。
存在的问题:
-
数据的暂时不一致性,因为在修改数据库后到更新缓存完成这段时间内对该数据的请求读取的是未更新的数据,但是在短暂的不一致后会达到最终一致性。
-
会出现脏数据的请求:
先进行写数据库1操作,然后请求1因为某些原因卡顿了,然后进行写数据库2操作,并且进行写缓存2操作,最后进行写缓存1操作,导致缓存的数据实际上是写数据库1时的数据,但是实际上数据库中的数据时写数据库2中的数据。 可以加锁来保证不出现脏数据。
这是暂时性的脏数据问题,但是在数据稳定,缓存过期以后,又能获取到最新的数据了。
失效模式
说白了就是数据更新时直接让对应的缓存失效(删除缓存),等到下次获取该数据时重新缓存。
问题:
-
也会存在相应的缓存短暂不一致,在修改数据之后,删除缓存之前的获取数据的请求读到的还是之前的数据。如果非要强一致,可以加一把分布式锁,直到删除缓存之后,获取缓存的请求才取消阻塞。上面的双写模式也类似。
-
也会出现脏数据的请求。
请求1、2、3, 1、2是写请求,3是读请求,首先1更新完数据再删除了缓存。然后到3请求读取数据,然后因为缓存被1请求删了,那么3请求就到数据库查询到了数据,然后因为某些原因出现卡顿,这时2号写请求更新了数据库数据并删除了缓存,再然后读请求三更新了缓存。但是他更新的却是查到的请求1修改的数据,这就导致了缓存不一致。也可以加锁来保证不出现脏数据。
尽管出现了脏数据,但是却能够始终保持最终一致性。这是暂时性的脏数据问题,在数据稳定,缓存过期以后,又能获取到最新的数据了。
总结
无论是双写模式还是失效模式,都会出现缓存不一致的情况,那么下面就是给出的一些建议。
- 如果是用户维度的数据(比如订单数据、用户数据),这种并发的几率非常小,比如用户修改自己的名片,那只是修改属于自己的数据,不会出现大并发的问题,总不会该用户并发的修改自己的名片吧。缓存数据加上过期时间,并配合上双写或者失效模式,就能保证缓存的一致性了。
- 如果是菜单或者商品等基础数据,会出现大并发读的,但是不出现大并发写的,可以使用双写模式,这样脏数据出现的几率也比较低,如果使用失效模式,出现脏数据的几率就会高一些,最保险是使用阿里的开源中间件canal,类似使用数据库主从同步的修改日志,把数据同步到缓存中。
- 缓存数据加过期时间也足够解决大部分业务对于缓存的要求。
- 通过加锁进行并发读写,读锁时可以并发读,写锁时按顺序排好队,所以可以用读写锁。(业务如果不关心脏数据或者允许短暂脏数据,可以忽略)。
- 我们能够放入缓存中的数据本就不应该是实时性,一致性要求超高的,所以缓存数据的时候加上过期时间,保证缓存数据在允许不一致的时间范围内更新即可。
- 我们不应该过度设计,增加系统的复杂性。
- 遇到实时性,一致性要求超高的数据就应该直接查数据库,即使慢点。