缓存有用吗
我们先从计算机一个重要的特性,计算机访问数据,在相对的一段时间内,计算机经常会访问相同的数据,这个特性我们叫做局部性原理,我们使用局部性原理把经常被加载的数据存起来,再次访问的时候就能提升性能,这就是缓存最大的作用
什么时候需要缓存
首先我们看一下操作系统在不同层数据传输需要的时间
- L1 cache reference 读取CPU的一级缓存 0.5 ns
- Branch mispredict(转移、分支预测) 5 ns
- L2 cache reference 读取CPU的二级缓存 7 ns
- Mutex lock/unlock 互斥锁\解锁 100 ns
- Main memory reference 读取内存数据 100 ns
- Compress 1K bytes with Zippy 1k字节压缩 10,000 ns
- Send 2K bytes over 1 Gbps network 在1Gbps的网络上发送2k字节 20,000 ns
- Read 1 MB sequentially from memory 从内存顺序读取1MB 250,000 ns
- Round trip within same datacenter 从一个数据中心往返一次,ping一下 500,000 ns
- Disk seek 磁盘搜索 10,000,000 ns
- Read 1 MB sequentially from network 从网络上顺序读取1兆的数据 10,000,000 ns
- Read 1 MB sequentially from disk 从磁盘里面读出1MB 30,000,000 ns
- Send packet CA->Netherlands->CA 一个包的一次远程访问 150,000,000 ns
从这个数据可以看出CPU读取L1缓存(SRAM) 比读取内存的速度快了200倍 而从网络中读取相同大小的数据比从内存中又慢了近10万倍 所以如果把经常使用的数据缓存起来就能大幅提供系统的性能
整个计算机系统使用到的缓存情况
每一层实际上都可以看做下一层的缓存,从山顶到山脚,计算机访问到的时间递增,而每一层的物理硬件造价递减,cpu计算数据先从山顶开始找数据,如果本层没有找到就去下层找,每向下找一层,找的层数越多,计算所需的时间自然就越多
常用缓存算法
几种常用的缓存算法
LRU (Least Recently Used 最近最少使用) 不断将最近使用的对象放到列表的顶部,这样最近最少使用的对象就会慢慢沉到底部,当容量不足的时候,可以将底部的对象去除,以留出空间给新的对象。
FIFO(First In First Out 先进先出) FIFO是将管理一个缓存列表,将先生成的缓存放在前面,最近生成的缓存放在列表的后面。当缓存空间满的时候,将最早生成的缓存去除。该算法很简单,但是缓存效果不好。
LFU(Least Frequently Used 最不经常使用) LFU是给每个缓存对象设置一个缓存频率,当缓存空间不足时,将最少使用的缓存对象去除。当遇到某些缓存对象之前历史上使用频率很高,但是之后不再使用时,这种算法会导致没有价值的缓存长期在缓存中驻留,导致实际可用的缓存空间减少。
RAND(Random Algorithm) 当缓存空间不足时,系统通过一个随机值删除缓存空间中的随机一个缓存对象。这种算法也是比较简单的,但是命中率很低。
服务器缓存的使用
分析了缓存的原理和使用情况后我们看一下我们的服务器开发中需要使用的情况 我们的缓存主要是 用来保存经常访问数据库的数据降低数据库压力 内存和磁盘的访问数据毕竟不是同一量级所以我们会在此加缓存
- 内存缓存
在单一应用的场景中我们可以直接使用内存缓存把缓存全部放到内存中 但是在分布式场景中就存在了缓存多次加载的问题
- 分布式缓存
于是就我们就使用了redis这样的nosql数据库来充当分布式缓存的作用 利用其内存数据库的高性能来提升查询数据库磁盘IO带来的开销
- 多级缓存
再后来我们将内存缓存和分布式缓存集合起来形成多级缓存来降低网络IO带来的开销
缓存带来的问题
使用缓存的过程中要注意的一些问题
- 缓存穿透
当用户访问一个缓存中不存在的key的时候每次其实都会查询数据库 这样就失去了缓存的意义了 这种情况的一般是添加对key的检验比如key都为数字的话 那么其他字符都为非法的情况可以直接禁止访问
- 缓存并发
有时候如果网站并发访问高,一个缓存如果失效,分布式情况可能出现多个服务器节点同时查询数据库,如果并发确实很大,这也可能造成DB压力过大,还有缓存频繁更新的问题 如果缓存时间又过小,数据库查询又比较慢,又可能进一步引发服务器雪崩
简单的做法对缓存查询加分布式锁,如果KEY不存在,就加锁,然后查DB入缓存,只不过利用锁的方式,会造成部分请求等待, 相对而已更好的做法是,通过单独的线程在缓存失效前更新缓存,同时通知其他节点刷新缓存时间 同时在缓存的超时时间的设置上对每个节点的实例添加一点随机值降低其同时并发的可能性
- 多级缓存的更新问题
使用多级缓存也存在内存缓存的更新问题, 这里我们可以使用redis的 pub/sub机制来通知其他节点更新内存缓存 重新从分布式缓存中加载最新的数据 当然也存在通知失败的情况,所以内存缓存还是必需有失效时间来保证其能强制刷新
- 缓存的一致性问题
缓存和数据库修改存在一定的时差无法做到完全强一致性,多级缓存的更新也存在一点延迟, 那么如果是重要的数据就会导致不一致了, 所以这也是使用缓存需要注意的一点, 一般选择缓存的数据都是对强一致性要求不高的场景
最后结合我们的微服务场景和上述谈论的问题 简单实现的一个多级缓存(已完成部分功能) 可供参考 这里