[亿级流量网站架构读后记录二、缓存篇]

[亿级流量网站架构读后记录二、缓存篇]

高并发

缓存

​ 作用即让数据更接近于使用者, 目的是让访问速度更快.

缓存命中率

​ 从缓存中读取数据的次数与总读取次数的比率.

缓存回收策略

  • 基于空间, 空间达到上限按照策略回收.

  • 基于容量, 缓存条目数量达到上限…

  • 基于时间, 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进行基准性能测试。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章