以Redis为例,详谈分布式系统缓存的细枝末节

前言:html

在分布式Web程序设计中,解决高并发以及内部解耦的关键技术离不开缓存和队列,而缓存角色相似计算机硬件中CPU的各级缓存。现在的业务规模稍大的互联网项目,即便在最初beta版的开发上,都会进行预留设计。可是在诸多应用场景里,也带来了某些高成本的技术问题,须要细致权衡。数据库

本系列主要围绕分布式系统中服务端缓存相关技术,也会结合朋友间的探讨说起本身的思考细节。文中如有不妥之处,恳请指正。本文是本系列的第一篇,打算尽量详细地谈谈缓存自身的基础设计应用,以及相关的操做细节等(具体应用主要以Redis举例)。缓存

1、服务端数据缓存性能优化

1服务器

一种区分网络

缓存基于不一样的条件有不少种划分方式,本地缓存(Local cache)和分布式缓存(Distributed cache)是一种常见分类,二者自身又包含不少细类。数据结构

本地并非指程序所在本地服务器(从严格概念来讲),而是更细粒度的指位于程序自身的内部存储空间,而分布式更多强调的是存储在进程以外的一个或者多个服务器上,彼此交互通讯,在具体软件项目的设计和应用中,多数时候是混合一体。固然,我的认为对缓存本质的理解才是最重要的,至于概念上的分类只是一个不一样理解下的划分而已。架构

2并发

一些技术成本运维

在具体项目架构设计时,单纯使用前者(本地缓存)的开发成本毋庸置疑是极低的,主要考虑的是本机的内存负载或者极少许的磁盘I/O影响。然后者的设计初心是为了利于分布式程序之间缓存数据的高效共享和管理,除了考虑缓存所在服务器自身的内存负载,设计时更须要充分考虑网络I/O、CPU的负载,以及某些场景下的磁盘I/O的代价,同时还在具体设计时尽量规避和权衡总体稳定性和效率,这些不只仅只是做为缓存服务器的硬件成本和技术维护。须要谨慎考虑的底层问题包括缓存间通讯、网络负载和延迟等各类须要权衡的细节。

其实若是理解了缓存本质就该知道,任何存储介质在适当的场景下均可以充当一个高效的缓存角色并进行项目集成和缓存间集群。常见主流的Memcached和Redis等均是属于后者范畴,甚至能够包括如基于NoSQL设计的MongoDB这类文档数据库(但这是从角色角度讲,而狭义划分上这是基于磁盘的存储库,须要注意,各有专攻)。

这些第三方缓存在进行项目集成和缓存间集群,也须要解决一些问题。甚至项目迭代到了后期阶段,每每还须要具有较高专业知识的运维同时参与,而且在开发中的逻辑设计和代码实现也会增长必定的工做量。因此有时候在具体项目的设计上,一方面要尽量预留,一方面还得根据实际状况尽量精简。

额外说下其余体会:在我的有限的技术学习和实践里,关于节点数据交互,尤为是服务间通讯,是不存在完美的闭环的,理论上也都是在“当前阶段”面向“高一致”的权衡罢了(大概跟生活是同样的吧)。

2、缓存数据库结构的设计细节

因为目前我的工做中大多数状况应用的是Redis 3.x,如下如有特性关联,均是以此做为参照说明。

1

实例(Instance)

根据业务场景,公共数据和业务耦合数据,必定分别使用不一样的实例。若是是单实例,才能够考虑以DB划分。当你使用的是Redis,那么DB在Redis里是有数据隔离,但没有严格权限限制,因此划库只是一种选择。在Cluster集群里则是保持默认单个库,不过实际中我会尝试根据项目大小来调整,至于在哪一个开发阶段则是做为预留设计。

额外须要注意的是,做为重度依赖服务器内存的缓存产品,若是开启了持久化(后面会提到),而且在为并发量极大的服务提供支持时,服务器硬件资源会出现大量抢占,请结合持久策略配置,考虑实例是否进行分盘存储。

持久化本质是将内存数据同步写入硬盘(刷盘),而磁盘I/O实在有限,被迫的写入阻塞除了形成线程阻塞和服务超时,还会致使额外异常甚至波及其余底层依赖服务。固然,个人建议是,若是条件容许,最好是在项目初期设计时就进行规划并肯定。

