在分布式系统中,特别是最近很火的微服务架构下,有没有或者能不能总结出一个业务静态数据的通用缓存处理机制或方案,这篇文章将结合一些实际的研发经验,尝试理清其中存在的关键问题以及探寻通用的解决之道。前端
这里静态数据是指不常常发生变化或者变化频率比较低的数据,好比车型库、用户基本信息、车辆基本信息等,车型库这种可能每月会更新一次,用户和车辆基本信息的变化来源于用户注册、修改,这个操做的频率相对也是比较低的。数据库
另外这类数据的另外一个特色是要求准确率和实时性都比较高,不能出现丢失、错误,以及过长时间的陈旧读。缓存
具体是否是应该归类为静态数据要看具体的业务,以及对变化频率高低的划分标准。在这里的业务定义中,上边这几类数据都归为静态数据。网络
在面向用户或车联网的业务场景中,车型信息、用户基本信息和车辆基本信息有着普遍而高频的业务需求,不少数据都须要对其进行关联处理。在这里缓存的目的就是为了提升数据查询效率。静态数据一般都保存在关系型数据库中,这类数据库的IO效率广泛不高,应对高并发的查询每每捉襟见肘。使用缓存能够极大的提高读操做的吞吐量,特别是KV类的缓存,没有复杂的关系操做,时间复杂度通常都在O(1)。注意这里说的缓存指内存缓存。数据结构
固然除了使用缓存,还能够经过其它手段来提升IO吞吐量,好比读写分离,分库分表,可是这类面向关系型数据库的方案更倾向于同时提升读写效率,对于单纯提高读吞吐量的需求,这类方案不够完全,不能在有限的资源状况下发挥更好的做用。架构
下面将直接给出一个我认为的通用处理机制,而后会对其进行分析。并发
对于某个具体的业务,其涉及到六个核心程序:分布式
业务服务:提供对某种业务数据的操做接口,好比车辆服务,提供对车辆基本信息的增删改查服务。微服务
关系数据库:使用若干表持久化业务数据,好比SQLServer、MySQL、Oracle等。高并发
持久化队列:可独立部署的队列程序,支持数据持久化,好比RabbitMQ、RocketMQ、Kafka等。
缓存处理程序:从队列接收数据,而后写入缓存。
数据一致处理程序:负责检查缓存数据库和关系型数据库中数据是否一致,若是不一致则使用关系数据库进行更新。
缓存数据库(Redis):支持持久化的缓存数据库,这里直接选了Redis,这个基本是业界标准了。
Java架构交流学习圈:874811168 面向1-3年经验 Java开发人员 帮助突破瓶颈 提高思惟能力
以及两个外部定义:
数据生产者:业务静态数据的来源,能够理解为前端APP、Web系统的某个功能或者模块。
数据消费者:须要使用这些业务静态数据的服务或者系统,好比报警系统须要获取车辆对应的用户信息以便发送报警。
下面以问答的形式来讲明为何是这样一种机制。
既然是微服务架构,固然离不开服务了,由于这里探讨的是业务静态数据,因此是业务服务。不过为了更好的理解,这里仍是简单说下服务出现的缘由。
当今业务每每须要在多个终端进行使用,好比PC、手机、平板等,既有网页的形式,又有APP的形式,另外某个数据可能在多种不一样的业务被须要,若是将数据操做分布在多个程序中极可能产生数据不一致的状况,另外代码不可避免的冗余,读写性能更很难控制,变动也基本上是不敢变的。经过一个业务服务能够将对业务数据的操做有序的管理起来,并经过接口的形式对外提供操做能力,代码不用冗余了,性能也好优化了,数据不一致也获得了必定的控制,编写上层应用的人也舒服了。
不少开发语言都提供了进程内缓存的支持,即便没有提供直接操做缓存的包或库,也能够经过静态变量的方式来实现。对数据的查询请求直接在进程内存完成,效率能够说是杠杠滴了。可是进程内缓存存在两个问题:
缓存数据的大小:进程能够缓存数据的大小受限于系统可用内存,同时若是机器上部署了多个服务,某个服务使用了太多的内存,则可能会影响其它服务的正常访问,所以不适合大量数据的缓存。
缓存雪崩:缓存同时大量过时或者进程重启的状况下,可能产生大量的缓存穿透,过多的请求打到关系数据库上,可能致使关系数据库的崩溃,引起更大的不可用问题。
Redis这类数据库能够解决进程内缓存的两个问题:
独立部署,不影响其它业务,还能够作集群,内存扩容比较方便。
支持数据持久化,即便Redis重启了,缓存的数据自身就能够很快恢复。
另外Redis提供了很好的读写性能,以及方便的水平扩容能力,还支持多种经常使用数据结构,使用起来比较方便,能够说是通用缓存首选。
队列在这里的目的是为了解耦,坦白的说这个方案中能够没有队列,业务服务在关系数据库操做完成后,直接更新到缓存也是能够的。 之因此加上这个队列是因为当前的业务开发有很明显的系统拆分的需求,特别是在微服务架构下,为了下降服务之间的耦合,使用队列是个经常使用选择,在某些开发模型中也是很推崇的,好比Actor模型。
举个例子,好比新注册一个用户,须要赠送其300积分,同时还要给其发个注册成功的邮件,若是将注册用户、赠送积分、发成功邮件都写到一块儿执行,会产生两个问题:一是注册操做耗时增长,二是其中某个处理引起总体不可用的概率增大,三是程序的扩展性很差;通多引入队列,将注册信息分别发到积分队列和通知队列,而后由积分模块和通知模块分别处理,用户、积分、通知三个模块的耦合下降了,相互影响变小了,之后再增长注册后的其它处理也就是增长个队列的事,总体的扩展性获得了加强。
队列做为一种经常使用的解耦方案,在缓存这里虽然产生的影响不大,可是除了缓存不免同时还会有其它业务处理,因此为了统一处理机制,这里保留了下来。(既然用了,就把它发扬光大。)
持久化是为了解决网络抖动或者崩溃致使数据丢失的问题,在数据从业务服务到队列,队列自身处理,再从队列到缓存处理程序,中间均可能丢失数据。为了解决丢失数据的问题,须要发送时确认、队列自身持久化、接收时确认;可是须要注意确认机制可能会致使重复数据的产生,由于在未收到确认时就须要从新发送或接收,而数据实际上可能被正常处理,只是确认丢失了;确认机制还会下降队列的吞吐量,可是根据咱们的定义业务静态数据的变动频率应该不高,若是同时还须要较高的并发分片是个不错的选择。
这里持久化队列推荐选择RabbitMQ,虽然吞吐量支持的不是很大,可是各方面综合不错,并发够用就好。
在业务服务操做完关系数据库后,数据发送到队列以前(或者不用队列就是直接写入缓存以前),业务服务崩溃了,这时候数据就不能更新到缓存了。还有一种状况是Redis发生了故障转移,master中的更新没有同步到slaver。经过引入这么一个检查程序,定时的检查关系数据库数据和缓存数据的差异,若是缓存数据比较陈旧,则更新之。这样提供了一种极端状况下的挽救措施。
这个检查程序的运行频率须要综合考虑数据库压力和可以承受的数据陈旧时间,不能把数据库查死了,也不能陈旧过久致使大量数据不一致。能够经过设置上次检查时间点的方式,每次只检查从上次检查时间点(或者最近几回,防止Redis故障转移数据未同步的问题)到本次检查时间点发生变动的数据,这样每次检查只对增量变动,效率更高。
同时须要理解在分布式系统中,微服务架构下,数据不一致是常常出现的,必须在一致性和可用性之间作出权衡,尽力去下降影响,好比使用准实时或最终一致性。
假设没有缓存处理程序,经过定时同步关系数据库和缓存数据库是否是就够了呢?这仍是取决于业务,若是是车型库这种数据,增长一个新的车型,原本以前就没有,时间上并非很敏感,这个是能够的。可是对于新增了用户或者车辆,数据消费者仍是但愿可以立刻使用最新的数据进行处理,越快越好,这时使用同步或者准同步更新就能更加贴近需求。Java架构交流学习圈:874811168 面向1-3年经验 Java开发人员 帮助突破瓶颈 提高思惟能力
使用缓存过时机制能够不须要缓存处理程序和数据一致检查程序,业务服务首先从Redis查询数据,若是数据存在就直接返回,若是不存在则从关系数据库查询,而后写入Redis,而后再返回,这也是一种经常使用的缓存处理机制,网上能够查询到不少,不少人用的也很好。
可是缓存的过时时间是个问题:缓存多长时间过时,设置的短能够下降数据的陈旧,可是会增长缓存穿透的几率,即便采用随机的缓存过时时间,在Redis重启或者故障转移的状况下仍是会可能致使缓存雪崩,雪崩的状况下采用数据预热机制,也可能会致使服务更长时间的不可用;设置的长能够提高缓存的使用率,可是增长了数据陈旧,在上边对静态数据的定义中对其准确率和实时性都有较高的要求,业务上能不能接受须要考虑。并且若是操做数据和查询存在波动的峰谷,是否是要引入动态TTL的机制,以达到缓存使用和直接访问数据库的一种平衡,这就须要权衡业务需求和技术方案。
经过上边的这些问题问答,再来看看上面提出的微服务架构下静态数据通用缓存处理机制。
对于微服务架构而言,这个机制借助队列这种通用的解耦方式,独立了缓存更新处理,经过准实时更新和定时检查,保证了缓存的实时性和极端状况下较短期内达到最终一致,经过缓存的持久化机制消除了缓存穿透和雪崩,在缓存的数据较大或读取并发较高时支持水平扩容,能够认为对业务静态数据提供了一种普遍适用的缓存处理机制。
这个方案在某些状况下多是没有必要的,好比你要缓存一个全国限行的城市列表,使用一个进程内缓存就够了。
最后剩下的就是工做量的问题了,这个会给开发和维护带来复杂性,队列有没有用的顺手的,人手是否是够,业务需求是什么样的,须要考虑清楚。