当前位置: 首页 > 科技观察

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

时间:2023-03-12 20:58:57 科技观察

在分布式web编程中,解决高并发和内部解耦的关键技术离不开缓存和队列,缓存的作用类似于计算机硬件中的CPU。各级缓存。现在业务规模稍大的互联网项目,即使在最初的beta版本开发中,也会保留设计。但在很多应用场景中,也带来了一些高成本的技术问题,需要慎重权衡。本系列主要关注分布式系统中服务端缓存相关技术,也会结合朋友们的讨论提及自己的思考细节。文中如有不当之处,敬请指正。本篇文章是本系列的第一篇,打算尽可能详细的讲一下缓存本身的基本设计和应用,以及相关的操作细节等(具体应用主要以Redis为例)).一、服务端数据缓存1A区分缓存的划分方式有很多种,根据不同的情况。本地缓存和分布式缓存是常见的分类,它们都包含许多子类别。本地并不是指程序所在的本地服务器(严格意义上),更细粒度的是指位于程序本身的内部存储空间,而分布式则强调存储在进程外部的一台或多台服务器术语相互之间的交互和沟通,在具体软件项目的设计和应用中,大部分时间都是混合在一起的。当然我个人认为理解缓存的本质是最重要的。至于概念上的分类,只是不同理解下的划分。2一些技术成本在设计具体的项目架构时,单纯使用前者(本地缓存)的开发成本无疑是极低的。主要考虑机器的内存负载或极少量磁盘I/O的影响。后者的设计初衷是为了方便分布式程序之间缓存数据的高效共享和管理。设计时除了要考虑缓存所在服务器的内存负载外,还需要充分考虑网络I/O、CPU和某些场景的负载。更低的磁盘I/O成本,同时在具体设计上尽可能避免和权衡整体的稳定性和效率,这些不仅仅是缓存服务器的硬件成本和技术维护。需要仔细考虑的低级注意事项包括权衡细节,例如缓存间通信、网络负载和延迟。其实,如果你了解缓存的本质,就应该知道,任何存储介质都可以在合适的场景下充当高效的缓存角色,进行项目集成和缓存跨集群。常见的主流Memcached和Redis都属于后一类,甚至可以包括MongoDB等基于NoSQL设计的文档数据库(不过这是从角色的角度来说的,狭义上这是一个基于磁盘的存储库,你需要注意,每个都有自己的专业)。这些第三方缓存在项目集成和缓存间集群的过程中也需要解决一些问题。甚至当项目迭代到后期,往往需要高专业知识的运维同时参与,开发中的逻辑设计和代码实现也会增加一定的工作量。因此,有时在具体项目的设计中,一方面要尽可能多地预留,另一方面也要根据实际情况尽可能精简。再说说其他的体会:在我有限的技术学习和实践中,节点数据交互,尤其是服务之间的通信,并没有一个完美的闭环。理论上,在“现阶段”也是面向“高一致性”的,只是一种权衡(大概和生活一样)。2、缓存数据库结构的设计细节由于目前个人工作中大部分情况下使用的是Redis3.x,如果有以下相关的特性,都作为参考。1实例(Instance)根据业务场景,公共数据和业务耦合数据必须分别使用不同的实例。如果是单实例,可以考虑除以DB。当你在使用Redis的时候,那么DB在Redis中有数据隔离,但是没有严格的权限限制,所以数据库规划只是一个选项。在Cluster集群中,保持默认的单库,但在实际中,我会尽量根据项目的大小进行调整。至于开发阶段,则作为保留设计使用。需要注意的是,作为一个严重依赖服务器内存的缓存产品,如果开启持久化(后面会提到),在为大并发量的服务提供支持时,会抢占大量的服务器硬件资源。请结合Persistent策略配置,考虑实例是否分盘存储。持久化的本质是将内存数据同步写入硬盘(磁盘刷写),但是磁盘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)缓存中value的大小(这里指的是单个条目)没有平均标准,但是Size越小越好(如果使用Redis,一次操作的值越大会直接影响整个Redis的响应时间,而不仅仅是网络I/O)。如果存储空间达到10M+,建议考虑是否可以将关联的业务场景拆分成热数据和非热数据。5持久性(Permanence)上面也简单提到过。一般来说,持久化和缓存本身并没有直接的关系。可以大致想象成一个对着硬盘,一个对着内存。但是在如今的web项目中,一些业务场景对缓存的依赖度很高。持久化一方面可以帮助提高缓存服务在重启后的快速恢复,另一方面可以提供特定场景下的存储特性。当然,由于持久化,必须牺牲一些性能,包括CPU抢占和硬盘I/O影响。然而,大多数时候,优势大于劣势。建议在应用缓存的时候,如果没有特殊情况,尽量使用持久化,无论是使用自己的机制还是第三方实现。如果使用Redis,它有自己的持久化策略,包括AOF和RDB。大多数情况下我都是同时配置的(当然最新的官方版本本身也提供了混合模式)。如果在一些非高并发的场景下,或者在一些中小型项目的管理模块中,只是作为一种优化手段,确定不需要持久化,也可以直接设置它关闭以节省性能开销。但建议在程序中设置。实例应该被标记以确保实例的公共使用范围。6消除如果缓存无限增长,即使设置了一个很短的过期时间(Expiration),在某个时间点,一批高并发的大数据会在短时间内达到可用内存的峰值,此时,程序与缓存服务器的交互会出现大量的延迟和错误,甚至给服务器本身带来严重的不稳定。因此,在生产环境中,尽量配置缓存的最大内存限制和合适的淘汰策略。如果使用Redis,自淘汰策略的选择更加灵活。我个人的设计是,当数据呈现出类似的幂律分布时,总是存在大量访问量低的数据。我会选择配置allkeys-lru和volatile-lru来消除访问最少的数据。再比如缓存作为日志应用,所以我一般在项目前期配置no-enviction,后期配置成volatile-ttl。当然,我也看到过特殊业务下的设计。缓存直接作为一个轻量级的持久化数据库,它是一个终端。和强大的事务)。因此,这是合理的,不应局限于传统的设计。毕竟架构总是根据业务实时组合变化的。3.一级缓存的基本CURD及相关1新建(Create)如果没有特殊的业务需求(如前所述),插入必须设置一个过期时间。同时尽量保证到期的随机性。如果是分批缓存,个人的做法是保证设置的过期时间至少是分散的,以减少缓存雪崩的风险和影响(我会在以后的扩展中尝试解释这些)。例如:批量缓存的对象是一个有100000条条目的结果集,缓存时间基准为60*60*2(sec),现在需要同时缓存。我的做法是默认生成一个随机数,比如random(范围0-1000),过期时间设置为(60*60*2+random)。2修改(Update)更新一条缓存数据,注意过期时间是否需要重新调整。同时,在很多场合,比如多个缓存同步时,建议直接删除缓存,而不是更新缓存。修改操作往往涉及到DB之间的同步操作,相对来说比较复杂,需要权衡分布式事务的问题,后续文章会写到。3在读取(Read)查找缓存时,如果有多个条目且确定数据量较小,一定要使用与key严格匹配的模式,尽量不要使用通配符。虽然发送命令的关键数据变长了,但是避免了在缓存中不必要的搜索性能损失。比如单纯相信Redis自身的存储优化,使用keys模式,对时间复杂度没有任何限制,同时造成大量线程阻塞(这里与主从复制无关)。如果您使用扫描分页作为折衷方案,则它不是一个“无忧”的实现。首先,您需要在程序代码的包中设置一个较低的容量。其次,一定要处理好程序逻辑中可能出现的数据幻读等问题。相关的管理和控制。另外,可以类比一个额外的场景,就是在DB中操作一个大表,命中的热点数据在后面分发。4Delete/Clear(删除/清除)删除缓存,一般有直接删除和设置过期时间两种方式(不要随时滑动增加过期时间),没有详细说明。但是听说过一个特殊的业务场合,批量请求同类型数据,即时性不是很高,并且设置了过期时间,时间略有分散。清除缓存,我没有在项目中应用过,更不提倡直接使用。但是如果是应用的话,需要慎重考虑两个地方:一个是清洗的时机,一个是清洗的及时性(如果是在Redis中,不管是flushdb还是flushall,都会造成一定的阻塞)5锁/信号(Locking)本身与缓存无关,属于一些并发特性的实现,有一定的适用场景。这在Redis中有一些基于原子的实现,但与本系列讨论无关。6为什么Publish-Subscribe会提到这个与Produce-Consume相关的action?这种机制本身不属于缓存本身的范畴,更多的是和MessageQueue相关。之所以提这个是因为现在主流的缓存产品都有这个特性,在很多场景下使用方便,配置简单,效率也足够快。只是它容易导致滥用。最重要的是,不必要的强耦合也降低了整体的灵活性和性能,可扩展性真的很有限。当然,这是我目前的看法。我的建议是:如果没有特殊的场景应用,尽量不要使用。至少我不会优先推荐使用缓存自带的发布订阅,甚至在缓存集群系统中,需要考虑更多的细节。推荐的方式是使用其他专业的中间件解决方案,例如基于MQ的产品替代方案。具体候选包括RabbitMQ、Kafka等优秀的开源作品,包括朋友提到的阿里巴巴近两年在国内开发的RocketMQ,但个人使用最多的还是RabbitMQ。当然,这里不做过多赘述,根据场景选择,针对合适的场景选择最合适的技术方案即可。本文先写到这里,下一篇会尝试展开相关话题。由于个人能力和经验有限,我也在不断的学习和实践中。文中如有不当之处,敬请指正。