导语
缓存由于高并发和高性能的特性,经常被用于提高数据库的性能。但是业务往往需要花费大量精力来维护缓存和数据库的一致性。由IEG技术运营部/存储与计算资源中心 & 腾讯云数据库团队联合打造的 TendisX 冷热混合存储, 使用 Redis 作为缓存, Tendisplus 作为后端持久化数据库,并且自动将热数据加载到缓存,冷数据从缓存侧淘汰。业务侧使用完全透明,无需考虑缓存不一致的问题,从而更方便业务开发。
本文首先介绍由 IEG 技术运营部团队 & 腾讯云数据库团队开发的TendisX 冷热混合存储方案的整体架构,然后分析常用的解决缓存和数据库一致的方案存在的一些问题,最后从缓存一致性、击穿、雪崩、穿透、故障恢复、性能、业务接入复杂度等方面对这几个方案进行对比。
背景
在数据高并发读写场景, 数据库大多情况无法满足业务要求。 Redis 由于数据结构简单,读取速度快, 常常作为后端数据库的缓存使用。但是在使用缓存的时候,业务需要考虑: 1)是先写缓存还是先写数据库? 2)写请求是否更新缓存?等等问题。
TendisX 冷热混合存储方案
TendisX 是 IEG 技术运营部自研,主要解决 Redis 内存占用高和缓存不一致问题的 NoSQL 存储系统。
TendisX 冷热混合存储架构核心组件由 Proxy 、缓存Redis、存储Tendisplus组成,其中每个组件的功能介绍如下:
Proxy组件:负责对客户端请求进行路由分发,将不同的Key的命令分发到正确的分片,同时Proxy还负责了部分监控数据的采集,以及高危命令在线禁用等功能。
缓存Redis:缓存Redis组件基于 Redis 4.0 进行开发。Redis 具有以下功能:缓存全量的 Key;仅淘汰冷 Key 的 Value; 自动从 Tendisplus 恢复数据; Redis 限速。
存储Tendisplus:Tendisplus 是腾讯基于 RocksDB 自研的 兼容 Redis 协议的KV存储引擎,该引擎已经在腾讯集团内部运营多年,性能和稳定性得到了充分的验证。在混合存储系统中主要负责全量数据的存储和读取,以及数据备份,增量日志备份等功能。
缓存与数据库一致性的常规解决方案
在使用缓存的过程中往往会由于不正确的使用方式,导致缓存和后端数据库数据不一致,本章将介绍缓存的常见使用方式以及业界的解决缓存和数据库不一致的方案。
缓存使用方式
缓存作为一种广泛使用的技术, 常见的主要有以下三种使用模式: Cache Aside Pattern, Cache Through Pattern 和 Cache Back Pattern(具体介绍可以参考这篇文章Things You Should Know About Database Caching)。Cache Aside Pattern 是最常用的模式,Facebook 在 Scaling MemCache at Facebook 论文中也使用这种缓存模式。Cache Aside Pattern 采用先写数据库,再删除缓存,然而在某些情况也可能导致缓存和数据库的不一致。
在缓存+数据库的架构中,缓存和数据库的操作总共有以下几种顺序:
- 先更新缓存,再更新数据库
业务方无法保证两个写操作都成功。当更新缓存成功,更新数据库失败时,数据库中就是旧数据。 - 先更新数据库,再更新缓存
在多线程并发时,会出现不一致行为。假设请求 A 先操作数据库,请求 B 后操作数据库,但是可能存在请求 B 先写缓存,请求 A 后写缓存的情况,从而导致数据库与缓存之间的数据不一致。 - 先写数据库,再删除缓存
业务要保证写数据库和删除缓存是一个原子操作,否则写数据成功,删除缓存失败就会出现数据不一致的问题。 - 先删缓存, 再写数据库
先删除缓存,再写数据库前,可能另外一个读请求, 在缓存未命中时, 从数据库获取到旧值,将其放到缓存。 这时缓存中是旧值,数据库是新值。
通过上面分析可以得出,业务方无论是先写缓存还是先写数据库,最终都有可能出现缓存和数据库的不一致。那到底如何才能使缓存和数据库达到一致的状态。针对这个问题, 业界通常使用下面两种解决方案: 1)采用延时双删策略 2)异步更新缓存策略 。
采用延时双删策略
延时双删除策略会在写库前后删除缓存中数据,并且给缓存数据设置合理的过期时间, 从而可以保证最终一致性。写流程具体如下:
- 先删除缓存,再写数据库
- 休眠一段时间(比如500毫秒)
- 再次删除缓存
为什么会休眠一段时间,这里主要是防止: 在写请求删除缓存但还未成功写入数据库后,读请求可能将旧值加载到缓存。
读流程:先读缓存,当缓存未命中,再从数据库中读取,然后再写入缓存。
延时双删策略虽然解决了上述讨论的缓存和数据库不一致的问题,但是以下问题:
- 休眠一段时间可能会对性能造成影响;
- 在第二次删除缓存失败后,会导致数据不一致,需要业务方实现重试删除机制。
异步更新缓存策略(基于订阅binlog的同步机制)
异步更新缓存策略通常会使用一个异步同步组件(比如 canal 或者我司的 MySync), 通过解析从库的 binlog 获取数据,并通过消息队列将其串行化写入到缓存。
写流程:
- 先删除缓存,再写数据库
- 额外组件通过解析从库 binlog, 将写操作发送到消息队列
- 缓存从消息队列中消费,更新缓存
读流程:
- 先从缓存读取
- 如果缓存未命中, 从数据库读取, 将数据发送到消息队列。
- 缓存从消息队列消费,更新缓存。
异步更新缓存策略通过消息队列的方式将并行化的操作串行化,从而解决了并发问题。但是也引入一些其他问题,比如缓存相对主库落后延迟较大。
TendisX 混合存储方案
TendisX 中, 业务仅仅可以访问 Redis, 所有的数据恢复工作对业务不可见。因此业务可以简单的将其当做一个Redis 服务来用,而不用考虑各种缓存的问题。
Redis 与社区版不同的地方:
- **引入版本号 每次数据更新操作都会将 version++, 每个 Key 都会保存对应的 Version, 从而简单实现 缓存层和存储层复制幂等。
- Redis 中缓存全量的 Key 触发淘汰时,仅淘汰其对应的 Value。
- Redis 限速 为了避免后端数据库(Tendisplus) 落后缓存太多。当缓存中有大量的未持久化到Tendisplus 的数据时,阻塞用户写。
- 增量 RDB 在 slave 断线重连后, master 仅将断线内新增的数据发给 slave
写请求流程如下:
- 写入 Redis 缓存,成功后返回。
- 后台异步回刷,将数据持久化到后端 Tendisplus。
读请求的处理流程:
- 从 Redis 读取数据 。
- 如果命中,那么就将数据返回给应用程序。
- 如果未命中,阻塞当前客户端, Redis 负责将 Key 从 Tendisplus 中恢复,然后返回给用户。
方案对比
下面主要从缓存问题、主备切换场景、性能以及业务接入复杂度方面,对比 TendisX 冷热存储和延时双删策略、异步刷新策略。
缓存问题
TendisX 在使用过程中不会出现缓存穿透、缓存击穿、缓存雪崩和缓存不一致等问题。但是延时双删策略和异步刷新策略要看业务方如何处理,解决这些问题。
缓存穿透
缓存穿透是指查询数据库不存在的数据。当查询缓存和数据库都没有的 Key 时,由于 TendisX 中 redis 缓存用户的所有 key, 因此可以直接通过 redis 判断该 key 是否存在,不会有缓存穿透的问题。
缓存击穿
如果某些 Key 可能会在某些时间点被超高并发地访问,则它是一种非常“热点”的数据。缓存击穿是指大量的请求同时查询一个失效的热点数据,导致请求全部转发到数据库。
当某个热点的 Key 失效时,当下次具有大量该 Key 的请求时,TendisX 执行流程如下:
- 阻塞当前 Key 的请求
- 从后端存储恢复数据
- 在Key 对应的Value 还未恢复的过程中, 如果有其他请求继续获取该 Key, 同样阻塞该请求
- 当key 从后端恢复后,依次唤醒该 Key 对应阻塞的请求。
缓存雪崩
缓存雪崩是指某一时刻发生大规模的缓存失效的情况,比如缓存故障或者缓存采用相同的过期时间,缓存在某一时刻同时失效, 请求全部转发给数据库。
TendisX 可以利用 Redis 集群模式实现高可用,即使缓存故障,也可以及时通过备缓存恢复服务。另外 TendisX 的用户请求统一由缓存服务,请求不会转发给后端数据库。
缓存一致性
延时双删策略
- 延时双删策略通过休眠一段时间,再次删除缓存来解决缓存不一致的问题。但是由于每次写都要睡眠一段时间, 写操作耗时较长,从而大大降低系统的吞吐量。
在第二次删除缓存失败后,缓存和数据库不一致。为了解决这个问题需要业务在失败后提供重试删除机制,大大增加了业务开发成本。
异步更新缓存策略
通过解析从库的 binlog ,再通过消息队列串行化发给缓存, 缓存较主库延迟较大。比如大概率会出现业务写入主库更新数据,缓存还未更新,业务从缓存中得到旧值。
TendisX 混合存储
对于并发读写,经常会出现缓存不一致的场景:
请求 A 进行写操作
请求 B 进行读操作
redis 是单线程串行化执行用户的请求, 当写操作先执行。
如果需要从 Tendisplus 中恢复数据,则请求 A 和 B 都阻塞。当数据恢复后,再唤醒阻塞的请求 A 和 B, 缓存和后端 Tendisplus 最终是一致的。
如果不需要从 Tendisplus 中恢复数据,则请求 A 和 B 肯定是顺序执行的,不会出现不一致的行为。
主备故障场景分析
当 TendisX 中 redis master 故障时, 后端的 Tendisplus 可以切到对应的 redis slave。主备之间通过异步复制,可能会造成部分数据丢失。 TendisX 在 redis 主备和 redis 和 Tendisplus 同步的时候提供了限速,也就是说 slave 最多比 master 慢 2秒的数据(业务方在最差情况下丢失的数据)。
当 TendisX 中后端 Tendisplus 主故障时, Tendisplus 备可以切换到对应的 redis master,从 redis master 继续同步数据,数据不会丢失。
对于延时双删策略和异步刷新策略
如果缓存故障,并且假设缓存使用 redis, 也可以使用 redis 自动故障切换。但是由于是先写数据库,不会有数据丢失。
如果数据库故障,如果数据库主备间使用异步复制,也会有一定的数据丢失。
数据可容忍的丢失程度。
性能分析
延时双删策略和异步刷新策略的劣势:
- 延时双删策略,第二次删除前要等待一段时间,大大降低了系统的吞吐量(虽然可以通过一些异步删除的方法来改善,增加业务方的使用难度)。
- 延时双删策略和异步刷新策略每次更新数据库前,都要删除缓存。下次读请求都要从后端加载数据,大大影响了性能。
- 延时双删策略和异步刷新策略每次都会写到后端数据库,写入流程耗时较长。
TendisX的一些性能优势:
- TendisX 写入仅仅是一次内存操作,响应时间短。
- TendisX 中 redis 缓存所有 key, 对于一些需要全量 Key的操作不用扫描后端数据库,大大缩短了时间。
- TendisX 每次直接写入到缓存,后续的请求都可以通过缓存提供,而不是从后端再次读取。
业务接入复杂度
延时双删策略需要业务同时操作缓存和数据库,同时业务方也要保证第二次删除缓存失败后的重试,对业务来说使用比较麻烦,不友好。
业务方可以将 TendisX 当做一个 Redis 缓存来用,不用担心各种缓存问题。
总结对比
下面通过一个表来直观的对比 TendisX 冷热混合存储和另外两种策略: