假装在面试(三):你遇见过缓存穿透吗,怎么解决的。
我们先来看一个缓存穿透的场景:
小明是一家公司的主程,这家公司准备上线一项很大的活动,这个活动的流量极大。数据库系统肯定承受不住这么大的流量冲击,所以小明在活动开始之前提前将所有的数据都导入了缓存,避免了流量直接访问数据库。
然后,小明在代码里面做了一层这样的逻辑,只有缓存中不存在的数据,才会去查询数据库。也就是说,只有在活动中新增的数据才需要查询数据库,因为活动之前的数据已经全部导入缓存中了,可以由缓存直接返回给客户端,大大的提高了系统的并发能力,完全足以应对这一次活动。
做好这一切后,小明信誓旦旦的上线了项目,但是到了活动开始的那一天,没多久就发现系统直接崩溃掉了,活动的接口根本无法访问。于是小明毫无疑问的被老板狠狠地批评了一顿。
小明痛定思痛,开始寻找自己设计的系统的问题,明明加上缓存之后,系统应对这个活动的流量应该绰绰有余,为什么还会很迅速的崩溃掉了呢。
缓存穿透
缓存穿透是指查询一个根本不存在的数据,所以缓存层没有命中,这条查询直接透过缓存进入了存储层(一般指数据库系统)。然而,由于这个数据并不存在,所以存储层也不会命中。且不说存储层进行空查询所消耗的成本很大,在这种场景下,缓存就已经失去了它存在的意义,没有办法保护存储层,所有的空查询都穿透了缓存,直接由存储层处理,在大并发场景下,存储层显然无法应对这些请求。
假设有一个登录系统,需要每次登录都要判断账户名user和密码password,正常情况下,在缓存中可以根据user拿到password来进行验证,不需要走到存储层。但是如果在大量用户将用户名输出错误的时候(本人就经常输错),缓存层根本找不到这些用户名,所以会全部到存储层查询。也就是说,客户端的误操作会导致缓存穿透。
另外,恶意攻击,爬虫也会造成大量的空查询,一样会让存储层承受巨大的压力,甚至崩溃。
缓存穿透如下图,所有的miss请求都已经透过缓存进入了存储层:
解决方案
缓存空对象
这个方案想到的人应该很多,以上面说的登录系统为例,加入有个用户名在存储层查询不到,那么就返回给缓存层一个null,缓存需要将这个用户名的值记录为null,下次这个用户再登录的时候,就直接由缓存层返回null,不需要经过存储层了。
逻辑如图,第二次请求相同的key,哪怕没有数据,也不会进入存储层了:
你可能发现了一些问题。这样设计虽然一定程度上保护了存储层,但是假如有个用户现在又注册了新用户名,但缓存里这个用户名的值还是null,那就无法登录了。
所以这种情况下,在数据写入端一定要做好数据同步,用户创建的账号数据写入存储层的时候,也一定要同步到缓存中才行。
这种做法需要注意的是,一定要做好数据的同步,新增的数据一定要及时同步到缓存中,否则会出现数据不一致,导致系统异常。
布隆过滤器
如果不了解布隆过滤器的同学,请看这里:
https://hackernoon.com/probabilistic-data-structures-bloom-filter-5374112a7832
在查询缓存之前先将key用布隆过滤器过滤一次,由于布隆过滤器可以返回"绝对不存在"的值,所以对于大多数空查询,在这一步就可以直接确定,不用再往下查询了。
布隆过滤器的优点很明显,不用太复杂的业务逻辑就可以防止缓存穿透。特别是在超大的系统下,有成百上千的接口使用了缓存,想要将这些接口全部缓存空对象,然后做数据同步,那是一项很耗时费力的工程。更不用说大公司的组件化系统,要改起来那可能需要很多团队互相协作,工作量巨大。
但是,使用布隆过滤器就不用这么麻烦。
当然,布隆过滤器的技术性显然也比缓存空对象要高,具体如何使用也需要看具体的情况。
以上就是解决缓存穿透的两种常见方案(其实主要是要理解布隆过滤器)。