第一章 基础
一、概述
1. 缓存是什么
简单的说,在web应用中,缓存就是存入内存的数据。
2. 缓存出现的原因在web应用未引入缓存之前,程序通常都直接从关系型数据库中读取数据,受磁盘IO限制,读取的速度总会达到一个瓶颈,而对于快速发展的业务来讲,显然已经无法满足快速响应的业务需求。而众所周知内存的读取速度要远远高于数据库的读取速度,这时候就提出了缓存的方案,提倡把热点数据存放在内存中以供快速访问,从而提高响应速度和减轻数据库压力。
3. 缓存进化史
1)HashMap
在最开始,采取最多的方法是直接在应用内部使用全局变量来存储缓存数据的方案,例如在应用启动的时候,把热点数据从数据库中查询出来并放到全局变量HashMap中,这一方案明显的缺点有两个,一是没有数据淘汰机制,二是会占用应用自身的内存空间。
2)LRUHashMap
相比HashMap,LRUHashMap提供了FIFO(先进先出)、LRU(最近最少使用)、LFU(最近最少频率使用)等多种数据淘汰算法,但仍然还是存在诸多问题,比如Lock是全局锁且直接加在方法上,这会导致锁竞争严重,在高并发场景下,性能必然会比较低;另外,LRUHashMap不支持过期时间、不支持自动刷新。
3)Guava cache
谷歌推出的Guava cache采用了分段加锁的策略,大大提高了访问效率;另外相比LRUHashMap还多了两种过期时间,一个是写后多久过期(expireAfterWrite),一个是读后多久过期(expireAfterAccess)。值得一提的是,Guava cache并没有在后台开线程对符合过期条件的Entry作过期处理,而是在下次进行读写操作的时候处理,这样做的好处是避免后台线程扫描时进行全局加锁。
4)Caffeine cache
Guava cache从本质上讲还是基于LRU的封装,而Caffeine cache实现了W-TinyLFU算法,吸收了LFU、LRU两种算法的优点,比较接近理想命中率。另外,Caffeine cache所有的数据都存储在ConcurrentHashMap中,在读写吞吐量上也非常优秀。
5)第三方缓存框架
以上几种方案,本质上都是直接使用的进程内的缓存(本地缓存)。而如果使用第三方缓存框架,虽然多了一次网络IO开销,速度上会稍差一点,但有更多明显的好处,不仅方便管理,而且可以直接使用服务器的内存,缓存数据量的大小不再受进程的限制,另外,像Redis这样的缓存框架,还支持分布式、集群和持久化等重要功能。
二、缓存架构设计 1. 如何设计和使用缓存1)确认是否需要缓存
在使用缓存之前,首先要确认项目是否真的需要使用缓存,如果没有需要就没有必要增加项目的技术复杂度。一般来说,可以从两个方面来判断否需要使用缓存:
①某些应用需要消耗大量的CPU去计算结果,导致CPU占用过高。
②数据库IO占用过高,数据库连接池比较繁忙,甚至出现了连接不够的情况。
2)如何选择缓存框架
缓存分为进程内缓存和使用第三方缓存框架,可以根据实际情况进行选择。比如数据量不是很大,数据更新频率也比较低,并且不需要淘汰算法,可以选择直接使用ConcurrentHashMap;如果需要淘汰算法和丰富的API,推荐选择Caffeine cache;如果是在分布式场景下做缓存或要实现缓存持久化,肯定要优先选择使用第三方缓存框架。还有一种常见的做法是把本地缓存和第三方缓存框架结合起来,形成一二级缓存。
3)如何制定合理的回填策略
不同的应用场景,对于缓存的要求不一样,对实时性的要求也不一样。例如榜单这种一天更新一次的数据,可以在每天晚上定时生成一次,而不是在前端用户访问的时候再去生成。再例如对于用户名称修改的操作,必须实时的更新到本地缓存和远程缓存中。
4)如何制定合理的过期策略
Caffeine提供了基于大小(size-based)、基于时间(time-based)、基于引用(reference-based)三种过期策略。Redis采用的是惰性删除+定期删除的策略。在应用时,要根据实际应用场景制定合理有效的过期策略,包括全局过期策略和单项数据的过期策略,都要考虑进去。比如在高并发场景下,热点数据要永不过期,其它数据的过期时间尽量随机,避免同一时刻出现大量缓存过期压垮数据库的情况,对查询数据库为空的数据也要考虑写进缓存,避免缓存机制被架空。
5)如何更新缓存
当有多个并发的请求更新数据,并不能保证更新数据库的顺序和更新缓存的顺序一致,那就会出现数据库中和缓存中数据不一致的情况,所以一般来说考虑删除缓存有以下两种情况:
①先删除缓存,再更新数据库。这样操作会有一个比较大的问题,就是当我们删除缓存后还未来得及写库时,假如有一个读请求进来,会直接读库并把旧数据放入缓存中,后续所有读请求访问到的将会是老数据。
②先更新数据库,再删除缓存(推荐这种方式)。这样操作可以避免上面的问题,但同样引入了新的问题,如果有一个数据是没有缓存的,查操作会直接落库,这时更新操作进来完成数据库更新并删除掉缓存,这时候查操作才刚刚执行完又进行缓存回填,就同样会导致缓存不一致的情况出现,但因为这种情况的触发条件非常苛刻,所以基本能满足需求。另外,还有个问题,如果我们缓存删除失败了,同样会出现数据不一致的情况,那么就只能靠过期超时来兜底了,也可以对此进行优化,比如删除失败时,可以将其放入队列后续进行异步删除。
6)在团队内制定标准
根据不同业务需要,设计不同的策略,最终成为整个团队都要去遵循的标准。
2. 多级缓存实现略。
3. 缓存穿透、缓存雪崩、缓存1)缓存穿透
高并发的访问一个缓存和数据库中都不存在的 key,此时缓存就起不到作用,请求每次都会走到数据库。解决方案:
①接口对用户权限、数据合法性做严格校验。
②将key缓存一个空值,并设置较短的过期时间。
③使用布隆过滤器存储所有可能访问的key,不存在的key直接过滤掉,存在的 key 则再进一步查询缓存和数据库。
2)缓存击穿
某一个热点数据在缓存过期的一瞬间,同时有大量的请求进来,造成瞬时数据库请求量大、压力骤增,甚至可能打垮数据库。解决方案:
①加互斥锁。在并发的多个请求中,只有第一个请求线程能拿到锁并执行数据库查询操作,其他的线程拿不到锁就阻塞等着,等到第一个线程将数据写入缓存后,直接走缓存。关于互斥锁的选择,可以选择Redis分布式锁,保证只有一个请求会走到数据库,也可以选择JVM锁,保证在单台服务器上只有一个请求走到数据库,需要注意的是,无论采用哪种锁,加锁时都要以key为维度去加锁,避免不同的key之间发生互相阻塞,从而造成性能严重损耗。
②热点数据不过期。直接将缓存设置为不过期,然后由定时任务去异步加载数据,更新缓存。使用这种方式需要考虑业务能接受数据不一致的时间和异常情况的处理,确保不会因缓存未及时更新而导致不可接受的业务错误发生。
2)缓存雪崩
大量的热点key设置了相同的过期时间,导在缓存在同一时刻全部失效,造成瞬时数据库请求量大、压力骤增,甚至打垮数据库。解决方案:
①分散过期时间。可以给缓存的过期时间时加上一个随机值时间,使得每个key的过期时间分布开来,不会集中在同一时刻失效。
②热点数据不过期。该方式和缓存击穿一样,也是要着重考虑刷新的时间间隔和数据异常如何处理的情况。
③加互斥锁。该方式和缓存击穿一样,按key维度加锁,对于同一个key,只允许一个线程去计算,其他线程原地阻塞等待第一个线程的计算结果,然后直接走缓存即可。
持续更新中...



