微服务实战(五):微服务化之缓存的设计

原文连接:微服务化之缓存的设计(做者:刘超)redis

 

 

在高并发场景下,须要经过缓存来减小数据库的压力,使得大量的访问进来可以命中缓存,只有少许的须要到数据库层。因为缓存基于内存,可支持的并发量远远大于基于硬盘的数据库。因此对于高并发设计,缓存的设计时必不可少的一环。算法

1、为何要使用缓存

为何要使用缓存呢?源于人类的一个梦想,就是多快好省的建设社会主义。

多快好省?不少客户都这么要求,可是做为具体作技术的你,固然知道,好就不能快,多就无法省。

但是没办法,客户都这样要求:sql

  • 这个能不能便宜一点,你咋这么贵呀,你看人家都很便宜的。(您好,这种打折的房间比较靠里,是不能面向大海的)
  • 大家的性能怎么这么差啊,用你这个系统跑的这么慢,你看人家广告中说速度能达到多少多少。(您好,你若是买一个顶配的,咱们也是有这种性能的)
  • 大家服务不行啊,你就不能彬彬有礼,穿着整齐,送点水果瓜子啥的?(您好,咱们兰州拉面馆没有这项服务,能够去对面的俏江南看一下)
  • 这么贵的菜,一盘就这么一点点,都吃不饱,就不能上一大盘么。(您好,对面的兰州拉面10块钱一大碗)


怎么办呢?劳动人民仍是颇有智慧的,就是聚焦核心需求,让最最核心的部分享用好和快,而非核心的部门就多和省就能够了。

你能够大部分时间住在公司旁边的出租屋里面,可是出去度假的一个星期,选一个面朝大海,春暖花开的五星级酒店。

你能够大部分时间都挤地铁,挤公交,跋涉2个小时从北五环到南五环,可是有急事的时候,你能够打车,想旅游的时候,能够租车。

你能够大部分时间都吃普通的餐馆,而朋友来了,就去高级饭店里面搓一顿。

在计算机世界也是这样样子的,如图所示。数据库

01.jpg


越是快的设备,存储量越小,越贵,而越是慢的设备,存储量越大,越便宜。

对于一家电商来说,咱们既但愿存储愈来愈多的数据,由于数据未来就是资产,就是财富,只有有了数据,咱们才知道用户须要什么,同时又但愿当我想访问这些数据的时候,可以快速的获得,双十一拼的就是速度和用户体验,要让用户有流畅的感受。

因此咱们要讲大量的数据都保存下来,放在便宜的存储里面,同时将常常访问的,放在贵的,小的存储里面,固然贵的快的每每比较资源有限,于是不能长时间被某些数据长期霸占,因此要你们轮着用,因此叫缓存,也就是暂时存着。后端

2、都有哪些类型的缓存

当一个应用刚开始的时候,架构比较简单,每每就是一个Tomcat,后面跟着一个数据库。缓存

02.png


简单的应用,并发量不大的时候,固然没有问题。

然而数据库至关于咱们应用的中军大账,是咱们整个架构中最最关键的一部分,也是最不能挂,也最不能会被攻破的一部分,于是全部对数据库的访问都须要一道屏障来进行保护,经常使用的就是缓存。

咱们以Tomcat为分界线,以外咱们称为接入层,接入层固然应该有缓存,还有CDN,这个在这篇文章中有详细的描述:《微服务的接入层设计与动静资源隔离》。

Tomcat以后,咱们称为应用层,应用层也应该有缓存,这是咱们这一节讨论的重点。

最简单的方式就是Tomcat里面有一层缓存,常称为本地缓存LocalCache。

这类的缓存常见的有Ehcache和Guava Cache,因为这类缓存在Tomcat本地,于是访问速度是很是快的。

可是本地缓存有个比较大的缺点,就是缓存是放在JVM里面的,会面临Full GC的问题,一旦出现了FullGC,就会对应用的性能和相应时间产生影响,固然也能够尝试jemalloc的分配方式。

还有一种方式,就是在Tomcat和Mysql中间加了一层Cache,咱们常称为分布式缓存。数据结构

03.png


分布式缓存常见的有Memcached和Redis,二者各有优缺点。

Memcached适合作简单的key-value存储,内存使用率比较高,并且因为是多核处理,对于比较大的数据,性能较好。

