设计模式
设计模式在实际代码中的使用示例
这些是本系列文章的参考资源
第二集延续了我们在这里开始的内容,并将讨论缓存写入技术。
请注意,
如果您正在寻找有关缓存的一般介绍和阅读技术,您可以访问此处
我完全明白你为什么惊讶。在读取技巧中我们已经提到了如何以及何时写入缓存层,那么为什么这里要采用不同的策略呢?
我们将那些真正与读取操作相关的技术称为读取技术。例如,获取交易列表。因此,即使我们已经执行了一些写入操作,但实际上我们执行写入操作只是为了完成读取操作。
所以,写入技巧本质上是在写入操作期间填充或更新缓存时使用的策略。它们最大的好处在于,当你之后要读取数据时。写入操作的示例包括:创建新事务、编辑用户信息等等。
正如另一篇文章中提到的,我们将讨论这些模式:
与上次一样,参与者如下:
正如读取(或内联缓存)一样,我们的资源管理器位于客户端和数据访问器之间。
此图展示了使用 Write Through 的写入操作的生命周期
步骤如下:
乍一看,这似乎并非明智之举:我们实际上增加了一个步骤,从而减慢了请求的速度。那么,这种策略究竟能给我们带来什么好处呢?
我们多次提到过,缓存数据最大的问题之一就是容易过期。而这个模式恰恰解决了这个问题。
在另一篇文章中,我们了解到处理过期条目的一种方法是使用TTL,这种方法仍然适用,但在这种情况下,过期是解决问题的最佳方法,因为我们并没有生成正在获取的数据。现在我们可以控制要读取的数据,然后每次写入数据时更新缓存,这将确保缓存条目永远不会过期。
当然,没有光明就没有阴影,除了写入延迟1之外,当客户端不需要频繁读取数据时,这种技术可能会变得有害。实际上,在这种情况下,你最终会浪费保持活动和同步缓存所需的资源,而无法获得读取的好处。
另一种技术仍然具有内联资源管理器,但通过数据访问器的写入是异步发生的。
这些是动作生命周期所涉及的步骤:
理解这种缓存技术为何以及如何有用的最好方法是举一个例子。
假设我们正在开发中TrulyAwesomeBankAPI
,希望Payment
使用缓存来实现交易创建。付款需要尽可能快地完成,但支持我们 API 的Truly Awesome Bank仍然采用老旧的基础设施,无法很好地应对峰值。
我们决定使用“后写”技术。这意味着每次执行交易时,Payment
我们都会将该交易保存在缓存中,并将响应返回给客户端。然后,我们还有另一个工作程序(在后台运行、在另一个进程中运行、基于 CRON 表达式或其他任何方式……)负责将缓存的账本版本与Truly Awesome Bank的真实账本同步。这样,无论Truly Awesome Bank在特定时间内能够支持多少请求,我们都可以快速提供响应。
由于无需等待外部数据源,我们的性能和稳定性也得到了提升。这使得整个架构对外部服务的容错能力更强,从而开启了新的弹性可能性:例如,我们可以实现简单的重试策略,甚至是熔断器,而完全不影响客户端……
然而,我们付出的代价是一致性:在工作人员完成同步过程之前,真实数据(如Truly Awesome Bank中的数据)和我们提供的数据(如缓存中的数据)是不同的,如果我们开始考虑如何处理错误情况2,事情会变得更加复杂。
好吧,为了完整起见,我们应该提一下Write Around,但在我看来,它看起来不像是一个真正的模式。事实上,在下图中你找不到任何“cache”这个词的踪迹。
基本上,Write Around是“直接调用数据访问器并仅在读取时缓存数据”,对我来说,这意味着“应用任何读取策略而不应用写入策略”。
您之所以使用这种非模式,仅仅是因为上述任何一种写作技巧都不适合您:也许您需要拥有超级一致的数据,或者也许您不需要经常读取数据。
在这些情况下,不采用写作技巧(或者如果您愿意,可以使用Write Around)也可以。
您可以在此处找到这些示例的更详细版本
是的,我做到了。这次用的是 Python。
我这里提供的示例是使用计时器模拟一个写入速度较慢的外部服务。具体来说,我们将大致模拟以下操作TrulyAmazingBankAPI
:创建一个想要保存的交易。
启动应用程序,几秒钟后您就可以准确地看到在写入和写入背后案例期间发生的事情的痕迹。
让我们逐一检查输出。
写虽然
>>> Save transaction
[14:59:17.971960] CacheManager.set
[14:59:17.971977] TrulyAwesomeBankAPIClient.save_transaction
>>> Get transaction
[14:59:19.974781] CacheManager.get
这里我们要做的第一件事是将条目保存在缓存中,然后将其保存在 AwesomeBank 中,当几秒钟后我们想要获取刚刚保存的交易时,我们使用缓存来检索它。
写在后面
>>> Save transaction
[14:59:24.976378] CacheManager.set
>>> Get transaction
[14:59:21.978355] CacheManager.get
--------------------------------------------
| AWESOME BANK DATABASE (before sync) |
--------------------------------------------
{}
[14:59:26.974325] TrulyAwesomeBankAPIClient.save_transaction
--------------------------------------------
| AWESOME BANK DATABASE (after sync) |
--------------------------------------------
{
UUID('0f41f108-0859-11e9-a138-b46bfc6c5cb9'): {
'id': UUID('0f41f108-0859-11e9-a138-b46bfc6c5cb9'),
'transaction': {
'type': 'PAYMENT',
'amount': 100,
'currency': 'EUR'
}
}
}
如果我们将请求称为“设置事务”和“获取事务”这两个操作,我们可以从输出中看到,在请求的整个生命周期中,唯一涉及的参与者是 CacheManager。
我们调用 TrulyAwesomeBankAPIClient 的唯一时刻是请求结束后 5 秒,此时我们完成同步。
请注意,由于计时器的存在,同步过程故意设计得笨重而缓慢。现实世界中的同步过程可能(通常情况下)比这复杂得多,事实上,当数据一致性至关重要时,同步应该成为一个主要关注点。
同步后,可以看到数据库已与缓存中的内容同步。从此时起,此条目将保持最新状态,直到发生其他写入操作。
好了,这就关闭了主动缓存部分。
首先,感谢您对上一篇文章的反馈!显然命名不太清晰,所以我在这里做了一些更新。我还借此机会重新审视了图表,这样它们就不会让您眼花缭乱了。至少不会太过分。
请继续反馈❤
直到下次!
1.值得一提的是,用户通常对写入延迟的容忍度远高于读取延迟。可惜我不记得这些数据是从哪里来的,所以无法提供实际的指标。对此持保留态度。
2.这些问题都与通常所说的“最终一致性”相关,这也是我在 Action 生命周期的最后一步使用了“最终”一词的原因。这个话题非常宏大,值得单独写一篇文章来阐述,但如果你真的想了解其中的奥秘,可以看看这篇文章。
鏂囩珷鏉ユ簮锛�https://dev.to/shikaan/-design-patterns-in-web-development----active-caching-2-37jc