前两篇文章中,我们介绍了进程内缓存与缓存服务器的选取。 今天我们来介绍一下缓存架构的常用实现方式。
常见的缓存架构主要有两种: 1. 旁路型缓存 2. 穿透型缓存
穿透型缓存的设计原则是将缓存与后端数据库的交互细节对应用层服务隐藏。 应用层服务所有的读写请求均请求缓存,读请求 miss 后,缓存向后端数据服务器请求数据,先更新缓存后返回。 而写请求也是同样的,先写入缓存服务器,后同步给后端服务器。
这样的架构存在一个问题,那就是原子性问题,如果不能保证读写的原子性,就无法保证数据的一致性。 在读写并发的环境中,读请求发生 miss,此时缓存服务器向后端服务器请求数据并写入缓存,但在写入缓存前,如果发生了一个完整的写请求,那么就会出现这个写请求写入的新缓存被读请求获取的旧数据覆盖的问题。
另一个让这套缓存架构没能成为常用架构的原因是实现的复杂度。 开发人员必须将代码分散于业务层与存储层,这给代码的开发和维护带来很高的复杂度。 但如果使用原生支持穿透型缓存的缓存服务器,这无疑也是一种实现成本很低的架构。
大部分业务场景是“一写多读”的场景,在这样的场景下,旁路型缓存是非常适用的。
上图展示了旁路型缓存的读 miss 情况的处理: 1. 应用服务器先请求缓存服务器,如果数据存在则直接返回 2. 如果缓存 miss,则应用服务器请求后端数据库 3. 应用服务器将后端数据库返回的数据更新到缓存服务器
对于写请求,这个模式要求所有的数据更新都需要删除缓存中对应的数据,官方建议旁路型缓存的设计原则是先操作后端数据库后操作缓存。
如果我们不是简单地删除数据而是试图去更新缓存中的数据,那么可能存在下面两个问题: 1. 一致性 — 如果先 set 缓存后写数据库,由于二者不能保证事务性,可能存在 set 缓存成功,写数据库失败,造成二者数据不一致,或并发情况下新数据被旧数据覆盖的问题 2. 复杂度 — 解决上述一致性问题以及在复杂的业务场景下,如何组织差异化数据进行更新操作是一项非常复杂的工作
而简单暴力的淘汰被更新数据显然复杂度是最低的。
同时如果是先操作淘汰缓存后写入后端数据库后,在读写并发环境下,会出现读请求 miss,请求后端服务器,在写入缓存前,写请求更新后端服务器,此时就会出现数据不一致的情况。 如果先操作数据库后操作缓存就可以保证数据的并发安全性。 但是仍然存在修改数据库成功,淘汰缓存失败造成不一致的可能。
https://docs.microsoft.com/en-us/azure/architecture/patterns/cache-aside。