缓存主要是将数据写入到读取速度更快的存储介质中,相比于db存储,可以提供高性能的数据快速访问,广泛应用于高并发和大数据场景。
缓存分类
缓存主要可以分为cdn缓存、反向代理缓存、本地应用缓存、以及分布式缓存等。
CDN缓存
CDN的基本原理是将各种缓存服务器部署在各个地域中,利用全局负载技术将用户的访问指向距离最近的工作正常的缓存服务器上,由缓存服务器直接响应用户请求。主要用于缓存一些静态资源,例如静态网页、图片、视频资源等。
优点:
- 访问速度会更快
- 减少服务本身和带宽压力,抗攻击
缺点:
- 不适合资源会更新的场景,一般通过不同名称来代替。
春晚项目中所有的静态资源全部走cdn,减轻文件服务器的压力。
一旦图片变更,通过jumbo下发新的资源,需要确保资源名称不会重复。为了确认名称唯一,最终采用的文件名的格式为:前缀+时间戳+jumbo+conan/tutor+文件md5编码。这样图片变更之后,app将会请求新的图片路径。
但是客户端本身内置一个兜底的cdn资源,确保jumbo接口挂之后仍可以正常进入app参加活动,这个需要文件名称不能发生变更,同时文件内容会改变。采用策略:文件一旦变更,立即强刷cdn,但是会有一定的时间差异,而且不能做到100%。
反向代理缓存
反向代理处理所有的对web服务器的请求,位于应用服务器机房,一般只适用于缓存体积较小的静态文件资源。
本地缓存
应用和cache在同一个进程内部,是一种进程内缓存。请求缓存速度会很快,没有过多的网络开销,适用于不需要集群支持或者集群情况下各节点无需互相通知的场景。但是多个应用都需要维护自己的单独的缓存,会浪费一定的内存。
缓存方式:
- 内存缓存:直接将数据存储到服务内容中,通过程序维护缓存对象,是访问速度最快的方式
- 硬盘缓存:将数据缓存到硬盘中,减少了网络传输开销,会比网络读取数据库更快
图文配置等内容均采用将db数据提前加载到本地内存中,读取时直接是内存匹配,可以支持更高的并发量。
缺点:
- 缓存数据大小较小,机器之间冗余缓存
- 数据存在多个机器之间,一致性难以保证
使用场景:
- 只读数据
- 并发量极高(秒杀业务)
- 一定程度上允许数据不一致
本地缓存目前在很多场景下都会有应用,但是只建议应用于只读场景或者一定程度上允许数据不一致的场景,且在一定程度上违背了服务层无状态的准则。(服务层无状态才可任意水平拓展)
分布式缓存
分布式缓存指缓存与服务应用隔离,缓存本身就是一个独立的应用,多个应用之间可以共享缓存。分布式缓存主要适用于一些经过运算的复杂数据,以及一些频繁访问的热点数据。
常见的分布式缓存为:
- memcached:一种内存对象缓存系统,在内存中维护对象的hash表,将数据加载到内存中,然后从内存中读取,因此可以提高读取速度。memcached本身没有“分布式”的功能,服务之间相互隔离。
- redis:一种远程内存数据库(非关系型数据库)
这两种缓存都是目前较为常用的KV结构缓存。使用过程中需要注意不能把redis当DB使用,我们本身redis目前也分为存储和缓存两种类型。
| Redis | Memcached | |
|---|---|---|
| 支持的数据结构 | 哈希、列表、集合、有序集合 | 纯kev-value |
| 持久化支持 | 有 | 无 |
| 高可用支持 | redis天然支持集群功能,可以实现主动复制,读写分离。官方也提供了sentinel集群管理工具,能够实现主从服务监控,故障自动转移,这一切,对于客户端都是透明的,无需程序改动,也无需人工介入 | 需要二次开发 |
| 存储value容量 | 最大512M | 最大1M |
| 内存分配 | 临时申请空间,可能导致碎片 | 预分配内存池的方式管理内存,能够省去内存分配时间 |
| 虚拟内存使用 | 有自己的VM机制,理论上能够存储比物理内存更多的数据,当数据超量时,会引发swap,把冷数据刷到磁盘上 | 所有的数据存储在物理内存里 |
| 网络模型 | 非阻塞IO复用模型,提供一些非KV存储之外的排序,聚合功能,在执行这些功能时,复杂的CPU计算,会阻塞整个IO调度 | 非阻塞IO复用模型 |
| 水平扩展的支持 | 暂无 | 暂无 |
| 多线程 | Redis支持单线程 | Memcached支持多线程,CPU利用方面Memcache优于Redis |
| 过期策略 | 有专门线程,清除缓存数据 | 懒淘汰机制:每次往缓存放入数据的时候,都会存一个时间,在读取的时候要和设置的时间做TTL比较来判断是否过期 |
| 单机QPS | 约10W | 约60W |
| 源代码可读性 | 代码清爽简洁 | 能是考虑了太多的扩展性,多系统的兼容性,代码不清爽 |
| 适用场景 | 复杂数据结构、有持久化、高可用需求、value存储内容较大 | 纯KV,数据量非常大,并发量非常大的业务 |
缓存问题
使用误区
缓存使用误区:
-
未考虑缓存雪崩
- 可以提前预估流量,数据库仍然需要有足够的性能应对缓存挂掉的场景
- 将缓存水平切分或者高可用设计,提升缓存的可靠性
-
缓存不要跨服务:
- 不要将缓存作为服务之间数据传输的媒介
- 多服务之间可能存在命名冲突,缓存淘汰时可能会互相影响
- 不符合微服务本身“数据库/缓存私有”的设计原则。
-
调用方缓存数据:
- 服务提供方本身可以缓存数据,对调用方屏蔽具体的实现细节
- 调用方本身又对缓存的数据做缓存,服务提供方数据变动之后,调用方无法感知。如动态配置、图文配置等,本身会有缓存,服务调用方再加缓存会有一定的数据不一致
正确用法:
- 设计高可用缓存设计或者提前升级DB性能
- 各个服务应该私有化自己的缓存存储、对上游业务屏蔽底层的复杂性。
- 对数据一致性较高的场景,尽量不要缓存其他服务的数据
- 需要考虑一定的缓存穿透的场景,若一个查询返回的数据为空(不管是数据不存在,还是系统故障),我们仍然把这个空结果进行缓存,但它的过期时间会很短,一般只有几分钟。
数据一致性
对于读请求,一般是查缓存,miss后查询db,并把结果回填到cache中。若cache hit,则直接返回数据。但是写请求一般会有两种差异。
淘汰缓存
大部分场景下,淘汰缓存会增加一次cache-miss,但是会减少很多修改缓存带来的业务复杂性。更新缓存在并发写的时候可能会出现数据不一致的情况。因此,一般场景下建议可以直接淘汰缓存。
先修改数据库
Cache Aside Pattern 建议写请求先修改数据库,后淘汰缓存。
若先修改缓存,如下图所示,可能在读写并发场景下,缓存先被淘汰,随后数据库主库数据被修改,此时从库还未修改,而读请求读取到从库的旧数据并回填cache,最终导致数据库与cache不一致。
但是如果在操作数据库→ 淘汰缓存这两部之间没有做到原子性,则也会出现数据不一致的情况。
此外,若操作完数据库,主从同步未完成,也可能出现数据不一致的情况。
先淘汰缓存
先操作数据库成功,删除缓存失败会导致数据的不一致性。删除缓存成功、修改数据失败仅仅是一个cache miss,并不会带来缓存一致性。但是发生写请求后(不管是先操作DB,还是先淘汰Cache),在主从数据库同步完成之前,如果有读请求,都可能发生读Cache Miss,读从库把旧数据存入缓存的情况。
主动同步解决
主从延迟是导致缓存与数据库不一致的重要原因,因此如何解决主从同步是个非常关键的问题。
解决思路:
- 升级硬件,提升DB性能
- 强制读主库(我们在购买成功页等场景下也需要读取电商的主库接口,但是需要业务方自行判断)
- 选择性读主库,接数据库中间件。
二次删除缓存
如图所示,在主从同步未完成,缓存中已经存在脏数据之后。等到主从同步结束之后,通知服务方再次淘汰缓存。
优点:能够将数据不一致的时间控制在主从延迟的较短时间范围内
缺点:业务更加复杂
所以一般更通用的方法为:在经验主从延迟事件后再次淘汰缓存。
本地缓存一致性
本地缓存数据会冗余多份,一旦缓存数据有变更,会造成缓存数据不一致的问题。比如我们经常把一些活动数据存到本地缓存中,一旦活动数据被修改,则所有机器的本地内存无法全部感知到,会存在数据不一致的可能性。该问题主要有以下几种方法:
- 单节点数据变更之后主动通知其他节点。弊端是集群中的所有节点互相耦合,节点较多时,连接关系会比较复杂。
- 通过MQ方式通知其他节点。某个节点数据发生变更后通过消息广播的形式将数据变化通知到所有的客户端机器上,但是会将系统设计变得更复杂(为什么不直接使用分布式缓存呢?)
- 放弃数据实时一致性。定时任务定时拉取最新数据并更新内存缓存。这种方式目前应用比较广泛,之前的一些活动配置信息、包括conan-config均采用该方法。
第一种第二种方法会带来额外的复杂性,且集群的机器越多,数据的一致性越难以保证,因此,建议使用第三种方法。