2

缓存“表”(Table)

通常缓存中并无传统RDBMS中直观的表概念(每每以键值对“KV”形式存在),但从结构上来说,键值对自己就能够组装为各类表结构。通常我会先生成数据库表关系图,而后分析何时存储字符串,何时存储对象,而后使用缓存键(KEY)进行表和字段(列)分割。

假定须要存储一个登陆服务器表数据,包含字段(列):name、sign、addr,那么能够考虑将数据结构拆分为如下形式:

{ key : "server:name" , value : "xxxx" }

{ key : "server:sign" , value : "yyyy" }

{ key : "server:addr" , value : "zzzz" }

须要注意的是,每每在分布式缓存产品中,例如Redis,存在多种数据结构(如String、Hash等),还须要根据数据关联性和列的数量,来选择对应缓存的存储数据结构,相关存储空间和时间复杂度是彻底不一样的,而这个在初期阶段是很难感觉到的。

同时,就算缓存的内存设置的足够大,剩余也不少,也一样须要考虑相似RDBMS中的单表容量问题,控制条目数量不能无限增加(好比预知到存储条目能够轻松达到百万级),“分库分表”的设计思路都是相通的。

3

缓存键(Key)

上面提到了基于缓存键来设计表,这里再单独说明一下键相关的我的规范。在键长度足够简短的前提下,若是关联相同业务模块,则必须设计为以同一个标识(代号)开头,目的是方便查找和统计管理。

如用户登陆服务器列表:

{ key : "ul:server:a" , value : "xxxx" }

{ key : "ul:server:b" , value : "yyyy" }

另外,每一个独立业务系统可考虑配置一个惟一的通用前缀标识。固然,这里不是必需,若实际工做中,若是使用的是不一样库,则能够忽略。

4

缓存值(value)

缓存中的值(这里指单一条目)的大小没有平均标准,但Size天然是越小越好(若使用的是Redis,一次操做的value较大会直接影响整个Redis的响应时间,不只仅是指网络I/O)。若是存储占用空间直达10M+,建议考虑关联的业务场景是否能够拆分为热点和非热点数据。

5

持久化(Permanence)

上面也简单提了下,通常来讲,持久和缓存自己是没有直接关系的,能够粗略想象为一个面向硬盘一个面向内存。但现在的Web项目里,有些业务场景是高度依赖缓存的,持久化能够一方面帮助提升缓存服务重启后的快速恢复,另外一方面提供特定场景下的存储特性。固然,因为持久化必然须要牺牲一些性能,包括CPU的抢占和硬盘I/O影响。不过大多数时候是利大于弊,建议在应用缓存的时候,没有特别状况的话,尽可能搭配持久化,不管是使用自身机制仍是第三方来实现。

若是是使用的Redis,其自身就具有相关持久策略,包含AOF和RDB,我在大多数状况下是二者同时配置的(固然,最新官方版本自己也提供了混合模式)。若是在一些非高并发的场景下,或者说在一些中小项目的管理模块里,仅仅只是做为优化手段,肯定了不需持久,也能够直接设置关闭,节约性能开销损耗,但建议在程序中将该实例作好标注,确保该实例的公共使用范围。

6

淘汰(Eliminate)

缓存若是无限制的增加,即便设置了较短的过时(Expiration ),在一些时间点上,高并发的一批大数据会在较短期内就达到了可以使用内存的峰顶,此时程序中与缓存服务器的交互会出现大量延迟和错误,甚至给服务器自身都带来了严重的不稳定性。因此在生产环境里尽可能给缓存配置最大内存限制,以及适当的淘汰策略。

若是使用的是Redis,自身淘汰策略选择比较灵活。

我的的设计是,在数据呈现相似幂律分布状况下,总有大量数据访问较低,我会选择配置allkeys-lru、volatile-lru,将最少访问的数据进行淘汰。再好比缓存是做为日志应用的,那么我通常是项目前期是配置no-enviction,后期会配置为volatile-ttl。

固然,我也见过一种特殊业务下的设计,缓存直接用来做为轻量的持久数据库使用,并且是终端,开始以为有些新奇,后来发现是很是符合业务设计的(好比几乎没有任何复杂逻辑和强事务)。因此合情合理,确实不该该禁锢在传统设计里,毕竟架构老是基于业务去实时组合和改变的。

顺便在此给你们推荐一个Java架构方面的交流学习群:698581634,里面会分享一些资深架构师录制的视频资料:有Spring,MyBatis,Netty源码分析,高并发、高性能、分布式、微服务架构的原理,JVM性能优化这些成为架构师必备的知识体系,主要针对Java开发人员提高本身,突破瓶颈,相信你来学习,会有提高和收获。

3、一级缓存的基础CURD及相关

1

新增(Create)

若是没有特殊业务需求(如上面提到的),插入必须设置过时时间。同时,尽可能保证过时随机性。若是是进行批量缓存,则我的的作法是保证设置的过时时间上至少是分散的,目的是为了下降缓存雪崩等风险和影响(关于这些我会在之后的扩展篇里尝试阐述)。

如:批量缓存的对象是一个结果集,条目有10万条,缓存时间基础为 60*60*2(sec),如今须要同时进行缓存。个人作法是默认生成一个随机数,如random(范围 0 - 1000),过时时间则设置为( 60*60*2 + random ) 。

2

修改(Update)

更新一条缓存的数据,注意是否须要从新调整过时时间。同时在不少场合,如多个缓存间同步时,建议直接删除该缓存,而不是更新缓存。修改操做不少时候是关联到DB间的同步操做的,相对考究的多一些,须要权衡分布式事务上的问题,后续文章里会写到。

3

读取(Read)

查找缓存时,若是存在多条,并肯定数据量不大,务必使用严格匹配key的模式,而尽可能不要使用通配符方式。虽然发送指令的key数据变长了,但却避免了没必要要的缓存内的搜索性能损耗。

例如单纯相信Redis里自身的存储优化,无限制的使用 keys pattern而不考虑时间复杂度,同时形成大量线程阻塞(这里与主从复制无关)。若是折中使用scan分页替代,也并不是一种“无忧”的实现,一是须要在程序代码的封装里设置较低的容量,二是请务必在程序逻辑里对数据幻读等潜在问题作相关的管控处理。

另外能够额外类比一种场景,操做DB中的大表,命中的热点数据分布靠后。

4

删除/清空(Delete/Clear)

删除缓存,通常有直接移除和设置时间过时(并非任什么时候候都是滑动增长过时)两种方式,没什么细节上的说明。不过我却是听过一种特殊业务场合,批量请求同类数据,而且即时性没有很高要求,设置过时时间并将时间稍做分散。

清空缓存,我在项目里目前并未应用,甚至也不提倡直接使用。可是假如在应用时,须要慎重考虑两个地方:一是清理时机,二是清理时效(若在Redis里,不管是flushdb或者flushall,都会造成必定阻塞)

5

锁/信号(Locking)

自己无关缓存,属于一些并发特性实现,有必定的适用场景。这在Redis中有一些基于原子的实现,但与本系列讨论无关。

6

发布-订阅(Publish-Subscribe)

为何提到这个跟生产消费(Produce-Consume)相关的动做呢?这个机制自己是不属于缓存自身的范畴的,而是更相关于消息队列(Message Queue)。之因此提到,是由于现在主流的缓存产品都自带这一特性,不少场景使用起来较方便,配置也简单,效率也够快。只是,每每会形成滥用。最关键是没必要要的强耦合也下降了总体灵活性和性能,扩展性也实在有限。固然,这是我目前的见解。

个人建议是:若是没有特殊的场景应用,尽可能不使用。至少本人是不会优先推荐使用缓存自身的发布订阅的,甚至在缓存集群系统中,须要考究的细节更多。

而推荐的方式是,使用其余专业中间件解决,如基于MQ的产品替代方案。具体的候选有优秀的开源做品如RabbitMQ、Kafka等,包括有朋友提到的近两年国内阿里研发的RocketMQ等等,可是我的目前使用较多的依然是RabbitMQ。固然,这里不去过多赘述了,根据场景选择,合适的场景选用最合适的技术方案便可吧。

原文连接:www.cnblogs.com/bsfz/p/9568591.html

相关文章
相关标签/搜索