边缘缓存模式(Cache-Aside Pattern),即按需将数据从数据存储加载到缓存中。此模式最大的作用就是提高性能减少不必要的查询。

1 模式

  1. 先从缓存查询数据
  2. 如果没有命中缓存则从数据存储查询
  3. 将数据写入缓存
  代码形如:
        public async Task<MyEntity> GetMyEntityAsync(int id)
{
// Define a unique key for this method and its parameters.
var key = string.Format("StoreWithCache_GetAsync_{0}", id);
var expiration = TimeSpan.FromMinutes();
bool cacheException = false;
try
{
// Try to get the entity from the cache.
var cacheItem = cache.GetCacheItem(key);
if (cacheItem != null)
{
return cacheItem.Value as MyEntity;
}
}
catch (DataCacheException)
{
// If there is a cache related issue, raise an exception
// and avoid using the cache for the rest of the call.
cacheException = true;
}
// If there is a cache miss, get the entity from the original store and cache it.
// Code has been omitted because it is data store dependent.
var entity = ...;
if (!cacheException)
{
try
{
// Avoid caching a null value.
if (entity != null)
{
// Put the item in the cache with a custom expiration time that
// depends on how critical it might be to have stale data.
cache.Put(key, entity, timeout: expiration);
}
}
catch (DataCacheException)
{
// If there is a cache related issue, ignore it
// and just return the entity.
}
}
return entity;
} public async Task UpdateEntityAsync(MyEntity entity)
{
// Update the object in the original data store
await this.store.UpdateEntityAsync(entity).ConfigureAwait(false);
// Get the correct key for the cached object.
var key = this.GetAsyncCacheKey(entity.Id);
// Then, invalidate the current cache object
this.cache.Remove(key);
} private string GetAsyncCacheKey(int objectId)
{
return string.Format("StoreWithCache_GetAsync_{0}", objectId);
}

2 关注点

2.1 缓存数据的选择

  对于相对静态的数据或频繁读取的数据,缓存是最有效的。

2.2 缓存数据的生命周期

  过期时间短,会导致频繁查询数据源而失去缓存的意义;设置时间过长,可能发生缓存的数据过时不同步的情况。

2.3 缓存过期策略的选择

  一般缓存产品都有自己内置的缓存过期策略,最长使用的是“最近最不常使用”算法,需要在使用时,了解产品的默认配置和可配置项,根据实际需求选择。

2.4 本地缓存与分布式缓存的选择

  本地缓存的查询速度更快,但在一个分布式环境中,需要保证不同机器的本地缓存统一,这需要我们在程序中处理;分布式缓存性能不如本地缓存,但大多数时候,分布式缓存更适合大型项目,分布式缓存产品家族自带的管理和诊断工具十分适合运维和性能监控。

2.5 一致性问题

  实现边缘缓存模式不能保证数据存储和缓存之间的实时一致性。数据存储中的某个项可能随时被外部进程更改,并且此更改可能在下次将项加载到缓存中之前不会反映在缓存中。

3 一致性

3.1 淘汰还是更新缓存

淘汰缓存:数据写入数据存储,并从缓存删除
优点:简单
缺点:缓存增加一次miss,需要重新加载数据到缓存
  更新缓存:数据写入数据存储,并更新缓存
  优点:缓存不会增加一次miss,命中率高
  缺点:更新缓存比淘汰缓存复杂
  淘汰还是更新缓存主要取决于——更新缓存的复杂度,数据查询时间以及更新频率。
  如果更新缓存复杂度低,从数据存储查询查询数据有比较耗时,更新频率又高,则为了保证缓存命中率,更新缓存比较适合。此外大大多数情景,都可以用淘汰缓存,来满足需求。

3.2 操作顺序

  先操作数据存储,再操作缓存
  先操作缓存,再操作数据存储

3.3 一致性问题

非并发场景——场景为Application修改数据(考虑操作失败的影响)

先操作缓存,再操作数据存储

淘汰缓存时:
step 1:淘汰缓存成功
step 2:更新数据存储失败
结果:数据存储未修改,缓存已失效
修改缓存时:
step 1:修改缓存成功
step 2:更新数据存储失败
结果:数据不一致
先操作数据存储,再操作缓存
  淘汰缓存时:
step 1:更新数据存储成功
step 2:淘汰缓存失败
结果:数据不一致
  修改缓存时:
step 1:更新数据存储成功
step 2:修改缓存失败
结果:数据不一致

并发场景——场景位Application1修改数据,Application2读取数据,(不考虑操作失败的影响,仅考虑执行顺序的影响)

先操作缓存,再操作数据存储
  淘汰缓存时:
step 1:Application1先淘汰缓存
step 2:Application2从缓存读取数据,但是没有命中缓存
step 3:Application2从数据存储读取旧数据
step 4:Application1完成数据存储的更新
step 5:Application2把旧数据写入缓存,造成缓存与数据存储不一致
结果:数据不一致
  更新缓存时:
step 1:Application1先更新缓存
step 2:Application2从缓存读取到新数据
step 3:Application2
step 4:Application1更新数据存储成功
step 5:Application2
结果:数据一致
先操作数据存储,再操作存储
  
  淘汰缓存时:
step 1:Application1更新数据存储完成
step 2:Application2从缓存读取数据,查询到旧数据
step 3:Application1从缓存中删除数据
结果:缓存已淘汰,下次加载新数据
  更新缓存时:
step 1:Application1更新数据存储完成
step 2:Application2从缓存读取数据,查询到旧数据
step 3:Application1从更新缓存数据
结果:数据一致
  由此可见,不管我们如何组织,都可能完全杜绝不一致问题,而影响一致性的两个关键因素是——“操作失败”和“时序”。
  对于“淘汰还是更新缓存”、“先缓存还是先数据存储”的选择,不应该脱离具体需求和业务场景而一概而论。我们把关注点重新回到“操作失败”和“时序”问题上来.
  对于“操作失败”,首先要从程序层面提高稳定性,比如“弹性编程”,“防御式编程”等技巧,其次,要设计补偿机制,在操作失败后要做到保存足够的信息(包括补偿操作需要的数据,异常信息等),并进行补偿操作(清洗异常数据,回滚数据到操作前的状态),通过补偿操作,实现最终一致性。
  对于“时序”问题,需要根据程序结构,梳理出潜在的时序问题。本文例子中,不设计补偿操作,如果引入的话,操作组合的时序图可能会更加复杂。解决的思路就是对于单个用户来说操作应该是“原子的”在分布式环境中,多个用户的操作应该是“串行”的。最简单的思路,就是使用分布式锁,来保证操作的串行化。当然,我们也可以通过队列来进行异步落库,实现最终一致性。

4 缓存穿透、缓存击穿、缓存雪崩

4.1 缓存穿透

  缓存穿透是指用户频繁查询数据存储中不存在的数据,这类数据,查不到数据所以也不会写入缓存,所以每次都会查询数据存储,导致数据存储压力过大。
  解决方案:
    • 增加数据校验
    • 查询不到时,缓存空对象

4.2 缓存击穿

  高并发下,当某个缓存失效时,可能出现多个进程同时查询数据存储,导致数据存储压力过大。
  解决方案:
      • 使用二级缓存
      • 通过加锁或者队列降低查询数据库存储的并发数量
      • 考虑延长部分数据是过期时间,或者设置为永不过期

4.3 缓存雪崩

  高并发下,大量缓存同时失效,导致大量请求同时查询数据存储,导致数据存储压力过大。
  解决方案:
      • 使用二级缓存
      • 通过加锁或者队列降低查询数据库存储的并发数量
      • 根据数据的变化频率,设置不同的过期时间,避免在同一时间大量失效
      • 考虑延长部分数据是过期时间,或者设置为永不过期

5 缓存体系

   
   
位置 缓存技术
客户端 浏览器缓存
客户端应用缓存
客户端网络 代理服务器开启缓存
广域网 使用代理服务器(含CDN)
使用镜像服务器
使用P2P技术
源站及源站网络 使用接入层提供的缓存机制
使用应用层提供的缓存机制
使用分布式缓存
静态化、伪静态化
使用服务器操作系统提供的缓存机制

  总之,设计不能脱离具体需求和业务场景而存在,这里没有最优的组合方式,以上对该模式涉及问题的讨论,旨在发掘潜在的问题,以便合理应对。

参考资料

  《CloudDesignPatternsBook》

  《亿级流量网站架构核心技术》

