一、数据一致性
数据一致性指的是缓存中的数据和数据库的数据在某一时刻是相同的,一般分为三种状态:
- 强一致性:数据库的更新操作和缓存的更新操作是原子性的,不管什么时候,数据库的数据和缓存的数据都是一致的,一般很难实现。
- 弱一致性:先更新数据库的数据,缓存中的数据更新是异步的,数据库更新后,缓存中的数据可能是新的也可能是旧的。
- 最终一致性:一般作为分布式系统的解决方案,是一种特殊的弱一致性,指的是在达到一定时间后,数据会达到一致性的状态。
1.常见的方案
- 先更新缓存,再更新数据库
- 先更新数据库,再更新缓存
- 先删除缓存,再更新数据库
- 先更新数据库,再删除缓存
对于新增的数据,一般不会直接写入数据库,因此此时数据库与缓存中的数据是一致性的。当我们首次查询时,再将数据放入缓存中。
1.1.先更新缓存,再更新数据库
- 更新失败问题:若缓存更新成功,数据库更新失败了,就会造成数据不一致的问题。
- 时间问题:更新缓存的时间较快。更新数据库较慢,因此会出现短暂的数据不一致的问题。
- 双写问题:若有两个线程A和B:A更新缓存-->B更新缓存-->B更新数据库-->A更新数据库。造成了数据不一致的问题。
1.2.先更新数据库,再更新缓存
- 更新失败问题:与上诉描述类似,当数据库更新成功,缓存更新失败,会出现数据不一致的问题。
- 双写问题:若有两个线程A和B:A更新数据库-->B更新数据库-->B更新缓存-->A更新缓存。但是这种情况比较少出现,因为缓存的修改是比较快的。A更新缓存的时间绝大多是都会在B更新缓存之前。
1.3.先删除缓存,再更新数据库
- 读写问题:若有两个线程A和B:A删除缓存-->B查询数据库-->B未查询到数据,更新旧数据到缓存-->A更新了数据库。造成了缓存与数据库数据不一致的问题。
解决方案:对于这种现象,一般可以使用延迟双删:先删除缓存-->更新数据库-->休眠几秒(或使用中间件)再次删除缓存,但这种方式是不可控的,降低了接口的性能,休眠时间也不好把控。
1.4.先更新数据库,再删除缓存
- 读写问题:若有两个线程A和B:A更新数据库-->B查询数据库-->B从缓存中拿到数据-->A删除缓存。在这种情况下,B拿到了旧的值,但是这种情况一般很少出现,因为删除的效率一般都比查询要快。
2.如何选择
一般情况下,我们都是选择删除缓存,因为删除缓存不会出现双写的问题,在查询的时候先去查询缓存再去查询数据库,若缓存中没有,数据库中有就放入缓存。
在这种情况下,我们要先更新数据库,再删除缓存,这种条件下只有在查询比删除快的情况下才会出现不一致的问题,然而这种情况是很难出现的。
因此,我们一般选择先更新数据库,再删除缓存。
二、缓存穿透
缓存穿透是指查询一个数据库不存在的数据,此时缓存中也不会存在,当有大量的请求去后端请求时,会使数据库的压力急剧增加,可能会使数据库宕机。
一般情况下,造成缓存穿透的原因主要有两个:
- 自身的业务代码或者数据库出现问题。
- 遭受恶意请求,爬虫获取很多空数据。
2.1.解决方案
2.1.1.缓存空对象
当有请求查询数据时,不管数据库中有没有信息,都将它放入数据库中,这样当下次再查询时,就不会命中数据库了。
但是,这样是最简单操作,但是也会存在以下两个问题:
- 会造成缓存数据库的资源浪费,保存了很多空的没有意义的数据,特别是当有恶意请求时,会使缓存的内存利用率急剧上升。
- 可能会造成一定的数据不一致问题:当我们缓存空对象时,设置了5分钟的过期时间,如果这5分钟内有这个数据进行新增,就会造成短暂的缓存与数据库的数据不一致的问题。
2.1.2.使用布隆过滤器
1970 年布隆提出了一种布隆过滤器的算法,用来判断一个元素是否在一个集合中。 这种算法由一个二进制数组和一个 Hash 算法组成。
本质上布隆过滤器是一种数据结构,比较巧妙的概率型数据结构(probabilistic data structure),特点是高效地插入和查询,可以用来告诉你 “某样东西一定不存在或者可能存在”。
相比于传统的 List、Set、Map 等数据结构,它更高效、占用空间更少,但是缺点是其返回的结果是概率性的,而不是确切的。
实际上,布隆过滤器广泛应用于网页黑名单系统、垃圾邮件过滤系统、爬虫网址判重系统等,Google 著名的分布式数据库 Bigtable 使用了布隆过滤器来查找不存在的行或列,以减少磁盘查找的IO次数,Google Chrome浏览器使用了布隆过滤器加速安全浏览服务。
布隆过滤器广泛用于防止缓存穿透(当黑客大量请求数据库不存在的数据,此时缓存中也不存在,就会直接打到数据库上,从而造成数据库崩溃),当保存一个数据的时候,首先计算出他的hash,再根据hash进行取模运算,计算出他的偏移量,将他放入bitmap中,下次有请求时首先从布隆过滤器中判断集合中是否有值。
三、缓存击穿
缓存击穿是指缓存中的一个热点数据突然过期了,此时还没有将它重新写入缓存中时,有大量的并发请求来查这个数据,造成数据库的压力瞬间增大,从而导致数据库宕机。
3.1.解决方案
3.1.1.使用互斥锁
使用redis的setnx或者redission的互斥锁来进行加锁,当缓存中的数据过期时,在对数据库进行查询操作时加锁,阻塞其他的进程访问数据库,当查到数据库的数据并将数据重新放入缓存中时进行释放锁,这样就不会有大量的请求访问数据库造成数据库的宕机。
3.1.2.热点数据永不过期
对于热点数据,我们可以使用一些定时任务定时去扫描,查询这些数据被使用了多少次,当发现此数据是热点数据且快要过期时将此数据的过期时间进行延期。
四、缓存雪崩
缓存雪崩是指在某一时刻,缓存服务宕机,或者缓存中的数据大量过期,此时有大量的请求过来查询,但此时缓存中没有数据或缓存服务已经停止,就会造成数据库的压力急剧增加,从而导致数据库服务宕机。
4.1.解决方案
4.1.1.缓存服务高可用
保证缓存服务的高可用,为了避免单点故障,可以将缓存服务部署为高可用,例如Redis的哨兵模式(Sentinel)或者集群模式(Cluster),当其中一台Redis宕机后,其他Redis服务器还可以继续提供服务。
4.1.2.接口熔断
依赖隔离组件为后端限流并降级。无论是缓存层还是存储层都会有出错的概率,可以将它们视同为资源。作为并发量较大的系统,假如有一个资源不可用,可能会造成线程全部阻塞(hang)在这个资源上,造成整个系统不可用。
4.1.3.缓存过期时间随机
为了避免在同一时间内大量数据同时过期,可以在缓存设置过期时间时加一个随机的时间(1-5分钟),这样就可以将缓存的过期时间分散开,减少因为数据库压力大而宕机的概率。