Instagram 最近在他们的软件工程博客发布了一篇有关缓存值 promise 的概念的帖子,在缓存值未被命中情况下,需要耗费一定时间从底层的数据库管理系统中获取到未命中的缓存值,但这可能会导致数据管理系统拥塞。此外,如果多个并发的请求在请求获取缓存中一个不存在的值,触发多个工作实例从底层数据库获取该值并填充到缓存中,会引起数据库的拥塞(参考下图)。
在帖子中,Instagram 的 NickCooper 展示一种信号通知机制,当缓存正在准备某个值时,可以向其他的对该值请求发送通知,所以其他请求暂停对该值获取,避免直接访问底层数据库。
这不是什么新方法(它是读写缓存的特性之一,因为其对数据库透明的,开箱即用),但这个方法值得好好讨论一下,在这篇文章中,我将分享展示该方法的一个简单应用,将读写缓存的更多优势给发挥出来。
Redis
在详细介绍我的方法应用之前,简单介绍下如何通过 redis 来实现的我的目标。
支持多种语言,应用客户端只需要一个 redis 客户端库和需要使用 Redis 的 key 的名称。可以在不同程序和网络边界之间,同其他的客户端生成/解析 promises。
集群模式下扩展更高效,不需要通过轮询或者其他低效的模式,Redis 在此更能体现其独特的优势。
在避免无效的工作和保持可伸缩性之间有效地权衡。作为分布式系统的一部分,更强有力的保证需要更多的协作。
我的应用方案主要依赖 redis 的三个特性:key 过期(TTL),原子操作(SETNX)和 Pub/Sub;一般来说,我只是很好的利用了我在前面一篇帖子(https://redislabs.com/blog/what-to-choose-for-your-synchronous-and-asynchronous-communication-needs-redis-streams-redis-pub-sub-kafka-etc-best-approaches-synchronous-asynchronous-communication/)中提到的原则:共享状态有益于协作,反之亦然。
Redis 很适合用于状态共享,使用 Pub/Sub 消息机制实现一个锁,帮我构建一个跨网络 promise 机制。
内存锁的介绍
接下来,我将介绍 redis 内存锁模式的实现,它的工作模式如下:
一个服务应用实例,需要从 redis 中获取 key 值 foo, 如果获取到,就直接返回。
如果 foo 这个 key 值不存在,可以通过 SET NX 创建一个 key 值 lock:foo,NX 参数确保如果存在通过并发设置请求,只有一个请求能够设置键值成功(key 的 value 值在此处不重要)。
3.如果这个 key 值在缓存存在,我们获取到其对应的 value 值,完成后,接下来将其保存到 redis 中,并在叫做 notif:foo 的 Pub/Sub 通道中发布一条消息,通知所有等待该值的客户端,可以从缓存中获取到该值。
4.如果不能获取到对应的锁,此时只需订阅到 notif:foo 的 Pub/Sub 通道,等待被通知对应的缓存值已经存在。
实际上,这个算法稍微有点复杂,因此我们能很好的处理并发和超时(无过期的锁/Promises 在分布式环境中是无用的)。我们的命名的稍微有点复杂,因为需要为每个资源指定一个一个名称空间,以便多个独立的服务使用同一个集群时,不会面临键名冲突的风险,除此之外,解决这类问题不存在复杂性。
代码
你可以从 github 上获取相关示例代码(https://github.com/kristoff-it/redis-memolock),示例代码是基于 Go 语言实现的。
如果你在 5S 中内运行该程序的两个实例,无论第一个实例是否运行结束(即,参数值已被缓存),还是在继续运行,“Cachemiss!”只会显示一次。
为什么这比其他解决方案更好?
两大原因:
MemoLock 不仅可以保护 DBMS,还可以保护其他的昂贵的资源。
即使我们只想限制查询集缓存,CQRS 也会告诉我们,数据存储在 DBMS 中对制定的查询不一定是最有用的数据格式。对查询数据进行必要的转换应该是业务逻辑的一部分,除非在必要的条件下,对通过存储过程对每个请求都要重新转换下数据。
本文转载自 中间件小哥 公众号。
原文链接:https://mp.weixin.qq.com/s/WhFVFTHKNEIt9Jec34rB0A
评论