边缘缓存模式(Cache-Aside Pattern)的更多相关文章

  1. 缓存实践Cache Aside Pattern

    Cache Aside Pattern旁路缓存,是对缓存应用的一个总结,包括读数据方案和写数据方案. 读数据方案 先读cache,如果命中则返回 如果miss则读db 将db的数据存入缓存 写数据方案 ...

  2. Cache Aside Pattern

    Cache Aside Pattern 即旁路缓存是缓存方案的经验实践,这个实践又分读实践,写实践 对于读请求 先读cache,再读db 如果,cache hit,则直接返回数据 如果,cache m ...

  3. Cache-Aside Pattern(缓存模式)

    Load data on demand into a cache from a data store. This pattern can improve performance and also he ...

  4. 缓存模式(Cache Aside、Read Through、Write Through、Write Behind)

    目录 概览 Cache-Aside 读操作 更新操作 缓存失效 缓存更新 Read-Through Write-Through Write-Behind 总结 参考 概览 缓存是一个有着更快的查询速度 ...

  5. 微软BI 之SSIS 系列 - Lookup 组件的使用与它的几种缓存模式 - Full Cache, Partial Cache, NO Cache

    开篇介绍 先简单的演示一下使用 Lookup 组件实现一个简单示例 - 从数据源表 A 中导出数据到目标数据表 B,如果 A 数据在 B 中不存在就插入新数据到B,如果存在就更新B 和 A 表数据保持 ...

  6. yii中缓存(cache)详解

    缓存是用于提升网站性能的一种即简单又有效的途径.通过存储相对静态的数据至缓存以备所需,我们可以省去生成这些数据的时间.在 Yii 中使用缓存主要包括配置和访问缓存组件 . 内部方法 一.缓存配置: 1 ...

  7. yii中缓存(cache)详解 - 彼岸あ年華ツ

    缓存是用于提升网站性能的一种即简单又有效的途径.通过存储相对静态的数据至缓存以备所需,我们可以省去生成 这些数据的时间.在 Yii 中使用缓存主要包括配置和访问缓存组件 . 内部方法 一.缓存配置: ...

  8. SQLite剖析之异步IO模式、共享缓存模式和解锁通知

    1.异步I/O模式    通常,当SQLite写一个数据库文件时,会等待,直到写操作完成,然后控制返回到调用程序.相比于CPU操作,写文件系统是非常耗时的,这是一个性能瓶颈.异步I/O后端是SQLit ...

  9. Handler+ExecutorService(线程池)+MessageQueue模式+缓存模式

    android线程池的理解,晚上在家无事 预习了一下android异步加载的例子,也学习到了一个很重要的东东 那就是线程池+缓存  下面看他们的理解.[size=1.8em]Handler+Runna ...

随机推荐

  1. vuex的实现

    源代码可以查看我的github:  https://github.com/Jasonwang911/TryHardEveryDay/tree/master/Vuex/vuex-resouce  欢迎s ...

  2. javaweb入门-----jsp概念

    jsp是什么? JSP:Java Server Pages java服务器端页面 *可以理解为 一个特殊的页面,其中既可以直接定义html标签,又可以定义java代码 *用于简化书写 <% %& ...

  3. 【一些小常识】Linux文件目录的通配符用法/*

    在使用linux命令的时候,一时有点搞不清*的用法,于是整理记录下,在做jenkins 持续集成时还是很有用的 “*”在通配符中是最常用的一种,主要整理下在使用Linux命令时,文件夹目录的用法. 1 ...

  4. C#开发可播放摄像头及任意格式视频的播放器

    前言 本文主要讲述,在WPF中,借助Vlc.DotNet调用VLC类库,实现视频播功能,下面我们先来做开发前的准备工作. 准备工作 首先,我们创建一个项目WpfVLC,然后,进入Neget搜索Vlc. ...

  5. Spring aop 影响本地事务的回滚总结

    1  @Before   不会,因为还没执行到service的业务逻辑 2  @ After    默认情况下,报错会影响事务回滚., 当设置@Order属性并设置值优先级大小, 即使报错也不会回滚了 ...

  6. node一键发布,并运行

    作为一个前端开发人员如果你只会写一些业务代码,从程序员的角度来考虑已经可以了.但是从架构的角度来考虑那远远不够: 在此记录下成长中的经历: 想要达成的目的:运行一个脚本实现代码的打包,上传至服务器并部 ...

  7. eclipse项目导入idea jdk版本不一致😵

    在导入eclipse项目到idea过程中遇到 Imported project refers to unkonwn jdks JavaSE-1.8 解决方法: file --> Project ...

  8. Rootkit与后门隐藏技术

    目录 简介 linux虚拟文件系统VFS rootkit的功能 隐藏文件 基本方法 高级方法 系统调用流程 hook sys_getdents sys_getdents的调用树 最底层的方法 隐藏进程 ...

  9. 洛谷 P2704 [NOI2001]炮兵阵地

    题意简述 给定一张地图,有山地H,平原P,平原可放置炮兵, 炮兵可以攻击沿横向左右各两格,沿纵向上下各两格的区域 求最多放几个炮兵,使他们两两攻击不到 题解思路 枚举第i层,第i - 1层,第i - ...

  10. Codeforces 343D Water Tree

    题意简述 维护一棵树,支持以下操作: 0 v:将以v为跟的子树赋值为1 1 v:将v到根节点的路径赋值为0 2 v:询问v的值 题解思路 树剖+珂朵莉树 代码 #include <set> ...