[亿级流量网站架构读后记录二、缓存篇]
高并发
缓存
作用即让数据更接近于使用者, 目的是让访问速度更快.
缓存命中率
从缓存中读取数据的次数与总读取次数的比率.
缓存回收策略
基于空间, 空间达到上限按照策略回收.
基于容量, 缓存条目数量达到上限…
基于时间, TTL(Time To Live), 存活达到一定时间…; TTI(Time To Idle), 空闲达到一定时间…
基于java对象引用, 比如软弱引用.
回收算法, 使用基于空间和容量的缓存会使用一定的策略移除旧数据, 常见如下:
FIFO(First In First Out): 先进先出算法, 即先放入缓存的先被移除.
LRU(Least Recently Used): 最近最少使用算法, 使用时间距离现在最久的那个被删除
LFU(Least Frequently Used): 最不常用算法, 一定时间段内使用频率最少的被移除
实际应用中基于LRU的缓存居多, 如Guava Cache, Ehcache 支持 LRU.
java 缓存类型
- 堆内存: 使用java堆内存来存储对象. 好处是不需要序列化和反序列化, 是最快的缓存.缺点就是当缓存数据量很大时, GC暂停时间会很长, 存储容量受限于堆空间大小. 一般使用软/弱引用来存储. 如Guava cache, Ehcache 3.x, MapDB实现.
- 堆外内存: 即缓存数据存储在堆外内存, 可减少GC时间, 可以支持更大的缓存空间. 但是需要序列化.所以会比堆缓存慢得多.
- 磁盘缓存: 即缓存数据存储在磁盘上, 当JVM重启数据还是存在的, 而堆内存和堆外缓存数据会丢失, 需要重新加载. 可以使用Ehcache 3.x, MapDB实现.
- 分布式缓存: 上边的缓存是进程内缓存和磁盘缓存, 在多JVM实例下, 会存在两个问题: 1.单机容量问题; 2.数据一致性问题(多台JVM实例的缓存数据不一致怎么办? 可以设置数据的过期时间定时更新数据); 3.缓存不命中时, 需要回溯到DB/服务请求多变问题, 每个实例在缓存不命中的情况下都会回溯到DB加载数据, 因此整体对DB的访问就变多了, 解决办法是使用一致性哈希分片算法. 因此, 要考虑使用分布式缓存.
两种模式如下:
- 单机时: 存储最热的数据到堆缓存, 相对热的数据到堆外缓存, 不热的数据到磁盘缓存.
- 集群时: 存储最热的数据到堆缓存, 相对热的数据到堆外缓存, 全量数据到分布式缓存.
技术举例:
Guava Cache 只提供堆缓存, 小巧灵活, 性能最好, 如果只使用堆缓存, 就它了.
Ehcache 3.x 提供了堆缓存, 堆外缓存, 磁盘缓存, 分布式缓存. 但是, 这个版本代码注释比较少, API 功能还不完善. 如果需要稳定的API和功能, 考虑使用2.x.
MapDB 是一款嵌入式Java数据库引擎和集合框架. 提供了Maps, Sets, Lists, Queues, Bitmaps的支持, 还支持ACID事务, 增量备份. 支持堆缓存, 堆外缓存, 磁盘缓存.
应用级缓存示例
多级缓存API封装
- 本地缓存初始化: 本地缓存过期时间使用分布式缓存过期时间的一半, 防止本地缓存数据缓存时间太长造成多实例间的数据不一致; 另外, 将缓存key前缀与本地缓存关联, 从而匹配缓存key前缀, 就可以找到相关联的本地的缓存.
- 写缓存: 先写本地缓存, 如果需要写分布式缓存, 则通过异步更新分布式缓存.
- 读缓存: 先读本地缓存, 本地不命中再批量查询分布式缓存, 在查询分布式缓存时通过分区批量查询(即将key分页查询).
NULL Cache: 当DB没有数据时, 写入NULL对象到缓存. 读取数据时, 如果发现NULL对象, 则返回null, 而不是回源到DB. 通过这种方式可防止当key对应的数据在DB中不存在时频繁查询DB的情况.
强制获取最新数据: 可通过ThreadLocal开关来决定是否强制刷新缓存.
失败统计
延迟报警(不能频繁报警, 可考虑N久报警了M次)
缓存使用模式实践
前人总结好的模式, 主要分为两大类: Cache-Aside和Cache-As-SoR(Read-through、Write-through、Write-behind)。
SoR(system-of-record):记录系统,或者可以叫做数据源。
Cache: 缓存,是SoR的快照数据,Cache的访问速度比SoR要快,放入Cache的目的是提升访问速度,减少回源到SoR的次数。
回源:即回到数据源头获取数据。
Cache-Aside:即业务代码围绕Cache写,比如读取缓存,不存在则回源。适合AOP实现。可能存在并发更新的情况:
- 如果是用户维度的数据,这种几率非常小,可以不考虑,加上过期时间来解决即可。
- 对于如商品这种基础数据,可考虑使用cannal订阅binlog,来进行增量更新分布式缓存,这样不会存在缓存数据不一致的情况。但是,缓存更新会存在延迟。而本地缓存可根据不一致容忍度设置合理的过期时间。
- 读服务场景,可以考虑使用一致性哈希,将相同的操作负载均衡到同一个实例,从而减少并发几率。或者设置比较短的过期时间。
Cache-As-SoR:即把Cache看作SoR,所有操作针对Cache进行,然后Cache再委托给SoR进行真实的读写。即业务代码中只看到Cache的操作。看不到SoR的操作。有三种实现:read-through、write-through、write-behind。
Read-through:业务代码首先调用Cache,如果Cache不命中由Cache回源到SoR,而不是业务代码。Guava cache和Ehcache 3.x都支持该模式。好处是,应用业务代码更简洁了,没有重复代码;解决Dog-pile effect,即当某个缓存失效时,又有大量相同的请求没命中缓存,从而使请求同时到后端,导致后端压力太大,此时限定一个请求去拿即可。
Write-Through:称为穿透写模式/直写模式–业务代码首先调用Cache写(新增/修改),然后由Cache负责写缓存和写SoR。目前只有Ehcache 3.x支持。
Write-Behind:也叫Wrtie-Back,即回写模式。不同于Write-Through是同步写SoR和Cache,Write-Behind是异步写。异步写可实现批量写,合并写,延时和限流。
Copy Pattern
有两种Copy Pattern,Copy-On-Read(在读时复制)和Copy-On-Write(在写时复制),在Guava Cache和Ehcache中堆缓存都是基于引用的,这样如果有人拿到缓存数据并修改,则发生不可预测的问题。Ehcache 3.x提供了支持。
性能测试,可使用JMH1.4进行基准性能测试。