可是缺点也比较明显,Memcached严格来说没有集群机制,横向扩展彻底靠客户端来实现。另外Memcached没法持久化,一旦挂了数据就都丢失了,若是想实现高可用,也是须要客户端进行双写才能够。

因此能够看出Memcached真的是设计出来,简简单单为了作一个缓存的。架构

04.png


Redis的数据结构就丰富的多了,单线程的处理全部的请求,对于比较大的数据,性能稍微差一点。并发

05.jpg


Redis提供持久化的功能,包括RDB的全量持久化,或者AOF的增量持久化,从而使得Redis挂了,数据是有机会恢复的。

Redis提供成熟的主备同步,故障切换的功能,从而保证了高可用性。

因此不少地方管Redis称为内存数据库,由于他的一些特性已经有了数据库的影子。

这也是不少人愿意用Redis的缘由,集合了缓存和数据库的优点,可是每每会滥用这些优点,从而忽略了架构层面的设计,使得Redis集群有很大的风险。

不少状况下,会将Redis当作数据库使用,开启持久化和主备同步机制,觉得就能够高枕无忧了。负载均衡

06.png


然而Redis的持久化机制,全量持久化则每每须要额外较大的内存,而在高并发场景下,内存原本就很紧张,若是形成swap,就会影响性能。增量持久化也涉及到写磁盘和fsync,也是会拖慢处理的速度,在平时还好,若是高并发场景下,仍然会影响吞吐量。

因此在架构设计角度,缓存就是缓存,要意识到数据会随时丢失的,要意识到缓存的存着的目的是拦截到数据库的请求。若是为了保证缓存的数据不丢失,从而影响了缓存的吞吐量,甚至稳定性,让缓存响应不过来,甚至挂掉,全部的请求击穿到数据库,就是更加严重的事情了。

若是很是须要进行持久化,能够考虑使用levelDB此类的,对于随机写入性能较好的key-value持久化存储,这样只有部分的确须要持久化的数据,才进行持久化,而非不管什么数据,统统往Redis里面扔,同时统一开启了持久化。

3、基于缓存的架构设计要点

因此基于缓存的设计:

一、多层次

这样某一层的缓存挂了,还有另外一层能够撑着,等待缓存的修复,例如分布式缓存由于某种缘由挂了,由于持久化的缘由,同步机制的缘由,内存过大的缘由等,修复须要一段时间,在这段时间内,至少本地缓存能够抗一阵,不至于一会儿就击穿数据库。并且对于特别特别热的数据,热到致使集中式的缓存处理不过来,网卡也被打满的状况,因为本地缓存不须要远程调用,也是分布在应用层的,能够缓解这种问题。

二、分场景

到底要解决什么问题,能够选择不一样的缓存。是要存储大的无格式的数据,仍是要存储小的有格式的数据,仍是要存储必定须要持久化的数据。具体的场景下一节详细谈。

三、要分片

使得每个缓存实例都不大,可是实例数目比较多,这样一方面能够实现负载均衡,防止单个实例称为瓶颈或者热点,另外一方面若是一个实例挂了,影响面会小不少,高可用性大大加强。分片的机制能够在客户端实现,可使用中间件实现,也可使用Redis的Cluster的方式,分片的算法每每都是哈希取模,或者一致性哈希。

4、缓存的使用场景

当你的应用扛不住,知道要使用缓存了,应该怎么作呢?

场景1:和数据库中的数据结构保持一致,原样缓存

这种场景是最多见的场景,也是不少架构使用缓存的适合,最早涉及到的场景。

基本就是数据库里面啥样,我缓存也啥样,数据库里面有商品信息,缓存里面也放商品信息,惟一不一样的是,数据库里面是全量的商品信息,缓存里面是最热的商品信息。

每当应用要查询商品信息的时候,先查缓存,缓存没有就查数据库,查出来的结果放入缓存,从而下次就查到了。

这个是缓存最最经典的更新流程。这种方式简单,直观,不少缓存的库都默认支持这种方式。

场景2:列表排序分页场景的缓存

有时候咱们须要得到一些列表数据,并对这些数据进行排序和分页。

例如咱们想获取点赞最多的评论,或者最新的评论,而后列出来,一页一页的翻下去。

在这种状况下,缓存里面的数据结构和数据库里面彻底不同。

