数据库缓存更新的套路( 二 )


Read Through
Read Through 套路就是在查询操作中更新缓存 , 也就是说 , 当缓存失效的时候(过期或LRU换出) , Cache Aside是由调用方负责把数据加载入缓存 , 而Read Through则用缓存服务自己来加载 , 从而对应用方是透明的 。
Write Through
Write Through 套路和Read Through相仿 , 不过是在更新数据时发生 。当有数据更新的时候 , 如果没有命中缓存 , 直接更新数据库 , 然后返回 。如果命中了缓存 , 则更新缓存 , 然后再由Cache自己更新数据库(这是一个同步操作)
下图自来Wikipedia的Cache词条 。其中的Memory你可以理解为就是我们例子里的数据库 。

数据库缓存更新的套路

文章插图
 
 
Write Behind Caching Pattern
Write Behind 又叫 Write Back 。一些了解linux操作系统内核的同学对write back应该非常熟悉 , 这不就是Linux文件系统的Page Cache的算法吗?是的 , 你看基础这玩意全都是相通的 。所以 , 基础很重要 , 我已经不是一次说过基础很重要这事了 。
Write Back套路 , 一句说就是 , 在更新数据的时候 , 只更新缓存 , 不更新数据库 , 而我们的缓存会异步地批量更新数据库 。这个设计的好处就是让数据的I/O操作飞快无比(因为直接操作内存嘛 ) , 因为异步 , write backg还可以合并对同一个数据的多次操作 , 所以性能的提高是相当可观的 。
但是 , 其带来的问题是 , 数据不是强一致性的 , 而且可能会丢失(我们知道Unix/Linux非正常关机会导致数据丢失 , 就是因为这个事) 。在软件设计上 , 我们基本上不可能做出一个没有缺陷的设计 , 就像算法设计中的时间换空间 , 空间换时间一个道理 , 有时候 , 强一致性和高性能 , 高可用和高性性是有冲突的 。软件设计从来都是取舍Trade-Off 。
另外 , Write Back实现逻辑比较复杂 , 因为他需要track有哪数据是被更新了的 , 需要刷到持久层上 。操作系统的write back会在仅当这个cache需要失效的时候 , 才会被真正持久起来 , 比如 , 内存不够了 , 或是进程退出了等情况 , 这又叫lazy write 。
在wikipedia上有一张write back的流程图 , 基本逻辑如下:
数据库缓存更新的套路

文章插图
 
 
 
再多唠叨一些
1)上面讲的这些Design Pattern , 其实并不是软件架构里的MySQL数据库和memcache/redis的更新策略 , 这些东西都是计算机体系结构里的设计 , 比如CPU的缓存 , 硬盘文件系统中的缓存 , 硬盘上的缓存 , 数据库中的缓存 。基本上来说 , 这些缓存更新的设计模式都是非常老古董的 , 而且历经长时间考验的策略 , 所以这也就是 , 工程学上所谓的Best Practice , 遵从就好了 。
2)有时候 , 我们觉得能做宏观的系统架构的人一定是很有经验的 , 其实 , 宏观系统架构中的很多设计都来源于这些微观的东西 。比如 , 云计算中的很多虚拟化技术的原理 , 和传统的虚拟内存不是很像么?Unix下的那些I/O模型 , 也放大到了架构里的同步异步的模型 , 还有Unix发明的管道不就是数据流式计算架构吗?TCP的好些设计也用在不同系统间的通讯中 , 仔细看看这些微观层面 , 你会发现有很多设计都非常精妙……所以 , 请允许我在这里放句观点鲜明的话——如果你要做好架构 , 首先你得把计算机体系结构以及很多老古董的基础技术吃透了 。
3)在软件开发或设计中 , 我非常建议在之前先去参考一下已有的设计和思路 , 看看相应的guideline , best practice或design pattern , 吃透了已有的这些东西 , 再决定是否要重新发明轮子 。千万不要似是而非地 , 想当然的做软件设计 。
4)上面 , 我们没有考虑缓存(Cache)和持久层(Repository)的整体事务的问题 。比如 , 更新Cache成功 , 更新数据库失败了怎么吗?或是反过来 。关于这个事 , 如果你需要强一致性 , 你需要使用“两阶段提交协议”——prepare, commit/rollback , 比如JAVA 7 的XAResource , 还有MySQL 5.7的 XA Transaction , 有些cache也支持XA , 比如EhCache 。当然 , XA这样的强一致性的玩法会导致性能下降 , 关于分布式的事务的相关话题 , 你可以看看《分布式系统的事务处理》一文 。


推荐阅读