title: "分布式缓存系统的设计" date: 2021-03-18T15:32:57+08:00 categories: [Cloud Native Computing] tags: [缓存, 分布式, redis]
很久不写技术文章了。这是一篇关于 Redis 构建分布式缓存系统的总结,结合之前项目上的使用场景,做一个系统性的梳理。
下面就以我做过的商品预约平台项目作为引子,引出分布式缓存设计的一些要点。
该商品预约平台的背景如下:
基于以上背景,这个预约系统的设计必须将性能作为主要优化目标,而缓存作为性能优化的不二选择,就承担了重要职责。
并不是所有数据都有必要被缓存,往往缓存的数据具有以下几个特点:
结合项目需要,排除掉一些不适合缓存的数据:
对于该预约项目,用户最频繁查询的数据是不同门店在不同日期下的库存数量。这类数据是缓存设计的重点照顾对象:
在即将完成业务系统开发时,我们就根据 Google SRE Books 提到的四个黄金指标,制定了监控系统性能的四个维度:
我们使用 Prometheus + Grafana 的组合实现监控可视化,这样每次测试人员进行压力测试时,都可以通过这些指标对系统进行调整。缓存影响最大的指标是请求率(一般用 TPS 或者 QPS)和响应时间。所以在设计缓存系统时,要不断参照这两个指标进行优化。
为了不让某一接口或者微服务的缓存失效导致其他接口或服务的并发量暴增,就要针对不同来源(数据库的表、接口等)的数据做分级缓存。比如用户在一次查询中涉及到“附近可预约门店”的查询、“活动期间不同日期剩余库存”的查询、“已预约数量“的查询,这三种查询逐层依赖后边的查询结果。
假设如果只针对库存数量做缓存,一旦这部分缓存失效,那么“附近可预约门店”的查询就会直接访问数据库查询全部门店的剩余库存来确定哪个门店可以预约。这样就导致查询库存的接口并发量骤增。所以分级缓存一定程度上缓解了缓存雪崩的问题。
我们的 QA 通常会写自动化脚本对后端 API 做定期的扫描,检查哪些接口的数据输入、输出有不合法的类型或是数值范围。除了巩固系统的健壮性,还能帮助缓存系统抵御缓存穿透的风险。
这是一个“先淘汰缓存"还是”先写数据库“的问题。通常没有明确的最佳方法。我们采用 Cache-Aside Pattern 的方式:
- 失效:应用程序先从 cache 取数据,没有得到,则从数据库中取数据,成功后,放到缓存中。
- 命中:应用程序从 cache 中取数据,取到后返回。
- 更新:先把数据存到数据库中,成功后,再让缓存失效。
缺点:可能有小概率脏数据。比如,一个是读操作,但是没有命中缓存,然后就到数据库中取数据,此时来了一个写操作,写完数据库后,让缓存失效,然后,之前的那个读操作再把老的数据放进去,所以,会造成脏数据。
考虑到写操作通常比读操作时间更长,所以 Cache-Aside Pattern 中的脏数据概率非常小,即便发生,用户在实际下单时系统仍然会去数据库里做数据校验,不会影响业务数据的正确性。
如果对缓存一致性有更高要求,可以采用延时双删策略或异步更新缓存。不过这两种方式本质都是用一定程度的串行化操作来解决并发造成的问题。
预加载热点数据时需要注意的点是,要考虑好服务一旦重启或是生产环境发生事故,要避免服务重启后再次造成二次事故。
首先要保证应用服务能做好熔断、限流、降级的措施。然后再针对不同情况做应对处理。
原因:热点缓存数据批量过期,导致大量缓存失效。
解决思路:
原因:热点 Key 突然过期。
解决思路:
原因:黑客通过访问缓存中不存在的数据,将大量请求直达数据库。
解决思路:
在设计缓存系统时优先排除掉大部分不需要缓存或者通过进程本地缓存的数据。搭建合理的监控手段,自动化测试框架,再结合预热、缓存淘汰策略、双写策略等最佳实践方法,不断优化缓存性能。
尤其要注意缓存的集中常见问题:雪崩、击穿和穿透。做好应用服务的熔断、降级、限流措施,保证在事故发生时针对每种情况都有预案。