若是彻底使用数据库进行实现,则按照某种条件将全部的行查询出来,而后按照某个字段进行排序,而后进行分页,一页一页的展现。

可是当数据量比较大的时候,这种方式每每成为瓶颈,首先涉及的数据库行数比较多,并且排序也是个很慢的活,尽管可能有索引,分页也是翻页到最后,越是慢。

在缓存里面,就不必每行一个key了,而是可使用Redis的列表方式进行存储,固然列表的长短是有限制的,确定放不下数据库里面这么多,可是你们会发现其实对于全部的列表,用户每每没有耐心看个十页八页的,例如百度上搜个东西,也是有排序和分页的,可是你每次都日后翻了吗,每页就十条,就算是十页,或者一百页,也就一千条数据,若是保持ID的话,彻底放的下。

若是已经排好序,放在Redis里面,那取出列表,翻页就很是快了。

能够后台有一个线程,异步的初始化和刷新缓存,在缓存里面保存一个时间戳,当有更新的时候,刷新时间戳,异步任务发现时间戳改变了,就刷新缓存。

场景3:计数缓存

计数对于数据库来说,是一个很是繁重的工做,须要查询大量的行,最后得出计数的结论,当数据改变的时候,须要从新刷一遍,很是影响性能。

所以能够有一个计数服务,后端是一个缓存,将计数做为结果放在缓存里面,当数据有改变的时候,调用计数服务增长或者减小计数,而非经过异步数据库count来更新缓存。

计数服务可使用Redis进行单个计数,或者hash表进行批量计数

场景4:重构维度缓存

有时候数据库里面保持的数据的维度是为了写入方便,而非为了查询方便的,然而同时查询过程,也须要处理高并发,于是须要为了查询方便,将数据从新以另外一个维度存储一遍,或者说将多给数据库的内容聚合一下,再存储一遍,从而不用每次查询的时候都从新聚合,若是仍是放在数据库,比较难维护,放在缓存就好一些。

例如一个商品的全部的帖子和帖子的用户,以及一个用户发表过的全部的帖子就是属于两个维度。

这须要写入一个维度的时候,同时异步通知,更新缓存中的另外一个维度。

在这种场景下,数据量相对比较大,于是单纯用内存缓存Memcached或者redis难以支撑,每每会选择使用levelDB进行存储,若是levelDB的性能跟不上,能够考虑在levelDB以前,再来一层Memcached。

场景5:较大的详情内容数据缓存

对于评论的详情,或者帖子的详细内容,属于非结构化的,并且内容比较大,于是使用Memcached比较好。

5、缓存三大矛盾问题

一、缓存实时性和一致性问题:当有了写入后咋办?

虽然使用了缓存,你们内心都有一个预期,就是实时性和一致性得不到彻底的保证,毕竟数据保存了多份,数据库一份,缓存中一份,当数据库中因写入而产生了新的数据,每每缓存是不会和数据库操做放在一个事务里面的,如何将新的数据更新到缓存里面,何时更新到缓存里面,不一样的策略不同。

从用户体验角度,固然是越实时越好,用户体验越流畅,彻底从这个角度出发,就应该有了写入,立刻废弃缓存,触发一次数据库的读取,从而更新缓存。可是这和第三个问题,高并发就矛盾了,若是全部的都实时从数据库里面读取,高并发场景下,数据库每每受不了。

二、缓存的穿透问题:当没有读到咋办?

为何会出现缓存读取不到的状况呢?

第一:可能读取的是冷数据,原来历来没有访问过,因此须要到数据库里面查询一下,而后放入缓存,再返回给客户。

第二:可能数据由于有了写入,被实时的从缓存中删除了,就如第一个问题中描述的那样,为了保证明时性,当数据库中的数据更新了以后,立刻删除缓存中的数据,致使这个时候的读取读不到,须要到数据库里面查询后,放入缓存,再返回给客户。

第三:多是缓存实效了,每一个缓存数据都会有实效时间,过了一段时间没有被访问,就会失效,这个时候数据就访问不到了,须要访问数据库后,再放入缓存。

第四:数据被换出,因为缓存内存是有限的,当使用快满了的时候,就会使用相似LRU策略,将不常用的数据换出,因此也要访问数据库。

