系统中的缓存无处不存在,数据库有缓存,浏览器有缓存,网络等等都存在缓存,这两天正好看了下缓存经常遇到的问题,在这里总结下。
缓存一般存储介质为内存,可以提高系统的相应速度,缺点是存储介质昂贵,高并发下会存在多级存储(数据库等)不一致等情况,所以要把重要的的数据放入缓存中,也要采取一些措施,尽可能的保证数据一致性
什么是好的缓存
我认为在确保缓存数据正确的情况下,缓存命中率高就是一个好的缓存
命中率=返回正确结果数/请求缓存次
缓存分类
大体分为本地缓存和分布式缓存
- 本地缓存:手动编写或是第三方框架Encache,Guava cache等等
- 分布式缓存:redis等等
本地缓存场景:
- 单机,或是集群场景下不需要共享缓存
- 多服务多应用场景下,各自本地内存数据一致性要求低
- 应用中不常变化的局部数据
方案
- 手动编程(hashMap,LinkedHashMap)
- 第三方框架Encache,Guava cache等等
- 区别:手动编程方式简单,需要提供基本的删除更新缓存方法,但是提供一些数据统计,缓存淘汰策略等比较麻烦 第三方框架会提供一些常用的的API,还可以配置缓存的最大大小,统计,缓存淘汰策略等等一些参数。encache还支持持久化
分布式缓存场景
- 多应用需要共享一份缓存
本地缓存与分布式缓存对比
- 优点
- 效率高,无网络开销
- 缺点
- 与业务服务耦合,服务占用内存与缓存占用内存相互污染
- 多应用无法共享缓存
- 各节点单独维护,浪费内存
- 一般无持久化,宕机无法持久化
- 数据结构单一
- 无法扩容
缓存基本注意事项
为了防止内存泄漏或是缓存数据和其他(DB)存储介质数据不一致的情况,需要注意的几个基本点
- 设置最大容量
- 设置合理的失效时间
- 设置合理的缓存淘汰策略
数据更新套路
基本套路
- 失效:应用程序先从cache取数据,没有得到,则从数据库中取数据,成功后,放到缓存中。
- 命中:应用程序从cache中取数据,取到后返回。
- 更新:先把数据存到数据库中,成功后,再让缓存失效(先更新DB,再删除缓存)
更新缓存&删除缓存
- 更新缓存:并发量大的情况,命中率高
- 删除缓存:编写更简单
数据一致性问题(假设缓存不存在失效时间,且以数据库中的数据为准)
先写数据库,再删除缓存
第一步写数据库操作成功,第二步删除缓存失败
不一致情况:会出现DB中是新数据,Cache中是旧数据,数据不一致
先删除缓存,再写数据库
第一步删除缓存成功,第二步写数据库失败,引发一次Cache miss,重新查询DB拉取最新数据
不一致情况:A,B两个线程顺序,A线程更新,B线程查询,A删除了缓存,B发现没有缓存,查询数据库中的旧值,并进行缓存,A线成更新完DB,这时数据库和缓存不一致
先写数据库,再更新缓存
第一步写数据库成功,第二步更新缓存失败
不一致情况:线程A更新了数据库,线程B更新了数据库,线程B更新了缓存,线程A更新了缓存(因为网络等原因,B却比A更早更新了缓存)
先更新缓存,再更新数据库(此方案不可取)
第一步更新缓存成功,第二步写数据库失败(此方案不可取,会直接产生一致性问题,因为最终要以数据库中为准)
不一致解决方案:
-
Timer异步线程二次删除(更新)缓存
-
更新缓存失败mq消息,异步去删除(更新)缓存。
-
新增一个线下的读binlog的异步淘汰模块
-
设置合理的缓存失效时间(推荐),这种是最简单的方式,允许系统短时间存在数据不一致,但是会最终一致
其他技巧:
1.启动加载全量数据(数据量小的时候,相当于缓存预热,更适合本地缓存)
2.Quatrz或Timer更新时间频率配置化(DB,ZK)
3.将缓存内容放到ZK,当数据放生变化时,直接通过zk的wather机制,更新缓存(数据实时性更强)
4.长短双缓存,短缓存失效后,查询DB或是接口异常,可使用长缓存作为返回值并延长长缓存的时间,后面每次查询都会经过DB或是接口,调用正常时同时更新短缓存和长缓存为最新数据。此方案保证接口至少可以返回数据(降级方案(有损的))
缓存淘汰策略
什么是缓存淘汰策略?
内存是特别昂贵的,所以不能无限制的往内存里放数据,使用缓存的时候都会限制允许使用内存最大大小来防止内存泄漏,那么问题来了,如果达到了Max值继续往内存里面添加会怎么样呢?这个时候就需要缓存的淘汰策略了 即当缓存达到设置的最大大小时候可根据缓存的淘汰策略,置换一部分缓存数据
常用的缓存淘汰策略
- FIFO(first in first out)
前进先出策略,最先进入缓存的数据在缓存容量空间不够的情况下会被优先清理,以释放空间加载新的数据。该策略算法主要比较缓存元素的创建时间。在数据实效性要求场景下可选择该类策略,优先保障最新数据可用
- LFU(less frequently used)
最少使用策略,无论是否过期,根据元素的被使用次数判断,清除使用次数较少的元素以释放空间。策略算法主要比较元素的 命中次数。 在 保证高频数据有效性 场景下,可选择这类策略
- LRU(least recently used)
最近最少使用策略,无论是否过期,根据元素最后一次被使用的时间戳,清除最远使用时间戳的元素以释放空间。该策略算法主要 比较元素最近一次被get使用时间。在热点数据场景下较适用,优先保证热点数据的有效性
缓存常见问题
缓存预热
描述:将访问次数多,比较热的数据提前加载到缓存中
解决方案:数据量小,启动项目时进行加载,数据量较多时,开发个后门管理页面,手动触发进行缓存预热
缓存穿透
描述:缓存穿透,是指查询一个数据库一定不存在的数据。正常的使用缓存流程大致是,数据查询先进行缓存查询,如果key不存在或者key已经过期,再对数据库进行查询,并把查询到的对象,放进缓存。如果数据库查询对象为空,则不放进缓存
问题:每次都去查询数据库,而每次查询都是空,每次又都不会进行缓存。假如有恶意攻击,就可以利用这个漏洞,对数据库造成压力,甚至压垮数据库
解决方案:如果从数据库查询的对象为空,也放入缓存,只是设定的缓存过期时间较短
缓存雪崩
描述:是指在某一个时间段,多个缓存key集中过期失效
问题:缓存集中过期,请求落到数据库,产生周期性压力波峰
解决方案:缓存失效时间添加个随机值(随机5分钟内失效等等),缓存失效时间分布均匀。
缓存击穿
描述:缓存在某个时间点过期的时候,恰好在这个时间点对这个Key有大量的并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮,缓存雪崩的区别在于这里针对某一key缓存,前者则是很多key
问题:大并发的请求可能会瞬间把后端DB压垮
解决方案: 1.加互斥锁(降低系统吞吐量,多个线程进行等待)
2.设置热点数据永远不过期,定时去更新最新数据(推荐)
最后用一张图来加深记忆吧
总结,关于缓存需要注意地方挺多的,数据不一致的问题也很复杂,也有很多不同的方案,最简单可靠的方式还是给缓存添加合理的过期时间。
本地缓存和分布式缓存还有一些不同之处,有时间再去整理下分布式缓存(redis)等一些知识点。
参考: