[原创]分布式系统之缓存的微观应用经验谈(一) 【设计基础细节篇】

本文涉及的产品
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
云数据库 Tair(兼容Redis),内存型 2GB
简介: 近几个月一直在忙些琐事,几乎年后都没怎么闲过。忙忙碌碌中就进入了2018年的秋天了,不得不感叹时间总是如白驹过隙,也不知道收获了什么和失去了什么。最近稍微休息,买了两本与技术无关的书,其一是Yann Martel 写的《The High Mountains of Portugal》(葡萄牙的高山),发现阅读此书是需要一些耐心的,对人生暗喻很深,也有足够的留白,有兴趣的朋友可以细品下。

分布式系统之缓存的微观应用经验谈(一) 【基础细节篇】


 

前言 

 

  近几个月一直在忙些琐事,几乎年后都没怎么闲过。忙忙碌碌中就进入了2018年的秋天了,不得不感叹时间总是如白驹过隙,也不知道收获了什么和失去了什么。最近稍微休息,买了两本与技术无关的书,其一是Yann Martel 写的《The High Mountains of Portugal》(葡萄牙的高山),发现阅读此书是需要一些耐心的,对人生暗喻很深,也有足够的留白,有兴趣的朋友可以细品下。好了,下面回归正题,尝试写写工作中缓存技术相关的一些实战经验和思考。


正文

 

  在分布式Web程序设计中,解决高并发以及内部解耦的关键技术离不开缓存和队列,而缓存角色类似计算机硬件中CPU的各级缓存。如今的业务规模稍大的互联网项目,即使在最初beta版的开发上,都会进行预留设计。但是在诸多应用场景里,也带来了某些高成本的技术问题,需要细致权衡。本系列主要围绕分布式系统中服务端缓存相关技术,也会结合朋友间的探讨提及自己的思考细节。文中若有不妥之处,恳请指正。

  第一篇这里尝试尽可能详细的谈谈缓存自身的基础设计应用,以及相关的操作细节等(具体应用主要以Redis 举例)。

 

  一、稍微说明基本特点和技术成本(本文主要指服务端数据缓存)


    1.1 一种区分

 

      缓存基于不同的条件有很多种划分方式,本地缓存(Local cache)和分布式缓存(Distributed cache)是一种常见分类,两者自身又包含很多细类。

      本地并不是指程序所在本地服务器(从严格概念来说),而是更细粒度的指位于程序自身的内部存储空间,而分布式更多强调的是存储在进程之外的一个或者多个服务器上,彼此交互通信,在具体软件项目的设计和应用中,多数时候是混合一体。

      (当然,个人认为对缓存本质的理解才是最重要的,至于概念上的分类只是一个不同理解下的划分而已)

 

    1.2 一些技术成本


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


      其实如果理解了缓存本质就该知道,任何存储介质在适当的场景下都可以充当一个高效的缓存角色并进行项目集成和缓存间集群。常见主流的Memcached和Redis等均是属于后者范畴,甚至可以包括如基于NoSql设计的MongoDB这类文档数据库(但这是从角色角度讲,而狭义划分上这是基于磁盘的存储库,需要注意,各有专攻)。这些第三方缓存在进行项目集成和缓存间集群,也需要解决一些问题。甚至项目迭代到了后期阶段,往往还需要具备较高专业知识的运维同时参与,并且在开发中的逻辑设计和代码实现也会增加一定的工作量。所以有时候在具体项目的设计上,一方面要尽可能预留,一方面还得根据实际情况尽可能精简。

 

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

 

  二、缓存数据库结构的一些设计细节

  

    (由于目前个人工作中大多数情况应用的是Redis 3.x,以下若有特性关联,均是以此作为参照说明。)


    2.1 实例(Instance)

 

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


      额外需要注意的是,作为重度依赖服务器内存的缓存产品,如果开启了持久化(后面会提到),并且在为并发量极大的服务提供支持时,服务器硬件资源会出现大量抢占,请结合持久策略配置,考虑实例是否进行分盘存储。持久化本质是将内存数据同步写入硬盘(刷盘),而磁盘I/O实在有限,被迫的写入阻塞除了造成线程阻塞和服务超时,还会导致额外异常甚至波及其他底层依赖服务。当然,我的建议是,如果条件允许,最好是在项目初期设计时就进行规划并确定。

 

    2.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中的单表容量问题,控制条目数量不能无限增长(比如预知到存储条目可以轻松达到百万级),“分库分表”的设计思路都是相通的。

 

    2.3 缓存键(Key)

 

      上面提到了基于缓存键来设计表,这里再单独说明一下键相关的个人规范。在键长度足够简短的前提下,如果关联相同业务模块,则必须设计为以同一个标识(代号)开头,目的是方便查找和统计管理。
如用户登录服务器列表:
        { key : "ul:server:a" , value : "xxxx" }
        { key : "ul:server:b" , value : "yyyy" }


      另外,每个独立业务系统可考虑配置一个唯一的通用前缀标识,当然,这里不是必需,若实际工作中,如果使用的是不同库,则可以忽略。

 

    2.4 缓存值(value)

 

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

 

    2.5 持久化(Permanence)

 

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


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

 

    2.6 淘汰(Eliminate)

 

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


      如果使用的是Redis,自身淘汰策略选择比较灵活。个人的设计是,在数据呈现类似幂律分布情况下,总有大量数据访问较低,我会选择配置allkeys-lru、volatile-lru,将最少访问的数据进行淘汰。再比如缓存是作为日志应用的,那么我一般是项目前期是配置no-enviction,后期会配置为volatile-ttl。当然,我也见过一种特殊业务下的设计,缓存直接用来作为轻量的持久数据库使用,而且是终端,开始觉得有些新奇,后来发现是非常符合业务设计的(比如几乎没有任何复杂逻辑和强事务)。所以合情合理,确实不应该禁锢在传统设计里,毕竟架构总是基于业务去实时组合和改变的。

 


  三、缓存的基础CURD和其他相关(在这里我主要讨论一级缓存)

 

    3.1 新增(Create)

 

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

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

 

    3.2 修改(Update)

 

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

 

    3.3 读取(Read)

 

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


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


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

 

    3.4 删除 / 清空(Delete / Clear)

 

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

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


    3.5 锁/信号(Locking)

 

      本身无关缓存,属于一些并发特性实现,有一定的适用场景。这在Redis中有一些基于原子的实现,但与本系列讨论无关。本人去年写过一篇与之相关的分享,详见:商城系统下单库存管控系列杂记(二)https://www.cnblogs.com/bsfz/p/7824428.html),但这里不赘述。

 

    3.6 发布-订阅(Publish-Subscribe)

 

      为什么提到这个跟生产消费(Produce-Consume)相关的动作呢?这个机制本身是不属于缓存自身的范畴的,而是更相关于消息队列(Message Queue)。之所以提到,是因为如今主流的缓存产品都自带这一特性,很多场景使用起来较方便,配置也简单,效率也够快。只是,往往会造成滥用。最关键是不必要的强耦合也降低了整体灵活性和性能,扩展性也实在有限。当然,这是我目前的看法。

      我的建议是:如果没有特殊的场景应用,尽量不使用。至少本人是不会优先推荐使用缓存自身的发布订阅的,甚至在缓存集群系统中,需要考究的细节更多。而推荐的方式是,使用其他专业中间件解决,如基于MQ的产品替代方案。具体的候选有优秀的开源作品如RabbitMQ、Kafka等,包括有朋友提到的近两年国内阿里研发的RocketMQ等等,但是个人目前使用较多的依然是RabbitMQ。当然,这里不去过多赘述了,根据场景选择,合适的场景选用最合适的技术方案即可吧。

 

 

结语

 

  本篇先写到这里,下一篇会围绕相关主题尝试扩展阐述。

  PS:由于个人能力和经验均有限,自己也在持续学习和实践,文中若有不妥之处,恳请指正。

 

【已更新:分布式系统之缓存的微观应用经验谈(二) 【主从和主备高可用篇】 https://www.cnblogs.com/bsfz/p/9769503.html

 

 

End.

 

 

 

相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore     ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库 ECS 实例和一台目标数据库 RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
目录
相关文章
|
21天前
|
缓存 NoSQL PHP
Redis作为PHP缓存解决方案的优势、实现方式及注意事项。Redis凭借其高性能、丰富的数据结构、数据持久化和分布式支持等特点,在提升应用响应速度和处理能力方面表现突出
本文深入探讨了Redis作为PHP缓存解决方案的优势、实现方式及注意事项。Redis凭借其高性能、丰富的数据结构、数据持久化和分布式支持等特点,在提升应用响应速度和处理能力方面表现突出。文章还介绍了Redis在页面缓存、数据缓存和会话缓存等应用场景中的使用,并强调了缓存数据一致性、过期时间设置、容量控制和安全问题的重要性。
36 5
|
1月前
|
缓存 NoSQL 数据库
运用云数据库 Tair 构建缓存为应用提速,完成任务得苹果音响、充电套装等好礼!
本活动将带大家了解云数据库 Tair(兼容 Redis),通过体验构建缓存以提速应用,完成任务,即可领取罗马仕安卓充电套装,限量1000个,先到先得。邀请好友共同参与活动,还可赢取苹果 HomePod mini、小米蓝牙耳机等精美好礼!
|
2月前
|
存储 缓存 数据库
缓存技术有哪些应用场景呢
【10月更文挑战第19天】缓存技术有哪些应用场景呢
|
3月前
|
缓存 NoSQL Java
谷粒商城笔记+踩坑(12)——缓存与分布式锁,Redisson+缓存数据一致性
缓存与分布式锁、Redisson分布式锁、缓存数据一致性【必须满足最终一致性】
145 14
谷粒商城笔记+踩坑(12)——缓存与分布式锁,Redisson+缓存数据一致性
|
2月前
|
存储 缓存 NoSQL
大数据-38 Redis 高并发下的分布式缓存 Redis简介 缓存场景 读写模式 旁路模式 穿透模式 缓存模式 基本概念等
大数据-38 Redis 高并发下的分布式缓存 Redis简介 缓存场景 读写模式 旁路模式 穿透模式 缓存模式 基本概念等
71 4
|
2月前
|
缓存 移动开发 前端开发
HTML5 应用程序缓存详解
HTML5 应用程序缓存(Application Cache)通过缓存 HTML、JavaScript、CSS 和图像等资源,使 Web 应用能在离线状态下运行。它利用 Manifest 文件(`.appcache`)定义缓存资源列表,浏览器会在加载页面时下载并缓存这些资源。此外,应用程序缓存还提供了事件处理机制,允许开发者监控缓存状态并进行手动管理。尽管这一技术已被视为过时,建议使用 Service Workers 和 Cache API 等现代替代方案来实现更强大的离线功能和缓存控制。
|
3月前
|
缓存 JavaScript 中间件
优化Express.js应用程序性能:缓存策略、请求压缩和路由匹配
在开发Express.js应用时,采用合理的缓存策略、请求压缩及优化路由匹配可大幅提升性能。本文介绍如何利用`express.static`实现缓存、`compression`中间件压缩响应数据,并通过精确匹配、模块化路由及参数化路由提高路由处理效率,从而打造高效应用。
184 13
|
3月前
|
机器学习/深度学习 缓存 NoSQL
深度学习在图像识别中的应用与挑战后端开发中的数据缓存策略
本文深入探讨了深度学习技术在图像识别领域的应用,包括卷积神经网络(CNN)的原理、常见模型如ResNet和VGG的介绍,以及这些模型在实际应用中的表现。同时,文章也讨论了数据增强、模型集成等改进性能的方法,并指出了当前面临的计算资源需求高、数据隐私等挑战。通过综合分析,本文旨在为深度学习在图像识别中的进一步研究和应用提供参考。 本文探讨了后端开发中数据缓存的重要性和实现方法,通过具体案例解析Redis在实际应用中的使用。首先介绍了缓存的基本概念及其在后端系统性能优化中的作用;接着详细讲解了Redis的常见数据类型和应用场景;最后通过一个实际项目展示了如何在Django框架中集成Redis,
|
2月前
|
存储 缓存 NoSQL
构建高性能Web应用:缓存的重要性及其实现
构建高性能Web应用:缓存的重要性及其实现
|
4月前
|
开发工具 Android开发 iOS开发
从零开始学 Xamarin 开发,新手教程全攻略,安装环境、创建项目、设计界面,轻松开启开发之旅!
【8月更文挑战第31天】Xamarin是一种高效的跨平台移动应用开发工具,迎合了日益增长的移动应用需求。本文为Xamarin新手提供了一套详尽的入门指南,涵盖开发环境搭建、项目创建与配置、用户界面设计及功能实现等关键步骤。通过具体示例,帮助初学者快速上手Xamarin开发,开启移动应用创作之旅。
65 0