第五:后端确实也没有,应用访问缓存没有,因而查询数据库,结果数据库里面也没有,只好返回客户为空,可是尴尬的是,每次出现这种状况的时候,都会面临着一次数据库的访问,纯属浪费资源,经常使用的方法是,讲这个key对应的结果为空的事实也进行缓存,这样缓存能够命中,可是命中后告诉客户端没有,减小了数据库的压力。

不管哪一种缘由致使的读取缓存读不到的状况,该怎么办?是个策略问题。

一种是同步访问数据库后,放入缓存,再返回给客户,这样实时性最好,可是给数据库的压力也最大。

另外一种方式就是异步的访问数据库,暂且返回客户一个fallback值,而后同时触发一个异步更新,这样下次就有了,这样数据库压力小不少,可是用户就访问不到实时的数据了。

三、缓存对数据库高并发访问:都来访问数据库咋办?

咱们原本使用缓存,是来拦截直接访问数据库请求的,从而保证数据库大本营永远处于健康的状态。可是若是一遇到不命中,就访问数据库的话,平时没有什么问题,可是大促状况下,数据库是受不了的。

一种状况是多个客户端,并发状态下,都不命中了,因而并发的都来访问数据库,其实只须要访问一次就好,这种状况能够经过加锁,只有一个到后端来实现。

另外就是即使采起了上述的策略,依然并发量很是大,后端的数据库依然受不了,则须要经过下降实时性,将缓存拦在数据库前面,暂且撑住,来解决。

6、解决缓存三大矛盾的刷新策略

一、实时策略

所谓的实时策略,是平时缓存使用的最经常使用的策略,也是保持实时性最好的策略。

读取的过程,应用程序先从cache取数据,没有获得,则从数据库中取数据,成功后,放到缓存中。若是命中,应用程序从cache中取数据,取到后返回。

写入的过程,把数据存到数据库中,成功后,再让缓存失效,失效后下次读取的时候,会被写入缓存。那为何不直接写缓存呢?由于若是两个线程同时更新数据库,一个将数据库改成10,一个将数据库改成20,数据库有本身的事务机制,能够保证若是20是后提交的,数据库里面改成20,可是回过头来写入缓存的时候就没有事务了,若是改成20的线程先更新缓存,改成10的线程后更新缓存,因而就会长时间出现缓存中是10,可是数据库中是20的现象。

这种方式实时性好,用户体验好,是默认应该使用的策略。

二、异步策略

所谓异步策略,就是当读取的时候读不到的时候,不直接访问数据库,而是返回一个fallback数据,而后往消息队列里面放入一个数据加载的事件,在背后有一个任务,收到事件后,会异步的读取数据库,因为有队列的做用,能够实现消峰,缓冲对数据库的访问,甚至能够将多个队列中的任务合并请求,合并更新缓存,提升了效率。

当更新的时候,异步策略老是先更新数据库和缓存中的一个,而后异步的更新另外一个。

一是先更新数据库,而后异步更新缓存。当数据库更新后,一样生成一个异步消息,放入消息队列中,等待背后的任务经过消息进行缓存更新,一样能够实现消峰和任务合并。缺点就是实时性比较差,估计要过一段时间才能看到更新,好处是数据持久性能够获得保证。

一是先更新缓存,而后异步更新数据库。这种方式读取和写入都用缓存,将缓存彻底挡在了数据库的前面,把缓存当成了数据库在用。因此通常会使用有持久化机制和主备的redis,可是仍然不能保证缓存不丢数据,因此这种状况适用于并发量大,可是数据没有那么关键的状况,好处是实时性好。

在实时策略扛不住大促的时候,能够根据场景,切换到上面的两种模式的一个,算是降级策略。

三、定时策略

若是并发量实在太大,数据量也大的状况,异步都难以知足,能够降级为定时刷新的策略,这种状况下,应用只访问缓存,不访问数据库,更新频率也不高,并且用户要求也不高,例如详情,评论等。这种状况下,因为数据量比较大,建议将一整块数据拆分红几部分进行缓存,并且区分更新频繁的和不频繁的,这样不用每次更新的时候,全部的都更新,只更新一部分。而且缓存的时候,能够进行数据的预整合,由于实时性不高,读取预整合的数据更快。有关缓存就说到这里,下一节讲分布式事务。

相关文章
相关标签/搜索