首页
关于
Search
1
给你10个市场数据调研报告的免费下载网站!以后竞品数据就从这里找!
182 阅读
2
php接口优化 使用curl_multi_init批量请求
144 阅读
3
《从菜鸟到大师之路 ElasticSearch 篇》
107 阅读
4
2024年备考系统架构设计师
104 阅读
5
PHP 文件I/O
92 阅读
php
thinkphp
laravel
工具
开源
mysql
数据结构
总结
思维逻辑
令人感动的创富故事
读书笔记
前端
vue
js
css
书籍
开源之旅
架构
消息队列
docker
教程
代码片段
redis
服务器
nginx
linux
科普
java
c
ElasticSearch
测试
php进阶
php基础
登录
Search
标签搜索
php函数
php语法
性能优化
安全
错误和异常处理
问题
vue
Composer
Session
缓存
框架
Swoole
api
并发
异步
正则表达式
php-fpm
mysql 索引
开发规范
协程
dafenqi
累计撰写
786
篇文章
累计收到
28
条评论
首页
栏目
php
thinkphp
laravel
工具
开源
mysql
数据结构
总结
思维逻辑
令人感动的创富故事
读书笔记
前端
vue
js
css
书籍
开源之旅
架构
消息队列
docker
教程
代码片段
副业
redis
服务器
nginx
linux
科普
java
c
ElasticSearch
测试
php进阶
php基础
页面
关于
搜索到
100
篇与
的结果
2023-09-22
《从菜鸟到大师之路 ElasticSearch 篇》
《从菜鸟到大师之路 ElasticSearch 篇》(一):ElasticSearch 基础概念、生态和应用场景为什么需要学习 ElasticSearch根据 DB Engine 的排名显示, ElasticSearch 是最受欢迎的 企业级搜索引擎 。下图红色勾选的是我们前面的系列详解的,除此之外你可以看到搜索库ElasticSearch在前十名内:所以为什么要学习 ElasticSearch 呢?1、在当前软件行业中,搜索是一个软件系统或平台的基本功能, 学习ElasticSearch就可以为相应的软件打造出良好的搜索体验。2、其次,ElasticSearch具备非常强的大数据分析能力。虽然Hadoop也可以做大数据分析,但是ElasticSearch的分析能力非常高,具备Hadoop不具备的能力。比如有时候用Hadoop分析一个结果,可能等待的时间比较长。3、ElasticSearch可以很方便的进行使用,可以将其安装在个人的笔记本电脑,也可以在生产环境中,将其进行水平扩展。4、国内比较大的互联网公司都在使用,比如小米、滴滴、携程等公司。另外,在腾讯云、阿里云的云平台上,也都有相应的ElasticSearch云产品可以使用。5、在当今大数据时代,掌握近实时的搜索和分析能力,才能掌握核心竞争力,洞见未来。什么是ElasticSearchElasticSearch是一款非常强大的、基于Lucene的开源搜索及分析引擎;它是一个实时的分布式搜索分析引擎,它能让你以前所未有的速度和规模,去探索你的数据。它被用作全文检索、结构化搜索、分析以及这三个功能的组合:Wikipedia_使用 Elasticsearch 提供带有高亮片段的全文搜索,还有 search-as-you-type 和 did-you-mean 的建议。卫报使用 Elasticsearch 将网络社交数据结合到访客日志中,为它的编辑们提供公众对于新文章的实时反馈。Stack Overflow_将地理位置查询融入全文检索中去,并且使用 more-like-this 接口去查找相关的问题和回答。GitHub_使用 Elasticsearch 对1300亿行代码进行查询。...除了搜索,结合Kibana、Logstash、Beats开源产品,Elastic Stack(简称ELK)还被广泛运用在大数据近实时分析领域,包括: 日志分析、指标监控、信息安全 等。它可以帮助你 探索海量结构化、非结构化数据,按需创建可视化报表,对监控数据设置报警阈值,通过使用机器学习,自动识别异常状况。 ElasticSearch是基于Restful WebApi,使用Java语言开发的搜索引擎库类,并作为Apache许可条款下的开放源码发布,是当前流行的企业级搜索引擎。其客户端在Java、C#、PHP、Python等许多语言中都是可用的。所以,ElasticSearch具备两个优势:天生支持分布式,可水平扩展;提供了Restful接口,降低全文检索的学习曲线,因为Restful接口,所以可以被任何编程语言调用;ElasticSearch的由来ElasticSearch背后的小故事。许多年前,一个刚结婚的名叫 Shay Banon 的失业开发者,跟着他的妻子去了伦敦,他的妻子在那里学习厨师。在寻找一个赚钱的工作的时候,为了给他的妻子做一个食谱搜索引擎,他开始使用 Lucene 的一个早期版本。直接使用 Lucene 是很难的,因此 Shay 开始做一个抽象层,Java 开发者使用它可以很简单的给他们的程序添加搜索功能。他发布了他的第一个开源项目 Compass。后来 Shay 获得了一份工作,主要是高性能,分布式环境下的内存数据网格。这个对于高性能,实时,分布式搜索引擎的需求尤为突出, 他决定重写 Compass,把它变为一个独立的服务并取名 Elasticsearch。第一个公开版本在2010年2月发布,从此以后,Elasticsearch 已经成为了 Github 上最活跃的项目之一,他拥有超过300名 contributors(目前736名 contributors )。一家公司已经开始围绕 Elasticsearch 提供商业服务,并开发新的特性,但是,Elasticsearch 将永远开源并对所有人可用。据说,Shay 的妻子还在等着她的食谱搜索引擎…。为什么不是直接使用LuceneElasticSearch是基于Lucene的,那么为什么不是直接使用Lucene呢?Lucene 可以说是当下最先进、高性能、全功能的搜索引擎库。但是 Lucene 仅仅只是一个库。为了充分发挥其功能,你需要使用 Java 并将 Lucene 直接集成到应用程序中。更糟糕的是,您可能需要获得信息检索学位才能了解其工作原理。Lucene 非常 复杂。Elasticsearch 也是使用 Java 编写的,它的内部使用 Lucene 做索引与搜索,但是它的目的是使全文检索变得简单,通过隐藏 Lucene 的复杂性,取而代之的提供一套简单一致的 RESTful API。然而,Elasticsearch 不仅仅是 Lucene,并且也不仅仅只是一个全文搜索引擎。它可以被下面这样准确的形容:一个分布式的实时文档存储,每个字段 可以被索引与搜索一个分布式实时分析搜索引擎能胜任上百个服务节点的扩展,并支持 PB 级别的结构化或者非结构化数据ElasticSearch的主要功能及应用场景我们在哪些场景下可以使用ES呢?主要功能1)海量数据的分布式存储以及集群管理,达到了服务与数据的高可用以及水平扩展;2)近实时搜索,性能卓越。对结构化、全文、地理位置等类型数据的处理;3)海量数据的近实时分析(聚合功能)应用场景1)网站搜索、垂直搜索、代码搜索;2)日志管理与分析、安全指标监控、应用性能监控、Web抓取舆情分析;ElasticSearch的基础概念我们还需对比结构化数据库,看看ES的基础概念,为我们后面学习作铺垫。Near Realtime(NRT) 近实时。数据提交索引后,立马就可以搜索到。Cluster 集群,一个集群由一个唯一的名字标识,默认为“elasticsearch”。集群名称非常重要,具有相同集群名的节点才会组成一个集群。集群名称可以在配置文件中指定。Node 节点:存储集群的数据,参与集群的索引和搜索功能。像集群有名字,节点也有自己的名称,默认在启动时会以一个随机的UUID的前七个字符作为节点的名字,你可以为其指定任意的名字。通过集群名在网络中发现同伴组成集群。一个节点也可是集群。Index 索引: 一个索引是一个文档的集合(等同于solr中的集合)。每个索引有唯一的名字,通过这个名字来操作它。一个集群中可以有任意多个索引。Type 类型:指在一个索引中,可以索引不同类型的文档,如用户数据、博客数据。从6.0.0 版本起已废弃,一个索引中只存放一类数据。Document 文档:被索引的一条数据,索引的基本信息单元,以JSON格式来表示。Shard 分片:在创建一个索引时可以指定分成多少个分片来存储。每个分片本身也是一个功能完善且独立的“索引”,可以被放置在集群的任意节点上。Replication 备份: 一个分片可以有多个备份(副本)为了方便理解,作一个ES和数据库的对比文档ElasticSearch是面向文档的,文档是所有可搜索数据的最小单位。例如:日志文件中的日志项;一张唱片的详细信息;一篇文章中的具体内容;在ElasticSearch中,文档会被序列化成Json格式:Json对象是由字段组成的;每个字段都有对应的字段类型(如:字符串、数值、日期类型等);每个文档都有一个唯一的ID(Unique ID)可以自己指定此ID;也可以通过ElasticSearch自动生成;我们可以将文档理解成关系型数据库中的一条数据记录,一条记录包含了一系列的字段。Json文档的格式不需要预先定义字段的类型可以指定或者由ElasticSearch自动推算;Json支持数组、支持嵌套;每一个文档中都包含有一份元数据,元数据的作用主要是用来标注文档的相关信息,如:_index:文档所属的索引名;_type:文档所属的类型名(从7.0开始,每一个索引只能创建一个Type:_doc,在此之前一个索引是可以设置多个Type的);_id:文档的Unqie Id;_source:文档的原始Json数据;_version:文档的版本信息;_score:文档的相关性算分;索引(Index)索引指的就是一类文档的集合,相当于文档的容器。索引体现了逻辑空间的概念,每个索引都有自己的Mapping定义,用来定义所包含的文档的字段名和字段类型;索引中的数据(文档)分散在Shard(分片)上,Shard体现了物理空间的概念;索引的Mapping与SettingMapping定义文档字段的类型;Setting定义不同的数据分布;索引的含义一般说“索引文档到ElasticSearch的索引中”,前面的索引指的是一个动词的含义,也就是保存一个文档到ElasticSearch中。后面的索引是指在ElasticSearch集群中,可以创建很多个不同的索引;索引分为:B树索引和倒排索引,而倒排索引在ElasticSearch中是非常重要的;ElasticSearch与RDBMS的代入理解与类比如下两者相对比,若对数据进行全文检索,以及进行算分时,ElasticSearch更加合适;当涉及的数据事务比较高时,那RDBMS更加合适。在实际生产中,一般是两者进行结合使用。集群ElasticSearch集群实际上是一个分布式系统,而分布式系统需要具备两个特性:高可用性服务可用性:允许有节点停止服务;数据可用性:部分节点丢失,不会丢失数据;可扩展性随着请求量的不断提升,数据量的不断增长,系统可以将数据分布到其他节点,实现水平扩展;ElasticSearch的集群通过不同的名字来进行区分,默认名字“elasticsearch”;可以通过配置文件修改或者命令行修改:-E cluster.name=test一个集群中可以有一个或者多个节点。节点节点是什么?节点是一个ElasticSearch的实例,其本质就是一个Java进程;一台机器上可以运行多个ElasticSearch实例,但是建议在生产环境中一台机器上只运行一个ElasticSearch实例;每个节点都有名字,可以通过配置文件进行配置,也可以通过命令行进行指定,如:-E node.name=node1每个节点在启动之后,会被分配一个UID,保存在data目录下;Master-Eligible【有资格、胜任者】 Node与Master Node的说明:每个节点启动之后,默认就是一个Master Eligible节点,当然可以在配置文件中将其禁止,node.master:falseMaster-Eligible Node可以参加选主流程,成为Master Node;当第一个节点启动时,它会将其选举为Master Node;每个节点都保存了集群状态,但只有Master Node才能修改集群的状态,包括如下:所有的节点信息;所有的索引和其相关的Mapping与Setting信息;分片的路由信息;Data Node与Coordinating【协调、整合】 Node的说明:Data Node:可以保存数据的节点,负责保存分片数据,在数据扩展上起到至关重要的作用;Coordinating Node:它通过接受Rest Client的请求,会将请求分发到合适的节点,最终将结果汇集到一起,再返回给Client;每个节点都默认起到Coordinating Node的职责;Hot Node(热节点)与Warm Node(冷节点)的说明:Hot Node:有更好配置的节点,其有更好的资源配置,如磁盘吞吐、CPU速度;Warm Node:资源配置较低的节点;Machine Learning Node:负责机器学习的节点,常用来做异常检测;配置节点类型每个节点在启动时,会读取elasticsearch.yml配置文件,来确定当前节点扮演什么角色。在生产环境中,应该将节点设置为单一的角色节点,这样可以有更好的性能,更清晰的职责,可以针对节点的不同给予不能的机器配置。分片Primary Shard(主分片)可以解决数据水平扩展的问题,通过主分片,可以将数据分布到集群内的所有节点之上。一个主分片是一个运行的Lucene的实例;注意:一个节点对应一个ES实例;一个节点可以有多个index(索引);一个index可以有多个shard(分片);一个分片是一个lucene index(此处的index是lucene自己的概念,与ES的index不是一回事);主分片数是在索引创建时指定,后续不允许修改,除非Reindex;Replica Shard(副本)可以解决数据高可用的问题,它是主分片的拷贝。副本分片数可以动态调整;增加副本数,在一定程度上可以提高服务的可用性;分片的设定对于生产环境中分片的设定,需要提前做好容量规划,因为主分片数是在索引创建时预先设定的,后续无法修改。分片数设置过小导致后续无法增加节点进行水平扩展。导致分片的数据量太大,数据在重新分配时耗时;分片数设置过大影响搜索结果的相关性打分,影响统计结果的准确性;单个节点上过多的分片,会导致资源浪费,同时也会影响性能;学习ElasticSearch的入手层面开发层面了解ElasticSearch有基本功能;底层分布式工作原理;针对数据进行数据建模;运维层面进行集群的容量规划;对集群进行滚动升级;对性能的优化;出现问题后,对问题的诊断与解决;方案层面学习ElasticSearch后,可以针对实际情况,解决搜索的相关问题;可以将ELK运用到大数据分析场景中;Elastic Stack生态Beats + Logstash + ElasticSearch + Kibana 如下是我从官方博客中找到图,这张图展示了ELK生态以及基于ELK的场景(最上方)。由于Elastic X-Pack是面向收费的,所以我们不妨也把X-Pack放进去,看看哪些是由X-Pack带来的,在阅读官网文档时将方便你甄别重点:BeatsBeats是一个面向轻量型采集器的平台,这些采集器可以从边缘机器向Logstash、ElasticSearch发送数据,它是由Go语言进行开发的,运行效率方面比较快。从下图中可以看出,不同Beats的套件是针对不同的数据源。LogstashLogstash是动态数据收集管道,拥有可扩展的插件生态系统,支持从不同来源采集数据,转换数据,并将数据发送到不同的存储库中。其能够与ElasticSearch产生强大的协同作用,后被Elastic公司在2013年收购。它具有如下特性:1)实时解析和转换数据;2)可扩展,具有200多个插件;3)可靠性、安全性。Logstash会通过持久化队列来保证至少将运行中的事件送达一次,同时将数据进行传输加密;4)监控;ElasticSearchElasticSearch对数据进行搜索、分析和存储,其是基于JSON的分布式搜索和分析引擎,专门为实现水平可扩展性、高可靠性和管理便捷性而设计的。它的实现原理主要分为以下几个步骤:1)首先用户将数据提交到ElasticSearch数据库中;2)再通过分词控制器将对应的语句分词;3)将分词结果及其权重一并存入,以备用户在搜索数据时,根据权重将结果排名和打分,将返回结果呈现给用户;ElasticSearch与DB的集成针对上图,可以分为两种情况:将ElasticSearch当成数据库来存储数据,好处是架构比较简单;若数据更新比较频繁,同时需要考虑数据事务性时,应该先将数据存入数据库,然后建立一个合适的同步机制,将数据同步到ElasticSearch中;KibanaKibana实现数据可视化,其作用就是在ElasticSearch中进行民航。Kibana能够以图表的形式呈现数据,并且具有可扩展的用户界面,可以全方位的配置和管理ElasticSearch。Kibana最早的时候是基于Logstash创建的工具,后被Elastic公司在2013年收购。1)Kibana可以提供各种可视化的图表;2)可以通过机器学习的技术,对异常情况进行检测,用于提前发现可疑问题;从日志收集系统看 ES Stack的发展我们看下ELK技术栈的演化,通常体现在日志收集系统中。一个典型的日志系统包括:收集:能够采集多种来源的日志数据传输:能够稳定的把日志数据解析过滤并传输到存储系统存储:存储日志数据分析:支持 UI 分析警告:能够提供错误报告,监控机制beats+elasticsearch+kibanaBeats采集数据后,存储在ES中,有Kibana可视化的展示。beats+logstath+elasticsearch+kibana该框架是在上面的框架的基础上引入了logstash,引入logstash带来的好处如下:(1)Logstash具有基于磁盘的自适应缓冲系统,该系统将吸收传入的吞吐量,从而减轻背压。(2)从其他数据源(例如数据库,S3或消息传递队列)中提取。(3)将数据发送到多个目的地,例如S3,HDFS或写入文件。(4)使用条件数据流逻辑组成更复杂的处理管道。beats结合logstash带来的优势:(1)水平可扩展性,高可用性和可变负载处理:beats和logstash可以实现节点之间的负载均衡,多个logstash可以实现logstash的高可用(2)消息持久性与至少一次交付保证:使用beats或Winlogbeat进行日志收集时,可以保证至少一次交付。从Filebeat或Winlogbeat到Logstash以及从Logstash到Elasticsearch的两种通信协议都是同步的,并且支持确认。Logstash持久队列提供跨节点故障的保护。对于Logstash中的磁盘级弹性,确保磁盘冗余非常重要。(3)具有身份验证和有线加密的端到端安全传输:从Beats到Logstash以及从 Logstash到Elasticsearch的传输都可以使用加密方式传递 。与Elasticsearch进行通讯时,有很多安全选项,包括基本身份验证,TLS,PKI,LDAP,AD和其他自定义领域增加更多的数据源 比如:TCP,UDP和HTTP协议是将数据输入Logstash的常用方法。beats+MQ+logstash+elasticsearch+kibana在如上的基础上我们可以在beats和logstash中间添加一些组件redis、kafka、RabbitMQ等,添加中间件将会有如下好处:(1)降低对日志所在机器的影响,这些机器上一般都部署着反向代理或应用服务,本身负载就很重了,所以尽可能的在这些机器上少做事;(2)如果有很多台机器需要做日志收集,那么让每台机器都向Elasticsearch持续写入数据,必然会对Elasticsearch造成压力,因此需要对数据进行缓冲,同时,这样的缓冲也可以一定程度的保护数据不丢失;(3)将日志数据的格式化与处理放到Indexer中统一做,可以在一处修改代码、部署,避免需要到多台机器上去修改配置;Elastic Stack最佳实践我们再看下官方开发成员分享的最佳实践。日志收集系统基本的日志系统增加数据源,和使用MQMetric收集和APM性能监控多数据中心方案通过冗余实现数据高可用两个数据采集中心(比如采集两个工厂的数据),采集数据后的汇聚数据分散,跨集群的搜索来源:https://www.pdai.tech/md/db/nosql-es/elasticsearch-x-introduce-2.html(二):ElasticSearch 技术原理图解图解 ElasticSearch云上的集群集群里的盒子云里面的每个白色正方形的盒子代表一个节点——Node。节点之间在一个或者多个节点直接,多个绿色小方块组合在一起形成一个 ElasticSearch 的索引。索引里的小方块在一个索引下,分布在多个节点里的绿色小方块称为分片——Shard。Shard=Lucene Index一个ElasticSearch的Shard本质上是一个Lucene Index。Lucene是一个Full Text 搜索库(也有很多其他形式的搜索库),ElasticSearch是建立在Lucene之上的。接下来的故事要说的大部分内容实际上是ElasticSearch如何基于Lucene工作的。图解 LuceneSegmentMini索引——segment在Lucene里面有很多小的segment,我们可以把它们看成Lucene内部的mini-index。Segment内部(有着许多数据结构)Inverted IndexStored FieldsDocument ValuesCacheInverted Index最最重要的Inverted IndexInverted Index主要包括两部分:一个有序的数据字典Dictionary(包括单词Term和它出现的频率)。与单词Term对应的Postings(即存在这个单词的文件)。当我们搜索的时候,首先将搜索的内容分解,然后在字典里找到对应Term,从而查找到与搜索相关的文件内容。查询“the fury”自动补全(AutoCompletion-Prefix)如果想要查找以字母“c”开头的字母,可以简单的通过二分查找(Binary Search)在Inverted Index表中找到例如“choice”、“coming”这样的词(Term)。昂贵的查找如果想要查找所有包含“our”字母的单词,那么系统会扫描整个Inverted Index,这是非常昂贵的。在此种情况下,如果想要做优化,那么我们面对的问题是如何生成合适的Term。问题的转化对于以上诸如此类的问题,我们可能会有几种可行的解决方案:1.* suffix -> xiffus *如果我们想以后缀作为搜索条件,可以为Term做反向处理。2.(60.6384, 6.5017) -> u4u8gyykk对于GEO位置信息,可以将它转换为GEO Hash。3.123 -> {1-hundreds, 12-tens, 123}对于简单的数字,可以为它生成多重形式的Term。解决拼写错误一个Python库为单词生成了一个包含错误拼写信息的树形状态机,解决拼写错误的问题。Stored Field字段查找当我们想要查找包含某个特定标题内容的文件时,Inverted Index就不能很好的解决这个问题,所以Lucene提供了另外一种数据结构Stored Fields来解决这个问题。本质上,Stored Fields是一个简单的键值对key-value。默Document Values 为了排序,聚合即使这样,我们发现以上结构仍然无法解决诸如:排序、聚合、facet,因为我们可能会要读取大量不需要的信息。所以,另一种数据结构解决了此种问题:Document Values。这种结构本质上就是一个列式的存储,它高度优化了具有相同类型的数据的存储结构。为了提高效率,ElasticSearch可以将索引下某一个Document Value全部读取到内存中进行操作,这大大提升访问速度,但是也同时会消耗掉大量的内存空间。总之,这些数据结构Inverted Index、Stored Fields、Document Values及其缓存,都在segment内部。搜索发生时搜索时,Lucene会搜索所有的segment然后将每个segment的搜索结果返回,最后合并呈现给客户。Lucene的一些特性使得这个过程非常重要:Segments是不可变的(immutable)Delete? 当删除发生时,Lucene做的只是将其标志位置为删除,但是文件还是会在它原来的地方,不会发生改变Update? 所以对于更新来说,本质上它做的工作是:先删除,然后重新索引(Re-index)随处可见的压缩Lucene非常擅长压缩数据,基本上所有教科书上的压缩方式,都能在Lucene中找到。缓存所有的所有Lucene也会将所有的信息做缓存,这大大提高了它的查询效率。缓存的故事当ElasticSearch索引一个文件的时候,会为文件建立相应的缓存,并且会定期(每秒)刷新这些数据,然后这些文件就可以被搜索到。随着时间的增加,我们会有很多segments,所以ElasticSearch会将这些segment合并,在这个过程中,segment会最终被删除掉这就是为什么增加文件可能会使索引所占空间变小,它会引起merge,从而可能会有更多的压缩。举个栗子有两个segment将会merge这两个segment最终会被删除,然后合并成一个新的segment这时这个新的segment在缓存中处于cold状态,但是大多数segment仍然保持不变,处于warm状态。以上场景经常在Lucene Index内部发生的。在Shard中搜索ElasticSearch从Shard中搜索的过程与Lucene Segment中搜索的过程类似。与在Lucene Segment中搜索不同的是,Shard可能是分布在不同Node上的,所以在搜索与返回结果时,所有的信息都会通过网络传输。需要注意的是:1次搜索查找2个shard = 2次分别搜索shard对于日志文件的处理当我们想搜索特定日期产生的日志时,通过根据时间戳对日志文件进行分块与索引,会极大提高搜索效率。当我们想要删除旧的数据时也非常方便,只需删除老的索引即可。在上种情况下,每个index有两个shards如何Scaleshard不会进行更进一步的拆分,但是shard可能会被转移到不同节点上所以,如果当集群节点压力增长到一定的程度,我们可能会考虑增加新的节点,这就会要求我们对所有数据进行重新索引,这是我们不太希望看到的,所以我们需要在规划的时候就考虑清楚,如何去平衡足够多的节点与不足节点之间的关系。节点分配与Shard优化为更重要的数据索引节点,分配性能更好的机器确保每个shard都有副本信息replica路由Routing每个节点,每个都存留一份路由表,所以当请求到任何一个节点时,ElasticSearch都有能力将请求转发到期望节点的shard进一步处理。ElasticSearch整体结构通过上文,在通过图解了解了ES整体的原理后,我们梳理下ES的整体结构一个 ES Index 在集群模式下,有多个 Node (节点)组成。每个节点就是 ES 的Instance (实例)。每个节点上会有多个 shard (分片), P1 P2 是主分片, R1 R2 是副本分片每个分片上对应着就是一个 Lucene Index(底层索引文件)Lucene Index 是一个统称由多个 Segment (段文件,就是倒排索引)组成。每个段文件存储着就是 Doc 文档。commit point记录了所有 segments 的信息Lucene索引结构上图中Lucene的索引结构中有哪些文件呢?文件的关系如下:Lucene处理流程上文图解过程,还需要理解Lucene处理流程, 这将帮助你更好的索引文档和搜索文档。创建索引的过程:准备待索引的原文档,数据来源可能是文件、数据库或网络对文档的内容进行分词组件处理,形成一系列的Term索引组件对文档和Term处理,形成字典和倒排表搜索索引的过程:对查询语句进行分词处理,形成一系列Term根据倒排索引表查找出包含Term的文档,并进行合并形成符合结果的文档集比对查询语句与各个文档相关性得分,并按照得分高低返回ElasticSearch分析器上图中很重要的一项是语法分析/语言处理, 所以我们还需要补充ElasticSearch分析器知识点。分析 包含下面的过程:首先,将一块文本分成适合于倒排索引的独立的 词条 ,之后,将这些词条统一化为标准格式以提高它们的“可搜索性”,或者 recall分析器执行上面的工作。分析器实际上是将三个功能封装到了一个包里:字符过滤器 首先,字符串按顺序通过每个字符过滤器。他们的任务是在分词前整理字符串。一个字符过滤器可以用来去掉HTML,或者将 & 转化成 and。分词器 其次,字符串被分词器分为单个的词条。一个简单的分词器遇到空格和标点的时候,可能会将文本拆分成词条。Token 过滤器 最后,词条按顺序通过每个 token 过滤器 。这个过程可能会改变词条(例如,小写化 Quick ),删除词条(例如, 像 a, and, the 等无用词),或者增加词条(例如,像 jump 和 leap 这种同义词)。Elasticsearch提供了开箱即用的字符过滤器、分词器和token 过滤器。这些可以组合起来形成自定义的分析器以用于不同的目的。内置分析器Elasticsearch还附带了可以直接使用的预包装的分析器。接下来我们会列出最重要的分析器。为了证明它们的差异,我们看看每个分析器会从下面的字符串得到哪些词条:"Set the shape to semi-transparent by calling set_trans(5)" 标准分析器标准分析器是Elasticsearch默认使用的分析器。它是分析各种语言文本最常用的选择。它根据 Unicode 联盟 定义的 单词边界 划分文本。删除绝大部分标点。最后,将词条小写。它会产生。set, the, shape, to, semi, transparent, by, calling, set_trans, 5 简单分析器简单分析器在任何不是字母的地方分隔文本,将词条小写。它会产生set, the, shape, to, semi, transparent, by, calling, set, trans 空格分析器空格分析器在空格的地方划分文本。它会产生Set, the, shape, to, semi-transparent, by, calling, set_trans(5) 语言分析器特定语言分析器可用于很多语言。它们可以考虑指定语言的特点。例如,英语 分析器附带了一组英语无用词(常用单词,例如 and 或者 the ,它们对相关性没有多少影响),它们会被删除。由于理解英语语法的规则,这个分词器可以提取英语单词的词干 。英语 分词器会产生下面的词条:set, shape, semi, transpar, call, set_tran, 5 注意看 transparent、 calling 和 set_trans 已经变为词根格式。什么时候使用分析器当我们索引一个文档,它的全文域被分析成词条以用来创建倒排索引。但是,当我们在全文域搜索的时候,我们需要将查询字符串通过相同的分析过程,以保证我们搜索的词条格式与索引中的词条格式一致。全文查询,理解每个域是如何定义的,因此它们可以做正确的事:当你查询一个全文域时, 会对查询字符串应用相同的分析器,以产生正确的搜索词条列表。当你查询一个精确值域时,不会分析查询字符串,而是搜索你指定的精确值。举个例子ES中每天一条数据, 按照如下方式查询:GET /_search?q=2014 # 12 results GET /_search?q=2014-09-15 # 12 results ! GET /_search?q=date:2014-09-15 # 1 result GET /_search?q=date:2014 # 0 results ! 为什么返回那样的结果?date 域包含一个精确值:单独的词条 2014-09-15。_all 域是一个全文域,所以分词进程将日期转化为三个词条:2014, 09, 和 15。当我们在_all域查询 2014,它匹配所有的12条推文,因为它们都含有 2014 :GET /_search?q=2014 # 12 results 当我们在_all域查询 2014-09-15,它首先分析查询字符串,产生匹配 2014,09, 或 15 中任意词条的查询。这也会匹配所有12条推文,因为它们都含有 2014 :GET /_search?q=2014-09-15 # 12 results ! 当我们在 date 域查询 2014-09-15,它寻找精确日期,只找到一个推文:GET /_search?q=date:2014-09-15 # 1 result 当我们在 date 域查询 2014,它找不到任何文档,因为没有文档含有这个精确日志:GET /_search?q=date:2014 # 0 results ! 来源:https://www.pdai.tech/md/db/nosql-es/elasticsearch-y-th-2.html(三):ElasticSearch 安装与基础使用安装 ElasticSearchElasticSearch 是基于Java平台的,所以先要安装Java。平台确认这里我准备了一台Centos7虚拟机, 为方便选择后续安装的版本,所以需要看下系统版本信息。[root@centos ~]# uname -a Linux centos 3.10.0-862.el7.x86_64 #1 SMP Fri Apr 20 16:44:24 UTC 2018 x86_64 x86_64 x86_64 GNU/Linux 安装Java安装 Elasticsearch 之前,你需要先安装一个较新的版本的 Java,最好的选择是,你可以从http://www.java.com/获得官方提供的最新版本的 Java。安装以后,确认是否安装成功:[root@centos ~]# java --version openjdk 14.0.2 2020-07-14 OpenJDK Runtime Environment 20.3 (slowdebug build 14.0.2+12) OpenJDK 64-Bit Server VM 20.3 (slowdebug build 14.0.2+12, mixed mode, sharing) 下载ElasticSearch从https://www.elastic.co/cn/downloads/elasticsearch下载ElasticSearch比如可以通过curl下载[root@centos opt]# curl -O https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-7.12.0-linux-x86_64.tar.gz % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 解压[root@centos opt]# tar zxvf /opt/elasticsearch-7.12.0-linux-x86_64.tar.gz ... [root@centos opt]# ll | grep elasticsearch drwxr-xr-x 9 root root 4096 Mar 18 14:21 elasticsearch-7.12.0 -rw-r--r-- 1 root root 327497331 Apr 5 21:05 elasticsearch-7.12.0-linux-x86_64.tar.gz 增加elasticSearch用户必须创建一个非root用户来运行ElasticSearch(ElasticSearch5及以上版本,基于安全考虑,强制规定不能以root身份运行。)如果你使用root用户来启动ElasticSearch,则会有如下错误信息:[root@centos opt]# cd elasticsearch-7.12.0/ [root@centos elasticsearch-7.12.0]# ./bin/elasticsearch [2021-04-05T21:36:46,510][ERROR][o.e.b.ElasticsearchUncaughtExceptionHandler] [centos] uncaught exception in thread [main] org.elasticsearch.bootstrap.StartupException: java.lang.RuntimeException: can not run elasticsearch as root at org.elasticsearch.bootstrap.Elasticsearch.init(Elasticsearch.java:163) ~[elasticsearch-7.12.0.jar:7.12.0] at org.elasticsearch.bootstrap.Elasticsearch.execute(Elasticsearch.java:150) ~[elasticsearch-7.12.0.jar:7.12.0] at org.elasticsearch.cli.EnvironmentAwareCommand.execute(EnvironmentAwareCommand.java:75) ~[elasticsearch-7.12.0.jar:7.12.0] at org.elasticsearch.cli.Command.mainWithoutErrorHandling(Command.java:116) ~[elasticsearch-cli-7.12.0.jar:7.12.0] at org.elasticsearch.cli.Command.main(Command.java:79) ~[elasticsearch-cli-7.12.0.jar:7.12.0] at org.elasticsearch.bootstrap.Elasticsearch.main(Elasticsearch.java:115) ~[elasticsearch-7.12.0.jar:7.12.0] at org.elasticsearch.bootstrap.Elasticsearch.main(Elasticsearch.java:81) ~[elasticsearch-7.12.0.jar:7.12.0] Caused by: java.lang.RuntimeException: can not run elasticsearch as root at org.elasticsearch.bootstrap.Bootstrap.initializeNatives(Bootstrap.java:101) ~[elasticsearch-7.12.0.jar:7.12.0] at org.elasticsearch.bootstrap.Bootstrap.setup(Bootstrap.java:168) ~[elasticsearch-7.12.0.jar:7.12.0] at org.elasticsearch.bootstrap.Bootstrap.init(Bootstrap.java:397) ~[elasticsearch-7.12.0.jar:7.12.0] at org.elasticsearch.bootstrap.Elasticsearch.init(Elasticsearch.java:159) ~[elasticsearch-7.12.0.jar:7.12.0] ... 6 more uncaught exception in thread [main] java.lang.RuntimeException: can not run elasticsearch as root at org.elasticsearch.bootstrap.Bootstrap.initializeNatives(Bootstrap.java:101) at org.elasticsearch.bootstrap.Bootstrap.setup(Bootstrap.java:168) at org.elasticsearch.bootstrap.Bootstrap.init(Bootstrap.java:397) at org.elasticsearch.bootstrap.Elasticsearch.init(Elasticsearch.java:159) at org.elasticsearch.bootstrap.Elasticsearch.execute(Elasticsearch.java:150) at org.elasticsearch.cli.EnvironmentAwareCommand.execute(EnvironmentAwareCommand.java:75) at org.elasticsearch.cli.Command.mainWithoutErrorHandling(Command.java:116) at org.elasticsearch.cli.Command.main(Command.java:79) at org.elasticsearch.bootstrap.Elasticsearch.main(Elasticsearch.java:115) at org.elasticsearch.bootstrap.Elasticsearch.main(Elasticsearch.java:81) For complete error details, refer to the log at /opt/elasticsearch-7.12.0/logs/elasticsearch.log 2021-04-05 13:36:46,979269 UTC [8846] INFO Main.cc@106 Parent process died - ML controller exiting 所以我们增加一个独立的elasticsearch用户来运行# 增加elasticsearch用户 [root@centos elasticsearch-7.12.0]# useradd elasticsearch [root@centos elasticsearch-7.12.0]# passwd elasticsearch Changing password for user elasticsearch. New password: BAD PASSWORD: The password contains the user name in some form Retype new password: passwd: all authentication tokens updated successfully. # 修改目录权限至新增的elasticsearch用户 [root@centos elasticsearch-7.12.0]# chown -R elasticsearch /opt/elasticsearch-7.12.0 # 增加data和log存放区,并赋予elasticsearch用户权限 [root@centos elasticsearch-7.12.0]# mkdir -p /data/es [root@centos elasticsearch-7.12.0]# chown -R elasticsearch /data/es [root@centos elasticsearch-7.12.0]# mkdir -p /var/log/es [root@centos elasticsearch-7.12.0]# chown -R elasticsearch /var/log/es 然后修改上述的data和log路径,vi /opt/elasticsearch-7.12.0/config/elasticsearch.yml# -------------------------------------- Paths --------------------------------------- # # Path to directory where to store the data (separate multiple locations by comma): # path.data: /data/es # # Path to log files: # path.logs: /var/log/es 修改Linux系统的限制配置1.修改系统中允许应用最多创建多少文件等的限制权限。Linux默认来说,一般限制应用最多创建的文件是65535个。但是ES至少需要65536的文件创建权限。2.修改系统中允许用户启动的进程开启多少个线程。默认的Linux限制root用户开启的进程可以开启任意数量的线程,其他用户开启的进程可以开启1024个线程。必须修改限制数为4096+。因为ES至少需要4096的线程池预备。ES在5.x版本之后,强制要求在linux中不能使用root用户启动ES进程。所以必须使用其他用户启动ES进程才可以。3.Linux低版本内核为线程分配的内存是128K。4.x版本的内核分配的内存更大。如果虚拟机的内存是1G,最多只能开启3000+个线程数。至少为虚拟机分配1.5G以上的内存。修改如下配置[root@centos elasticsearch-7.12.0]# vi /etc/security/limits.conf elasticsearch soft nofile 65536 elasticsearch hard nofile 65536 elasticsearch soft nproc 4096 elasticsearch hard nproc 4096 启动ElasticSearch[root@centos elasticsearch-7.12.0]# su elasticsearch [elasticsearch@centos elasticsearch-7.12.0]$ ./bin/elasticsearch -d [2021-04-05T22:03:38,332][INFO ][o.e.n.Node ] [centos] version[7.12.0], pid[13197], build[default/tar/78722783c38caa25a70982b5b042074cde5d3b3a/2021-03-18T06:17:15.410153305Z], OS[Linux/3.10.0-862.el7.x86_64/amd64], JVM[AdoptOpenJDK/OpenJDK 64-Bit Server VM/15.0.1/15.0.1+9] [2021-04-05T22:03:38,348][INFO ][o.e.n.Node ] [centos] JVM home [/opt/elasticsearch-7.12.0/jdk], using bundled JDK [true] [2021-04-05T22:03:38,348][INFO ][o.e.n.Node ] [centos] JVM arguments [-Xshare:auto, -Des.networkaddress.cache.ttl=60, -Des.networkaddress.cache.negative.ttl=10, -XX:+AlwaysPreTouch, -Xss1m, -Djava.awt.headless=true, -Dfile.encoding=UTF-8, -Djna.nosys=true, -XX:-OmitStackTraceInFastThrow, -XX:+ShowCodeDetailsInExceptionMessages, -Dio.netty.noUnsafe=true, -Dio.netty.noKeySetOptimization=true, -Dio.netty.recycler.maxCapacityPerThread=0, -Dio.netty.allocator.numDirectArenas=0, -Dlog4j.shutdownHookEnabled=false, -Dlog4j2.disable.jmx=true, -Djava.locale.providers=SPI,COMPAT, --add-opens=java.base/java.io=ALL-UNNAMED, -XX:+UseG1GC, -Djava.io.tmpdir=/tmp/elasticsearch-17264135248464897093, -XX:+HeapDumpOnOutOfMemoryError, -XX:HeapDumpPath=data, -XX:ErrorFile=logs/hs_err_pid%p.log, -Xlog:gc*,gc+age=trace,safepoint:file=logs/gc.log:utctime,pid,tags:filecount=32,filesize=64m, -Xms1894m, -Xmx1894m, -XX:MaxDirectMemorySize=993001472, -XX:G1HeapRegionSize=4m, -XX:InitiatingHeapOccupancyPercent=30, -XX:G1ReservePercent=15, -Des.path.home=/opt/elasticsearch-7.12.0, -Des.path.conf=/opt/elasticsearch-7.12.0/config, -Des.distribution.flavor=default, -Des.distribution.type=tar, -Des.bundled_jdk=true] 查看安装是否成功[root@centos ~]# netstat -ntlp | grep 9200 tcp6 0 0 127.0.0.1:9200 :::* LISTEN 13549/java tcp6 0 0 ::1:9200 :::* LISTEN 13549/java [root@centos ~]# curl 127.0.0.1:9200 { "name" : "centos", "cluster_name" : "elasticsearch", "cluster_uuid" : "ihttW8b2TfWSkwf_YgPH2Q", "version" : { "number" : "7.12.0", "build_flavor" : "default", "build_type" : "tar", "build_hash" : "78722783c38caa25a70982b5b042074cde5d3b3a", "build_date" : "2021-03-18T06:17:15.410153305Z", "build_snapshot" : false, "lucene_version" : "8.8.0", "minimum_wire_compatibility_version" : "6.8.0", "minimum_index_compatibility_version" : "6.0.0-beta1" }, "tagline" : "You Know, for Search" } 安装KibanaKibana是界面化的查询数据的工具,下载时尽量下载与ElasicSearch一致的版本。下载Kibana从https://www.elastic.co/cn/downloads/kibana下载Kibana解压[root@centos opt]# tar -vxzf kibana-7.12.0-linux-x86_64.tar.gz 使用elasticsearch用户权限[root@centos opt]# chown -R elasticsearch /opt/kibana-7.12.0-linux-x86_64 #配置Kibana的远程访问 [root@centos opt]# vi /opt/kibana-7.12.0-linux-x86_64/config/kibana.yml server.host: 0.0.0.0 启动需要切换至elasticsearch用户。[root@centos opt]# su elasticsearch [elasticsearch@centos opt]$ cd /opt/kibana-7.12.0-linux-x86_64/ [elasticsearch@centos kibana-7.12.0-linux-x86_64]$ ./bin/kibana log [22:30:22.185] [info][plugins-service] Plugin "osquery" is disabled. log [22:30:22.283] [warning][config][deprecation] Config key [monitoring.cluster_alerts.email_notifications.email_address] will be required for email notifications to work in 8.0." log [22:30:22.482] [info][plugins-system] Setting up [100] plugins: [taskManager,licensing,globalSearch,globalSearchProviders,banners,code,usageCollection,xpackLegacy,telemetryCollectionManager,telemetry,telemetryCollectionXpack,kibanaUsageCollection,securityOss,share,newsfeed,mapsLegacy,kibanaLegacy,translations,legacyExport,embeddable,uiActionsEnhanced,expressions,charts,esUiShared,bfetch,data,home,observability,console,consoleExtensions,apmOss,searchprofiler,painlessLab,grokdebugger,management,indexPatternManagement,advancedSettings,fileUpload,savedObjects,visualizations,visTypeVislib,visTypeVega,visTypeTimelion,features,licenseManagement,watcher,canvas,visTypeTagcloud,visTypeTable,visTypeMetric,visTypeMarkdown,tileMap,regionMap,visTypeXy,graph,timelion,dashboard,dashboardEnhanced,visualize,visTypeTimeseries,inputControlVis,discover,discoverEnhanced,savedObjectsManagement,spaces,security,savedObjectsTagging,maps,lens,reporting,lists,encryptedSavedObjects,dashboardMode,dataEnhanced,cloud,upgradeAssistant,snapshotRestore,fleet,indexManagement,rollup,remoteClusters,crossClusterReplication,indexLifecycleManagement,enterpriseSearch,beatsManagement,transform,ingestPipelines,eventLog,actions,alerts,triggersActionsUi,stackAlerts,ml,securitySolution,case,infra,monitoring,logstash,apm,uptime] log [22:30:22.483] [info][plugins][taskManager] TaskManager is identified by the Kibana UUID: xxxxxx ... 如果是后台启动:[elasticsearch@centos kibana-7.12.0-linux-x86_64]$ nohup ./bin/kibana & 界面访问可以导入simple data查看数据其实以上的安装各个版本大同小异,都相差不大,所以,没有按目前的新版本来进行安装演示。官方网站也有具体的安装操作步骤,也可以参考。配置密码访问使用基本许可证时,默认情况下禁用Elasticsearch安全功能。由于我测试环境是放在公网上的,所以需要设置下密码访问。相关文档可以参考:https://www.elastic.co/guide/en/elasticsearch/reference/7.12/security-minimal-setup.html停止kibana和elasticsearch服务将xpack.security.enabled设置添加到ES\_PATH\_CONF/elasticsearch.yml文件并将值设置为true启动elasticsearch (./bin/elasticsearch -d)执行如下密码设置器,./bin/elasticsearch-setup-passwords interactive来设置各个组件的密码将elasticsearch.username设置添加到KIB\_PATH\_CONF/kibana.yml 文件并将值设置给elastic用户: elasticsearch.username: "elastic"创建kibana keystore, ./bin/kibana-keystore create在kibana keystore 中添加密码 ./bin/kibana-keystore add elasticsearch.password重启kibana 服务即可 nohup ./bin/kibana &然后就可以使用密码登录了:查询和聚合的基础使用安装完ElasticSearch 和 Kibana后,为了快速上手,我们通过官网GitHub提供的一个数据进行入门学习,主要包括查询数据和聚合数据。从索引文档开始索引一个文档PUT /customer/_doc/1 { "name": "John Doe" } 为了方便测试,我们使用kibana的dev tool来进行学习测试:查询刚才插入的文档学习准备:批量索引文档ES 还提供了批量操作,比如这里我们可以使用批量操作来插入一些数据,供我们在后面学习使用。使用批量来批处理文档操作比单独提交请求要快得多,因为它减少了网络往返。下载测试数据数据是index为bank,accounts.json 下载地址:https://github.com/elastic/elasticsearch/blob/v6.8.18/docs/src/test/resources/accounts.json(如果你无法下载,也可以clone ES的官方仓库:https://github.com/elastic/elasticsearch,选择本文中使用的版本分支,然后进入/docs/src/test/resources/accounts.json目录获取)。数据的格式如下:{ "account_number": 0, "balance": 16623, "firstname": "Bradshaw", "lastname": "Mckenzie", "age": 29, "gender": "F", "address": "244 Columbus Place", "employer": "Euron", "email": "bradshawmckenzie@euron.com", "city": "Hobucken", "state": "CO" } 批量插入数据将accounts.json拷贝至指定目录,我这里放在/opt/下面,然后执行:curl -H "Content-Type: application/json" -XPOST "localhost:9200/bank/_bulk?pretty&refresh" --data-binary "@/opt/accounts.json" 查看状态[elasticsearch@centos root]$ curl "localhost:9200/_cat/indices?v=true" | grep bank % Total % Received % Xferd Average Speed Time Time Time Current Dload Upload Total Spent Left Speed 100 1524 100 1524 0 0 119k 0 --:--:-- --:--:-- --:--:-- 124k yellow open bank yq3eSlAWRMO2Td0Sl769rQ 1 1 1000 0 379.2kb 379.2kb 查询数据我们通过kibana来进行查询测试。查询所有match_all表示查询所有的数据,sort即按照什么字段排序GET /bank/_search { "query": { "match_all": {} }, "sort": [ { "account_number": "asc" } ] } 结果相关字段解释took – Elasticsearch运行查询所花费的时间(以毫秒为单位)timed_out –搜索请求是否超时_shards - 搜索了多少个碎片,以及成功,失败或跳过了多少个碎片的细目分类。max_score – 找到的最相关文档的分数hits.total.value - 找到了多少个匹配的文档hits.sort - 文档的排序位置(不按相关性得分排序时)hits._score - 文档的相关性得分(使用match_all时不适用)分页查询(from+size)本质上就是from和size两个字段GET /bank/_search { "query": { "match_all": {} }, "sort": [ { "account_number": "asc" } ], "from": 10, "size": 10 } 结果指定字段查询:match如果要在字段中搜索特定字词,可以使用match; 如下语句将查询address 字段中包含 mill 或者 lane的数据。GET /bank/_search { "query": { "match": { "address": "mill lane" } } } 结果(由于ES底层是按照分词索引的,所以上述查询结果是address 字段中包含 mill 或者 lane的数据)。查询段落匹配:match_phrase如果我们希望查询的条件是 address字段中包含 "mill lane",则可以使用match_phrase。GET /bank/_search { "query": { "match_phrase": { "address": "mill lane" } } } 结果多条件查询: bool如果要构造更复杂的查询,可以使用bool查询来组合多个查询条件。例如,以下请求在bank索引中搜索40岁客户的帐户,但不包括居住在爱达荷州(ID)的任何人。GET /bank/_search { "query": { "bool": { "must": [ { "match": { "age": "40" } } ], "must_not": [ { "match": { "state": "ID" } } ] } } } 结果must, should, must_not 和 filter 都是bool查询的子句。那么filter和上述query子句有啥区别呢?查询条件:query or filter先看下如下查询, 在bool查询的子句中同时具备query/must 和 filter。GET /bank/_search { "query": { "bool": { "must": [ { "match": { "state": "ND" } } ], "filter": [ { "term": { "age": "40" } }, { "range": { "balance": { "gte": 20000, "lte": 30000 } } } ] } } } 结果两者都可以写查询条件,而且语法也类似。区别在于,query 上下文的条件是用来给文档打分的,匹配越好 _score 越高;filter 的条件只产生两种结果:符合与不符合,后者被过滤掉。所以,我们进一步看只包含filter的查询。GET /bank/_search { "query": { "bool": { "filter": [ { "term": { "age": "40" } }, { "range": { "balance": { "gte": 20000, "lte": 30000 } } } ] } } } 结果,显然无_score聚合查询:Aggregation我们知道SQL中有group by,在ES中它叫Aggregation,即聚合运算。简单聚合比如我们希望计算出account每个州的统计数量, 使用aggs关键字对state字段聚合,被聚合的字段无需对分词统计,所以使用state.keyword对整个字段统计。GET /bank/_search { "size": 0, "aggs": { "group_by_state": { "terms": { "field": "state.keyword" } } } } 结果因为无需返回条件的具体数据, 所以设置size=0,返回hits为空。doc_count表示bucket中每个州的数据条数。嵌套聚合ES还可以处理个聚合条件的嵌套。比如承接上个例子, 计算每个州的平均结余。涉及到的就是在对state分组的基础上,嵌套计算avg(balance):GET /bank/_search { "size": 0, "aggs": { "group_by_state": { "terms": { "field": "state.keyword" }, "aggs": { "average_balance": { "avg": { "field": "balance" } } } } } } 结果对聚合结果排序可以通过在aggs中对嵌套聚合的结果进行排序。比如承接上个例子, 对嵌套计算出的avg(balance),这里是average_balance,进行排序。GET /bank/_search { "size": 0, "aggs": { "group_by_state": { "terms": { "field": "state.keyword", "order": { "average_balance": "desc" } }, "aggs": { "average_balance": { "avg": { "field": "balance" } } } } } } 结果来源:https://www.pdai.tech/md/db/nosql-es/elasticsearch-x-usage.html(四):ElasticSearch 索引管理索引管理的引入我们在前文中增加文档时,如下的语句会动态创建一个customer的index:PUT /customer/_doc/1 { "name": "John Doe" } 而这个index实际上已经自动创建了它里面的字段(name)的类型。我们不妨看下它自动创建的mapping:{ "mappings": { "_doc": { "properties": { "name": { "type": "text", "fields": { "keyword": { "type": "keyword", "ignore_above": 256 } } } } } } } 那么如果我们需要对这个建立索引的过程做更多的控制:比如想要确保这个索引有数量适中的主分片,并且在我们索引任何数据之前,分析器和映射已经被建立好。那么就会引入两点:第一个禁止自动创建索引,第二个是手动创建索引。禁止自动创建索引可以通过在 config/elasticsearch.yml 的每个节点下添加下面的配置:action.auto_create_index: false 手动创建索引就是接下来文章的内容。更多关于 ElasticSearch 数据库的学习文章,请参阅:NoSQL 数据库之 ElasticSearch ,本系列持续更新中。索引的格式在请求体里面传入设置或类型映射,如下所示:PUT /my_index { "settings": { ... any settings ... }, "mappings": { "properties": { ... any properties ... } } } settings: 用来设置分片,副本等配置信息mappings: 字段映射,类型等properties: 由于type在后续版本中会被Deprecated, 所以无需被type嵌套索引管理操作我们通过kibana的devtool来学习索引的管理操作。创建索引我们创建一个user 索引test-index-users,其中包含三个属性:name,age, remarks; 存储在一个分片一个副本上。PUT /test-index-users { "settings": { "number_of_shards": 1, "number_of_replicas": 1 }, "mappings": { "properties": { "name": { "type": "text", "fields": { "keyword": { "type": "keyword", "ignore_above": 256 } } }, "age": { "type": "long" }, "remarks": { "type": "text" } } } } 执行结果插入测试数据查看数据我们再测试下不匹配的数据类型(age):POST /test-index-users/_doc { "name": "test user", "age": "error_age", "remarks": "hello eeee" } 你可以看到无法类型不匹配的错误:修改索引查看刚才的索引,curl 'localhost:9200/_cat/indices?v' | grep usersyellow open test-index-users LSaIB57XSC6uVtGQHoPYxQ 1 1 1 0 4.4kb 4.4kb 我们注意到刚创建的索引的状态是yellow的,因为我测试的环境是单点环境,无法创建副本,但是在上述number_of_replicas配置中设置了副本数是1;所以在这个时候我们需要修改索引的配置。修改副本数量为0:PUT /test-index-users/_settings { "settings": { "number_of_replicas": 0 } } 再次查看状态:green open test-index-users LSaIB57XSC6uVtGQHoPYxQ 1 1 1 0 4.4kb 4.4kb 打开/关闭索引关闭索引一旦索引被关闭,那么这个索引只能显示元数据信息,不能够进行读写操作。当关闭以后,再插入数据时:打开索引打开后又可以重新写数据了删除索引最后我们将创建的test-index-users删除。DELETE /test-index-users 查看索引由于test-index-users被删除,所以我们看下之前bank的索引的信息。mappingGET /bank/_mapping settingsGET /bank/_settings Kibana 管理索引在Kibana如下路径,我们可以查看和管理索引前文介绍了索引的一些操作,特别是手动创建索引,但是批量和脚本化必然需要提供一种模板方式快速构建和管理索引,这就是本文要介绍的索引模板(Index Template),它是一种告诉Elasticsearch在创建索引时如何配置索引的方法。为了更好的复用性,在7.8中还引入了组件模板。索引模板索引模板是一种告诉Elasticsearch在创建索引时如何配置索引的方法。使用方式在创建索引之前可以先配置模板,这样在创建索引(手动创建索引或通过对文档建立索引)时,模板设置将用作创建索引的基础。模板类型模板有两种类型:索引模板和组件模板。组件模板是可重用的构建块,用于配置映射,设置和别名;它们不会直接应用于一组索引。索引模板可以包含组件模板的集合,也可以直接指定设置,映射和别名。索引模板中的优先级可组合模板优先于旧模板。如果没有可组合模板匹配给定索引,则旧版模板可能仍匹配并被应用。如果使用显式设置创建索引并且该索引也与索引模板匹配,则创建索引请求中的设置将优先于索引模板及其组件模板中指定的设置。如果新数据流或索引与多个索引模板匹配,则使用优先级最高的索引模板。内置索引模板Elasticsearch具有内置索引模板,每个索引模板的优先级为100,适用于以下索引模式:logs-*-*metrics-*-*synthetics-*-*所以在涉及内建索引模板时,要避免索引模式冲突。更多可以参考https://www.elastic.co/guide/en/elasticsearch/reference/current/index-templates.html。案例首先创建两个索引组件模板:PUT _component_template/component_template1 { "template": { "mappings": { "properties": { "@timestamp": { "type": "date" } } } } } PUT _component_template/runtime_component_template { "template": { "mappings": { "runtime": { "day_of_week": { "type": "keyword", "script": { "source": "emit(doc['@timestamp'].value.dayOfWeekEnum.getDisplayName(TextStyle.FULL, Locale.ROOT))" } } } } } } 执行结果如下创建使用组件模板的索引模板PUT _index_template/template_1 { "index_patterns": ["bar*"], "template": { "settings": { "number_of_shards": 1 }, "mappings": { "_source": { "enabled": true }, "properties": { "host_name": { "type": "keyword" }, "created_at": { "type": "date", "format": "EEE MMM dd HH:mm:ss Z yyyy" } } }, "aliases": { "mydata": { } } }, "priority": 500, "composed_of": ["component_template1", "runtime_component_template"], "version": 3, "_meta": { "description": "my custom" } } 执行结果如下创建一个匹配bar*的索引bar-testPUT /bar-test 然后获取mappingGET /bar-test/_mapping 执行结果如下模拟多组件模板由于模板不仅可以由多个组件模板组成,还可以由索引模板自身组成;那么最终的索引设置将是什么呢?ElasticSearch设计者考虑到这个,提供了API进行模拟组合后的模板的配置。模拟某个索引结果比如上面的template_1, 我们不用创建bar*的索引(这里模拟bar-pdai-test),也可以模拟计算出索引的配置:POST /_index_template/_simulate_index/bar-pdai-test 执行结果如下模拟组件模板结果当然,由于template\_1模板是由两个组件模板组合的,我们也可以模拟出template\_1被组合后的索引配置:POST /_index_template/_simulate/template_1 执行结果如下:{ "template" : { "settings" : { "index" : { "number_of_shards" : "1" } }, "mappings" : { "runtime" : { "day_of_week" : { "type" : "keyword", "script" : { "source" : "emit(doc['@timestamp'].value.dayOfWeekEnum.getDisplayName(TextStyle.FULL, Locale.ROOT))", "lang" : "painless" } } }, "properties" : { "@timestamp" : { "type" : "date" }, "created_at" : { "type" : "date", "format" : "EEE MMM dd HH:mm:ss Z yyyy" }, "host_name" : { "type" : "keyword" } } }, "aliases" : { "mydata" : { } } }, "overlapping" : [ ] } 模拟组件模板和自身模板结合后的结果新建两个模板PUT /_component_template/ct1 { "template": { "settings": { "index.number_of_shards": 2 } } } PUT /_component_template/ct2 { "template": { "settings": { "index.number_of_replicas": 0 }, "mappings": { "properties": { "@timestamp": { "type": "date" } } } } } 模拟在两个组件模板的基础上,添加自身模板的配置POST /_index_template/_simulate { "index_patterns": ["my*"], "template": { "settings" : { "index.number_of_shards" : 3 } }, "composed_of": ["ct1", "ct2"] } 执行的结果如下{ "template" : { "settings" : { "index" : { "number_of_shards" : "3", "number_of_replicas" : "0" } }, "mappings" : { "properties" : { "@timestamp" : { "type" : "date" } } }, "aliases" : { } }, "overlapping" : [ ] } 链接:https://pdai.tech/md/db/nosql-es/elasticsearch-x-index-template.html(五):ElasticSearch DSL 查询原理与实践DSL 查询之复合查询在查询中会有多种条件组合的查询,在 ElasticSearch 中叫复合查询。它提供了5种复合查询方式:bool query(布尔查询)、boosting query(提高查询)、constant\_score(固定分数查询)、dis\_max(最佳匹配查询)、function_score(函数查询)。复合查询引入在前文中,我们使用bool查询来组合多个查询条件。比如之前介绍的语句:GET /bank/_search { "query": { "bool": { "must": [ { "match": { "age": "40" } } ], "must_not": [ { "match": { "state": "ID" } } ] } } } 这种查询就是本文要介绍的复合查询,并且bool查询只是复合查询一种。bool query(布尔查询)通过布尔逻辑将较小的查询组合成较大的查询。概念Bool查询语法有以下特点子查询可以任意顺序出现可以嵌套多个查询,包括bool查询如果bool查询中没有must条件,should中必须至少满足一条才会返回结果。bool查询包含四种操作符,分别是must,should,must_not,filter。他们均是一种数组,数组里面是对应的判断条件。must: 必须匹配。贡献算分must_not:过滤子句,必须不能匹配,但不贡献算分should: 选择性匹配,至少满足一条。贡献算分filter: 过滤子句,必须匹配,但不贡献算分一些例子看下官方举例例子1POST _search { "query": { "bool" : { "must" : { "term" : { "user.id" : "kimchy" } }, "filter": { "term" : { "tags" : "production" } }, "must_not" : { "range" : { "age" : { "gte" : 10, "lte" : 20 } } }, "should" : [ { "term" : { "tags" : "env1" } }, { "term" : { "tags" : "deployed" } } ], "minimum_should_match" : 1, "boost" : 1.0 } } } 在filter元素下指定的查询对评分没有影响 , 评分返回为0。分数仅受已指定查询的影响。例子2GET _search { "query": { "bool": { "filter": { "term": { "status": "active" } } } } } 这个例子查询查询为所有文档分配0分,因为没有指定评分查询。例子3GET _search { "query": { "bool": { "must": { "match_all": {} }, "filter": { "term": { "status": "active" } } } } } 此bool查询具有match_all查询,该查询为所有文档指定1.0分。例子4GET /_search { "query": { "bool": { "should": [ { "match": { "name.first": { "query": "shay", "_name": "first" } } }, { "match": { "name.last": { "query": "banon", "_name": "last" } } } ], "filter": { "terms": { "name.last": [ "banon", "kimchy" ], "_name": "test" } } } } } 每个query条件都可以有一个_name属性,用来追踪搜索出的数据到底match了哪个条件。boosting query(提高查询)不同于bool查询,bool查询中只要一个子查询条件不匹配那么搜索的数据就不会出现。而boosting query则是降低显示的权重/优先级(即score)。概念比如搜索逻辑是 name = 'apple' and type ='fruit',对于只满足部分条件的数据,不是不显示,而是降低显示的优先级(即score)。例子首先创建数据POST /test-dsl-boosting/_bulk { "index": { "_id": 1 }} { "content":"Apple Mac" } { "index": { "_id": 2 }} { "content":"Apple Fruit" } { "index": { "_id": 3 }} { "content":"Apple employee like Apple Pie and Apple Juice" } 对匹配pie的做降级显示处理GET /test-dsl-boosting/_search { "query": { "boosting": { "positive": { "term": { "content": "apple" } }, "negative": { "term": { "content": "pie" } }, "negative_boost": 0.5 } } } 执行结果如下constant_score(固定分数查询)查询某个条件时,固定的返回指定的score;显然当不需要计算score时,只需要filter条件即可,因为filter context忽略score。例子首先创建数据POST /test-dsl-constant/_bulk { "index": { "_id": 1 }} { "content":"Apple Mac" } { "index": { "_id": 2 }} { "content":"Apple Fruit" } 查询appleGET /test-dsl-constant/_search { "query": { "constant_score": { "filter": { "term": { "content": "apple" } }, "boost": 1.2 } } } 执行结果如下dis_max(最佳匹配查询)分离最大化查询(Disjunction Max Query)指的是: 将任何与任一查询匹配的文档作为结果返回,但只将最佳匹配的评分作为查询的评分结果返回。更多关于 ElasticSearch 数据库的学习文章,请参阅:NoSQL 数据库之 ElasticSearch ,本系列持续更新中。例子假设有个网站允许用户搜索博客的内容,以下面两篇博客内容文档为例:POST /test-dsl-dis-max/_bulk { "index": { "_id": 1 }} {"title": "Quick brown rabbits","body": "Brown rabbits are commonly seen."} { "index": { "_id": 2 }} {"title": "Keeping pets healthy","body": "My quick brown fox eats rabbits on a regular basis."} 用户输入词组 “Brown fox” 然后点击搜索按钮。事先,我们并不知道用户的搜索项是会在 title 还是在 body 字段中被找到,但是,用户很有可能是想搜索相关的词组。用肉眼判断,文档 2 的匹配度更高,因为它同时包括要查找的两个词:现在运行以下 bool 查询:GET /test-dsl-dis-max/_search { "query": { "bool": { "should": [ { "match": { "title": "Brown fox" }}, { "match": { "body": "Brown fox" }} ] } } } 为了理解导致这样的原因,需要看下如何计算评分的。should 条件的计算分数GET /test-dsl-dis-max/_search { "query": { "bool": { "should": [ { "match": { "title": "Brown fox" }}, { "match": { "body": "Brown fox" }} ] } } } 要计算上述分数,首先要计算match的分数1.第一个match 中 brown的分数doc 1 分数 = 0.6931471 2.title中没有fox,所以第一个match 中 brown fox 的分数 = brown分数 + 0 = 0.6931471doc 1 分数 = 0.6931471 + 0 = 0.6931471 3.第二个 match 中 brown分数doc 1 分数 = 0.21110919 doc 2 分数 = 0.160443 4.第二个 match 中 fox分数doc 1 分数 = 0 doc 2 分数 = 0.60996956 5.所以第二个 match 中 brown fox分数 = brown分数 + fox分数doc 1 分数 = 0.21110919 + 0 = 0.21110919 doc 2 分数 = 0.160443 + 0.60996956 = 0.77041256 6.所以整个语句分数, should分数 = 第一个match + 第二个match分数doc 1 分数 = 0.6931471 + 0.21110919 = 0.90425634 doc 2 分数 = 0 + 0.77041256 = 0.77041256 引入了dis_max不使用 bool 查询,可以使用 dis_max 即分离 最大化查询(Disjunction Max Query) 。分离(Disjunction)的意思是 或(or) ,这与可以把结合(conjunction)理解成 与(and) 相对应。分离最大化查询(Disjunction Max Query)指的是: 将任何与任一查询匹配的文档作为结果返回,但只将最佳匹配的评分作为查询的评分结果返回 :GET /test-dsl-dis-max/_search { "query": { "dis_max": { "queries": [ { "match": { "title": "Brown fox" }}, { "match": { "body": "Brown fox" }} ], "tie_breaker": 0 } } } 0.77041256怎么来的呢? 下文给你解释它如何计算出来的。dis_max 条件的计算分数分数 = 第一个匹配条件分数+ tie_breaker *第二个匹配的条件的分数GET /test-dsl-dis-max/_search { "query": { "dis_max": { "queries": [ { "match": { "title": "Brown fox" }}, { "match": { "body": "Brown fox" }} ], "tie_breaker": 0 } } } doc 1 分数 = 0.6931471 + 0.21110919 * 0 = 0.6931471 doc 2 分数 = 0.77041256 = 0.77041256这样你就能理解通过dis_max将doc 2 置前了, 当然这里如果缺省tie_breaker字段的话默认就是0,你还可以设置它的比例(在0到1之间)来控制排名。(显然值为1时和should查询是一致的)。function_score(函数查询)简而言之就是用自定义function的方式来计算_score。可以ES有哪些自定义function呢?script_score 使用自定义的脚本来完全控制分值计算逻辑。如果你需要以上预定义函数之外的功能,可以根据需要通过脚本进行实现。weight 对每份文档适用一个简单的提升,且该提升不会被归约:当weight为2时,结果为2 * _score。random_score 使用一致性随机分值计算来对每个用户采用不同的结果排序方式,对相同用户仍然使用相同的排序方式。field_value_factor 使用文档中某个字段的值来改变_score,比如将受欢迎程度或者投票数量考虑在内。衰减函数(Decay Function) - linear,exp,gauss例子以最简单的random_score 为例。GET /_search { "query": { "function_score": { "query": { "match_all": {} }, "boost": "5", "random_score": {}, "boost_mode": "multiply" } } } 进一步的,它还可以使用上述function的组合(functions)。GET /_search { "query": { "function_score": { "query": { "match_all": {} }, "boost": "5", "functions": [ { "filter": { "match": { "test": "bar" } }, "random_score": {}, "weight": 23 }, { "filter": { "match": { "test": "cat" } }, "weight": 42 } ], "max_boost": 42, "score_mode": "max", "boost_mode": "multiply", "min_score": 42 } } } script_score 可以使用如下方式。GET /_search { "query": { "function_score": { "query": { "match": { "message": "elasticsearch" } }, "script_score": { "script": { "source": "Math.log(2 + doc['my-int'].value)" } } } } } 更多相关内容,可以参考官方文档,PS: 形成体系化认知以后,具体用的时候查询下即可。DSL 查询之全文搜索DSL查询极为常用的是对文本进行搜索,我们叫全文搜索,本文主要对全文搜索进行详解。谈谈如何从官网学习很多读者在看官方文档学习时存在一个误区,以DSL中full text查询为例,其实内容是非常多的, 没有取舍/没重点去阅读,要么需要花很多时间,要么头脑一片浆糊。所以这里重点谈谈我的理解。第一点:全局观,即我们现在学习内容在整个体系的哪个位置?如下图,可以很方便的帮助你构筑这种体系:第二点: 分类别,从上层理解,而不是本身比如Full text Query中,我们只需要把如下的那么多点分为3大类,你的体系能力会大大提升。第三点: 知识点还是API? API类型的是可以查询的,只需要知道大致有哪些功能就可以了。Match类型第一类:match 类型match 查询的步骤在前文中我们已经介绍了match查询。准备一些数据这里我们准备一些数据,通过实例看match 查询的步骤。PUT /test-dsl-match { "settings": { "number_of_shards": 1 }} POST /test-dsl-match/_bulk { "index": { "_id": 1 }} { "title": "The quick brown fox" } { "index": { "_id": 2 }} { "title": "The quick brown fox jumps over the lazy dog" } { "index": { "_id": 3 }} { "title": "The quick brown fox jumps over the quick dog" } { "index": { "_id": 4 }} { "title": "Brown fox brown dog" } 查询数据GET /test-dsl-match/_search { "query": { "match": { "title": "QUICK!" } } } Elasticsearch 执行上面这个 match 查询的步骤是:1.检查字段类型 。标题 title 字段是一个 string 类型( analyzed )已分析的全文字段,这意味着查询字符串本身也应该被分析。2.分析查询字符串 。将查询的字符串 QUICK! 传入标准分析器中,输出的结果是单个项 quick 。因为只有一个单词项,所以 match 查询执行的是单个底层 term 查询。3.查找匹配文档 。用 term 查询在倒排索引中查找 quick 然后获取一组包含该项的文档,本例的结果是文档:1、2 和 3 。4.为每个文档评分 。用 term 查询计算每个文档相关度评分 _score ,这是种将词频(term frequency,即词 quick 在相关文档的 title 字段中出现的频率)和反向文档频率(inverse document frequency,即词 quick 在所有文档的 title 字段中出现的频率),以及字段的长度(即字段越短相关度越高)相结合的计算方式。验证结果match多个词深入我们在上文中复合查询中已经使用了match多个词,比如“Quick pets”; 这里我们通过例子带你更深入理解match多个词。match多个词的本质查询多个词"BROWN DOG!"GET /test-dsl-match/_search { "query": { "match": { "title": "BROWN DOG" } } } 因为 match 查询必须查找两个词(["brown","dog"]),它在内部实际上先执行两次 term 查询,然后将两次查询的结果合并作为最终结果输出。为了做到这点,它将两个 term 查询包入一个 bool 查询中。所以上述查询的结果,和如下语句查询结果是等同的。GET /test-dsl-match/_search { "query": { "bool": { "should": [ { "term": { "title": "brown" } }, { "term": { "title": "dog" } } ] } } } match多个词的逻辑上面等同于should(任意一个满足),是因为 match还有一个operator参数,默认是or, 所以对应的是should。所以上述查询也等同于:GET /test-dsl-match/_search { "query": { "match": { "title": { "query": "BROWN DOG", "operator": "or" } } } } 那么我们如果是需要and操作呢,即同时满足呢?GET /test-dsl-match/_search { "query": { "match": { "title": { "query": "BROWN DOG", "operator": "and" } } } } 等同于GET /test-dsl-match/_search { "query": { "bool": { "must": [ { "term": { "title": "brown" } }, { "term": { "title": "dog" } } ] } } } 控制match的匹配精度如果用户给定 3 个查询词,想查找至少包含其中 2 个的文档,该如何处理?将 operator 操作符参数设置成 and 或者 or 都是不合适的。match 查询支持 minimum_should_match 最小匹配参数,这让我们可以指定必须匹配的词项数用来表示一个文档是否相关。我们可以将其设置为某个具体数字,更常用的做法是将其设置为一个百分数,因为我们无法控制用户搜索时输入的单词数量:GET /test-dsl-match/_search { "query": { "match": { "title": { "query": "quick brown dog", "minimum_should_match": "75%" } } } } 当给定百分比的时候, minimum_should_match 会做合适的事情:在之前三词项的示例中,75% 会自动被截断成 66.6% ,即三个里面两个词。无论这个值设置成什么,至少包含一个词项的文档才会被认为是匹配的。当然也等同于:GET /test-dsl-match/_search { "query": { "bool": { "should": [ { "match": { "title": "quick" }}, { "match": { "title": "brown" }}, { "match": { "title": "dog" }} ], "minimum_should_match": 2 } } } 其它match类型match_pharsematch_phrase在前文中我们已经有了解,我们再看下另外一个例子。GET /test-dsl-match/_search { "query": { "match_phrase": { "title": { "query": "quick brown" } } } } 很多人对它仍然有误解的,比如如下例子:GET /test-dsl-match/_search { "query": { "match_phrase": { "title": { "query": "quick brown f" } } } } 这样的查询是查不出任何数据的,因为前文中我们知道了match本质上是对term组合,match_phrase本质是连续的term的查询,所以f并不是一个分词,不满足term查询,所以最终查不出任何内容了。match\_pharse\_prefix那有没有可以查询出quick brown f的方式呢?ELasticSearch在match_phrase基础上提供了一种可以查最后一个词项是前缀的方法,这样就可以查询quick brown f了GET /test-dsl-match/_search { "query": { "match_phrase_prefix": { "title": { "query": "quick brown f" } } } } (ps: prefix的意思不是整个text的开始匹配,而是最后一个词项满足term的prefix查询而已)。match\_bool\_prefix除了match_phrase_prefix,ElasticSearch还提供了match_bool_prefix查询GET /test-dsl-match/_search { "query": { "match_bool_prefix": { "title": { "query": "quick brown f" } } } } 它们两种方式有啥区别呢?match_bool_prefix本质上可以转换为:GET /test-dsl-match/_search { "query": { "bool" : { "should": [ { "term": { "title": "quick" }}, { "term": { "title": "brown" }}, { "prefix": { "title": "f"}} ] } } } 所以这样你就能理解,match_bool_prefix查询中的quick,brown,f是无序的。multi_match如果我们期望一次对多个字段查询,怎么办呢?ElasticSearch提供了multi_match查询的方式{ "query": { "multi_match" : { "query": "Will Smith", "fields": [ "title", "*_name" ] } } } *表示前缀匹配字段。query string类型第二类:query string 类型query_string此查询使用语法根据运算符(例如AND或)来解析和拆分提供的查询字符串NOT。然后查询在返回匹配的文档之前独立分析每个拆分的文本。可以使用该query_string查询创建一个复杂的搜索,其中包括通配符,跨多个字段的搜索等等。尽管用途广泛,但查询是严格的,如果查询字符串包含任何无效语法,则返回错误。例如:GET /test-dsl-match/_search { "query": { "query_string": { "query": "(lazy dog) OR (brown dog)", "default_field": "title" } } } 这里查询结果,你需要理解本质上查询这四个分词(term)or的结果而已,所以doc 3和4也在其中。对构筑知识体系已经够了,但是它其实还有很多参数和用法。query\_string\_simple该查询使用一种简单的语法来解析提供的查询字符串并将其拆分为基于特殊运算符的术语。然后查询在返回匹配的文档之前独立分析每个术语。尽管其语法比query_string查询更受限制 ,但simple\_query\_string 查询不会针对无效语法返回错误。而是,它将忽略查询字符串的任何无效部分。举例:GET /test-dsl-match/_search { "query": { "simple_query_string" : { "query": "\"over the\" + (lazy | quick) + dog", "fields": ["title"], "default_operator": "and" } } } Interval类型第三类:interval类型Intervals是时间间隔的意思,本质上将多个规则按照顺序匹配。比如:GET /test-dsl-match/_search { "query": { "intervals" : { "title" : { "all_of" : { "ordered" : true, "intervals" : [ { "match" : { "query" : "quick", "max_gaps" : 0, "ordered" : true } }, { "any_of" : { "intervals" : [ { "match" : { "query" : "jump over" } }, { "match" : { "query" : "quick dog" } } ] } } ] } } } } } 因为interval之间是可以组合的,所以它可以表现的很复杂。DSL 查询之 TermDSL查询另一种极为常用的是对词项进行搜索,官方文档中叫”term level“查询,本文主要对term level搜索进行详解。Term查询引入如前文所述,查询分基于文本查询和基于词项的查询:本文主要讲基于词项的查询。Term查询很多比较常用,也不难,就是需要结合实例理解。这里综合官方文档的内容,我设计一个测试场景的数据,以覆盖所有例子。准备数据PUT /test-dsl-term-level { "mappings": { "properties": { "name": { "type": "keyword" }, "programming_languages": { "type": "keyword" }, "required_matches": { "type": "long" } } } } POST /test-dsl-term-level/_bulk { "index": { "_id": 1 }} {"name": "Jane Smith", "programming_languages": [ "c++", "java" ], "required_matches": 2} { "index": { "_id": 2 }} {"name": "Jason Response", "programming_languages": [ "java", "php" ], "required_matches": 2} { "index": { "_id": 3 }} {"name": "Dave Pdai", "programming_languages": [ "java", "c++", "php" ], "required_matches": 3, "remarks": "hello world"} 字段是否存在:exist由于多种原因,文档字段的索引值可能不存在:源JSON中的字段是null或[]该字段已"index" : false在映射中设置字段值的长度超出ignore_above了映射中的设置字段值格式错误,并且ignore_malformed已在映射中定义所以exist表示查找是否存在字段。id查询:idsids 即对id查找GET /test-dsl-term-level/_search { "query": { "ids": { "values": [3, 1] } } } 前缀:prefix通过前缀查找某个字段GET /test-dsl-term-level/_search { "query": { "prefix": { "name": { "value": "Jan" } } } } 分词匹配:term前文最常见的根据分词查询GET /test-dsl-term-level/_search { "query": { "term": { "programming_languages": "php" } } } 多个分词匹配:terms按照读个分词term匹配,它们是or的关系。GET /test-dsl-term-level/_search { "query": { "terms": { "programming_languages": ["php","c++"] } } } 按某个数字字段分词匹配:term set设计这种方式查询的初衷是用文档中的数字字段动态匹配查询满足term的个数。GET /test-dsl-term-level/_search { "query": { "terms_set": { "programming_languages": { "terms": [ "java", "php" ], "minimum_should_match_field": "required_matches" } } } } 通配符:wildcard通配符匹配,比如*GET /test-dsl-term-level/_search { "query": { "wildcard": { "name": { "value": "D*ai", "boost": 1.0, "rewrite": "constant_score" } } } } 范围:range常常被用在数字或者日期范围的查询。GET /test-dsl-term-level/_search { "query": { "range": { "required_matches": { "gte": 3, "lte": 4 } } } } 正则:regexp通过正则表达式查询。以"Jan"开头的name字段。GET /test-dsl-term-level/_search { "query": { "regexp": { "name": { "value": "Ja.*", "case_insensitive": true } } } } 模糊匹配:模糊官方文档对模糊匹配:编辑距离是将一个术语转换为另一个术语所需的一个字符更改的次数。这些更改可以包括:更改字符(box→ fox)删除字符(black→ lack)插入字符(sic→ sick)转置两个相邻字符(act→ cat)GET /test-dsl-term-level/_search { "query": { "fuzzy": { "remarks": { "value": "hell" } } } } 来源:https://www.pdai.tech/md/db/nosql-es/elasticsearch-x-dsl-term.htmlhttps://www.pdai.tech/md/db/nosql-es/elasticsearch-x-dsl-full-text.html(六):ElasticSearch 聚合查询原理与实践除了查询之外,最常用的聚合了,ElasticSearch提供了三种聚合方式:桶聚合(Bucket Aggregation),指标聚合(Metric Aggregation) 和 管道聚合(Pipline Aggregation)。本文主要讲讲桶聚合(Bucket Aggregation)。聚合查询之Bucket聚合聚合的引入我们在SQL结果中常有:SELECT COUNT(color) FROM table GROUP BY color ElasticSearch中桶在概念上类似于 SQL 的分组(GROUP BY),而指标则类似于 COUNT() 、 SUM() 、 MAX() 等统计方法。进而引入了两个概念:桶(Buckets) 满足特定条件的文档的集合指标(Metrics) 对桶内的文档进行统计计算所以ElasticSearch包含3种聚合(Aggregation)方式桶聚合(Bucket Aggregation) - 本文中详解指标聚合(Metric Aggregation) - 下文中讲解管道聚合(Pipline Aggregation) - 再下一篇讲解聚合管道化,简单而言就是上一个聚合的结果成为下个聚合的输入;(PS:指标聚合和桶聚合很多情况下是组合在一起使用的,其实你也可以看到,桶聚合本质上是一种特殊的指标聚合,它的聚合指标就是数据的条数count)。如何理解Bucket聚合如果你直接去看文档,大概有几十种:要么你需要花大量时间学习,要么你已经迷失或者即将迷失在知识点中...所以你需要稍微站在设计者的角度思考下,不难发现设计上大概分为三类(当然有些是第二和第三类的融合)。(图中并没有全部列出内容,因为图要表达的意图我觉得还是比较清楚的,这就够了;有了这种思虑和认知,会大大提升你的认知效率。)。按知识点学习聚合我们先按照官方权威指南中的一个例子,学习Aggregation中的知识点。准备数据让我们先看一个例子。我们将会创建一些对汽车经销商有用的聚合,数据是关于汽车交易的信息:车型、制造商、售价、何时被出售等。首先我们批量索引一些数据:POST /test-agg-cars/_bulk { "index": {}} { "price" : 10000, "color" : "red", "make" : "honda", "sold" : "2014-10-28" } { "index": {}} { "price" : 20000, "color" : "red", "make" : "honda", "sold" : "2014-11-05" } { "index": {}} { "price" : 30000, "color" : "green", "make" : "ford", "sold" : "2014-05-18" } { "index": {}} { "price" : 15000, "color" : "blue", "make" : "toyota", "sold" : "2014-07-02" } { "index": {}} { "price" : 12000, "color" : "green", "make" : "toyota", "sold" : "2014-08-19" } { "index": {}} { "price" : 20000, "color" : "red", "make" : "honda", "sold" : "2014-11-05" } { "index": {}} { "price" : 80000, "color" : "red", "make" : "bmw", "sold" : "2014-01-01" } { "index": {}} { "price" : 25000, "color" : "blue", "make" : "ford", "sold" : "2014-02-12" } 标准的聚合有了数据,开始构建我们的第一个聚合。汽车经销商可能会想知道哪个颜色的汽车销量最好,用聚合可以轻易得到结果,用 terms 桶操作:GET /test-agg-cars/_search { "size" : 0, "aggs" : { "popular_colors" : { "terms" : { "field" : "color.keyword" } } } } 聚合操作被置于顶层参数 aggs 之下(如果你愿意,完整形式 aggregations 同样有效)。然后,可以为聚合指定一个我们想要名称,本例中是:popular_colors 。最后,定义单个桶的类型 terms。结果如下:因为我们设置了 size 参数,所以不会有 hits 搜索结果返回。popular_colors 聚合是作为 aggregations 字段的一部分被返回的。每个桶的 key 都与 color 字段里找到的唯一词对应。它总会包含 doc_count 字段,告诉我们包含该词项的文档数量。每个桶的数量代表该颜色的文档数量。多个聚合同时计算两种桶的结果:对color和对make。GET /test-agg-cars/_search { "size" : 0, "aggs" : { "popular_colors" : { "terms" : { "field" : "color.keyword" } }, "make_by" : { "terms" : { "field" : "make.keyword" } } } } 结果如下:聚合的嵌套这个新的聚合层让我们可以将 avg 度量嵌套置于 terms 桶内。实际上,这就为每个颜色生成了平均价格。GET /test-agg-cars/_search { "size" : 0, "aggs": { "colors": { "terms": { "field": "color.keyword" }, "aggs": { "avg_price": { "avg": { "field": "price" } } } } } } 结果如下:动态脚本的聚合这个例子告诉你,ElasticSearch还支持一些基于脚本(生成运行时的字段)的复杂的动态聚合。GET /test-agg-cars/_search { "runtime_mappings": { "make.length": { "type": "long", "script": "emit(doc['make.keyword'].value.length())" } }, "size" : 0, "aggs": { "make_length": { "histogram": { "interval": 1, "field": "make.length" } } } } 结果如下:按分类学习Bucket聚合我们在具体学习时,也无需学习每一个点,基于上面图的认知,我们只需用20%的时间学习最为常用的80%功能即可,其它查查文档而已。前置条件的过滤:filter在当前文档集上下文中定义与指定过滤器(Filter)匹配的所有文档的单个存储桶。通常,这将用于将当前聚合上下文缩小到一组特定的文档。GET /test-agg-cars/_search { "size": 0, "aggs": { "make_by": { "filter": { "term": { "type": "honda" } }, "aggs": { "avg_price": { "avg": { "field": "price" } } } } } } 结果如下:对filter进行分组聚合:filters设计一个新的例子, 日志系统中,每条日志都是在文本中,包含warning/info等信息。PUT /test-agg-logs/_bulk?refresh { "index" : { "_id" : 1 } } { "body" : "warning: page could not be rendered" } { "index" : { "_id" : 2 } } { "body" : "authentication error" } { "index" : { "_id" : 3 } } { "body" : "warning: connection timed out" } { "index" : { "_id" : 4 } } { "body" : "info: hello pdai" } 我们需要对包含不同日志类型的日志进行分组,这就需要filters:GET /test-agg-logs/_search { "size": 0, "aggs" : { "messages" : { "filters" : { "other_bucket_key": "other_messages", "filters" : { "infos" : { "match" : { "body" : "info" }}, "warnings" : { "match" : { "body" : "warning" }} } } } } } 结果如下:对number类型聚合:Range基于多桶值源的聚合,使用户能够定义一组范围-每个范围代表一个桶。在聚合过程中,将从每个存储区范围中检查从每个文档中提取的值,并“存储”相关/匹配的文档。请注意,此聚合包括 from 值,但不包括 to 每个范围的值。GET /test-agg-cars/_search { "size": 0, "aggs": { "price_ranges": { "range": { "field": "price", "ranges": [ { "to": 20000 }, { "from": 20000, "to": 40000 }, { "from": 40000 } ] } } } } 结果如下:对IP类型聚合:IP Range专用于IP值的范围聚合。GET /ip_addresses/_search { "size": 10, "aggs": { "ip_ranges": { "ip_range": { "field": "ip", "ranges": [ { "to": "10.0.0.5" }, { "from": "10.0.0.5" } ] } } } } 返回{ ... "aggregations": { "ip_ranges": { "buckets": [ { "key": "*-10.0.0.5", "to": "10.0.0.5", "doc_count": 10 }, { "key": "10.0.0.5-*", "from": "10.0.0.5", "doc_count": 260 } ] } } } CIDR Mask分组此外还可以用CIDR Mask分组GET /ip_addresses/_search { "size": 0, "aggs": { "ip_ranges": { "ip_range": { "field": "ip", "ranges": [ { "mask": "10.0.0.0/25" }, { "mask": "10.0.0.127/25" } ] } } } } 返回{ ... "aggregations": { "ip_ranges": { "buckets": [ { "key": "10.0.0.0/25", "from": "10.0.0.0", "to": "10.0.0.128", "doc_count": 128 }, { "key": "10.0.0.127/25", "from": "10.0.0.0", "to": "10.0.0.128", "doc_count": 128 } ] } } } 增加key显示GET /ip_addresses/_search { "size": 0, "aggs": { "ip_ranges": { "ip_range": { "field": "ip", "ranges": [ { "to": "10.0.0.5" }, { "from": "10.0.0.5" } ], "keyed": true // here } } } } 返回{ ... "aggregations": { "ip_ranges": { "buckets": { "*-10.0.0.5": { "to": "10.0.0.5", "doc_count": 10 }, "10.0.0.5-*": { "from": "10.0.0.5", "doc_count": 260 } } } } } 自定义key显示GET /ip_addresses/_search { "size": 0, "aggs": { "ip_ranges": { "ip_range": { "field": "ip", "ranges": [ { "key": "infinity", "to": "10.0.0.5" }, { "key": "and-beyond", "from": "10.0.0.5" } ], "keyed": true } } } } 返回{ ... "aggregations": { "ip_ranges": { "buckets": { "infinity": { "to": "10.0.0.5", "doc_count": 10 }, "and-beyond": { "from": "10.0.0.5", "doc_count": 260 } } } } } 对日期类型聚合:Date Range专用于日期值的范围聚合。GET /test-agg-cars/_search { "size": 0, "aggs": { "range": { "date_range": { "field": "sold", "format": "yyyy-MM", "ranges": [ { "from": "2014-01-01" }, { "to": "2014-12-31" } ] } } } } 结果如下:此聚合与Range聚合之间的主要区别在于 from和to值可以在Date Math表达式中表示,并且还可以指定日期格式,通过该日期格式将返回from and to响应字段。请注意,此聚合包括from值,但不包括to每个范围的值。对柱状图功能:Histrogram直方图 histogram 本质上是就是为柱状图功能设计的。创建直方图需要指定一个区间,如果我们要为售价创建一个直方图,可以将间隔设为 20,000。这样做将会在每个 $20,000 档创建一个新桶,然后文档会被分到对应的桶中。对于仪表盘来说,我们希望知道每个售价区间内汽车的销量。我们还会想知道每个售价区间内汽车所带来的收入,可以通过对每个区间内已售汽车的售价求和得到。可以用 histogram 和一个嵌套的 sum 度量得到我们想要的答案:GET /test-agg-cars/_search { "size" : 0, "aggs":{ "price":{ "histogram":{ "field": "price.keyword", "interval": 20000 }, "aggs":{ "revenue": { "sum": { "field" : "price" } } } } } } histogram 桶要求两个参数:一个数值字段以及一个定义桶大小间隔。sum 度量嵌套在每个售价区间内,用来显示每个区间内的总收入。如我们所见,查询是围绕 price 聚合构建的,它包含一个 histogram 桶。它要求字段的类型必须是数值型的同时需要设定分组的间隔范围。间隔设置为 20,000 意味着我们将会得到如 [0-19999, 20000-39999, ...]这样的区间。接着,我们在直方图内定义嵌套的度量,这个 sum 度量,它会对落入某一具体售价区间的文档中 price 字段的值进行求和。这可以为我们提供每个售价区间的收入,从而可以发现到底是普通家用车赚钱还是奢侈车赚钱。响应结果如下:结果很容易理解,不过应该注意到直方图的键值是区间的下限。键 0 代表区间 0-19,999 ,键 20000 代表区间 20,000-39,999 ,等等。当然,我们可以为任何聚合输出的分类和统计结果创建条形图,而不只是 直方图 桶。让我们以最受欢迎 10 种汽车以及它们的平均售价、标准差这些信息创建一个条形图。我们会用到 terms 桶和 extended_stats 度量:GET /test-agg-cars/_search { "size" : 0, "aggs": { "makes": { "terms": { "field": "make.keyword", "size": 10 }, "aggs": { "stats": { "extended_stats": { "field": "price" } } } } } } 上述代码会按受欢迎度返回制造商列表以及它们各自的统计信息。我们对其中的 stats.avg 、 stats.count 和 stats.std_deviation 信息特别感兴趣,并用 它们计算出标准差:std_err = std_deviation / count 报表:聚合查询之 Metric 聚合前文主要讲了 ElasticSearch提供的三种聚合方式之桶聚合(Bucket Aggregation),本文主要讲讲指标聚合(Metric Aggregation)。如何理解metric聚合在bucket聚合中,我画了一张图辅助你构筑体系,那么metric聚合又如何理解呢?如果你直接去看官方文档,大概也有十几种:那么metric聚合又如何理解呢?我认为从两个角度:从分类看:Metric聚合分析分为单值分析和多值分析两类从功能看:根据具体的应用场景设计了一些分析api, 比如地理位置,百分数等等融合上述两个方面,我们可以梳理出大致的一个mind图:单值分析: 只输出一个分析结果cardinality 基数(distinct去重)weighted_avg 带权重的avgmedian_absolute_deviation 中位值avg 平均值max 最大值min 最小值sum 和value_count 数量标准stat型其它类型多值分析: 单值之外的top_hits 分桶后的top hitstop_metricsgeo_bounds Geo boundsgeo_centroid Geo-centroidgeo_line Geo-Linepercentiles 百分数范围percentile_ranks 百分数排行stats 包含avg,max,min,sum和countmatrix_stats 针对矩阵模型extended_statsstring_stats 针对字符串stats型百分数型地理位置型Top型通过上述列表(我就不画图了),我们构筑的体系是基于分类和功能,而不是具体的项(比如avg,percentiles...);这是不同的认知维度: 具体的项是碎片化,分类和功能这种是你需要构筑的体系。单值分析: 标准stat类型avg 平均值计算班级的平均分POST /exams/_search?size=0 { "aggs": { "avg_grade": { "avg": { "field": "grade" } } } } 返回{ ... "aggregations": { "avg_grade": { "value": 75.0 } } } max 最大值计算销售最高价POST /sales/_search?size=0 { "aggs": { "max_price": { "max": { "field": "price" } } } } 返回{ ... "aggregations": { "max_price": { "value": 200.0 } } } min 最小值计算销售最低价POST /sales/_search?size=0 { "aggs": { "min_price": { "min": { "field": "price" } } } } 返回{ ... "aggregations": { "min_price": { "value": 10.0 } } } sum 和计算销售总价POST /sales/_search?size=0 { "query": { "constant_score": { "filter": { "match": { "type": "hat" } } } }, "aggs": { "hat_prices": { "sum": { "field": "price" } } } } 返回{ ... "aggregations": { "hat_prices": { "value": 450.0 } } } value_count 数量销售数量统计POST /sales/_search?size=0 { "aggs" : { "types_count" : { "value_count" : { "field" : "type" } } } } 返回{ ... "aggregations": { "types_count": { "value": 7 } } } 单值分析: 其它类型weighted_avg 带权重的avgPOST /exams/_search { "size": 0, "aggs": { "weighted_grade": { "weighted_avg": { "value": { "field": "grade" }, "weight": { "field": "weight" } } } } } 返回{ ... "aggregations": { "weighted_grade": { "value": 70.0 } } } cardinality 基数(distinct去重)POST /sales/_search?size=0 { "aggs": { "type_count": { "cardinality": { "field": "type" } } } } 返回{ ... "aggregations": { "type_count": { "value": 3 } } } median_absolute_deviation 中位值GET reviews/_search { "size": 0, "aggs": { "review_average": { "avg": { "field": "rating" } }, "review_variability": { "median_absolute_deviation": { "field": "rating" } } } } 返回{ ... "aggregations": { "review_average": { "value": 3.0 }, "review_variability": { "value": 2.0 } } } 非单值分析:stats型stats 包含avg,max,min,sum和countPOST /exams/_search?size=0 { "aggs": { "grades_stats": { "stats": { "field": "grade" } } } } 返回{ ... "aggregations": { "grades_stats": { "count": 2, "min": 50.0, "max": 100.0, "avg": 75.0, "sum": 150.0 } } } matrix_stats 针对矩阵模型以下示例说明了使用矩阵统计量来描述收入与贫困之间的关系。GET /_search { "aggs": { "statistics": { "matrix_stats": { "fields": [ "poverty", "income" ] } } } } 返回{ ... "aggregations": { "statistics": { "doc_count": 50, "fields": [ { "name": "income", "count": 50, "mean": 51985.1, "variance": 7.383377037755103E7, "skewness": 0.5595114003506483, "kurtosis": 2.5692365287787124, "covariance": { "income": 7.383377037755103E7, "poverty": -21093.65836734694 }, "correlation": { "income": 1.0, "poverty": -0.8352655256272504 } }, { "name": "poverty", "count": 50, "mean": 12.732000000000001, "variance": 8.637730612244896, "skewness": 0.4516049811903419, "kurtosis": 2.8615929677997767, "covariance": { "income": -21093.65836734694, "poverty": 8.637730612244896 }, "correlation": { "income": -0.8352655256272504, "poverty": 1.0 } } ] } } } extended_stats根据从汇总文档中提取的数值计算统计信息。GET /exams/_search { "size": 0, "aggs": { "grades_stats": { "extended_stats": { "field": "grade" } } } } 上面的汇总计算了所有文档的成绩统计信息。聚合类型为extended_stats,并且字段设置定义将在其上计算统计信息的文档的数字字段。{ ... "aggregations": { "grades_stats": { "count": 2, "min": 50.0, "max": 100.0, "avg": 75.0, "sum": 150.0, "sum_of_squares": 12500.0, "variance": 625.0, "variance_population": 625.0, "variance_sampling": 1250.0, "std_deviation": 25.0, "std_deviation_population": 25.0, "std_deviation_sampling": 35.35533905932738, "std_deviation_bounds": { "upper": 125.0, "lower": 25.0, "upper_population": 125.0, "lower_population": 25.0, "upper_sampling": 145.71067811865476, "lower_sampling": 4.289321881345245 } } } } string_stats 针对字符串用于计算从聚合文档中提取的字符串值的统计信息。这些值可以从特定的关键字字段中检索。POST /my-index-000001/_search?size=0 { "aggs": { "message_stats": { "string_stats": { "field": "message.keyword" } } } } 返回{ ... "aggregations": { "message_stats": { "count": 5, "min_length": 24, "max_length": 30, "avg_length": 28.8, "entropy": 3.94617750050791 } } } 非单值分析:百分数型percentiles 百分数范围针对从聚合文档中提取的数值计算一个或多个百分位数。GET latency/_search { "size": 0, "aggs": { "load_time_outlier": { "percentiles": { "field": "load_time" } } } } 默认情况下,百分位度量标准将生成一定范围的百分位:[1,5,25,50,75,95,99]。{ ... "aggregations": { "load_time_outlier": { "values": { "1.0": 5.0, "5.0": 25.0, "25.0": 165.0, "50.0": 445.0, "75.0": 725.0, "95.0": 945.0, "99.0": 985.0 } } } } percentile_ranks 百分数排行根据从汇总文档中提取的数值计算一个或多个百分位等级。GET latency/_search { "size": 0, "aggs": { "load_time_ranks": { "percentile_ranks": { "field": "load_time", "values": [ 500, 600 ] } } } } 返回{ ... "aggregations": { "load_time_ranks": { "values": { "500.0": 90.01, "600.0": 100.0 } } } } 上述结果表示90.01%的页面加载在500ms内完成,而100%的页面加载在600ms内完成。非单值分析:地理位置型geo_bounds Geo boundsPUT /museums { "mappings": { "properties": { "location": { "type": "geo_point" } } } } POST /museums/_bulk?refresh {"index":{"_id":1}} {"location": "52.374081,4.912350", "name": "NEMO Science Museum"} {"index":{"_id":2}} {"location": "52.369219,4.901618", "name": "Museum Het Rembrandthuis"} {"index":{"_id":3}} {"location": "52.371667,4.914722", "name": "Nederlands Scheepvaartmuseum"} {"index":{"_id":4}} {"location": "51.222900,4.405200", "name": "Letterenhuis"} {"index":{"_id":5}} {"location": "48.861111,2.336389", "name": "Musée du Louvre"} {"index":{"_id":6}} {"location": "48.860000,2.327000", "name": "Musée d'Orsay"} POST /museums/_search?size=0 { "query": { "match": { "name": "musée" } }, "aggs": { "viewport": { "geo_bounds": { "field": "location", "wrap_longitude": true } } } } 上面的汇总展示了如何针对具有商店业务类型的所有文档计算位置字段的边界框。{ ... "aggregations": { "viewport": { "bounds": { "top_left": { "lat": 48.86111099738628, "lon": 2.3269999679178 }, "bottom_right": { "lat": 48.85999997612089, "lon": 2.3363889567553997 } } } } } geo_centroid Geo-centroidPUT /museums { "mappings": { "properties": { "location": { "type": "geo_point" } } } } POST /museums/_bulk?refresh {"index":{"_id":1}} {"location": "52.374081,4.912350", "city": "Amsterdam", "name": "NEMO Science Museum"} {"index":{"_id":2}} {"location": "52.369219,4.901618", "city": "Amsterdam", "name": "Museum Het Rembrandthuis"} {"index":{"_id":3}} {"location": "52.371667,4.914722", "city": "Amsterdam", "name": "Nederlands Scheepvaartmuseum"} {"index":{"_id":4}} {"location": "51.222900,4.405200", "city": "Antwerp", "name": "Letterenhuis"} {"index":{"_id":5}} {"location": "48.861111,2.336389", "city": "Paris", "name": "Musée du Louvre"} {"index":{"_id":6}} {"location": "48.860000,2.327000", "city": "Paris", "name": "Musée d'Orsay"} POST /museums/_search?size=0 { "aggs": { "centroid": { "geo_centroid": { "field": "location" } } } } 上面的汇总显示了如何针对所有具有犯罪类型的盗窃文件计算位置字段的质心。{ ... "aggregations": { "centroid": { "location": { "lat": 51.00982965203002, "lon": 3.9662131341174245 }, "count": 6 } } } geo_line Geo-LinePUT test { "mappings": { "dynamic": "strict", "_source": { "enabled": false }, "properties": { "my_location": { "type": "geo_point" }, "group": { "type": "keyword" }, "@timestamp": { "type": "date" } } } } POST /test/_bulk?refresh {"index": {}} {"my_location": {"lat":37.3450570, "lon": -122.0499820}, "@timestamp": "2013-09-06T16:00:36"} {"index": {}} {"my_location": {"lat": 37.3451320, "lon": -122.0499820}, "@timestamp": "2013-09-06T16:00:37Z"} {"index": {}} {"my_location": {"lat": 37.349283, "lon": -122.0505010}, "@timestamp": "2013-09-06T16:00:37Z"} POST /test/_search?filter_path=aggregations { "aggs": { "line": { "geo_line": { "point": {"field": "my_location"}, "sort": {"field": "@timestamp"} } } } } 将存储桶中的所有 geo_point 值聚合到由所选排序字段排序的 LineString 中。{ "aggregations": { "line": { "type" : "Feature", "geometry" : { "type" : "LineString", "coordinates" : [ [ -122.049982, 37.345057 ], [ -122.050501, 37.349283 ], [ -122.049982, 37.345132 ] ] }, "properties" : { "complete" : true } } } } 非单值分析:Top型top_hits 分桶后的 top hits。POST /sales/_search?size=0 { "aggs": { "top_tags": { "terms": { "field": "type", "size": 3 }, "aggs": { "top_sales_hits": { "top_hits": { "sort": [ { "date": { "order": "desc" } } ], "_source": { "includes": [ "date", "price" ] }, "size": 1 } } } } } } 返回{ ... "aggregations": { "top_tags": { "doc_count_error_upper_bound": 0, "sum_other_doc_count": 0, "buckets": [ { "key": "hat", "doc_count": 3, "top_sales_hits": { "hits": { "total" : { "value": 3, "relation": "eq" }, "max_score": null, "hits": [ { "_index": "sales", "_type": "_doc", "_id": "AVnNBmauCQpcRyxw6ChK", "_source": { "date": "2015/03/01 00:00:00", "price": 200 }, "sort": [ 1425168000000 ], "_score": null } ] } } }, { "key": "t-shirt", "doc_count": 3, "top_sales_hits": { "hits": { "total" : { "value": 3, "relation": "eq" }, "max_score": null, "hits": [ { "_index": "sales", "_type": "_doc", "_id": "AVnNBmauCQpcRyxw6ChL", "_source": { "date": "2015/03/01 00:00:00", "price": 175 }, "sort": [ 1425168000000 ], "_score": null } ] } } }, { "key": "bag", "doc_count": 1, "top_sales_hits": { "hits": { "total" : { "value": 1, "relation": "eq" }, "max_score": null, "hits": [ { "_index": "sales", "_type": "_doc", "_id": "AVnNBmatCQpcRyxw6ChH", "_source": { "date": "2015/01/01 00:00:00", "price": 150 }, "sort": [ 1420070400000 ], "_score": null } ] } } } ] } } } top_metricsPOST /test/_bulk?refresh {"index": {}} {"s": 1, "m": 3.1415} {"index": {}} {"s": 2, "m": 1.0} {"index": {}} {"s": 3, "m": 2.71828} POST /test/_search?filter_path=aggregations { "aggs": { "tm": { "top_metrics": { "metrics": {"field": "m"}, "sort": {"s": "desc"} } } } } 返回{ "aggregations": { "tm": { "top": [ {"sort": [3], "metrics": {"m": 2.718280076980591 } } ] } } } 聚合查询之Pipline聚合前文主要讲了 ElasticSearch提供的三种聚合方式之指标聚合(Metric Aggregation),本文主要讲讲管道聚合(Pipeline Aggregation)。简单而言就是让上一步的聚合结果成为下一个聚合的输入,这就是管道。如何理解pipeline聚合如何理解管道聚合呢?最重要的是要站在设计者角度看这个功能的要实现的目的:让上一步的聚合结果成为下一个聚合的输入,这就是管道。管道机制的常见场景首先回顾下,我们之前在Tomcat管道机制中向你介绍的常见的管道机制设计中的应用场景。责任链模式管道机制在设计模式上属于责任链模式,如果你不理解,请参看如下文章:责任链模式: 通过责任链模式, 你可以为某个请求创建一个对象链. 每个对象依序检查此请求并对其进行处理或者将它传给链中的下一个对象。FilterChain在软件开发的常接触的责任链模式是FilterChain,它体现在很多软件设计中:比如Spring Security框架中比如HttpServletRequest处理的过滤器中当一个request过来的时候,需要对这个request做一系列的加工,使用责任链模式可以使每个加工组件化,减少耦合。也可以使用在当一个request过来的时候,需要找到合适的加工方式。当一个加工方式不适合这个request的时候,传递到下一个加工方法,该加工方式再尝试对request加工。网上找了图,这里我们后文将通过Tomcat请求处理向你阐述。ElasticSearch设计管道机制简单而言:让上一步的聚合结果成为下一个聚合的输入,这就是管道。接下来,无非就是对不同类型的聚合有接口的支撑,比如:第一个维度:管道聚合有很多不同类型,每种类型都与其他聚合计算不同的信息,但是可以将这些类型分为两类:父级 父级聚合的输出提供了一组管道聚合,它可以计算新的存储桶或新的聚合以添加到现有存储桶中。兄弟 同级聚合的输出提供的管道聚合,并且能够计算与该同级聚合处于同一级别的新聚合。第二个维度:根据功能设计的意图比如前置聚合可能是Bucket聚合,后置的可能是基于Metric聚合,那么它就可以成为一类管道进而引出了:xxx bucket(是不是很容易理解了)Bucket聚合 -> Metric聚合:bucket聚合的结果,成为下一步metric聚合的输入Average bucketMin bucketMax bucketSum bucketStats bucketExtended stats bucket对构建体系而言,理解上面的已经够了,其它的类型不过是锦上添花而言。一些例子这里我们通过几个简单的例子看看即可,具体如果需要使用看看文档即可。Average bucket 聚合POST _search { "size": 0, "aggs": { "sales_per_month": { "date_histogram": { "field": "date", "calendar_interval": "month" }, "aggs": { "sales": { "sum": { "field": "price" } } } }, "avg_monthly_sales": { // tag::avg-bucket-agg-syntax[] "avg_bucket": { "buckets_path": "sales_per_month>sales", "gap_policy": "skip", "format": "#,##0.00;(#,##0.00)" } // end::avg-bucket-agg-syntax[] } } } 嵌套的bucket聚合:聚合出按月价格的直方图Metic聚合:对上面的聚合再求平均值。字段类型:buckets_path:指定聚合的名称,支持多级嵌套聚合。gap\_policy 当管道聚合遇到不存在的值,有点类似于term等聚合的(missing)时所采取的策略,可选择值为:skip、insert\_zeros。skip:此选项将丢失的数据视为bucket不存在。它将跳过桶并使用下一个可用值继续计算。format 用于格式化聚合桶的输出(key)。输出结果如下{ "took": 11, "timed_out": false, "_shards": ..., "hits": ..., "aggregations": { "sales_per_month": { "buckets": [ { "key_as_string": "2015/01/01 00:00:00", "key": 1420070400000, "doc_count": 3, "sales": { "value": 550.0 } }, { "key_as_string": "2015/02/01 00:00:00", "key": 1422748800000, "doc_count": 2, "sales": { "value": 60.0 } }, { "key_as_string": "2015/03/01 00:00:00", "key": 1425168000000, "doc_count": 2, "sales": { "value": 375.0 } } ] }, "avg_monthly_sales": { "value": 328.33333333333333, "value_as_string": "328.33" } } } Stats bucket 聚合进一步的stat bucket也很容易理解了。POST /sales/_search { "size": 0, "aggs": { "sales_per_month": { "date_histogram": { "field": "date", "calendar_interval": "month" }, "aggs": { "sales": { "sum": { "field": "price" } } } }, "stats_monthly_sales": { "stats_bucket": { "buckets_path": "sales_per_month>sales" } } } } 返回{ "took": 11, "timed_out": false, "_shards": ..., "hits": ..., "aggregations": { "sales_per_month": { "buckets": [ { "key_as_string": "2015/01/01 00:00:00", "key": 1420070400000, "doc_count": 3, "sales": { "value": 550.0 } }, { "key_as_string": "2015/02/01 00:00:00", "key": 1422748800000, "doc_count": 2, "sales": { "value": 60.0 } }, { "key_as_string": "2015/03/01 00:00:00", "key": 1425168000000, "doc_count": 2, "sales": { "value": 375.0 } } ] }, "stats_monthly_sales": { "count": 3, "min": 60.0, "max": 550.0, "avg": 328.3333333333333, "sum": 985.0 } } } 来源:https://pdai.tech/md/db/nosql-es/elasticsearch-x-agg-bucket.htmlhttps://www.pdai.tech/md/db/nosql-es/elasticsearch-x-agg-metric.htmlhttps://pdai.tech/md/db/nosql-es/elasticsearch-x-agg-pipeline.html(七):ElasticSearch 文档索引与读取流程详解ElasticSearch 中最重要原理是文档的索引和读取!索引文档流程文档索引步骤顺序单个文档新建单个文档所需要的步骤顺序:客户端向 Node 1 发送新建、索引或者删除请求。节点使用文档的 _id 确定文档属于分片 0 。请求会被转发到 Node 3,因为分片 0 的主分片目前被分配在 Node 3 上。Node 3 在主分片上面执行请求。如果成功了,它将请求并行转发到 Node 1 和 Node 2 的副本分片上。一旦所有的副本分片都报告成功, Node 3 将向协调节点报告成功,协调节点向客户端报告成功。多个文档使用 bulk 修改多个文档步骤顺序:客户端向 Node 1 发送 bulk 请求。Node 1 为每个节点创建一个批量请求,并将这些请求并行转发到每个包含主分片的节点主机。主分片一个接一个按顺序执行每个操作。当每个操作成功时,主分片并行转发新文档(或删除)到副本分片,然后执行下一个操作。一旦所有的副本分片报告所有操作成功,该节点将向协调节点报告成功,协调节点将这些响应收集整理并返回给客户端。文档索引过程详解先看下整体的索引流程协调节点默认使用文档ID参与计算(也支持通过routing),以便为路由提供合适的分片。shard = hash(document_id) % (num_of_primary_shards) 当分片所在的节点接收到来自协调节点的请求后,会将请求写入到Memory Buffer,然后定时(默认是每隔1秒)写入到Filesystem Cache,这个从Momery Buffer到Filesystem Cache的过程就叫做refresh;当然在某些情况下,存在Momery Buffer和Filesystem Cache的数据可能会丢失,ES是通过translog的机制来保证数据的可靠性的。其实现机制是接收到请求后,同时也会写入到translog中,当Filesystem cache中的数据写入到磁盘中时,才会清除掉,这个过程叫做flush。在flush过程中,内存中的缓冲将被清除,内容被写入一个新段,段的fsync将创建一个新的提交点,并将内容刷新到磁盘,旧的translog将被删除并开始一个新的translog。flush触发的时机是定时触发(默认30分钟)或者translog变得太大(默认为512M)时。分步骤看数据持久化过程通过分步骤看数据持久化过程:write -> refresh -> flush -> mergewrite 过程一个新文档过来,会存储在 in-memory buffer 内存缓存区中,顺便会记录 Translog(Elasticsearch 增加了一个 translog ,或者叫事务日志,在每一次对 Elasticsearch 进行操作时均进行了日志记录)。这时候数据还没到 segment ,是搜不到这个新文档的。数据只有被 refresh 后,才可以被搜索到。refresh 过程refresh 默认 1 秒钟,执行一次上图流程。ES 是支持修改这个值的,通过 index.refresh_interval 设置 refresh (冲刷)间隔时间。refresh 流程大致如下:in-memory buffer 中的文档写入到新的 segment 中,但 segment 是存储在文件系统的缓存中。此时文档可以被搜索到最后清空 in-memory buffer。注意: Translog 没有被清空,为了将 segment 数据写到磁盘文档经过 refresh 后,segment 暂时写到文件系统缓存,这样避免了性能 IO 操作,又可以使文档搜索到。refresh 默认 1 秒执行一次,性能损耗太大。一般建议稍微延长这个 refresh 时间间隔,比如 5s。因此,ES 其实就是准实时,达不到真正的实时。flush 过程每隔一段时间—例如 translog 变得越来越大—索引被刷新(flush);一个新的 translog 被创建,并且一个全量提交被执行。上个过程中 segment 在文件系统缓存中,会有意外故障文档丢失。那么,为了保证文档不会丢失,需要将文档写入磁盘。那么文档从文件缓存写入磁盘的过程就是 flush。写入磁盘后,清空 translog。具体过程如下:所有在内存缓冲区的文档都被写入一个新的段。缓冲区被清空。一个Commit Point被写入硬盘。文件系统缓存通过 fsync 被刷新(flush)。老的 translog 被删除。merge 过程由于自动刷新流程每秒会创建一个新的段 ,这样会导致短时间内的段数量暴增。而段数目太多会带来较大的麻烦。每一个段都会消耗文件句柄、内存和cpu运行周期。更重要的是,每个搜索请求都必须轮流检查每个段;所以段越多,搜索也就越慢。Elasticsearch通过在后台进行Merge Segment来解决这个问题。小的段被合并到大的段,然后这些大的段再被合并到更大的段。当索引的时候,刷新(refresh)操作会创建新的段并将段打开以供搜索使用。合并进程选择一小部分大小相似的段,并且在后台将它们合并到更大的段中。这并不会中断索引和搜索。一旦合并结束,老的段被删除:新的段被刷新(flush)到了磁盘。** 写入一个包含新段且排除旧的和较小的段的新提交点。新的段被打开用来搜索。老的段被删除。合并大的段需要消耗大量的I/O和CPU资源,如果任其发展会影响搜索性能。Elasticsearch在默认情况下会对合并流程进行资源限制,所以搜索仍然 有足够的资源很好地执行。更多关于 ElasticSearch 数据库的学习文章,请参阅:搜索引擎 ElasticSearch ,本系列持续更新中。深入ElasticSearch索引文档的实现机制写操作的关键点在考虑或分析一个分布式系统的写操作时,一般需要从下面几个方面考虑:可靠性:或者是持久性,数据写入系统成功后,数据不会被回滚或丢失。一致性:数据写入成功后,再次查询时必须能保证读取到最新版本的数据,不能读取到旧数据。原子性:一个写入或者更新操作,要么完全成功,要么完全失败,不允许出现中间状态。隔离性:多个写入操作相互不影响。实时性:写入后是否可以立即被查询到。性能:写入性能,吞吐量到底怎么样。Elasticsearch作为分布式系统,也需要在写入的时候满足上述的四个特点,我们在后面的写流程介绍中会涉及到上述四个方面。接下来,我们一层一层剖析Elasticsearch内部的写机制。Lucene的写众所周知,Elasticsearch 内部使用了 Lucene 完成索引创建和搜索功能,Lucene中写操作主要是通过 IndexWriter 类实现,IndexWriter 提供三个接口: public long addDocument(); public long updateDocuments(); public long deleteDocuments(); 通过这三个接口可以完成单个文档的写入,更新和删除功能,包括了分词,倒排创建,正排创建等等所有搜索相关的流程。只要Doc通过IndesWriter写入后,后面就可以通过IndexSearcher搜索了,看起来功能已经完善了,但是仍然有一些问题没有解:上述操作是单机的,而不是我们需要的分布式。文档写入Lucene后并不是立即可查询的,需要生成完整的Segment后才可被搜索,如何保证实时性?Lucene生成的Segment是在内存中,如果机器宕机或掉电后,内存中的Segment会丢失,如何保证数据可靠性 ?Lucene不支持部分文档更新,但是这又是一个强需求,如何支持部分更新?上述问题,在Lucene中是没有解决的,那么就需要Elasticsearch中解决上述问题。我们再来看Elasticsearch中的写机制。Elasticsearch的写Elasticsearch采用多Shard方式,通过配置routing规则将数据分成多个数据子集,每个数据子集提供独立的索引和搜索功能。当写入文档的时候,根据routing规则,将文档发送给特定Shard中建立索引。这样就能实现分布式了。此外,Elasticsearch整体架构上采用了一主多副的方式:每个Index由多个Shard组成,每个Shard有一个主节点和多个副本节点,副本个数可配。但每次写入的时候,写入请求会先根据\_routing规则选择发给哪个Shard,Index Request中可以设置使用哪个Filed的值作为路由参数,如果没有设置,则使用Mapping中的配置,如果mapping中也没有配置,则使用\_id作为路由参数,然后通过_routing的Hash值选择出Shard(在OperationRouting类中),最后从集群的Meta中找出出该Shard的Primary节点。请求接着会发送给Primary Shard,在Primary Shard上执行成功后,再从Primary Shard上将请求同时发送给多个Replica Shard,请求在多个Replica Shard上执行成功并返回给Primary Shard后,写入请求执行成功,返回结果给客户端。这种模式下,写入操作的延时就等于latency = Latency(Primary Write) + Max(Replicas Write)。只要有副本在,写入延时最小也是两次单Shard的写入时延总和,写入效率会较低,但是这样的好处也很明显,避免写入后,单机或磁盘故障导致数据丢失,在数据重要性和性能方面,一般都是优先选择数据,除非一些允许丢数据的特殊场景。采用多个副本后,避免了单机或磁盘故障发生时,对已经持久化后的数据造成损害,但是Elasticsearch里为了减少磁盘IO保证读写性能,一般是每隔一段时间(比如5分钟)才会把Lucene的Segment写入磁盘持久化,对于写入内存,但还未Flush到磁盘的Lucene数据,如果发生机器宕机或者掉电,那么内存中的数据也会丢失,这时候如何保证?对于这种问题,Elasticsearch学习了数据库中的处理方式:增加CommitLog模块,Elasticsearch中叫TransLog。在每一个Shard中,写入流程分为两部分,先写入Lucene,再写入TransLog。写入请求到达Shard后,先写Lucene文件,创建好索引,此时索引还在内存里面,接着去写TransLog,写完TransLog后,刷新TransLog数据到磁盘上,写磁盘成功后,请求返回给用户。这里有几个关键点:一是和数据库不同,数据库是先写CommitLog,然后再写内存,而Elasticsearch是先写内存,最后才写TransLog,一种可能的原因是Lucene的内存写入会有很复杂的逻辑,很容易失败,比如分词,字段长度超过限制等,比较重,为了避免TransLog中有大量无效记录,减少recover的复杂度和提高速度,所以就把写Lucene放在了最前面。二是写Lucene内存后,并不是可被搜索的,需要通过Refresh把内存的对象转成完整的Segment后,然后再次reopen后才能被搜索,一般这个时间设置为1秒钟,导致写入Elasticsearch的文档,最快要1秒钟才可被从搜索到,所以Elasticsearch在搜索方面是NRT(Near Real Time)近实时的系统。三是当Elasticsearch作为NoSQL数据库时,查询方式是GetById,这种查询可以直接从TransLog中查询,这时候就成了RT(Real Time)实时系统。四是每隔一段比较长的时间,比如30分钟后,Lucene会把内存中生成的新Segment刷新到磁盘上,刷新后索引文件已经持久化了,历史的TransLog就没用了,会清空掉旧的TransLog。上面介绍了Elasticsearch在写入时的两个关键模块,Replica和TransLog,接下来,我们看一下Update流程:Lucene中不支持部分字段的Update,所以需要在Elasticsearch中实现该功能,具体流程如下:收到Update请求后,从Segment或者TransLog中读取同id的完整Doc,记录版本号为V1。将版本V1的全量Doc和请求中的部分字段Doc合并为一个完整的Doc,同时更新内存中的VersionMap。获取到完整Doc后,Update请求就变成了Index请求。加锁。再次从versionMap中读取该id的最大版本号V2,如果versionMap中没有,则从Segment或者TransLog中读取,这里基本都会从versionMap中获取到。检查版本是否冲突(V1==V2),如果冲突,则回退到开始的“Update doc”阶段,重新执行。如果不冲突,则执行最新的Add请求。在Index Doc阶段,首先将Version + 1得到V3,再将Doc加入到Lucene中去,Lucene中会先删同id下的已存在doc id,然后再增加新Doc。写入Lucene成功后,将当前V3更新到versionMap中。释放锁,部分更新的流程就结束了。介绍完部分更新的流程后,大家应该从整体架构上对Elasticsearch的写入有了一个初步的映象,接下来我们详细剖析下写入的详细步骤。Elasticsearch写入请求类型Elasticsearch中的写入请求类型,主要包括下列几个:Index(Create),Update,Delete和Bulk,其中前3个是单文档操作,后一个Bulk是多文档操作,其中Bulk中可以包括Index(Create),Update和Delete。在6.0.0及其之后的版本中,前3个单文档操作的实现基本都和Bulk操作一致,甚至有些就是通过调用Bulk的接口实现的。估计接下来几个版本后,Index(Create),Update,Delete都会被当做Bulk的一种特例化操作被处理。这样,代码和逻辑都会更清晰一些。下面,我们就以Bulk请求为例来介绍写入流程。红色:Client Node。绿色:Primary Node。蓝色:Replica Node。Client NodeClient Node 也包括了前面说过的Parse Request,这里就不再赘述了,接下来看一下其他的部分。1.Ingest Pipeline在这一步可以对原始文档做一些处理,比如HTML解析,自定义的处理,具体处理逻辑可以通过插件来实现。在Elasticsearch中,由于Ingest Pipeline会比较耗费CPU等资源,可以设置专门的Ingest Node,专门用来处理Ingest Pipeline逻辑。如果当前Node不能执行Ingest Pipeline,则会将请求发给另一台可以执行Ingest Pipeline的Node。2.Auto Create Index判断当前Index是否存在,如果不存在,则需要自动创建Index,这里需要和Master交互。也可以通过配置关闭自动创建Index的功能。3.Set Routing设置路由条件,如果Request中指定了路由条件,则直接使用Request中的Routing,否则使用Mapping中配置的,如果Mapping中无配置,则使用默认的_id字段值。在这一步中,如果没有指定id字段,则会自动生成一个唯一的_id字段,目前使用的是UUID。4.Construct BulkShardRequest由于Bulk Request中会包括多个(Index/Update/Delete)请求,这些请求根据routing可能会落在多个Shard上执行,这一步会按Shard挑拣Single Write Request,同一个Shard中的请求聚集在一起,构建BulkShardRequest,每个BulkShardRequest对应一个Shard。5.Send Request To Primary这一步会将每一个BulkShardRequest请求发送给相应Shard的Primary Node。Primary NodePrimary 请求的入口是在PrimaryOperationTransportHandler的messageReceived,我们来看一下相关的逻辑流程。1.Index or Update or Delete循环执行每个Single Write Request,对于每个Request,根据操作类型(CREATE/INDEX/UPDATE/DELETE)选择不同的处理逻辑。其中,Create/Index是直接新增Doc,Delete是直接根据_id删除Doc,Update会稍微复杂些,我们下面就以Update为例来介绍。2.Translate Update To Index or Delete这一步是Update操作的特有步骤,在这里,会将Update请求转换为Index或者Delete请求。首先,会通过GetRequest查询到已经存在的同_id Doc(如果有)的完整字段和值(依赖_source字段),然后和请求中的Doc合并。同时,这里会获取到读到的Doc版本号,记做V1。3.Parse Doc这里会解析Doc中各个字段。生成ParsedDocument对象,同时会生成uid Term。在Elasticsearch中,_uid = type # _id,对用户,_Id可见,而Elasticsearch中存储的是_uid。这一部分生成的ParsedDocument中也有Elasticsearch的系统字段,大部分会根据当前内容填充,部分未知的会在后面继续填充ParsedDocument。4.Update MappingElasticsearch中有个自动更新Mapping的功能,就在这一步生效。会先挑选出Mapping中未包含的新Field,然后判断是否运行自动更新Mapping,如果允许,则更新Mapping。5.Get Sequence Id and Version由于当前是Primary Shard,则会从SequenceNumber Service获取一个sequenceID和Version。SequenceID在Shard级别每次递增1,SequenceID在写入Doc成功后,会用来初始化LocalCheckpoint。Version则是根据当前Doc的最大Version递增1。6.Add Doc To Lucene这一步开始的时候会给特定\_uid加锁,然后判断该\_uid对应的Version是否等于之前Translate Update To Index步骤里获取到的Version,如果不相等,则说明刚才读取Doc后,该Doc发生了变化,出现了版本冲突,这时候会抛出一个VersionConflict的异常,该异常会在Primary Node最开始处捕获,重新从“Translate Update To Index or Delete”开始执行。如果Version相等,则继续执行,如果已经存在同id的Doc,则会调用Lucene的UpdateDocument(uid, doc)接口,先根据uid删除Doc,然后再Index新Doc。如果是首次写入,则直接调用Lucene的AddDocument接口完成Doc的Index,AddDocument也是通过UpdateDocument实现。这一步中有个问题是,如何保证Delete-Then-Add的原子性,怎么避免中间状态时被Refresh?答案是在开始Delete之前,会加一个Refresh Lock,禁止被Refresh,只有等Add完后释放了Refresh Lock后才能被Refresh,这样就保证了Delete-Then-Add的原子性。Lucene的UpdateDocument接口中就只是处理多个Field,会遍历每个Field逐个处理,处理顺序是invert index,store field,doc values,point dimension,后续会有文章专门介绍Lucene中的写入。7.Write Translog写完Lucene的Segment后,会以keyvalue的形式写TransLog,Key是\_id,Value是Doc内容。当查询的时候,如果请求是GetDocByID,则可以直接根据\_id从TransLog中读取到,满足NoSQL场景下的实时性要去。需要注意的是,这里只是写入到内存的TransLog,是否Sync到磁盘的逻辑还在后面。这一步的最后,会标记当前SequenceID已经成功执行,接着会更新当前Shard的LocalCheckPoint。8.Renew Bulk Request这里会重新构造Bulk Request,原因是前面已经将UpdateRequest翻译成了Index或Delete请求,则后续所有Replica中只需要执行Index或Delete请求就可以了,不需要再执行Update逻辑,一是保证Replica中逻辑更简单,性能更好,二是保证同一个请求在Primary和Replica中的执行结果一样。9.Flush Translog这里会根据TransLog的策略,选择不同的执行方式,要么是立即Flush到磁盘,要么是等到以后再Flush。Flush的频率越高,可靠性越高,对写入性能影响越大。10.Send Requests To Replicas这里会将刚才构造的新的Bulk Request并行发送给多个Replica,然后等待Replica的返回,这里需要等待所有Replica返回后(可能有成功,也有可能失败),Primary Node才会返回用户。如果某个Replica失败了,则Primary会给Master发送一个Remove Shard请求,要求Master将该Replica Shard从可用节点中移除。这里,同时会将SequenceID,PrimaryTerm,GlobalCheckPoint等传递给Replica。发送给Replica的请求中,Action Name等于原始ActionName + [R],这里的R表示Replica。通过这个[R]的不同,可以找到处理Replica请求的Handler。11.Receive Response From ReplicasReplica中请求都处理完后,会更新Primary Node的LocalCheckPoint。Replica NodeReplica 请求的入口是在ReplicaOperationTransportHandler的messageReceived,我们来看一下相关的逻辑流程。1.Index or Delete根据请求类型是Index还是Delete,选择不同的执行逻辑。这里没有Update,是因为在Primary Node中已经将Update转换成了Index或Delete请求了。2.Parse Doc3.Update Mapping以上都和Primary Node中逻辑一致。4.Get Sequence Id and VersionPrimary Node中会生成Sequence ID和Version,然后放入ReplicaRequest中,这里只需要从Request中获取到就行。5.Add Doc To Lucene由于已经在Primary Node中将部分Update请求转换成了Index或Delete请求,这里只需要处理Index和Delete两种请求,不再需要处理Update请求了。比Primary Node会更简单一些。6.Write Translog7.Flush Translog以上都和Primary Node中逻辑一致。最后上面详细介绍了Elasticsearch的写入流程及其各个流程的工作机制,我们在这里再次总结下之前提出的分布式系统中的六大特性:可靠性:由于Lucene的设计中不考虑可靠性,在Elasticsearch中通过Replica和TransLog两套机制保证数据的可靠性。一致性:Lucene中的Flush锁只保证Update接口里面Delete和Add中间不会Flush,但是Add完成后仍然有可能立即发生Flush,导致Segment可读。这样就没法保证Primary和所有其他Replica可以同一时间Flush,就会出现查询不稳定的情况,这里只能实现最终一致性。原子性:Add和Delete都是直接调用Lucene的接口,是原子的。当部分更新时,使用Version和锁保证更新是原子的。隔离性:仍然采用Version和局部锁来保证更新的是特定版本的数据。实时性:使用定期Refresh Segment到内存,并且Reopen Segment方式保证搜索可以在较短时间(比如1秒)内被搜索到。通过将未刷新到磁盘数据记入TransLog,保证对未提交数据可以通过ID实时访问到。性能:性能是一个系统性工程,所有环节都要考虑对性能的影响,在Elasticsearch中,在很多地方的设计都考虑到了性能,一是不需要所有Replica都返回后才能返回给用户,只需要返回特定数目的就行;二是生成的Segment现在内存中提供服务,等一段时间后才刷新到磁盘,Segment在内存这段时间的可靠性由TransLog保证;三是TransLog可以配置为周期性的Flush,但这个会给可靠性带来伤害;四是每个线程持有一个Segment,多线程时相互不影响,相互独立,性能更好;五是系统的写入流程对版本依赖较重,读取频率较高,因此采用了versionMap,减少热点数据的多次磁盘IO开销。Lucene中针对性能做了大量的优化。前文介绍了索引文档流程,本文带你理解 ES 文档的读取过程。读取文档流程文档查询步骤顺序先看下整体的查询流程单个文档以下是从主分片或者副本分片检索文档的步骤顺序:客户端向 Node 1 发送获取请求。节点使用文档的 _id 来确定文档属于分片 0 。分片 0 的副本分片存在于所有的三个节点上。在这种情况下,它将请求转发到 Node 2 。Node 2 将文档返回给 Node 1 ,然后将文档返回给客户端。在处理读取请求时,协调结点在每次请求的时候都会通过轮询所有的副本分片来达到负载均衡。在文档被检索时,已经被索引的文档可能已经存在于主分片上但是还没有复制到副本分片。在这种情况下,副本分片可能会报告文档不存在,但是主分片可能成功返回文档。一旦索引请求成功返回给用户,文档在主分片和副本分片都是可用的。多个文档使用 mget 取回多个文档的步骤顺序:以下是使用单个 mget 请求取回多个文档所需的步骤顺序:客户端向 Node 1 发送 mget 请求。Node 1 为每个分片构建多文档获取请求,然后并行转发这些请求到托管在每个所需的主分片或者副本分片的节点上。一旦收到所有答复, Node 1 构建响应并将其返回给客户端。文档读取过程详解所有的搜索系统一般都是两阶段查询,第一阶段查询到匹配的DocID,第二阶段再查询DocID对应的完整文档,这种在Elasticsearch中称为query\_then\_fetch。在初始查询阶段时,查询会广播到索引中每一个分片拷贝(主分片或者副本分片)。每个分片在本地执行搜索并构建一个匹配文档的大小为 from + size 的优先队列。PS:在2. 搜索的时候是会查询Filesystem Cache的,但是有部分数据还在Memory Buffer,所以搜索是近实时的。每个分片返回各自优先队列中 所有文档的 ID 和排序值 给协调节点,它合并这些值到自己的优先队列中来产生一个全局排序后的结果列表。接下来就是 取回阶段,协调节点辨别出哪些文档需要被取回并向相关的分片提交多个 GET 请求。每个分片加载并丰富文档,如果有需要的话,接着返回文档给协调节点。一旦所有的文档都被取回了,协调节点返回结果给客户端。深入ElasticSearch读取文档的实现机制读操作一致性指的是写入成功后,下次读操作一定要能读取到最新的数据。对于搜索,这个要求会低一些,可以有一些延迟。但是对于NoSQL数据库,则一般要求最好是强一致性的。结果匹配上,NoSQL作为数据库,查询过程中只有符合不符合两种情况,而搜索里面还有是否相关,类似于NoSQL的结果只能是0或1,而搜索里面可能会有0.1,0.5,0.9等部分匹配或者更相关的情况。结果召回上,搜索一般只需要召回最满足条件的Top N结果即可,而NoSQL一般都需要返回满足条件的所有结果。搜索系统一般都是两阶段查询,第一个阶段查询到对应的Doc ID,也就是PK;第二阶段再通过Doc ID去查询完整文档,而NoSQL数据库一般是一阶段就返回结果。在Elasticsearch中两种都支持。目前NoSQL的查询,聚合、分析和统计等功能上都是要比搜索弱的。Lucene的读Elasticsearch使用了Lucene作为搜索引擎库,通过Lucene完成特定字段的搜索等功能,在Lucene中这个功能是通过IndexSearcher的下列接口实现的:public TopDocs search(Query query, int n); public Document doc(int docID); public int count(Query query); ......(其他) 第一个search接口实现搜索功能,返回最满足Query的N个结果;第二个doc接口通过doc id查询Doc内容;第三个count接口通过Query获取到命中数。这三个功能是搜索中的最基本的三个功能点,对于大部分Elasticsearch中的查询都是比较复杂的,直接用这个接口是无法满足需求的,比如分布式问题。这些问题都留给了Elasticsearch解决,我们接下来看Elasticsearch中相关读功能的剖析。Elasticsearch的读Elasticsearch中每个Shard都会有多个Replica,主要是为了保证数据可靠性,除此之外,还可以增加读能力,因为写的时候虽然要写大部分Replica Shard,但是查询的时候只需要查询Primary和Replica中的任何一个就可以了。在上图中,该Shard有1个Primary和2个Replica Node,当查询的时候,从三个节点中根据Request中的preference参数选择一个节点查询。preference可以设置_local,_primary,_replica以及其他选项。如果选择了primary,则每次查询都是直接查询Primary,可以保证每次查询都是最新的。如果设置了其他参数,那么可能会查询到R1或者R2,这时候就有可能查询不到最新的数据。PS: 上述代码逻辑在 OperationRouting.Java 的 searchShards 方法中。接下来看一下,Elasticsearch中的查询是如何支持分布式的。Elasticsearch中通过分区实现分布式,数据写入的时候根据_routing规则将数据写入某一个Shard中,这样就能将海量数据分布在多个Shard以及多台机器上,已达到分布式的目标。这样就导致了查询的时候,潜在数据会在当前index的所有的Shard中,所以Elasticsearch查询的时候需要查询所有Shard,同一个Shard的Primary和Replica选择一个即可,查询请求会分发给所有Shard,每个Shard中都是一个独立的查询引擎,比如需要返回Top 10的结果,那么每个Shard都会查询并且返回Top 10的结果,然后在Client Node里面会接收所有Shard的结果,然后通过优先级队列二次排序,选择出Top 10的结果返回给用户。这里有一个问题就是请求膨胀,用户的一个搜索请求在Elasticsearch内部会变成Shard个请求,这里有个优化点,虽然是Shard个请求,但是这个Shard个数不一定要是当前Index中的Shard个数,只要是当前查询相关的Shard即可,这个需要基于业务和请求内容优化,通过这种方式可以优化请求膨胀数。Elasticsearch中的查询主要分为两类,Get请求:通过ID查询特定Doc;Search请求:通过Query查询匹配DocPS:上图中内存中的Segment是指刚Refresh Segment,但是还没持久化到磁盘的新Segment,而非从磁盘加载到内存中的Segment。对于Search类请求,查询的时候是一起查询内存和磁盘上的Segment,最后将结果合并后返回。这种查询是近实时(Near Real Time)的,主要是由于内存中的Index数据需要一段时间后才会刷新为Segment。对于Get类请求,查询的时候是先查询内存中的TransLog,如果找到就立即返回,如果没找到再查询磁盘上的TransLog,如果还没有则再去查询磁盘上的Segment。这种查询是实时(Real Time)的。这种查询顺序可以保证查询到的Doc是最新版本的Doc,这个功能也是为了保证NoSQL场景下的实时性要求所有的搜索系统一般都是两阶段查询,第一阶段查询到匹配的DocID,第二阶段再查询DocID对应的完整文档,这种在Elasticsearch中称为query\_then\_fetch,还有一种是一阶段查询的时候就返回完整Doc,在Elasticsearch中称作query\_and\_fetch,一般第二种适用于只需要查询一个Shard的请求。除了一阶段,两阶段外,还有一种三阶段查询的情况。搜索里面有一种算分逻辑是根据TF(Term Frequency)和DF(Document Frequency)计算基础分,但是Elasticsearch中查询的时候,是在每个Shard中独立查询的,每个Shard中的TF和DF也是独立的,虽然在写入的时候通过_routing保证Doc分布均匀,但是没法保证TF和DF均匀,那么就有会导致局部的TF和DF不准的情况出现,这个时候基于TF、DF的算分就不准。为了解决这个问题,Elasticsearch中引入了DFS查询,比如DFS_query_then_fetch,会先收集所有Shard中的TF和DF值,然后将这些值带入请求中,再次执行query_then_fetch,这样算分的时候TF和DF就是准确的,类似的有DFS_query_and_fetch。这种查询的优势是算分更加精准,但是效率会变差。另一种选择是用BM25代替TF/DF模型。在新版本Elasticsearch中,用户没法指定DFS_query_and_fetch和query_and_fetch,这两种只能被Elasticsearch系统改写。Elasticsearch查询流程Elasticsearch中的大部分查询,以及核心功能都是Search类型查询,上面我们了解到查询分为一阶段,二阶段和三阶段,这里我们就以最常见的的二阶段查询为例来介绍查询流程。Client NodeClient Node 也包括了前面说过的Parse Request,这里就不再赘述了,接下来看一下其他的部分。1.Get Remove Cluster Shard判断是否需要跨集群访问,如果需要,则获取到要访问的Shard列表。2.Get Search Shard Iterator获取当前Cluster中要访问的Shard,和上一步中的Remove Cluster Shard合并,构建出最终要访问的完整Shard列表。这一步中,会根据Request请求中的参数从Primary Node和多个Replica Node中选择出一个要访问的Shard。3.For Every Shard:Perform遍历每个Shard,对每个Shard执行后面逻辑。4.Send Request To Query Shard将查询阶段请求发送给相应的Shard。5.Merge Docs上一步将请求发送给多个Shard后,这一步就是异步等待返回结果,然后对结果合并。这里的合并策略是维护一个Top N大小的优先级队列,每当收到一个shard的返回,就把结果放入优先级队列做一次排序,直到所有的Shard都返回。翻页逻辑也是在这里,如果需要取Top 30~ Top 40的结果,这个的意思是所有Shard查询结果中的第30到40的结果,那么在每个Shard中无法确定最终的结果,每个Shard需要返回Top 40的结果给Client Node,然后Client Node中在merge docs的时候,计算出Top 40的结果,最后再去除掉Top 30,剩余的10个结果就是需要的Top 30~ Top 40的结果。上述翻页逻辑有一个明显的缺点就是每次Shard返回的数据中包括了已经翻过的历史结果,如果翻页很深,则在这里需要排序的Docs会很多,比如Shard有1000,取第9990到10000的结果,那么这次查询,Shard总共需要返回1000 * 10000,也就是一千万Doc,这种情况很容易导致OOM。另一种翻页方式是使用search\_after,这种方式会更轻量级,如果每次只需要返回10条结构,则每个Shard只需要返回search\_after之后的10个结果即可,返回的总数据量只是和Shard个数以及本次需要的个数有关,和历史已读取的个数无关。这种方式更安全一些,推荐使用这种。如果有aggregate,也会在这里做聚合,但是不同的aggregate类型的merge策略不一样,具体的可以在后面的aggregate文章中再介绍。6.Send Request To Fetch Shard选出Top N个Doc ID后发送给这些Doc ID所在的Shard执行Fetch Phase,最后会返回Top N的Doc的内容。Query Phase接下来我们看第一阶段查询的步骤:1.Create Search Context创建Search Context,之后Search过程中的所有中间状态都会存在Context中,这些状态总共有50多个,具体可以查看DefaultSearchContext或者其他SearchContext的子类。2.Parse Query解析Query的Source,将结果存入Search Context。这里会根据请求中Query类型的不同创建不同的Query对象,比如TermQuery、FuzzyQuery等,最终真正执行TermQuery、FuzzyQuery等语义的地方是在Lucene中。这里包括了dfsPhase、queryPhase和fetchPhase三个阶段的preProcess部分,只有queryPhase的preProcess中有执行逻辑,其他两个都是空逻辑,执行完preProcess后,所有需要的参数都会设置完成。由于Elasticsearch中有些请求之间是相互关联的,并非独立的,比如scroll请求,所以这里同时会设置Context的生命周期。同时会设置lowLevelCancellation是否打开,这个参数是集群级别配置,同时也能动态开关,打开后会在后面执行时做更多的检测,检测是否需要停止后续逻辑直接返回。3.Get From Cache判断请求是否允许被Cache,如果允许,则检查Cache中是否已经有结果,如果有则直接读取Cache,如果没有则继续执行后续步骤,执行完后,再将结果加入Cache。4.Add CollectorsCollector主要目标是收集查询结果,实现排序,对自定义结果集过滤和收集等。这一步会增加多个Collectors,多个Collector组成一个List。FilteredCollector:先判断请求中是否有Post Filter,Post Filter用于Search,Agg等结束后再次对结果做Filter,希望Filter不影响Agg结果。如果有Post Filter则创建一个FilteredCollector,加入Collector List中。PluginInMultiCollector:判断请求中是否制定了自定义的一些Collector,如果有,则创建后加入Collector List。MinimumScoreCollector:判断请求中是否制定了最小分数阈值,如果指定了,则创建MinimumScoreCollector加入Collector List中,在后续收集结果时,会过滤掉得分小于最小分数的Doc。EarlyTerminatingCollector:判断请求中是否提前结束Doc的Seek,如果是则创建EarlyTerminatingCollector,加入Collector List中。在后续Seek和收集Doc的过程中,当Seek的Doc数达到Early Terminating后会停止Seek后续倒排链。CancellableCollector:判断当前操作是否可以被中断结束,比如是否已经超时等,如果是会抛出一个TaskCancelledException异常。该功能一般用来提前结束较长的查询请求,可以用来保护系统。EarlyTerminatingSortingCollector:如果Index是排序的,那么可以提前结束对倒排链的Seek,相当于在一个排序递减链表上返回最大的N个值,只需要直接返回前N个值就可以了。这个Collector会加到Collector List的头部。EarlyTerminatingSorting和EarlyTerminating的区别是,EarlyTerminatingSorting是一种对结果无损伤的优化,而EarlyTerminating是有损的,人为掐断执行的优化。TopDocsCollector:这个是最核心的Top N结果选择器,会加入到Collector List的头部。TopScoreDocCollector和TopFieldCollector都是TopDocsCollector的子类,TopScoreDocCollector会按照固定的方式算分,排序会按照分数+doc id的方式排列,如果多个doc的分数一样,先选择doc id小的文档。而TopFieldCollector则是根据用户指定的Field的值排序。5.lucene::search这一步会调用Lucene中IndexSearch的search接口,执行真正的搜索逻辑。每个Shard中会有多个Segment,每个Segment对应一个LeafReaderContext,这里会遍历每个Segment,到每个Segment中去Search结果,然后计算分数。搜索里面一般有两阶段算分,第一阶段是在这里算的,会对每个Seek到的Doc都计算分数,为了减少CPU消耗,一般是算一个基本分数。这一阶段完成后,会有个排序。然后在第二阶段,再对Top 的结果做一次二阶段算分,在二阶段算分的时候会考虑更多的因子。二阶段算分在后续操作中。具体请求,比如TermQuery、WildcardQuery的查询逻辑都在Lucene中,后面会有专门文章介绍。6.rescore根据Request中是否包含rescore配置决定是否进行二阶段排序,如果有则执行二阶段算分逻辑,会考虑更多的算分因子。二阶段算分也是一种计算机中常见的多层设计,是一种资源消耗和效率的折中。Elasticsearch中支持配置多个Rescore,这些rescore逻辑会顺序遍历执行。每个rescore内部会先按照请求参数window选择出Top window的doc,然后对这些doc排序,排完后再合并回原有的Top 结果顺序中。7.suggest::execute()如果有推荐请求,则在这里执行推荐请求。如果请求中只包含了推荐的部分,则很多地方可以优化。推荐不是今天的重点,这里就不介绍了,后面有机会再介绍。8.aggregation::execute()如果含有聚合统计请求,则在这里执行。Elasticsearch中的aggregate的处理逻辑也类似于Search,通过多个Collector来实现。在Client Node中也需要对aggregation做合并。aggregate逻辑更复杂一些,就不在这里赘述了,后面有需要就再单独开文章介绍。上述逻辑都执行完成后,如果当前查询请求只需要查询一个Shard,那么会直接在当前Node执行Fetch Phase。Fetch PhaseElasticsearch作为搜索系统时,或者任何搜索系统中,除了Query阶段外,还会有一个Fetch阶段,这个Fetch阶段在数据库类系统中是没有的,是搜索系统中额外增加的阶段。搜索系统中额外增加Fetch阶段的原因是搜索系统中数据分布导致的,在搜索中,数据通过routing分Shard的时候,只能根据一个主字段值来决定,但是查询的时候可能会根据其他非主字段查询,那么这个时候所有Shard中都可能会存在相同非主字段值的Doc,所以需要查询所有Shard才能不会出现结果遗漏。同时如果查询主字段,那么这个时候就能直接定位到Shard,就只需要查询特定Shard即可,这个时候就类似于数据库系统了。另外,数据库中的二级索引又是另外一种情况,但类似于查主字段的情况,这里就不多说了。基于上述原因,第一阶段查询的时候并不知道最终结果会在哪个Shard上,所以每个Shard中管都需要查询完整结果,比如需要Top 10,那么每个Shard都需要查询当前Shard的所有数据,找出当前Shard的Top 10,然后返回给Client Node。如果有100个Shard,那么就需要返回100 * 10 = 1000个结果,而Fetch Doc内容的操作比较耗费IO和CPU,如果在第一阶段就Fetch Doc,那么这个资源开销就会非常大。所以,一般是当Client Node选择出最终Top N的结果后,再对最终的Top N读取Doc内容。通过增加一点网络开销而避免大量IO和CPU操作,这个折中是非常划算的。Fetch阶段的目的是通过DocID获取到用户需要的完整Doc内容。这些内容包括了DocValues,Store,Source,Script和Highlight等,具体的功能点是在SearchModule中注册的,系统默认注册的有:ExplainFetchSubPhaseDocValueFieldsFetchSubPhaseScriptFieldsFetchSubPhaseFetchSourceSubPhaseVersionFetchSubPhaseMatchedQueriesFetchSubPhaseHighlightPhaseParentFieldSubFetchPhase除了系统默认的8种外,还有通过插件的形式注册自定义的功能,这些SubPhase中最重要的是Source和Highlight,Source是加载原文,Highlight是计算高亮显示的内容片断。上述多个SubPhase会针对每个Doc顺序执行,可能会产生多次的随机IO,这里会有一些优化方案,但是都是针对特定场景的,不具有通用性。Fetch Phase执行完后,整个查询流程就结束了。来源:https://www.pdai.tech/md/db/nosql-es/elasticsearch-y-th-3.htmlhttps://www.pdai.tech/md/db/nosql-es/elasticsearch-y-th-4.html(八):ElasticSearch 集群部署、分片与故障转移相关概念单机 & 集群单台 Elasticsearch 服务器提供服务,往往都有最大的负载能力,超过这个阈值,服务器性能就会大大降低甚至不可用,所以生产环境中,一般都是运行在指定服务器集群中。除了负载能力,单点服务器也存在其他问题:单台机器存储容量有限单服务器容易出现单点故障,无法实现高可用单服务的并发处理能力有限配置服务器集群时,集群中节点数量没有限制, 大于等于 2 个节点就可以看做是集群了。 一般出于高性能及高可用方面来考虑集群中节点 数量都是 3 个以上。集群 Cluster一个集群就是由一个或多个服务器节点组织在一起,共同持有整个的数据,并一起提供索引和搜索功能。一个 Elasticsearch 集群有一个唯一的名字标识,这个名字默认就是”elasticsearch”。这个名字是重要的,因为一个节点只能通过指定某个集群的名字,来加入这个集群。节点 Node集群中包含很多服务器,一个节点就是其中的一个服务器。作为集群的一部分,它存储数据,参与集群的索引和搜索功能。一个节点也是由一个名字来标识的,默认情况下,这个名字是一个随机的漫威漫画角色 的名字,这个名字会在启动的时候赋予节点。这个名字对于管理工作来说挺重要的,因为在这个管理过程中,你会去确定网络中的哪些服务器对应于 Elasticsearch集群中的哪些节点。一个节点可以通过配置集群名称的方式来加入一个指定的集群。默认情况下,每个节点都会被安排加入到一个叫做“elasticsearch”的集群中,这意味着,如果你在你的网络中启动了若干个节点,并假定它们能够相互发现彼此,它们将会自动地形成并加入到一个叫做 “elasticsearch”的集群中。在一个集群里,只要你想,可以拥有任意多个节点。而且,如果当前你的网络中没有运行任何 Elasticsearch 节点,这时启动一个节点,会默认创建并加入一个叫做“elasticsearch”的集群。为什么要部署 Elasticsearch 集群单机部署的 Elasticsearch 在做数据存储时会遇到存储数据上线和机器故障问题,因此对于 Elasticsearch 集群的部署是有必要的。搭建 Elasticsearch 集群,可以将创建的索引库拆分成多个分片(索引可以被拆分为不同的部分进行存储,称为分片。在集群环境下,一个索引的不同分片可以拆分到不同的节点中),存储到不同的节点上,以此来解决海量数据存储问题;将分片上的数据分布在不同的节点上可以解决单点故障问题。Elasticsearch集群职责在Elasticsearch集群中,不同的节点可以承担不同的职责,例如:Master节点:负责集群的管理和调度,包括分配和重新分配分片、节点的加入和退出、索引的创建和删除等。Data节点:负责存储数据和执行搜索请求,包括分片的读写、搜索请求的处理等。Ingest节点:负责对文档进行预处理,例如对文档进行解析、转换、过滤等操作。Coordinating节点:负责协调搜索请求,将请求转发给适当的Data节点进行处理,并将结果汇总返回给客户端。在实际的生产环境中,可以根据集群的规模和负载情况来决定节点的职责划分。例如,在小型集群中,可以将所有节点都设置为Master节点和Data节点;在大型集群中,可以将一部分节点设置为Master节点,一部分节点设置为Data节点,同时还可以设置一些Coordinating节点和Ingest节点来协调搜索请求和处理文档预处理。Windows 集群部署集群创建 elasticsearch-cluster 文件夹,在内部复制三个 elasticsearch 服务 修改集群文件目录中每个节点的 config/elasticsearch.yml 配置文件 node-1001 节点#节点 1 的配置信息: #集群名称,节点之间要保持一致 cluster.name: my-elasticsearch #节点名称,集群内要唯一 node.name: node-1001 node.master: true node.data: true #ip 地址 network.host: localhost #http 端口 http.port: 1001 #tcp 监听端口 transport.tcp.port: 9301 #discovery.seed_hosts: ["localhost:9301", "localhost:9302","localhost:9303"] #discovery.zen.fd.ping_timeout: 1m #discovery.zen.fd.ping_retries: 5 #集群内的可以被选为主节点的节点列表 #cluster.initial_master_nodes: ["node-1", "node-2","node-3"] #跨域配置 #action.destructive_requires_name: true http.cors.enabled: true http.cors.allow-origin: "*" node-1002 节点#节点 2 的配置信息: #集群名称,节点之间要保持一致 cluster.name: my-elasticsearch #节点名称,集群内要唯一 node.name: node-1002 node.master: true node.data: true #ip 地址 network.host: localhost #http 端口 http.port: 1002 #tcp 监听端口 transport.tcp.port: 9302 discovery.seed_hosts: ["localhost:9301"] discovery.zen.fd.ping_timeout: 1m discovery.zen.fd.ping_retries: 5 #集群内的可以被选为主节点的节点列表 #cluster.initial_master_nodes: ["node-1", "node-2","node-3"] #跨域配置 #action.destructive_requires_name: true http.cors.enabled: true http.cors.allow-origin: "*" node-1003 节点#节点 3 的配置信息: #集群名称,节点之间要保持一致 cluster.name: my-elasticsearch #节点名称,集群内要唯一 node.name: node-1003 node.master: true node.data: true #ip 地址 network.host: localhost #http 端口 http.port: 1003 #tcp 监听端口 transport.tcp.port: 9303 #候选主节点的地址,在开启服务后可以被选为主节点 discovery.seed_hosts: ["localhost:9301", "localhost:9302"] discovery.zen.fd.ping_timeout: 1m discovery.zen.fd.ping_retries: 5 #集群内的可以被选为主节点的节点列表 #cluster.initial_master_nodes: ["node-1", "node-2","node-3"] #跨域配置 #action.destructive_requires_name: true http.cors.enabled: true http.cors.allow-origin: "*" 启动集群启动前先删除每个节点中的 data 目录中所有内容(如果存在) 分别双击执行 bin/elasticsearch.bat, 启动节点服务器,启动后,会自动加入指定名称的集群测试集群查看集群状态node-1001 节点node-1002 节点node-1003 节点向集群中的 node-1001 节点增加索引 向集群中的 node-1002 节点查询索引 Linux 集群编写内容如下的docker-compose文件,将其上传到Linux的/root目录下:version: '2.2' services: es01: image: elasticsearch:7.12.1 container_name: es01 environment: - node.name=es01 - cluster.name=es-docker-cluster # 集群名称相同 - discovery.seed_hosts=es02,es03 # 可以发现的其他节点 - cluster.initial_master_nodes=es01,es02,es03 # 可以选举为主节点 - "ES_JAVA_OPTS=-Xms512m -Xmx512m" volumes: - data01:/usr/share/elasticsearch/data # 数据卷 ports: - 9200:9200 # 容器内外端口映射 networks: - elastic es02: image: elasticsearch:7.12.1 container_name: es02 environment: - node.name=es02 - cluster.name=es-docker-cluster - discovery.seed_hosts=es01,es03 - cluster.initial_master_nodes=es01,es02,es03 - "ES_JAVA_OPTS=-Xms512m -Xmx512m" volumes: - data02:/usr/share/elasticsearch/data ports: - 9201:9200 networks: - elastic es03: image: elasticsearch:7.12.1 container_name: es03 environment: - node.name=es03 - cluster.name=es-docker-cluster - discovery.seed_hosts=es01,es02 - cluster.initial_master_nodes=es01,es02,es03 - "ES_JAVA_OPTS=-Xms512m -Xmx512m" volumes: - data03:/usr/share/elasticsearch/data networks: - elastic ports: - 9202:9200 volumes: data01: driver: local data02: driver: local data03: driver: local networks: elastic: driver: bridge es运行需要修改一些linux系统权限,进入并修改/etc/sysctl.conf文件vi /etc/sysctl.conf 在文件中添加下面的内容:vm.max_map_count=262144 然后执行命令,让配置生效:sysctl -p 通过docker-compose启动集群:docker-compose up -d 启动完成后,使用docker查看运行的容器,可以看到已启动Elasticsearch集群:Elasticsearch集群健康状态Elasticsearch集群的健康状态可以通过以下命令或API来查看:命令行方式:可以使用curl命令或者httpie命令来访问Elasticsearch的API来获取集群健康状态,例如:curl -X GET "localhost:9200/_cat/health?v" 或者http GET localhost:9200/_cat/health?v 其中,localhost:9200是Elasticsearch的地址和端口号,_cat/health是API的路径,v表示显示详细信息。执行以上命令后,会返回如下信息:epoch timestamp cluster status node.total node.data shards pri relo init unassign pending_tasks max_task_wait_time active_shards_percent 1578318307 02:38:27 elasticsearch green 1 1 6 3 0 0 0 0 - 100.0% 其中,status字段表示集群的健康状态,有以下几种取值:green:所有主分片和副本分片都正常分配到节点上。yellow:所有主分片都正常分配到节点上,但是有一些副本分片还没有分配到节点上。red:有一些主分片没有分配到节点上,导致数据不可用。API方式:可以使用Elasticsearch的API来获取集群健康状态,例如:GET /_cluster/health 执行以上命令后,会返回如下信息:{ "cluster_name" : "my_cluster", "status" : "green", "timed_out" : false, "number_of_nodes" : 1, "number_of_data_nodes" : 1, "active_primary_shards" : 6, "active_shards" : 6, "relocating_shards" : 0, "initializing_shards" : 0, "unassigned_shards" : 0, "delayed_unassigned_shards" : 0, "number_of_pending_tasks" : 0, "number_of_in_flight_fetch" : 0, "task_max_waiting_in_queue_millis" : 0, "active_shards_percent_as_number" : 100.0 } 其中,status字段表示集群的健康状态,其他字段的含义和命令行方式相同。Elasticsearch集群分片Elasticsearch 集群中的数据被分成多个分片(shard),每个分片是一个独立的Lucene索引。分片可以在集群中的不同节点上分布,以提高搜索和写入性能。分片有两种类型:主分片(primary shard)和副本分片(replica shard)。主分片是每个文档的主要存储位置,每个主分片都有一个唯一的标识符,并且只能在一个节点上存在。当一个文档被索引时,它被路由到一个主分片,然后被写入该分片的Lucene索引。副本分片是主分片的拷贝,它们可以在不同的节点上存在。副本分片的数量可以在索引创建时指定,它们可以提高搜索性能和可用性。当一个主分片不可用时,副本分片可以被用来提供搜索结果。副本分片也可以用来平衡负载,因为它们可以被用来处理读取请求。在 Elasticsearch 集群中,分片的数量和副本的数量可以通过索引的设置进行配置。通常,主分片的数量应该小于或等于集群中的节点数,以确保每个节点都有主分片。副本分片的数量应该根据集群的负载和可用性需求进行配置。当索引创建完成的时候,主分片的数量就固定了,但是复制分片的数量可以随时调整。Elasticsearch故障转移集群的 master 节点会监控集群中的所有节点的状态,一旦发现有节点宕机,就会立即将宕机的节点分片的数据迁移到其他节点上,以此来保证数据安全,这个流程叫故障转移。与此同时剩余节点中会重新选举主节点,当原来的主节点恢复正常时,原来迁移到其他节点上面的分片会被迁移到恢复的节点上,但此时原来的主节点不再是主节点(哥不再是当年的哥)。总结Elasticsearch 故障转移的实现主要依赖于以下两个机制:分片复制机制:Elasticsearch将索引分为多个分片,每个分片都有多个副本,分布在不同的节点上。当一个节点发生故障时,其他节点上的副本可以接管该分片的工作,保证数据的可用性。主从复制机制:Elasticsearch集群中的每个分片都有一个主节点和多个从节点。当主节点宕机时,从节点会自动选举一个新的主节点,以继续处理该分片的请求。在实际应用中,为了进一步提高Elasticsearch集群的可用性和稳定性,可以采用以下措施:配置多个节点:将Elasticsearch集群部署在多个节点上,以分散风险,避免单点故障。监控节点状态:使用监控工具对Elasticsearch节点进行实时监控,及时发现并处理故障。自动化运维:使用自动化运维工具对Elasticsearch集群进行管理和维护,减少人为操作的错误和风险。定期备份数据:定期备份Elasticsearch集群中的数据,以防止数据丢失和损坏,保证数据的可恢复性。来源:https://blog.csdn.net/qq_53847859/article/details/130278452https://blog.csdn.net/qq_44732146/article/details/120723894(九):ElasticSearch 集群规划与运维经验总结ES 集群容量及索引规划集群规模评估评估什么计算资源的CPU和内存存储资源的类型及容量节点数量根据什么评估业务场景:日志分析、指标监控、网站搜索查询及写入QPS索引数据总量集群规模评估准则32C64G单节点配置通常可承载5W次/s的写入;写入量和数据量较大时,优先选择32C64G的节点配置;1T的数据量预计需消耗2-4GB的内存空间;实际存储空间通常为原始数据量2.8倍(1副本)搜索场景优先选择大内存节点配置索引配置评估评估什么怎么划分索引索引的分片数如何设置根据什么评估业务场景:日志分析、指标监控、网站搜索单日新增的数据量索引配置评估准则单个分片大小控制在30-50GB集群总分片数量控制在3w以内1GB的内存空间支持20-30个分片为佳一个节点建议不超过1000个分片索引分片数量建议和节点数量保持一致集群规模较大时建议设置专用主节点专用主节点配置建议在8C16G以上如果是时序数据,建议结合ILM索引生命周期管理ES 集群写入性能优化写入性能优化写入数据不指定doc_id,让 ES 自动生成使用自定义 routing 功能,尽量将请求转发到较少的分片对于规模较大的集群,建议提前创建好索引,且使用固定的 Index mapping对于数据实时性要求不高的场景,可以将索引的 refresh_interval 设置为 30s对于追求写入效率的场景,可以将正在写入的索引设置为单副本,写入完成后打开副本使用 bulk 接口批量写入数据,每次 bulk 数据量大小控制在 10M 左右尽量选择 SSD 磁盘类型,并且可选择挂载多块云硬盘(云上目前最大支持 3块盘)ES 集群运维经验总结常见分片未分配原因总结磁盘满了:the node is above the high watermark cluster setting [cluster.routing.allocation.disk.watermark.high=95%], using more disk space than the maximum allowed [95.0%], actual free: [4.055101177689788%] 解决方法:扩容磁盘或者删除数据分配文档数超过最大值限制:failure IllegalArgumentException[number of documents in the index cannot exceed 2147483519 解决方法:向新索引中写入数据,并合理设置分片大小主分片所在节点掉线cannot allocate because a previous copy of the primary shard existed but can no longer be found on the nodes in the cluster 解决方法:找到节点掉线原因并重新启动节点加入集群,等待分片恢复。索引属性与节点属性不匹配node does not match index setting [index.routing.allocation.require] filters [temperature:“warm”,_id:“comdNq4ZSd2Y6ycB9Oubsg”] 解决方法:重新设置索引的属性,和节点保持一致,若要修改节点属性,则需要重启节点节点长时间掉线后再次加入集群,导致引入脏数据cannot allocate because all found copies of the shard are either stale or corrupt 解决方法:使用reroute API:未分配的分片太多,导致达到了分片恢复的最大阈值,其他分片需要排队等待reached the limit of incoming shard recoveries [2], cluster setting [cluster.routing.allocation.node_concurrent_incoming_recoveries=2] (can also be set via [cluster.routing.allocation.node_concurrent_recoveries]) 解决方法:使用cluster/settings调大分片恢复的并发度和速度。集群状态 shard unassigned 排查事情起因很简单,同事对于我写的一个索引报了如下问题。出于学习目的排查下。常见的ES集群有三种状态,如下:Green:主/副分片都已经分配好且可用;集群处于最健康的状态100%可用;Yellow:主分片可用,但是副分片不可用。这种情况ES集群所有的主分片都是已经分配好了的,但是至少有一个副本是未分配的。这种情况下数据也是完整的;但是集群的高可用性会被弱化。Red:存在不可用的主分片。此时只是部分数据可以查询,已经影响到了整体的读写,需要重点关注。这种情况ES集群至少一个主分片(以及它的全部副本)都缺失。查看集群状态如下图所示分别为green和red的样子。GET /_cluster/health 对于上述red的情况。需要重点关注unassigned_shards没有正常分配的分片。找到异常索引方法一:查看所有索引状况,如下就是有问题的。右侧查找 red 关键词。GET /_cat/indices 方法二:直接查看unassigned的shard。查找 unassigned 关键词。GET /_cat/shards 查看不分配原因使用 Cluster Allocation Explain API ,返回集群为什么不分配分片的详细原因。GET /_cluster/allocation/explain?pretty curl -X GET "http://xxx.io:48888/_cluster/allocation/explain?pretty" 常见的 unassigned 原因,上一小节也具体描述过了。来源:https://blog.csdn.net/mijichui2153/article/details/125374880https://blog.csdn.net/shen2308/article/details/108548347(十):ElasticSearch 分片/副本与数据操作流程前言一台服务器上无法存储大量数据,ES把一个index里面的数据分成多个shard分布式的存储在多个服务器上(对大的索引分片),拆成多个,分不到不同的节点上)。ES就是通过shard来解决节点的容量上限问题的,通过主分片可以将数据分布到集群内的所有节点上。主分片数是在索引创建时指定的,一般不允许修改,除非Reindex。一个索引中的数据保存在多个分片中(默认为一个)相当于水平分表。一个分片表示一个Lucene的实例,它本身就是一个完整的搜索引擎。我们的文档被存储和索引到分片内,这些对应用程序是透明的,即应用程序直接与索引交互而不是分片。首先看一下一个ES集群大概的组织形式图。非常直观!!!!!由上图可以看到shard可以分为主分片(primary shard)和副分片(replaca shard)。图示为三个节点。其中任何一个都是有可能故障或者宕机的,此时其承载的shard的数据就会丢失;通过设置一个或者多个replica shard就可以在发生故障的时候提供备份服务。一方面可以保证数据不丢失,另一方面还可以提升操作的吞吐量和性能。解读 shard我们可以通过shards查看分片的具体信息。命令会列出详细的列出哪些节点包含哪些分片。也会告诉你这个分片的主/副信息、每个分片的文档数和这些文档在磁盘上占用的字节数。同时也会告诉你这些节点位于那个ip。#查看所有分片的情况 GET /_cat/shards?v #指定查看某个索引的分片的情况 GET /_cat/shards/es_qidian_flow_online_v2_202202?v GET /_cat/shards/es_qidian_flow_online_v2_202202?pretty&v 注:GET /_cat/shards?v 这里"v"而不是"?v",如上这里是url链接参数的语法而已。他的的作用就是列出表头(还是要比较好,不熟悉的情况至少知道每个字段的含义)。如下为一个实例。下图 为消息记录搜索功能对应的一个索引的具体分片信息,结合前面的示意图进行解读。①可以看到node节点号和ip是完全能对应上的;也可以看到对于消息记录搜索这个case总共有三个节点和腾讯云上能看到的配置相同。②shard列为分片号,分别为0~8;针对其中任一分片例如4,可以看到其有一个主分片(p)和一个副分片(r)。其中每一个分片有点像是mongodb的1主1从的一份副本集,不过在具体组织上还是有区别的。即mongodb任一个主or从其实都是对应一个mongod实例;但是对于这里的es而言总共只有三个node,2*9=18个主副分片都是分布在这三个node上,只不过在组织的时候尽量让主副分片不落在同一个node上就可以了。和前面的示意图也完全一致。③docs可以看到每个分片的文档数,可以留意到同一分片的主副节点的文档数是完全一致的。这从侧面也说明了主副节点之间"副本集"的关系(一份数据冗余存储)。④store可以看到磁盘存储空间的占用情况。⑤增减节点(node)的时候shard会自动在nodes中负载均衡;其实从上面截图中我们也可以看到三个节点承载的分片数是差不多的。⑥primary shard的数量在建立索引的时候设置,后续不能修改。replica shard数量应该是可以修改的。默认情况会分别创建5个primary shard和5个replica shard。关于截图这个case的9个分片就是创建索引的时候指定的,如下模板和索引中都能看到相关设置。注:当然硬要修改分片数也是可以的,es也提供了相应的api来进行reindex。不过这个代价有点高,要重建整个索引。思考:其实之所以代价高是es的路由机制决定的,分片数变化后就routing不到原来的数据了。当然我们可以进行更深层次的思考,尤其是和mongodb这种可以任意扩充分片的数据库进行对比。会发现本质原因对于ES来说shard就是承载数据的最小单位,写入的时候也完全是按照shard的数量进行的路由;而mongodb最小承载单位是chunk,chunk不是固定在某个shard一成不变,而是在balancer的控制下动态地游走于各个shard之间。⑦primary shard不能和自己的replica shard放到同一个节点上(否则该节点挂了后这个分片的数据就彻底丢失了,起不到容错作用),但是可以和其他shard的replica节点放到一个节点上。⑧每个doc肯定只存在于某一个primary shard以及其对应的replica shard中,不可能存在于多个primary shard上。⑨replica的容错。举个例子master节点(node)宕机后,其上存在的primary shard也一并异常。这个时候会选举产生一个新的master,它会将异常primary shard对应的replica shard提升为primary shard。重启宕机node,master copy备份的数据到重启后的node,然后还会同步宕机所落后的数据,并将这个shard降级为replica。注:关于node下面会讲到。注:统计数据量的时候副本集只能算一份数据,不要重复统计了哦!(大概8亿/月)ES 集群的各节点(client/master/data)前面第⑨点提到了发生宕机时master会参与恢复,这里简要的介绍下master。其实除了master ES集群中的节点根据其被配置后所实际执行的功能可以大致分为 client/master/data三种。查看节点信息(谁是master节点),如下图有 "*" 号的就是 master 节点。GET _cat/nodes?v 前面所说的配置无非就是主节点和数据节点的配置,如下:node.master: true node.data: false 主节点(master node)elasticsearch.yml node.master: true node.data: false 主要功能:维护元数据,管理集群节点状态;不负责数据写入和查询。 配置要点:内存可以相对小一些,但是机器一定要稳定,最好是独占的机器。 当node.master被设置为true的时候就为主节点。理想情况下这个master节点就干一些管理性质的工作,比如维护索引元数据(创建删除索引)、负责切换primary shard和replica shard身份等。主节点的稳定性非常重要。默认情况集群中任何一个node.master没有被设为false的节点都有可能被选为主节点,为了让集群更加稳定分离主节点和数据节点是一个比较好的选择。要是master节点宕机了,那么会自动重新选举一个节点为master。如果非master节点宕机了,会有master节点让宕机节点上的primary shard的身份转移到其他机器上的replica shard。等到宕机机器重启恢复后,master节点会控制将缺失的replica shard分片补充过去,同步后续修改数据之类的,让集群恢复到有容错保障的状态。稳定的主节点对集群的健康是非常重要的。默认情况下任何一个集群中的节点都有可能被选为主节点。索引数据和搜索查询等操作会占用大量的CPU、内存、IO资源,为了确保一个集群的稳定,分离主节点和数据节点是一个比较好的选择。我们可以通过配置指定一个节点应该是数据节点还是master节点。客户端节点(client node)elasticsearch.yml node.master: false node.data: false 主要功能:负责任务分发和结果汇聚,分担数据节点压力。 配置要点:大内存,最好是独占的机器 当node.master/data都被设置为false的时候即为客户端节点。此时该节点只能处理路由请求、处理搜索、分片索引操作等。独立的客户端节点在大型集群中是非常有用的,它协调主节点和数据节点。数据节点(data node)elasticsearch.yml node.master: false node.data: true 主要功能:负责数据的写入与查询,压力大。 配置要点:大内存,最好是独占的机器。 node.master未设置为false、data被设置为true即为数据节点。数据节点的作用就是实际承载数据的节点,也是执行增删改查、聚合操作的节点。数据节点对cpu、内存、io要求较高。注意当资源不够用的时候需要向集群中添加新的节点。混合节点(mixed node) 不建议elasticsearch.yml node.master: true node.data: true ES 的扩容ES的扩容就是新增节点(node),然后在将所有的shard在节点之间进行一个重新的分配(负载均衡)就可以了。ES 集群数据写入拉取流程ES数据写入流程1)客户端选择一个node发送请求过去,这个node就是coordinating node(协调节点);2)coordinating node,对document进行路由,将请求转发给对应的node(有primary shard);3)实际的node上的primary shard处理请求,然后将数据同步到replica node;4)coordinating node,如果发现primary node和所有replica node都搞定之后,就返回响应结果给客户端。ES数据拉取流程1)客户端发送请求到任意一个node,成为coordinate node;2)coordinate node对document进行路由,将请求转发到对应的node,此时会使用round-robin随机轮询算法,在primary shard以及其所有replica中随机选择一个,让读请求负载均衡;3)接收请求的node返回document给coordinate node;4)coordinate node返回document给客户端。1.写入document时,每个document会自动分配一个全局唯一的id即doc id,同时也是根据doc id进行hash路由到对应的primary shard上。也可以手动指定doc id,比如用订单id,用户id。 2.读取document时,你可以通过doc id来查询,然后会根据doc id进行hash,判断出来当时把doc id分配到了哪个shard上面去,从那个shard去查询 ES同mongodb分片集群比较MongoDB就是一个个的节点先组成副本集,一个个的副本集在mongos/configserver的组织下成为一个完整的分片集群;其逻辑组织(主副关系)形式和物理实体(ip)组织形式是完全一样的。ES更像是实现约定好固定的shard数量(主副),这些shard尽量均匀的分布在N个节点(node)上;其逻辑组织形式(主副关系)和物理实体(ip)组织形式是完全独立的。最大的不同就是ES各个节点的角色更加平等。对于ES而言,无论是写入还是查询客户端的请求可以作用与任意一个node。这个node就是协调节点,它的角色有点像是mongos了;负责路由、聚合等。其实从这个实现我们知道ES的每一个node都是要维护一个routing table的。比对mongodb和ES后发现,两者对于一个副本集在命令上存在差异,但意思是差不多的。ES把主、副称为primary shard和replica shard,但是mongodb里面称为primary节点和secondary节点。简单理解节点可以认为为一个ip实例(一台机器);对于ES来说一个节点通常包括多个shard,所以意思还是那个意思但是你不能用节点。但是对于mongodb来说一个mongod就是一个实例,所以直接称为节点就好了。对于写入操作。mongos的角色可能由任一个node承担,但是真正往某个shard写数据的时候其实还是优先作用与primary shard然后在同步到replica shard,这一点和mongodb差不多;只不过mongodb的一个副本集里面叫主从节点罢了。对于查询操作。mongos的角色可能由任一个node承担,但是具体进行查询的时候则是在对应分片的primary和replica中进行轮询;而mongodb则是提供了不同的选项。下面也贴一个mongodb分片集的示意图,方便对比。来源:https://blog.csdn.net/mijichui2153/article/details/124987714(十一):ElasticSearch 数据备份与迁移Elasticsearch 默认配置是数据持久化的,就是 ES 会定时地把缓存数据刷新到硬盘,从而达到数据持久化地效果。在生产环境中,ES 的数据持久化是必须的,防止出现断电时数据的丢失。固然,除了数据持久化外,咱们也是得作到数据备份的,防止出现数据损坏时没法恢复数据的状况。备份方案离线方案SnapshotReindexLogstashElasticSearch-dumpElasticSearch-Exporter增量备份方案logstash备份数据配置文件elasticsearch.yml在配置文件config/elasticsearch.yml 中添加一行数据,设置ES备份的快照数据存储路径。如果没有此目录则须要自行建立。配置好后,须要重启ES。path.repo: ["/usr/local/elasticsearch/snapshot"] 建立仓库其实就是在ES库中建立一个备份存储的目的仓库,这里以仓库名称为 backup 为例,有以下两种方式。在linux服务器上执行如下命令。curl -H "Content-Type: application/json" -XPUT -u elastic:xxx http://ES的IP:9200/_snapshot/backup -d '{"type": "fs","settings": {"location": "/usr/local/elasticsearch/snapshot"}}' 在kibana的Dev Tools开发工具中调用接口。PUT _snapshot/backup { "type": "fs", "settings": { "location": "data_bk", "compress": true, "max_snapshot_bytes_per_sec" : "50mb", "max_restore_bytes_per_sec" : "50mb" } } 调用参数说明:compress #是否压缩,默认为是。 max_snapshot_bytes_per_sec #每一个节点快照速率。默认40mb/s。 max_restore_bytes_per_sec #节点恢复速率。默认40mb/s。 返回结果以下,则说明建立成功。{ "acknowledged": true } 删除备份数据在备份数据以前,最好是先根据备份数据的名称删除原来已经备份好的数据。相同名称的备份数据是不能重复备份的。这里以备份数据的名称为 bk_20190926 为例,后面的执行都以此为例,有以下两种方式。在linux服务器上执行如下命令。curl -XDELETE http://ES的ip:端口/_snapshot/backup/bk_20190926 在kibana的Dev Tools开发工具中调用接口。DELETE _snapshot/backup/bk_20190926 返回结果以下,则说明删除成功。{ "acknowledged": true } 开始备份数据备份数据一样是与删除数据同样,直接调用ES的接口实现的,有以下两种方式。在linux服务器上执行如下命令。curl -XPUT http://ES的ip:端口/_snapshot/backup/bk_20190926?wait_for_completion=true 在kibana的Dev Tools开发工具中调用接口。PUT _snapshot/backup/bk_20190926?wait_for_completion=true 返回结果以下,则说明已经备份成功。{ "snapshot": { "snapshot": "bk_20190926", "uuid": "K4fze5eGSvOwot_xWtz0Hw", "version_id": 6050399, "version": "6.5.3", "indices": [ "first_index" ], "include_global_state": true, "state": "SUCCESS", "start_time": "2019-09-27T05:36:39.398Z", "start_time_in_millis": 1569562599398, "end_time": "2019-09-27T05:36:39.723Z", "end_time_in_millis": 1569562599723, "duration_in_millis": 325, "failures": [], "shards": { "total": 5, "failed": 0, "successful": 5 } } } 同时,能够在ES所在的服务器的目录/usr/local/elasticsearch/snapshot/data_bk下查看到增长了不少文件,这些就是备份数据所需的文件。查看备份数据备份完数据后,直接在服务器上能够看到这些备份的文件,可是这些文件并非一眼就能看出你备份了哪些数据的,此时你能够经过调用ES的接口来查看你备份了哪些数据。一样有两种方式调用。在linux服务器上执行如下命令。curl -XGET http://ES的ip:端口/_snapshot/backup/_all 在kibana的Dev Tools开发工具中调用接口。GET _snapshot/backup/_all 返回结果以下,你备份了多少快照均可以在这里看到,snapshots列表的最后一个元素就是你最近备份的快照。{ "snapshots": [ { "snapshot": "bk_20190926", "uuid": "K4fze5eGSvOwot_xWtz0Hw", "version_id": 6050399, "version": "6.5.3", "indices": [ "first_index" ], "include_global_state": true, "state": "SUCCESS", "start_time": "2019-09-27T05:36:39.398Z", "start_time_in_millis": 1569562599398, "end_time": "2019-09-27T05:36:39.723Z", "end_time_in_millis": 1569562599723, "duration_in_millis": 325, "failures": [], "shards": { "total": 5, "failed": 0, "successful": 5 } } ] } 恢复数据数据备份好了,若是真的出现了不可逆的数据损坏状况,此时就能够进行数据恢复了。备份data文件夹data文件夹其实就是当前ES的数据存储地,防止恢复数据出现异常,先把ES目录下面的data目录备份一下。tar -cvf data-20190626.tar.gz data 清空数据恢复数据以前,先把当前ES的数据清空掉。有以下两种方式。(1)在linux服务器上执行如下命令。curl -XDELETE http://ES的ip:端口/_all (2)在kibana的Dev Tools开发工具中调用接口。DELETE _all 返回结果以下,则说明清空数据成功。{ "acknowledged": true } 恢复数据恢复数据一样有以下两种方式操做。(1)在linux服务器上执行如下命令。curl -XPOST http://ES的ip:端口/_snapshot/backup/bk_20190926/_restore (2)在kibana的Dev Tools开发工具中调用接口。POST _snapshot/backup/bk_20190926/_restore 返回结果以下,则说明恢复数据成功。{ "accepted": true } 至此,ES的数据备份和恢复就介绍完啦!总结这里只是讲解了手动的操做ES的数据备份和恢复,在程序里面咱们同样能够经过调用ES的接口来进行数据备份和恢复,例如经过java程序来定时天天进行ES地数据备份,而后删除昨天或前天的备份数据,只保留一份或两份备份数据,以此来节约磁盘空间。ElasticSearch 数据迁移ES数据迁移有三种方式Rolling upgrades回滚snapshot快照elasticdump方式迁移需考虑的问题版本问题,从低版本到高版本数据的迁移多租户的适配问题多次或者分批迁移数据数据在迁移时候富化FieldMapping 和数据信息分离Rolling upgrades滚动升级方法说明该方法更好的使用在跨版本ES集群迁移中,它允许 ES集群一次升级一个节点,因此在升级期间不会中断服务。不支持在升级持续时间之后在同一集群中运行多个版本的 ES,因为无法将分片从升级的节点复制到运行旧版本的节点。所以在升级前需要对当前使用版本进行备份,以便在升级出现异常时进行回滚。同时在升级过程中优先选择data节点,在data节点升级完成后,在对集群中master节点进行升级。支持滚动升级准则:同一主要版本的次要版本之间v 从 5.6 到 6.8v 从 6.8 到 7.17.5v 从 7.17.0 到 7.17.5 的任何版本从 6.7 或更早版本直接升级到 7.17.5 需要 完全重启集群。在做滚动升级时需要保证ElasticSearch间的集群节点通讯,所以要保证安全认证同步。具体步骤升级前准备在开始将集群升级到版本 7.17.5 之前,您应该执行以下操作:1、检查弃用日志以查看您是否正在使用任何弃用的功能并相应地更新您的代码。2、查看重大更改并对版本 7.17.5 的代码和配置进行任何必要的更改。3、如果您使用任何插件,请确保每个插件都有一个与 Elasticsearch 版本 7.17.5 兼容的版本。4、在升级生产集群之前,在隔离环境中测试升级。5、通过拍摄快照备份您的数据!(或对当前集群所有节点进行数据和安装包进行全量备份)升级集群禁用自动分片功能(若在升级过程中不考虑IO性能瓶颈,可以忽略),关闭一个数据节点时,分配过程会等待。index.unassigned.node_left.delayed_timeout(默认为一分钟),然后才开始将该节点上的分片复制到集群中的其他节点,这可能涉及大量 I/O。由于节点很快将重新启动,因此此 I/O 是不必要的。您可以通过在关闭数据节点之前禁用副本分配来避免争分夺秒 :PUT _cluster/settings{ “persistent”: { “cluster.routing.allocation.enable”: “primaries” }} 停止不必要的索引并执行同步刷新(可选的)POST _flush/synced 暂停要升级节点与集群间其他节点进行数据通讯,避免有新的数据产生(可选的)POST _ml/set_upgrade_mode?enabled=true 关闭当前节点,在当前服务器中升级该节点ElasticSearch版本,其中ElasticSeaarch参考原节点进行配置。升级ElasticSearch使用插件使用elasticsearch-plugin脚本安装每个已安装的 Elasticsearch 插件的升级版本。升级节点时必须升级所有插件。语法:$ES_HOME bin/elasticsearch-plugin install XXXX 启动升级的节点语法:$ES_HOME bin/nohup ./elasticsearch & 重新启用分片分配(若为禁用自动分片功能,无需执行此步骤)PUT _cluster/settings{ “persistent”: { “cluster.routing.allocation.enable”: null }} 等待节点恢复GET _cat/health?v=true 注意在滚动升级期间,分配给运行新版本的节点的主分片不能将其副本分配给使用旧版本的节点。新版本可能具有旧版本无法理解的不同数据格式。如果无法将副本分片分配给另一个节点(集群中只有一个升级节点),则副本分片保持未分配状态,状态保持不变yellow。在这种情况下,一旦没有初始化或重新定位分片,您就可以继续(检查init和relo列)。一但另一个节点升级,就可以分配副本并且状态将更改为green。重复任务当节点恢复并且集群稳定后,对每个需要更新的节点重复这些步骤。重新启动节点与集群间其他节点进行数据通讯(若已经暂停该功能,若未暂停,忽略此操作)POST _ml/set_upgrade_mode?enabled=false 注意在滚动升级期间,集群继续正常运行。但是,在升级集群中的所有节点之前,任何新功能都会被禁用或以向后兼容的模式运行。一旦升级完成并且所有节点都在运行新版本,新功能就会开始运行。一但发生这种情况,就无法返回以向后兼容模式运行。运行先前版本的节点将不允许加入完全更新的集群。如果升级过程中出现网络故障,将所有剩余的旧节点与集群隔离开来,您必须使旧节点脱机并升级它们以使其能够加入集群。如果您在升级过程中同时停止一半或更多符合主节点条件的节点,则集群将不可用,这意味着升级不再是滚动升级。如果发生这种情况,您应该升级并重新启动所有已停止的符合主节点资格的节点,以允许集群再次形成,就像执行全集群重启升级一样。可能还需要升级所有剩余的旧节点,然后它们才能在重新形成后加入集群。snapshot快照首先创建快照仓库注意:对于快照仓库需要每个节点都对其有访问权限,所以在实际使用中需要使用nfs挂载。使用Psotman方式创建仓库Postman:PUT http://192.168.115.130:9200/_snapshot/my_repository{ “type”: “fs”, “settings”: { “location”: “/home/elastic/my_repo_floder”, “compress”: true, “max_restore_bytes_per_sec”: “50mb”, “max_snapshot_bytes_per_sec”: “50mb” } } 说明:my_repository为镜像仓库名称,Location 为镜像路径。使用Curl方式创建仓库curl -XPUT ‘http://192.168.115.130:9200/_snapshot/my_repository’ -H ‘content-Type:application/json’ -d ‘{ “type”: “fs”, “settings”: { “location”: “/home/elastic/my_repo_floder”, “compress”: ****true****, “max_restore_bytes_per_sec”: “50mb”, “max_snapshot_bytes_per_sec”: “50mb” } }’ 备份索引(全量)使用Psotman方式备份Postman:PUT http://192.168.115.130:9200/_snapshot/my_repository/snapshot_1?wait_for_completion=true 使用Curl方式备份curl -XPUT ‘http://192.168.115.130:9200/_snapshot/my_repository/snapshot_1?wait_for_completion=true’ 日志显示 completed with state SUCCESS查看备份对应索引信息Postman:GET http://192.168.115.130:9200/_snapshot/my_hdfs_repository/snapshot_1#snapshot_1是备份文件名称 elasticdump方式Elasticdump工具是依赖于npm进行安装的,离线安装elasticdump步骤如下:#第一步下载node安装包 wget https://nodejs.org/dist/v16.14.0/node-v16.14.0-linux-x64.tar.xz #第二步 在一台外网服务器安装node 解压:tar xvf node-v16.14.0-linux-x64.tar.xz 建立软链接 ln -s ~/node-v16.14.0-linux-x64/bin/node /usr/bin/node ln -s ~/node-v16.14.0-linux-x64/bin/npm /usr/bin/npm 确认安装成功 node -v npm -v #第三步安装npm-pack-all npm install -g npm-pack-all #第四步安装elasticdump npm install elasticdump -g #第五步 打包elasticdump 进入到elasticdump安装目录 cd node-v16.3.0-linux-x64/lib/node_modules/elasticdump/ 执行 npm-pack-all 当前目录生成 elasticdump-6.82.0.tgz #第六步 将node安装包和 elasticdump安装报复制到离线安装的服务器 node-v16.14.0-linux-x64.tar.xz elasticdump-6.82.0.tgz #第七步 按照第二步安装node 和npm #第八步 安装elasticdump npm install elasticdump-6.82.0.tgz #第九步建立软连接 ls ~/node_modules/elasticdump/bin/elasticdump /usr/bin/elasticdump #第十步确认安装成功 elasticdump --help #导出分词器 [root@localhost ~]# elasticdump --input=http://ip:9200/my_index --output=http://127.0.0.1:9200/my_index --type=analyzer #导出映射mapping [root@localhost ~]# elasticdump --input=http://ip:9200/ --output=http://127.0.0.1:9200/ --all=true --type=mapping #导出全部数据 [root@localhost ~]# elasticdump --input=http://ip:9200/ --output=http://127.0.0.1:9200/ --all=true --type=data #如果集群配置了x-pack认证 [root@localhost ~]# elasticdump --input=http://user:password@ip:9200/ --output=http://user:password@127.0.0.1:9200/ --all=true --type=data 迁移注意事项ES数据迁移有两种情况一、ES版本不做变更,只是数据进行迁移;二、ES版本升级,同时数据迁移至新版本ES中上述三种方法均能完成ES的数据迁移,在实际操作时,请根据实际生产环境进行选择,优先选择Rolling upgrades滚动升级,同时需要注意一下几点:ES版本发生变化,需要关注JAVA版本是否要随之变化,ES7版本时开始内嵌JAVA版本为17版本,由原1.8版本升级到17版本,jdk跨度较大,对API的调用挑战性较强。需要经过大量测试,必要时需要对代码进行重构。ES存储数据类型发生变化,ES6版本中为自定义手动创建的,但是在ES7中只有一种数据类型为“doc”;当迁移数据量较大时,数据迁移花费时间较长,建议在业务平滑起或者晚上进行;来源:https://blog.csdn.net/m0_67401270/article/details/126362511https://www.modb.pro/db/561895(十二):ElasticSearch 常用 Curl 命令实践Elasticsearch 常用命令 curl ,这是日常工作中常用的命令,也是非常实用的命令,所以,今天单独拉出来和大家一同学习一下。前言测试环境:Centos7.2 64位 jdk1.8.0_91 elasticsearch-2.2.0 CURL 命令简单认为是可以在命令行下访问url的一个工具curl是利用URL语法在命令行方式下工作的开源文件传输工具,使用curl可以简单实现常见的get/post请求。-x 指定http请求的方法(HEAD GET POST PUT DELETE)-d 指定要传输的数据CURL建立索引库PUT/POST都可以:[hadoop@h153 ~]$ curl -XPUT 'http://192.168.205.153:9200/index_name/' {"acknowledged":true} CURL创建索引[hadoop@h153 ~]$ curl -XPOST http://192.168.205.153:9200/hui/employee/1 -d '{undefined "first_name" : "John", "last_name" : "Smith", "age" : 25, "about" : "I love to go rock climbing", "interests": [ "sports", "music" ] }' {"_index":"hui","_type":"employee","_id":"1","_version":1,"_shards":{"total":2,"successful":1,"failed":0},"created":true} 使用文件的方式创建:[hadoop@h153 ~]$ vi qiang.json {undefined "first_name" : "John", "last_name" : "Smith", "age" : 25, "about" : "I love to go rock climbing", "interests": [ "sports", "music" ] } [hadoop@h153 ~]$ curl -XPOST '192.168.205.153:9200/qiang' -d @qiang.json {"acknowledged":true} PUT和POST用法:PUT是幂等方法,POST不是。所以PUT用于更新、POST用于新增比较合适。PUT,DELETE操作是幂等的。所谓幂等是指不管进行多少次操作,结果都一样。比如我用PUT修改一篇文章,然后在做同样的操作,每次操作后的结果并没有不同,DELETE也是一样。POST操作不是幂等的,比如常见的POST重复加载问题:当我们多次发出同样的POST请求后,其结果是创建出了若干的资源。还有一点需要注意的就是,创建操作可以使用POST,也可以使用PUT,区别在于POST是作用在一个集合资源之上的(/articles),而PUT操作是作用在一个具体资源之上的(/articles/123),比如说很多资源使用数据库自增主键作为标识信息,而创建的资源的标识信息到底是什么只能由服务端提供,这个时候就必须使用POST。创建索引注意事项:索引库名称必须要全部小写,不能以下划线开头,也不能包含逗号。如果没有明确指定索引数据的ID,那么es会自动生成一个随机的ID,需要使用POST参数[hadoop@h153 ~]$ curl -XPOST http://192.168.205.153:9200/hui/emp/ -d '{"first_name" : "John"}' {"_index":"hui","_type":"emp","_id":"AV8MoiLdq8PZVDlk6J74","_version":1,"_shards":{"total":2,"successful":1,"failed":0},"created":true} 如果想要确定我们创建的都是全新的内容:1:使用自增ID2:在url后面添加参数[hadoop@h153 ~]$ curl -XPUT http://192.168.205.153:9200/hui/emp/2?op_type=create -d '{"name":"zs","age":25}' {"_index":"hui","_type":"emp","_id":"2","_version":1,"_shards":{"total":2,"successful":1,"failed":0},"created":true} [hadoop@h153 ~]$ curl -XPUT http://192.168.205.153:9200/hui/emp/2/_create -d '{"name":"laoxiao","age":25}' {"error":{"root_cause":[{"type":"document_already_exists_exception","reason":"[emp][2]: document already exists","shard":"2","index":"hui"}],"type":"document_already_exists_exception","reason":"[emp][2]: document already exists","shard":"2","index":"hui"},"status":409} # 注:如果存在同名文件,Elasticsearch将会返回一个409Conflict的HTTP反馈码 GET查询索引根据员工id查询[hadoop@h153 ~]$ curl -XGET http://192.168.205.153:9200/hui/employee/1?pretty { "_index" : "hui", "_type" : "employee", "_id" : "1", "_version" : 1, "found" : true, "_source" : { "first_name" : "John", "last_name" : "Smith", "age" : 25, "about" : "I love to go rock climbing", "interests" : [ "sports", "music" ] } } [hadoop@h153 ~]$ curl -XGET http://192.168.205.153:9200/hui/emp/2?pretty { "_index" : "hui", "_type" : "emp", "_id" : "2", "_version" : 1, "found" : true, "_source" : { "name" : "zs", "age" : 25 } } 在任意的查询字符串中添加pretty参数,es可以得到易于识别的json结果。curl后添加-i 参数,这样你就能得到反馈头文件。[hadoop@h153 ~]$ curl -i 'http://192.168.205.153:9200/hui/emp/1?pretty' HTTP/1.1 404 Not Found Content-Type: application/json; charset=UTF-8 Content-Length: 76 { "_index" : "hui", "_type" : "emp", "_id" : "1", "found" : false } 检索文档中的一部分,如果只需要显示指定字段[hadoop@h153 ~]$ curl -XGET http://192.168.205.153:9200/hui/employee/1?_source=name,age {"_index":"hui","_type":"employee","_id":"1","_version":1,"found":true,"_source":{"age":25}} 如果只需要source的数据[hadoop@h153 ~]$ curl -XGET http://192.168.205.153:9200/hui/employee/1?_source {"_index":"hui","_type":"employee","_id":"1","_version":1,"found":true,"_source":{undefined "first_name" : "John", "last_name" : "Smith", "age" : 25, "about" : "I love to go rock climbing", "interests": [ "sports", "music" ] }} 查询所有你可以再返回的hits中发现我们录入的文档。搜索会默认返回最前的10个数值:[hadoop@h153 ~]$ curl -XGET http://192.168.205.153:9200/hui/employee/_search {"took":21,"timed_out":false,"_shards":{"total":5,"successful":5,"failed":0},"hits":{"total":1,"max_score":1.0,"hits":[{"_index":"hui","_type":"employee","_id":"1","_score":1.0,"_source":{undefined "first_name" : "John", "last_name" : "Smith", "age" : 25, "about" : "I love to go rock climbing", "interests": [ "sports", "music" ] }}]}} 根据条件进行查询[hadoop@h153 ~]$ curl -XGET http://192.168.205.153:9200/hui/_search?q=last_name:Smith {"took":26,"timed_out":false,"_shards":{"total":5,"successful":5,"failed":0},"hits":{"total":1,"max_score":0.30685282,"hits":[{"_index":"hui","_type":"employee","_id":"1","_score":0.30685282,"_source":{ "first_name" : "John", "last_name" : "Smith", "age" : 25, "about" : "I love to go rock climbing", "interests": [ "sports", "music" ] }}]}} 不根据条件查询:[hadoop@h153 ~]$ curl -XGET http://192.168.205.153:9200/hui/_search {"took":5,"timed_out":false,"_shards":{"total":5,"successful":5,"failed":0},"hits":{"total":3,"max_score":1.0,"hits":[{"_index":"hui","_type":"emp","_id":"AV8MoiLdq8PZVDlk6J74","_score":1.0,"_source":{"first_name" : "John"}},{"_index":"hui","_type":"emp","_id":"2","_score":1.0,"_source":{"name":"zs","age":25}},{"_index":"hui","_type":"employee","_id":"1","_score":1.0,"_source":{ "first_name" : "John", "last_name" : "Smith", "age" : 25, "about" : "I love to go rock climbing", "interests": [ "sports", "music" ] }}]}} DSL查询Domain Specific Language(领域特定语言),这里只给出了最简单的例子,还可以写的更复杂,比如还可以添加过滤等复杂的条件:[hadoop@h153 ~]$ curl -XGET http://192.168.205.153:9200/hui/employee/_search -d '{"query":{"match":{"last_name":"Smith"}}}' {"took":5,"timed_out":false,"_shards":{"total":5,"successful":5,"failed":0},"hits":{"total":1,"max_score":0.30685282,"hits":[{"_index":"hui","_type":"employee","_id":"1","_score":0.30685282,"_source":{ "first_name" : "John", "last_name" : "Smith", "age" : 25, "about" : "I love to go rock climbing", "interests": [ "sports", "music" ] }}]}} MGET查询使用mget API获取多个文档,先再创建一个索引:,再进行查询。curl -XPOST http://192.168.205.153:9200/website/blog/2 -d '{"first_name":"John" , "last_name":"Smith"}'[hadoop@h153 ~]$ curl -XGET http://192.168.205.153:9200/_mget?pretty -d '{"docs":[{"_index":"hui","_type":"emp","_id":2,"_source":"name"},{"_index":"website","_type":"blog","_id":2}]}' # 返回结果 { "docs" : [ { "_index" : "hui", "_type" : "emp", "_id" : "2", "_version" : 1, "found" : true, "_source" : { "name" : "zs" } }, { "_index" : "website", "_type" : "blog", "_id" : "2", "_version" : 1, "found" : true, "_source" : { "first_name" : "John", "last_name" : "Smith" } } ] } 如果你需要的文档在同一个_index或者同一个中,你就可以在URL中指定一个默认的或者_type`/_index`/_index/_typecurl -XGET http://192.168.205.153:9200/hui/_mget?pretty -d '{"docs":[{"_type":"employee","_id":1},{"_type":"emp","_id":2}]}' 如果所有的文档拥有相同的 以及,直接在请求中添加ids的数组即可_index`_type`curl -XGET http://192.168.205.153:9200/hui/emp/_mget?pretty -d '{"ids":["1","2"]}' 统计es的索引数量[hadoop@h153 ~]$ curl -XGET 'http://192.168.205.153:9200/_cat/count' 1508265400 02:36:40 44542 当然我们如果想统计某些特定索引下文档数量也是可以的。例如我们想要hui下的索引数量:[hadoop@h153 ~]$ curl -XGET 'http://192.168.205.153:9200/_cat/count/hui' 1508265602 02:40:02 0 查看所有的索引信息[hadoop@h153 ~]$ curl -XGET 'http://192.168.205.153:9200/_cat/indices?pretty' green open test 5 1 0 0 1.5kb 795b green open qiang 5 0 0 0 795b 795b green open hui 5 1 4 0 41.6kb 20.8kb 当然如果我们想要查看固定类型的索引信息是否存在:[hadoop@h153 ~]$ curl -XGET 'http://192.168.205.153:9200/_cat/indices/hui?pretty' green open hui 5 1 4 0 41.6kb 20.8kb 下面我们介绍一个如何进行强制段合并的命令:[hadoop@h153 ~]$ curl -XPOST 'http://192.168.205.153:9200/hui/_forcemerge?max_num_segments=1' {"_shards":{"total":5,"successful":5,"failed":0}} HEAD使用如果只想检查一下文档是否存在,你可以使用HEAD来替代 GET方法,这样就只会返回HTTP头文件:[hadoop@h153 ~]$ curl -i -XHEAD http://192.168.205.153:9200/hui/emp/1 HTTP/1.1 404 Not Found Content-Type: text/plain; charset=UTF-8 Content-Length: 0 Elasticsearch的更新ES可以使用PUT或者POST对文档进行更新,如果指定ID的文档已经存在,则执行更新操作。注意:执行更新操作的时候ES首先将旧的文档标记为删除状态然后添加新的文档旧的文档不会立即消失,但是你也无法访问ES会在你继续添加更多数据的时候在后台清理已经标记为删除状态的文档局部更新,可以添加新字段或者更新已有字段(必须使用POST):[hadoop@h153 ~]$ curl -XPOST http://192.168.205.153:9200/hui/emp/2/_update -d '{"doc":{"city":"beijing","car":"BMW"}}' {"_index":"hui","_type":"emp","_id":"2","_version":2,"_shards":{"total":2,"successful":1,"failed":0}} Elasticsearch的删除[hadoop@h153 ~]$ curl -XDELETE http://192.168.205.153:9200/hui/emp/2/ {"found":true,"_index":"hui","_type":"emp","_id":"2","_version":3,"_shards":{"total":2,"successful":1,"failed":0}} 注:如果文档存在,found属性值为true,属性的值+1,这个就是内部管理的一部分,它保证了我们在多个节点间的不同操作的顺序都被正确标记了。_versionElasticsearch的批量操作bulk与mget类似,bulk API可以帮助我们同时执行多个请求,格式:– action:index/create/update/delete – metadata:_index,_type,_id – request body:_source(删除操作不需要) { action: { metadata }}\n { request body }\n { action: { metadata }}\n { request body }\n 使用时注意,不能直接在json字符串中添加\n字符,应该按回车curl -XPOST -d[hadoop@h153 ~]$ curl -XPOST '192.168.205.153:9200/_bulk?pretty' -H 'Content-Type: application/json' -d' > { "delete": { "_index": "hui", "_type": "employee", "_id": "1" }} > { "create": { "_index": "website", "_type": "blog", "_id": "123" }} > { "title": "My first blog post" } > { "index": { "_index": "website", "_type": "blog" }} > { "title": "My second blog post" } > { "update": { "_index": "website", "_type": "blog", "_id": "123", "_retry_on_conflict" : 3} } > { "doc" : {"title" : "My updated blog post"} } > ' { "took" : 197, "errors" : false, "items" : [ { "delete" : { "_index" : "hui", "_type" : "employee", "_id" : "1", "_version" : 2, "_shards" : { "total" : 2, "successful" : 1, "failed" : 0 }, "status" : 200, "found" : true } }, { "create" : { "_index" : "website", "_type" : "blog", "_id" : "123", "_version" : 1, "_shards" : { "total" : 2, "successful" : 1, "failed" : 0 }, "status" : 201 } }, { "create" : { "_index" : "website", "_type" : "blog", "_id" : "AV8XEEpF4TG7AylMbq5H", "_version" : 1, "_shards" : { "total" : 2, "successful" : 1, "failed" : 0 }, "status" : 201 } }, { "update" : { "_index" : "website", "_type" : "blog", "_id" : "123", "_version" : 2, "_shards" : { "total" : 2, "successful" : 1, "failed" : 0 }, "status" : 200 } } ] } create和index的区别:如果数据存在,使用create操作失败,会提示文档已经存在,使用index则可以成功执行。使用文件的方式vi requests curl -XPOST/PUT localhost:9200/_bulk --data-binary @request bulk一次最大处理多少数据量:bulk会把将要处理的数据载入内存中,所以数据量是有限制的最佳的数据量不是一个确定的数值,它取决于你的硬件,你的文档大小以及复杂性,你的索引以及搜索的负载一般建议是1000-5000个文档,如果你的文档很大,可以适当减少队列,大小建议是5-15MB,默认不能超过100M,可以在es的配置文件中修改这个值http.max_content_length: 100mbElasticsearch的版本控制普通关系型数据库使用的是(悲观并发控制(PCC)):当我们在读取一个数据前先锁定这一行,然后确保只有读取到数据的这个线程可以修改这一行数据。ES使用的是(乐观并发控制(OCC)):ES不会阻止某一数据的访问,然而,如果基础数据在我们读取和写入的间隔中发生了变化,更新就会失败,这时候就由程序来决定如何处理这个冲突。它可以重新读取新数据来进行更新,又或者将这一情况直接反馈给用户。ES如何实现版本控制(使用es内部版本号):首先得到需要修改的文档,获取版本( _version )号:[hadoop@h153 ~]$ curl -XGET http://192.168.205.153:9200/hui/emp/2 {"_index":"hui","_type":"emp","_id":"2","_version":2,"found":true,"_source":{"name":"zs","age":25}} 在执行更新操作的时候把版本号传过去:[hadoop@h153 ~]$ curl -XPUT http://192.168.205.153:9200/hui/emp/2?version=3 -d '{"name":"zs","age":25}' {"error":{"root_cause":[{"type":"version_conflict_engine_exception","reason":"[emp][2]: version conflict, current [2], provided [3]","shard":"2","index":"hui"}],"type":"version_conflict_engine_exception","reason":"[emp][2]: version conflict, current [2], provided [3]","shard":"2","index":"hui"},"status":409} [hadoop@h153 ~]$ curl -XPUT http://192.168.205.153:9200/hui/emp/2?version=2 -d '{"name":"zs","age":25}'(覆盖) {"_index":"hui","_type":"emp","_id":"2","_version":3,"_shards":{"total":2,"successful":1,"failed":0},"created":false} [hadoop@h153 ~]$ curl -XPOST http://192.168.205.153:9200/hui/emp/2/_update?version=3 -d '{"doc":{"city":"beijing","car":"BMW"}}'(部分更新) {"_index":"hui","_type":"emp","_id":"2","_version":4,"_shards":{"total":2,"successful":1,"failed":0}} 注:如果传递的版本号和待更新的文档的版本号不一致,则会更新失败。ES如何实现版本控制(使用外部版本号)如果你的数据库已经存在了版本号,或者是可以代表版本的时间戳。这时就可以在es的查询url后面添加来使用这些号码。 注意:版本号码必须要是大于0小于9223372036854775807(Java中long的最大正值)的整数。version_type=externales在处理外部版本号的时候,它不再检查是否与请求中指定的数值是否相等,而是检查当前的是否比指定的数值小,如果小,则请求成功。example:·_version·`·_version·`[hadoop@h153 ~]$ curl -XPUT 'http://192.168.205.153:9200/hui/emp/2?version=10&version_type=external' -d '{"name":"laoxiao"}' {"_index":"hui","_type":"emp","_id":"2","_version":10,"_shards":{"total":2,"successful":1,"failed":0},"created":false} 注意:此处url前后的引号不能省略,否则执行的时候会报错。Elasticsearch的插件站点插件(以网页形式展现):BigDesk Plugin (作者 Luká? Vl?ek)简介:监控es状态的插件,推荐!Elasticsearch Head Plugin (作者 Ben Birch)简介:很方便对es进行各种操作的客户端。Paramedic Plugin (作者 Karel Mina?ík)简介:es监控插件SegmentSpy Plugin (作者 Zachary Tong)简介:查看es索引segment状态的插件Inquisitor Plugin (作者 Zachary Tong)简介:这个插件主要用来调试你的查询。这个主要提供的是节点的实时状态监控,包括jvm的情况, linux的情况,elasticsearch的情况:安装bin/plugin install lukas-vlcek/bigdesk删除bin/plugin remove bigdesk登录网页查看:http://192.168.205.153:9200/_plugin/bigdesk/ 安装head插件:bin/plugin -install mobz/elasticsearch-head 登录网页查看:http://192.168.205.153:9200/_plugin/head/ (支持谷歌浏览器,不支持360浏览器)可以通过该命令查看安装了哪些插件:[hadoop@h153 ~]$ ./elasticsearch-2.2.0/bin/plugin list Installed plugins in /home/hadoop/elasticsearch-2.2.0/plugins: - ik - head 健康状态查询命令行查看:curl http://localhost:9200/_cluster/health?pretty[hadoop@h153 ~]$ curl '192.168.205.153:9200/_cluster/health?pretty' { "cluster_name" : "my-application", "status" : "yellow", "timed_out" : false, "number_of_nodes" : 1, "number_of_data_nodes" : 1, "active_primary_shards" : 5, "active_shards" : 5, "relocating_shards" : 0, "initializing_shards" : 0, "unassigned_shards" : 5, "delayed_unassigned_shards" : 0, "number_of_pending_tasks" : 0, "number_of_in_flight_fetch" : 0, "task_max_waiting_in_queue_millis" : 0, "active_shards_percent_as_number" : 50.0 } 其他命令[hadoop@localhost elasticsearch-2.2.0]$ curl -XGET http://192.168.205.142:9200/ { "name" : "node-1", "cluster_name" : "my-application", "version" : { "number" : "2.2.0", "build_hash" : "8ff36d139e16f8720f2947ef62c8167a888992fe", "build_timestamp" : "2016-01-27T13:32:39Z", "build_snapshot" : false, "lucene_version" : "5.4.1" }, "tagline" : "You Know, for Search" } [hadoop@localhost elasticsearch-2.2.0]$ curl -XGET 192.168.205.142:9200/_cat/health?v epoch timestamp cluster status node.total node.data shards pri relo init unassign pending_tasks max_task_wait_time active_shards_percent 1489745233 18:07:13 my-application green 3 3 6 3 0 0 0 0 - 100.0% 来源:https://blog.csdn.net/m0_37739193/article/details/78228876(十三):ElasticSearch 可视化管理工具本文主要介绍几款比较常见的可视工具,供大家自行选择,工具没有好坏之分,只有适合之说,所以,选择合适自己的才是最好的。工欲善其事,必先利其器。Elasticsearch 和我们的数据库是一样的都需要客户端才可以看到相关数据。推荐的五种客户端1.Elasticsearch-Head , Elasticsearch-Head 插件在5.x版本之后已不再维护,界面比较老旧。2.cerebro 据传该插件不支持ES中5.x以上版本。3.kinaba 功能强大,但操作复杂,以后可以考虑。4.Dejavu 也是一个 Elasticsearch 的 Web UI 工具,其 UI界面更符合当下主流的前端页面风格,因此使用起来很方便。但是网上可借鉴的文档较少,我也没有细查。5.ElasticHD 不依赖ES的插件安装,更便捷;导航栏直接填写对应的ES IP和端口就可以操作Es了。Dejavu 下载、安装、使用github地址:https://github.com/appbaseio/dejavu/Docker安装docker run -p 1358:1358 -d appbaseio/dejavu 启动访问:http://localhost:1358/使用效果这个数据预览页面非常直观,索引/类型/文档 看得一清二楚查询功能elasticsearch-head 下载、安装、使用Windows配置Node环境Head插件是采用HTML编写的,它的运行需要Node.js环境。npm在安装Nodejs时顺带已经安装成功了。安装GruntGrunt是一个基于命令的Javascript工程命令行构建工具。使用npm安装Grunt的安装命令如下:npm install -g grunt-cli 测试是否安装成功grunt -version 下载Head插件源码https://github.com/mobz/elasticsearch-head到elasticsearch-head-5.0.0 目录下,安装依赖:npm install 修改Elastic search配置编辑 elasticsearch-x.x.x/config/elasticsearch.yml,加入如下配置:http.cors.enabled: true http.cors.allow-origin: "*" 作用是开启HTTP对外提供服务,使 Head插件能够访问Elasticsearch集群,修改完成之后需重启 Elasticsearch。修改Head插件配置文件打开elasticsearch-head-master/Gruntfile.js,找到下面connect属性,修改hostname的值为Elasticsearch的访问IP:(默认没有hostname这一项,此时hostname值实际为localhost,所以本处不配置也可以)connect: { server: { options: { hostname: 'localhost', port: 9100, base: '.', keepalive: true } } } 启动Head插件切换到elasticsearch-head-master/目录下,运行启动命令:grunt server 启动结果如下:建议将命令写为批处理:cd /d D:\dev\ES\elasticsearch-head-5.0.0 grunt server @cmd /k Docker docker run -d -p 9100:9100 docker.io/mobz/elasticsearch-head:5 使用访问 http://localhost:9100使用效果ElasticHD 下载、安装、使用目前支持如下功能:ES 实时搜索; ES DashBoard 数据可视化; ES Index Template (在线修改、查看、上传); SQL Converts to DSL; ES 基本查询文档 不要下载源码,要下载可执行程序:https://github.com/360EntSecGroup-Skylar/ElasticHD/releases/在bin下启动bat即可,如果不行,就需要使用cmd启动。命令:cd D:\Eshome\esHD (这里替换掉你下载解压后的文件夹目录) ElasticHD -p 127.0.0.1:9800 如果你觉得每次都这样启动麻烦,可以用个记事本写下来,然后把记事本后缀名改成.bat ,这样就双击启动了。然后,我们浏览器访问下(如果你启动的服务想要别的电脑访问,就不要使用127.0.0.1 ,要使用局域网IP或者外网的固定IP)。参考来源:https://blog.csdn.net/feiying0canglang/article/details/126347285https://blog.csdn.net/zth_killer/article/details/122744578(十四):ElasticSearch 性能优化详解Elasticsearch 作为一个开箱即用的产品,在生产环境上线之后,我们其实不一定能确保其的性能和稳定性。如何根据实际情况提高服务的性能,其实有很多技巧。这章我们分享从实战经验中总结出来的 elasticsearch 性能优化,主要从硬件配置优化、索引优化设置、查询方面优化、数据结构优化、集群架构优化等方面讲解。硬件配置优化升级硬件设备配置一直都是提高服务能力最快速有效的手段,在系统层面能够影响应用性能的一般包括三个因素:CPU、内存和 IO,可以从这三方面进行 ES 的性能优化工作。CPU 配置一般说来,CPU 繁忙的原因有以下几个:线程中有无限空循环、无阻塞、正则匹配或者单纯的计算;发生了频繁的 GC;多线程的上下文切换;大多数 Elasticsearch 部署往往对 CPU 要求不高。因此,相对其它资源,具体配置多少个(CPU)不是那么关键。你应该选择具有多个内核的现代处理器,常见的集群使用 2 到 8 个核的机器。如果你要在更快的 CPUs 和更多的核数之间选择,选择更多的核数更好。多个内核提供的额外并发远胜过稍微快一点点的时钟频率。内存配置如果有一种资源是最先被耗尽的,它可能是内存。排序和聚合都很耗内存,所以有足够的堆空间来应付它们是很重要的。即使堆空间是比较小的时候,也能为操作系统文件缓存提供额外的内存。因为 Lucene 使用的许多数据结构是基于磁盘的格式,Elasticsearch 利用操作系统缓存能产生很大效果。64 GB 内存的机器是非常理想的,但是 32 GB 和 16 GB 机器也是很常见的。少于8 GB 会适得其反(你最终需要很多很多的小机器),大于 64 GB 的机器也会有问题。由于 ES 构建基于 lucene,而 lucene 设计强大之处在于 lucene 能够很好的利用操作系统内存来缓存索引数据,以提供快速的查询性能。lucene 的索引文件 segements 是存储在单文件中的,并且不可变,对于 OS 来说,能够很友好地将索引文件保持在 cache 中,以便快速访问;因此,我们很有必要将一半的物理内存留给 lucene;另一半的物理内存留给 ES(JVM heap)。内存分配当机器内存小于 64G 时,遵循通用的原则,50% 给 ES,50% 留给 lucene。当机器内存大于 64G 时,遵循以下原则:如果主要的使用场景是全文检索,那么建议给 ES Heap 分配 4~32G 的内存即可;其它内存留给操作系统,供 lucene 使用(segments cache),以提供更快的查询性能。如果主要的使用场景是聚合或排序,并且大多数是 numerics,dates,geo\_points 以及 not\_analyzed 的字符类型,建议分配给 ES Heap 分配 4~32G 的内存即可,其它内存留给操作系统,供 lucene 使用,提供快速的基于文档的聚类、排序性能。如果使用场景是聚合或排序,并且都是基于 analyzed 字符数据,这时需要更多的 heap size,建议机器上运行多 ES 实例,每个实例保持不超过 50% 的 ES heap 设置(但不超过 32 G,堆内存设置 32 G 以下时,JVM 使用对象指标压缩技巧节省空间),50% 以上留给 lucene。禁止 swap禁止 swap,一旦允许内存与磁盘的交换,会引起致命的性能问题。可以通过在 elasticsearch.yml 中 bootstrap.memory_lock: true,以保持 JVM 锁定内存,保证 ES 的性能。GC 设置老的版本中推荐默认设置为:Concurrent-Mark and Sweep(CMS),给的理由是当时G1 还有很多 BUG。原因是:已知JDK 8附带的HotSpot JVM的早期版本存在一些问题,当启用G1GC收集器时,这些问题可能导致索引损坏。受影响的版本早于JDK 8u40随附的HotSpot版本。实际上如果你使用的JDK8较高版本,或者JDK9+,我推荐你使用G1 GC;因为我们目前的项目使用的就是G1 GC,运行效果良好,对Heap大对象优化尤为明显。修改jvm.options文件,将下面几行:-XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction=75 -XX:+UseCMSInitiatingOccupancyOnly 更改为-XX:+UseG1GC -XX:MaxGCPauseMillis=50 其中 -XX:MaxGCPauseMillis是控制预期的最高GC时长,默认值为200ms,如果线上业务特性对于GC停顿非常敏感,可以适当设置低一些。但是 这个值如果设置过小,可能会带来比较高的cpu消耗。G1对于集群正常运作的情况下减轻G1停顿对服务时延的影响还是很有效的,但是如果是你描述的GC导致集群卡死,那么很有可能换G1也无法根本上解决问题。通常都是集群的数据模型或者Query需要优化。磁盘硬盘对所有的集群都很重要,对大量写入的集群更是加倍重要(例如那些存储日志数据的)。硬盘是服务器上最慢的子系统,这意味着那些写入量很大的集群很容易让硬盘饱和,使得它成为集群的瓶颈。在经济压力能承受的范围下,尽量使用固态硬盘(SSD)。固态硬盘相比于任何旋转介质(机械硬盘,磁带等),无论随机写还是顺序写,都会对 IO 有较大的提升。1.如果你正在使用 SSDs,确保你的系统 I/O 调度程序是配置正确的。当你向硬盘写数据,I/O 调度程序决定何时把数据实际发送到硬盘。大多数默认 *nix发行版下的调度程序都叫做 cfq(完全公平队列)。2.调度程序分配时间片到每个进程。并且优化这些到硬盘的众多队列的传递。但它是为旋转介质优化的:机械硬盘的固有特性意味着它写入数据到基于物理布局的硬盘会更高效。3.这对 SSD 来说是低效的,尽管这里没有涉及到机械硬盘。但是,deadline 或者 noop 应该被使用。deadline 调度程序基于写入等待时间进行优化,noop 只是一个简单的 FIFO 队列。这个简单的更改可以带来显著的影响。仅仅是使用正确的调度程序,我们看到了 500 倍的写入能力提升。如果你使用旋转介质(如机械硬盘),尝试获取尽可能快的硬盘(高性能服务器硬盘,15k RPM 驱动器)。使用 RAID0 是提高硬盘速度的有效途径,对机械硬盘和 SSD 来说都是如此。没有必要使用镜像或其它 RAID 变体,因为 Elasticsearch 在自身层面通过副本,已经提供了备份的功能,所以不需要利用磁盘的备份功能,同时如果使用磁盘备份功能的话,对写入速度有较大的影响。最后,避免使用网络附加存储(NAS)。人们常声称他们的 NAS 解决方案比本地驱动器更快更可靠。除却这些声称,我们从没看到 NAS 能配得上它的大肆宣传。NAS 常常很慢,显露出更大的延时和更宽的平均延时方差,而且它是单点故障的。索引优化设置索引优化主要是在 Elasticsearch 的插入层面优化,Elasticsearch 本身索引速度其实还是蛮快的,具体数据,我们可以参考官方的 benchmark 数据。我们可以根据不同的需求,针对索引优化。批量提交当有大量数据提交的时候,建议采用批量提交(Bulk 操作);此外使用 bulk 请求时,每个请求不超过几十M,因为太大会导致内存使用过大。比如在做 ELK 过程中,Logstash indexer 提交数据到 Elasticsearch 中,batch size 就可以作为一个优化功能点。但是优化 size 大小需要根据文档大小和服务器性能而定。像 Logstash 中提交文档大小超过 20MB,Logstash 会将一个批量请求切分为多个批量请求。如果在提交过程中,遇到 EsRejectedExecutionException 异常的话,则说明集群的索引性能已经达到极限了。这种情况,要么提高服务器集群的资源,要么根据业务规则,减少数据收集速度,比如只收集 Warn、Error 级别以上的日志。增加 Refresh 时间间隔为了提高索引性能,Elasticsearch 在写入数据的时候,采用延迟写入的策略,即数据先写到内存中,当超过默认1秒(index.refresh_interval)会进行一次写入操作,就是将内存中 segment 数据刷新到磁盘中,此时我们才能将数据搜索出来,所以这就是为什么 Elasticsearch 提供的是近实时搜索功能,而不是实时搜索功能。如果我们的系统对数据延迟要求不高的话,我们可以通过延长 refresh 时间间隔,可以有效地减少 segment 合并压力,提高索引速度。比如在做全链路跟踪的过程中,我们就将 index.refresh_interval 设置为30s,减少 refresh 次数。再如,在进行全量索引时,可以将 refresh 次数临时关闭,即 index.refresh_interval设置为-1,数据导入成功后再打开到正常模式,比如30s。在加载大量数据时候可以暂时不用 refresh 和 repliccas,index.refresh_interval 设置为-1,index.number_of_replicas 设置为0。相关原理,请参考:ES 原理之索引文档流程详解修改 index\_buffer\_size 的设置索引缓冲的设置可以控制多少内存分配给索引进程。这是一个全局配置,会应用于一个节点上所有不同的分片上。indices.memory.index_buffer_size: 10% indices.memory.min_index_buffer_size: 48mb indices.memory.index_buffer_size 接受一个百分比或者一个表示字节大小的值。默认是10%,意味着分配给节点的总内存的10%用来做索引缓冲的大小。这个数值被分到不同的分片(shards)上。如果设置的是百分比,还可以设置 min_index_buffer_size (默认 48mb)和 max_index_buffer_size(默认没有上限)。修改 translog 相关的设置一是控制数据从内存到硬盘的操作频率,以减少硬盘 IO。可将 sync_interval 的时间设置大一些。默认为5s。index.translog.sync_interval: 5s 也可以控制 tranlog 数据块的大小,达到 threshold 大小时,才会 flush 到 lucene 索引文件。默认为512m。index.translog.flush_threshold_size: 512mb translog我们在 ES原理之索引文档流程详解 也有介绍。注意_id字段的使用_id 字段的使用,应尽可能避免自定义 _id,以避免针对 ID 的版本管理;建议使用 ES 的默认 ID 生成策略或使用数字类型 ID 做为主键。注意\_all字段及\_source字段的使用_all 字段及 _source 字段的使用,应该注意场景和需要,_all 字段包含了所有的索引字段,方便做全文检索,如果无此需求,可以禁用;_source 存储了原始的 document 内容,如果没有获取原始文档数据的需求,可通过设置 includes、excludes 属性来定义放入 _source 的字段。合理的配置使用 index 属性合理的配置使用 index 属性,analyzed 和 not_analyzed,根据业务需求来控制字段是否分词或不分词。只有 groupby 需求的字段,配置时就设置成 not_analyzed,以提高查询或聚类的效率。减少副本数量Elasticsearch 默认副本数量为3个,虽然这样会提高集群的可用性,增加搜索的并发数,但是同时也会影响写入索引的效率。在索引过程中,需要把更新的文档发到副本节点上,等副本节点生效后在进行返回结束。使用 Elasticsearch 做业务搜索的时候,建议副本数目还是设置为3个,但是像内部 ELK 日志系统、分布式跟踪系统中,完全可以将副本数目设置为1个。查询方面优化Elasticsearch 作为业务搜索的近实时查询时,查询效率的优化显得尤为重要。路由优化当我们查询文档的时候,Elasticsearch 如何知道一个文档应该存放到哪个分片中呢?它其实是通过下面这个公式来计算出来的。shard = hash(routing) % number_of_primary_shards routing 默认值是文档的 id,也可以采用自定义值,比如用户 ID。不带 routing 查询在查询的时候因为不知道要查询的数据具体在哪个分片上,所以整个过程分为2个步骤:分发:请求到达协调节点后,协调节点将查询请求分发到每个分片上。聚合:协调节点搜集到每个分片上查询结果,再将查询的结果进行排序,之后给用户返回结果。带 routing 查询查询的时候,可以直接根据 routing 信息定位到某个分配查询,不需要查询所有的分配,经过协调节点排序。向上面自定义的用户查询,如果 routing 设置为 userid 的话,就可以直接查询出数据来,效率提升很多。Filter VS Query尽可能使用过滤器上下文(Filter)替代查询上下文(Query)Query:此文档与此查询子句的匹配程度如何?Filter:此文档和查询子句匹配吗?Elasticsearch 针对 Filter 查询只需要回答「是」或者「否」,不需要像 Query 查询一样计算相关性分数,同时Filter结果可以缓存。深度翻页在使用 Elasticsearch 过程中,应尽量避免大翻页的出现。正常翻页查询都是从 from 开始 size 条数据,这样就需要在每个分片中查询打分排名在前面的 from+size 条数据。协同节点收集每个分配的前 from+size 条数据。协同节点一共会受到 N*(from+size) 条数据,然后进行排序,再将其中 from 到 from+size 条数据返回出去。如果 from 或者 size 很大的话,导致参加排序的数量会同步扩大很多,最终会导致 CPU 资源消耗增大。可以通过使用 Elasticsearch scroll 和 scroll-scan 高效滚动的方式来解决这样的问题。也可以结合实际业务特点,文档 id 大小如果和文档创建时间是一致有序的,可以以文档 id 作为分页的偏移量,并将其作为分页查询的一个条件。脚本(script)合理使用我们知道脚本使用主要有 3 种形式,内联动态编译方式、_script 索引库中存储和文件脚本存储的形式;一般脚本的使用场景是粗排,尽量用第二种方式先将脚本存储在 _script 索引库中,起到提前编译,然后通过引用脚本 id,并结合 params 参数使用,即可以达到模型(逻辑)和数据进行了分离,同时又便于脚本模块的扩展与维护。Cache的设置及使用QueryCache: ES查询的时候,使用filter查询会使用query cache, 如果业务场景中的过滤查询比较多,建议将querycache设置大一些,以提高查询速度。indices.queries.cache.size:10%(默认),可设置成百分比,也可设置成具体值,如256mb。当然也可以禁用查询缓存(默认是开启), 通过index.queries.cache.enabled:false设置。FieldDataCache: 在聚类或排序时,field data cache会使用频繁,因此,设置字段数据缓存的大小,在聚类或排序场景较多的情形下很有必要,可通过indices.fielddata.cache.size:30% 或具体值10GB来设置。但是如果场景或数据变更比较频繁,设置cache并不是好的做法,因为缓存加载的开销也是特别大的。ShardRequestCache: 查询请求发起后,每个分片会将结果返回给协调节点(Coordinating Node), 由协调节点将结果整合。如果有需求,可以设置开启; 通过设置index.requests.cache.enable: true来开启。不过,shard request cache只缓存hits.total, aggregations, suggestions类型的数据,并不会缓存hits的内容。也可以通过设置indices.requests.cache.size: 1%(默认)来控制缓存空间大小。更多查询优化经验query\_string 或 multi\_match的查询字段越多, 查询越慢。可以在mapping阶段,利用copy\_to属性将多字段的值索引到一个新字段,multi\_match时,用新的字段查询。日期字段的查询, 尤其是用now 的查询实际上是不存在缓存的,因此, 可以从业务的角度来考虑是否一定要用now, 毕竟利用query cache 是能够大大提高查询效率的。查询结果集的大小不能随意设置成大得离谱的值, 如query.setSize不能设置成 Integer.MAX_VALUE, 因为ES内部需要建立一个数据结构来放指定大小的结果集数据。避免层级过深的聚合查询, 层级过深的aggregation , 会导致内存、CPU消耗,建议在服务层通过程序来组装业务,也可以通过pipeline的方式来优化。复用预索引数据方式来提高AGG性能:如通过 terms aggregations 替代 range aggregations, 如要根据年龄来分组,分组目标是: 少年(14岁以下) 青年(14-28) 中年(29-50) 老年(51以上), 可以在索引的时候设置一个age\_group字段,预先将数据进行分类。从而不用按age来做range aggregations, 通过age\_group字段就可以了。通过开启慢查询配置定位慢查询不论是数据库还是搜索引擎,对于问题的排查,开启慢查询日志是十分必要的,ES 开启慢查询的方式有多种,但是最常用的是调用模板 API 进行全局设置:PUT /_template/{TEMPLATE_NAME} { "template":"{INDEX_PATTERN}", "settings" : { "index.indexing.slowlog.level": "INFO", "index.indexing.slowlog.threshold.index.warn": "10s", "index.indexing.slowlog.threshold.index.info": "5s", "index.indexing.slowlog.threshold.index.debug": "2s", "index.indexing.slowlog.threshold.index.trace": "500ms", "index.indexing.slowlog.source": "1000", "index.search.slowlog.level": "INFO", "index.search.slowlog.threshold.query.warn": "10s", "index.search.slowlog.threshold.query.info": "5s", "index.search.slowlog.threshold.query.debug": "2s", "index.search.slowlog.threshold.query.trace": "500ms", "index.search.slowlog.threshold.fetch.warn": "1s", "index.search.slowlog.threshold.fetch.info": "800ms", "index.search.slowlog.threshold.fetch.debug": "500ms", "index.search.slowlog.threshold.fetch.trace": "200ms" }, "version" : 1 } PUT {INDEX_PAATERN}/_settings { "index.indexing.slowlog.level": "INFO", "index.indexing.slowlog.threshold.index.warn": "10s", "index.indexing.slowlog.threshold.index.info": "5s", "index.indexing.slowlog.threshold.index.debug": "2s", "index.indexing.slowlog.threshold.index.trace": "500ms", "index.indexing.slowlog.source": "1000", "index.search.slowlog.level": "INFO", "index.search.slowlog.threshold.query.warn": "10s", "index.search.slowlog.threshold.query.info": "5s", "index.search.slowlog.threshold.query.debug": "2s", "index.search.slowlog.threshold.query.trace": "500ms", "index.search.slowlog.threshold.fetch.warn": "1s", "index.search.slowlog.threshold.fetch.info": "800ms", "index.search.slowlog.threshold.fetch.debug": "500ms", "index.search.slowlog.threshold.fetch.trace": "200ms" } 这样,在日志目录下的慢查询日志就会有输出记录必要的信息了。{CLUSTER_NAME}_index_indexing_slowlog.log {CLUSTER_NAME}_index_search_slowlog.log 数据结构优化基于 Elasticsearch 的使用场景,文档数据结构尽量和使用场景进行结合,去掉没用及不合理的数据。尽量减少不需要的字段如果 Elasticsearch 用于业务搜索服务,一些不需要用于搜索的字段最好不存到 ES 中,这样即节省空间,同时在相同的数据量下,也能提高搜索性能。避免使用动态值作字段,动态递增的 mapping,会导致集群崩溃;同样,也需要控制字段的数量,业务中不使用的字段,就不要索引。控制索引的字段数量、mapping 深度、索引字段的类型,对于 ES 的性能优化是重中之重。以下是 ES 关于字段数、mapping 深度的一些默认设置:index.mapping.nested_objects.limit: 10000 index.mapping.total_fields.limit: 1000 index.mapping.depth.limit: 20 Nested Object vs Parent/Child尽量避免使用 nested 或 parent/child 的字段,能不用就不用;nested query 慢,parent/child query 更慢,比 nested query 慢上百倍;因此能在 mapping 设计阶段搞定的(大宽表设计或采用比较 smart 的数据结构),就不要用父子关系的 mapping。如果一定要使用 nested fields,保证 nested fields 字段不能过多,目前 ES 默认限制是 50。因为针对 1 个 document,每一个 nested field,都会生成一个独立的 document,这将使 doc 数量剧增,影响查询效率,尤其是 JOIN 的效率。index.mapping.nested_fields.limit: 50 选择静态映射,非必需时,禁止动态映射尽量避免使用动态映射,这样有可能会导致集群崩溃,此外,动态映射有可能会带来不可控制的数据类型,进而有可能导致在查询端出现相关异常,影响业务。此外,Elasticsearch 作为搜索引擎时,主要承载 query 的匹配和排序的功能,那数据的存储类型基于这两种功能的用途分为两类,一是需要匹配的字段,用来建立倒排索引对 query 匹配用,另一类字段是用做粗排用到的特征字段,如 ctr、点击数、评论数等等。document 模型设计对于 MySQL,我们经常有一些复杂的关联查询。在 es 里该怎么玩儿,es 里面的复杂的关联查询尽量别用,一旦用了性能一般都不太好。最好是先在 Java 系统里就完成关联,将关联好的数据直接写入 es 中。搜索的时候,就不需要利用 es 的搜索语法来完成 join 之类的关联搜索了。document 模型设计是非常重要的,很多操作,不要在搜索的时候才想去执行各种复杂的乱七八糟的操作。es 能支持的操作就那么多,不要考虑用 es 做一些它不好操作的事情。如果真的有那种操作,尽量在 document 模型设计的时候,写入的时候就完成。另外对于一些太复杂的操作,比如 join/nested/parent-child 搜索都要尽量避免,性能都很差的。集群架构设计合理的部署 Elasticsearch 有助于提高服务的整体可用性。主节点、数据节点和协调节点分离Elasticsearch 集群在架构拓朴时,采用主节点、数据节点和负载均衡节点分离的架构,在 5.x 版本以后,又可将数据节点再细分为“Hot-Warm”的架构模式。Elasticsearch 的配置文件中有 2 个参数,node.master 和 node.data。这两个参数搭配使用时,能够帮助提供服务器性能。主(master)节点配置 node.master:true 和 node.data:false,该 node 服务器只作为一个主节点,但不存储任何索引数据。我们推荐每个集群运行3 个专用的 master 节点来提供最好的弹性。使用时,你还需要将 discovery.zen.minimum_master_nodes setting 参数设置为 2,以免出现脑裂(split-brain)的情况。用 3 个专用的 master 节点,专门负责处理集群的管理以及加强状态的整体稳定性。因为这 3 个 master 节点不包含数据也不会实际参与搜索以及索引操作,在 JVM 上它们不用做相同的事,例如繁重的索引或者耗时,资源耗费很大的搜索。因此不太可能会因为垃圾回收而导致停顿。因此,master 节点的 CPU,内存以及磁盘配置可以比 data 节点少很多的。数据(data)节点配置 node.master:false 和 node.data:true,该 node 服务器只作为一个数据节点,只用于存储索引数据,使该 node 服务器功能单一,只用于数据存储和数据查询,降低其资源消耗率。在 Elasticsearch 5.x 版本之后,data 节点又可再细分为“Hot-Warm”架构,即分为热节点(hot node)和暖节点(warm node)。hot 节点:hot 节点主要是索引节点(写节点),同时会保存近期的一些频繁被查询的索引。由于进行索引非常耗费 CPU 和 IO,即属于 IO 和 CPU 密集型操作,建议使用 SSD 的磁盘类型,保持良好的写性能;我们推荐部署最小化的 3 个 hot 节点来保证高可用性。根据近期需要收集以及查询的数据量,可以增加服务器数量来获得想要的性能。将节点设置为 hot 类型需要 elasticsearch.yml 如下配置:node.attr.box_type: hot 如果是针对指定的 index 操作,可以通过 settings 设置 index.routing.allocation.require.box_type: hot 将索引写入 hot 节点。warm 节点:这种类型的节点是为了处理大量的,而且不经常访问的只读索引而设计的。由于这些索引是只读的,warm 节点倾向于挂载大量磁盘(普通磁盘)来替代 SSD。内存、CPU 的配置跟 hot 节点保持一致即可;节点数量一般也是大于等于 3 个。将节点设置为 warm 类型需要 elasticsearch.yml 如下配置:node.attr.box_type: warm 同时,也可以在 elasticsearch.yml 中设置 index.codec:best_compression 保证 warm 节点的压缩配置。当索引不再被频繁查询时,可通过 index.routing.allocation.require.box_type:warm,将索引标记为 warm,从而保证索引不写入 hot 节点,以便将 SSD 磁盘资源用在刀刃上。一旦设置这个属性,ES 会自动将索引合并到 warm 节点。协调(coordinating)节点协调节点用于做分布式里的协调,将各分片或节点返回的数据整合后返回。该节点不会被选作主节点,也不会存储任何索引数据。该服务器主要用于查询负载均衡。在查询的时候,通常会涉及到从多个 node 服务器上查询数据,并将请求分发到多个指定的 node 服务器,并对各个 node 服务器返回的结果进行一个汇总处理,最终返回给客户端。在 ES 集群中,所有的节点都有可能是协调节点,但是,可以通过设置 node.master、node.data、node.ingest 都为 false 来设置专门的协调节点。需要较好的 CPU 和较高的内存。node.master:false和node.data:true,该node服务器只作为一个数据节点,只用于存储索引数据,使该node服务器功能单一,只用于数据存储和数据查询,降低其资源消耗率。node.master:true和node.data:false,该node服务器只作为一个主节点,但不存储任何索引数据,该node服务器将使用自身空闲的资源,来协调各种创建索引请求或者查询请求,并将这些请求合理分发到相关的node服务器上。node.master:false和node.data:false,该node服务器即不会被选作主节点,也不会存储任何索引数据。该服务器主要用于查询负载均衡。在查询的时候,通常会涉及到从多个node服务器上查询数据,并将请求分发到多个指定的node服务器,并对各个node服务器返回的结果进行一个汇总处理,最终返回给客户端。关闭 data 节点服务器中的 http 功能针对 Elasticsearch 集群中的所有数据节点,不用开启 http 服务。将其中的配置参数这样设置,http.enabled:false,同时也不要安装 head, bigdesk, marvel 等监控插件,这样保证 data 节点服务器只需处理创建/更新/删除/查询索引数据等操作。http 功能可以在非数据节点服务器上开启,上述相关的监控插件也安装到这些服务器上,用于监控 Elasticsearch 集群状态等数据信息。这样做一来出于数据安全考虑,二来出于服务性能考虑。一台服务器上最好只部署一个 node一台物理服务器上可以启动多个 node 服务器节点(通过设置不同的启动 port),但一台服务器上的 CPU、内存、硬盘等资源毕竟有限,从服务器性能考虑,不建议一台服务器上启动多个 node 节点。集群分片设置ES 一旦创建好索引后,就无法调整分片的设置,而在 ES 中,一个分片实际上对应一个 lucene 索引,而 lucene 索引的读写会占用很多的系统资源,因此,分片数不能设置过大;所以,在创建索引时,合理配置分片数是非常重要的。一般来说,我们遵循一些原则:控制每个分片占用的硬盘容量不超过 ES 的最大 JVM 的堆空间设置(一般设置不超过 32 G,参考上面的 JVM 内存设置原则),因此,如果索引的总容量在 500 G 左右,那分片大小在 16 个左右即可;当然,最好同时考虑原则 2。考虑一下 node 数量,一般一个节点有时候就是一台物理机,如果分片数过多,大大超过了节点数,很可能会导致一个节点上存在多个分片,一旦该节点故障,即使保持了 1 个以上的副本,同样有可能会导致数据丢失,集群无法恢复。所以,一般都设置分片数不超过节点数的 3 倍。链接:https://pdai.tech/md/db/nosql-es/elasticsearch-y-peformance.html(十五):ElasticSearch 性能监控我们要监控哪些Elasticsearch metricElasticsearch 提供了大量的 Metric,可以帮助您检测到问题的迹象,在遇到节点不可用、out-of-memory、long garbage collection times 的时候采取相应措施。 一些关键的检测如下:Search and indexing performance(搜索、索引性能)Memory and garbage collectionHost-level system and network metricsCluster health and node availabilityResource saturation(饱和) and errors这里提供了一个metric 搜集和监控的框架 Monitoring 101 series,所有这些指标都可以通过 Elasticsearch 的 API 以及 Elasticsearch 的 Marvel 和 Datadog 等通用监控工具访问。搜索性能指标搜索请求是Elasticsearch中的两个主要请求类型之一,另一个是索引请求。 这些请求有时类似于传统数据库系统中的读写请求。 Elasticsearch提供与搜索过程的两个主要阶段(查询和获取)相对应的度量。 下图显示了从开始到结束的搜索请求的路径。step1.客户端向Node 2 发送搜索请求 step2.Node 2(此时客串协调角色)将查询请求发送到索引中的每一个分片的副本step3. 每个分片(Lucene实例,迷你搜素引擎)在本地执行查询,然后将结果交给Node 2。Node 2 sorts and compiles them into a global priority queue.step4. Node 2发现需要获取哪些文档,并向相关的分片发送多个GET请求。step5. 每个分片loads documents然后将他们返回给Node 2step6. Node 2将搜索结果交付给客户端节点处理时,由谁分发,就由谁交付。如果您使用Elasticsearch主要用于搜索,或者如果搜索是面向客户的功能。您应该监视查询延迟和设定阈值。 监控关于查询和提取的相关指标很重要,可以帮助您确定搜索随时间的变化。 例如,您可能希望跟踪查询请求的尖峰和长期增长,以便您可以做好准备。搜索性能指标的要点:Query load: 监控当前正在进行的查询数量可以让您了解群集在任何特定时刻处理的请求数量。您可能还想监视搜索线程池队列的大小,稍后我们将在本文中进一步解释。Query latency: 虽然Elasticsearch没有明确提供此度量标准,但监控工具可以帮助您使用可用的指标来计算平均查询延迟,方法是以定期查询总查询次数和总经过时间。 如果延迟超过阈值,则设置警报,如果触发,请查找潜在的资源瓶颈,或调查是否需要优化查询。Fetch latency: 搜索过程的第二部分,即提取阶段通常比查询阶段要少得多的时间。如果您注意到这一指标不断增加,可能是磁盘性能不好、highlighting影响、requesting too many results的原因。索引性能指标索引请求类似于传统数据库系统中的写入请求,如果es的写入工作量很重,那么监控和分析您能够如何有效地使用新数据更新索引非常重要。在了解指标之前,让我们来探索Elasticsearch更新索引的过程,在新数据被添加进索引、更新或删除已有数据,索引中的每个shard都有两个过程:refresh 和 flush Index fresh 新索引的文档不能立马被搜索的。 首先,它们被写入一个内存中的缓冲区(in-memory buffer),等待下一次索引刷新,默认情况下每秒一次。刷新是以in-memory buffer为基础创建in-memory segment的过程(The refresh process creates a new in-memory segment from the contents of the in-memory buffer )。这样索引进的文档才能是可被搜索的,创建完segment后,清空buffer 如下图:A special segment on segments索引由shards构成,shard又由很多segments组成,The core data structure from Lucene, a segment is essentially a change set for the index. 这些segments在每次刷新的时候被创建,随后会在后台进行合并,以确保资源的高效利用(每个segment都要占file handles、memory、CPU) segments 是mini的倒排索引,这些倒排索引映射了terms到documents。每当搜索索引的时候,每个主副shards都必须被遍历。更深一步说shards上的每个segment会被依次搜索。 segment是不可变的,因此updating a document 意味着如下:writing the information to a new segment during the refresh processmarking the old information as deleted当多个outdated segment合并后才会被删除。(意思是不单个删除,合并后一起删)。Index flush在新索引的document添加到in-memory buffer的同时,它们也会被附加到分片的translog(a persistent, write-ahead transaction log of operations)中。每隔30分钟,或者每当translog达到最大大小(默认情况下为512MB)时,将触发flush 。在flush 期间,在in-memory buffer上的documents会被refreshed(存到新的segments上),所有内存中的segments都提交到磁盘,并且translog被清空。 translog有助于防止节点发生故障时的数据丢失。It is designed to help a shard recover operations that may otherwise have been lost between flushes. 这个translog每5秒将操作信息(索引,删除,更新或批量请求(以先到者为准))固化到磁盘上。Elasticsearch提供了许多指标,可用于评估索引性能并优化更新索引的方式。索引性能指标的要点:Indexing latency: Elasticsearch不会直接公开此特定指标,但是监控工具可以帮助您从可用的index_total和index_time_in_millis指标计算平均索引延迟。 如果您注意到延迟增加,您可能会一次尝试索引太多的文档(Elasticsearch的文档建议从5到15兆字节的批量索引大小开始,并缓慢增加)。如果您计划索引大量文档,并且不需要立即可用于搜索。则可以通过减少刷新频率来优化。索引设置API使您能够暂时禁用刷间隔。curl -XPUT <nameofhost>:9200/<name_of_index>/_settings -d '{ "index" : { "refresh_interval" : "-1" } }' 完成索引后,您可以恢复为默认值“1s”</name_of_index>Flush latency: 在flush完成之前,数据不会被固化到磁盘中。因此追踪flush latency很有用。比如我们看到这个指标稳步增长,表明磁盘性能不好。这个问题将最终导致无法向索引添加新的数据。 可以尝试降低index.translog.flush_threshold_size。这个设置决定translog的最大值(在flush被触发前)。内存使用和GC指标在运行Elasticsearch时,内存是您要密切监控的关键资源之一。 Elasticsearch和Lucene以两种方式利用节点上的所有可用RAM:JVM heap和文件系统缓存。 Elasticsearch运行在Java虚拟机(JVM)中,这意味着JVM垃圾回收的持续时间和频率将成为其他重要的监控领域。JVM heap: A Goldilocks taleElasticsearch强调了JVM堆大小的重要性,这是“正确的” - 不要将其设置太大或太小,原因如下所述。 一般来说,Elasticsearch的经验法则是将少于50%的可用RAM分配给JVM堆,而不会超过32 GB。 您分配给Elasticsearch的堆内存越少,Lucene就可以使用更多的RAM,这很大程度上依赖于文件系统缓存来快速提供请求。 但是,您也不想将堆大小设置得太小,因为应用程序面临来自频繁GC的不间断暂停,可能会遇到内存不足错误或吞吐量降低的问题 Elasticsearch的默认安装设置了1 GB的JVM heap大小,对于大多数用例来说,太小了。 您可以将所需的heap大小导出为环境变量并重新启动Elasticsearch:export ES_HEAP_SIZE=10g 如上我们设置了es heap大小为10G,通过如下命令进行校验:curl -XGET http://:9200/_cat/nodes?h=heap.max Garbage collectionElasticsearch依靠垃圾收集过程来释放heap memory。因为垃圾收集使用资源(为了释放资源!),您应该注意其频率和持续时间,以查看是否需要调整heap大小。设置过大的heap会导致GC时间过长,这些长时间的停顿会让集群错误的认为该节点已经脱离。JVM指标的要点JVM heap in use: 当JVM heap 使用率达到75%时,es启动GC。如上图所示,可以监控node的JVM heap,并且设置一个警报,确认哪个节点是否一直超过%85。如果一直超过,则表明垃圾的收集已经跟不上垃圾的产生。此时可以通过增加heap(需要满足建议法则不超过32G),或者通过增加节点来扩展集群,分散压力。JVM heap used vs. JVM heap committed: 与commit的内存(保证可用的数量)相比,了解当前正在使用多少JVM heap的情况可能会有所帮助。heap memory的图一般是个锯齿图,在垃圾收集的时候heap上升,当收集完成后heap下降。如果这个锯齿图向上偏移,说明垃圾的收集速度低于rate of object creation,这可能会导致GC时间放缓,最终OutOfMemoryErrors。Garbage collection duration and frequency: Both young- and old-generation garbage collectors undergo “stop the world” phases, as the JVM halts execution of the program to collect dead objects。在此期间节点cannot complete any task。主节点每30秒会去检查其他节点的状态,如果任何节点的垃圾回收时间超过30秒,则会导致主节点任务该节点脱离集群。Memory usage: 如上所述,es非常会利用除了分配给JVM heap的任何RAM。像Kafka一样,es被设计为依赖操作系统的文件系统缓存来快速可靠地提供请求。 许多变量决定了Elasticsearch是否成功读取文件系统缓存,如果segment file最近由es写入到磁盘,它已经in the cache。然而如果节点被关闭并重新启动,首次查询某个segment的时候,数据很可能是必须从磁盘中读取,这是确保您的群集保持稳定并且节点不会崩溃的重要原因之一。 总的来说,监控节点上的内存使用情况非常重要,并且尽可能多给es分配RAM,so it can leverage the speed of the file system cache without running out of space。es主机的网络和系统虽然Elasticsearch通过API提供了许多特定于应用程序的指标,但您也应该从每个节点收集和监视几个主机级别的指标。 Host指标要点:Disk space: 如果数据很多,这个指标很关键。如果disk space 过小,讲不能插入或更新任何内容,并且节点会挂掉。可以使用Curator这样的工具来删除特定的索引以保持disk的可用性。 如果不让删除索引,另外的办法是添加磁盘、添加节点。请记住analyzed field占用磁盘的空间远远高于non-analyzed fields。I/O utilization: 由于创建,查询和合并segment,Elasticsearch会对磁盘进行大量写入和读取,于具有不断遇到大量I / O活动的节点的写入繁重的集群,Elasticsearch建议使用SSD来提升性能。CPU utilization: 在每个节点类型的热图(如上所示)中可视化CPU使用情况可能会有所帮助。 例如,您可以创建三个不同的图表来表示集群中的每组节点(例如,数据节点,主节点,客户端节点), 如果看到CPU使用率的增加,这通常是由于搜索量大或索引工作负载引起的。 如果需要,可以添加更多节点来重新分配负载。Network bytes sent/received: 节点之间的通讯是集群平衡的关键。因此需要监控network来确保集群的health以及对集群的需求(例如,segment在节点之间进行复制或重新平衡)。 Elasticsearch提供有关集群通信的指标,但也可以查看发送和接收的字节数,以查看network接收的流量。Open file descriptors: 文件描述符用于节点到节点的通信,客户端连接和文件操作。如果这个number达到了系统的最大值,则只有在旧的连接和文件操作关闭之后才能进行新的连接和文件操作。 如果超过80%的可用文件描述符被使用,您可能需要增加系统的最大文件描述符数量。大多数Linux系统每个进程只允许1024个文件描述符。 在生产中使用Elasticsearch时,您应该将操作系统文件描述符计数重新设置为更大,如64,000。HTTP connections:可以用任何语言发送请求,但Java将使用RESTful API通过HTTP与Elasticsearch进行通信。 如果打开的HTTP连接总数不断增加,可能表示您的HTTP客户端没有正确建立持久连接。 重新建立连接会在您的请求响应时间内添加额外的毫秒甚至秒。 确保您的客户端配置正确,以避免对性能造成负面影响,或使用已正确配置HTTP连接的官方Elasticsearch客户端。 集群健康和节点可用性指标要点Cluster status: 如果集群状态为黄色,则至少有一个副本分片未分配或丢失。 搜索结果仍将完成,但如果更多的分片消失,您可能会丢失数据。 红色的群集状态表示至少有一个主分片丢失,并且您缺少数据,这意味着搜索将返回部分结果。 您也将被阻止索引到该分片。 Consider setting up an alert to trigger if status has been yellow for more than 5 min or if the status has been red for the past minute.Initializing and unassigned shards: 当首次创建索引或者重启节点,其分片将在转换到“started”或“unassigned”状态之前暂时处于“initializing”状态,此时主节点正在尝试将分片分配到集群中的数据节点。 如果您看到分片仍处于初始化或未分配状态太长时间,则可能是您的集群不稳定的警告信号。资源saturation and errorses节点使用线程池来管理线程如何消耗内存和CPU。 由于线程池设置是根据处理器数量自动配置的,所以调整它们通常没有意义。However, it’s a good idea to keep an eye on queues and rejections to find out if your nodes aren’t able to keep up; 如果无法跟上,您可能需要添加更多节点来处理所有并发请求。Fielddata和过滤器缓存使用是另一个要监视的地方,as evictions may point to inefficient queries or signs of memory pressure。Thread pool queues and rejections每个节点维护许多类型的线程池; 您要监视的确切位置将取决于您对es的具体用途,一般来说,监控的最重要的是搜索,索引,merge和bulk,它们与请求类型(搜索,索引,合并和批量操作)相对应。 线程池队列的大小反应了当前等待的请求数。 队列允许节点跟踪并最终服务这些请求,而不是丢弃它们。 一旦超过线程池的maximum queue size,Thread pool rejections就会发生。指标要点:Thread pool queues: 大队列不理想,因为它们耗尽资源,并且如果节点关闭,还会增加丢失请求的风险。如果你看到线程池rejected稳步增加,你可能希望尝试减慢请求速率(如果可能),增加节点上的处理器数量或增加群集中的节点数量。 如下面的截图所示,查询负载峰值与搜索线程池队列大小的峰值相关,as the node attempts to keep up with rate of query requests。Bulk rejections and bulk queues: 批量操作是一次发送许多请求的更有效的方式。 通常,如果要执行许多操作(创建索引或添加,更新或删除文档),则应尝试以批量操作发送请求,而不是发送许多单独的请求。 bulk rejections 通常与在一个批量请求中尝试索引太多文档有关。根据Elasticsearch的文档,批量rejections并不是很需要担心的事。However, you should try implementing a linear or exponential backoff strategy to efficiently deal with bulk rejections。Cache usage metrics: 每个查询请求都会发送到索引中的每个分片的每个segment中,Elasticsearch caches queries on a per-segment basis to speed up response time。另一方面,如果您的缓存过多地堆积了这些heap,那么它们可能会减慢速度,而不是加快速度! 在es中,文档中的每个字段可以以两种形式存储:exact value 和 full text。 例如,假设你有一个索引,它包含一个名为location的type。每个type的文档有个字段叫city。which is stored as an analyzed string。你索引了两个文档,一个的city字段为“St. Louis”,另一个的city字段为“St. Paul”。在倒排索引中存储时将变成小写并忽略掉标点符号,如下表分词的好处是你可以搜索st。结果会搜到两个。如果将city字段保存为exact value,那只能搜“St. Louis”, 或者 “St. Paul”。 Elasticsearch使用两种主要类型的缓存来更快地响应搜索请求:fielddata和filter。Fielddata cache: fielddata cache 在字段排序或者聚合时使用。 a process that basically has to uninvert the inverted index to create an array of every field value per field, in document order. For example, if we wanted to find a list of unique terms in any document that contained the term “st” from the example above, we would:1.扫描倒排索引查看哪些文档(documents)包含这个term(在本例中为Doc1和Doc2) 。2.对1中的每个步骤,通过索引中的每个term 从文档中来收集tokens,创建如下结构。3.现在反向索引被再反向,从doc中compile 独立的tokens(st, louis, and paul)。compile这样的fielddata可能会消耗大量堆内存。特别是大量的documents和terms的情况下。 所有字段值都将加载到内存中。对于1.3之前的版本,fielddata缓存大小是无限制的。 从1.3版开始,Elasticsearch添加了一个fielddata断路器,如果查询尝试加载需要超过60%的堆的fielddata,则会触发。Filter cache: 过滤缓存也使用JVM堆。 在2.0之前的版本中,Elasticsearch自动缓存过滤的查询,最大值为堆的10%,并且将最近最少使用的数据逐出。 从版本2.0开始,Elasticsearch会根据频率和段大小自动开始优化其过滤器缓存(缓存仅发生在索引中少于10,000个文档的段或小于总文档的3%)。 因此,过滤器缓存指标仅适用于使用2.0之前版本的Elasticsearch用户。 例如,过滤器查询可以仅返回年份字段中的值在2000-2005范围内的文档。 在首次执行过滤器查询时,Elasticsearch将创建一个与其相匹配的文档的位组(如果文档匹配则为1,否则为0)。 使用相同过滤器后续执行查询将重用此信息。 无论何时添加或更新新的文档,也会更新bitset。 如果您在2.0之前使用的是Elasticsearch版本,那么您应该关注过滤器缓存以及驱逐指标(更多关于以下内容)。Fielddata cache evictions: 理想情况下,我们需要限制fielddata evictions的数量,因为他们很吃I/O。如果你看到很多evictions并且你又不能增加内存。es建议限制fielddata cache的大小为20%的heap size。这些是可以在elasticsearch.yml中进行配置的。当fielddata cache达到20%的heap size时,es将驱逐最近最少使用的fielddata,然后允许您将新的fielddata加载到缓存中。 es还建议使用doc values,因为它们与fielddata的用途相同。由于它们存储在磁盘上,它们不依赖于JVM heap。尽管doc values不能被用来分析字符串, they do save fielddata usage when aggregating or sorting on other types of fields。在2.0版本后,doc values会在文档被index的时候自动创建,which has reduced fielddata/heap usage for many users。Filter cache evictions: 如前所述,filter cache eviction 指标只有在es2.0之前的版本可用。每个segment都维护自己的filter cache eviction。因为eviction在大的segment上操作成本较高,没有的明确的方法来评估eviction。但是如果你发现eviction很频繁,表明你并没有很好地利用filter,此时你需要重新创建filter,即使放弃原有的缓存,你也可能需要调整查询方式(用bool query 而不是 and/or/not filter)。Pending tasks:pending task只能由主节点来进行处理,这些任务包括创建索引并将shards分配给节点。任务分优先次序。如果任务的产生比处理速度更快,将会产生堆积。待处理任务的数量是您的群集运行平稳的良好指标,如果您的主节点非常忙,并且未完成的任务数量不会减少,我们需要仔细检查原因。Unsuccessful GET requests: GET请求比正常的搜索请求更简单 - 它根据其ID来检索文档。 get-by-ID请求不成功意味着找不到文档如何收集 ElasticSearch 的指标ElasticSearch 指标收集工具集群健康状态和各种性能 API有表格数据的_cat API开源的监控工具 (ElasticHQ, Kopf, Marvel)ElasticSearch 的 RESTFull API + JSON默认情况下,集群对外开启了 9200 端口,用于整个集群的管理操作、索引的增删改查,以及整体集群、节点、索引的状态信息,通常需要关注如下几个对外 API。Node Stats API: 节点状态 APICluster Stats API: 集群状态 APIIndex Stats API: 索引状态 APICluster Health API: 集群健康状态 APIPending Tasks API: 阻塞任务状态 API如下表列出了一些常见的指标以及对应的 API 接口。Node Stats APINode 状态接口是一个功能强大的工具,能提供除集群运行状况和挂起任务外几乎全部的性能指标。注意: 对于节点状态 API 来讲,最重要的就是 indices 。在 ElasticSearch 的所有对外接口参数中,pretty 的 URI 参数标识以 json 格式进行输出,否则将输出的是字符串。# 查看集群全部节点的指标 $ curl "localhost:9200/_nodes/stats" # 输出的3级指标 { "_nodes":{ "total":6, "successful":6, "failed":0 }, "cluster_name":"prod-one-id", "nodes":{ "T3bjsBQUSeu0bstT7m8LCA":{ "timestamp":1604802841283, "name":"iZbp11gqesu0zk5sqrgwu4Z", "transport_address":"172.16.71.231:9300", "host":"172.16.71.231", "ip":"172.16.71.231:9300", "roles":Array[3], "indices":Object{...}, "os":Object{...}, "process":Object{...}, "jvm":Object{...}, "thread_pool":Object{...}, "fs":Object{...}, "transport":Object{...}, "http":Object{...}, "breakers":Object{...}, "script":Object{...}, "discovery":Object{...}, "ingest":Object{...}, "adaptive_selection":Object{...} }, "93HMUUReSYeQEaNTfNUWCQ":Object{...}, "_6TNUy4nSZ-jxumgiroqlg":Object{...}, "cEEZcNJGS0mSgppe82SZ9Q":Object{...}, "utKipUwYQpi9ac4Q7sI53g":Object{...}, "SE7IppNARjugsLSnPhil9g":Object{...} } } 在集群规模比较大时,整个 node 的状态数据会比较多,此时可以指定 id,address,name 或者节点的其他属性来查看指定节点的状态信息。# 可以指定节点id,ip,name $ curl -s "http://localhost:9200/_nodes/T3bjsBQUSeu0bstT7m8LCA/stats" $ curl -s "http://localhost:9200/_nodes/172.16.71.231/stats" $ curl -s "http://localhost:9200/_nodes/iZbp11gqesu0zk5sqrgwu4Z/stats" 当然,有时候,我们依然觉得,单个节点的指标比较多,我们对某些指标项目进行过滤。# 查看某个节点的指定指标 $ curl -s "http://localhost:9200/_nodes/172.16.71.231/stats/jvm,os " Cluster Stats API集群指标接口提供了集群范围内的信息,因此,它基本上是集群中每个节点的所有统计数据相加。虽然提供的数据不够详细,但是对于快速了解集群状态是非常有用的。集群级别比较重要的几个指标:status: 集群状态 (green|red|yellow)nodes: 集群的整体节点统计信息 (/_nodes/stats 的求和,指标和指标项会比较精简:fs,jvm,os,process )indices: 集群的索引整体状况# 查看集群整体状况 $ curl -s "localhost:9200/_cluster/stats" # 输出的三级指标 { "_nodes":{ "failed":0, "successful":6, "total":6 }, "cluster_name":"prod-one-id", "indices":{ "completion":Object{...}, "count":5, "docs":Object{...}, "fielddata":Object{...}, "query_cache":Object{...}, "segments":Object{...}, "shards":Object{...}, "store":Object{...} }, "nodes":{ "count":Object{...}, "fs":Object{...}, "jvm":Object{...}, "network_types":Object{...}, "os":Object{...}, "plugins":Array[0], "process":Object{...}, "versions":Array[1] }, "status":"green", "timestamp":1604806385128 } Index Stats APIIndex 状态接口可以反映一个指定索引的状态信息。使用该接口可以快速查看索引的分片状态,主分片的各个操作详情统计,以及单个索引的详情统计,indices 下具体索引的详情信息 (indexing,get,search,merges,refresh,flush)# 查看指定索引的状态信息 # .elastichq 为索引名称 $ curl -s localhost:9200/.elastichq/_stats # 输出的三级指标 { "_shards":{ "total":10, "successful":10, "failed":0 }, "_all":{ "primaries":Object{...}, "total":Object{...} }, "indices":{ ".elastichq":{ "primaries":Object{...}, "total":Object{...} } } } Cluster Health HTTP API在所有对外接口中,提供集群级别的运行态数据外,还提供了集群健康状态的接口。该接口可以公开整个集群运行状况的关键信息。$ curl localhost:9200/_cluster/health # 集群健康状态 { "cluster_name":"prod-one-id", "status":"green", "timed_out":false, "number_of_nodes":6, "number_of_data_nodes":6, "active_primary_shards":13, "active_shards":31, "relocating_shards":0, "initializing_shards":0, "unassigned_shards":0, "delayed_unassigned_shards":0, "number_of_pending_tasks":0, "number_of_in_flight_fetch":0, "task_max_waiting_in_queue_millis":0, "active_shards_percent_as_number":100 } Pending Tasks API待处理任务 API 是一种快速查看群集中待处理任务的快速方法。需要注意的是,pending task 是只有主节点才能执行的任务,比如创建新索引或者重建集群的分片。如果主节点无法跟上这些请求的速度,则挂起的任务将开始排队。$ curl localhost:9200/_cluster/pending_tasks {"tasks":[]} 正常情况下,将返回空的待处理任务。否则,您将收到关于每个未决任务的优先级、它在队列中等待了多长时间以及它代表了什么动作的信息。{ "tasks" : [ { "insert_order" : 13612, "priority" : "URGENT", "source" : "delete-index [old_index]", "executing" : true, "time_in_queue_millis" : 26, "time_in_queue" : "26ms" }, { "insert_order" : 13613, "priority" : "URGENT", "source" : "shard-started ([new_index][0], node[iNTLLuV0R_eYdGGDhBkMbQ], [P], v[1], s[INITIALIZING], a[id=8IFnF0A5SMmKQ1F6Ot-VyA], unassigned_info[[reason=INDEX_CREATED], at[2016-07-28T19:46:57.102Z]]), reason [after recovery from store]", "executing" : false, "time_in_queue_millis" : 23, "time_in_queue" : "23ms" }, { "insert_order" : 13614, "priority" : "URGENT", "source" : "shard-started ([new_index][0], node[iNTLLuV0R_eYdGGDhBkMbQ], [P], v[1], s[INITIALIZING], a[id=8IFnF0A5SMmKQ1F6Ot-VyA], unassigned_info[[reason=INDEX_CREATED], at[2016-07-28T19:46:57.102Z]]), reason [master {master-node-1}{iNTLLuV0R_eYdGGDhBkMbQ}{127.0.0.1}{127.0.0.1:9300} marked shard as initializing, but shard state is [POST_RECOVERY], mark shard as started]", "executing" : false, "time_in_queue_millis" : 20, "time_in_queue" : "20ms" } ] } cat APICAT 接口也提供了一些查看相同指标的可选方案,类似于 UNIX 系统中的 cat 命令。$ curl http://localhost:9200/_cat =^.^= /_cat/allocation /_cat/shards /_cat/shards/{index} /_cat/master /_cat/nodes /_cat/tasks /_cat/indices /_cat/indices/{index} /_cat/segments /_cat/segments/{index} /_cat/count /_cat/count/{index} /_cat/recovery /_cat/recovery/{index} /_cat/health /_cat/pending_tasks /_cat/aliases /_cat/aliases/{alias} /_cat/thread_pool /_cat/thread_pool/{thread_pools} /_cat/plugins /_cat/fielddata /_cat/fielddata/{fields} /_cat/nodeattrs /_cat/repositories /_cat/snapshots/{repository} /_cat/templates 比如,我们可以使用 curl localhost:9200/_cat/nodes?help 来查看 node api 相关的指标和描述,进而采用这些描述来查询具体的指标项。如果我们只想查看节点的堆内存使用率、合并数量 (merges) 以及段数量 (segments),可以采用如下方式来查看:# 指定查看每个节点的堆内存使用率,段数量和合并数量 $ curl "http://localhost:9200/_cat/nodes?h=http,heapPercent,segmentsCount,mergesTotal" 172.16.71.231:9200 56 99 108182 172.16.71.229:9200 31 95 122551 172.16.71.232:9200 50 66 73871 172.16.71.230:9200 41 63 76470 172.16.71.234:9200 32 64 93256 172.16.71.233:9200 14 90 136450 注意: 上述输出相当于是 Node Stats API 中的 jvm.mem.heap_used_percent,segments.count,merges.total,整个 CAT 接口是一个可以快速获取集群,节点,索引以及分片的状态数据,并且能够以可读的方式展示出来。已实现的开源工具虽然整个 ES 对外的接口已经能够提供很好的接口来描述瞬时的指标,但是通常情况下,我们有很多节点需要进行持续的监控,而接口的 JSON 格式又不便于我们进行解析和分析,很难快速识别到问题节点并及时发现问题趋势。为了更加有效的监控 ElasticSearch,我们通常需要一些工具来定期采集 API 的指标数据,然后聚合指标结果来反应当前集群的整体状态。而在开源社区中,也产生了很多这种类似的工具系统。Elastic HQElasticHQ 是一个可座位托管方案,插件化下载的开源监控工具。它能够提供你的集群,节点,索引,以及一些相关的查询和映射的指标。ElasticHQ 会自动对指标进行颜色编码,以突出潜在的问题。插件化安装:$ ${ES_HOME}/bin/elasticsearch-plugin install royrusso/elasticsearch-HQ 安装完成后,可以访问 http://localhost:9200/_plugin/hq/ 来访问当前集群的监控信息。使用 Docker 进行托管方式安装:$ docker run -itd -p 8081:5000 -v /opt/data/elastichq:/src/db --restart=always --name elastichq elastichq/elasticsearch-hq 接下来,就可以访问主机的 8081 端口来查看 ElasticHQ 的监控管理了,需要注意的是,此时需要添加集群地址。其他监控插件开源领域也有其他插件,比如 kopf 和 Cerebro 前者比较老,且现在不再更新了,而后者是一个比较全面的监控工具,且支持 LDAP 工具登录。Cerebro:https://github.com/lmenezes/cerebro/# ldap 的配置信息 $ cat env-ldap # Set it to ldap to activate ldap authorization AUTH_TYPE=ldap # Your ldap url LDAP_URL=ldap://exammple.com:389 LDAP_BASE_DN=OU=users,DC=example,DC=com # Usually method should be "simple" otherwise, set it to the SASL mechanisms LDAP_METHOD=simple # user-template executes a string.format() operation where # username is passed in first, followed by base-dn. Some examples # - %s => leave user untouched # - %s@domain.com => append "@domain.com" to username # - uid=%s,%s => usual case of OpenLDAP LDAP_USER_TEMPLATE=%s@example.com # User identifier that can perform searches LDAP_BIND_DN=admin@example.com LDAP_BIND_PWD=adminpass # Group membership settings (optional) # If left unset LDAP_BASE_DN will be used # LDAP_GROUP_BASE_DN=OU=users,DC=example,DC=com # Attribute that represent the user, for example uid or mail # LDAP_USER_ATTR=mail # If left unset LDAP_USER_TEMPLATE will be used # LDAP_USER_ATTR_TEMPLATE=%s # Filter that tests membership of the group. If this property is empty then there is no group membership check # AD example => memberOf=CN=mygroup,ou=ouofthegroup,DC=domain,DC=com # OpenLDAP example => CN=mygroup # LDAP_GROUP=memberOf=memberOf=CN=mygroup,ou=ouofthegroup,DC=domain,DC=com $ docker run -p 9000:9000 --env-file env-ldap lmenezes/cerebro 登录首页集群概况以及索引信息节点状态来源:https://www.datadoghq.com/blog/monitor-elasticsearch-performance-metrics/ https://www.datadoghq.com/blog/monitor-elasticsearch-performance-metrics/
2023年09月22日
107 阅读
5 评论
0 点赞
2023-09-19
《从菜鸟到大师之路 Redis 篇》
《从菜鸟到大师之路 Redis 篇》(一):Redis 基础理论与安装配置Nosql 数据库介绍是一种 非关系型 数据库服务,它能 解决常规数据库的并发能力 ,比如 传统的数据库的IO与性能的瓶颈 ,同样它是关系型数据库的一个补充,有着比较好的高效率与高性能。专注于key-value查询的redis、memcached、ttserver。解决以下问题对数据库的高并发读写需求大数据的高效存储和访问需求高可扩展性和高可用性的需求什么是 RedisRedis 是一款 内存高速缓存 数据库。Redis全称为: Remote Dictionary Server(远程数据服务) ,使用C语言编写,Redis是一个key-value存储系统(键值存储系统),支持丰富的数据类型,如:String、list、set、zset、hash。Redis是一种支持key-value等多种数据结构的存储系统。可用于缓存,事件发布或订阅,高速队列等场景。支持网络,提供字符串,哈希,列表,队列,集合结构直接存取,基于内存,可持久化。 官方资料Redis官网:http://redis.io/Redis官方文档:http://redis.io/documentationRedis教程:http://www.w3cschool.cn/redis/redis-intro.htmlRedis下载:http://redis.io/download为什么要使用 Redis一个产品的使用场景肯定是需要根据产品的特性,先列举一下Redis的特点读写性能优异Redis能读的速度是110000次/s,写的速度是81000次/s (测试条件见下一节)。数据类型丰富Redis支持二进制案例的 Strings, Lists, Hashes, Sets 及 Ordered Sets 数据类型操作。原子性Redis的所有操作都是原子性的,同时Redis还支持对几个操作全并后的原子性执行。丰富的特性Redis支持 publish/subscribe, 通知, key 过期等特性。持久化Redis支持RDB, AOF等持久化方式发布订阅Redis支持发布/订阅模式分布式Redis Cluster所以,无论是运维还是开发、测试,对于 NoSQL 数据库之一的 Redis 也是必学知识体系之一。下面是官方的bench-mark根据如下条件获得的性能测试(读的速度是110000次/s,写的速度是81000次/s)测试完成了50个并发执行100000个请求。设置和获取的值是一个256字节字符串。Linux box是运行Linux 2.6,这是X3320 Xeon 2.5 ghz。文本执行使用loopback接口(127.0.0.1)。Redis有哪些优缺点优点读写性能优异 , Redis能读的速度是110000次/s,写的速度是81000次/s。支持数据持久化 ,支持AOF和RDB两种持久化方式。支持事务 ,Redis的所有操作都是原子性的,同时Redis还支持对几个操作合并后的原子性执行。数据结构丰富 ,除了支持string类型的value外还支持hash、set、zset、list等数据结构。支持主从复制 ,主机会自动将数据同步到从机,可以进行读写分离。缺点数据库容量受到物理内存的限制,不能用作海量数据的高性能读写 ,因此Redis适合的场景主要 局限在较小数据量 的高性能操作和运算上。Redis 不具备自动容错和恢复功能 ,主机从机的宕机都会导致前端部分读写请求失败,需要等待机器重启或者手动切换前端的IP才能恢复。主机宕机,宕机前有部分数据未能及时同步到从机,切换IP后还会引入数据不一致的问题 ,降低了系统的可用性。Redis 较难支持在线扩容,在集群容量达到上限时在线扩容会变得很复杂。 为避免这一问题,运维人员在系统上线时必须确保有足够的空间,这对资源造成了很大的浪费。Redis的使用场景redis 应用场景总结 redis 平时我们用到的地方蛮多的,下面就了解的应用场景做个总结:热点数据的缓存缓存是Redis最常见的应用场景,之所有这么使用,主要是因为Redis读写性能优异。而且逐渐有取代memcached,成为首选服务端缓存的组件。而且,Redis内部是支持事务的,在使用时候能有效保证数据的一致性。有两种方式保存数据作为缓存使用时,一般 有两种方式保存数据 :方案一:读取前,先去读Redis,如果没有数据,读取数据库,将数据拉入Redis实施起来简单,但是有两个需要注意的地方:避免缓存击穿。(数据库没有就需要命中的数据,导致Redis一直没有数据,而一直命中数据库。)数据的实时性相对会差一点。方案二:插入数据时,同时写入Redis数据实时性强,但是开发时不便于统一处理。当然,两种方式根据实际情况来适用。如: 方案一适用于对于数据实时性要求不是特别高的场景。方案二适用于字典表、数据量不大的数据存储。限时业务的运用redis中可以使用 expire 命令设置一个键的生存时间,到时间后redis会删除它。利用这一特性可以 运用在限时的优惠活动信息、手机验证码 等业务场景。计数器相关问题redis由于 incrby 命令可以实现 原子性的递增 ,所以可以 运用于高并发的秒杀活动、分布式序列号的生成、具体业务还体现在比如限制一个手机号发多少条短信、一个接口一分钟限制多少请求、一个接口一天限制调用多少次 等等。int类型,incr方法 例如: 文章的阅读量、微博点赞数、允许一定的延迟,先写入Redis再定时同步到数据库 。分布式锁这个主要利用redis的 setnx 命令进行,setnx:"set if not exists"就是如果不存在则成功设置缓存同时返回1,否则返回0 ,这个特性在很多后台中都有所运用,因为我们服务器是集群的,定时任务可能在两台机器上都会运行,所以在定时任务中首先 通过setnx设置一个lock, 如果成功设置则执行,如果没有成功设置,则表明该定时任务已执行。当然结合具体业务,我们可以给这个lock加一个过期时间,比如说30分钟执行一次的定时任务,那么这个过期时间设置为小于30分钟的一个时间就可以,这个与定时任务的周期以及定时任务执行消耗时间相关。String 类型setnx方法,只有不存在时才能添加成功,返回truepublic static boolean getLock(String key) { Long flag = jedis.setnx(key, "1"); if (flag == 1) { jedis.expire(key, 10); } return flag == 1; } public static void releaseLock(String key) { jedis.del(key); }在分布式锁的场景中,主要用在比如秒杀系统等。延时操作比如 在订单生产后我们占用了库存,10分钟后去检验用户是否真正购买,如果没有购买将该单据设置无效,同时还原库存。 由于redis自2.8.0之后版本提供Keyspace Notifications功能,允许客户订阅Pub/Sub频道,以便以某种方式接收影响Redis数据集的事件。所以我们对于上面的需求就可以用以下解决方案,我们 在订单生产时,设置一个key,同时设置10分钟后过期, 我们在后台实现一个监听器,监听key的实效,监听到key失效时将后续逻辑加上。 当然我们也可以利用 rabbitmq、activemq 等消息中间件的 延迟队列服务 实现该需求。排行榜相关问题关系型数据库在排行榜方面查询速度普遍偏慢,所以可以借助redis的 SortedSet 进行热点数据的排序。比如点赞排行榜,做一个SortedSet, 然后以用户的openid作为上面的username, 以用户的点赞数作为上面的score, 然后针对每个用户做一个hash, 通过zrangebyscore就可以按照点赞数获取排行榜,然后再根据username获取用户的hash信息,这个当时在实际运用中性能体验也蛮不错的。点赞、好友等相互关系的存储Redis 利用集合的一些命令,比如 求交集、并集、差集 等。在微博应用中,每个用户关注的人存在一个集合中,就很容易实现求两个人的共同好友功能。假如上面的微博ID是t1001,用户ID是u3001用 like:t1001 来维护 t1001 这条微博的所有点赞用户点赞了这条微博:sadd like:t1001 u3001取消点赞:srem like:t1001 u3001是否点赞:sismember like:t1001 u3001点赞的所有用户:smembers like:t1001点赞数:scard like:t1001是不是比数据库简单多了。 7000字 Redis 超详细总结笔记 !建议收藏简单队列由于Redis有 list push和list pop 这样的命令,所以能够很方便的执行队列操作。List提供了两个阻塞的弹出操作:blpop/brpop,可以设置超时时间blpop:blpop key1 timeout 移除并获取列表的第一个元素,如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止。brpop:brpop key1 timeout 移除并获取列表的最后一个元素,如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止。上面的操作。其实就是java的阻塞队列。学习的东西越多。学习成本越低队列:先进先除:rpush blpop,左头右尾,右边进入队列,左边出队列栈:先进后出:rpush brpop更多关于Redis的应用场景解析请参阅: Redis 16 大应用场景Redis为什么这么快1、 完全基于内存,绝大部分请求是纯粹的内存操作 ,非常快速。数据存在内存中,类似于 HashMap,HashMap 的优势就是查找和操作的时间复杂度都是O(1);2、 数据结构简单,对数据操作也简单 ,Redis 中的数据结构是专门进行设计的;3、 采用单线程 ,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗;4、 使用多路 I/O 复用模型,非阻塞 IO ;5、使用底层模型不同,它们之间底层实现方式以及与客户端之间通信的应用协议不一样, Redis 直接自己构建了 VM 机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求;Redis 为什么是单线程的?代码更清晰,处理逻辑更简单;不用考虑各种锁的问题,不存在加锁和释放锁的操作,没有因为可能出现死锁而导致的性能问题;不存在多线程切换而消耗CPU;无法发挥多核CPU的优势,但可以采用多开几个Redis实例来完善;Redis真的是单线程的吗?Redis6.0之前是单线程的,Redis6.0之后开始支持多线程;Redis内部使用了基于epoll的多路复用,也可以多部署几个Redis服务器解决单线程的问题;Redis主要的性能瓶颈是内存和网络;内存好说,加内存条就行了,而网络才是大麻烦,所以Redis6内存好说,加内存条就行了;而网络才是大麻烦,所以Redis6.0引入了多线程的概念;Redis6.0在网络IO处理方面引入了多线程,如网络数据的读写和协议解析等,需要注意的是,执行命令的核心模块还是单线程的;Redis 安装Linux下安装Redis下载安装# 安装gcc yum install gcc # 下载redis wget下载或者直接去 http://redis.io/download 官网下载 wget http://download.redis.io/releases/redis-7.0.0.tar.gz # 把下载好的redis解压 tar xzf redis-7.0.0.tar.gz # 进入到解压好的redis-7.0.0.tar.gz目录下,进行编译与安装 cd redis-7.0.0.tar.gz make make install修改配置文件按需修改自己想要的redis配置。# 编辑redis.conf配置文件 vim redis.conf # Redis使用后台模式 daemonize yes # 关闭保护模式 protected-mode no # 注释以下内容开启远程访问 # bind 127.0.0.1 # 修改启动端口为6381 port 6381启动Redis# 启动并指定配置文件 src/redis‐server redis.conf(注意要使用后台启动,所以修改redis.conf里的daemonize改为yes) # 验证启动是否成功 ps -ef|grep redis # 进入redis客户端 src/redis-cli redis-cli -h 192.168.239.131 -p 6379 (指定ip 端口连接redis) # 退出客户端 quit # 退出redis服务 pkill redis‐server kill 进程号 src/redis‐cli shutdown # 设置redis密码 config set requirepass 123456 # 验证密码 auth 123456 # 查看密码 config get requirepassWindows 下安装 Redis下载安装下载地址:https://github.com/MicrosoftArchive/redis/tags直接下载 Redis-x64-3.2.100.msi 版本即可,双击安装:都选择默认即可,下一步、下一步安装就行了,非常的简单。然后可以去连接一下,cmd窗口输入命令telnet 127.0.0.1 6379正常连接,也可以正常操作Redis.conf 详解找到启动时指定的配置文件(redis.conf):单位# Redis configuration file example. # # Note that in order to read the configuration file, Redis must be # started with the file path as first argument: # # ./redis-server /path/to/redis.conf # Note on units: when memory size is needed, it is possible to specify # it in the usual form of 1k 5GB 4M and so forth: # # 1k => 1000 bytes # 1kb => 1024 bytes # 1m => 1000000 bytes # 1mb => 1024*1024 bytes # 1g => 1000000000 bytes # 1gb => 1024*1024*1024 bytes # # units are case insensitive so 1GB 1Gb 1gB are all the same.配置文件中 unit 单位对大小写不敏感。包含################################## INCLUDES ################################### # Include one or more other config files here. This is useful if you # have a standard template that goes to all Redis servers but also need # to customize a few per-server settings. Include files can include # other files, so use this wisely. # # Notice option "include" won't be rewritten by command "CONFIG REWRITE" # from admin or Redis Sentinel. Since Redis always uses the last processed # line as value of a configuration directive, you'd better put includes # at the beginning of this file to avoid overwriting config change at runtime. # # If instead you are interested in using includes to override configuration # options, it is better to use include as the last line. # # include /path/to/local.conf # include /path/to/other.conf配置文件可以将多个配置文件合起来使用。NETWORK 网络bind 127.0.0.1 #绑定的 IP protected-mode no #保护模式 port 6379 #端口设置GENERAL 通用daemonize yes # 以守护进程的方式运行,默认是 no ,我们需要自己开启为 yes pidfile /var/run/redis_6379.pid # 如果是后台启动,我们需要指定一个pid 文件 # 日志级别 # Specify the server verbosity level. # This can be one of: # debug (a lot of information, useful for development/testing) # verbose (many rarely useful info, but not a mess like the debug level) # notice (moderately verbose, what you want in production probably) # warning (only very important / critical messages are logged) loglevel notice logfile "" # 日志文件的位置 databases 16 # 数据库的数量,默认是 16 always-show-logo yes # 是否总是显示 LOGO快照 SNAPSHOTTING持久化, 在规定的时间内,执行了多少次操作则会持久化到文件 。Redis 是内存数据库, 如果没有持久化,那么数据断电即失 。################################ SNAPSHOTTING ################################ # # Save the DB on disk: # # save <seconds> <changes> # # Will save the DB if both the given number of seconds and the given # number of write operations against the DB occurred. # # In the example below the behaviour will be to save: # after 900 sec (15 min) if at least 1 key changed # after 300 sec (5 min) if at least 10 keys changed # after 60 sec if at least 10000 keys changed # # Note: you can disable saving completely by commenting out all "save" lines. # # It is also possible to remove all the previously configured save # points by adding a save directive with a single empty string argument # like in the following example: # # save "" # 如果 900s 内,至少有 1 个 key 进行了修改,进行持久化操作 save 900 1 # 如果 300s 内,至少有 10 个 key 进行了修改,进行持久化操作 save 300 10 save 60 10000 stop-writes-on-bgsave-error yes # 如果持久化出错,是否还要继续工作 rdbcompression yes # 是否压缩 rdb 文件,需要消耗一些 cpu 资源 rdbchecksum yes # 保存 rdb 文件的时候,进行错误的检查校验 dir ./ # rdb 文件保存的目录SECURITY 安全可以 设置 Redis 的密码 ,默认是没有密码的。[root@xxx bin]# redis-cli -p 6379 127.0.0.1:6379> ping PONG 127.0.0.1:6379> config get requirepass # 获取 redis 密码 1) "requirepass" 2) "" 127.0.0.1:6379> config set requirepass "123456" # 设置 redis 密码 OK 127.0.0.1:6379> ping (error) NOAUTH Authentication required. # 发现所有的命令都没有权限了 127.0.0.1:6379> auth 123456 # 使用密码登录 OK 127.0.0.1:6379> config get requirepass 1) "requirepass" 2) "123456" 127.0.0.1:6379> CLIENTS 限制################################### CLIENTS #################################### # Set the max number of connected clients at the same time. By default # this limit is set to 10000 clients, however if the Redis server is not # able to configure the process file limit to allow for the specified limit # the max number of allowed clients is set to the current file limit # minus 32 (as Redis reserves a few file descriptors for internal uses). # # Once the limit is reached Redis will close all the new connections sending # an error 'max number of clients reached'. # # maxclients 10000 # 设置能链接上 redis 的最大客户端数量 # maxmemory <bytes> # redis 设置最大的内存容量 maxmemory-policy noeviction # 内存达到上限之后的处理策略 - noeviction:当内存使用达到阈值的时候,所有引起申请内存的命令会报错。 - allkeys-lru:在所有键中采用lru算法删除键,直到腾出足够内存为止。 - volatile-lru:在设置了过期时间的键中采用lru算法删除键,直到腾出足够内存为止。 - allkeys-random:在所有键中采用随机删除键,直到腾出足够内存为止。 - volatile-random:在设置了过期时间的键中随机删除键,直到腾出足够内存为止。 - volatile-ttl:在设置了过期时间的键空间中,具有更早过期时间的key优先移除。APPEND ONLY 模式 AOF 配置appendonly no # 默认是不开启 AOF 模式的,默认使用 rdb 方式持久化,大部分情况下,rdb 完全够用 appendfilename "appendonly.aof" # 持久化的文件的名字 # appendfsync always # 每次修改都会 sync 消耗性能 appendfsync everysec # 每秒执行一次 sync 可能会丢失这 1s 的数据。 # appendfsync no # 不执行 sync 这个时候操作系统自己同步数据,速度最快。参考来源:https://www.pdai.tech/md/db/nosql-redis/db-redis-overview.html https://www.cnblogs.com/itzhouq/p/redis4.html拓展Redis简易入门15招(二):Redis 9 种数据类型和应用场景Redis 数据结构简介Redis 基础文章非常多,关于 基础数据结构类型 ,我推荐你先看下官方网站内容,然后再看下面的小结。首先对 redis 来说,所有的 key(键)都是字符串。我们在谈基础数据结构时,讨论的是存储值的数据类型,主要包括常见的5种数据类型,分别是:String、List、Set、Zset、Hash。5 种基础数据类型内容其实比较简单,我觉得理解的重点在于这个结构怎么用,能够用来做什么?所以我在梳理时,围绕图例,命令,执行和场景来阐述。String 字符串String是redis中最基本的数据类型,一个key对应一个value。String 类型是 二进制安全的 ,意思是 redis 的 string 可以包含任何数据。如数字,字符串,jpg图片或者序列化的对象。图例下图是一个String类型的实例,其中键为hello,值为world图片命令使用命令执行127.0.0.1:6379> set hello world OK 127.0.0.1:6379> get hello "world" 127.0.0.1:6379> del hello (integer) 1 127.0.0.1:6379> get hello (nil) 127.0.0.1:6379> set counter 2 OK 127.0.0.1:6379> get counter "2" 127.0.0.1:6379> incr counter (integer) 3 127.0.0.1:6379> get counter "3" 127.0.0.1:6379> incrby counter 100 (integer) 103 127.0.0.1:6379> get counter "103" 127.0.0.1:6379> decr counter (integer) 102 127.0.0.1:6379> get counter "102"实战场景缓存 :经典使用场景,把常用信息,字符串,图片或者视频等信息放到redis中,redis 作为缓存层,mysql做持久化层,降低mysql的读写压力。计数器 :redis是单线程模型,一个命令执行完才会执行下一个,同时数据可以一步落地到其他的数据源。session :常见方案spring session + redis实现session共享。List列表Redis 中的 List 其实就是 链表 (Redis 用 双端链表 实现 List )。使用 List 结构,我们可以轻松地实现 最新消息排队功能 (比如新浪微博的TimeLine)。List 的另一个应用就是消息队列,可以利用List的 PUSH 操作,将任务存放在 List 中,然后工作线程再用 POP 操作将任务取出进行执行。图例命令使用使用列表的技巧lpush+lpop=Stack(栈)lpush+rpop=Queue(队列)lpush+ltrim=Capped Collection(有限集合)lpush+brpop=Message Queue(消息队列)命令执行127.0.0.1:6379> lpush mylist 1 2 ll ls mem (integer) 5 127.0.0.1:6379> lrange mylist 0 -1 1) "mem" 2) "ls" 3) "ll" 4) "2" 5) "1" 127.0.0.1:6379> lindex mylist -1 "1" 127.0.0.1:6379> lindex mylist 10 # index不在 mylist 的区间范围内 (nil)实战场景微博TimeLine : 有人发布微博,用lpush加入时间轴,展示新的列表信息。消息队列Set集合Redis 的 Set 是 String 类型的 无序集合 。集合成员是唯一的,这就意味着 集合中不能出现重复的数据。 Redis 中集合是通过 哈希表 实现的,所以 添加,删除,查找的复杂度都是 O(1) 。图例命令使用命令执行127.0.0.1:6379> sadd myset hao hao1 xiaohao hao (integer) 3 127.0.0.1:6379> smembers myset 1) "xiaohao" 2) "hao1" 3) "hao" 127.0.0.1:6379> sismember myset hao (integer) 1实战场景标签(tag) ,给用户添加标签,或者用户给消息添加标签,这样有同一标签或者类似标签的可以给推荐关注的事或者关注的人。点赞,或点踩,收藏等 ,可以放到set中实现Hash散列Redis hash 是一个 string 类型的 field(字段) 和 value(值) 的映射表 ,hash 特别适合用于 存储对象 。图例命令使用命令执行127.0.0.1:6379> hset user name1 hao (integer) 1 127.0.0.1:6379> hset user email1 hao@163.com (integer) 1 127.0.0.1:6379> hgetall user 1) "name1" 2) "hao" 3) "email1" 4) "hao@163.com" 127.0.0.1:6379> hget user user (nil) 127.0.0.1:6379> hget user name1 "hao" 127.0.0.1:6379> hset user name2 xiaohao (integer) 1 127.0.0.1:6379> hset user email2 xiaohao@163.com (integer) 1 127.0.0.1:6379> hgetall user 1) "name1" 2) "hao" 3) "email1" 4) "hao@163.com" 5) "name2" 6) "xiaohao" 7) "email2" 8) "xiaohao@163.com"实战场景缓存 :能直观,相比string更节省空间,的维护缓存信息,如用户信息,视频信息等。Zset有序集合Redis 有序集合和集合一样也是 string 类型元素的集合,且 不允许重复的成员 。 不同的是每个元素都会关联一个 double 类型的分数 。redis 正是通过分数来为集合中的成员进行从小到大的排序。有序集合的成员是唯一的, 但分数(score)却可以重复。 有序集合是通过 两种数据结构 实现:压缩列表(ziplist) : ziplist是为了提高存储效率而设计的一种特殊编码的双向链表。它可以存储字符串或者整数,存储整数时是采用整数的二进制而不是字符串形式存储。它能在O(1)的时间复杂度下完成list两端的push和pop操作。但是因为每次操作都需要重新分配ziplist的内存,所以实际复杂度和ziplist的内存使用量相关跳跃表(zSkiplist) : 跳跃表的性能可以保证在查找,删除,添加等操作的时候在对数期望时间内完成,这个性能是可以和平衡树来相比较的,而且在实现方面比平衡树要优雅,这是采用跳跃表的主要原因。跳跃表的复杂度是O(log(n))。图例命令使用命令执行127.0.0.1:6379> zadd myscoreset 100 hao 90 xiaohao (integer) 2 127.0.0.1:6379> ZRANGE myscoreset 0 -1 1) "xiaohao" 2) "hao" 127.0.0.1:6379> ZSCORE myscoreset hao "100"实战场景排行榜 :有序集合经典使用场景。例如小说视频等网站需要对用户上传的小说视频做排行榜,榜单可以按照用户关注数,更新时间,字数等打分,做排行。3 种特殊类型Redis 除了上文中 5 种基础数据类型 ,还有 3 种特殊的数据类型 ,分别是 HyperLogLogs(基数统计), Bitmaps (位图) 和 geospatial (地理位置) 。HyperLogLogs(基数统计)Redis 2.8.9 版本更新了 Hyperloglog 数据结构!什么是基数?举个例子, A = {1, 2, 3, 4, 5}, B = {3, 5, 6, 7, 9};那么 基数(不重复的元素)= 1, 2, 4, 6, 7, 9 ;( 允许容错,即可以接受一定误差 )HyperLogLogs 基数统计用来解决什么问题?这个结构可以 非常省内存的去统计各种计数 ,比如 注册 IP 数、每日访问 IP 数、页面实时UV、在线用户数,共同好友数 等。它的优势体现在哪?一个大型的网站,每天 IP 比如有 100 万,粗算一个 IP 消耗 15 字节,那么 100 万个 IP 就是 15M。而 HyperLogLog 在 Redis 中每个键占用的内容都是 12K,理论存储近似接近 2^64 个值,不管存储的内容是什么,它一个基于基数估算的算法,只能比较准确的估算出基数,可以使用少量固定的内存去存储并识别集合中的唯一元素。而且这个估算的基数并不一定准确,是一个带有 0.81% 标准错误的近似值(对于可以接受一定容错的业务场景,比如IP数统计,UV 等,是可以忽略不计的)。相关命令使用127.0.0.1:6379> pfadd key1 a b c d e f g h i # 创建第一组元素 (integer) 1 127.0.0.1:6379> pfcount key1 # 统计元素的基数数量 (integer) 9 127.0.0.1:6379> pfadd key2 c j k l m e g a # 创建第二组元素 (integer) 1 127.0.0.1:6379> pfcount key2 (integer) 8 127.0.0.1:6379> pfmerge key3 key1 key2 # 合并两组:key1 key2 -> key3 并集 OK 127.0.0.1:6379> pfcount key3 (integer) 13Bitmap (位存储)Bitmap 即 位图数据结构 ,都是 操作二进制位 来进行记录,只有 0 和 1 两个状态。用来解决什么问题?比如: 两个状态统计用户信息,活跃,不活跃!登录,未登录!打卡,不打卡! 的,都可以使用 Bitmaps!如果存储一年的打卡状态需要多少内存呢?365 天 = 365 bit 1字节 = 8bit 46 个字节左右!相关命令使用使用bitmap 来记录 周一到周日的打卡 !周一:1 周二:0 周三:0 周四:1 ......127.0.0.1:6379> setbit sign 0 1 (integer) 0 127.0.0.1:6379> setbit sign 1 1 (integer) 0 127.0.0.1:6379> setbit sign 2 0 (integer) 0 127.0.0.1:6379> setbit sign 3 1 (integer) 0 127.0.0.1:6379> setbit sign 4 0 (integer) 0 127.0.0.1:6379> setbit sign 5 0 (integer) 0 127.0.0.1:6379> setbit sign 6 1 (integer) 0查看某一天是否有打卡!127.0.0.1:6379> getbit sign 3 (integer) 1 127.0.0.1:6379> getbit sign 5 (integer) 0统计操作,统计 打卡的天数!127.0.0.1:6379> bitcount sign # 统计这周的打卡记录,就可以看到是否有全勤! (integer) 3geospatial (地理位置)Redis 的 Geo 在 Redis 3.2 版本就推出了! 这个功能可以推算地理位置的信息: 两地之间的距离, 方圆几里的人geoadd 添加地理位置127.0.0.1:6379> geoadd china:city 118.76 32.04 manjing 112.55 37.86 taiyuan 123.43 41.80 shenyang (integer) 3 127.0.0.1:6379> geoadd china:city 144.05 22.52 shengzhen 120.16 30.24 hangzhou 108.96 34.26 xian (integer) 3规则两级无法直接添加,我们一般会下载城市数据(这个网址可以查询 GEO:http://www.jsons.cn/lngcode)!有效的经度从-180度到180度。有效的纬度从-85.05112878度到85.05112878度。# 当坐标位置超出上述指定范围时,该命令将会返回一个错误。 127.0.0.1:6379> geoadd china:city 39.90 116.40 beijin (error) ERR invalid longitude,latitude pair 39.900000,116.400000geopos 获取指定的成员的经度和纬度127.0.0.1:6379> geopos china:city taiyuan manjing 1) 1) "112.54999905824661255" 1) "37.86000073876942196" 2) 1) "118.75999957323074341" 1) "32.03999960287850968"获得当前定位, 一定是一个坐标值!geodist如果不存在, 返回空。 单位如下:mkmmi 英里ft 英尺127.0.0.1:6379> geodist china:city taiyuan shenyang m "1026439.1070" 127.0.0.1:6379> geodist china:city taiyuan shenyang km "1026.4391"georadius附近的人 ==> 获得所有附近的人的地址, 定位, 通过半径来查询。 获得指定数量的人127.0.0.1:6379> georadius china:city 110 30 1000 km 以 100,30 这个坐标为中心, 寻找半径为1000km的城市 1) "xian" 2) "hangzhou" 3) "manjing" 4) "taiyuan" 127.0.0.1:6379> georadius china:city 110 30 500 km 1) "xian" 127.0.0.1:6379> georadius china:city 110 30 500 km withdist 1) 1) "xian" 2) "483.8340" 127.0.0.1:6379> georadius china:city 110 30 1000 km withcoord withdist count 2 1) 1) "xian" 2) "483.8340" 3) 1) "108.96000176668167114" 2) "34.25999964418929977" 2) 1) "manjing" 2) "864.9816" 3) 1) "118.75999957323074341" 2) "32.03999960287850968"参数:key 经度 纬度 半径 单位 [显示结果的经度和纬度] [显示结果的距离] [显示的结果的数量]georadiusbymember显示与指定成员一定半径范围内的其他成员127.0.0.1:6379> georadiusbymember china:city taiyuan 1000 km 1) "manjing" 2) "taiyuan" 3) "xian" 127.0.0.1:6379> georadiusbymember china:city taiyuan 1000 km withcoord withdist count 2 1) 1) "taiyuan" 2) "0.0000" 3) 1) "112.54999905824661255" 2) "37.86000073876942196" 2) 1) "xian" 2) "514.2264" 3) 1) "108.96000176668167114" 2) "34.25999964418929977"参数与 georadius 一样geohash(较少使用)该命令返回11个字符的hash字符串127.0.0.1:6379> geohash china:city taiyuan shenyang 1) "ww8p3hhqmp0" 2) "wxrvb9qyxk0"将二维的经纬度转换为一维的字符串, 如果两个字符串越接近, 则距离越近底层geo 底层的实现原理 实际上就是 Zset , 我们可以通过 Zset命令来操作 geo。127.0.0.1:6379> type china:city zset查看全部元素 删除指定的元素127.0.0.1:6379> zrange china:city 0 -1 withscores 1) "xian" 2) "4040115445396757" 3) "hangzhou" 4) "4054133997236782" 5) "manjing" 6) "4066006694128997" 7) "taiyuan" 8) "4068216047500484" 9) "shenyang" 1) "4072519231994779" 2) "shengzhen" 3) "4154606886655324" 127.0.0.1:6379> zrem china:city manjing (integer) 1 127.0.0.1:6379> zrange china:city 0 -1 1) "xian" 2) "hangzhou" 3) "taiyuan" 4) "shenyang" 5) "shengzhen"Stream 类型为什么会设计StreamRedis5.0 中还增加了一个数据结构 Stream,从字面上看是 流类型 ,但其实从功能上看,应该是 Redis 对消息队列(MQ,Message Queue)的完善实现 。Reids 的消息队列用过 Redis 做消息队列的都了解,基于 Reids 的消息队列实现有很多种,例如:PUB/SUB,订阅/发布模式但是发布订阅模式是无法持久化的,如果出现网络断开、Redis 宕机等,消息就会被丢弃;基于List LPUSH+BRPOP 或者 基于Sorted-Set的实现支持了持久化,但是不支持多播,分组消费等设计一个消息队列需要考虑什么?为什么上面的结构无法满足广泛的MQ场景? 这里便引出一个核心的问题:如果我们期望设计一种数据结构来实现消息队列,最重要的就是要理解设计一个消息队列需要考虑什么?初步的我们很容易想到消息的生产消息的消费单播和多播(多对多)阻塞和非阻塞读取消息有序性消息的持久化其它还要考虑啥嗯?借助美团技术团队的一篇文章,消息队列设计精要中的图片我们不妨看看Redis考虑了哪些设计?消息ID的序列化生成消息遍历消息的阻塞和非阻塞读取消息的分组消费未完成消息的处理消息队列监控...这也是我们需要理解Stream的点,但是结合上面的图,我们也应该理解 Redis Stream也是一种超轻量MQ并没有完全实现消息队列所有设计要点,这决定着它适用的场景。Stream详解经过梳理总结,我认为从以下几个大的方面去理解Stream是比较合适的,总结如下:Stream的结构设计生产和消费基本的增删查改单一消费者的消费消费组的消费监控状态Stream的结构每个 Stream 都有唯一的名称,它就是 Redis 的 key,在我们首次使用 xadd 指令追加消息时自动创建。 上图解析:Consumer Group :消费组 ,使用 XGROUP CREATE 命令创建,一个消费组有多个消费者(Consumer), 这些消费者之间是竞争关系。last_delivered_id :游标 ,每个消费组会有个游标 last_delivered_id,任意一个消费者读取了消息都会使游标 last_delivered_id 往前移动。pending_ids :消费者(Consumer)的状态变量 ,作用是维护消费者的未确认的 id。 pending_ids 记录了当前已经被客户端读取的消息,但是还没有 ack (Acknowledge character:确认字符)。如果客户端没有ack,这个变量里面的消息ID会越来越多,一旦某个消息被ack,它就开始减少。这个pending_ids变量在Redis官方被称之为PEL,也就是Pending Entries List,这是一个很核心的数据结构,它用来确保客户端至少消费了消息一次,而不会在网络传输的中途丢失了没处理。此外我们 还需要理解两点 :消息ID : 消息ID的形式是timestampInMillis-sequence,例如1527846880572-5,它表示当前的消息在毫米时间戳1527846880572时产生,并且是该毫秒内产生的第5条消息。消息ID可以由服务器自动生成,也可以由客户端自己指定,但是形式必须是整数-整数,而且必须是后面加入的消息的ID要大于前面的消息ID。消息内容 : 消息内容就是键值对,形如hash结构的键值对,这没什么特别之处。增删改查消息队列相关命令:XADD #添加消息到末尾XTRIM # 对流进行修剪,限制长度XDEL #删除消息XLEN #获取流包含的元素数量,即消息长度XRANGE #获取消息列表,会自动过滤已经删除的消息XREVRANGE #反向获取消息列表,ID 从大到小XREAD #以阻塞或非阻塞方式获取消息列表# *号表示服务器自动生成ID,后面顺序跟着一堆key/value 127.0.0.1:6379> xadd codehole * name laoqian age 30 # 名字叫laoqian,年龄30岁 1527849609889-0 # 生成的消息ID 127.0.0.1:6379> xadd codehole * name xiaoyu age 29 1527849629172-0 127.0.0.1:6379> xadd codehole * name xiaoqian age 1 1527849637634-0 127.0.0.1:6379> xlen codehole (integer) 3 127.0.0.1:6379> xrange codehole - + # -表示最小值, +表示最大值 127.0.0.1:6379> xrange codehole - + 1) 1) 1527849609889-0 1) 1) "name" 1) "laoqian" 2) "age" 3) "30" 2) 1) 1527849629172-0 1) 1) "name" 1) "xiaoyu" 2) "age" 3) "29" 3) 1) 1527849637634-0 1) 1) "name" 1) "xiaoqian" 2) "age" 3) "1" 127.0.0.1:6379> xrange codehole 1527849629172-0 + # 指定最小消息ID的列表 1) 1) 1527849629172-0 2) 1) "name" 2) "xiaoyu" 3) "age" 4) "29" 2) 1) 1527849637634-0 2) 1) "name" 2) "xiaoqian" 3) "age" 4) "1" 127.0.0.1:6379> xrange codehole - 1527849629172-0 # 指定最大消息ID的列表 1) 1) 1527849609889-0 2) 1) "name" 2) "laoqian" 3) "age" 4) "30" 2) 1) 1527849629172-0 2) 1) "name" 2) "xiaoyu" 3) "age" 4) "29" 127.0.0.1:6379> xdel codehole 1527849609889-0 (integer) 1 127.0.0.1:6379> xlen codehole # 长度不受影响 (integer) 3 127.0.0.1:6379> xrange codehole - + # 被删除的消息没了 1) 1) 1527849629172-0 2) 1) "name" 2) "xiaoyu" 3) "age" 4) "29" 2) 1) 1527849637634-0 2) 1) "name" 2) "xiaoqian" 3) "age" 4) "1" 127.0.0.1:6379> del codehole # 删除整个Stream (integer) 1独立消费我们可以在不定义消费组的情况下进行Stream消息的独立消费,当Stream没有新消息时,甚至可以阻塞等待。 Redis设计了一个单独的消费指令 xread ,可以将Stream当成普通的消息队列(list)来使用。使用xread时,我们可以完全忽略消费组(Consumer Group)的存在,就好比Stream就是一个普通的列表(list)。# 从Stream头部读取两条消息 127.0.0.1:6379> xread count 2 streams codehole 0-0 1) 1) "codehole" 2) 1) 1) 1527851486781-0 2) 1) "name" 2) "laoqian" 3) "age" 4) "30" 2) 1) 1527851493405-0 2) 1) "name" 2) "yurui" 3) "age" 4) "29" # 从Stream尾部读取一条消息,毫无疑问,这里不会返回任何消息 127.0.0.1:6379> xread count 1 streams codehole $ (nil) # 从尾部阻塞等待新消息到来,下面的指令会堵住,直到新消息到来 127.0.0.1:6379> xread block 0 count 1 streams codehole $ # 我们从新打开一个窗口,在这个窗口往Stream里塞消息 127.0.0.1:6379> xadd codehole * name youming age 60 1527852774092-0 # 再切换到前面的窗口,我们可以看到阻塞解除了,返回了新的消息内容 # 而且还显示了一个等待时间,这里我们等待了93s 127.0.0.1:6379> xread block 0 count 1 streams codehole $ 1) 1) "codehole" 2) 1) 1) 1527852774092-0 2) 1) "name" 2) "youming" 3) "age" 4) "60" (93.11s)客户端如果想要使用xread进行顺序消费,一定要记住当前消费到哪里了,也就是返回的消息ID。 下次继续调用xread时,将上次返回的最后一个消息ID作为参数传递进去,就可以继续消费后续的消息。block 0表示永远阻塞,直到消息到来,block 1000表示阻塞1s,如果1s内没有任何消息到来,就返回nil127.0.0.1:6379> xread block 1000 count 1 streams codehole $ (nil) (1.07s)消费组消费消费组消费图 相关命令:XGROUP CREATE #创建消费者组XREADGROUP GROUP #读取消费者组中的消息XACK - #将消息标记为"已处理"XGROUP SETID #为消费者组设置新的最后递送消息IDXGROUP DELCONSUMER #删除消费者XGROUP DESTROY #删除消费者组XPENDING #显示待处理消息的相关信息XCLAIM #转移消息的归属权XINFO #查看流和消费者组的相关信息;XINFO GROUPS #打印消费者组的信息;XINFO STREAM #打印流信息创建消费组Stream通过xgroup create指令创建消费组(Consumer Group),需要传递起始消息ID参数用来初始化last_delivered_id变量。127.0.0.1:6379> xgroup create codehole cg1 0-0 # 表示从头开始消费 OK # $表示从尾部开始消费,只接受新消息,当前Stream消息会全部忽略 127.0.0.1:6379> xgroup create codehole cg2 $ OK 127.0.0.1:6379> xinfo stream codehole # 获取Stream信息 1) length 2) (integer) 3 # 共3个消息 3) radix-tree-keys 4) (integer) 1 5) radix-tree-nodes 6) (integer) 2 7) groups 8) (integer) 2 # 两个消费组 9) first-entry # 第一个消息 10) 1) 1527851486781-0 2) 1) "name" 2) "laoqian" 3) "age" 4) "30" 11) last-entry # 最后一个消息 12) 1) 1527851498956-0 2) 1) "name" 2) "xiaoqian" 3) "age" 4) "1" 127.0.0.1:6379> xinfo groups codehole # 获取Stream的消费组信息 1) 1) name 2) "cg1" 3) consumers 4) (integer) 0 # 该消费组还没有消费者 5) pending 6) (integer) 0 # 该消费组没有正在处理的消息 2) 1) name 2) "cg2" 3) consumers # 该消费组还没有消费者 4) (integer) 0 5) pending 6) (integer) 0 # 该消费组没有正在处理的消息消费组消费Stream提供了xreadgroup指令可以进行消费组的组内消费,需要提供消费组名称、消费者名称和起始消息ID。它同xread一样,也可以阻塞等待新消息。读到新消息后,对应的消息ID就会进入消费者的PEL(正在处理的消息)结构里,客户端处理完毕后使用xack指令通知服务器,本条消息已经处理完毕,该消息ID就会从PEL中移除。# >号表示从当前消费组的last_delivered_id后面开始读 # 每当消费者读取一条消息,last_delivered_id变量就会前进 127.0.0.1:6379> xreadgroup GROUP cg1 c1 count 1 streams codehole > 1) 1) "codehole" 2) 1) 1) 1527851486781-0 2) 1) "name" 2) "laoqian" 3) "age" 4) "30" 127.0.0.1:6379> xreadgroup GROUP cg1 c1 count 1 streams codehole > 1) 1) "codehole" 2) 1) 1) 1527851493405-0 2) 1) "name" 2) "yurui" 3) "age" 4) "29" 127.0.0.1:6379> xreadgroup GROUP cg1 c1 count 2 streams codehole > 1) 1) "codehole" 2) 1) 1) 1527851498956-0 2) 1) "name" 2) "xiaoqian" 3) "age" 4) "1" 2) 1) 1527852774092-0 2) 1) "name" 2) "youming" 3) "age" 4) "60" # 再继续读取,就没有新消息了 127.0.0.1:6379> xreadgroup GROUP cg1 c1 count 1 streams codehole > (nil) # 那就阻塞等待吧 127.0.0.1:6379> xreadgroup GROUP cg1 c1 block 0 count 1 streams codehole > # 开启另一个窗口,往里塞消息 127.0.0.1:6379> xadd codehole * name lanying age 61 1527854062442-0 # 回到前一个窗口,发现阻塞解除,收到新消息了 127.0.0.1:6379> xreadgroup GROUP cg1 c1 block 0 count 1 streams codehole > 1) 1) "codehole" 2) 1) 1) 1527854062442-0 2) 1) "name" 2) "lanying" 3) "age" 4) "61" (36.54s) 127.0.0.1:6379> xinfo groups codehole # 观察消费组信息 1) 1) name 2) "cg1" 3) consumers 4) (integer) 1 # 一个消费者 5) pending 6) (integer) 5 # 共5条正在处理的信息还有没有ack 2) 1) name 2) "cg2" 3) consumers 4) (integer) 0 # 消费组cg2没有任何变化,因为前面我们一直在操纵cg1 5) pending 6) (integer) 0 # 如果同一个消费组有多个消费者,我们可以通过xinfo consumers指令观察每个消费者的状态 127.0.0.1:6379> xinfo consumers codehole cg1 # 目前还有1个消费者 1) 1) name 2) "c1" 3) pending 4) (integer) 5 # 共5条待处理消息 5) idle 6) (integer) 418715 # 空闲了多长时间ms没有读取消息了 # 接下来我们ack一条消息 127.0.0.1:6379> xack codehole cg1 1527851486781-0 (integer) 1 127.0.0.1:6379> xinfo consumers codehole cg1 1) 1) name 2) "c1" 3) pending 4) (integer) 4 # 变成了5条 5) idle 6) (integer) 668504 # 下面ack所有消息 127.0.0.1:6379> xack codehole cg1 1527851493405-0 1527851498956-0 1527852774092-0 1527854062442-0 (integer) 4 127.0.0.1:6379> xinfo consumers codehole cg1 1) 1) name 2) "c1" 3) pending 4) (integer) 0 # pel空了 5) idle 6) (integer) 745505信息监控Stream 提供了XINFO来实现对服务器信息的监控,可以查询:查看队列信息127.0.0.1:6379> Xinfo stream mq 1) "length" 2) (integer) 7 3) "radix-tree-keys" 4) (integer) 1 5) "radix-tree-nodes" 6) (integer) 2 7) "groups" 8) (integer) 1 9) "last-generated-id" 10) "1553585533795-9" 11) "first-entry" 12) 1) "1553585533795-3" 2) 1) "msg" 2) "4" 13) "last-entry" 14) 1) "1553585533795-9" 2) 1) "msg" 2) "10"消费组信息127.0.0.1:6379> Xinfo groups mq 1) 1) "name" 2) "mqGroup" 3) "consumers" 4) (integer) 3 5) "pending" 6) (integer) 3 7) "last-delivered-id" 8) "1553585533795-4" 消费者组成员信息 127.0.0.1:6379> XINFO CONSUMERS mq mqGroup 1) 1) "name" 2) "consumerA" 3) "pending" 4) (integer) 1 5) "idle" 6) (integer) 18949894 2) 1) "name" 2) "consumerB" 3) "pending" 4) (integer) 1 5) "idle" 6) (integer) 3092719 3) 1) "name" 2) "consumerC" 3) "pending" 4) (integer) 1 5) "idle" 6) (integer) 23683256至此,消息队列的操作说明大体结束!Stream用在什么样场景可用作 即时通信,大数据分析,异地数据备份 等客户端可以平滑扩展,提高处理能力 消息ID的设计是否考虑了时间回拨的问题?在分布式算法 - ID算法设计中, 一个常见的问题就是时间回拨问题,那么Redis的消息ID设计中是否考虑到这个问题呢?XADD生成的1553439850328-0,就是Redis生成的消息ID,由两部分组成:时间戳-序号。时间戳是毫秒级单位,是生成消息的Redis服务器时间,它是个64位整型(int64)。序号是在这个毫秒时间点内的消息序号,它也是个64位整型。可以通过multi批处理,来验证序号的递增:127.0.0.1:6379> MULTI OK 127.0.0.1:6379> XADD memberMessage * msg one QUEUED 127.0.0.1:6379> XADD memberMessage * msg two QUEUED 127.0.0.1:6379> XADD memberMessage * msg three QUEUED 127.0.0.1:6379> XADD memberMessage * msg four QUEUED 127.0.0.1:6379> XADD memberMessage * msg five QUEUED 127.0.0.1:6379> EXEC 1) "1553441006884-0" 2) "1553441006884-1" 3) "1553441006884-2" 4) "1553441006884-3" 5) "1553441006884-4"由于一个redis命令的执行很快,所以可以看到在同一时间戳内,是通过序号递增来表示消息的。为了保证消息是有序的,因此 Redis 生成的 ID 是单调递增有序的。由于 ID 中包含时间戳部分,为了避免服务器时间错误而带来的问题(例如服务器时间延后了),Redis 的每个 Stream 类型数据都维护一个 latest_generated_id属性,用于记录最后一个消息的ID。 若发现当前时间戳退后(小于latest_generated_id所记录的),则采用时间戳不变而序号递增的方案来作为新消息ID(这也是序号为什么使用int64的原因,保证有足够多的的序号),从而保证ID的单调递增性质。 强烈建议使用Redis的方案生成消息ID,因为这种时间戳+序号的单调递增的ID方案,几乎可以满足你全部的需求。但同时,记住ID是支持自定义的,别忘了!消费者崩溃带来的会不会消息丢失问题?为了解决组内消息读取但处理期间消费者崩溃带来的消息丢失问题 ,STREAM 设计了 Pending 列表,用于记录读取但并未处理完毕的消息。命令XPENDIING 用来获消费组或消费内消费者的未处理完毕的消息。演示如下:127.0.0.1:6379> XPENDING mq mqGroup # mpGroup的Pending情况 1) (integer) 5 # 5个已读取但未处理的消息 2) "1553585533795-0" # 起始ID 3) "1553585533795-4" # 结束ID 4) 1) 1) "consumerA" # 消费者A有3个 2) "3" 2) 1) "consumerB" # 消费者B有1个 2) "1" 3) 1) "consumerC" # 消费者C有1个 2) "1" 127.0.0.1:6379> XPENDING mq mqGroup - + 10 # 使用 start end count 选项可以获取详细信息 1) 1) "1553585533795-0" # 消息ID 2) "consumerA" # 消费者 3) (integer) 1654355 # 从读取到现在经历了1654355ms,IDLE 4) (integer) 5 # 消息被读取了5次,delivery counter 2) 1) "1553585533795-1" 2) "consumerA" 3) (integer) 1654355 4) (integer) 4 # 共5个,余下3个省略 ... 127.0.0.1:6379> XPENDING mq mqGroup - + 10 consumerA # 在加上消费者参数,获取具体某个消费者的Pending列表 1) 1) "1553585533795-0" 2) "consumerA" 3) (integer) 1641083 4) (integer) 5 # 共3个,余下2个省略 ...** 每个Pending的消息有4个属性:消息ID所属消费者IDLE,已读取时长delivery counter,消息被读取次数上面的结果我们可以看到,我们之前读取的消息,都被记录在Pending列表中,说明全部读到的消息都没有处理,仅仅是读取了。 那如何表示消费者处理完毕了消息呢?使用命令 XACK 完成告知消息处理完成 ,演示如下:127.0.0.1:6379> XACK mq mqGroup 1553585533795-0 # 通知消息处理结束,用消息ID标识 (integer) 1 127.0.0.1:6379> XPENDING mq mqGroup # 再次查看Pending列表 1) (integer) 4 # 已读取但未处理的消息已经变为4个 2) "1553585533795-1" 3) "1553585533795-4" 4) 1) 1) "consumerA" # 消费者A,还有2个消息处理 2) "2" 2) 1) "consumerB" 2) "1" 3) 1) "consumerC" 2) "1" 127.0.0.1:6379>有了这样一个Pending机制,就意味着在某个消费者读取消息但未处理后,消息是不会丢失的。等待消费者再次上线后,可以读取该Pending列表,就可以继续处理该消息了,保证消息的有序和不丢失。消费者彻底宕机后如何转移给其它消费者处理?还有一个问题,就是若某个消费者宕机之后,没有办法再上线了,那么就需要将该消费者Pending的消息,转义给其他的消费者处理,就是消息转移。 消息转移的操作时将某个消息转移到自己的Pending列表中。使用语法 XCLAIM 来实现,需要设置组、转移的目标消费者和消息ID,同时需要提供IDLE(已被读取时长),只有超过这个时长,才能被转移。演示如下:# 当前属于消费者A的消息1553585533795-1,已经15907,787ms未处理了 127.0.0.1:6379> XPENDING mq mqGroup - + 10 1) 1) "1553585533795-1" 2) "consumerA" 3) (integer) 15907787 4) (integer) 4 # 转移超过3600s的消息1553585533795-1到消费者B的Pending列表 127.0.0.1:6379> XCLAIM mq mqGroup consumerB 3600000 1553585533795-1 1) 1) "1553585533795-1" 2) 1) "msg" 2) "2" # 消息1553585533795-1已经转移到消费者B的Pending中。 127.0.0.1:6379> XPENDING mq mqGroup - + 10 1) 1) "1553585533795-1" 2) "consumerB" 3) (integer) 84404 # 注意IDLE,被重置了 4) (integer) 5 # 注意,读取次数也累加了1次以上代码,完成了一次消息转移。转移除了要指定ID外,还需要指定IDLE,保证是长时间未处理的才被转移。被转移的消息的IDLE会被重置,用以保证不会被重复转移,以为可能会出现将过期的消息同时转移给多个消费者的并发操作,设置了IDLE,则可以避免后面的转移不会成功,因为IDLE不满足条件。例如下面的连续两条转移,第二条不会成功。127.0.0.1:6379> XCLAIM mq mqGroup consumerB 3600000 1553585533795-1 127.0.0.1:6379> XCLAIM mq mqGroup consumerC 3600000 1553585533795-1这就是消息转移。至此我们使用了一个 Pending 消息的 ID,所属消费者和IDLE 的属性,还有一个属性就是消息被读取次数,delivery counter,该属性的作用由于统计消息被读取的次数,包括被转移也算。这个属性主要用在判定是否为错误数据上。坏消息问题,Dead Letter,死信问题正如上面所说,如果某个消息,不能被消费者处理,也就是不能被XACK,这是要长时间处于Pending列表中,即使被反复的转移给各个消费者也是如此。此时该消息的delivery counter就会累加(上一节的例子可以看到),当累加到某个我们预设的临界值时,我们就认为是坏消息(也叫死信,DeadLetter,无法投递的消息),由于有了判定条件,我们将坏消息处理掉即可,删除即可。删除一个消息,使用XDEL语法,演示如下:# 删除队列中的消息 127.0.0.1:6379> XDEL mq 1553585533795-1 (integer) 1 # 查看队列中再无此消息 127.0.0.1:6379> XRANGE mq - + 1) 1) "1553585533795-0" 2) 1) "msg" 2) "1" 2) 1) "1553585533795-2" 2) 1) "msg" 2) "3"注意本例中,并没有删除 Pending 中的消息因此你查看Pending,消息还会在。可以执行 XACK 标识其处理完毕!参考文章: https://pdai.tech/md/db/nosql-redis/db-redis-data-type-stream.htmlhttps://pdai.tech/md/db/nosql-redis/db-redis-data-type-special.html(三):Redis 常用管理命令redis set keyRedis SET 命令用于给键(key)设置值的。如果 key 已经存储其他值,SET 就覆写旧值。语法结构如下:set keyname 值返回值:设置成功时,返回OK。实例:set freekey free;结果:redis getRedis get命令用于获取键(key)中的值的。如果key不存在,返回 nil。语法结构如下:get keyname返回值:返回keyname对应的值,如果key不存在,则返回nil。假如key中存的值不是字符串类型那么返回错误。实例:get freekey结果:redis -cliRedis 命令是在redis 服务上执行的。那么要连接redis服务器需要一个redis客户端。Redis 客户端在我们之前下载的的 redis的安装包中。我们要启动redis客户端,可以在DOS进入redis安装目录,然后通过执行redis -cli来启动客户端,该命令会连接本地的 redis 服务。如下图:连接远程redis服务器另起一个cmd,执行客户端连接到redis服务器,即服务端,进行测试,命令如下:redis-cli.exe -h 127.0.0.1 -p 6379 -a 123456其中127.0.01是redis的服务器地址,6379是端口,-a 123456是设置的密码。结果如下:Redis setnxRedis setnx命令也是用于设置key的值,但是它 和redis set命令有点不一样 。 只在key不存在的情况下, 给key设置,假如key已经存在,那么 redis setnx将啥都不做。语法结构如下:setnx keyname value返回值:命令设置成功返回1,失败返回0。实例:setnx nxkey hello结果:redis setexredis setex命令也是用于设置key的值,但是它 和redis set命令有点不一样,它可以额外设置key值的生存周期。语法结构如下SETEX key seconds value返回值:命令成功时返回 OK 。当 seconds 参数不合法时, 命令将返回一个错误。如果key已经存在那么覆盖旧值。实例SETEX setexkey 100 hello --指的是设定setexkey键的生存周期为100秒。ttl setexkey --查看setexkey键的剩余时间。结果:redis psetexredis psetex命令:用于给redis设置key的值, 并且附带上值的生存时间,不同于setex命令,它设置值的生存时间为毫秒。语法结构如下:PSETEX key seconds value返回值:命令成功时返回 OK 。当 seconds 参数不合法时, 命令将返回一个错误。如果key已经存在那么覆盖旧值。实例:PSETEX psetexkey 5000 free--指的是设定setexkey键的生存周期为1000毫秒。pttl psetexkey --查看psetexkey键的剩余时间。结果:redis getsetredis getset命令:用于给redis设置key的新值,返回之前旧的key值。如果key值之前不存在,那会报错。语法结构如下:getset key value实例: --给key设置值 set getsetkey "free" --给key设置新值 getset getsetkey "free1" --获取key值 get getsetkey结果:redis appendredis append 命令是 用于对redis字符串进行追加,当键值已经存在的情况下,在键值的末尾追加上提供的value值。语法结构:append key value返回值:如果key存在并且是一个字符串,append命令会把value的值追加到原来的键值末尾,并返回现有的字符串长度。如果key不存在,那么他就直接对key值进行赋值,和set key命令一样。实例:--给key设值set mykey "free"--在key值后面追加字符append mykey " redis "--获取key值get mykey结果:redis msetredis mset命令用于给redis的键(key)赋值命令。 不同于redis set,它可以一次给多个键同时进行赋值。语法结构:mset key1 value1 key2 value2 ...返回值:总是返回OK。和redis set命令一样,当key值存在时,对其值进行覆盖。实例:--给key设值mset mykey1 "free" mykey2 "free redis "--获取键值get mykey1 get mykey2结果:redis mgetredis mget命令 用于批量获取给定的多个键(key)的值,它是redis mset命令的逆过程。语法结构:redis mget key1 key2...返回值:返回给定的一个或者多个键(key)的值。如果给定的键不存在,那么这个键返回的值将是nil。实例:--给key设值mset mykey1 "free" mykey2 "free redis "--获取键值mget mykey1 mykey2结果:redis incrredis incr命令 用于对数值类型的键(key)值进行加1操作,然后返回加1之后的数值。语法结构:redis incr key返回值:如果key值存在,并为数值类型,那么对其加1进行返回。如果key值不存在,那么当做0处理,返回1。如果key值不是数值类型,那么会返回错误。实例:--给key设值set key 2--给key加1incr key--获取key的值 get key --对不是数值的执行incr结果 set key "free" incr key结果:redis decrredis decr命令 用于对数值类型的键(key)值进行递减操作(即减1操作),然后返回递减之后的结果值。语法结构:decr key返回值:如果key值存在,并为数值类型,那么对其递减1,然后返回结果值。如果key值不存在,那么当做0处理,返回-1。如果key值不是数值类型,那么会返回错误。实例:--给key设值set key 2--给key进行递减1decr key--获取key的值get key--对不是数值的执行decr结果set key "free"decr key结果:redis lindex key indexredis lindex key index命令主要 用于获取链表类型中指定下标的数据。语法结构:lindex key index返回链表类型key中下标为index的数据。index表示链表的下标,0表示链表头第一个元素,-1表示链表尾最后一个元素。返回值:指定链表下标index的元素。如果index指定的下标大于链表的长度,就会报下标越界。实例:--给链表插入数据rpush mylist10 "hello" "free" "redis" "hello" --获取链表数据lindex mylist10 0lindex mylist10 1lindex mylist10 -1结果:redis ltrimredis ltrim命令主要 用于截取redis链表类型的指定下标区间内的元素,不在指定区间内的元素都会被删除。语法结构:ltrim key start endkey:#指定要截取的链表键。start/end:#指定要截取的区间,start是开始,end是结尾。如ltrim key 0,1表示保留前两个元素,其它元素都删除掉。start(end)都是表示链表的下标,链表下标是从0开始表示链表头,第一个元素。-1表示链表尾,最后一个元素。返回值:命令执行成功返回OK,当key不是链表类型时,返回错误。下标区间不能超过链表的长度,会报下标越界错误。实例:--给链表插入数据rpush mylist "hello" "free" "redis" "ok"--截取中间两个元素ltrim mylist 1 2 --查看截取后的链表元素lrange mylist 0 -1结果:redis hgetredis hget命令主要用 获取redis哈希表中域(field)的值。语法结构:hget key field获取哈希表key中域(field)的值。返回值:正常返回给定域的值,如果给定的域不存在,那么返回nil错误。实例:--创建一个哈希表hset myhash field1 "free"--获取指定域的值hget myhash field1结果:redis hdelredis hdel命令 用于删除哈希表中指定的域(field),只可以批量移除多个域,不存在的域会被忽略。语法结构:hdel hash field field1 ....hash #指定哈希表的键,field是哈希表的域。返回值:返回被成功移除域的数量,不存在的域不计算在内。实例:--创建一个哈希表hset myhash field1 "free"hset myhash field2 "redis"--判断指定的域是否存在hdel myhash field1 field2 field3结果:节选自:https://www.wenjiangs.com/doc/q3km1de9s1t7(四):Redis 发布与订阅(pub/sub)什么是发布订阅?Redis 发布订阅(pub/sub)是一种消息通信模式:发送者(pub)发送消息,订阅者(sub)接收消息。 Redis 的 subscribe 命令可以 让客户端订阅任意数量的频道, 每当有新信息发送到被订阅的频道时, 信息就会被发送给所有订阅指定频道的客户端。 ☛ 下图展示了频道 channel1 , 以及订阅这个频道的三个客户端 —— client2 、 client5 和 client1 之间的关系: ☛ 当有新消息通过 publish 命令发送给频道 channel1 时, 这个消息就会被发送给订阅它的三个客户端: 为什么要用发布订阅?熟悉消息中间件 的同学都知道,针对 消息订阅发布 功能,市面上很多大厂使用的是 kafka、RabbitMQ、ActiveMQ, RocketMQ 等这几种,redis的订阅发布功能跟这三者相比, 相对轻量,针对数据准确和安全性要求没有那么高可以直接使用,适用于小公司。 redis 的List数据类型结构提供了 blpop 、brpop 命令结合 rpush、lpush 命令可以实现消息队列机制,基于双端链表实现的发布与订阅功能 这种方式存在 两个局限性 :不能支持一对多的消息分发。如果生产者生成的速度远远大于消费者消费的速度,易堆积大量未消费的消息◇ 双端队列图解 如下:✦ 解析:双端队列模式只能有一个或多个消费者轮着去消费,却不能将消息同时发给其他消费者 ◇ 发布/订阅模式图解如下 :✦ 解析:redis订阅发布模式,生产者生产完消息通过频道分发消息,给订阅了该频道的所有消费发布/订阅如何使用?Redis 有两种发布/订阅模式 :基于频道(Channel)的发布/订阅基于模式(pattern)的发布/订阅操作命令 如下基于频道(Channel)的发布/订阅"发布/订阅" 包含2种角色:发布者和订阅者。发布者可以向指定的频道(channel)发送消息;订阅者可以订阅一个或者多个频道(channel),所有订阅此频道的订阅者都会收到此消息。订阅者订阅频道 subscribe channel [channel ...]--------------------------客户端1(订阅者) :订阅频道 --------------------- # 订阅 “meihuashisan” 和 “csdn” 频道(如果不存在则会创建频道) 127.0.0.1:6379> subscribe meihuashisan csdn Reading messages... (press Ctrl-C to quit) 1) "subscribe" -- 返回值类型:表示订阅成功! 2) "meihuashisan" -- 订阅频道的名称 3) (integer) 1 -- 当前客户端已订阅频道的数量 1) "subscribe" 2) "csdn" 3) (integer) 2 #注意:订阅后,该客户端会一直监听消息,如果发送者有消息发给频道,这里会立刻接收到消息发布者发布消息 publish channel message-----------------------客户端2(发布者):发布消息给频道 ------------------- # 给“meihuashisan”这个频道 发送一条消息:“I am meihuashisan” 127.0.0.1:6379> publish meihuashisan "I am meihuashisan" (integer) 1 # 接收到信息的订阅者数量,无订阅者返回0 客户端2(发布者)发布消息给频道后,此时我们再来观察 客户端1(订阅者)的客户端窗口变化: --------------------------客户端1(订阅者) :订阅频道 ----------------- 127.0.0.1:6379> subscribe meihuashisan csdn Reading messages... (press Ctrl-C to quit) 1) "subscribe" -- 返回值类型:表示订阅成功! 2) "meihuashisan" -- 订阅频道的名称 3) (integer) 1 -- 当前客户端已订阅频道的数量 1) "subscribe" 2) "csdn" 3) (integer) 2 --------------------变化如下:(实时接收到了该频道的发布者的消息)------------ 1) "message" -- 返回值类型:消息 2) "meihuashisan" -- 来源(从哪个频道发过来的) 3) "I am meihuashisan" -- 消息内容命令操作图解 如下:注意: 如果是先发布消息,再订阅频道,不会收到订阅之前就发布到该频道的消息!注意:进入订阅状态的客户端,不能使用除了 subscribe、unsubscribe、psubscribe 和 punsubscribe 这四个属于"发布/订阅"之外的命令,否则会报错!这里的客户端指的是 jedis、lettuce的客户端,redis-cli是无法退出订阅状态的!实现原理底层通过字典实现。pubsub_channels 是一个字典类型,保存订阅频道的信息:字典的key为订阅的频道, 字典的value是一个链表, 链表中保存了所有订阅该频道的客户端struct redisServer { /* General */ pid_t pid; //省略百十行 // 将频道映射到已订阅客户端的列表(就是保存客户端和订阅的频道信息) dict *pubsub_channels; /* Map channels to list of subscribed clients */ }实现图如下:频道订阅 :订阅频道时先检查字段内部是否存在;不存在则为当前频道创建一个字典且创建一个链表存储客户端id;否则直接将客户端id插入到链表中。取消频道订阅 :取消时将客户端id从对应的链表中删除;如果删除之后链表已经是空链表了,则将会把这个频道从字典中删除。发布 :首先根据 channel 定位到字典的键, 然后将信息发送给字典值链表中的所有客户端基于模式(pattern)的发布/订阅如果有某个/某些模式和该频道匹配,所有订阅这个/这些频道的客户端也同样会收到信息。 图解 下图展示了一个带有频道和模式的例子, 其中 com.ahead.* 频道匹配了 com.ahead.juc 频道和 com.ahead.thread 频道, 并且有不同的客户端分别订阅它们三个,如下图:当有信息发送到com.ahead.thread 频道时, 信息除了发送给 client 4 和 client 5 之外, 还会发送给订阅 com.ahead.* 频道模式的 client x 和 client y✦ 解析 :反之也是,如果当有消息发送给 com.ahead.juc 频道,消息发送给订阅了 juc 频道的客户端之外,还会发送给订阅了 com.ahead.* 频道的客户端: client x 、client y通配符中?表示1个占位符,表示任意个占位符(包括0),?表示1个以上占位符。订阅者订阅频道 psubscribe pattern [pattern ...]--------------------------客户端1(订阅者) :订阅频道 -------------------- # 1. ------------订阅 “a?” "com.*" 2种模式频道-------------- 127.0.0.1:6379> psubscribe a? com.* # 进入订阅状态后处于阻塞,可以按Ctrl+C键退出订阅状态 Reading messages... (press Ctrl-C to quit) ---------------订阅成功------------------- 1) "psubscribe" -- 返回值的类型:显示订阅成功 2) "a?" -- 订阅的模式 3) (integer) 1 -- 目前已订阅的模式的数量 1) "psubscribe" 2) "com.*" 3) (integer) 2 ---------------接收消息 (已订阅 “a?” "com.*" 两种模式!)----------------- # ---- 发布者第1条命令:publish ahead "hello" 结果:没有接收到消息,匹配失败,不满足 “a?” ,“?”表示一个占位符, a后面的head有4个占位符 # ---- 发布者第2条命令: publish aa "hello" (满足 “a?”) 1) "pmessage" -- 返回值的类型:信息 2) "a?" -- 信息匹配的模式:a? 3) "aa" -- 信息本身的目标频道:aa 4) "hello" -- 信息的内容:"hello" # ---- 发布者第3条命令:publish com.juc "hello2"(满足 “com.*”, *表示任意个占位符) 1) "pmessage" -- 返回值的类型:信息 2) "com.*" -- 匹配模式:com.* 3) "com.juc" -- 实际频道:com.juc 4) "hello2" -- 信息:"hello2" ---- 发布者第4条命令:publish com. "hello3"(满足 “com.*”, *表示任意个占位符) 1) "pmessage" -- 返回值的类型:信息 2) "com.*" -- 匹配模式:com.* 3) "com." -- 实际频道:com. 4) "hello3" -- 信息:"hello3"发布者发布消息 publish channel message------------------------客户端2(发布者):发布消息给频道 ------------------ 注意:订阅者已订阅 “a?” "com.*" 两种模式! # 1. ahead 不符合“a?”模式,?表示1个占位符 127.0.0.1:6379> publish ahead "hello" (integer) 0 -- 匹配失败,0:无订阅者 # 2. aa 符合“a?”模式,?表示1个占位符 127.0.0.1:6379> publish aa "hello" (integer) 1 # 3. 符合“com.*”模式,*表示任意个占位符 127.0.0.1:6379> publish com.juc "hello2" (integer) 1 # 4. 符合“com.*”模式,*表示任意个占位符 127.0.0.1:6379> publish com. "hello3" (integer) 1命令操作图解 如下:实现原理底层是pubsubPattern节点的链表。struct redisServer { //... list *pubsub_patterns; // ... } // 1303行订阅模式列表结构: typedef struct pubsubPattern { client *client; -- 订阅模式客户端 robj *pattern; -- 被订阅的模式 } pubsubPattern;实现图 如下:模式订阅 :新增一个pubsub_pattern数据结构添加到链表的最后尾部,同时保存客户端ID。取消模式订阅 :从当前的链表pubsub_pattern结构中删除需要取消的pubsubPattern结构。使用小结订阅者(listener)负责订阅频道(channel);发送者(publisher)负责向频道发送二进制的字符串消息,然后频道收到消息时,推送给订阅者。使用场景电商中,用户下单成功之后向指定频道发送消息,下游业务订阅支付结果这个频道处理自己相关业务逻辑粉丝关注功能文章推送使用注意客户端需要及时消费和处理消息。客户端订阅了channel之后,如果接收消息不及时,可能导致DCS实例消息堆积,当达到消息堆积阈值(默认值为32MB),或者达到某种程度(默认8MB)一段时间(默认为1分钟)后,服务器端会自动断开该客户端连接,避免导致内部内存耗尽。客户端需要支持重连。当连接断开之后,客户端需要使用subscribe或者psubscribe重新进行订阅,否则无法继续接收消息。不建议用于消息可靠性要求高的场景中。Redis的pubsub不是一种可靠的消息系统。当出现客户端连接退出,或者极端情况下服务端发生主备切换时,未消费的消息会被丢弃。深入理解我们通过几个问题,来深入理解Redis的订阅发布机制基于频道(Channel)的发布/订阅如何实现的?底层是通过字典(图中的pubsub_channels)实现的 ,这个字典就用于保存订阅频道的信息:字典的键为正在被订阅的频道, 而字典的值则是一个链表, 链表中保存了所有订阅这个频道的客户端。数据结构比如说,在下图展示的这个 pubsub_channels 示例中, client2 、 client5 和 client1 就订阅了 channel1 , 而其他频道也分别被别的客户端所订阅:订阅当客户端调用 SUBSCRIBE 命令时, 程序就将客户端和要订阅的频道在 pubsub_channels 字典中关联起来。举个例子,如果客户端 client10086 执行命令 SUBSCRIBE channel1 channel2 channel3 ,那么前面展示的 pubsub_channels 将变成下面这个样子:发布当调用 PUBLISH channel message 命令, 程序首先根据 channel 定位到字典的键, 然后将信息发送给字典值链表中的所有客户端。比如说,对于以下这个 pubsub_channels 实例, 如果某个客户端执行命令 PUBLISH channel1 "hello moto" ,那么 client2 、 client5 和 client1 三个客户端都将接收到 "hello moto" 信息:退订使用 UNSUBSCRIBE 命令可以退订指定的频道, 这个命令执行的是订阅的反操作:它从 pubsub_channels 字典的给定频道(键)中, 删除关于当前客户端的信息, 这样被退订频道的信息就不会再发送给这个客户端。基于模式(Pattern)的发布/订阅如何实现的?底层是pubsubPattern节点的链表。数据结构redisServer.pubsub_patterns 属性是一个链表,链表中保存着所有和模式相关的信息:struct redisServer { // ... list *pubsub_patterns; // ... };链表中的每个节点都包含一个 redis.h/pubsubPattern 结构:typedef struct pubsubPattern { redisClient *client; robj *pattern; } pubsubPattern;client 属性保存着订阅模式的客户端,而 pattern 属性则保存着被订阅的模式。每当调用 PSUBSCRIBE 命令订阅一个模式时, 程序就创建一个包含客户端信息和被订阅模式的 pubsubPattern 结构, 并将该结构添加到 redisServer.pubsub_patterns 链表中。作为例子,下图展示了一个包含两个模式的 pubsub_patterns 链表, 其中 client123 和 client256 都正在订阅 tweet.shop.* 模式:订阅如果这时客户端 client10086 执行 PSUBSCRIBE broadcast.list.* , 那么 pubsub_patterns 链表将被更新成这样:通过遍历整个 pubsub_patterns 链表,程序可以检查所有正在被订阅的模式,以及订阅这些模式的客户端。发布发送信息到模式的工作也是由 PUBLISH 命令进行的, 显然就是匹配模式获得Channels,然后再把消息发给客户端。退订使用 PUNSUBSCRIBE 命令可以退订指定的模式, 这个命令执行的是订阅模式的反操作:程序会删除 redisServer.pubsub_patterns 链表中, 所有和被退订模式相关联的 pubsubPattern 结构, 这样客户端就不会再收到和模式相匹配的频道发来的信息。SpringBoot结合Redis发布/订阅实例?最佳实践是通过RedisTemplate,关键代码如下:// 发布 redisTemplate.convertAndSend("my_topic_name", "message_content"); // 配置订阅 RedisMessageListenerContainer container = new RedisMessageListenerContainer(); container.setConnectionFactory(connectionFactory); container.addMessageListener(xxxMessageListenerAdapter, "my_topic_name");总结1、redis的订阅频道的信息是redis服务器进程自己维持在pubsub_channels链表字典当中。字典的KEY为被订阅的频道,值为订阅的客户端。2、当发送者发送消息时,redis服务器遍历频道对应的所有客户端,然后将消息发送到所订阅的客户端上。3、当有信息发送时,除了订阅该频道的客户端会收到消息,以及和订阅了匹配频道的客户端,其它客户端是收不到该信息的。4、退订频道、退订模式和订阅频道、订阅模式是两组反操作。应用场景俗话说的好,知识学得好不好,还得看用到哪。反正笔者看到redis的发布与订阅的模式的特点后,第一时间想到的是可以用来做一个实时聊天系统,还可以用来做分布式架构中写的过程,利用redis的实时发布功能,把要写入的值及时快速的分发到各个写入程序当中,保证分布式架构中数据的完整一致性。再比如博客系统和自媒体平台中,粉丝关注功能,就比当我发布文章时,就可以及时推送文章到粉丝的客户端上。总而言之,应用的场景比较多,需要大家多思考,多交流。参考来源:https://blog.csdn.net/w15558056319/article/details/121490953 pdai.tech/md/db/nosql-redis/db-redis-x-pub-sub.html https://www.wenjiangs.com/doc/mt0ueji7b8sc(五):Redis 事件机制详解前言Redis 采用事件驱动机制来处理大量的网络 IO。 它并没有使用 libevent 或者 libev 这样的成熟开源方案,而是自己实现一个非常简洁的事件驱动库 ae_event 。什么是事件驱动?所谓事件驱动,简单地说就是你点什么按钮(即产生什么事件),电脑执行什么操作(即调用什么函数) .当然事件不仅限于用户的操作. 事件驱动的核心自然是事件。从事件角度说,事件驱动程序的 基本结构是由一个事件收集器、一个事件发送器和一个事件处理器组成。事件收集器专门负责收集所有事件,包括来自用户的(如鼠标、键盘事件等)、来自硬件的(如时钟事件等)和来自软件的(如操作系统、应用程序本身等)。事件发送器负责将收集器收集到的事件分发到目标对象中。事件处理器做具体的事件响应工作。事件驱动库的代码主要是在src/ae.c中实现的,其示用意如下所示。Redis 服务器就是一个事件驱动程序,服务器需要处理以下 两类事件 :文件事件(file event) :Redis服务器通过套接字与客户端(或者其他Redis服务器)进行连接,而文件事件就是服务器对套接字操作的抽象,服务器与客户端(或者是其他服务器)通信会产生相应的文件事件,而服务器则通过监听并处理这些事件来完成一系列网络通信操作。时间事件(time event) :Redis服务器中的一些操作需要在给定的时间点执行,而时间事件就是服务器对这类定时操作的抽象。文件事件Redis基于Reactor模式开发了自己的网络事件处理器:这个处理器被称为文件事件处理器(file event handler):文件事件处理器使用I/O多路复用(multiplexing)程序来同时监听多个套接字,并且根据套接字目前执行的任务来为套接字关联不同的所时间处理器当被监听的套接字准备好执行连接应答(accept),读取(read),写入(write),关闭(close)等操作时,与操作相对应的文件事件就会产生,这时文件处理器就会调用套接字之前关联好的事件处理器来处理这些事件虽然文件事件处理器以单线程的方式运行,但通过使用I/O多路复用程序来监听多个套接字,文件时间处理器既能实现高性能的网络通信模型,又可以很好的与Redis服务器中的其他同样以单线程方式运行的模块进行对接,这就保持了Redis内部单线程设计的简单性。文件事件处理器的构成文件事件处理器主要有 四部分组成 ,他们分别是 套接字,I.O多路复用程序,文件事件分派器,以及事件处理程序 。文件事件是对套接字操作的抽象,每当一个套接字准备好执行连接应答(accept),写入,读取,关闭等操作时,就会产生一个文件事件。因为一个服务器通常会连接多个套接字,所以多个文件事件可能会并发出现I/O多路复用程序负责监听多个套接字,并向文件事件分派器传送那些产生了事件的套接字尽管多个文件事件可能会并发地产生地出现,但是I/O多路复用程序总是会将所有产生事件的套接字都会放到一个队列里面,然后通过这个队列,以有序(sequentially),同步(synchronously),每次一个套接字的方式向文件事件分派器传送套接字。当上一个套接字产生的事件被处理完毕后,I/O多路服用程序才会继续向文件事件分派器传送下一个套接字文件事件分派器接受一个I/O多路复用程序传送来的套接字,并且根据套接字产生的事件的类型,调用相应的时间处理程序I/O多路服用程序的实现Redis的I/O多路复用程序的所有功能都是通过包装常见的select,epoll,evport和kqueue这些I/O多路复用函数库实现的,每个I/O多路复用函数库在Redis源码中都对应一个单独的文件。下图就是基于多路复用的 Redis IO 模型。因为Redis为每个I/O多路复用函数库实现了相同的API,所以I/O多路复用程序的底层实现是可以互换的。事件的类型I/O多路复用程序可以监听多个套接字的READABLE事件和WRITEABLE事件,这两个事件和套接字操作之间的对应关系如下:当套接字变得可读时(客户端对套接字执行write操作,或者执行close操作),或者有新的可应答(acceptable)套接字出现时(客户端对服务器的监听套接字主席那个connect操作),套接字产生READABLE事件当套接字变得可写(客户端对套接字执行read操作),套接字产生WRITEABLE事件I/O多路复用程序允许服务器同时监听套接字的READABLE事件和WRITEABLE事件,如果套接字同时产生这两种事件,那么文件事件分派器会优先处理READABLE事件,READABLE事件处理完之后,才会处理WRITEABLE事件。更多关于 Redis 学习的文章,请参阅:NoSQL 数据库系列之 Redis ,本系列持续更新中。APIaeCreteFileEvent函数接受一个套接字描述符,一个事件类型,以及一个时间处理器作为参数,将套接字的给定事件加入到I/O多路复用程序的监听范围之内,并对事件和时间处理器进行关联。aeDeleteFileEvent函数接受一个套接字描述符和监听事件类型作为参数,让I/O多路复用程序取消给定套接字的给定事件监听,并且取消事件和时间处理器之间的联系。aeGetFileEvents函数接受一个套接字描述符,返回该套接字正在监听的事件类型:如果套接字没有任务事件被监听,那么函数返回AE_NONE。如果套接字的读事件正在被监听,那么函数返回AE_READABLE。如果套接字的写事件正在被监听,那么函数返回AE_WRITABLE如果套接字的读事件和写事件正在被监听,那么函数返回AE_READABLE|AE_WRITEABLE。文件事件处理器Redis为文件事件编写了多个处理器,这些事件处理器分别用于实现不同的网络通信需求:为了对连接服务器的各个客户端进行应答,服务器要为监听套接字关联应答处理器。为了接受客户端传来的命令请求,服务器要为客户端套接字关联命令请求处理器。为了向客户端返回命令的执行结果,服务器要为客户端套接字关联命令回复处理器当主服务器和从服务器进行复制操作时,朱从服务器都需要关联特别为复制功能编写的复制处理器连接应答处理器networking.c/accpetTcpHandler函数是Redis连接应答处理器,这个处理器用于对连接服务器监听套接字的客户端进行应答,具体实现为sys/socket.h/accpet函数的包装。当Redis服务器进行初始化的时候,程序会将这个连接应答处理器和服务器监听套接字的AE_READABLE事件进行关联,当有客户端用sys/socketh/connect函数连接服务器监听套接字的时候,套接字就会产生AE_READABLE事件,引发连接应答处理器执行。命令请求处理器networking.c/readQueryFromClient函数是Redis命令处理器,这个命令处理器负责从套接字中读入客户端发送的命令请求内容,具体实现为unistd.h/read函数的包装。当一个客户端通过连接应答处理器成功连接到服务器之后,服务器就会将客户端套接字的AE_READABLE事件和命令请求处理器关联起来,当客户端向服务器发送命令请求时,套接字就会产生AE_READABLE事件,引发命令请求处理器执行,并执行相应的套接字读入操作。在客户端连接服务器的整个过程,服务器都会一直为客户端套接字的AE_READABLE事件关联命令请求处理器。命令回复处理器sendReplyToClient函数是Redis命令回复处理器,这个处理器负责将服务器执行命令后得到的回复命令通过套接字返回给客户端。当服务器有命令回复需要传送给客户端时,服务器会将客户端套接字的AE_WRITEABLE事件和命令处理器关联起来,当客户端准备好接受服务器传回的命令回复时,就会产生AE_WRITABLE事件,引发命令回复处理器执行,并执行相应的套接字写入操作。当命令回复处理器发送完毕后,服务器就会解除命令回复处理器与套接字得AE_WRITABLE事件之间的关联。更多关于 Redis 学习的文章,请参阅:NoSQL 数据库系列之 Redis,本系列持续更新中。一次完整的客户端与服务器连接事件当一个 Redis 服务器正在运作,那么这个服务器的舰艇套接字得 AE_READABLE事件应该正处于监听状态下,而事件所对应的处理器为连接应答处理器。当 Redis 客户端向服务器发起连接,那么舰艇套接字将产生 AE_READABLE 事件,触发连接应答处理器执行。处理器会对客户端的连接应答请求进行应答,然后创建客户端套接字,以及客户端状态,并将客户端套接字的 AE_RAEDABLE事件与命令请求处理器进行关联,使得客户端可以主动向服务器发送命令请求。之后,假设客户端向主服务器发送一个命令请求,那么客户端套接字将产生一个 AE_READABLE 事件,引发命令请求处理器执行,处理器读取客户端的命令内容,然后传给相关程序去执行。执行命令将产生相应的命令回复,为了将这些命令回复传送给客户端,服务器会将客户端套接字的 AE_WRITABLE 事件与命令回复处理器进行关联。当客户端尝试读取命令回复的时候,客户端套接字将产生 AE_WRITABLE 事件,触发命令回复处理器执行,当命令回复处理器将命令回复全部写入套接字之后,服务器就会解除客户端套接字的 AE_WRITABLE 事件与命令回复处理器之间的关联。时间事件Redis 时间事件可以分为 两类 :定时时间:让一段程序在指定的时间之后执行周期性时间:让一段程序每隔指定时间就执行一次一个时间事件主要有三个属性组成id:服务器为时间事件创建的全局唯一ID(标识号)。ID号按从小到大的顺序递增11,新事件的ID号比旧事件的ID号要大when:毫秒精度的UNIX时间戳,记录了时间事件到达时间timeProc:时间事件处理器,一个函数。当时间事件到达时,服务器就会调用相应的处理器来处理事件一个定时事件是定时事件还是周期事件取决于时间事件处理器的返回值。如果事件处理器返回ae.h/AE_NOMORE,那么这个事件为定时事件:该事件在到达一次之后就会被删除,之后不再到达如果事件处理器返回一个非AE_NOMORE得整数值,那么这个事件为周期性事件:当一个时间事件到达之后,服务器会根据事件处理器返回的值,对时间事件的when属性进行更新,让这个事件在一段时间之后再次到达,并以这种方式一直更新并运行下去。Redis中时间事件使用场景时间事件的最主要应用是在redis服务器需要对自身的资源与配置进行定期的调度,从而确保服务器的长久运行。这些操作都是由serverCron函数实现。该函数做了以下操作。1、更新redis服务器各类统计信息,包括时间,内存占用,数据库占用等2、清理数据库中过期的key(过期删除)3、关闭和清理连接失败的客户端4、尝试进行aof个rdb的持久化操作(数据持久化)5、如果服务器是主服务器,会定期将数据向从服务器做同步操作(主从复制)。6、如果是集群模式,对集群定期进行同步和连接测试等操作(健康检查)。Redis启动后,会定期执行serverCron函数,直到redis关闭为止,默认每秒执行10次,也就是100ms执行一次。可以在redis配置文件(redis.conf)中的hz选项调整执行频率。#redis执行任务的频率为1s除以hz, 一秒钟执行多少次 hz 10实现服务器将所有时间事件都放在一个无序链表中,每当时间事件执行器被运行时,它就遍历整个链表,查找所有已经达到的时间事件,并且调用相应的事件处理器 时间事件的链表为无序链表,指的不是链表不按ID排序,而是说该丽娜表不按when属性的大小排序。正因为链表没有按照when属性进行排序,所以当时间事件执行器运行时,它必须遍历链表中的所有时间事件,这样才能确保服务器中所有时间事件都被处理。更多关于 Redis 学习的文章,请参阅:NoSQL 数据库系列之 Redis ,本系列持续更新中。时间事件的应用实例:serverCron函数持续运行的Redis服务器需要定期对自身的资源和状态进行检查和调整,从而确保服务器可以长期,稳定的运行,这些定期操作有redis.c/serverCron函数负责执行,它的主要工作:更新服务器的各类统计信息,比如时间,内存占用,数据库占用情况清理数据库中的过期键值对关闭和清理连接失效的客户端尝试进行AOF或者RDB持久化操作如果服务器时主服务器,那么对从服务器定期同步和连接测试如果处于集群模式,那么对集群进行定期同步和连接测试Redis服务器以周期性事件的方式运行serverCron函数,在服务器运行期间,每隔一段时间,serverCron就会执行一次,直到服务器关闭事件的调度与执行因为服务器中同时存在文件事件和时间事件,所以服务器必须对这两种事件进行调度,决定何时处理文件事件,何时处理时间事件,已经花费多少事件处理事件。事件的调度和执行由ae.c/aeProcessEvents函数负责。将aeProcessEvents函数置于一个循环里面,加上初始化和清理函数,这就构成了Redis服务器的主函数,一下是该函数的伪代码:def main(): #初始化服务器 init_server() #一直处理事件,知道服务器关闭 while server_is_not_shutdown(): aeProcessEvents() #服务器关闭,执行清理操作 clean_server() 从事件处理的角度,Redis服务器的运行流程可以用下图表示:总结Redis的高性能为什么是单线程也可以性能这么高。它的事件处理机制起着非常重要的一个作用。选用的模型为reactor模型。让单线程去做了多线程可以做的事情,并且还没有线程安全问题。来源:https://blog.csdn.net/weixin_43809223/article/details/109631305(六):Redis 事务详解什么是Redis事务Redis 事务的 本质是一组命令的集合。事务支持一次执行多个命令,一个事务中所有命令都会被序列化。 在事务执行过程,会按照顺序串行化执行队列中的命令,其他客户端提交的命令请求不会插入到事务执行命令序列中。总结说: redis 事务就是一次性、顺序性、排他性的执行一个队列中的一系列命令。Redis事务相关命令和使用MULTI、EXEC、DISCARD 和 WATCH 是 Redis 事务相关的命令。MULTI :开启事务,redis会将后续的命令逐个放入队列中,然后使用EXEC命令来原子化执行这个命令系列。EXEC:执行事务中的所有操作命令。DISCARD:取消事务,放弃执行事务块中的所有命令。WATCH:监视一个或多个key,如果事务在执行前,这个key(或多个key)被其他命令修改,则事务被中断,不会执行事务中的任何命令。UNWATCH:取消WATCH对所有key的监视。标准的事务执行给k1、k2分别赋值,在事务中修改k1、k2,执行事务后,查看k1、k2值都被修改。127.0.0.1:6379> set k1 v1 OK 127.0.0.1:6379> set k2 v2 OK 127.0.0.1:6379> MULTI OK 127.0.0.1:6379> set k1 11 QUEUED 127.0.0.1:6379> set k2 22 QUEUED 127.0.0.1:6379> EXEC 1) OK 2) OK 127.0.0.1:6379> get k1 "11" 127.0.0.1:6379> get k2 "22" 127.0.0.1:6379>事务取消127.0.0.1:6379> MULTI OK 127.0.0.1:6379> set k1 33 QUEUED 127.0.0.1:6379> set k2 34 QUEUED 127.0.0.1:6379> DISCARD OK事务出现错误的处理语法错误(编译器错误)在开启事务后,修改k1值为11,k2值为22,但k2语法错误,最终导致事务提交失败, k1、k2保留原值。127.0.0.1:6379> set k1 v1 OK 127.0.0.1:6379> set k2 v2 OK 127.0.0.1:6379> MULTI OK 127.0.0.1:6379> set k1 11 QUEUED 127.0.0.1:6379> sets k2 22 (error) ERR unknown command `sets`, with args beginning with: `k2`, `22`, 127.0.0.1:6379> exec (error) EXECABORT Transaction discarded because of previous errors. 127.0.0.1:6379> get k1 "v1" 127.0.0.1:6379> get k2 "v2" 127.0.0.1:6379>Redis类型错误(运行时错误)在开启事务后,修改k1值为11,k2值为22,但将k2的类型作为List, 在运行时检测类型错误,最终导致事务提交失败,此时事务并没有回滚,而是跳过错误命令继续执行, 结果k1值改变、k2保留原值。127.0.0.1:6379> set k1 v1 OK 127.0.0.1:6379> set k1 v2 OK 127.0.0.1:6379> MULTI OK 127.0.0.1:6379> set k1 11 QUEUED 127.0.0.1:6379> lpush k2 22 QUEUED 127.0.0.1:6379> EXEC 1) OK 2) (error) WRONGTYPE Operation against a key holding the wrong kind of value 127.0.0.1:6379> get k1 "11" 127.0.0.1:6379> get k2 "v2" 127.0.0.1:6379>CAS操作实现乐观锁WATCH 命令可以为 Redis 事务提供 check-and-set (CAS)行为。CAS? 乐观锁? Redis官方的例子帮你理解被 WATCH 的键会被监视,并会发觉这些键是否被改动过了。如果有至少一个被监视的键在 EXEC 执行之前被修改了, 那么整个事务都会被取消, EXEC 返回nil-reply来表示事务已经失败。举个例子, 假设我们需要原子性地为某个值进行增 1 操作(假设 INCR 不存在)。首先我们可能会这样做:val = GET mykey val = val + 1 SET mykey $val上面的这个实现在只有一个客户端的时候可以执行得很好。但是, 当多个客户端同时对同一个键进行这样的操作时, 就会产生竞争条件。举个例子, 如果客户端 A 和 B 都读取了键原来的值, 比如 10 , 那么两个客户端都会将键的值设为 11 , 但正确的结果应该是 12 才对。有了 WATCH ,我们就可以轻松地解决这类问题了:WATCH mykey val = GET mykey val = val + 1 MULTI SET mykey $val EXEC使用上面的代码, 如果在 WATCH 执行之后, EXEC 执行之前, 有其他客户端修改了 mykey 的值, 那么当前客户端的事务就会失败。程序需要做的, 就是不断重试这个操作, 直到没有发生碰撞为止。 这种形式的锁被称作乐观锁 , 它是一种非常强大的锁机制。并且因为大多数情况下, 不同的客户端会访问不同的键, 碰撞的情况一般都很少, 所以通常并不需要进行重试。watch是如何监视实现的呢?Redis使用WATCH命令来决定事务是继续执行还是回滚,那就需要在MULTI之前使用WATCH来监控某些键值对,然后使用MULTI命令来开启事务,执行对数据结构操作的各种命令,此时这些命令入队列。当使用EXEC执行事务时,首先会比对WATCH所监控的键值对,如果没发生改变,它会执行事务队列中的命令,提交事务;如果发生变化,将不会执行事务中的任何命令,同时事务回滚。当然无论是否回滚,Redis都会取消执行事务前的WATCH命令。watch 命令实现监视在事务开始前用WATCH监控k1,之后修改k1为11,说明事务开始前k1值被改变,MULTI开始事务,修改k1值为12,k2为22,执行EXEC,发回nil,说明事务回滚;查看下k1、k2的值都没有被事务中的命令所改变。127.0.0.1:6379> set k1 v1 OK 127.0.0.1:6379> set k2 v2 OK 127.0.0.1:6379> WATCH k1 OK 127.0.0.1:6379> set k1 11 OK 127.0.0.1:6379> MULTI OK 127.0.0.1:6379> set k1 12 QUEUED 127.0.0.1:6379> set k2 22 QUEUED 127.0.0.1:6379> EXEC (nil) 127.0.0.1:6379> get k1 "11" 127.0.0.1:6379> get k2 "v2" 127.0.0.1:6379>UNWATCH取消监视127.0.0.1:6379> set k1 v1 OK 127.0.0.1:6379> set k2 v2 OK 127.0.0.1:6379> WATCH k1 OK 127.0.0.1:6379> set k1 11 OK 127.0.0.1:6379> UNWATCH OK 127.0.0.1:6379> MULTI OK 127.0.0.1:6379> set k1 12 QUEUED 127.0.0.1:6379> set k2 22 QUEUED 127.0.0.1:6379> exec 1) OK 2) OK 127.0.0.1:6379> get k1 "12" 127.0.0.1:6379> get k2 "22"Redis事务执行步骤通过上文命令执行,很显然 Redis事务执行是三个阶段 :开启 :以MULTI开始一个事务入队 :将多个命令入队到事务中,接到这些命令并不会立即执行,而是放到等待执行的事务队列里面执行 :由EXEC命令触发事务当一个客户端切换到事务状态之后, 服务器会根据这个客户端发来的不同命令执行不同的操作:如果客户端发送的命令为 EXEC 、 DISCARD 、 WATCH 、 MULTI 四个命令的其中一个, 那么服务器立即执行这个命令。与此相反, 如果客户端发送的命令是 EXEC 、 DISCARD 、 WATCH 、 MULTI 四个命令以外的其他命令, 那么服务器并不立即执行这个命令, 而是将这个命令放入一个事务队列里面, 然后向客户端返回 QUEUED 回复。更深入的理解我们再通过几个问题来深入理解Redis事务。为什么 Redis 不支持回滚?如果你有使用关系式数据库的经验,那么“ Redis 在事务失败时不进行回滚,而是继续执行余下的命令 ”这种做法可能会让你觉得有点奇怪。以下是这种做法的优点:Redis 命令只会因为错误的语法而失败(并且这些问题不能在入队时发现),或是命令用在了错误类型的键上面:这也就是说,从实用性的角度来说,失败的命令是由编程错误造成的,而这些错误应该在开发的过程中被发现,而不应该出现在生产环境中。因为不需要对回滚进行支持,所以 Redis 的内部可以保持简单且快速。有种观点认为 Redis 处理事务的做法会产生 bug , 然而需要注意的是, 在通常情况下, 回滚并不能解决编程错误带来的问题 。举个例子, 如果你本来想通过 INCR 命令将键的值加上 1 , 却不小心加上了 2 , 又或者对错误类型的键执行了 INCR , 回滚是没有办法处理这些情况的。如何理解Redis与事务的ACID?一般来说, 事务有四个性质称为ACID,分别是原子性,一致性,隔离性和持久性 。这是基础,但是很多文章对Redis 是否支持ACID有一些异议,我觉的有必要梳理下:原子性atomicity首先通过上文知道 运行期的错误是不会回滚的,很多文章由此说Redis事务违背原子性的;而官方文档认为是遵从原子性的。Redis官方文档给的理解是, Redis的事务是原子性的:所有的命令,要么全部执行,要么全部不执行。 而不是完全成功。一致性consistencyredis事务可以保证命令失败的情况下得以回滚,数据能恢复到没有执行之前的样子,是保证一致性的,除非redis进程意外终结。隔离性Isolationredis事务是严格遵守隔离性的,原因是redis是单进程单线程模式(v6.0之前),可以保证命令执行过程中不会被其他客户端命令打断。但是,Redis不像其它结构化数据库有隔离级别这种设计。持久性Durabilityredis事务是不保证持久性的 ,这是因为redis持久化策略中不管是RDB还是AOF都是异步执行的,不保证持久性是出于对性能的考虑。Redis事务其它实现基于Lua脚本,Redis可以保证脚本内的命令一次性、按顺序地执行,其同时也不提供事务运行错误的回滚,执行过程中如果部分命令运行错误,剩下的命令还是会继续运行完基于中间标记变量,通过另外的标记变量来标识事务是否执行完成,读取数据时先读取该标记变量判断是否事务执行完成。但这样会需要额外写代码实现,比较繁琐来源:https://www.pdai.tech/md/db/nosql-redis/db-redis-x-trans.html(七):Redis 持久化(RDB和AOF)Redis 持久化介绍为了防止数据丢失以及服务重启时能够恢复数据,Redis支持数据的持久化,主要分为两种方式,分别是RDB和AOF; 当然实际场景下还会使用这两种的混合模式。为什么需要持久化?Redis是个基于内存的数据库。那服务一旦宕机,内存中的数据将全部丢失。通常的解决方案是从后端数据库恢复这些数据,但后端数据库有性能瓶颈,如果是大数据量的恢复:1、会对数据库带来巨大的压力2、数据库的性能不如Redis。导致程序响应慢所以对Redis来说,实现数据的持久化,避免从后端数据库中恢复数据,是至关重要的。Redis持久化有哪些方式呢?为什么我们需要重点学RDB和AOF? 从严格意义上说, Redis服务提供四种持久化存储方案:RDB、AOF、虚拟内存(VM)和 DISKSTORE。 虚拟内存(VM)方式,从Redis Version 2.4开始就被官方明确表示不再建议使用,Version 3.2版本中更找不到关于虚拟内存(VM)的任何配置范例,Redis的主要作者Salvatore Sanfilippo还专门写了一篇论文,来反思Redis对虚拟内存(VM)存储技术的支持问题。至于DISKSTORE方式,是从Redis Version 2.8版本开始提出的一个存储设想,到目前为止Redis官方也没有在任何stable版本中明确建议使用这用方式。在Version 3.2版本中同样找不到对于这种存储方式的明确支持。从网络上能够收集到的各种资料来看,DISKSTORE方式和RDB方式还有着一些千丝万缕的联系,不过各位读者也知道,除了官方文档以外网络资料很多就是大抄。最关键的是目前官方文档上能够看到的Redis对持久化存储的支持明确的就只有两种方案(https://redis.io/topics/persistence):RDB和AOF。所以本文也只会具体介绍这两种持久化存储方案的工作特定和配置要点。RDB(Redis DataBase)持久化RDB 就是 Redis DataBase 的缩写,中文名为 快照/内存快照 , RDB持久化是把当前进程数据生成快照保存到磁盘上的过程,由于是某一时刻的快照,那么快照中的值要早于或者等于内存中的值。 Redis 会单独创建(fork)一个子进程进行持久化,会先将数据写入一个临时文件中,待持久化过程结束了,再用这个临时文件替换上次持久化好的文件。整个过程中,主进程不进行任何 IO 操作,这就确保的极高的性能。如果需要大规模的数据的恢复,且对数据恢复的完整性不是非常敏感,那 RDB 方式要比 AOF 方式更加高效。RDB 唯一的缺点是最后一次持久化的数据可能会丢失。触发rdb持久化的方式有2种,分别是 手动触发 和 自动触发 。手动触发手动触发分别对应 save和bgsave命令save命令 :阻塞当前Redis服务器,直到RDB过程完成为止,对于内存 比较大的实例会造成长时间阻塞, 线上环境不建议使用bgsave命令 :Redis进程执行fork操作创建子进程,RDB持久化过程由子 进程负责,完成后自动结束。阻塞只发生在fork阶段,一般时间很短bgsave流程图如下所示具体流程如下redis客户端执行bgsave命令或者自动触发bgsave命令;主进程判断当前是否已经存在正在执行的子进程,如果存在,那么主进程直接返回;如果不存在正在执行的子进程,那么就fork一个新的子进程进行持久化数据,fork过程是阻塞的,fork操作完成后主进程即可执行其他操作;子进程先将数据写入到临时的rdb文件中,待快照数据写入完成后再原子替换旧的rdb文件;同时发送信号给主进程,通知主进程rdb持久化完成,主进程更新相关的统计信息(info Persitence下的rdb_*相关选项)。自动触发在以下 4种情况时会自动触发 :redis.conf中配置save m n,即在m秒内有n次修改时,自动触发bgsave生成rdb文件;主从复制时,从节点要从主节点进行全量复制时也会触发bgsave操作,生成当时的快照发送到从节点;执行debug reload命令重新加载redis时也会触发bgsave操作;默认情况下执行shutdown命令时,如果没有开启aof持久化,那么也会触发bgsave操作;redis.conf中配置RDB快照周期:内存快照虽然可以通过技术人员手动执行SAVE或BGSAVE命令来进行,但生产环境下多数情况都会设置其周期性执行条件。Redis中默认的周期新设置# 周期性执行条件的设置格式为 save <seconds> <changes> # 默认的设置为: save 900 1 save 300 10 save 60 10000 # 以下设置方式为关闭RDB快照功能 save ""以上三项默认信息设置代表的意义是:如果900秒内有1条Key信息发生变化,则进行快照;如果300秒内有10条Key信息发生变化,则进行快照;如果60秒内有10000条Key信息发生变化,则进行快照。读者可以按照这个规则,根据自己的实际请求压力进行设置调整。其它相关配置# 文件名称 dbfilename dump.rdb # 文件保存路径 dir /home/work/app/redis/data/ # 如果持久化出错,主进程是否停止写入 stop-writes-on-bgsave-error yes # 是否压缩 rdbcompression yes # 导入时是否检查 rdbchecksum yesdbfilename :RDB文件在磁盘上的名称。dir :RDB文件的存储路径。默认设置为“./”,也就是Redis服务的主目录。stop-writes-on-bgsave-error :上文提到的在快照进行过程中,主进程照样可以接受客户端的任何写操作的特性,是指在快照操作正常的情况下。如果快照操作出现异常(例如操作系统用户权限不够、磁盘空间写满等等)时,Redis就会禁止写操作。这个特性的主要目的是使运维人员在第一时间就发现Redis的运行错误,并进行解决。一些特定的场景下,您可能需要对这个特性进行配置,这时就可以调整这个参数项。该参数项默认情况下值为yes,如果要关闭这个特性,指定即使出现快照错误Redis一样允许写操作,则可以将该值更改为no。rdbcompression :该属性将在字符串类型的数据被快照到磁盘文件时,启用LZF压缩算法。Redis官方的建议是请保持该选项设置为yes,因为“it’s almost always a win”。rdbchecksum :从RDB快照功能的 version 5 版本开始,一个64位的CRC冗余校验编码会被放置在RDB文件的末尾,以便对整个RDB文件的完整性进行验证。这个功能大概会多损失10%左右的性能,但获得了更高的数据可靠性。所以如果您的 Redis 服务需要追求极致的性能,就可以将这个选项设置为no。更多关于 Redis 学习的文章,请参阅:NoSQL 数据库系列之 Redis ,本系列持续更新中。RDB 更深入理解我们通过几个实战问题来深入理解RDB由于生产环境中我们为Redis开辟的内存区域都比较大(例如6GB),那么将内存中的数据同步到硬盘的过程可能就会持续比较长的时间,而实际情况是这段时间Redis服务一般都会收到数据写操作请求。 那么如何保证数据一致性呢? RDB中的核心思路是Copy-on-Write,来保证在进行快照操作的这段时间,需要压缩写入磁盘上的数据在内存中不会发生变化。在正常的快照操作中,一方面Redis主进程会fork一个新的快照进程专门来做这个事情,这样保证了Redis服务不会停止对客户端包括写请求在内的任何响应。另一方面这段时间发生的数据变化会以副本的方式存放在另一个新的内存区域,待快照操作结束后才会同步到原来的内存区域。举个例子:如果主线程对这些数据也都是读操作(例如图中的键值对 A),那么,主线程和 bgsave 子进程相互不影响。但是,如果主线程要修改一块数据(例如图中的键值对 C),那么,这块数据就会被复制一份,生成该数据的副本。然后,bgsave 子进程会把这个副本数据写入 RDB 文件,而在这个过程中,主线程仍然可以直接修改原来的数据。在进行快照操作的这段时间,如果发生服务崩溃怎么办?很简单,在没有将数据全部写入到磁盘前,这次快照操作都不算成功。如果出现了服务崩溃的情况,将以上一次完整的RDB快照文件作为恢复内存数据的参考。也就是说,在快照操作过程中不能影响上一次的备份数据。Redis服务会在磁盘上创建一个临时文件进行数据操作,待操作成功后才会用这个临时文件替换掉上一次的备份。可以每秒做一次快照吗?对于快照来说,所谓“连拍”就是指连续地做快照。这样一来,快照的间隔时间变得很短,即使某一时刻发生宕机了,因为上一时刻快照刚执行,丢失的数据也不会太多。但是,这其中的快照间隔时间就很关键了。如下图所示,我们先在 T0 时刻做了一次快照,然后又在 T0+t 时刻做了一次快照,在这期间,数据块 5 和 9 被修改了。如果在 t 这段时间内,机器宕机了,那么,只能按照 T0 时刻的快照进行恢复。此时,数据块 5 和 9 的修改值因为没有快照记录,就无法恢复了。所以,要想尽可能恢复数据,t 值就要尽可能小,t 越小,就越像“连拍”。那么,t 值可以小到什么程度呢,比如说 是不是可以每秒做一次快照?毕竟,每次快照都是由 bgsave 子进程在后台执行,也不会阻塞主线程。 这种想法其实是错误的。虽然 bgsave 执行时不阻塞主线程,但是, 如果频繁地执行全量快照,也会带来两方面的开销:一方面,频繁将全量数据写入磁盘,会给磁盘带来很大压力,多个快照竞争有限的磁盘带宽,前一个快照还没有做完,后一个又开始做了,容易造成恶性循环。另一方面,bgsave 子进程需要通过 fork 操作从主线程创建出来。虽然,子进程在创建后不会再阻塞主线程,但是,fork 这个创建过程本身会阻塞主线程,而且主线程的内存越大,阻塞时间越长。如果频繁 fork 出 bgsave 子进程,这就会频繁阻塞主线程了。那么,有什么其他好方法吗?此时,我们可以做 增量快照,就是指做了一次全量快照后,后续的快照只对修改的数据进行快照记录,这样可以避免每次全量快照的开销。 这个比较好理解。但是它需要我们使用额外的元数据信息去记录哪些数据被修改了,这会带来额外的空间开销问题。那么,还有什么方法既能利用 RDB 的快速恢复,又能以较小的开销做到尽量少丢数据呢?且看后文中 4.0版本中引入的RDB和AOF的混合方式 。RDB优缺点优点RDB文件是某个时间节点的快照,默认使用LZF算法进行压缩,压缩后的文件体积远远小于内存大小,适用于备份、全量复制等场景;Redis加载RDB文件恢复数据要远远快于AOF方式;缺点RDB方式实时性不够,无法做到秒级的持久化;每次调用bgsave都需要fork子进程,fork子进程属于重量级操作,频繁执行成本较高;RDB文件是二进制的,没有可读性,AOF文件在了解其结构的情况下可以手动修改或者补全;版本兼容RDB文件问题;针对RDB不适合实时持久化的问题,Redis提供了AOF持久化方式来解决。AOF(Append Only File)持久化Redis是 “写后”日志,Redis先执行命令,把数据写入内存,然后才记录日志。 日志里记录的是Redis收到的每一条命令,这些命令是以文本形式保存。PS: 大多数的数据库采用的是写前日志(WAL),例如MySQL,通过写前日志和两阶段提交,实现数据和逻辑的一致性。而AOF日志采用写后日志,即 先写内存,后写日志 。为什么采用写后日志?Redis要求高性能,采用写日志有两方面好处:避免额外的检查开销 :Redis 在向 AOF 里面记录日志的时候,并不会先去对这些命令进行语法检查。所以,如果先记日志再执行命令的话,日志中就有可能记录了错误的命令,Redis 在使用日志恢复数据时,就可能会出错。不会阻塞当前的写操作但这种方式存在 潜在风险 :如果命令执行完成,写日志之前宕机了,会丢失数据。主线程写磁盘压力大,导致写盘慢,阻塞后续操作。如何实现AOFAOF日志记录Redis的每个写命令,步骤分为: 命令追加(append)、文件写入(write)和文件同步(sync)。命令追加 :当AOF持久化功能打开了,服务器在执行完一个写命令之后,会以协议格式将被执行的写命令追加到服务器的 aof_buf 缓冲区。文件写入和同步 :关于何时将 aof_buf 缓冲区的内容写入AOF文件中,Redis提供了 三种写回策略:Always ,同步写回:每个写命令执行完,立马同步地将日志写回磁盘; Everysec ,每秒写回:每个写命令执行完,只是先把日志写到AOF文件的内存缓冲区,每隔一秒把缓冲区中的内容写入磁盘; No ,操作系统控制的写回:每个写命令执行完,只是先把日志写到AOF文件的内存缓冲区,由操作系统决定何时将缓冲区内容写回磁盘。三种写回策略的优缺点上面的三种写回策略体现了一个重要原则: ** trade-off,取舍,指在性能和可靠性保证之间做取舍。关于AOF的同步策略是涉及到操作系统的 write 函数和 fsync 函数的,在《Redis设计与实现》中是这样说明的:为了提高文件写入效率,在现代操作系统中,当用户调用write函数,将一些数据写入文件时,操作系统通常会将数据暂存到一个内存缓冲区里,当缓冲区的空间被填满或超过了指定时限后,才真正将缓冲区的数据写入到磁盘里。这样的操作虽然提高了效率,但也为数据写入带来了安全问题:如果计算机停机,内存缓冲区中的数据会丢失。为此,系统提供了fsync、fdatasync同步函数,可以强制操作系统立刻将缓冲区中的数据写入到硬盘里,从而确保写入数据的安全性。redis.conf中配置AOF默认情况下,Redis是没有开启AOF的 ,可以通过配置redis.conf文件来开启AOF持久化,关于AOF的配置如下:# appendonly参数开启AOF持久化 appendonly no # AOF持久化的文件名,默认是appendonly.aof appendfilename "appendonly.aof" # AOF文件的保存位置和RDB文件的位置相同,都是通过dir参数设置的 dir ./ # 同步策略 # appendfsync always appendfsync everysec # appendfsync no # aof重写期间是否同步 no-appendfsync-on-rewrite no # 重写触发配置 auto-aof-rewrite-percentage 100 auto-aof-rewrite-min-size 64mb # 加载aof出错如何处理 aof-load-truncated yes # 文件重写策略 aof-rewrite-incremental-fsync yes code here...以下是Redis中关于AOF的主要配置信息:appendonly #默认情况下AOF功能是关闭的,将该选项改为yes以便打开Redis的AOF功能。appendfilename #这个参数项很好理解了,就是AOF文件的名字。appendfsync #这个参数项是AOF功能最重要的设置项之一,主要用于设置“真正执行”操作命令向AOF文件中同步的策略。什么叫“真正执行”呢?还记得Linux操作系统对磁盘设备的操作方式吗?为了保证操作系统中I/O队列的操作效率,应用程序提交的I/O操作请求一般是被放置在linux Page Cache中的,然后再由Linux操作系统中的策略自行决定正在写到磁盘上的时机。而Redis中有一个fsync()函数,可以将Page Cache中待写的数据真正写入到物理设备上,而缺点是频繁调用这个fsync()函数干预操作系统的既定策略,可能导致I/O卡顿的现象频繁 。与上节对应,appendfsync参数项可以设置三个值,分别是:always、everysec、no,默认的值为everysec。no-appendfsync-on-rewrite:always和everysec的设置会使真正的I/O操作高频度的出现,甚至会出现长时间的卡顿情况,这个问题出现在操作系统层面上,所有靠工作在操作系统之上的Redis是没法解决的。为了尽量缓解这个情况,Redis提供了这个设置项,保证在完成fsync函数调用时,不会将这段时间内发生的命令操作放入操作系统的Page Cache(这段时间Redis还在接受客户端的各种写操作命令)。auto-aof-rewrite-percentage:上文说到在生产环境下,技术人员不可能随时随地使用“BGREWRITEAOF”命令去重写AOF文件。所以更多时候我们需要依靠Redis中对AOF文件的自动重写策略。Redis中对触发自动重写AOF文件的操作提供了两个设置:auto-aof-rewrite-percentage表示如果当前AOF文件的大小超过了上次重写后AOF文件的百分之多少后,就再次开始重写AOF文件。例如该参数值的默认设置值为100,意思就是如果AOF文件的大小超过上次AOF文件重写后的1倍,就启动重写操作。auto-aof-rewrite-min-size:参考auto-aof-rewrite-percentage选项的介绍,auto-aof-rewrite-min-size设置项表示启动AOF文件重写操作的AOF文件最小大小。如果AOF文件大小低于这个值,则不会触发重写操作。注意,auto-aof-rewrite-percentage和auto-aof-rewrite-min-size只是用来控制Redis中自动对AOF文件进行重写的情况,如果是技术人员手动调用“BGREWRITEAOF”命令,则不受这两个限制条件左右。深入理解AOF重写AOF会记录每个写命令到AOF文件,随着时间越来越长,AOF文件会变得越来越大。如果不加以控制,会对Redis服务器,甚至对操作系统造成影响,而且AOF文件越大,数据恢复也越慢。 为了解决AOF文件体积膨胀的问题,Redis提供AOF文件重写机制来对AOF文件进行“瘦身”。图例解释AOF重写Redis通过创建一个新的AOF文件来替换现有的AOF,新旧两个AOF文件保存的数据相同,但新AOF文件没有了冗余命令。AOF重写会阻塞吗?AOF重写过程是由后台进程bgrewriteaof来完成的。主线程fork出后台的bgrewriteaof子进程,fork会把主线程的内存拷贝一份给bgrewriteaof子进程,这里面就包含了数据库的最新数据。然后,bgrewriteaof子进程就可以在不影响主线程的情况下,逐一把拷贝的数据写成操作,记入重写日志。所以aof在重写时,在fork进程时是会阻塞住主线程的。AOF日志何时会重写?有两个配置项控制AOF重写的触发:auto-aof-rewrite-min-size :表示运行AOF重写时文件的最小大小,默认为64MB。auto-aof-rewrite-percentage :这个值的计算方式是,当前aof文件大小和上一次重写后aof文件大小的差值,再除以上一次重写后aof文件大小。也就是当前aof文件比上一次重写后aof文件的增量大小,和上一次重写后aof文件大小的比值。重写日志时,有新数据写入咋整?重写过程总结为:“一个拷贝,两处日志”。在fork出子进程时的拷贝,以及在重写时,如果有新数据写入,主线程就会将命令记录到两个aof日志内存缓冲区中。如果AOF写回策略配置的是always,则直接将命令写回旧的日志文件,并且保存一份命令至AOF重写缓冲区,这些操作对新的日志文件是不存在影响的。(旧的日志文件:主线程使用的日志文件,新的日志文件:bgrewriteaof进程使用的日志文件)而在bgrewriteaof子进程完成会日志文件的重写操作后,会提示主线程已经完成重写操作,主线程会将AOF重写缓冲中的命令追加到新的日志文件后面。这时候在高并发的情况下,AOF重写缓冲区积累可能会很大,这样就会造成阻塞,Redis后来通过Linux管道技术让aof重写期间就能同时进行回放,这样aof重写结束后只需回放少量剩余的数据即可。最后通过修改文件名的方式,保证文件切换的原子性。在AOF重写日志期间发生宕机的话,因为日志文件还没切换,所以恢复数据时,用的还是旧的日志文件。总结操作:主线程fork出子进程重写aof日志子进程重写日志完成后,主线程追加aof日志缓冲替换日志文件温馨提示:这里的进程和线程的概念有点混乱。因为后台的bgreweiteaof进程就只有一个线程在操作,而主线程是Redis的操作进程,也是单独一个线程。这里想表达的是Redis主进程在fork出一个后台进程之后,后台进程的操作和主进程是没有任何关联的,也不会阻塞主线程。主线程fork出子进程的是如何复制内存数据的?fork采用操作系统提供的写时复制(copy on write)机制,就是为了避免一次性拷贝大量内存数据给子进程造成阻塞。fork子进程时,子进程时会拷贝父进程的页表,即虚实映射关系(虚拟内存和物理内存的映射索引表),而不会拷贝物理内存。这个拷贝会消耗大量cpu资源,并且拷贝完成前会阻塞主线程,阻塞时间取决于内存中的数据量,数据量越大,则内存页表越大。拷贝完成后,父子进程使用相同的内存地址空间。但主进程是可以有数据写入的,这时候就会拷贝物理内存中的数据。如下图(进程1看做是主进程,进程2看做是子进程):在主进程有数据写入时,而这个数据刚好在页c中,操作系统会创建这个页面的副本(页c的副本),即拷贝当前页的物理数据,将其映射到主进程中,而子进程还是使用原来的的页c。在重写日志整个过程时,主线程有哪些地方会被阻塞?fork子进程时,需要拷贝虚拟页表,会对主线程阻塞。主进程有bigkey写入时,操作系统会创建页面的副本,并拷贝原有的数据,会对主线程阻塞。子进程重写日志完成后,主进程追加aof重写缓冲区时可能会对主线程阻塞。为什么AOF重写不复用原AOF日志?两方面原因:父子进程写同一个文件会产生竞争问题,影响父进程的性能。如果AOF重写过程中失败了,相当于污染了原本的AOF文件,无法做恢复数据使用。RDB和AOF混合方式(4.0版本)Redis 4.0 中提出了一个混合使用 AOF 日志和内存快照的方法。简单来说,内存快照以一定的频率执行,在两次快照之间,使用 AOF 日志记录这期间的所有命令操作。这样一来,快照不用很频繁地执行,这就避免了频繁 fork 对主线程的影响。而且,AOF 日志也只用记录两次快照间的操作,也就是说,不需要记录所有操作了,因此,就不会出现文件过大的情况了,也可以避免重写开销。如下图所示,T1 和 T2 时刻的修改,用 AOF 日志记录,等到第二次做全量快照时,就可以清空 AOF 日志,因为此时的修改都已经记录到快照中了,恢复时就不再用日志了。这个方法既能享受到 RDB 文件快速恢复的好处,又能享受到 AOF 只记录操作命令的简单优势, 实际环境中用的很多。从持久化中恢复数据数据的备份、持久化做完了,我们如何从这些持久化文件中恢复数据呢?如果一台服务器上有既有RDB文件,又有AOF文件,该加载谁呢?其实想要从这些文件中恢复数据,只需要重新启动Redis即可。我们还是通过图来了解这个流程:redis重启时判断是否开启aof,如果开启了aof,那么就优先加载aof文件;如果aof存在,那么就去加载aof文件,加载成功的话redis重启成功,如果aof文件加载失败,那么会打印日志表示启动失败,此时可以去修复aof文件后重新启动;若aof文件不存在,那么redis就会转而去加载rdb文件,如果rdb文件不存在,redis直接启动成功;如果rdb文件存在就会去加载rdb文件恢复数据,如加载失败则打印日志提示启动失败,如加载成功,那么redis重启成功,且使用rdb文件恢复数据;那么为什么会优先加载AOF呢?因为AOF保存的数据更完整,通过上面的分析我们知道AOF基本上最多损失1s的数据。性能与实践通过上面的分析,我们都知道RDB的快照、AOF的重写都需要fork,这是一个重量级操作,会对Redis造成阻塞。因此为了不影响Redis主进程响应,我们需要尽可能降低阻塞。降低fork的频率,比如可以手动来触发RDB生成快照、与AOF重写;控制Redis最大使用内存,防止fork耗时过长;使用更牛逼的硬件;合理配置Linux的内存分配策略,避免因为物理内存不足导致fork失败。在线上我们到底该怎么做?我提供一些自己的实践经验。如果Redis中的数据并不是特别敏感或者可以通过其它方式重写生成数据,可以关闭持久化,如果丢失数据可以通过其它途径补回;自己制定策略定期检查Redis的情况,然后可以手动触发备份、重写数据;单机如果部署多个实例,要防止多个机器同时运行持久化、重写操作,防止出现内存、CPU、IO资源竞争,让持久化变为串行;可以加入主从机器,利用一台从机器进行备份处理,其它机器正常响应客户端的命令;RDB持久化与AOF持久化可以同时存在,配合使用。来源:https://www.pdai.tech/md/db/nosql-redis/db-redis-x-rdb-aof.html(八):Redis 主从复制及数据恢复实践概念主从复制,是指将一台 Redis 服务器的数据,复制到其他的 Redis 服务器。前者称之为主节点(master/leader),后者称之为从节点(slave/flower);数据的复制都是单向的,只能从主节点到从节点。Master 以写为主,Slave 以读为主。默认情况下,每台 Redis 服务器都是主节点。且一个主节点可以有多个从节点或者没有从节点,但是一个从节点只能有一个主节点。主从复制的作用1、数据冗余 :主从复制实现了数据的热备份,是持久化的之外的一种数据冗余方式。2、故障恢复 :当主节点出现问题时,可以由从节点提供服务,实现快速的故障恢复。实际也是一种服务的冗余。3、负载均衡 :在主从复制的基础上,配合读写分离,可以由主节点提供写服务,由从节点提供读服务(即写 Redis 数据时应用连接主节点,读 Redis 的时候应用连接从节点),分担服务器负载;尤其是在写少读多的场景下,通过多个节点分担读负载,可以大大提高 Redis 服务器的并发量。4、高可用(集群)的基石 :除了上述作用以外,主从复制还是哨兵模式和集群能够实施的基础,因此说主从复制是 Redis 高可用的基础。一般来说,要将Redis 运用于工程项目中, 只使用一台 Redis 是万万不能的(可能会宕机),原因如下: 1、从结构上,单个 Redis 服务器会发生单点故障,并且一台服务器需要处理所有的请求负载,压力很大;2、从容量上,单个 Redis 服务器内存容量有限,就算一台 Redis 服务器内存容量为 265G, 也不能将所有的内存用作 Redis 存储内存,一般来说, 单台 Redis最大使用内存不应该超过 20G。 电商网站上的商品,一般都是一次上传,无数次浏览的,说专业点就是“ 多读少写 ”。对于这种场景,我们可以使用如下这种架构:主从复制,读写分离 !80% 的情况下,都是在进行读操作。这种架构可以减少服务器压力,经常使用实际生产环境中, 最少是“一主二从”的配置 。真实环境中不可能使用单机 Redis。主从复制原理注意:在2.8版本之前只有全量复制,而2.8版本后有全量和增量复制:全量(同步)复制 :比如第一次同步时增量(同步)复制 :只会把主从库网络断连期间主库收到的命令,同步给从库全量复制当我们启动多个 Redis 实例的时候,它们相互之间就可以通过 replicaof(Redis 5.0 之前使用 slaveof)命令形成主库和从库的关系,之后会按照三个阶段完成数据的第一次同步。确立主从关系例如,现在有实例 1(ip:172.16.19.3)和实例 2(ip:172.16.19.5),我们在实例 2 上执行以下这个命令后,实例 2 就变成了实例 1 的从库,并从实例 1 上复制数据:replicaof 172.16.19.3 6379全量复制的三个阶段你可以先看一下下面这张图,有个整体感知,接下来我再具体介绍。第一阶段是主从库间建立连接、协商同步的过程 ,主要是为全量复制做准备。在这一步,从库和主库建立起连接,并告诉主库即将进行同步,主库确认回复后,主从库间就可以开始同步了。具体来说,从库给主库发送 psync 命令,表示要进行数据同步,主库根据这个命令的参数来启动复制。psync 命令包含了主库的 runID 和复制进度 offset 两个参数。runID,是每个 Redis 实例启动时都会自动生成的一个随机 ID,用来唯一标记这个实例。当从库和主库第一次复制时,因为不知道主库的 runID,所以将 runID 设为“?”。offset,此时设为 -1,表示第一次复制。主库收到 psync 命令后,会用 FULLRESYNC 响应命令带上两个参数:主库 runID 和主库目前的复制进度 offset,返回给从库。从库收到响应后,会记录下这两个参数。这里有个地方需要注意,FULLRESYNC 响应表示第一次复制采用的全量复制,也就是说,主库会把当前所有的数据都复制给从库。更多关于 Redis 学习的文章,请参阅:NoSQL 数据库系列之 Redis ,本系列持续更新中。第二阶段,主库将所有数据同步给从库。 从库收到数据后,在本地完成数据加载。这个过程依赖于内存快照生成的 RDB 文件。具体来说,主库执行 bgsave 命令,生成 RDB 文件,接着将文件发给从库。从库接收到 RDB 文件后,会先清空当前数据库,然后加载 RDB 文件。这是因为从库在通过 replicaof 命令开始和主库同步前,可能保存了其他数据。为了避免之前数据的影响,从库需要先把当前数据库清空。在主库将数据同步给从库的过程中,主库不会被阻塞,仍然可以正常接收请求。否则,Redis 的服务就被中断了。但是,这些请求中的写操作并没有记录到刚刚生成的 RDB 文件中。为了保证主从库的数据一致性,主库会在内存中用专门的 replication buffer,记录 RDB 文件生成后收到的所有写操作。第三个阶段,主库会把第二阶段执行过程中新收到的写命令,再发送给从库。 具体的操作是,当主库完成 RDB 文件发送后,就会把此时 replication buffer 中的修改操作发给从库,从库再重新执行这些操作。这样一来,主从库就实现同步了。增量复制在 Redis 2.8 版本引入了增量复制。为什么会设计增量复制?如果主从库在命令传播时出现了网络闪断,那么,从库就会和主库重新进行一次全量复制,开销非常大。 从 Redis 2.8 开始,网络断了之后,主从库会采用增量复制的方式继续同步。增量复制的流程你可以先看一下下面这张图,有个整体感知,接下来我再具体介绍。先看两个概念: replication buffer 和 repl_backlog_bufferrepl_backlog_buffer :它是为了从库断开之后,如何找到主从差异数据而设计的环形缓冲区,从而避免全量复制带来的性能开销。如果从库断开时间太久,repl_backlog_buffer环形缓冲区被主库的写命令覆盖了,那么从库连上主库后只能乖乖地进行一次全量复制,所以repl_backlog_buffer配置尽量大一些,可以降低主从断开后全量复制的概率。而在repl_backlog_buffer中找主从差异的数据后,如何发给从库呢?这就用到了replication buffer。replication buffer :Redis和客户端通信也好,和从库通信也好,Redis都需要给分配一个 内存buffer进行数据交互,客户端是一个client,从库也是一个client,我们每个client连上Redis后,Redis都会分配一个client buffer,所有数据交互都是通过这个buffer进行的:Redis先把数据写到这个buffer中,然后再把buffer中的数据发到client socket中再通过网络发送出去,这样就完成了数据交互。所以主从在增量同步时,从库作为一个client,也会分配一个buffer,只不过这个buffer专门用来传播用户的写命令到从库,保证主从数据一致,我们通常把它叫做replication buffer。如果在网络断开期间,repl_backlog_size环形缓冲区写满之后,从库是会丢失掉那部分被覆盖掉的数据,还是直接进行全量复制呢?对于这个问题来说, 有两个关键点: 1.一个从库如果和主库断连时间过长,造成它在主库repl_backlog_buffer的slave_repl_offset位置上的数据已经被覆盖掉了,此时从库和主库间将进行全量复制。2.每个从库会记录自己的slave_repl_offset,每个从库的复制进度也不一定相同。在和主库重连进行恢复时,从库会通过psync命令把自己记录的slave_repl_offset发给主库,主库会根据从库各自的复制进度,来决定这个从库可以进行增量复制,还是全量复制。Redis 主从复制部署实践环境配置只配置从库,不用配置主库。[root@xxx bin]# redis-cli -p 6379 127.0.0.1:6379> ping PONG 127.0.0.1:6379> info replication # 查看当前库的信息 # Replication role:master # 角色 connected_slaves:0 # 当前没有从库 master_replid:2467dd9bd1c252ce80df280c925187b3417055ad master_replid2:0000000000000000000000000000000000000000 master_repl_offset:0 second_repl_offset:-1 repl_backlog_active:0 repl_backlog_size:1048576 repl_backlog_first_byte_offset:0 repl_backlog_histlen:0 127.0.0.1:6379> 复制 3 个配置文件,然后修改对应的信息1、端口2、pid 名称3、log 文件名称4、dump.rdb 名称port 6381 pidfile /var/run/redis_6381.pid logfile "6381.log" dbfilename dump6381.rdb修改完毕后,启动我们的 3 个 redis 服务器,可以通过进程信息查询。[root@xxx ~]# ps -ef|grep redis root 426 1 0 16:53 ? 00:00:00 redis-server *:6379 root 446 1 0 16:54 ? 00:00:00 redis-server *:6380 root 457 1 0 16:54 ? 00:00:00 redis-server *:6381 root 464 304 0 16:54 pts/3 00:00:00 grep --color=auto redis一主二从环境默认情况下,每台 Redis 服务器都是主节点,我们一般情况下,只用配置从机就好了。主机:6379, 从机:6380 和 6381配置的方式有两种:一种是直接使用命令配置,这种方式当 Redis 重启后配置会失效。另一种方式是使用配置文件。这里使用命令演示一下。下面将80 和 81 两个配置为在从机。127.0.0.1:6380> SLAVEOF 127.0.0.1 6379 # SLAVEOF host port OK 127.0.0.1:6380> info replication # Replication role:slave # 角色已经是从机了 master_host:127.0.0.1 # 主节点地址 master_port:6379 # 主节点端口 master_link_status:up master_last_io_seconds_ago:6 master_sync_in_progress:0 slave_repl_offset:0 slave_priority:100 slave_read_only:1 connected_slaves:0 master_replid:907bcdf00c69d361ede43f4f6181004e2148efb7 master_replid2:0000000000000000000000000000000000000000 master_repl_offset:0 second_repl_offset:-1 repl_backlog_active:1 repl_backlog_size:1048576 repl_backlog_first_byte_offset:1 repl_backlog_histlen:0 127.0.0.1:6380> 配置好了之后,看主机:127.0.0.1:6379> info replication # Replication role:master connected_slaves:2 # 主节点下有两个从节点 slave0:ip=127.0.0.1,port=6380,state=online,offset=420,lag=1 slave1:ip=127.0.0.1,port=6381,state=online,offset=420,lag=1 master_replid:907bcdf00c69d361ede43f4f6181004e2148efb7 master_replid2:0000000000000000000000000000000000000000 master_repl_offset:420 second_repl_offset:-1 repl_backlog_active:1 repl_backlog_size:1048576 repl_backlog_first_byte_offset:1 repl_backlog_histlen:420 127.0.0.1:6379> 真实的主从配置应该是在配置文件中配置,这样才是永久的。这里使用命令是暂时的。配置文件 redis.conf################################# REPLICATION ################################# # Master-Replica replication. Use replicaof to make a Redis instance a copy of # another Redis server. A few things to understand ASAP about Redis replication. # # +------------------+ +---------------+ # | Master | ---> | Replica | # | (receive writes) | | (exact copy) | # +------------------+ +---------------+ # # 1) Redis replication is asynchronous, but you can configure a master to # stop accepting writes if it appears to be not connected with at least # a given number of replicas. # 2) Redis replicas are able to perform a partial resynchronization with the # master if the replication link is lost for a relatively small amount of # time. You may want to configure the replication backlog size (see the next # sections of this file) with a sensible value depending on your needs. # 3) Replication is automatic and does not need user intervention. After a # network partition replicas automatically try to reconnect to masters # and resynchronize with them. # # replicaof <masterip> <masterport> # 这里配置 # If the master is password protected (using the "requirepass" configuration # directive below) it is possible to tell the replica to authenticate before # starting the replication synchronization process, otherwise the master will # refuse the replica request. # # masterauth <master-password>配置方式也是一样的。几个问题1、主机可以写,从机不能写只能读。主机中的所有信息和数据都会保存在从机中。如果从机尝试进行写操作就会报错。127.0.0.1:6381> get k1 # k1的值是在主机中写入的,从机中可以读取到。"v1"127.0.0.1:6381> set k2 v2 # 从机尝试写操作,报错了(error) READONLY You can't write against a read only replica.127.0.0.1:6381> 2、如果主机断开了,从机依然链接到主机,可以进行读操作,但是还是没有写操作。这个时候,主机如果恢复了,从机依然可以直接从主机同步信息。3、使用命令行配置的主从机,如果从机重启了,就会变回主机。如果再通过命令变回从机的话,立马就可以从主机中获取值。这是复制原理决定的。复制的两种模式Slave 启动成功连接到 Master 后会发送一个 sync 同步命令。 Master 接收到命令后,启动后台的存盘进程,同时收集所有接收到的用于修改数据集的命令,在后台进程执行完毕后,master 将传送整个数据文件到 slave ,并完成一次完全同步。但是只要重新连接 master ,一次完全同步(全量复制)将被自动执行。我们的数据一定可以在从机中看到。这种模式的原理图:第二种模式这种模式的话,将 6381 的主节点配置为 6380 。主节点 6379 只有一个从机。如果现在 6379 节点宕机了, 6380 和 6381 节点都是从节点,只能进行读操作,都不会自动变为主节点。需要手动将其中一个变为主节点,使用如下命令:SLAVEOF no oneredis主从复制危险操作使用热更新配置误操作redis主从复制如果使用热更新配置,有时候会因为选错主机把从库误认为主库,结果在主库上执行了 slaveof ,这样就会导致主库上的数据被清空,因为从库上是没有数据的。从库在同步主库的时候会把原本自己的数据全部清空。误操作过程#从库数据为0 127.0.0.1:6379> DBSIZE (integer) 0 #主库数据为2001 127.0.0.1:6379> DBSIZE (integer) 2001 #在主库上操作同步本该从库的数据 127.0.0.1:6379> SLAVEOF 192.168.81.220 6379 OK #再次查看数据,数据已经清空 127.0.0.1:6379> DBSIZE (integer) 0避免热更新配置误操作1.不使用热更新,直接在配置文件里配置主从。2.在执行slaveof的时候先执行bgsave,把数据手动备份一下,然后在数据目录,将rdb文件复制成另一个文件,做备份,这样即使出现问题也能即使恢复。bgsave之后不用重启,直接备份rdb文件即可。#手动持久化 127.0.0.1:6379> BGSAVE Background saving started #备份rdb文件 [root@redis-1 ~]# cd /data/redis_cluster/redis_6379/ [root@redis-1 /data/redis_cluster/redis_6379]# cd data/ [root@redis-1 /data/redis_cluster/redis_6379/data]# cp redis_6379.rdb redis_6379.rdb.bak #再次同步错误的主库,造成数据丢失 127.0.0.1:6379> SLAVEOF 192.168.81.220 6379 OK 127.0.0.1:6379> keys * (empty list or set) #还原备份的rdb文件,先停掉redis,在还原 [root@redis-1 ~]# redis-cli shutdown [root@redis-1 /data/redis_cluster/redis_6379/data]# cp redis_6379.rdb.bak redis_6379.rdb #查看还原后的数据 [root@redis-1 ~]# redis-server /data/redis_cluster/redis_6379/conf/redis_6379.conf 127.0.0.1:6379> DBSIZE (integer) 2001模拟 redis 主从复制错误数据恢复模拟 redis 主从同步误操作数据恢复。大致思路1.清空两台redis的数据2.在主库上创建一些数据,然后使用bgsave命令,将数据保存到磁盘,再将磁盘的rdb文件备份3.再将从库的数据同步过来,模拟主库数据丢失4.从rdb备份文件还原数据库这个实验的主要的目的是操作redis备份还原。清空数据两台redis都需要操作,先关闭再删除数据再启动。[root@redis-1 ~]# redis-cli shutdown [root@redis-1 ~]# rm -rf /data/redis_cluster/redis_6379/data/* [root@redis-1 ~]# redis-server /data/redis_cluster/redis_6379/conf/redis_6379.conf [root@redis-1 ~]# redis-cli 127.0.0.1:6379> keys * (empty list or set)在主库批量创建数据并备份#批量创建数据 [root@redis-1 ~]# for i in {0..2000}; do redis-cli set k_${i} v_${i}; echo "k_${i} is ok"; done 127.0.0.1:6379> DBSIZE (integer) 2001 #将近数据写入到 127.0.0.1:6379> BGSAVE Background saving started #备份rdb文件 [root@redis-1 /data/redis_cluster/redis_6379/data]# cp redis_6379.rdb redis_6379.rdb.bak [root@redis-1 /data/redis_cluster/redis_6379/data]# ll 总用量 56 -rw-r--r--. 1 root root 27877 1月 28 21:00 redis_6379.rdb -rw-r--r--. 1 root root 27877 1月 28 21:01 redis_6379.rdb.bak同步从库的数据造成数据丢失这时从库的数据应该是空的。[root@redis-2 ~]# redis-cli 127.0.0.1:6379> keys * (empty list or set)主库同步从库的数据,同步完主库数据丢失,这样就模拟了主库数据丢失的情况。127.0.0.1:6379> SLAVEOF 192.168.81.220 6379 OK 127.0.0.1:6379> keys * (empty list or set)恢复主库的数据先关掉redis,还原,最后在开启redis。#关掉redis [root@redis-1 ~]# redis-cli shutdown #还原rdb备份文件 [root@redis-1 /data/redis_cluster/redis_6379/data]# cp redis_6379.rdb.bak redis_6379.rdb cp:是否覆盖"redis_6379.rdb"?y #启动redis [root@redis-1 ~]# redis-server /data/redis_cluster/redis_6379/conf/redis_6379.conf #查看数据是否还原 [root@redis-1 ~]# redis-cli 127.0.0.1:6379> keys *模拟线上环境主库宕机故障恢复思路1.首先保证主从同步已经配置完成,主库从库都有数据2.关闭redis主库,模拟redis主库宕机3.查看redis从库是否还存在数据,是否可读可写(不能写,只能读)4.关闭从库的slaveof,停止主从同步,将应用连接redis的地址改成从库,保证业务不断5.修复主库,主库上线后,与从库建立主从复制关系,原来的从库(redis-2)就变成了主库,现在的主库变成了从库(redis-1)这时 关掉应用程序,停止数据写入6.然后将现在主库(redis-2)的数据同步到现在的从库(redis-1)7.关闭现在从库(redis-1)的slaveof,停止主从复制,然后将现在主库(redis-2)配置salveof,同步原来的主库(redis-1)8.数据同步完,原来的主库从库就恢复完毕了简单明了的一句话: 主库因为某种原因宕机无法提供服务了,直接将从库切换成主库提供服务,然后后原来的主库恢复后同步当前主库的数据,然后停掉所有线上运行的程序,将现在的主库去同步恢复后的主库,重新生成主从关系。配置主从模拟线上环境配置主从前先保证主库上面有数据#登陆主库redis-1查看是否有数据 [root@redis-1 ~]# redis-cli 127.0.0.1:6379> DBSIZE (integer) 2001 #登陆从库redis-2查看是否有数据 [root@redis-2 ~]# redis-cli 127.0.0.1:6379> keys * (empty list or set) #从库没有数据的情况下在从库上配置主从,同步主库的数据 127.0.0.1:6379> SLAVEOF 192.168.81.210 6379 OK #数据已经同步 127.0.0.1:6379> DBSIZE (integer) 2001模拟主库宕机验证从库是否可读写#直接关掉主库,造成宕机 [root@redis-1 ~]# redis-cli shutdown #查看从库是否可读写 只能读,不能写 [root@redis-2 ~]# redis-cli get k_1 "v_1" [root@redis-2 ~]# redis-cli set k999 k_1 (error) READONLY You can't write against a read only slave.主库一宕机,从库就会一直输出日志提示连接不上主库关闭从库的主从复制保证业务的不间断现在主库是不可用的,从库也只能读不能写,但是数据只有这么一份了,我们只能关闭从库上的主从复制,让从库变成主库,再配置业务的redis地址,首先要保证业务的不中断#关闭从库redis-2的主从同步配置,使其成为主库 [root@redis-2 ~]# redis-cli slaveof no one OK #将应用的redis地址修改为从库,只要从库关掉了主从配置,他自己就是一个可读可写的库了,库里有故障前的所有数据,可以先保证业务的不间断修复故障的主库同步原来从库的数据修复完主库,已经是有数据的了,为什么还要同步从库的数据呢,因为在主库挂掉的那一瞬间,从库去掉了主从配置,自己已经成了主库,并且也提供了一段时间的数据写入,这时从库的数据时最完整的。同步现在主库(原来的从库)的数据时,先要将应用关掉,不要在往里写数据了在主库(原来的从库上)写入几个新的数据,模拟产生的新数据[root@redis-2 ~]# redis-cli 127.0.0.1:6379> set zuixinshujv vvvvv OK在重新上线的主库上配置主从同步,使自己变成从库,同步主库的(原来的从库)数据同步之后,现在从库(重新修复的主库已经有最新数据了)同步之前先将应用停掉,不要再往redis中写数据[root@redis-1 ~]# redis-cli 127.0.0.1:6379> SLAVEOF 192.168.81.220 6379 OK 127.0.0.1:6379> get zuixinshujv "vvvvv"从库重新上线为主库这里的从库重新上线指的就是原来故障的主库,现在已经同步到最新数据了,因此要上线成为主库,之前选他作为主库就是因为他的性能比从库各方面都要高,避免将来因为性能再次发生故障,因此要切换#关闭从库(原来的主库)的主从配置 [root@redis-1 ~]# redis-cli 127.0.0.1:6379> SLAVEOF no one OK #在主库(原来的从库)配置主从复制 [root@redis-2 ~]# redis-cli 127.0.0.1:6379> SLAVEOF 192.168.81.210 6379 OK将应用的redis地址再次修改为主库的地址目前主库已经恢复了,并且主从之前重新建立了主从同步关系,现在就可以把应用的redis地址修改为主库,启动应用就可以了。更深入理解 Redis 主从复制我们通过几个问题来深入理解主从复制。当主服务器不进行持久化时复制的安全性 在进行主从复制设置时,强烈建议在主服务器上开启持久化,当不能这么做时,比如考虑到延迟的问题,应该将实例配置为避免自动重启。为什么不持久化的主服务器自动重启非常危险呢?为了更好的理解这个问题,看下面这个失败的例子,其中主服务器和从服务器中数据库都被删除了。我们设置节点A为主服务器,关闭持久化,节点B和C从节点A复制数据。这时出现了一个崩溃,但Redis具有自动重启系统,重启了进程,因为关闭了持久化,节点重启后只有一个空的数据集。节点B和C从节点A进行复制,现在节点A是空的,所以节点B和C上的复制数据也会被删除。当在高可用系统中使用Redis Sentinel,关闭了主服务器的持久化,并且允许自动重启,这种情况是很危险的。比如主服务器可能在很短的时间就完成了重启,以至于Sentinel都无法检测到这次失败,那么上面说的这种失败的情况就发生了。如果数据比较重要,并且在使用主从复制时关闭了主服务器持久化功能的场景中,都应该禁止实例自动重启。为什么全量复制使用RDB而不使用AOF?1、RDB文件内容是经过压缩的二进制数据(不同数据类型数据做了针对性优化),文件很小。而AOF文件记录的是每一次写操作的命令,写操作越多文件会变得很大,其中还包括很多对同一个key的多次冗余操作。在主从全量数据同步时,传输RDB文件可以尽量降低对主库机器网络带宽的消耗,从库在加载RDB文件时,一是文件小,读取整个文件的速度会很快,二是因为RDB文件存储的都是二进制数据,从库直接按照RDB协议解析还原数据即可,速度会非常快,而AOF需要依次重放每个写命令,这个过程会经历冗长的处理逻辑,恢复速度相比RDB会慢得多,所以使用RDB进行主从全量复制的成本最低。2、假设要使用AOF做全量复制,意味着必须打开AOF功能,打开AOF就要选择文件刷盘的策略,选择不当会严重影响Redis性能。而RDB只有在需要定时备份和主从全量复制数据时才会触发生成一次快照。而在很多丢失数据不敏感的业务场景,其实是不需要开启AOF的。为什么还有无磁盘复制模式?Redis 默认是磁盘复制,但是如果使用比较低速的磁盘,这种操作会给主服务器带来较大的压力。Redis从2.8.18版本开始尝试支持无磁盘的复制。使用这种设置时,子进程直接将RDB通过网络发送给从服务器,不使用磁盘作为中间存储。无磁盘复制模式:master 创建一个新进程直接 dump RDB 到slave的socket,不经过主进程,不经过硬盘。适用于disk较慢,并且网络较快的时候。使用repl-diskless-sync配置参数来启动无磁盘复制。使用repl-diskless-sync-delay 参数来配置传输开始的延迟时间;master等待一个repl-diskless-sync-delay的秒数,如果没slave来的话,就直接传,后来的得排队等了; 否则就可以一起传。为什么还会有从库的从库的设计?通过分析主从库间第一次数据同步的过程,你可以看到,一次全量复制中,对于主库来说,需要完成两个耗时的操作:生成 RDB 文件和传输 RDB 文件。如果从库数量很多,而且都要和主库进行全量复制的话,就会导致主库忙于 fork 子进程生成 RDB 文件,进行数据全量复制。fork 这个操作会阻塞主线程处理正常请求,从而导致主库响应应用程序的请求速度变慢。此外,传输 RDB 文件也会占用主库的网络带宽,同样会给主库的资源使用带来压力。那么,有没有好的解决方法可以分担主库压力呢?其实是有的,这就是“ 主 - 从 - 从” 模式。在刚才介绍的主从库模式中,所有的从库都是和主库连接,所有的全量复制也都是和主库进行的。现在,我们可以通过“主 - 从 - 从”模式将主库生成 RDB 和传输 RDB 的压力,以级联的方式分散到从库上。简单来说,我们在部署主从集群的时候,可以手动选择一个从库(比如选择内存资源配置较高的从库),用于级联其他的从库。然后,我们可以再选择一些从库(例如三分之一的从库),在这些从库上执行如下命令,让它们和刚才所选的从库,建立起主从关系。replicaof 所选从库的IP 6379这样一来,这些从库就会知道,在进行同步时,不用再和主库进行交互了,只要和级联的从库进行写操作同步就行了,这就可以减轻主库上的压力,如下图所示:级联的“主-从-从”模式好了,到这里,我们了解了主从库间通过全量复制实现数据同步的过程,以及通过“主 - 从 - 从”模式分担主库压力的方式。那么,一旦主从库完成了全量复制,它们之间就会一直维护一个网络连接,主库会通过这个连接将后续陆续收到的命令操作再同步给从库,这个过程也称为基于长连接的命令传播,可以避免频繁建立连接的开销。读写分离及其中的问题在主从复制基础上实现的读写分离,可以实现Redis的读负载均衡:由主节点提供写服务,由一个或多个从节点提供读服务(多个从节点既可以提高数据冗余程度,也可以最大化读负载能力);在读负载较大的应用场景下,可以大大提高Redis服务器的并发量。下面介绍在使用Redis读写分离时,需要注意的问题。延迟与不一致问题前面已经讲到,由于主从复制的命令传播是异步的,延迟与数据的不一致不可避免。如果应用对数据不一致的接受程度程度较低,可能的优化措施包括:优化主从节点之间的网络环境(如在同机房部署);监控主从节点延迟(通过offset)判断,如果从节点延迟过大,通知应用不再通过该从节点读取数据;使用集群同时扩展写负载和读负载等。在命令传播阶段以外的其他情况下,从节点的数据不一致可能更加严重,例如连接在数据同步阶段,或从节点失去与主节点的连接时等。从节点的slave-serve-stale-data参数便与此有关:它控制这种情况下从节点的表现;如果为yes(默认值),则从节点仍能够响应客户端的命令,如果为no,则从节点只能响应info、slaveof等少数命令。该参数的设置与应用对数据一致性的要求有关;如果对数据一致性要求很高,则应设置为no。数据过期问题在单机版Redis中,存在两种删除策略:惰性删除 :服务器不会主动删除数据,只有当客户端查询某个数据时,服务器判断该数据是否过期,如果过期则删除。 定期删除 :服务器执行定时任务删除过期数据,但是考虑到内存和CPU的折中(删除会释放内存,但是频繁的删除操作对CPU不友好),该删除的频率和执行时间都受到了限制。在主从复制场景下,为了主从节点的数据一致性,从节点不会主动删除数据,而是由主节点控制从节点中过期数据的删除。由于主节点的惰性删除和定期删除策略,都不能保证主节点及时对过期数据执行删除操作,因此,当客户端通过Redis从节点读取数据时,很容易读取到已经过期的数据。Redis 3.2中,从节点在读取数据时,增加了对数据是否过期的判断:如果该数据已过期,则不返回给客户端;将Redis升级到3.2可以解决数据过期问题。故障切换问题在没有使用哨兵的读写分离场景下,应用针对读和写分别连接不同的Redis节点;当主节点或从节点出现问题而发生更改时,需要及时修改应用程序读写Redis数据的连接;连接的切换可以手动进行,或者自己写监控程序进行切换,但前者响应慢、容易出错,后者实现复杂,成本都不算低。总结在使用读写分离之前,可以考虑其他方法增加Redis的读负载能力:如尽量优化主节点(减少慢查询、减少持久化等其他情况带来的阻塞等)提高负载能力;使用Redis集群同时提高读负载能力和写负载能力等。如果使用读写分离,可以使用哨兵,使主从节点的故障切换尽可能自动化,并减少对应用程序的侵入。参考文章:https://jiangxl.blog.csdn.net/article/details/120646992 https://www.cnblogs.com/itzhouq/p/redis5.htmlhttps://www.pdai.tech/md/db/nosql-redis/db-redis-x-copy.html(九):Redis sentinel 集群原理部署及数据恢复在上文主从复制的基础上,如果主节点出现故障该怎么办呢?在 Redis 主从集群中,哨兵机制是实现主从库自动切换的关键机制,它有效地解决了主从复制模式下故障转移的问题。哨兵机制(Redis Sentinel)Redis Sentinel,即 Redis 哨兵,在 Redis 2.8 版本开始引入。哨兵的核心功能是主节点的自动故障转移。下图是一个典型的哨兵集群监控的逻辑图:哨兵实现了什么功能呢?下面是Redis官方文档的描述:监控(Monitoring) :哨兵会不断地检查主节点和从节点是否运作正常。自动故障转移(Automatic failover) :当主节点不能正常工作时,哨兵会开始自动故障转移操作,它会将失效主节点的其中一个从节点升级为新的主节点,并让其他从节点改为复制新的主节点。配置提供者(Configuration provider) :客户端在初始化时,通过连接哨兵来获得当前Redis服务的主节点地址。通知(Notification) :哨兵可以将故障转移的结果发送给客户端。其中,监控和自动故障转移功能,使得哨兵可以及时发现主节点故障并完成转移;而配置提供者和通知功能,则需要在与客户端的交互中才能体现。哨兵集群的组建上图中哨兵集群是如何组件的呢?哨兵实例之间可以相互发现,要归功于 Redis 提供的 pub/sub 机制,也就是 发布/订阅 机制。在主从集群中,主库上有一个名为 __sentinel__:hello 的频道,不同哨兵就是通过它来相互发现,实现互相通信的。在下图中,哨兵 1 把自己的 IP(172.16.19.3)和端口(26579)发布到 __sentinel__:hello 频道上,哨兵 2 和 3 订阅了该频道。那么此时,哨兵 2 和 3 就可以从这个频道直接获取哨兵 1 的 IP 地址和端口号。然后,哨兵 2、3 可以和哨兵 1 建立网络连接。通过这个方式,哨兵 2 和 3 也可以建立网络连接,这样一来,哨兵集群就形成了。它们相互间可以通过网络连接进行通信,比如说对主库有没有下线这件事儿进行判断和协商。哨兵监控 Redis 库哨兵监控什么呢?怎么监控呢? 这是由哨兵向主库发送 INFO 命令来完成的。就像下图所示,哨兵 2 给主库发送 INFO 命令,主库接受到这个命令后,就会把从库列表返回给哨兵。接着,哨兵就可以根据从库列表中的连接信息,和每个从库建立连接,并在这个连接上持续地对从库进行监控。哨兵 1 和 3 可以通过相同的方法和从库建立连接。主库下线的判定哨兵如何判断主库已经下线了呢?首先要理解两个概念: 主观下线和客观下线主观下线:任何一个哨兵都是可以监控探测,并作出Redis节点下线的判断;客观下线:有哨兵集群共同决定Redis节点是否下线;当某个哨兵(如下图中的哨兵2)判断主库“主观下线”后,就会给其他哨兵发送 is-master-down-by-addr 命令。接着,其他哨兵会根据自己和主库的连接情况,做出 Y 或 N 的响应,Y 相当于赞成票,N 相当于反对票。如果赞成票数(这里是2)是大于等于哨兵配置文件中的 quorum 配置项(比如这里如果是quorum=2), 则可以判定主库客观下线了。哨兵集群的选举判断完主库下线后,由哪个哨兵节点来执行主从切换呢?这里就需要哨兵集群的选举机制了。为什么必然会出现选举/共识机制?为了避免哨兵的单点情况发生,所以需要一个哨兵的分布式集群。作为分布式集群,必然涉及共识问题(即选举问题);同时故障的转移和通知都只需要一个主的哨兵节点就可以了。哨兵的选举机制是什么样的?哨兵的选举机制其实很简单,就是一个 Raft 选举算法: 选举的票数大于等于num(sentinels)/2+1时,将成为领导者,如果没有超过,继续选举 任何一个想成为 Leader 的哨兵,要满足两个条件: 第一,拿到半数以上的赞成票;第二,拿到的票数同时还需要大于等于哨兵配置文件中的 quorum 值。以 3 个哨兵为例,假设此时的 quorum 设置为 2,那么,任何一个想成为 Leader 的哨兵只要拿到 2 张赞成票,就可以了。更进一步理解这里很多人会搞混 判定客观下线 和 是否能够主从切换(用到选举机制) 两个概念,我们再看一个例子。Redis 1主4从,5个哨兵,哨兵配置quorum为2,如果3个哨兵故障,当主库宕机时,哨兵能否判断主库“客观下线”?能否自动切换?经过实际测试:1、哨兵集群可以判定主库“主观下线”。 由于quorum=2,所以当一个哨兵判断主库“主观下线”后,询问另外一个哨兵后也会得到同样的结果,2个哨兵都判定“主观下线”,达到了quorum的值,因此,哨兵集群可以判定主库为“客观下线”。2、但哨兵不能完成主从切换。 哨兵标记主库“客观下线后”,在选举“哨兵领导者”时,一个哨兵必须拿到超过多数的选票(5/2+1=3票)。但目前只有2个哨兵活着,无论怎么投票,一个哨兵最多只能拿到2票,永远无法达到N/2+1选票的结果。新主库的选出主库既然判定客观下线了,那么如何从剩余的从库中选择一个新的主库呢?过滤掉不健康的(下线或断线),没有回复过哨兵ping响应的从节点选择salve-priority从节点优先级最高(redis.conf)的选择复制偏移量最大,只复制最完整的从节点故障的转移新的主库选择出来后,就可以开始进行故障的转移了。假设根据我们一开始的图:(我们假设:判断主库客观下线了,同时选出sentinel 3是哨兵leader)故障转移流程如下将slave-1脱离原从节点(PS: 5.0 中应该是replicaof no one),升级主节点,将从节点slave-2指向新的主节点通知客户端主节点已更换将原主节点(oldMaster)变成从节点,指向新的主节点转移之后搭建redis哨兵集群环境准备配置哨兵集群步骤:1.在所有节点搭建redis2.配置主从复制,一主两从3.在所有节点配置sentinel,启动sentinel后,配置文件会自动增加在所有机器上部署redis192.168.81.210配置1.创建redis部署路径 [root@redis-1 ~]# mkdir -p /data/redis_cluster/redis_6379/{conf,pid,logs,data} 2.下载redis [root@redis-1 ~]# mkdir /data/soft [root@redis-1 ~]# cd /data/soft [root@redis-1 /data/soft]# wget https://repo.huaweicloud.com/redis/redis-3.2.9.tar.gz 3.便于安装redis [root@redis-1 /data/soft]# tar xf redis-3.2.9.tar.gz -C /data/redis_cluster/ [root@redis-1 /data/soft]# cd /data/redis_cluster/ [root@redis-1 /data/redis_cluster]# ln -s redis-3.2.9/ redis [root@redis-1 /data/redis_cluster]# cd redis/src [root@redis-1 /data/redis_cluster/redis]# make && make install 4.准备配置文件 [root@redis-1 ~]# vim /data/redis_cluster/redis_6379/conf/redis_6379.conf daemonize yes bind 192.168.81.210 127.0.0.1 port 6379 pidfile /data/redis_cluster/redis_6379/pid/redis_6379.pid logfile /data/redis_cluster/redis_6379/logs/redis_6379.log databases 16 dbfilename redis_6379.rdb dir /data/redis_cluster/redis_6379/data/ save 900 1 save 300 100 save 60 10000 5.启动redis [root@redis-1 ~]# redis-server /data/redis_cluster/redis_6379/conf/redis_6379.conf 192.168.81.220配置由于redis-1已经部署好了一套redis,我们可以直接复制过来使用1.使用rsync将redis-1的redis目录拷贝过来你 [root@redis-1 ~]# rsync -avz /data root@192.168.81.220:/ 2.查看拷贝过来的目录文件 [root@redis-2 ~]# ls /data/redis_cluster/ redis redis-3.2.9 redis_6379 [root@redis-2 ~]# ls /data/redis_cluster/redis_6379/ conf data logs pid 3.编译安装redis,使系统能使用redis命令 直接执行make install即可,因为编译步骤在redis-1已经做了 [root@redis-2 ~]# cd /data/redis_cluster/redis-3.2.9/ [root@redis-2 /data/redis_cluster/redis-3.2.9]# make install 4.修改redis配置文件 [root@redis-2 ~]# vim /data/redis_cluster/redis_6379/conf/redis_6379.conf bind 192.168.81.220 127.0.0.1 5.启动redis [root@redis-2 ~]# redis-server /data/redis_cluster/redis_6379/conf/redis_6379.conf 192.168.81.230配置 由于redis-1已经部署好了一套redis,我们可以直接复制过来使用 1.使用rsync将redis-1的redis目录拷贝过来你 [root@redis-1 ~]# rsync -avz /data root@192.168.81.230:/ 2.查看拷贝过来的目录文件 [root@redis-3 ~]# ls /data/redis_cluster/ redis redis-3.2.9 redis_6379 [root@redis-3 ~]# ls /data/redis_cluster/redis_6379/ conf data logs pid 3.编译安装redis,使系统能使用redis命令 直接执行make install即可,因为编译步骤在redis-1已经做了 [root@redis-3 ~]# cd /data/redis_cluster/redis-3.2.9/ [root@redis-3 /data/redis_cluster/redis-3.2.9]# make install 4.修改redis配置文件 [root@redis-3 ~]# vim /data/redis_cluster/redis_6379/conf/redis_6379.conf bind 192.168.81.230 127.0.0.1 5.启动redis [root@redis-3 ~]# redis-server /data/redis_cluster/redis_6379/conf/redis_6379.conf三台redis部署完成[root@redis-1 ~]# ps aux | grep redis root 21860 0.1 0.5 139020 9740 ? Ssl 09:36 0:16 redis-server 192.168.81.210:6379 root 25296 0.0 0.0 112724 984 pts/0 S+ 13:15 0:00 grep --color=auto redis [root@redis-1 ~]# ssh 192.168.81.220 "ps aux | grep redis" root 47658 0.1 0.5 141068 10780 ? Ssl 1月28 1:24 redis-server 192.168.81.220:6379 root 63254 0.0 0.0 113176 1588 ? Ss 13:15 0:00 bash -c ps aux | grep redis root 63271 0.0 0.0 112724 968 ? S 13:15 0:00 grep redis [root@redis-1 ~]# ssh 192.168.81.230 "ps aux | grep redis" root 56584 0.1 0.7 136972 7548 ? Ssl 13:13 0:00 redis-server 192.168.81.230:6379 root 56644 0.0 0.1 113176 1588 ? Ss 13:15 0:00 bash -c ps aux | grep redis root 56661 0.0 0.0 112724 968 ? S 13:15 0:00 grep redis配置redis主从要在两台slave上同步主库配置1.配置主从复制 [root@redis-2 ~]# redis-cli 127.0.0.1:6379> SLAVEOF 192.168.81.210 6379 OK [root@redis-3 ~]# redis-cli 127.0.0.1:6379> SLAVEOF 192.168.81.220 6379 OK 2.主库新建一个key 127.0.0.1:6379> set name jiangxl OK 3.从库查看是否复制 [root@redis-2 ~]# redis-cli 127.0.0.1:6379> get name "jiangxl" [root@redis-3 ~]# redis-cli 127.0.0.1:6379> get name "jiangxl"部署哨兵进程sentinel配置文件解释sentinel monitor mymaster 192.168.81.210 6379 2 //设置主节点信息,mymaster是主节点别名,就是随便起一个名字,然后填写主节点的ip地址,2表示当主节点挂掉后,有2个sentinel同意后才会选举新的master,一组哨兵集群,要把名称都写成一样的 sentinel down-after-milliseconds mymaster 3000 //主库宕机多少秒,从库在进行切换,因为有时因为网络波动,如果只要主库一宕机就切换主从,那么redis可能一直处于正在切换状态 sentinel parallel-syncs mymaster 1 //允许几个节点同时向主库同步数据 sentinel failover-timeout mymaster 18000 //故障转移超时时间,当从库同步主库的rdb文件,多长时间没有同步完就认为超时三台redis服务器都要按如下配置,已经将配置文件中的bind写成了系统变量,在配合cat写入到文件,因此直接执行如下命令即可1.创建哨兵服务配置路径 mkdir -p /data/redis_cluster/redis_26379/{conf,data,pid,logs} 2.写入哨兵配置文件 cat > /data/redis_cluster/redis_26379/conf/redis_26379.conf <<EOF bind $(ifconfig | awk 'NR==2{print $2}') port 26379 daemonize yes logfile /data/redis_cluster/redis_26379/logs/redis_26379.log dir /data/redis_cluster/redis_26379/data sentinel monitor mymaster 192.168.81.210 6379 2 sentinel down-after-milliseconds mymaster 3000 sentinel parallel-syncs mymaster 1 sentinel failover-timeout mymaster 18000 EOF配置完记得查看下配置文件bind一列是否是各自主机的ip地址启动哨兵观察配置文件的变化三台机器都这么操作启动哨兵redis-sentinel /data/redis_cluster/redis_26379/conf/redis_26379.conf观察哨兵启动前后配置文件的变化启动前启动后每台哨兵主机都自动增加了一个myid的配置,这个就是当主库挂掉后,哨兵选举的依据, 判断谁的myid大谁就当选为主库。 每台哨兵主机还自动增加了sentinel known-sentinel这个配置,这个配置每个哨兵会记录集群中其他节点的id号,这样就能够实现信息共享,即使应用在询问哨兵进程谁是主库,这时由于每个哨兵进程都有其他节点的信息,因此就能里面告诉应用谁是主库。模拟主库故障验证应用是否可用配置完哨兵后,每个节点上都有集群的信息共享,当主库挂掉后,哨兵进程确认主库下线了,哨兵根据各自的id大小选举新的主库,接替主库的工作,保证应用程序不受影响,当主库修复好后,在通过提权的方式先同步目前主库的数据,在让自身成为主库。#关闭主库的redis服务,reids正常关闭,sentinel直接kill [root@redis-1 ~]# redis-cli shutdown [root@redis-1 ~]# pkill redis #查看配置文件看看谁的myid大 redis-2的myid比较大 [root@redis-1 ~]# grep 'known-sentinel' /data/redis_cluster/redis_26379/conf/redis_26379.conf sentinel known-sentinel mymaster 192.168.81.220 26379 df44bb3e9fdf8c635628b1ae724b2db7d3ef144c sentinel known-sentinel mymaster 192.168.81.230 26379 de282d14bb0a79df90603eb92243cd1f362dd46d #测试redis-2是否可用写入数据 可以写入数据,redis-2被选为主库 [root@redis-1 ~]# redis-cli -h 192.168.81.220 set gzzy_test guzhangzhuanyi OK [root@redis-1 ~]# redis-cli -h 192.168.81.220 config get slaveof 1) "slaveof" 2) "" #测试redis-3是否可用写入数据 写入数据失败,并且同步的是redis-2的数据,因此redis-2为主库 [root@redis-1 ~]# redis-cli -h 192.168.81.230 set kkkk111 vvv (error) READONLY You can't write against a read only slave. [root@redis-1 ~]# redis-cli -h 192.168.81.230 config get slaveof 1) "slaveof" 2) "192.168.81.220 6379"主库挂掉其他节点配置文件的变化主库挂掉后,其他两个节点选举出master后,配置文件也会填写为新master的地址。至此,一个 Redis 哨兵集群架构说部署完成了。Redis 哨兵集群主库故障数据恢复实践当主库修复后重新上线首先通过哨兵知道谁是当前的主库,然后就会去找主库同步数据,并且会自动修改配置文件,当数据同步后,想恢复的主库重新成为主库则需要把主库的权重调高,然后重新选举,这时原来的主库就能成为新的主库,调整完再将主库的权重值调成默认的。实现思路1.将故障的主库重新恢复2.查看当前的主从状态,验证由于主库宕机,与从库产生的数据是否同步3.调整权重值4.重新选举,使原来的主库变成新的主库5.恢复的主库重新成为新的主库后,要把调整的权重值全部变成默认值主库可以重新加入哨兵集群的前提:剩余的两个节点必须有一个是master,且这两个节点配置文件已经指定了新的master地址恢复损坏的主库#恢复主库 [root@redis-1 ~]# redis-server /data/redis_cluster/redis_6379/conf/redis_6379.conf [root@redis-1 ~]# [root@redis-1 ~]# redis-sentinel /data/redis_cluster/redis_26379/conf/redis_26379.conf #查看其他两个节点的日志输出,任意一个节点都会输出,表示redis-1已经加入集群了 tail -f /data/redis_cluster/redis_26379/logs/redis_26379.log 78223:X 30 Jan 12:05:09.073 # -sdown sentinel ac621a57296db0cead07751a4f0a19c570daa7f9 192.168.81.210 26379 @ mymaster 192.168.81.220 6379,查看恢复的主库redis-1配置文件[root@redis-1 ~]# cat /data/redis_cluster/redis_26379/conf/redis_26379.conf bind 192.168.81.210 port 26379 daemonize yes logfile "/data/redis_cluster/redis_26379/logs/redis_26379.log" dir "/data/redis_cluster/redis_26379/data" sentinel myid ac621a57296db0cead07751a4f0a19c570daa7f9 sentinel monitor mymaster 192.168.81.220 6379 2可以看到已经自动修改为当前库的地址查看恢复的主库redis-1的主从关系#已经同步了当前主库redis-2 [root@redis-1 ~]# redis-cli 127.0.0.1:6379> CONFIG GET slaveof 1) "slaveof" 2) "192.168.81.220 6379" #已经可以看到主库宕机阶段,从库变为主库产生的最新数据 127.0.0.1:6379> get gzzy_test "guzhangzhuanyi"配置恢复的主库的权重值,使其重新选举为主库哨兵的选举首先是查看谁的权重优先级比较高的当选为主库权重优先级一致,就比较id,id大的当选#查看其他两个节点的权重值 [root@redis-1 ~]# redis-cli -h 192.168.81.220 -p 6379 config get slave-priority 1) "slave-priority" 2) "100" [root@redis-1 ~]# redis-cli -h 192.168.81.230 -p 6379 config get slave-priority 1) "slave-priority" 2) "100" #将其他两个节点的权重值改为0 [root@redis-1 ~]# redis-cli -h 192.168.81.220 -p 6379 config set slave-priority 0 OK [root@redis-1 ~]# redis-cli -h 192.168.81.230 -p 6379 config set slave-priority 0 OK #设置恢复的主库的权限优先级高于其他两个节点 [root@redis-1 ~]# redis-cli -h 192.168.81.210 -p 6379 config set slave-priority 150 OK [root@redis-1 ~]# redis-cli -h 192.168.81.210 -p 6379 config get slave-priority 1) "slave-priority" 2) "150" #重新选举 [root@redis-1 ~]# redis-cli -h 192.168.81.210 -p 26379 sentinel failover mymaster #查看其他节点sentinel输出的日志 [root@redis-3 ~]# tail -f /data/redis_cluster/redis_26379/logs/redis_26379.log 78223:X 30 Jan 12:32:27.591 * +convert-to-slave slave 192.168.81.220:6379 192.168.81.220 6379 @ mymaster 192.168.81.210 6379根据日志的输出,可以明显的看出调整了redis-1的权重优先级为150,比其他两个节点的高,因此redis-1就变成了主库。查看节点的主从复制关系。主库没有同步的库,其他两个节点都同步redis-1的主库。[root@redis-1 ~]# redis-cli -h 192.168.81.210 -p 6379 config get slaveof 1) "slaveof" 2) "" [root@redis-1 ~]# redis-cli -h 192.168.81.220 -p 6379 config get slaveof 1) "slaveof" 2) "192.168.81.210 6379" [root@redis-1 ~]# redis-cli -h 192.168.81.230 -p 6379 config get slaveof 1) "slaveof" 2) "192.168.81.210 6379"将权重值调整为默认值将权重值调整为默认值,方便下次选举时作为判断条件。[root@redis-1 ~]# redis-cli -h 192.168.81.210 -p 6379 config set slave-priority 100 OK [root@redis-1 ~]# redis-cli -h 192.168.81.220 -p 6379 config set slave-priority 100 OK [root@redis-1 ~]# redis-cli -h 192.168.81.230 -p 6379 config set slave-priority 100 OK参考文章:https://www.pdai.tech/md/db/nosql-redis/db-redis-x-sentinel.html https://jiangxl.blog.csdn.net/article/details/120703648 https://jiangxl.blog.csdn.net/article/details/120703831(十):Redis Cluster 集群分片技术如果面对海量数据那么必然需要构建master(主节点分片)之间的集群,同时必然需要吸收高可用(主从复制和哨兵机制)能力,即每个master分片节点还需要有slave节点,这是分布式系统中典型的纵向扩展(集群的分片技术)的体现;所以在 Redis 3.0 版本中对应的设计就是Redis Cluster。Redis 集群的设计目标Redis-cluster 是一种服务器 Sharding技术 ,Redis3.0以后版本正式提供支持。Redis Cluster在设计时考虑了什么?Redis Cluster goals高性能可线性扩展至最多 1000 节点。集群中没有代理,(集群节点间)使用异步复制,没有归并操作(merge operations on values)。可接受的写入安全 :系统尝试(采用best-effort方式)保留所有连接到master节点的client发起的写操作。通常会有一个小的时间窗,时间窗内的已确认写操作可能丢失(即,在发生failover之前的小段时间窗内的写操作可能在failover中丢失)。而在(网络)分区故障下,对少数派master的写入,发生写丢失的时间窗会很大。可用性 :Redis Cluster 在以下场景下集群总是可用:大部分master节点可用,并且对少部分不可用的master,每一个master至少有一个当前可用的slave。更进一步,通过使用 replicas migration 技术,当前没有slave的master会从当前拥有多个slave的master接受到一个新slave来确保可用性。Clients and Servers roles in the Redis Cluster protocolRedis Cluster的节点负责维护数据,和获取集群状态,这包括将keys映射到正确的节点。集群节点同样可以自动发现其他节点、检测不工作节点、以及在发现故障发生时晋升slave节点到master。所有集群节点通过由TCP和二进制协议组成的称为 Redis Cluster Bus 的方式来实现集群的节点自动发现、故障节点探测、slave升级为master等任务。每个节点通过cluster bus连接所有其他节点。节点间使用 gossip协议 进行集群信息传播,以此来实现新节点发现,发送ping包以确认对端工作正常,以及发送cluster消息用来标记特定状态。cluster bus还被用来在集群中创博Pub/Sub消息,以及在接收到用户请求后编排手动failover。Write safetyRedis Cluster在节点间采用了异步复制,以及 last failover wins 隐含合并功能(implicit merge function)(【译注】不存在合并功能,而是总是认为最近一次failover的节点是最新的)。这意味着最后被选举出的master所包含的数据最终会替代(同一前master下)所有其他备份(replicas/slaves)节点(包含的数据)。当发生分区问题时,总是会有一个时间窗内会发生写入丢失。然而,对连接到多数派master(majority of masters)的client,以及连接到少数派master(mimority of masters)的client,这个时间窗是不同的。相比较连接到少数master(minority of masters)的client,对连接到多数master(majority of masters)的client发起的写入,Redis cluster会更努力地尝试将其保存。下面的场景将会导致在主分区的master上,已经确认的写入在故障期间发生丢失:写入请求达到master,但是当master执行完并回复client时,写操作可能还没有通过异步复制传播到它的slave。如果master在写操作抵达slave之前挂了,并且master无法触达(unreachable)的时间足够长而导致了slave节点晋升,那么这个写操作就永远地丢失了。通常很难直接观察到,因为master尝试回复client(写入确认)和传播写操作到slave通常几乎是同时发生。然而,这却是真实世界中的故障方式。(【译注】不考虑返回后宕机的场景,因为宕机导致的写入丢失,在单机版redis上同样存在,这不是redis cluster引入的目的及要解决的问题)。另一种理论上可能发生写入丢失的模式是:master因为分区原因不可用(unreachable)该master被某个slave替换(failover)一段时间后,该master重新可用在该old master变为slave之前,一个client通过过期的路由表对该节点进行写入。上述第二种失败场景通常难以发生,因为:少数派master(minority master)无法与多数派master(majority master)通信达到一定的时间后,它将拒绝写入,并且当分区恢复后,该master在重新与多数派master建立连接后,还将保持拒绝写入状态一小段时间来感知集群配置变化。留给client可写入的时间窗很小。发生这种错误还有一个前提是,client一直都在使用过期的路由表(而实际上集群因为发生了failover,已有slave发生了晋升)。写入少数派master(minority side of a partition)会有一个更长的时间窗会导致数据丢失。因为如果最终导致了failover,则写入少数派master的数据将会被多数派一侧(majority side)覆盖(在少数派master作为slave重新接入集群后)。特别地,如果要发生failover,master必须至少在NODE_TIMEOUT时间内无法被多数masters(majority of maters)连接,因此如果分区在这一时间内被修复,则不会发生写入丢失。当分区持续时间超过NODE_TIMEOUT时,所有在这段时间内对少数派master(minority side)的写入将会丢失。然而少数派一侧(minority side)将会在NODE_TIMEOUT时间之后如果还没有连上多数派一侧,则它会立即开始拒绝写入,因此对少数派master而言,存在一个进入不可用状态的最大时间窗。在这一时间窗之外,不会再有写入被接受或丢失。可用性(Availability)Redis Cluster在少数派分区侧不可用。在多数派分区侧,假设由多数派masters存在并且不可达的master有一个slave,cluster将会在NODE_TIMEOUT外加重新选举所需的一小段时间(通常1~2秒)后恢复可用。这意味着,Redis Cluster被设计为可以忍受一小部分节点的故障,但是如果需要在大网络分裂(network splits)事件中(【译注】比如发生多分区故障导致网络被分割成多块,且不存在多数派master分区)保持可用性,它不是一个合适的方案(【译注】比如,不要尝试在多机房间部署redis cluster,这不是redis cluster该做的事)。假设一个cluster由N个master节点组成并且每个节点仅拥有一个slave,在多数侧只有一个节点出现分区问题时,cluster的多数侧(majority side)可以保持可用,而当有两个节点出现分区故障时,只有 1-(1/(N_2-1)) 的可能性保持集群可用。也就是说,如果有一个由5个master和5个slave组成的cluster,那么当两个节点出现分区故障时,它有 1/(5_2-1)=11.11%的可能性发生集群不可用。Redis cluster提供了一种成为 Replicas Migration 的有用特性特性,它通过自动转移备份节点到孤master节点,在真实世界的常见场景中提升了cluster的可用性。在每次成功的failover之后,cluster会自动重新配置slave分布以尽可能保证在下一次failure中拥有更好的抵御力。性能(Performance)Redis Cluster不会将命令路由到其中的key所在的节点,而是向client发一个重定向命令 (- MOVED) 引导client到正确的节点。最终client会获得一个最新的cluster(hash slots分布)展示,以及哪个节点服务于命令中的keys,因此clients就可以获得正确的节点并用来继续执行命令。因为master和slave之间使用异步复制,节点不需要等待其他节点对写入的确认(除非使用了WAIT命令)就可以回复client。同样,因为multi-key命令被限制在了临近的key(near keys)(【译注】即同一hash slot内的key,或者从实际使用场景来说,更多的是通过hash tag定义为具备相同hash字段的有相近业务含义的一组keys),所以除非触发resharding,数据永远不会在节点间移动。普通的命令(normal operations)会像在单个redis实例那样被执行。这意味着一个拥有N个master节点的Redis Cluster,你可以认为它拥有N倍的单个Redis性能。同时,query通常都在一个round trip中执行,因为client通常会保留与所有节点的持久化连接(连接池),因此延迟也与客户端操作单台redis实例没有区别。在对数据安全性、可用性方面提供了合理的弱保证的前提下,提供极高的性能和可扩展性,这是Redis Cluster的主要目标。避免合并(merge)操作Redis Cluster设计上避免了在多个拥有相同key-value对的节点上的版本冲突(及合并/merge),因为在redis数据模型下这是不需要的。Redis的值同时都非常大;一个拥有数百万元素的list或sorted set是很常见的。同样,数据类型的语义也很复杂。传输和合并这类值将会产生明显的瓶颈,并可能需要对应用侧的逻辑做明显的修改,比如需要更多的内存来保存meta-data等。这里(【译注】刻意避免了merge)并没有严格的技术限制。CRDTs或同步复制状态机可以塑造与redis类似的复杂的数据类型。然而,这类系统运行时的行为与Redis Cluster其实是不一样的。Redis Cluster被设计用来支持非集群redis版本无法支持的一些额外的场景。主要模块介绍Redis Cluster Specification同时还介绍了Redis Cluster中主要模块,这里面包含了很多基础和概念,我们需要先了解下。哈希槽(Hash Slot)Redis-cluster 没有使用一致性hash,而是引入了哈希槽的概念 。Redis-cluster中有16384(即2的14次方)个哈希槽,每个key通过CRC16校验后对16383取模来决定放置哪个槽。Cluster中的每个节点负责一部分hash槽(hash slot)。比如集群中存在三个节点,则可能存在的一种分配如下:节点A包含0到5500号哈希槽;节点B包含5501到11000号哈希槽;节点C包含11001 到 16384号哈希槽。Keys hash tagsHash tags提供了一种途径, 用来将多个(相关的)key分配到相同的hash slot中 。这时Redis Cluster中实现multi-key操作的基础。hash tag规则如下,如果满足如下规则,{和}之间的字符将用来计算HASH_SLOT,以保证这样的key保存在同一个slot中。key包含一个{字符并且 如果在这个{的右面有一个}字符并且 如果在{和}之间存在至少一个字符例如:{user1000}.following和{user1000}.followers这两个key会被hash到相同的hash slot中,因为只有user1000会被用来计算hash slot值。foo{}{bar}这个key不会启用hash tag因为第一个{和}之间没有字符。foozap这个key中的{bar部分会被用来计算hash slotfoo{bar}{zap}这个key中的bar会被用来计算计算hash slot,而zap不会Cluster nodes属性每个节点在cluster中有一个唯一的名字。这个名字由160bit随机十六进制数字表示,并在节点启动时第一次获得(通常通过/dev/urandom)。节点在配置文件中保留它的ID,并永远地使用这个ID,直到被管理员使用CLUSTER RESET HARD命令hard reset这个节点。节点ID被用来在整个cluster中标识每个节点。一个节点可以修改自己的IP地址而不需要修改自己的ID。Cluster可以检测到IP /port的改动并通过运行在cluster bus上的gossip协议重新配置该节点。节点ID不是唯一与节点绑定的信息,但是他是唯一的一个总是保持全局一致的字段。每个节点都拥有一系列相关的信息。一些信息时关于本节点在集群中配置细节,并最终在cluster内部保持一致的。而其他信息,比如节点最后被ping的时间,是节点的本地信息。每个节点维护着集群内其他节点的以下信息:node id, 节点的IP和port,节点标签,master node id(如果这是一个slave节点),最后被挂起的ping的发送时间(如果没有挂起的ping则为0),最后一次收到pong的时间,当前的节点configuration epoch ,链接状态,以及最后是该节点服务的hash slots。对节点字段更详细的描述,可以参考对命令 CLUSTER NODES的描述。CLUSTER NODES命令可以被发送到集群内的任意节点,他会提供基于该节点视角(view)下的集群状态以及每个节点的信息。下面是一个发送到一个拥有3个节点的小集群的master节点的CLUSTER NODES输出的例子。$ redis-cli cluster nodes d1861060fe6a534d42d8a19aeb36600e18785e04 127.0.0.1:6379 myself - 0 1318428930 1 connected 0-1364 3886e65cc906bfd9b1f7e7bde468726a052d1dae 127.0.0.1:6380 master - 1318428930 1318428931 2 connected 1365-2729 d289c575dcbc4bdd2931585fd4339089e461a27d 127.0.0.1:6381 master - 1318428931 1318428931 3 connected 2730-4095在上面的例子中,按顺序列出了不同的字段:node id, address:port, flags, last ping sent, last pong received, configuration epoch, link state, slots.Cluster总线每个Redis Cluster节点有一个额外的TCP端口用来接受其他节点的连接。这个端口与用来接收client命令的普通TCP端口有一个固定的offset。该端口等于普通命令端口加上10000.例如,一个Redis街道口在端口6379坚挺客户端连接,那么它的集群总线端口16379也会被打开。节点到节点的通讯只使用集群总线,同时使用集群总线协议:有不同的类型和大小的帧组成的二进制协议。集群总线的二进制协议没有被公开文档话,因为他不希望被外部软件设备用来预计群姐点进行对话。集群拓扑Redis Cluster是一张全网拓扑,节点与其他每个节点之间都保持着TCP连接。在一个拥有N个节点的集群中,每个节点由N-1个TCP传出连接,和N-1个TCP传入连接。这些TCP连接总是保持活性(be kept alive)。当一个节点在集群总线上发送了ping请求并期待对方回复pong,(如果没有得到回复)在等待足够成时间以便将对方标记为不可达之前,它将先尝试重新连接对方以刷新与对方的连接。而在全网拓扑中的Redis Cluster节点,节点使用gossip协议和配置更新机制来避免在正常情况下节点之间交换过多的消息,因此集群内交换的消息数目(相对节点数目)不是指数级的。节点握手节点总是接受集群总线端口的链接,并且总是会回复ping请求,即使ping来自一个不可信节点。然而,如果发送节点被认为不是当前集群的一部分,所有其他包将被抛弃。节点认定其他节点是当前集群的一部分有 两种方式 :1.如果一个节点出现在了一条MEET消息中。一条meet消息非常像一个PING消息,但是它会强制接收者接受一个节点作为集群的一部分。节点只有在接收到系统管理员的如下命令后,才会向其他节点发送MEET消息:CLUSTER MEET ip port2.如果一个被信任的节点gossip了某个节点,那么接收到gossip消息的节点也会那个节点标记为集群的一部分。也就是说,如果在集群中,A知道B,而B知道C,最终B会发送gossip消息到A,告诉A节点C是集群的一部分。这时,A会把C注册未网络的一部分,并尝试与C建立连接。这意味着,一旦我们把某个节点加入了连接图(connected graph),它们最终会自动形成一张全连接图(fully connected graph)。这意味着只要系统管理员强制加入了一条信任关系(在某个节点上通过meet命令加入了一个新节点),集群可以自动发现其他节点。请求重定向Redis cluster采用去中心化的架构,集群的主节点各自负责一部分槽,客户端如何确定key到底会映射到哪个节点上呢?这就是我们要讲的请求重定向。在cluster模式下,节点对请求的处理过程如下:检查当前key是否存在当前NODE?通过crc16(key)/16384计算出slot查询负责该slot负责的节点,得到节点指针该指针与自身节点比较若slot不是由自身负责,则返回MOVED重定向若slot由自身负责,且key在slot中,则返回该key对应结果若key不存在此slot中,检查该slot是否正在迁出(MIGRATING)?若key正在迁出,返回ASK错误重定向客户端到迁移的目的服务器上若Slot未迁出,检查Slot是否导入中?若Slot导入中且有ASKING标记,则直接操作否则返回MOVED重定向这个过程中有两点需要具体理解下:MOVED重定向 和 ASK重定向。Moved 重定向槽命中:直接返回结果槽不命中:即当前键命令所请求的键不在当前请求的节点中,则当前节点会向客户端发送一个Moved 重定向,客户端根据Moved 重定向所包含的内容找到目标节点,再一次发送命令。从下面可以看出 php 的槽位9244不在当前节点中,所以会重定向到节点 192.168.2.23:7001中。redis-cli会帮你自动重定向(如果没有集群方式启动,即没加参数 -c,redis-cli不会自动重定向),并且编写程序时,寻找目标节点的逻辑需要交予程序员手动完成。cluster keyslot keyName # 得到keyName的槽ASK 重定向Ask重定向发生于集群伸缩时,集群伸缩会导致槽迁移,当我们去源节点访问时,此时数据已经可能已经迁移到了目标节点,使用Ask重定向来解决此种情况。smart客户端上述两种重定向的机制使得客户端的实现更加复杂,提供了smart客户端(JedisCluster)来减低复杂性,追求更好的性能。客户端内部负责计算/维护键-> 槽 -> 节点映射,用于快速定位目标节点。实现原理:从集群中选取一个可运行节点,使用 cluster slots得到槽和节点的映射关系将上述映射关系存到本地,通过映射关系就可以直接对目标节点进行操作(CRC16(key) -> slot -> node),很好地避免了Moved重定向,并为每个节点创建JedisPool至此就可以用来进行命令操作状态检测及维护Redis Cluster中节点状态如何维护呢?这里便涉及 有哪些状态,底层协议Gossip,及具体的通讯(心跳)机制 。@pdaiCluster中的每个节点都维护一份在自己看来当前整个集群的状态,主要包括:当前集群状态集群中各节点所负责的slots信息,及其migrate状态集群中各节点的master-slave状态集群中各节点的存活状态及不可达投票当集群状态变化时,如新节点加入、slot迁移、节点宕机、slave提升为新Master,我们希望这些变化尽快的被发现,传播到整个集群的所有节点并达成一致。节点之间相互的心跳(PING,PONG,MEET)及其携带的数据是集群状态传播最主要的途径。Gossip协议Redis Cluster 通讯底层是Gossip协议,所以需要对Gossip协议有一定的了解。gossip 协议(gossip protocol)又称 epidemic 协议(epidemic protocol),是基于流行病传播方式的节点或者进程之间信息交换的协议。在分布式系统中被广泛使用,比如我们可以使用 gossip 协议来确保网络中所有节点的数据一样。Gossip协议已经是P2P网络中比较成熟的协议了。Gossip协议的最大的好处是, 即使集群节点的数量增加,每个节点的负载也不会增加很多,几乎是恒定的。这就允许Consul管理的集群规模能横向扩展到数千个节点。 Gossip算法又被称为反熵(Anti-Entropy),熵是物理学上的一个概念,代表杂乱无章,而反熵就是在杂乱无章中寻求一致,这充分说明了Gossip的特点:在一个有界网络中,每个节点都随机地与其他节点通信,经过一番杂乱无章的通信,最终所有节点的状态都会达成一致。每个节点可能知道所有其他节点,也可能仅知道几个邻居节点,只要这些节可以通过网络连通,最终他们的状态都是一致的,当然这也是疫情传播的特点。上面的描述都比较学术,其实Gossip协议对于我们吃瓜群众来说一点也不陌生,Gossip协议也成为流言协议,说白了就是八卦协议,这种传播规模和传播速度都是非常快的,你可以体会一下。所以计算机中的很多算法都是源自生活,而又高于生活的。Gossip协议的使用Redis 集群是去中心化的,彼此之间状态同步靠 gossip 协议通信,集群的消息有以下几种类型:Meet 通过「cluster meet ip port」命令,已有集群的节点会向新的节点发送邀请,加入现有集群。Ping 节点每秒会向集群中其他节点发送 ping 消息,消息中带有自己已知的两个节点的地址、槽、状态信息、最后一次通信时间等。Pong 节点收到 ping 消息后会回复 pong 消息,消息中同样带有自己已知的两个节点信息。Fail 节点 ping 不通某节点后,会向集群所有节点广播该节点挂掉的消息。其他节点收到消息后标记已下线。基于Gossip协议的故障检测集群中的每个节点都会定期地向集群中的其他节点发送PING消息,以此交换各个节点状态信息,检测各个节点状态: 在线状态、疑似下线状态PFAIL、已下线状态FAIL。自己保存信息:当主节点A通过消息得知主节点B认为主节点D进入了疑似下线(PFAIL)状态时,主节点A会在自己的clusterState.nodes字典中找到主节点D所对应的clusterNode结构,并将主节点B的下线报告添加到clusterNode结构的fail_reports链表中,并后续关于结点D疑似下线的状态通过Gossip协议通知其他节点。一起裁定:如果集群里面,半数以上的主节点都将主节点D报告为疑似下线,那么主节点D将被标记为已下线(FAIL)状态,将主节点D标记为已下线的节点会向集群广播主节点D的FAIL消息,所有收到FAIL消息的节点都会立即更新nodes里面主节点D状态标记为已下线。最终裁定:将 node 标记为 FAIL 需要满足以下两个条件:有半数以上的主节点将 node 标记为 PFAIL 状态。当前节点也将 node 标记为 PFAIL 状态。通讯状态和维护我们理解了Gossip协议基础后,就可以进一步理解Redis节点之间相互的通讯 心跳 (PING,PONG,MEET)实现和维护了。我们通过几个问题来具体理解。什么时候进行心跳?Redis节点会记录其向每一个节点上一次发出ping和收到pong的时间,心跳发送时机与这两个值有关。通过下面的方式既能保证及时更新集群状态,又不至于使心跳数过多:每次Cron向所有未建立链接的节点发送ping或meet每1秒从所有已知节点中随机选取5个,向其中上次收到pong最久远的一个发送ping每次Cron向收到pong超过timeout/2的节点发送ping收到ping或meet,立即回复pong发送哪些心跳数据?Header,发送者自己的信息所负责slots的信息主从信息ip port信息状态信息Gossip,发送者所了解的部分其他节点的信息ping_sent, pong_receivedip, port信息状态信息,比如发送者认为该节点已经不可达,会在状态信息中标记其为PFAIL或FAIL如何处理心跳?新节点加入发送meet包加入集群从pong包中的gossip得到未知的其他节点循环上述过程,直到最终加入集群Slots信息判断发送者声明的slots信息,跟本地记录的是否有不同如果不同,且发送者epoch较大,更新本地记录如果不同,且发送者epoch小,发送Update信息通知发送者Master slave信息发现发送者的master、slave信息变化,更新本地状态节点Fail探测(故障发现)超过超时时间仍然没有收到pong包的节点会被当前节点标记为PFAILPFAIL标记会随着gossip传播每次收到心跳包会检测其中对其他节点的PFAIL标记,当做对该节点FAIL的投票维护在本机对某个节点的PFAIL标记达到大多数时,将其变为FAIL标记并广播FAIL消息注:Gossip的存在使得集群状态的改变可以更快的达到整个集群。每个心跳包中会包含多个Gossip包,那么多少个才是合适的呢,redis的选择是N/10,其中N是节点数,这样可以保证在PFAIL投票的过期时间内,节点可以收到80%机器关于失败节点的gossip,从而使其顺利进入FAIL状态。将信息广播给其它节点?当需要发布一些非常重要需要立即送达的信息时,上述心跳加Gossip的方式就显得捉襟见肘了,这时就需要向所有集群内机器的广播信息,使用广播发的场景:节点的Fail信息 :当发现某一节点不可达时,探测节点会将其标记为PFAIL状态,并通过心跳传播出去。当某一节点发现这个节点的PFAIL超过半数时修改其为FAIL并发起广播。Failover Request信息 :slave尝试发起FailOver时广播其要求投票的信息新Master信息 :Failover成功的节点向整个集群广播自己的信息故障恢复(Failover)master节点挂了之后,如何进行故障恢复呢?当slave发现自己的master变为FAIL状态时,便尝试进行Failover,以期成为新的master。由于挂掉的master可能会有多个slave。Failover的过程需要经过类Raft协议的过程在整个集群内达到一致, 其过程如下:slave发现自己的master变为FAIL将自己记录的集群currentEpoch加1,并广播Failover Request信息其他节点收到该信息,只有master响应,判断请求者的合法性,并发送FAILOVER_AUTH_ACK,对每一个epoch只发送一次ack尝试failover的slave收集FAILOVER_AUTH_ACK超过半数后变成新Master广播Pong通知其他集群节点扩容&缩容Redis Cluster是如何进行扩容和缩容的呢?扩容当集群出现容量限制或者其他一些原因需要扩容时,redis cluster提供了比较优雅的集群扩容方案。1.首先将新节点加入到集群中,可以通过在集群中任何一个客户端执行cluster meet 新节点ip:端口,或者通过redis-trib add node添加,新添加的节点默认在集群中都是主节点。2.迁移数据 迁移数据的大致流程是,首先需要确定哪些槽需要被迁移到目标节点,然后获取槽中key,将槽中的key全部迁移到目标节点,然后向集群所有主节点广播槽(数据)全部迁移到了目标节点。直接通过redis-trib工具做数据迁移很方便。现在假设将节点A的槽10迁移到B节点,过程如下:B:cluster setslot 10 importing A.nodeId A:cluster setslot 10 migrating B.nodeId循环获取槽中key,将key迁移到B节点A:cluster getkeysinslot 10 100 A:migrate B.ip B.port "" 0 5000 keys key1[ key2....]向集群广播槽已经迁移到B节点cluster setslot 10 node B.nodeId缩容缩容的大致过程与扩容一致,需要判断下线的节点是否是主节点,以及主节点上是否有槽,若主节点上有槽,需要将槽迁移到集群中其他主节点,槽迁移完成之后,需要向其他节点广播该节点准备下线(cluster forget nodeId)。最后需要将该下线主节点的从节点指向其他主节点,当然最好是先将从节点下线。更深入理解通过几个例子,再深入理解Redis Cluster为什么Redis Cluster的Hash Slot 是16384?我们知道一致性hash算法是2的16次方,为什么hash slot是2的14次方呢?在redis节点发送心跳包时需要把所有的槽放到这个心跳包里,以便让节点知道当前集群信息,16384=16k,在发送心跳包时使用char进行bitmap压缩后是2k(2 8 (8 bit) 1024(1k) = 16K),也就是说使用2k的空间创建了16k的槽数。虽然使用CRC16算法最多可以分配65535(2^16-1)个槽位,65535=65k,压缩后就是8k(8 8 (8 bit) 1024(1k) =65K),也就是说需要需要8k的心跳包,作者认为这样做不太值得;并且一般情况下一个redis集群不会有超过1000个master节点,所以16k的槽位是个比较合适的选择。为什么Redis Cluster中不建议使用发布订阅呢?在集群模式下,所有的publish命令都会向所有节点(包括从节点)进行广播,造成每条publish数据都会在集群内所有节点传播一次,加重了带宽负担,对于在有大量节点的集群中频繁使用pub,会严重消耗带宽,不建议使用。(虽然官网上讲有时候可以使用Bloom过滤器或其他算法进行优化的)其它常见方案还有一些方案出现在历史舞台上,我挑了几个经典的。简单了解下,增强下关联的知识体系。Redis Sentinel 集群 + Keepalived/Haproxy底层是 Redis Sentinel 集群,代理着 Redis 主从,Web 端通过 VIP 提供服务。当主节点发生故障,比如机器故障、Redis 节点故障或者网络不可达,Redis 之间的切换通过 Redis Sentinel 内部机制保障,VIP 切换通过 Keepalived 保障。优点:秒级切换对应用透明缺点:维护成本高存在脑裂Sentinel 模式存在短时间的服务不可用Twemproxy多个同构 Twemproxy(配置相同)同时工作,接受客户端的请求,根据 hash 算法,转发给对应的 Redis。Twemproxy 方案比较成熟了,但是效果并不是很理想。一方面是定位问题比较困难,另一方面是它对自动剔除节点的支持不是很友好。优点:开发简单,对应用几乎透明历史悠久,方案成熟缺点:代理影响性能LVS 和 Twemproxy 会有节点性能瓶颈Redis 扩容非常麻烦Twitter 内部已放弃使用该方案,新使用的架构未开源CodisCodis 是由豌豆荚开源的产品,涉及组件众多,其中 ZooKeeper 存放路由表和代理节点元数据、分发 Codis-Config 的命令;Codis-Config 是集成管理工具,有 Web 界面供使用;Codis-Proxy 是一个兼容 Redis 协议的无状态代理;Codis-Redis 基于 Redis 2.8 版本二次开发,加入 slot 支持,方便迁移数据。优点:开发简单,对应用几乎透明性能比 Twemproxy 好有图形化界面,扩容容易,运维方便缺点:代理依旧影响性能组件过多,需要很多机器资源修改了 Redis 代码,导致和官方无法同步,新特性跟进缓慢开发团队准备主推基于 Redis 改造的 reborndb链接:https://pdai.tech/md/db/nosql-redis/db-redis-x-cluster.html(十一):Redis Cluster 交叉复制与故障切换实战cluster 集群架构图通过hash分配数据分片到不同的redis主机。在应用端配置redis cluster地址时需要将所有节点的ip和端口都添加上。使用cluster集群创建的key,在哪个节点上创建的只能是自身节点可以查到数据,其他节点看不到。redis cluster不合理的架构图不太合理的架构图cluster集群每个机器上都有多个master和slave,如果master节点的数据备份都在自己主机的slave上,那么当服务器1坏掉后,这个机器上的数据就丢失了,数据丢失整个应用就崩溃了。合理的架构图每个节点slave都存放在别的主机,即使当前主机挂掉,另一台直接还原数据即可。部署一个cluster三主三从集群具体步骤在三台主机上部署redis,分别启动两个不同端口的redis,一个主库一个从库配置cluster集群自动发现,使得集群中各个主机都知道其他主机上的redis节点配置集群hash分配槽位,有了槽位才可以存储数据使用cluster replicate使多出来的三个主库变成从库,这样就实现了三主三从环境准备部署redis cluster节点搭建一个三主三从的redis cluster集群。配置文件中的bind也可以写成如下样子,自动识别bind地址。bind $(ifconfig | awk 'NR==2{print $2}')配置文件含义port 6380 //redis端口daemonize yes //后台启动logfile /data/redis_cluster/redis_6380/logs/redis_6380.log //日志路径pidfile /data/redis_cluster/redis_6380/pid/redis_6380.log //pid存放路径dbfilename "redis_6380.rdb" //数据文件名称dir /data/redis_cluster/redis_6380/data //数据文件存放目录cluster-enabled yes //开启集群模式cluster-config-file node_6380.conf //集群数据文件路径,保存集群信息的文件cluster-node-timeout 15000 //集群故障转移时间,多长时间无响应就切redis-1配置配置文件自动识别bind地址#创建节点配置文件路径 [root@redis-1 ~]# mkdir -p /data/redis_cluster/redis_{6380,6381}/{conf,data,logs,pid} #准备两个配置文件一个6380,一个6381 [root@redis-1 ~]# cat > /data/redis_cluster/redis_6380/conf/redis_6380.conf <<EOF bind $(ifconfig | awk 'NR==2{print $2}') port 6380 daemonize yes logfile /data/redis_cluster/redis_6380/logs/redis_6380.log pidfile /data/redis_cluster/redis_6380/pid/redis_6380.log dbfilename "redis_6380.rdb" dir /data/redis_cluster/redis_6380/data cluster-enabled yes cluster-config-file node_6380.conf cluster-node-timeout 15000 EOF [root@redis-1 ~]# cat > /data/redis_cluster/redis_6381/conf/redis_6381.conf <<EOF bind $(ifconfig | awk 'NR==2{print $2}') port 6381 daemonize yes logfile /data/redis_cluster/redis_6381/logs/redis_6381.log pidfile /data/redis_cluster/redis_6381/pid/redis_6381.log dbfilename "redis_6381.rdb" dir /data/redis_cluster/redis_6381/data cluster-enabled yes cluster-config-file node_6381.conf cluster-node-timeout 15000 EOF #启动rediscluster [root@redis-1 ~]# redis-server /data/redis_cluster/redis_6380/conf/redis_6380.conf [root@redis-1 ~]# redis-server /data/redis_cluster/redis_6381/conf/redis_6381.conf #查看进程和端口 [root@redis-1 ~]# ps aux | grep redis [root@redis-1 ~]# netstat -lnpt | grep redisredis-2配置配置文件自动识别bind地址[root@redis-2 ~]# mkdir -p /data/redis_cluster/redis_{6380,6381}/{conf,data,logs,pid} [root@redis-2 ~]# cat > /data/redis_cluster/redis_6380/conf/redis_6380.conf <<EOF bind $(ifconfig | awk 'NR==2{print $2}') port 6380 daemonize yes logfile /data/redis_cluster/redis_6380/logs/redis_6380.log pidfile /data/redis_cluster/redis_6380/pid/redis_6380.log dbfilename "redis_6380.rdb" dir /data/redis_cluster/redis_6380/data cluster-enabled yes cluster-config-file node_6380.conf cluster-node-timeout 15000 EOF [root@redis-2 ~]# cat > /data/redis_cluster/redis_6381/conf/redis_6381.conf <<EOF bind $(ifconfig | awk 'NR==2{print $2}') port 6381 daemonize yes logfile /data/redis_cluster/redis_6381/logs/redis_6381.log pidfile /data/redis_cluster/redis_6381/pid/redis_6381.log dbfilename "redis_6381.rdb" dir /data/redis_cluster/redis_6381/data cluster-enabled yes cluster-config-file node_6381.conf cluster-node-timeout 15000 EOF [root@redis-2 ~]# [root@redis-2 ~]# redis-server /data/redis_cluster/redis_6380/conf/redis_6380.conf [root@redis-2 ~]# redis-server /data/redis_cluster/redis_6381/conf/redis_6381.conf [root@redis-2 ~]# [root@redis-2 ~]# ps aux | grep redis [root@redis-2 ~]# netstat -lnpt | grep redisredis-3配置手动填写bind ip地址[root@redis-3 ~]# mkdir -p /data/redis_cluster/redis_{6380,6381}/{conf,data,logs,pid} [root@redis-3 ~]# cat > /data/redis_cluster/redis_6380/conf/redis_6380.conf <<EOF bind 192.168.81.230 port 6380 daemonize yes logfile /data/redis_cluster/redis_6380/logs/redis_6380.log pidfile /data/redis_cluster/redis_6380/pid/redis_6380.log dbfilename "redis_6380.rdb" dir /data/redis_cluster/redis_6380/data cluster-enabled yes cluster-config-file node_6380.conf cluster-node-timeout 15000 EOF [root@redis-3 ~]# cat > /data/redis_cluster/redis_6381/conf/redis_6381.conf <<EOF bind 192.168.81.230 port 6381 daemonize yes logfile /data/redis_cluster/redis_6381/logs/redis_6381.log pidfile /data/redis_cluster/redis_6381/pid/redis_6381.log dbfilename "redis_6381.rdb" dir /data/redis_cluster/redis_6381/data cluster-enabled yes cluster-config-file node_6381.conf cluster-node-timeout 15000 EOF [root@redis-3 ~]# [root@redis-3 ~]# redis-server /data/redis_cluster/redis_6380/conf/redis_6380.conf [root@redis-3 ~]# redis-server /data/redis_cluster/redis_6381/conf/redis_6381.conf [root@redis-3 ~]# [root@redis-3 ~]# ps aux | grep redis [root@redis-3 ~]# netstat -lnpt | grep redis查看redis cluster进程每个节点启动了cluster后,进程名上会增加cluster。每个redis节点会开放两个端口,服务端口6380,集群通信端口16380(在服务端口基础上增加10000)。#查看进程 [root@redis-1 ~]# ps aux | grep redis avahi 6935 0.0 0.1 62272 2296 ? Ss 1月29 0:02 avahi-daemon: running [redis-1.local] root 31846 0.3 0.5 141068 10800 ? Ssl 1月30 10:13 redis-server 192.168.81.210:6379 root 31859 0.3 0.4 136972 7744 ? Ssl 1月30 11:43 redis-sentinel 192.168.81.210:26379 [sentinel] root 78126 0.2 0.4 136972 7584 ? Ssl 14:40 0:00 redis-server 192.168.81.210:6380 [cluster] root 78130 0.4 0.4 136972 7588 ? Ssl 14:40 0:00 redis-server 192.168.81.210:6381 [cluster] root 78136 0.0 0.0 112728 988 pts/2 R+ 14:40 0:00 grep --color=auto redis [root@redis-1 ~]# netstat -lnpt | grep redis tcp 0 0 192.168.81.210:26379 0.0.0.0:* LISTEN 31859/redis-sentine tcp 0 0 127.0.0.1:6379 0.0.0.0:* LISTEN 31846/redis-server tcp 0 0 192.168.81.210:6379 0.0.0.0:* LISTEN 31846/redis-server tcp 0 0 192.168.81.210:6380 0.0.0.0:* LISTEN 78126/redis-server tcp 0 0 192.168.81.210:6381 0.0.0.0:* LISTEN 78130/redis-server tcp 0 0 192.168.81.210:16380 0.0.0.0:* LISTEN 78126/redis-server tcp 0 0 192.168.81.210:16381 0.0.0.0:* LISTEN 78130/redis-server查看集群信息文件内容集群模式的redis除了原有的配置文件之外又增加了一个集群配置文件,当集群内节点信息发生变化时,如添加节点,节点下线,故障转移等,节点都会自动保存集群状态到配置文件,redis自动维护集群配置文件,不需要手动修改防止节点重启时产生错乱。在集群启动后会生成一个数据文件,这个数据文件其实保存的就是集群的信息,在没有配置集群互相发现时,单个节点只保存自己的集群信息,文件中有节点id信息,每个节点的id都是唯一的。当配置了互相发现了配置文件中就会增加所有节点的信息。[root@redis-1 ~]# cat /data/redis_cluster/redis_6380/data/node_6380.conf b7748aedb5e51921db67c54e0c6263ed28043948 :0 myself,master - 0 0 0 connected vars currentEpoch 0 lastVoteEpoch 0 [root@redis-1 ~]# cat /data/redis_cluster/redis_6381/data/node_6381.conf 1ec79d498ecf9f272373740e402398e4c69cacb2 :0 myself,master - 0 0 0 connected vars currentEpoch 0 lastVoteEpoch 0 也可以登录redis进行查看 [root@redis-1 ~]# for i in {1..3} do for j in {0..1} do echo "192.168.81.2${i}0---638${j}" redis-cli -h 192.168.81.2${i}0 -p 638${j} cluster nodes done done 192.168.81.210---6380 b7748aedb5e51921db67c54e0c6263ed28043948 :6380 myself,master - 0 0 0 connected 192.168.81.210---6381 1ec79d498ecf9f272373740e402398e4c69cacb2 :6381 myself,master - 0 0 0 connected 192.168.81.220---6380 87ea6206f3db1dbaa49522bed15aed6f3bf16e22 :6380 myself,master - 0 0 0 connected 192.168.81.220---6381 bedd9482b08a06b0678fba01bb1c24165e56636c :6381 myself,master - 0 0 0 connected 192.168.81.230---6380 759ad5659d449dc97066480e1b7efbc10b34461d :6380 myself,master - 0 0 0 connected 192.168.81.230---6381 a2c95db5d6f9f288e6768c8d00e90fb7631f3021 :6381 myself,master - 0 0 0 connected更多关于 Redis 学习的文章,请参阅:NoSQL 数据库系列之 Redis,本系列持续更新中。配置cluster集群互相发现互相发现概念cluster集群互相发现只需要在一个节点上配置,所有节点都会接收到配置信息并自动加入到配置文件中。例如在redis-1的6380节点上增加了本机的6381端口和redis-2的6380端口,这时在redis-2上查看6380的配置里面就能看到6380节点和redis-1的6380以及6381节点信息,这时redis-3的两个节点还有本机的6381则还是一条,因为他们没有加入。在哪个节点添加的发现另一个节点的信息,那么当前这个节点就已经加入到了集群中。其实只要在集群的任意一个节点配置,集群的所有节点都会自动添加配置。下面演示一个在reids-1上添加几个节点,在redis-2上看是否自动配置。#在redis-1的6380节点上增加本机的6381和redis-2的6380端口 [root@redis-1 ~]# redis-cli -h 192.168.81.210 -p 6380 192.168.81.210:6380> CLUSTER MEET 192.168.81.210 6381 OK 192.168.81.210:6380> CLUSTER MEET 192.168.81.220 6380 OK #查看redis-2的6380集群配置文件 [root@redis-2 ~]# cat /data/redis_cluster/redis_6380/data/node_6380.conf 1ec79d498ecf9f272373740e402398e4c69cacb2 192.168.81.210:6381 master - 0 1612169525469 1 connected b7748aedb5e51921db67c54e0c6263ed28043948 192.168.81.210:6380 master - 0 1612169525369 0 connected 87ea6206f3db1dbaa49522bed15aed6f3bf16e22 192.168.81.220:6380 myself,master - 0 0 2 connected vars currentEpoch 2 lastVoteEpoch 0 很明显的看出已经将redis-1的6380和6381以及redis-2本机的6380端口都加到了集群配置文件中 #查看redis-2的6381节点集群配置文件 [root@redis-2 ~]# cat /data/redis_cluster/redis_6381/data/node_6381.conf bedd9482b08a06b0678fba01bb1c24165e56636c :0 myself,master - 0 0 0 connected vars currentEpoch 0 lastVoteEpoch 0将集群的所有节点进行互相发现在集群的任意一个节点配置就可以#配置互相发现 [root@redis-1 ~]# redis-cli -h 192.168.81.210 -p 6380 192.168.81.210:6380> CLUSTER MEET 192.168.81.210 6381 OK 192.168.81.210:6380> CLUSTER MEET 192.168.81.220 6380 OK 192.168.81.210:6380> CLUSTER MEET 192.168.81.220 6381 OK 192.168.81.210:6380> CLUSTER MEET 192.168.81.230 6380 OK 192.168.81.210:6380> CLUSTER MEET 192.168.81.230 6381 OK #查看配置文件是否增加,所有节点的配置文件都会生成 [root@redis-1 ~]# cat /data/redis_cluster/redis_6381/data/node_6381.conf 759ad5659d449dc97066480e1b7efbc10b34461d 192.168.81.230:6380 master - 0 1612169812886 4 connected 1ec79d498ecf9f272373740e402398e4c69cacb2 192.168.81.210:6381 myself,master - 0 0 1 connected 87ea6206f3db1dbaa49522bed15aed6f3bf16e22 192.168.81.220:6380 master - 0 1612169814797 2 connected bedd9482b08a06b0678fba01bb1c24165e56636c 192.168.81.220:6381 master - 0 1612169815806 0 connected a2c95db5d6f9f288e6768c8d00e90fb7631f3021 192.168.81.230:6381 master - 0 1612169815708 5 connected b7748aedb5e51921db67c54e0c6263ed28043948 192.168.81.210:6380 master - 0 1612169816814 3 connected vars currentEpoch 5 lastVoteEpoch 0cluster集群分配操作redis cluster通讯流程集群内消息传递是同步的在分布式存储中需要提供维护节点元数据信息的机制,所谓元数据是指:节点负责哪些数据,是否出现故障灯状态信息,redis集群采用gossip协议,gossip协议工作原理就是节点彼此不断交换信息,一段时间后所有的节点偶会指定集群完整信息,这种方式类似于流言传播,因此只需要在一台节点配置集群信息所有节点都能收到信息通信过程:1.集群中的每一个节点都会单独开辟一个tcp通道用于节点之间彼此通信,通信端口在基础端口上增加100002.每个节点在固定周期内通过特定规则选择结构节点发送ping消息3.接收到ping消息的节点用pong作为消息响应,集群中每个节点通过一定规则挑选要通信的节点,每个节点可能知道全部节点的信息,也可能知道部分节点信息,只要这些节点彼此可以正常通信,最终他们就会达成一致的状态,当节点出现故障,新节点加入,主从角色变化等,彼此之间不断发生ping/pong消息,最终达成同步的模板通讯消息类型:gossip,信息交换,常见的消息分为ping、pong、meet、fail。通讯示意图没有分配槽位时集群的状态,所有节点执行cluster info,cluster_state都是fail,fail状态表示集群不可用,没有分配槽位,cluster_slots都会显示0。手动配置集群槽位每个cluster集群都有16384个槽位,我们有三台机器,想要手动分配平均就需要使用16384除3。redis-1 0-5461 redis-2 5462-10922 redis-3 10923-16383分配槽位语法格式(交互式):CLUSTER ADDSLOTS 0 5461。分配槽位语法:redis-cli -h 192.168.81.210 -p 6380 cluster addslots {0…5461}。 删除槽位分配语法格式:redis-cli -h 192.168.81.210 -p 6380 cluster delslots {5463…10921}。 #配置手动分配槽位 [root@redis-1 ~]# redis-cli -h 192.168.81.210 -p 6380 cluster addslots {0..5461} OK [root@redis-1 ~]# redis-cli -h 192.168.81.220 -p 6380 cluster addslots {5462..10922} OK [root@redis-1 ~]# redis-cli -h 192.168.81.230 -p 6380 cluster addslots {10923..16383} OK #查看集群状态,到目前为止集群已经是可用的了 [root@redis-1 ~]# redis-cli -h 192.168.81.220 -p 6380 cluster info cluster_state:ok cluster_slots_assigned:16384 cluster_slots_ok:16384 cluster_slots_pfail:0 cluster_slots_fail:0 cluster_known_nodes:6 cluster_size:3 cluster_current_epoch:5 cluster_my_epoch:2 cluster_stats_messages_sent:170143 cluster_stats_messages_received:170142 #查看nodes文件内容 [root@redis-1 ~]# redis-cli -h 192.168.81.220 -p 6380 cluster nodes a2c95db5d6f9f288e6768c8d00e90fb7631f3021 192.168.81.230:6381 master - 0 1612251539412 5 connected bedd9482b08a06b0678fba01bb1c24165e56636c 192.168.81.220:6381 master - 0 1612251538402 0 connected 87ea6206f3db1dbaa49522bed15aed6f3bf16e22 192.168.81.220:6380 myself,master - 0 0 2 connected 5462-10922 b7748aedb5e51921db67c54e0c6263ed28043948 192.168.81.210:6380 master - 0 1612251540418 3 connected 0-5461 1ec79d498ecf9f272373740e402398e4c69cacb2 192.168.81.210:6381 master - 0 1612251537394 1 connected 759ad5659d449dc97066480e1b7efbc10b34461d 192.168.81.230:6380 master - 0 1612251536386 4 connected 10923-16383创建key验证集群是否可用不是所有的key都能插入,有的key插入的时候就提示说你应该去192.168.81.230上插入,这时手动到对应的主机上执行就可以插入,这是由于cluster集群槽位都是分布在不同节点的,每次新建一个key,都会通过hash算法均匀的在不同节点去创建不同节点创建的key只由自己节点可以看到自己创建的数据[root@redis-1 ~]# redis-cli -h 192.168.81.210 -p 6380 192.168.81.210:6380> set k1 v1 (error) MOVED 12706 192.168.81.230:6380 192.168.81.210:6380> set k2 v2 OK 192.168.81.210:6380> set k3 v3 OK 192.168.81.210:6380> set k4 v4 (error) MOVED 8455 192.168.81.220:6380 192.168.81.210:6380> set k5 v5 (error) MOVED 12582 192.168.81.230:6380 192.168.81.210:6380> set k6 v6 OK [root@redis-1 ~]# redis-cli -h 192.168.81.230 -p 6380 192.168.81.230:6380> set k1 v1 OK 192.168.81.230:6380> set k5 v5 OK [root@redis-1 ~]# redis-cli -h 192.168.81.220 -p 6380 192.168.81.220:6380> set k4 v4 OKASK路由解决key创建提示去别的主机创建可以通过ASK路由解决创建key时提示去别的主机进行创建。ASK路由创建key时,如果可以在本机直接创建就会执行创建key的命令,如果不能再本机执行,他会根据提示的主机去对应主机上创建key。ASK路径的特性:每次通过hash在指定主机上创建了key后就会停留在这个主机上。只需要执行redis-cli时加上-c参数即可。[root@redis-1 ~]# redis-cli -c -h 192.168.81.210 -p 6380 192.168.81.210:6380> set k8 v8 -> Redirected to slot [8331] located at 192.168.81.220:6380 OK 192.168.81.220:6380> set k9 v9 -> Redirected to slot [12458] located at 192.168.81.230:6380 OK 192.168.81.230:6380> set k10 v10 OK 192.168.81.230:6380> set k11 v11 OK 192.168.81.230:6380> set k12 v12 -> Redirected to slot [2863] located at 192.168.81.210:6380 OK 192.168.81.210:6380> set k13 v13 -> Redirected to slot [6926] located at 192.168.81.220:6380 OK 192.168.81.220:6380> set k14 v14 -> Redirected to slot [11241] located at 192.168.81.230:6380 OK #很清楚的展示了在哪台主机上创建验证hash分配是否均已cluster架构是分布式的,创建的key会通过hash将数据均已的分布在每台主机的槽位上。#插入一千条数据,查看三个节点是否分配均已 插入的时候使用-c,自动在某个节点上插入数据 [root@redis-1 ~]# for i in {1..1000} do redis-cli -c -h 192.168.81.210 -p 6380 set key_${i} value_${i} done #查看每个节点的数据量,可以看到非常均匀,误差只有一点点 [root@redis-1 ~]# redis-cli -h 192.168.81.210 -p 6380 192.168.81.210:6380> DBSIZE (integer) 339 [root@redis-1 ~]# redis-cli -h 192.168.81.220 -p 6380 192.168.81.220:6380> DBSIZE (integer) 339 [root@redis-1 ~]# redis-cli -h 192.168.81.230 -p 6380 192.168.81.230:6380> DBSIZE (integer) 336 192.168.81.230:6380> exit配置cluster集群三主三从高可用实现步骤使用cluster replicate将主机的6381redis节点交叉成为别的主机6380节点的从库查看集群状态即可三主三从架构图三主三从是redis cluster最常用的架构,每个从节点复制的都不是本机主库的数据,而是其他节点主库的数据,这样即使某一台主机坏掉了,从节点备份还是在其他机器上,这样就做到了高可用,三主三从架构允许最多坏一台主机。三主三从我们采用交叉复制架构类型,这样可以做到最多坏一台主机集群还是正常可以用的,如果每台主机的6381节点都是6380节点的备份,那么这台机器坏了,集群就不可用了,因此想要做到高可用,就采用交叉复制。交叉复制的架构,当主节点挂掉了,主节点备份的从节点就会自动成为主节点,当主节点上线后。每个节点的6380端口都是主库,6381端口都是从库。从节点对应的主节点关系redis-1的6381从节点对应的主节点是redis-2的6380主节点redis-2的6381从节点对应的主节点是redis-3的6380主节点redis-3的6381从节点对应的主节点是redis-1的6380主节点将每一个节点都配置rdb持久化在所有节点端口的配置文件中加上rdb持久化配置即可。vim /data/redis_cluster/redis_6380/conf/redis_6380.conf bind 192.168.81.210 port 6380 daemonize yes logfile /data/redis_cluster/redis_6380/logs/redis_6380.log pidfile /data/redis_cluster/redis_6380/pid/redis_6380.log dbfilename "redis_6380.rdb" dir /data/redis_cluster/redis_6380/data cluster-enabled yes cluster-config-file node_6380.conf cluster-node-timeout 15000 #持久化配置 save 60 10000 save 300 10 save 900 1 重启redis redis-cli -h 192.168.81.210 -p 6380 shutdown redis-server /data/redis_cluster/redis_6380/conf/redis_6380.conf配置三主三从配置三主三从规范操作步骤将集群信息粘到txt中,只保留下6380端口信息配置命令在txt中准备好在复制到命令行主节点我们已经有了,目前6个节点全是主节点,我们需要把所有主机的6381的主节点配置成从节点。从节点对应的主节点关系:redis-1的6381从节点对应的主节点是redis-2的6380主节点redis-2的6381从节点对应的主节点是redis-3的6380主节点redis-3的6381从节点对应的主节点是redis-1的6380主节点CLUSTER REPLICATE 是配置当前节点成为某个主节点的从节点,replicate命令其实就相当于执行了slaveof,同步了某一个主库,并且在日志中查看到的就是主从同步的过程#配置从节点连接主节点,交叉式复制 [root@redis-1 ~]# redis-cli -h 192.168.81.210 -p 6381 CLUSTER REPLICATE 87ea6206f3db1dbaa49522bed15aed6f3bf16e22 OK [root@redis-1 ~]# redis-cli -h 192.168.81.220 -p 6381 CLUSTER REPLICATE 759ad5659d449dc97066480e1b7efbc10b34461d OK [root@redis-1 ~]# redis-cli -h 192.168.81.230 -p 6381 CLUSTER REPLICATE b7748aedb5e51921db67c54e0c6263ed28043948 OK #查看集群节点信息,发现已经是三主三从了 [root@redis-1 ~]# redis-cli -h 192.168.81.230 -p 6381 cluster nodes a2c95db5d6f9f288e6768c8d00e90fb7631f3021 192.168.81.230:6381 myself,slave b7748aedb5e51921db67c54e0c6263ed28043948 0 0 5 connected bedd9482b08a06b0678fba01bb1c24165e56636c 192.168.81.220:6381 slave 759ad5659d449dc97066480e1b7efbc10b34461d 0 1612323918342 4 connected b7748aedb5e51921db67c54e0c6263ed28043948 192.168.81.210:6380 master - 0 1612323919350 3 connected 0-5461 1ec79d498ecf9f272373740e402398e4c69cacb2 192.168.81.210:6381 slave 87ea6206f3db1dbaa49522bed15aed6f3bf16e22 0 1612323917331 2 connected 759ad5659d449dc97066480e1b7efbc10b34461d 192.168.81.230:6380 master - 0 1612323916826 4 connected 10923-16383 87ea6206f3db1dbaa49522bed15aed6f3bf16e22 192.168.81.220:6380 master - 0 1612323920357 2 connected 5462-10922配置完主从,可以看到集群中已经有slave节点了,并且也是交叉复制的。打开主库的日志可以看到哪个从库同步了主库的日志,打开从库的日志可以看到同步了哪个主库的日志。模拟故障转移三主三从架构允许最多坏一台主机,模拟将redis-1机器的主库6380挂掉,查看集群间的故障迁移 思路 :1.将redis-1的6380主库关掉,查看集群状态信息是否将slave自动切换为master2.当master上线后会变成一个节点的从库3.将master通过cluster failover重新成为主库模拟坏掉redis-1的主库并验证就能是否可用#挂掉redis-1的主库 [root@redis-1 ~]# redis-cli -h 192.168.81.210 -p 6380 shutdown #查看日志 先是由于主库挂了状态变成fail,当从库变成主库后,状态再次变为ok [root@redis-1 ~]# tail -f /data/redis_cluster/redis_6381/logs/redis_6381.log 124058:S 03 Feb 13:16:00.233 # Cluster state changed: fail 124058:S 03 Feb 13:17:01.857 # Cluster state changed: ok #查看集群信息 [root@redis-1 ~]# redis-cli -h 192.168.81.210 -p 6381 cluster nodes #查看集群状态 [root@redis-1 ~]# redis-cli -h 192.168.81.210 -p 6381 cluster info cluster_state:ok cluster_slots_assigned:16384 cluster_slots_ok:16384 cluster_slots_pfail:0 cluster_slots_fail:0 cluster_known_nodes:6 cluster_size:3 cluster_current_epoch:7 cluster_my_epoch:2 cluster_stats_messages_sent:18202 cluster_stats_messages_received:17036 #验证集群是否可用 [root@redis-1 ~]# redis-cli -c -h 192.168.81.210 -p 6381 set k1111 v1111 OK当主库挂掉后,查看集群信息时会看到提示主库已经是fail状态,此时可用看到192.168.81.230机器的6381端口成为了master,192.168.81.230的6381端口是redis-1的从库,从库变为主库后,集群状态再次变为ok。redis-1节点的主库恢复目前的架构图当主库重新加入集群后,架构图就变成了如下样子,主库的6380就成为了192.168.81.230的从库,而192.168.81.230的从库变成了192.168.81.210的主库。#启动redis-1的6380主库 [root@redis-1 ~]# redis-server /data/redis_cluster/redis_6380/conf/redis_6380.conf #查看集群信息 [root@redis-1 ~]# redis-cli -c -h 192.168.81.210 -p 6380 cluster nodes 1ec79d498ecf9f272373740e402398e4c69cacb2 192.168.81.210:6381 slave 87ea6206f3db1dbaa49522bed15aed6f3bf16e22 0 1612330250901 2 connected b7748aedb5e51921db67c54e0c6263ed28043948 192.168.81.210:6380 myself,slave a2c95db5d6f9f288e6768c8d00e90fb7631f3021 0 0 3 connected 759ad5659d449dc97066480e1b7efbc10b34461d 192.168.81.230:6380 master - 0 1612330255958 4 connected 10923-16383 bedd9482b08a06b0678fba01bb1c24165e56636c 192.168.81.220:6381 slave 759ad5659d449dc97066480e1b7efbc10b34461d 0 1612330252920 4 connected a2c95db5d6f9f288e6768c8d00e90fb7631f3021 192.168.81.230:6381 master - 0 1612330254941 7 connected 0-5461 87ea6206f3db1dbaa49522bed15aed6f3bf16e22 192.168.81.220:6380 master - 0 1612330256960 2 connected 5462-10922将恢的主库重新变为主库目前主库已经重新上线了,且现在是192.168.81.230的从库,而原来192.168.81.230的从库变成了现在192.168.81.210的主库,我们需要把关系切换回来,不能让一台机器上同时存在两台主库,每次故障处理后一定要把架构修改会原来的样子。从库切换成主库也特别简单,只需要执行一个cluster falover即可变为主库。cluster falover确实也类似于关系互换,简单理解就是原来的从变成了主,现在的主变成了从,这样一来就可以把故障恢复的主机重新变为主库。cluster falover原理:falover原理也就是先执行了slave no one,然后在对应的由主库变为从库的机器上执行了slave of。#将故障上线的主库重新成为主库 [root@redis-1 ~]# redis-cli -h 192.168.81.210 -p 6380 192.168.81.210:6380> CLUSTER FAILOVER OK #查看集群信息,192.168.81.210的发现6380重新成为了master,192.168.81.230的从库变成了slave [root@redis-1 ~]# redis-cli -h 192.168.81.210 -p 6381 cluster nodes 759ad5659d449dc97066480e1b7efbc10b34461d 192.168.81.230:6380 master - 0 1612331847795 12 connected 10923-16383 87ea6206f3db1dbaa49522bed15aed6f3bf16e22 192.168.81.220:6380 master - 0 1612331849307 11 connected 5462-10922 b7748aedb5e51921db67c54e0c6263ed28043948 192.168.81.210:6380 master - 0 1612331848299 10 connected 0-5461 bedd9482b08a06b0678fba01bb1c24165e56636c 192.168.81.220:6381 slave 759ad5659d449dc97066480e1b7efbc10b34461d 0 1612331850317 12 connected a2c95db5d6f9f288e6768c8d00e90fb7631f3021 192.168.81.230:6381 slave b7748aedb5e51921db67c54e0c6263ed28043948 0 1612331851324 10 connected 1ec79d498ecf9f272373740e402398e4c69cacb2 192.168.81.210:6381 myself,slave 87ea6206f3db1dbaa49522bed15aed6f3bf16e22 0 0 8 connected查看集群信息,192.168.81.210的发现6380重新成为了master,192.168.81.230的从库变成了slave。到此cluster集群故障转移成功,集群状态一切正常。[root@redis-1 ~]# redis-cli -h 192.168.81.210 -p 6380 cluster info cluster_state:ok cluster_slots_assigned:16384 cluster_slots_ok:16384 cluster_slots_pfail:0 cluster_slots_fail:0 cluster_known_nodes:6 cluster_size:3 cluster_current_epoch:16 cluster_my_epoch:16 cluster_stats_messages_sent:18614 cluster_stats_messages_received:3497需要注意的几点生产环境数据量可能非常大,当主库故障重新上线时,执行CLUSTER FAILOVER会很慢,因为这个就相当于是主从复制切换了,从库(刚上线的原来主库)关闭主从复制,主库(主库坏掉前的从库)同步从库(刚上线的原来主库)数据,然后从库(刚上线的原来主库)重新变为主库,这个时间一定要等,切记,千万不要因为慢在主库上(主库坏掉前的从库)同步手动进行了CLUSTER REPLICATE,这样确实会非常快的将主库(主库坏掉前的从库)重新变为从库,但也意味着这个节点数据全部丢失,因为clusert replicate相当于slaveof,slaveof会把自己的库清掉,这时候从库(刚上线的原来主库)在执行这CLUSTER FAILOVER同步着主库(主库坏掉前的从库)的数据,主库那边执行了replicate去同步从库(刚上线的原来主库),从而导致从库(刚上线的原来主库)还没有同步完主库(主库坏掉前的从库的数据),主库(主库坏掉前的从库)数据就丢失,整个集群还是可以用的,只是这个主库节点和从节点数据全部丢失,其他两个主库从库还能使用。切记,当从库执行CLUSTER FAILOVER变为主库时,一定不要在主库上执行CLUSTER REPLICATE变为从库,虽然CLUSTER REPLICATE变为从库很快,但是会清空自己的数据去同步主库,这时主库还没有数据,因此就会导致数据全部丢失。CLUSTER FAILOVER:首先执行slave on one变为一个单独的节点,然后在要变成从库的节点上执行slaveof,只要从库执行完slave of,执行CLUSTER FAILOVER的节点就变成了主库。CLUSTER REPLICATE:只是执行了slaveof使自身成为从节点。当redis cluster主从正在同步时,不要执行cluster replicate,当主从复制完在执行,如何看主从是否复制完就要看节点的rdb文件是否是.tmp结尾的,如果是tmp结尾就说明他们正在同步数据,此时不要对集群做切换操作总结3.0版本以后推出集群功能cluster集群有16384个槽位,误差在2%之间槽位与序号顺序无关,重点是槽的数量通过发现集群,与集群之间实现消息传递配置文件无需手动修改,都是自动生成的分配操作,必须将所有的槽位分配完毕理清复制关系,画图,按照图形执行复制命令当集群状态为ok时,集群才可以正常使用反复测试,批量插入key,验证分配是否均匀测试高可用,关闭任意主节点,集群是否自动转移当主节点修复后,执行主从关系切换做实验尽量贴合生成环境,尽量使用和生成环境一样数量的数据评估和记录同步数据、故障转移完成的时间向领导汇报时要有图、文档、实验环境,随时都可以演示当应用需要连接 redis cluster 集群时要将所有节点都写在配置文件中。来源:https://jiangxl.blog.csdn.net/article/details/120879397(十二):使用 Redis 官方工具自动部署 Cluster 集群实践手动搭建集群便于理解集群创建的流程和细节 ,不过手动搭建集群需要很多步骤,当集群节点众多时,必然会加大搭建集群的复杂度和运维成本,因此 官方提供了 redis-trib.rb 的工具方便我们快速搭建集群。 redis-trib.rb是采用 Ruby 实现的 redis 集群管理工具,内部通过 Cluster相关命令帮我们简化集群创建、检查、槽迁移和均衡等常见运维操作,使用前要安装 ruby 依赖环境。redis-trib.rb无法实现所有节点都交叉复制 ,总会有一个节点不交叉,因此在安装完cluster以后,需要手动调整交叉。环境准备安装ruby环境只在使用redis-trib的机器上安装即可//安装ruby管理工具 [root@redis-1 ~]# yum -y install rubygems //移除官网源 [root@redis-1 ~]# gem sources --remove https://rubygems.org/ https://rubygems.org/ removed from sources //增加阿里云源 [root@redis-1 ~]# gem sources -a http://mirrors.aliyun.com/rubygems/ http://mirrors.aliyun.com/rubygems/ added to sources //更新缓存 [root@redis-1 ~]# gem update --system ruby2.3.0以下版本执行会报错 //安装ruby支持redis的插件 [root@redis-1 ~]# gem install redis -v 3.3.5 Fetching: redis-3.3.5.gem (100%) Successfully installed redis-3.3.5 Parsing documentation for redis-3.3.5 Installing ri documentation for redis-3.3.5 1 gem installed使用redis-trib自动部署cluster集群所有节点安装redis#创建部署路径 mkdir -p /data/redis_cluster/redis_{6380,6381}/{conf,data,logs,pid} #准备配置文件 cat > /data/redis_cluster/redis_6380/conf/redis_6380.conf <<EOF bind $(ifconfig | awk 'NR==2{print $2}') port 6380 daemonize yes logfile /data/redis_cluster/redis_6380/logs/redis_6380.log pidfile /data/redis_cluster/redis_6380/pid/redis_6380.log dbfilename "redis_6380.rdb" dir /data/redis_cluster/redis_6380/data cluster-enabled yes cluster-config-file node_6380.conf cluster-node-timeout 15000 save 60 10000 save 300 10 save 900 1 EOF cat > /data/redis_cluster/redis_6381/conf/redis_6381.conf <<EOF bind $(ifconfig | awk 'NR==2{print $2}') port 6381 daemonize yes logfile /data/redis_cluster/redis_6381/logs/redis_6381.log pidfile /data/redis_cluster/redis_6381/pid/redis_6381.log dbfilename "redis_6381.rdb" dir /data/redis_cluster/redis_6381/data cluster-enabled yes cluster-config-file node_6381.conf cluster-node-timeout 15000 save 60 10000 save 300 10 save 900 1 EOF #启动redis ./redis_shell.sh start 6380 ./redis_shell.sh start 6381使用redis-trib部署cluster集群语法格式: ./redis-trib.rb create --replicas 每个主节点的副本数量(从库数量) cluster节点地址create //创建 –replicas //指定主库的副本数量,也就是从库数量使用redis-trib安装的cluster集群,总会有一个节点不是交叉复制的,需要手动调整,因为trib也是根据节点地址交叉对应,到了最后一个机器已经没有第二个可以与它交叉的机器,它只能和自己去复制[root@redis-1 ~]# cd /data/redis_cluster/redis-3.2.9/src [root@redis-1 /data/redis_cluster/redis-3.2.9/src]# ./redis-trib.rb create --replicas 1 192.168.81.210:6380 192.168.81.220:6380 192.168.81.230:6380 192.168.81.210:6381 192.168.81.220:6381 192.168.81.230:6381 #安装完查看集群准备已经是可用的 [root@redis-1 ~]# redis-cli -h 192.168.81.210 -p 6380 192.168.81.210:6380> CLUSTER info cluster_state:ok cluster_slots_assigned:16384 cluster_slots_ok:16384 cluster_slots_pfail:0 cluster_slots_fail:0 cluster_known_nodes:6 cluster_size:3 cluster_current_epoch:6 cluster_my_epoch:1 cluster_stats_messages_sent:1618 cluster_stats_messages_received:1618手动调整三主三从交叉复制由于只有redis-3的复制不是交叉的,如果直接让redis-3去交叉复制某一个节点,那么就没有节点去复制redis-3的6380了,因此我们要手动调整所有节点之间的交叉入职举个例子:redis-3的6381要成为redis-1的6380的主库,需要去redis-3的6381redis交互式操作#获取主节点的信息 [root@redis-1 ~]# redis-cli -h 192.168.81.210 -p 6381 cluster nodes | grep 6380 | awk '{print $1.$2}' 812ae8ccf55d8994f1f9d30a20f6cff42fb24b4a192.168.81.230:6380 ce75dacf45d3ad4f852b7fb4d359a295b8a2bcdd192.168.81.220:6380 b61b8d0421b94b9de7267dda6c6f401a42622047192.168.81.210:6380 #配置三主三从交叉复制 [root@redis-1 ~]# redis-cli -h 192.168.81.210 -p 6381 192.168.81.210:6381> CLUSTER REPLICATE ce75dacf45d3ad4f852b7fb4d359a295b8a2bcdd OK [root@redis-2 ~]# redis-cli -h 192.168.81.220 -p 6381 192.168.81.220:6381> CLUSTER REPLICATE 812ae8ccf55d8994f1f9d30a20f6cff42fb24b4a OK [root@redis-3 ~]# redis-cli -h 192.168.81.230 -p 6381 192.168.81.230:6381> CLUSTER REPLICATE b61b8d0421b94b9de7267dda6c6f401a42622047 OK #查看集群信息已经交叉复制 [root@redis-1 ~]# redis-cli -h 192.168.81.210 -p 6381 cluster nodes 812ae8ccf55d8994f1f9d30a20f6cff42fb24b4a 192.168.81.230:6380 master - 0 1612342768677 3 connected 10923-16383 bdd20b03b573b2def6a9ee5053a75867709fc908 192.168.81.210:6381 myself,slave ce75dacf45d3ad4f852b7fb4d359a295b8a2bcdd 0 0 4 connected 9b7641253ea66073d865accdd4460d2877f9ff5d 192.168.81.220:6381 slave 812ae8ccf55d8994f1f9d30a20f6cff42fb24b4a 0 1612342767669 5 connected ce75dacf45d3ad4f852b7fb4d359a295b8a2bcdd 192.168.81.220:6380 master - 0 1612342766658 2 connected 5461-10922 904a0109976cae38e5a3059fd70ce2727a0ed8fb 192.168.81.230:6381 slave b61b8d0421b94b9de7267dda6c6f401a42622047 0 1612342769686 3 connected b61b8d0421b94b9de7267dda6c6f401a42622047 192.168.81.210:6380 master - 0 1612342770189 1 connected 0-5460查看集群完整性如果集群没问题会输出ok[root@redis-1 ~]# cd /data/redis_cluster/redis-3.2.9/src [root@redis-1 /data/redis_cluster/redis-3.2.9/src]# ./redis-trib.rb check 192.168.81.210:6380验证hash分配是否均匀#首先插入1000条数据 [root@redis-1 ~]# for i in {1..1000} do redis-cli -c -h 192.168.81.210 -p 6380 set key_${i} value_${i} done #查看每个节点的数据量 [root@redis-1 ~]# redis-cli -c -h 192.168.81.210 -p 6380 dbsize (integer) 334 [root@redis-1 ~]# redis-cli -c -h 192.168.81.220 -p 6380 dbsize (integer) 336 [root@redis-1 ~]# redis-cli -c -h 192.168.81.230 -p 6380 dbsize (integer) 330查看集群分配的误差值[root@redis-1 /data/redis_cluster/redis-3.2.9/src]# ./redis-trib.rb rebalance 192.168.81.210:6380 >>> Performing Cluster Check (using node 192.168.81.210:6380) [OK] All nodes agree about slots configuration. >>> Check for open slots... >>> Check slots coverage... [OK] All 16384 slots covered. *** No rebalancing needed! All nodes are within the 2.0% threshold.链接:https://blog.csdn.net/weixin_44953658/article/details/121265752(十三):Redis Cluster 集群扩容原理与实践Cluster 集群扩容概念当redis数据量日渐增长,当内存不够用的时候,这时候就需要集群扩容了,cluster集群扩容可以增加内存也可以增加节点 ,因为redis数据都是存在内存中。redis cluster增加节点进行扩容步骤: 1.在新的服务器上部署redis cluster2.使用工具将新部署的节点加到集群中3.使用工具将集群槽位重新分配4.将主从复制关系调整成交叉模式扩容原理原来的节点算好要拿出多少的槽位给新加的节点,新加的节点准备导入的槽位,准备的前提条件就是加入集群,一切准备就绪后,主节点将划分出来的槽位分配给新节点,然后将相关槽位的数据迁移到新的节点。4个节点的redis cluster,每个节点的槽位时16384/4,一个节点4096个槽位 。扩容前后的架构图对比图新增节点后,主从复制就变成了四主四从,只需要变动192.168.81.230的从库关系即可,192.168.81.230节点从库复制192.168.81.240节点的主库,192.168.81.240从库复制192.168.81.210的主库环境准备在新节点部署redis cluster#将redis管理工具从redis-1拷贝到redis-4并安装 [root@redis-1 ~]# scp -rp /data/redis_cluster root@192.168.81.240:/data [root@redis-4 ~]# cd /data/redis_cluster/redis-3.2.9 [root@redis-4 /data/redis_cluster/redis-3.2.9]# make install #创建部署路径 [root@redis-4 ~]# mkdir -p /data/redis_cluster/redis_{6390,6391}/{conf,data,logs,pid} #准备配置文件 [root@redis-4 ~]# cat > /data/redis_cluster/redis_6390/conf/redis_6390.conf <<EOF bind $(ifconfig | awk 'NR==2{print $2}') port 6390 daemonize yes logfile /data/redis_cluster/redis_6390/logs/redis_6390.log pidfile /data/redis_cluster/redis_6390/pid/redis_6390.log dbfilename "redis_6390.rdb" dir /data/redis_cluster/redis_6390/data cluster-enabled yes cluster-config-file node_6390.conf cluster-node-timeout 15000 save 60 10000 save 300 10 save 900 1 EOF [root@redis-4 ~]# cat > /data/redis_cluster/redis_6391/conf/redis_6391.conf <<EOF bind $(ifconfig | awk 'NR==2{print $2}') port 6391 daemonize yes logfile /data/redis_cluster/redis_6391/logs/redis_6391.log pidfile /data/redis_cluster/redis_6391/pid/redis_6391.log dbfilename "redis_6391.rdb" dir /data/redis_cluster/redis_6391/data cluster-enabled yes cluster-config-file node_6391.conf cluster-node-timeout 15000 save 60 10000 save 300 10 save 900 1 EOF #启动redis [root@redis-4 ~]# ./redis_shell.sh start 6390 [root@redis-4 ~]# ./redis_shell.sh start 6391使用工具将redis-4加入集群在原来集群的任意一台机器安装了 ruby 环境即可操作。安装ruby环境//安装ruby管理工具 [root@redis-1 ~]# yum -y install rubygems //移除官网源 [root@redis-1 ~]# gem sources --remove https://rubygems.org/ https://rubygems.org/ removed from sources //增加阿里云源 [root@redis-1 ~]# gem sources -a http://mirrors.aliyun.com/rubygems/ http://mirrors.aliyun.com/rubygems/ added to sources //更新缓存 [root@redis-1 ~]# gem update --system ruby2.3.0以下版本执行会报错 //安装ruby支持redis的插件 [root@redis-1 ~]# gem install redis -v 3.3.5 Fetching: redis-3.3.5.gem (100%) Successfully installed redis-3.3.5 Parsing documentation for redis-3.3.5 Installing ri documentation for redis-3.3.5 1 gem installed将redis-4加入集群需要将redis-4的6390和6391端口都加入到集群,可以使用工具进行添加。命令: ./redis-trib.rb add-node 新节点:端口 现有集群:端口[root@redis-1 ~]# cd /data/redis_cluster/redis-3.2.9/src/ [root@redis-1 /data/redis_cluster/redis-3.2.9/src]# ./redis-trib.rb add-node 192.168.81.240:6390 192.168.81.210:6380 [root@redis-1 /data/redis_cluster/redis-3.2.9/src]# ./redis-trib.rb add-node 192.168.81.240:6391 192.168.81.210:6380查看集群信息,已经有8个节点[root@redis-1 ~]# redis-cli -h 192.168.81.210 -p 6380 cluster nodes ce75dacf45d3ad4f852b7fb4d359a295b8a2bcdd 192.168.81.220:6380 master - 0 1612424799243 2 connected 5461-10922 9b7641253ea66073d865accdd4460d2877f9ff5d 192.168.81.220:6381 master - 0 1612424801262 8 connected 10923-16383 b19722a1d3d482a2c6eaaec15e5e72018600389f 192.168.81.240:6391 master - 0 1612424797227 0 connected 6b6ca5d58187ecbf0bff15d71a5789f4aa78cfa2 192.168.81.240:6390 master - 0 1612424796216 9 connected bdd20b03b573b2def6a9ee5053a75867709fc908 192.168.81.210:6381 slave ce75dacf45d3ad4f852b7fb4d359a295b8a2bcdd 0 1612424796721 4 connected 812ae8ccf55d8994f1f9d30a20f6cff42fb24b4a 192.168.81.230:6380 slave 9b7641253ea66073d865accdd4460d2877f9ff5d 0 1612424800253 8 connected 904a0109976cae38e5a3059fd70ce2727a0ed8fb 192.168.81.230:6381 slave b61b8d0421b94b9de7267dda6c6f401a42622047 0 1612424798232 6 connected b61b8d0421b94b9de7267dda6c6f401a42622047 192.168.81.210:6380 myself,master - 0 0 1 connected 0-5460将槽位重新分配当新节点加入集群后,需要重新分配槽位,否则整个集群是无法使用的。命令格式: ./redis-trib.rb reshard 集群任意一个主库的ip:端口 分配的时候可以选择all,直接将所有节点分出一部分槽位迁移给新节点。也可以指定某个节点迁移出一部分槽位给新节点。所有节点分出槽位给新节点[root@redis-1 ~]# cd /data/redis_cluster/redis-3.2.9/src [root@redis-1 /data/redis_cluster/redis-3.2.9/src]# ./redis-trib.rb reshard 192.168.81.210:6380 How many slots do you want to move (from 1 to 16384)? 4096 //需要迁移的槽位数量,也就是要拿出多少个槽位给新节点,我们输入4096,因为16384除4刚好是4096 What is the receiving node ID? 6b6ca5d58187ecbf0bff15d71a5789f4aa78cfa2 //迁移给目标节点的ID号,也就是新节点的6390ID号,6390作为新节点的主库 Please enter all the source node IDs. Type 'all' to use all the nodes as source nodes for the hash slots. Type 'done' once you entered all the source nodes IDs. Source node #1:all //迁移方式:all将所有主节点分出一部分槽位给新节点 Do you want to proceed with the proposed reshard plan (yes/no)? yes //是否继续分配设置要迁移的槽位数量,填写4096填写要迁移到目标节点的ID号,也就是要迁移给谁,这里我们要迁移给新加的节点,我们要让新机器的6390节点成为主库,因此就填写6390节点的ID号。设置要从哪个节点上迁移槽位,可以一台一台的迁移,也可以填写all,all的意思是从所有节点上一共取出4096个槽位分给新机器,如果使用all迁移,会把所有主节点迁移出一部分槽位给新节点,执行完all直接就退出工具。我们使用all自动将所有主节点进行迁移,直接输入all即可自动迁移,一般都使用all。提示我们是否继续分配,我们选择yes迁移完成自动退出程序迁移指定节点的槽位给新节点前面步骤一致,只需要在source node选择指定节点即可。填写要迁移的主节点ID,填写完主机节点ID后,输入done,回车之后开始迁移数据。提示我们是否继续,我们输入yes图片开始数据迁移查看集群信息及状态当6390分配完槽位后,可以看下集群信息是否分配成功。[root@redis-1 ~]# redis-cli -h 192.168.81.210 -p 6380 cluster nodes可以看到6390上有3段槽位号,说明是从三个节点上分出来的,正好也验证了之前说的一句话,槽位顺序不一定要存在,只要槽位数量够就可以再次使用reshard命令即可看到都是4096个槽位查看集群状态[root@redis-1 ~]# redis-cli -h 192.168.81.210 -p 6380 cluster info cluster_state:ok cluster_slots_assigned:16384 cluster_slots_ok:16384 cluster_slots_pfail:0 cluster_slots_fail:0 cluster_known_nodes:8 #节点数已经是8个了 cluster_size:4 cluster_current_epoch:11 cluster_my_epoch:1 cluster_stats_messages_sent:67364 cluster_stats_messages_received:67293配置四主四从交叉复制目前是5个主节点3个从节点,显然是不合理的,我们要手动配置一些交叉复制实现四主四从。只需要操作192.168.81.230的6381端口和192.168.81.240的6391端口即可192.168.81.230的6381端口作为192.168.81.240的6390端口的从库192.168.81.240的6391作为192.168.81.210的6380端口的从库再配置与新节点交叉复制的时候,建议先操作192.168.81.230,这样192.168.81.210的主库就没有需要传输rdb文件到从库了,也可以减轻主库的压力,如果先让192.168.81.240配置交叉,这样一来192.168.81.210的主库就有2份复制了,主库就需要一次传输2份rdb文件,压力也就大了注意:先做192.168.81.230的交叉在做192.168.81.240的交叉配置四主四从交叉复制#将master主库的所有ID获取下来 [root@redis-1 ~]# redis-cli -h 192.168.81.210 -p 6380 cluster nodes | grep 'master' | awk '{print $1,$2}' ce75dacf45d3ad4f852b7fb4d359a295b8a2bcdd 192.168.81.220:6380 6b6ca5d58187ecbf0bff15d71a5789f4aa78cfa2 192.168.81.240:6390 812ae8ccf55d8994f1f9d30a20f6cff42fb24b4a 192.168.81.230:6380 b61b8d0421b94b9de7267dda6c6f401a42622047 192.168.81.210:6380 #建议在记事本里准备好命令 redis-3同步redis-4 192.168.81.230:6381> CLUSTER REPLICATE 6b6ca5d58187ecbf0bff15d71a5789f4aa78cfa2 redis-4同步redis-3 192.168.81.240:6391> CLUSTER REPLICATE b61b8d0421b94b9de7267dda6c6f401a42622047查看集群信息及状态[root@redis-1 ~]# redis-cli -h 192.168.81.210 -p 6380 cluster nodes [root@redis-1 ~]# redis-cli -h 192.168.81.210 -p 6380 cluster info cluster_state:ok cluster_slots_assigned:16384 cluster_slots_ok:16384 cluster_slots_pfail:0 cluster_slots_fail:0 cluster_known_nodes:8 cluster_size:4 cluster_current_epoch:11 cluster_my_epoch:1 cluster_stats_messages_sent:69698 cluster_stats_messages_received:69627 [root@redis-1 ~]# 已经是四主四从了,并且集群状态也是ok来源:https://jiangxl.blog.csdn.net/article/details/121329856(十四):Redis Cluster 集群收缩原理与实践Cluster 集群收缩概念当项目压力承载力过高时,需要增加节点来提高负载,当项目压力不是很大时,也希望能够将集群收缩下来,给其他项目使用 ,这就要用到集群收缩了集群收缩操作和集群扩容是一样的,只需要把方向反过来即可。扩容的时候执行一次命令就可以实现槽位迁移成功,而收缩的时候有几个主节点就需要执行多少次,比如除去要下线的节点,还有3个主节点,那么就需要执行三次,填写迁移出槽位的数量也需要除以3,每个节点也需要平均分配。收缩的时候首先要填写分出多少个槽位,然后填写要分给谁,最后填写从哪分出槽位,一般分多少个槽位,就需要看要下线的主机上有多少个槽位,然后除以集群主节点数,使每一个主机点分到的槽位都是相同的,填写要分配给谁的时候,第一次填写第一个主节点的ID,第二次填写第二个主节点的ID,最后填写提供槽位的节点ID,就是下线节点的ID号。集群收缩扩容槽位的时候不会影响数据的使用。集群收缩的源端就是要下线的主节点,目标端就是在线的主节点(分配给谁的节点)。咱们要清楚一点,只有主节点是有槽位的,因此呢需要将主节点的槽位分配给其他主节点,当槽位清空后,这个主机节点就可以下线了。收缩集群前后对比图 集群收缩操作步骤: 1.执行reshard命令将需要下线的主节点进行槽位分散。2.有几个主节点就需要执行几次reshard命令,首先填写要分出的槽位数,然后填写分给谁,最后填写从哪里分。3.当槽位分散完成后,要下线的主节点没有任何数据时,将节点从集群中删除。集群信息 目前集群时四主四从共8个节点,我们需要将集群改为三主三从,收缩出两个节点给其他程序使用。将6390主节点从集群中收缩计算需要分给每一个节点的槽位数可以看到6390节点上有4096个槽位,删除要下线的6390节点后,我们还有3个主节点,4096除3得到1365,分配槽位的时候给每个节点分配1365个槽位即可均匀。分配1365个槽位给192.168.81.210的6380节点我们需要将192.168.81.240的6390节点分出1365个槽位给192.168.81.210的6380节点。只需要把What is the receiving node ID填写成192.168.81.210的6380节点ID即可,指的是分配出来的槽位要给谁。然后source node填写192.168.81.240的6390节点的ID,这里指的是从哪个节点上分出1365个槽位,填写ID后,回车后会提示还要从哪个节点上分配槽位,因为只有6390需要分出槽位,所以在这里填写done,表示只有这个一个节点分出1365个槽位给其他节点。[root@redis-1 /data/redis_cluster/redis-3.2.9/src]# ./redis-trib.rb reshard 192.168.81.210:6380 How many slots do you want to move (from 1 to 16384)? 1365 #分配出多少个槽位 What is the receiving node ID? 80e256579658eb256c5b710a3f82c439665794ba #将槽位分给那个节点 Please enter all the source node IDs. Type 'all' to use all the nodes as source nodes for the hash slots. Type 'done' once you entered all the source nodes IDs. Source node #1:6bee155f136f40e28e1f60c8ddec3b158cd8f8e8 #从哪个节点分出槽位 Source node #2:done Do you want to proceed with the proposed reshard plan (yes/no)? yes #输入yes继续下面是收缩节点的过程截图。数据迁移过程。槽位分出迁移成功。分配1365个槽位给192.168.81.220的6380节点[root@redis-1 /data/redis_cluster/redis-3.2.9/src]# ./redis-trib.rb reshard 192.168.81.210:6380 How many slots do you want to move (from 1 to 16384)? 1365 #分配出多少个槽位 What is the receiving node ID? 10dc7f3f9a753140a8494adbbe5a13d0026451a1 #将槽位分给那个节点 Please enter all the source node IDs. Type 'all' to use all the nodes as source nodes for the hash slots. Type 'done' once you entered all the source nodes IDs. Source node #1:6bee155f136f40e28e1f60c8ddec3b158cd8f8e8 #从哪个节点分出槽位 Source node #2:done Do you want to proceed with the proposed reshard plan (yes/no)? yes #输入yes继续收缩过程截图展示。分配1365个槽位给192.168.81.230的6380节点[root@redis-1 /data/redis_cluster/redis-3.2.9/src]# ./redis-trib.rb reshard 192.168.81.210:6380 How many slots do you want to move (from 1 to 16384)? 1366 #分配出多少个槽位 What is the receiving node ID? a4381138fdc142f18881b7b6ca8ae5b0d02a3228 #将槽位分给那个节点 Please enter all the source node IDs. Type 'all' to use all the nodes as source nodes for the hash slots. Type 'done' once you entered all the source nodes IDs. Source node #1:6bee155f136f40e28e1f60c8ddec3b158cd8f8e8 #从哪个节点分出槽位 Source node #2:done Do you want to proceed with the proposed reshard plan (yes/no)? yes #输入yes继续 code here...收缩过程截图展示。当最后一个节点迁移完数据后,6390主节点槽位数变为0。查看当前集群槽位分配槽位及数据已经从6390即将下线的主机迁移完毕,可以看下当前集群三个主节点的槽位数。可以非常清楚的看到,现在每个主节点的槽位数为5461。如果觉得槽位重新分配后顺序不太满意,那么在执行一下reshard,把其它节点的槽位都分给192.168.81.210的6380上,这样一来,210的6380拥有的槽位就是0-16383,然后在将210的槽位一个节点分给5461个,分完之后,各节点的顺序就一致了。验证数据迁移过程是否导致数据异常多开几个窗口,一个执行数据槽位迁移,一个不断创建key,一个查看key的创建进度,一个查看key的数据。持续测试,发现没有任何数据异常,全部显示ok。将下线的主节点从集群中删除删除节点使用redis-trib删除一个节点,如果这个节点存在复制关系,有节点在复制当前节点或者当前节点复制别的节点的数据,redis-trib会自动处理复制关系,然后将节点删除,节点删除后会把对应的进程也停止运行。删除节点之前必须确保该节点没有任何槽位和数据,否则会删除失败。命令: ./redis-trib.rb del-node 节点IP:端口 ID[root@redis-1 /data/redis_cluster/redis-3.2.9/src]# ./redis-trib.rb del-node 192.168.81.240:6390 6bee155f136f40e28e1f60c8ddec3b158cd8f8e8 >>> Removing node 6bee155f136f40e28e1f60c8ddec3b158cd8f8e8 from cluster 192.168.81.240:6390 >>> Sending CLUSTER FORGET messages to the cluster... >>> SHUTDOWN the node. [root@redis-1 /data/redis_cluster/redis-3.2.9/src]# ./redis-trib.rb del-node 192.168.81.240:6391 f6b9320dfbc929ad5a31cdb149360b0fd8de2e60 >>> Removing node f6b9320dfbc929ad5a31cdb149360b0fd8de2e60 from cluster 192.168.81.240:6391 >>> Sending CLUSTER FORGET messages to the cluster... >>> SHUTDOWN the node.调整主从交叉复制删掉192.168.81.240服务器上的两个redis节点后,192.168.81.210服务器上的6380就没有了复制关系,我们需要把192.168.81.230的6381节点复制192.168.81.210的6380节点。[root@redis-1 ~]# redis-cli -h 192.168.81.230 -p 6381 192.168.81.230:6381> CLUSTER REPLICATE 80e256579658eb256c5b710a3f82c439665794ba OK当节点存在数据无法删除[root@redis-1 /data/redis_cluster/redis-3.2.9/src]# ./redis-trib.rb del-node 192.168.81.220:6380 10dc7f3f9a753140a8494adbbe5a13d0026451a1 >>> Removing node 10dc7f3f9a753140a8494adbbe5a13d0026451a1 from cluster 192.168.81.220:6380 [ERR] Node 192.168.81.220:6380 is not empty! Reshard data away and try again.将下线主机清空集群信息redis-trib虽然能够将节点在集群中删除,但是无法将其的集群信息清空,如果集群信息还有保留,那么该接地那就无法加入其它集群。在下线的redis节点上使用cluster reset删除集群信息即可。192.168.81.240:6390> CLUSTER reset OK来源:https://jiangxl.blog.csdn.net/article/details/121465277(十五):Redis 与Java\Php\Springboot 等应用的连接与使用前言我们之前对Redis的学习都是在命令行窗口,那么如何使用Java来对Redis进行操作呢?官方对于Java连接Redis的开发工具推荐了Jedis,通过Jedis同样可以实现对Redis的各种操作。本篇文章会介绍基于Linux上的Redis的Java连接操作。准备步骤修改配置文件redis.conf:(1)注释以下属性,因为我们是需要进行远程连接的:#bind:127.0.0.1(2)将protected-mode 设置为noprotected-mode no(3)设置为允许后台连接daemonize yes注意: 在远程服务器进行连接需要确保将以下三个步骤都完成:设置服务器的安全组开放6379端口防火墙开放端口:firewall-cmd --zone=public --add-port=6379/tcp --permanet重启防火墙:systemctl restart firewalld.serviceJedis连接Redis创建一个Maven项目,并导入以下依赖:<dependencies> <dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> <version>3.2.0</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.62</version> </dependency> </dependencies>测试连接:package com.yixin; import redis.clients.jedis.Jedis; public class RedisTest { public static void main(String[] args) { //连接本地的 Redis 服务 Jedis jedis = new Jedis("服务器地址", 6379); String response = jedis.ping(); System.out.println(response); // PONG } } code here...输出结果:看到PONG说明我们成功连接上了我们服务器上的Redis了!基本操作操作String数据类型package com.yixin; import redis.clients.jedis.Jedis; import java.util.Set; public class Redis_String { public static void main(String[] args) { //连接本地的 Redis 服务 Jedis jedis = new Jedis("服务器地址", 6379); String response = jedis.ping(); System.out.println(response); // PONG //删除当前选择数据库中的所有key System.out.println("删除当前选择数据库中的所有key:"+jedis.flushDB()); //Spring实例 //设置 redis 字符串数据 //新增<'name','yixin'>的键值对 jedis.set("name", "yixin"); // 获取存储的数据并输出 System.out.println("redis 存储的字符串为: "+ jedis.get("name")); //判断某个键是否存在 System.out.println("判断某个键是否存在:"+jedis.exists("name")); //系统中所有的键 Set<String> keys = jedis.keys("*"); System.out.println(keys); //按索引查询 System.out.println("按索引查询:"+jedis.select(0)); //查看键name所存储的值的类型 System.out.println("查看键name所存储的值的类型:"+jedis.type("name")); // 随机返回key空间的一个 System.out.println("随机返回key空间的一个:"+jedis.randomKey()); //重命名key System.out.println("重命名key:"+jedis.rename("name","username")); System.out.println("取出改后的name:"+jedis.get("username")); //删除键username System.out.println("删除键username:"+jedis.del("username")); //删除当前选择数据库中的所有key System.out.println("删除当前选择数据库中的所有key:"+jedis.flushDB()); //查看当前数据库中key的数目 System.out.println("返回当前数据库中key的数目:"+jedis.dbSize()); //删除数据库中的所有key System.out.println("删除所有数据库中的所有key:"+jedis.flushAll()); } }操作List数据类型 package com.yixin; import redis.clients.jedis.Jedis; import java.util.List; public class Redis_List { public static void main(String[] args) { //连接本地的 Redis 服务 Jedis jedis = new Jedis("服务器地址", 6379); String response = jedis.ping(); System.out.println(response); // PONG System.out.println("删除当前选择数据库中的所有key:"+jedis.flushDB()); //List实例 //存储数据到列表中 jedis.lpush("list", "num1"); jedis.lpush("list", "num2"); jedis.lpush("list", "num3"); // 获取存储的数据并输出 List<String> list = jedis.lrange("list", 0 ,-1); for(int i=0; i<list.size(); i++) { System.out.println("列表项为: "+list.get(i)); } } }输出结果: 事务操作package com.yixin; import com.alibaba.fastjson.JSONObject; import redis.clients.jedis.Jedis; import redis.clients.jedis.Transaction; public class Redis_Transaction { public static void main(String[] args) { //连接本地的 Redis 服务 Jedis jedis = new Jedis("服务器地址", 6379); String response = jedis.ping(); System.out.println(response); // PONG //事务测试 jedis.flushDB(); JSONObject jsonObject = new JSONObject(); jsonObject.put("hello","world"); jsonObject.put("name","yixin"); //开启事务 Transaction multi = jedis.multi(); String result = jsonObject.toJSONString(); // jedis.watch(result) try { multi.set("user1", result); multi.set("user2", result); int i = 1 / 0; // 代码抛出异常事务,执行失败! multi.exec(); // 执行事务! }catch (Exception e){ multi.discard();// 放弃事务 e.printStackTrace(); }finally { System.out.println(jedis.get("user1")); System.out.println(jedis.get("user2")); jedis.close(); } } }输出结果:对于其他命令也基本类似,就不一一演示出来了,之前学过的Redis命令,在Java中同样可以进行使用。SpringBoot集成Redis介绍这次我们并不使用jedis来进行连接,而是使用lettuce来进行连接,jedis和lettuce的对比如下:jedis:采用的直连,多个线程操作的话,是不安全的;想要避免不安全,使用jedis pool连接池。更像BIO模式lettuce:采用netty,实例可以在多个线程中共享,不存在线程不安全的情况;可以减少线程数量。更像NIO模式集成Redis创建Spring Boot项目导入依赖<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>编写配置文件application.properties:#配置redis # Redis服务器地址 spring.redis.host=服务器地址 # Redis服务器连接端口 spring.redis.port=6379 # Redis数据库索引(默认为0) spring.redis.database=0 # Redis服务器连接密码(默认为空) spring.redis.password= # 连接池最大连接数(使用负值表示没有限制) 默认 8 spring.redis.lettuce.pool.max-active=8 # 连接池最大阻塞等待时间(使用负值表示没有限制) 默认 -1 spring.redis.lettuce.pool.max-wait=-1 # 连接池中的最大空闲连接 默认 8 spring.redis.lettuce.pool.max-idle=8 # 连接池中的最小空闲连接 默认 0 spring.redis.lettuce.pool.min-idle=0编写测试类package com.yixin; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.data.redis.core.RedisTemplate; @SpringBootTest class SpringbootRedisApplicationTests { @Autowired private RedisTemplate<String,String> redisTemplate; @Test void contextLoads() { redisTemplate.opsForValue().set("name","yixin"); System.out.println(redisTemplate.opsForValue().get("name")); } }输出: 这样就已经成功连接了! 在这种连接方式中,redisTemplate操作着不同的数据类型,api和我们的指令是一样的。opsForValue:操作字符串 类似StringopsForList:操作List 类似ListopsForSet:操作Set,类似SetopsForHash:操作HashopsForZSet:操作ZSetopsForGeo:操作GeospatialopsForHyperLogLog:操作HyperLogLog除了基本的操作,我们常用的方法都可以直接通过redisTemplate操作,比如事务,和基本的CRUD。保存对象编写实体类注意:要实现序列号Serializable。package com.yixin.pojo; import java.io.Serializable; public class User implements Serializable { private String name; private int age; public User(){ } public User(String name, int age) { this.name = name; this.age = age; } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } @Override public String toString() { return "User{" + "name='" + name + '\'' + ", age=" + age + '}'; } }编写RedsTemplate配置Tip:在开发当中,我们可以直接把这个模板拿去使用。package com.yixin.config; import com.fasterxml.jackson.annotation.JsonAutoDetect; import com.fasterxml.jackson.annotation.PropertyAccessor; import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; import org.springframework.data.redis.serializer.StringRedisSerializer; @Configuration public class RedisConfig { @Bean @SuppressWarnings("all") public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) { //为了自己开发方便,一般直接使用 <String, Object> RedisTemplate<String, Object> template = new RedisTemplate<String, Object>(); template.setConnectionFactory(factory); // Json序列化配置 Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class); ObjectMapper om = new ObjectMapper(); om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); jackson2JsonRedisSerializer.setObjectMapper(om); // String 的序列化 StringRedisSerializer stringRedisSerializer = new StringRedisSerializer(); // key采用String的序列化方式 template.setKeySerializer(stringRedisSerializer); // hash的key也采用String的序列化方式 template.setHashKeySerializer(stringRedisSerializer); // value序列化方式采用jackson template.setValueSerializer(jackson2JsonRedisSerializer); // hash的value序列化方式采用jackson template.setHashValueSerializer(jackson2JsonRedisSerializer); template.afterPropertiesSet(); return template; } } code here...存储对象package com.yixin; import com.yixin.pojo.User; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.data.redis.core.RedisTemplate; @SpringBootTest class SpringbootRedisApplicationTests { @Autowired private RedisTemplate<String,Object> redisTemplate; @Test void contextLoads() { User user=new User("yixin",18); redisTemplate.opsForValue().set("user",user); System.out.println(redisTemplate.opsForValue().get("user")); } }输出结果:PHP 使用 Redis安装开始在 PHP 中使用 Redis 前, 我们需要确保已经安装了 redis 服务及 PHP redis 驱动,且你的机器上能正常使用 PHP。接下来让我们安装 PHP redis 驱动:下载地址为:https://github.com/phpredis/phpredis/releases。PHP安装redis扩展以下操作需要在下载的 phpredis 目录中完成:$ wget https://github.com/phpredis/phpredis/archive/3.1.4.tar.gz #下载 $ tar zxvf 3.1.4.tar.gz # 解压 $ cd phpredis-3.1.4 # 进入 phpredis 目录 $ /usr/local/php/bin/phpize # php安装后的路径 $ ./configure --with-php-config=/usr/local/php/bin/php-config #编译 $ make && make install修改php.ini文件vi /usr/local/php/lib/php.ini增加如下内容:extension_dir = "/usr/local/php/lib/php/extensions/no-debug-zts-20090626" extension=redis.so安装完成后重启php-fpm 或 apache 。查看phpinfo信息,就能看到redis扩展。连接到 redis 服务实例<?php //连接本地的 Redis 服务 $redis = new Redis(); $redis->connect('127.0.0.1', 6379); echo "Connection to server successfully"; //查看服务是否运行 echo "Server is running: " . $redis->ping(); ?>执行脚本,输出结果为:Connection to server successfully Server is running: PONGRedis PHP String(字符串) 实例<?php //连接本地的 Redis 服务 $redis = new Redis(); $redis->connect('127.0.0.1', 6379); echo "Connection to server successfully"; //设置 redis 字符串数据 $redis->set("tutorial-name", "Redis tutorial"); // 获取存储的数据并输出 echo "Stored string in redis:: " . $redis->get("tutorial-name"); ?>执行脚本,输出结果为:Connection to server successfully Stored string in redis:: Redis tutorialRedis PHP List(列表) 实例<?php //连接本地的 Redis 服务 $redis = new Redis(); $redis->connect('127.0.0.1', 6379); echo "Connection to server successfully"; //存储数据到列表中 $redis->lpush("tutorial-list", "Redis"); $redis->lpush("tutorial-list", "Mongodb"); $redis->lpush("tutorial-list", "Mysql"); // 获取存储的数据并输出 $arList = $redis->lrange("tutorial-list", 0 ,5); echo "Stored string in redis"; print_r($arList); ?>执行脚本,输出结果为:Connection to server successfully Stored string in redis Mysql Mongodb RedisRedis PHP Keys 实例<?php //连接本地的 Redis 服务 $redis = new Redis(); $redis->connect('127.0.0.1', 6379); echo "Connection to server successfully"; // 获取数据并输出 $arList = $redis->keys("*"); echo "Stored keys in redis:: "; print_r($arList); ?>执行脚本,输出结果为:Connection to server successfully Stored string in redis:: tutorial-name tutorial-list参考:https://www.runoob.com/redis/redis-php.html https://yixinforu.blog.csdn.net/article/details/122906569(十六):Redis 常用运维脚本设计思路redis 经常需要去管理,而编译安装的 redis 没有启动脚本以及运维相关的脚本,我们可以自己设计一个。脚本需求 :可以启动、关闭、重启redis启动:当redis没有运行的时候直接启动并输出启动成功,运行了就输出已经启动,避免重复进程关闭:如果进程存在就关闭并输出已经关闭,没有进程则直接输出redis没有启动重启:当进程存在就先执行关闭再启动,并输出重启成功,如果进程不存在直接执行启动可以查看redis进程可以登录redis可以查看redis日志由于redis是多端口实例,因此需要能够实现指定一个端口就能够启动这个端口的进程实现思路 :将所有的功能都做成函数通过判断$1输入的是什么指令,并执行对应的脚本编写脚本定义各种变量将redis部署路径、端口号、配置文件、主机IP都定义成变量。redis_port=$2 #redis端口 redis_name="redis_${redis_port}" #redis节点所在目录名称,即redis_6379 redis_home=/data/redis_cluster/${redis_name} #redis节点所在万年竹路径 redis_conf=${redis_home}/conf/${redis_name}.conf #redis配置文件路径 redis_host=`ifconfig ens33 | awk 'NR==2{print $2}'` #主机ip redis_pass=$3 #redis密码,用到了在登陆那边加个-a参数 red="\e[031m" green="\e[032m" yellow="\e[033m" black="\e[0m"编写使用模块主要实现如何使用这个脚本Usage(){ echo "usage: sh $0 {start|stop|restart|login|ps|logs|-h} PORT" }编写启动模块思路:首先判断指定端口的redis是否存在,如果不存在就执行启动命令,启动后输出启动成功,然后将开启的端口列出来。这里还需要判断一下state的值是不是空的,因为到重启模块需要判断,在重启模块会定义一个state值,这里检测到state的值为空就输出echo的内容,到了restart的时候如果进程一开始是没有的无需输出echo内容,主要是为了重启的时候不输出这些echo。启动后echo的时候,也会判断state的值,如果不为空就表示是重启了,就提示重启成功。Start(){ redis_cz=`netstat -lnpt | grep redis | grep "${redis_port}" | wc -l` if [ $redis_cz -eq 0 ];then redis-server ${redis_conf} if [ -z $state ];then echo -e "${green}redis ${redis_port}实例启动成功!${black}" else echo -e "${green}redis ${redis_port}实例重启成功!${black}" fi netstat -lnpt | grep ${redis_port} else if [ -z $state ];then echo -e "${yellow}redis "${redis_port}"实例已经是启动状态!${black}" netstat -lnpt | grep ${redis_port} fi fi }编写关闭模块思路:首先判断进程是否存在,如果存在就执行关闭命令,不存在就直接输出没有启动。这里还需要判断一下state的值是不是空的,因为到重启模块需要判断,在重启模块会定义一个state值,这里检测到state的值为空就输出echo的内容,到了restart的时候如果进程一开始是没有的无需输出echo内容,主要是为了重启的时候不输出这些echo。Stop(){ redis_cz=`netstat -lnpt | grep redis | grep "${redis_port}" | wc -l` if [ $redis_cz -gt 0 ];then redis-cli -h $redis_host -p $redis_port shutdown if [ -z $state ];then echo -e "${green}redis ${redis_port}实例关闭成功!" fi else if [ -z $state ];then echo -e "${red}redis "${redis_port}"实例没有启动!${black}" fi fi }编写重启模块思路:重启模块直接调用Stop模块和Start模块即可。重启模块一开始要增加一个state的变量,当执行stop模块的时候就去判断state的值,如果不为空即使是没有进程也不需要输出stop模块的echo命令,直接执行start,属于跳过某个命令的实现吧。Restart(){ state=restart Stop Start }编写登陆模块思路:首先判断redis有没有启动,如果没有启动就询问是否启动,按y启动,按n就退出。Login(){ redis_cz=`netstat -lnpt | grep redis | grep "${redis_port}" | wc -l` if [ $redis_cz -gt 0 ];then redis-cli -h $redis_host -p $redis_port else echo -e "${red}redis ${redis_port}实例没有启动!${black}" echo -en "${yellow}是否要启动reis? [y/n]${black}" read action case $action in y|Y) Start Login ;; n|N) exit 1 ;; esac fi }编写查看进程模块思路:直接用ps查即可。Ps(){ ps aux | grep redis }编写查看日志模块思路:配合各种变量去找到指令路径的日志即可。Logs(){ tail -f ${redis_home}/logs/${redis_name}.log }编写帮助信息模块思路:通过echo输出提示信息。Help(){ Usage echo "+-------------------------------------------------------------------------------+" echo "| start 启动redis |" echo "| stop 关闭redis |" echo "| restart 重启redis |" echo "| login 登陆redis |" echo "| ps 查看redis的进程信息,不需要加端口号 |" echo "| logs 查看redis日志持续输出 |" echo "| 除ps命令外,所有命令后面都需要加端口号 |" echo "+-------------------------------------------------------------------------------+" }编写判断脚本参数模块思路:判断脚本的参数是否不等于2,如果传入的参数不是两个的时候(因为很多模块都需要传入指令和端口这俩参数),再判断$1传入的值是不是ps和-h,因为ps和-h只需要一个参数即可,如果不是ps和-h,那么久输出使用方法,然后退出脚本。if [ $# -ne 2 ];then if [ "$1" != "ps" ] && [ "$1" != "-h" ];then Usage exit 1 fi fi编写指令判断模块思路:通过case实现,根据不同的指令执行不同的函数。case $1 in start) Start ;; stop) Stop ;; restart) Restart ;; login) Login ;; ps) Ps ;; logs) Logs ;; -h) Help ;; *) Help ;; esac整合脚本内容#!/bin/bash #redis控制脚本 redis_port=$2 redis_name="redis_${redis_port}" redis_home=/data/redis_cluster/${redis_name} redis_conf=${redis_home}/conf/${redis_name}.conf redis_host=`ifconfig ens33 | awk 'NR==2{print $2}'` redis_pass=$3 red="\e[031m" green="\e[032m" yellow="\e[033m" black="\e[0m" Usage(){ echo "usage: sh $0 {start|stop|restart|login|ps|logs|-h} PORT" } Start(){ redis_cz=`netstat -lnpt | grep redis | grep "${redis_port}" | wc -l` if [ $redis_cz -eq 0 ];then redis-server ${redis_conf} if [ -z $state ];then echo -e "${green}redis ${redis_port}实例启动成功!${black}" else echo -e "${green}redis ${redis_port}实例重启成功!${black}" fi netstat -lnpt | grep ${redis_port} else if [ -z $state ];then echo -e "${yellow}redis "${redis_port}"实例已经是启动状态!${black}" netstat -lnpt | grep ${redis_port} fi fi } Stop(){ redis_cz=`netstat -lnpt | grep redis | grep "${redis_port}" | wc -l` if [ $redis_cz -gt 0 ];then redis-cli -h $redis_host -p $redis_port shutdown if [ -z $state ];then echo -e "${green}redis ${redis_port}实例关闭成功!" fi else if [ -z $state ];then echo -e "${red}redis "${redis_port}"实例没有启动!${black}" fi fi } Restart(){ state=restart Stop Start } Login(){ redis_cz=`netstat -lnpt | grep redis | grep "${redis_port}" | wc -l` if [ $redis_cz -gt 0 ];then redis-cli -h $redis_host -p $redis_port else echo -e "${red}redis ${redis_port}实例没有启动!${black}" echo -en "${yellow}是否要启动reis? [y/n]${black}" read action case $action in y|Y) Start Login ;; n|N) exit 1 ;; esac fi } Ps(){ ps aux | grep redis } Logs(){ tail -f ${redis_home}/logs/${redis_name}.log } Help(){ Usage echo "+-------------------------------------------------------------------------------+" echo "| start 启动redis |" echo "| stop 关闭redis |" echo "| restart 重启redis |" echo "| login 登陆redis |" echo "| ps 查看redis的进程信息,不需要加端口号 |" echo "| logs 查看redis日志持续输出 |" echo "| 除ps命令外,所有命令后面都需要加端口号 |" echo "+-------------------------------------------------------------------------------+" } if [ $# -ne 2 ];then if [ "$1" != "ps" ] && [ "$1" != "-h" ];then Usage exit 1 fi fi case $1 in start) Start ;; stop) Stop ;; restart) Restart ;; login) Login ;; ps) Ps ;; logs) Logs ;; -h) Help ;; *) Help ;; esac使用 redis 运维脚本查看帮助信息[root@redis-1 ~]# sh redis_shell.sh -h usage: sh redis_shell.sh {start|stop|restart|login|ps|logs|-h} PORT +-------------------------------------------------------------------------------+ | start 启动redis | | stop 关闭redis | | restart 重启redis | | login 登陆redis | | ps 查看redis的进程信息,不需要加端口号 | | logs 查看redis日志持续输出 | | 除ps命令外,所有命令后面都需要加端口号 | +-------------------------------------------------------------------------------+启动redis第一次启动会提示启动成功,第二次在启动提示已经启动[root@redis-1 ~]# sh redis_shell.sh start 6379 redis 6379实例启动成功! tcp 0 0 127.0.0.1:6379 0.0.0.0:* LISTEN 101765/redis-server tcp 0 0 192.168.81.210:6379 0.0.0.0:* LISTEN 101765/redis-server [root@redis-1 ~]# sh redis_shell.sh start 6379 redis 6379实例已经是启动状态! tcp 0 0 127.0.0.1:6379 0.0.0.0:* LISTEN 101765/redis-server tcp 0 0 192.168.81.210:6379 0.0.0.0:* LISTEN 101765/redis-server [root@redis-1 ~]# 关闭 redis[root@redis-1 ~]# sh redis_shell.sh stop 6379 redis 6379实例关闭成功! [root@redis-1 ~]# [root@redis-1 ~]# sh redis_shell.sh stop 6379 redis 6379实例没有启动!重启 redis[root@redis-1 ~]# sh redis_shell.sh restart 6379 redis 6379实例重启成功! tcp 0 0 127.0.0.1:6379 0.0.0.0:* LISTEN 102654/redis-server tcp 0 0 192.168.81.210:6379 0.0.0.0:* LISTEN 102654/redis-server 登陆 redis启动了redis进行登陆[root@redis-1 ~]# sh redis_shell.sh login 6379 192.168.81.210:6379> DBSIZE (integer) 0没有启动redis进行登陆,首先询问是否启动,启动即可进入,不启动就退出[root@redis-1 ~]# sh redis_shell.sh login 6379 redis 6379实例没有启动! 是否要启动reis? [y/n]y redis 6379实例启动成功! tcp 0 0 127.0.0.1:6379 0.0.0.0:* LISTEN - tcp 0 0 192.168.81.210:6379 0.0.0.0:* LISTEN - 192.168.81.210:6379> DBSIZE (integer) 0 192.168.81.210:6379> exit [root@redis-1 ~]# sh redis_shell.sh stop 6379 redis 6379实例关闭成功! [root@redis-1 ~]# sh redis_shell.sh login 6379 redis 6379实例没有启动! 是否要启动reis? [y/n]n查看进程无需跟端口号[root@redis-1 ~]# sh redis_shell.sh ps avahi 6935 0.0 0.1 62272 2296 ? Ss 1月29 0:04 avahi-daemon: running [redis-1.local] root 79457 0.1 0.4 136972 7720 ? Ssl 2月01 1:43 redis-server 192.168.81.210:6380 [cluster] root 79461 0.1 0.4 136972 7688 ? Ssl 2月01 1:44 redis-server 192.168.81.210:6381 [cluster] root 101261 0.0 0.3 151888 5648 pts/2 S+ 13:10 0:01 vim redis_shell.sh root 102767 0.0 0.0 113176 1412 pts/0 S+ 13:51 0:00 sh redis_shell.sh ps root 102772 0.0 0.0 112728 968 pts/0 R+ 13:51 0:00 grep redis查看日志持续输出日志信息[root@redis-1 ~]# sh redis_shell.sh logs 6379来源:https://jiangxl.blog.csdn.net/article/details/121027928(十七):Redis 缓存问题(一致性、击穿、穿透、雪崩、污染)缓存存在的意义将一些数据(最近访问的)放在缓存中,当客户端需要访问数据库中数据时,可以先访问缓存,如果它里面存在这样对应的数据就不会去访问数据库,从而减小数据库的压力。那么客户端对数据库的操作有 增删改查,但是只有当查数据库里面的信息时才会先访问缓存,那么缓存里的数据时如何更新的?它会不会有数据更新不及时的问题?如何保证缓存和数据库数据一致性缓存数据插入的时机当客户端来说, 查询数据时的步骤如下 :1、首先到缓存查询数据,如果数据存在则直接获取数据返回2、如果缓存不存在,需要查询数据库,从数据库获取数据并插入缓存,将数据返回3、当第二次查询这个数据时并且这个数据在缓存中尚未过期,查询操作就可以查询缓存拿到对应的数据缓存更新数据(3种方案)客户端对数据库进行一个更改操作:1、先删除缓存在更新数据库进行更新数据库数据时,先删除缓存,然后更新数据库,后续的请求再次读取数据时,会从数据库中读取数据更新到缓存。存在问题:删除缓存之后,更新数据库之前,这个时间段内如果有新的请求过来,就会从数据库中读到旧的数据并写入缓存,再次造成数据不一致,并且后续读操作都是旧数据。2、先更新数据库在删除缓存进行更新操作,先更新数据库,成功之后,在删除缓存,后续请求将新数据写回缓存。存在问题:更新MySQL之后和删除缓存之前的这段时间内,请求读取的还是缓存内的旧数据,不过等数据库更新完成后,就会恢复一致。3、异步更新缓存数据库的更新操作完成后不直接操作缓存,将操作命令封装成消息放到消息队列里,然后由Redis自己去更新数据,消息队列保证数据操作数据的一致性,保证缓存数据的数据正常。缓存问题缓存穿透大量请求在数据库查不到相应数据概念缓存穿透是指用户想查询一个数据,发现Redis中没有,也就是缓存没有命中,就像持久性数据库发起查询,发现数据库也没有这个数据,于是查询失败了, 当用户请求很多的情况下,缓存没有命中,数据库也没有数据,会都直接访问数据库,给数据库造成很大的压力,这就是缓存穿透。 解决方案第一种解决方案:使用布隆过滤器判断对应的数据是否在这个数据库里,使用布隆过滤器,如果全返回1,则可能存在;如果返回结果存在一个不是1,那就肯定不在这个数据库中,这样就可以拒绝这个请求去访问数据库,大大降低数据库的压力。 布隆过滤器(Bloom Filter)的 核心实现是一个超大的位数组和几个哈希函数 。假设位数组的长度为m,哈希函数的个数为k。以上图为例,具体的操作流程:假设集合里面有3个元素{x, y, z},哈希函数的个数为3。首先将位数组进行初始化,将里面每个位都设置位0。对于集合里面的每一个元素,将元素依次通过3个哈希函数进行映射,每次映射都会产生一个哈希值,这个值对应位数组上面的一个点,然后将位数组对应的位置标记为1。查询W元素是否存在集合中的时候,同样的方法将W通过哈希映射到位数组上的3个点。如果3个点的其中有一个点不为1,则可以判断该元素一定不存在集合中。反之,如果3个点都为1,则该元素可能存在集合中。注意:此处不能判断该元素是否一定存在集合中,可能存在一定的误判率。可以从图中可以看到:假设某个元素通过映射对应下标为4,5,6这3个点。虽然这3个点都为1,但是很明显这3个点是不同元素经过哈希得到的位置,因此这种情况说明元素虽然不在集合中,也可能对应的都是1,这是误判率存在的原因。使用布隆过滤器之后,将存储的数据放入布隆过滤器,每次数据查询首先查询布隆过滤器,当在过滤器中判断存在时在到数据库缓存查询,如果没有进入数据查询,如果在过滤器不存在,则直接返回告诉用户该数据查不到,这样能大大减轻数据库查询压力。第二种方案:缓存空对象当数据库数据不存在时,及时返回的空对象也缓存起来,同时设置一个过期时间,之后在访问数据将从缓存中获取,保护了数据库。 存在问题: 1、对空值设置过期时间,会存在更新数据库数据到缓存数据失效的一段时间,缓存数据有问题,会对要保证数据一致性的业务造成影响2、会需要更多的空间来存储更多的控制,造成内存中有大量的空值的键缓存击穿请求量太大,缓存突然过期 缓存击穿是 指一个key是一个热点key,在不停的扛着大量的并发,当缓存中的key在失效的瞬间,持续的大并发就会穿破缓存,直接请求到数据库。对数据库造成瞬间压力过大。解决方案第一种方案:热点数据永不过期从缓存角度看,没有设置过期时间,就不会存在缓存过期之后产生的问题。第二种方案:加互斥锁使用分布式锁,保证对每个key的访问同一时刻只能一个线程去查询后端服务,其他没有获取锁权限的线程则等待即可。缓存雪崩在某一个时间段,缓存集中过期失效或者Redis宕机。 对于数据库而言,所有请求压力会全部到达数据库,导致数据库调用量暴增,可能也造成数据库宕机的情况。 解决方案第一种方案:Redis采用高可用这种方案的思路就是讲数据在Redis中存放在服务器上,即使一个服务器挂掉,其他服务器还可以继续工作。第二种方案:限流降级这种思路就是在缓存失效后,通过加锁或者队列来控制读取数据库的线程数量让线程在队列排队,控制整体请求速率。第三种方案:数据预热数据预热及时在正是部署服务之前,先访问一遍数据,可以将大部分的数据加载到缓存中,在即将发生大并发之前已经加载不同的key,设置不同的过期时间,让缓存失效的时间更加均匀。双写一致性含义双写一致性的含义就是: 保证缓存中的数据和DB中数据一致。单线程下的解决方案单线程下实际上就是指并发不大,或者说对缓存和DB数据一致性要求不是很高的情况。该问题就是经典的: 缓存+数据库读写的模式,就是 Cache Aside Pattern解决思路查询的时候,先查缓存,缓存中有数据,直接返回;缓存中没有数据,去查询数据库,然后更新缓存。更新DB的后,删除缓存。剖析:(1).为什么更新DB后,是删除缓存,而不是更新缓存呢?举个例子,比如该DB更新的频率很高,比如1min中内更新100次把,如果更新缓存,缓存也对应了更新了100次,但缓存在这一分钟内根本没被调用,或者说该缓存10min才可能会被查询一次,那么频繁更新缓存是不是就产生了很多不必要的开销呢。所以我们这里的思路是: 用到缓存的时候,才去计算缓存。 (2).该方案高并发场景下是否适用?不适用比如更新DB后,还有没有来得及删除缓存,别的请求就已经读取到缓存的数据了,此时读取的数据和DB中的实际的数据是不一致的。高并发下的解决方案使用内存队列解决,把 读请求 和 写请求 都放到队列中,按顺序执行(即串行化的方式解决) 。(要定义多个队列,不同的商品放到不同的队列中,换言之,同一个队列中只有一类商品)剖析:这种方案也有弊端,当并发量高了,队列容易阻塞,这个队列的位置,反而成了整个系统的瓶颈了,所以说100%完美的方案不存在,只有最适合的方案,没有最完美的方案。并发竞争含义多个微服务系统要同时操作redis的同一个key,比如正确的顺序是 A→B→C,A执行的时候,突然网络抖动了一下,导致B,C先执行了,从而导致整个流程业务错误。解决方案引入分布式锁(zookeeper 或 redis自身) 每个系统在操作之前,都要先通过 Zookeeper 获取分布式锁, 确保同一时间,只能有一个系统实例在操作这个个 Key,别系统都不允许读和写 。热点缓存key的重建优化背景开发人员使用“缓存+过期时间”的策略既可以加速数据读写,又保证数据的定期更新,这种模式基本能够满足绝大部分需求。但是有两个问题如果同时出现,可能就会对应用造成致命的危害:当前key是一个热点key(例如一个热门的娱乐新闻),并发量非常大。重建缓存不能在短时间完成,可能是一个复杂计算,例如复杂的SQL、多次IO、 多个依赖等。在缓存失效的瞬间,有大量线程来重建缓存,造成后端负载加大,甚至可能会让应用崩溃。解决方案要解决这个问题主要就是要 避免大量线程同时重建缓存。 我们可以利用 互斥锁 来解决,此方法 只允许一个线程重建缓存 ,其他线程等待重建缓存的线程执行完,重新从缓存获取数据即可。代码思路分享:String get(String key) { // 从Redis中获取数据 String value = redis.get(key); // 如果value为空, 则开始重构缓存 if (value == null) { // 只允许一个线程重建缓存, 使用nx, 并设置过期时间ex String mutexKey = "mutext:key:" + key; if (redis.set(mutexKey, "1", "ex 180", "nx")) { // 从数据源获取数据 value = db.get(key); // 回写Redis, 并设置过期时间 redis.setex(key, timeout, value); // 删除key\_mutex redis.delete(mutexKey); } else { //其它线程休息50ms,重写递归获取 Thread.sleep(50); get(key); } } return value; }缓存污染(或满了)缓存污染问题说的是缓存中一些只会被访问一次或者几次的的数据,被访问完后,再也不会被访问到,但这部分数据依然留存在缓存中,消耗缓存空间。 缓存污染会随着数据的持续增加而逐渐显露,随着服务的不断运行,缓存中会存在大量的永远不会再次被访问的数据。缓存空间是有限的,如果缓存空间满了,再往缓存里写数据时就会有额外开销,影响Redis性能。这部分额外开销主要是指写的时候判断淘汰策略,根据淘汰策略去选择要淘汰的数据,然后进行删除操作。最大缓存设置多大系统的设计选择是一个权衡的过程:大容量缓存是能带来性能加速的收益,但是成本也会更高,而小容量缓存不一定就起不到加速访问的效果。一般来说,我会建议 把缓存容量设置为总数据量的 15% 到 30%,兼顾访问性能和内存空间开销。 对于 Redis 来说,一旦确定了缓存最大容量,比如 4GB,你就可以使用下面这个命令来设定缓存的大小了:CONFIG SET maxmemory 4gb不过,缓存被写满是不可避免的, 所以需要数据淘汰策略。缓存淘汰策略Redis共支持 八种淘汰策略 ,分别是 noeviction、volatile-random、volatile-ttl、volatile-lru、volatile-lfu、allkeys-lru、allkeys-random 和 allkeys-lfu 策略。怎么理解呢?主要看分三类看:不淘汰noeviction(v4.0后默认的)对设置了过期时间的数据中进行淘汰随机:volatile-randomttl:volatile-ttllru:volatile-lrulfu:volatile-lfu全部数据进行淘汰随机:allkeys-randomlru:allkeys-lrulfu:allkeys-lfuBigKey的危害及优化什么是BigKey在Redis中,一个字符串最大512MB,一个二级数据结构(例如hash、list、set、zset)可以存储大约40亿个(2^32-1)个元素,但实际中如果下面两种情况,我就会认为它是bigkey。字符串类型:它的big体现在单个value值很大,一般认为超过10KB就是bigkey。非字符串类型:哈希、列表、集合、有序集合,它们的big体现在元素个数太多。一般来说, string类型控制在10KB以内,hash、list、set、zset元素个数不要超过5000 。反例:一个包含200万个元素的list。非字符串的bigkey,不要使用del删除,使用hscan、sscan、zscan方式渐进式删除,同时要注意防止bigkey过期时间自动删除问题(例如一个200万的zset设置1小时过期,会触发del操作,造成阻塞)BigKey的危害导致redis阻塞网络拥塞bigkey也就意味着每次获取要产生的网络流量较大,假设一个bigkey为1MB,客户端每秒访问量为1000,那么每秒产生1000MB的流量,对于普通的千兆网卡(按照字节算是128MB/s)的服务器来说简直是灭顶之灾,而且一般服务器会采用单机多实例的方式来部署,也就是说一个bigkey可能会对其他实例也造成影响,其后果不堪设想。过期删除 有个bigkey,它安分守己(只执行简单的命令,例如hget、lpop、zscore等),但它设置了过期时间,当它过期后,会被删除,如果没有使用Redis 4.0的过期异步删除(lazyfree-lazy-expire yes),就会存在阻塞Redis的可能性。BigKey的产生一般来说,bigkey的产生都是由于程序设计不当,或者对于数据规模预料不清楚造成的,来看几个例子:社交类 :粉丝列表,如果某些明星或者大v不精心设计下,必是bigkey。统计类 :例如按天存储某项功能或者网站的用户集合,除非没几个人用,否则必是bigkey。缓存类 :将数据从数据库load出来序列化放到Redis里,这个方式非常常用,但有两个地方需注意:第一,是不是有必要把所有字段都缓存;第二,有没有相关关联的数据,有的同学为了图方便把相关数据都存一个key下,产生bigkey。BigKey的优化拆big list:list1、list2、...listNbig hash:可以将数据分段存储,比如一个大的key,假设存了1百万的用户数据,可以拆分成200个key,每个key下面存放5000个用户数据合理采用数据结构 如果bigkey不可避免,也要思考一下要不要每次把所有元素都取出来(例如有时候仅仅需要hmget,而不是hgetall),删除也是一样,尽量使用优雅的方式来处理.反例:set user:1:name tom set user:1:age 19 set user:1:favor football推荐hash存对象hmset user:1 name tom age 19 favor football控制key的生命周期,redis不是垃圾桶。建议使用expire设置过期时间(条件允许可以打散过期时间,防止集中过期)。参考文章:https://blog.csdn.net/xkyjwcc/article/details/121704554 https://www.cnblogs.com/shoshana-kong/p/17226404.html(十八):Redis 内存消耗及回收Redis 是一个开源、高性能的 Key-Value 数据库,被广泛应用在服务器各种场景中。Redis 是一种内存数据库,将数据保存在内存中,读写效率要比传统的将数据保存在磁盘上的数据库要快很多。所以,监控 Redis 的内存消耗并了解 Redis 内存模型对高效并长期稳定使用 Redis 至关重要。在介绍之前先说明下,一般生产环境下,对开发同事不会开放直连 redis 集群的权限,一般是提供 daas 平台,通过可视化命令窗口,输入 redis 命令,一般只有 read 权限;对于 write 操作,需要提 redis 数据变更单,而对于 redis 内存、大 key、慢命令,一般都会将信息集成及中显示在监控看板,而不需要开发同事自己去输入命令;但是基本的相关知识还是要具备的。reids 内存分析redis 内存使用情况: info memory 示例:可以看到,当前节点内存碎片率为 226893824/209522728 ≈ 1.08,使用的内存分配器是 jemalloc。used_memory_rss 通常情况下是大于 used_memory 的,因为内存碎片的存在。但是 当操作系统把 redis 内存 swap 到硬盘时,memory_fragmentation_ratio 会小于 1 。redis 使用硬盘作为内存,因为硬盘的速度,redis 性能会受到极大的影响。redis 内存使用redis 的内存使用分布:自身内存,键值对象占用、缓冲区内存占用及内存碎片占用。redis 空进程自身消耗非常的少,可以忽略不计,优化内存可以不考虑此处的因素。对象内存对象内存,也即真实存储的数据所占用的内存。redis k-v 结构存储, 对象占用可以简单的理解为 k-size + v-size。 redis 的键统一都为字符串类型,值包含多种类型:string、list、hash、set、zset五种基本类型及基于 string 的 Bitmaps 和 HyperLogLog 类型等。在实际的应用中,一定要做好 kv 的构建形式及内存使用预期,。缓冲内存缓冲内存包括三部分: 客户端缓存、复制积压缓存及 AOF 缓冲区。客户端缓存接入redis服务器的TCP连接输入输出缓冲内存占用,TCP 输入缓冲占用是不受控制的,最大允许空间为 1G。输出缓冲占用可以通过 client-output-buffer-limit 参数配置。redis 客户端主要分为 从客户端、订阅客户端和普通客户端。从客户端连接占用也就是我们所说的 slave,主节点会为每一个从节点建立一条连接用于命令复制,缓冲配置为:client-output-buffer-limit slave 256mb 64mb 60。主从之间的间络延迟及挂载的从节点数量是影响内存占用的主要因素。因此在涉及需要异 地部署 主从时要特别注意,另外,也要 避免主节点上挂载过多的从节点(<=2);订阅客户端内存占用发布订阅功能连接客户端使用单独的缓冲区,默认配置:client-output-buffer-limit pubsub 32mb 8mb 60。当消费慢于生产时会造成缓冲区积压,因此需要特别注意消费者角色配比及生产、消费速度的监控。普通客户端内存占用除了上述之外的其它客户端,如我们通常的应用连接,默认配置:client-output-buffer-limit normal 1000。可以看到,普通客户端没有配置缓冲区限制,通常一般的客户端内存消耗也可以忽略不计。但是当 redis 服务器响应较慢时,容易造成大量的慢连接,主要表现为连接数的突增,如果不能及时处理,此时会严重影响 redis 服务节点的服务及恢复。关于此, 在实际应用中需要注意几点:maxclients 最大连接数配置必不可少。合理预估单次操作数据量(写或读)及网络时延 ttl。禁止线上大吞吐量命令操作,如 keys 等。高并发应用情景下,redis内存使用需要有实时的监控预警机制。复制积压缓冲区v2.8 之后提供的一个可重用的固定大小缓冲区,用以实现向从节点的部分复制功能,避免全量复制。配置单数:repl-backlog-size,默认 1M。单个主节点配置一个复制积压缓冲区。AOF缓冲区AOF重写期间增量的写入命令保存,此部分缓存占用大小取决于 AOF 重写时间及增量。内存碎片内存占用固定范围内存块儿分配。redis默认使用jemalloc内存分配器,其它包括glibc、tcmalloc。内存分配器会首先将可管理的内存分配为规定不同大小的内存块以备不同的数据存储需求,但是,我们知道实际应用中需要存储的数据大小不一,规范不一,内存分配器只能选择最接近数据需求大小的内存块儿进行分配,这样就伴随着“占不满”空间的碎片浪费。jemalloc针对内存碎片有相应的优化策略,正常碎片率为mem_fragmentation_ratio在1.03左右。第二部分我们说过,对string值得频繁append及range操作会会导致内存碎片问题,另外,第七部分,SDS惰性内存回收也会导致内存碎片,同时过期键内存回收也伴随着所释放空间的无法充分利用,导致内存碎片率上升的问题。碎片处理:应用层面:尽量避免差异化的键值使用,做好数据对齐。redis服务层面:可以通过重启服务,进行碎片整理。maxmemory 及 maxmemory-policyredis 基于以上配置控制 redis 最大可用内存及内存回收。需要注意的是内存回收执行影响redis的性能,避免频繁的内存回收开销。redis 子进程内存消耗子进程即 redis 执行持久化(RDB/AOF)时 fork 的子任务进程。关于 linux 系统的写时复制机制父子进程会共享相同的物理内存页 ,父进程处理写请求时会对需要修改的页复制一份副本进行修改,子进程读取的内存则为fork时的父进程内存快照,因此,子进程的内存消耗由期间的写操作增量决定。关于 linux 的透明大页机制THP(Transparent Huge Page)THP 机制会降低 fork 子进程的速度 :写时复制内存页由 4KB 增大至 2M。高并发情境下,写时复制内存占用消耗影响会很大,因此需要选择性关闭。关于linux配置一般需要配置 linux 系统 vm.overcommit_memory = 1 ,以允许系统可以分配所有的物理内存。防止fork任务因内存而失败。redis 内存管理redis 的内存管理主要分为两方面: 内存上限控制及内存回收管理 。内存上限:maxmemory目的:缓存应用内存回收机制触发 + 防止物理内存用尽(redis 默认无限使用服务器内存) + 服务节点内存隔离(单服务器上部署多个 redis 服务节点)在进行内存分配及限制时要充分考虑内存碎片占用影响。动态调整,扩展redis服务节点可用内存:config set maxmemory {}内存回收回收时机:键过期、内存占用达到上限过期键删除redis 键过期时间保存在内部的过期字典中,redis 采用惰性删除机制+定时任务删除机制。惰性删除即读时删除,读取带有超时属性的键时,如果键已过期,则删除然后返回空值。这种方式存在问题是,触发时机,加入过期键长时间未被读取,那么它将会一直存在内存中,造成内存泄漏。定时任务删除redis 内部维护了一个定时任务(默认每秒10次,可配置),通过自适应法进行删除。删除逻辑如下:需要说明的一点是,快慢模式执行的删除逻辑相同,这是超时时间不同。内存溢出控制当内存达到 maxmemory,会触发内存回收策略,具体策略依据 maxmemory-policy 来执行。noevication:默认不回收,达到内存上限,则不再接受写操作,并返回错误。volatile-lru:根据LRU算法删除设置了过期时间的键,如果没有则不执行回收。allkeys-lru:根据LRU算法删除键,针对所有键。allkeys-random:随机删除键。volatitle-random:随机删除设置了过期时间的键。volatilte-ttl:根据键ttl,删除最近过期的键,同样如果没有设置过期的键,则不执行删除。动态配置:config set maxmemory-policy {}在设置了maxmemory情况下,每次的redis操作都会检查执行内存回收,因此对于线上环境,要确保所这只的 maxmemory > used_memory。另外,可以通过动态配置 maxmemory 来主动触发内存回收。更多关于 Redis 学习的文章,请参阅:NoSQL 数据库系列之 Redis,本系列持续更新中。内存回收策略内存回收触发有两种情况,也就是 内存使用达到maxmemory上限时候触发的溢出回收 ,还有一种是我们设置了 ** 过期的对象到期的时候触发的到期释放的内存回收。Redis内存使用达到maxmemory上限时候触发的溢出回收;Redis 提供了几种策略 (maxmemory-policy) 来让用户自己决定该如何腾出新的空间以继续提供读写服务:(1)volatile-lru:从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰(2)volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰(3)volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰(4)allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key(这个是最常用的)(5)allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰(6)no-eviction:禁止驱逐数据,也就是说当内存不足以容纳新写入数据时,新写入操作会报错。这个应该没人使用吧!4.0版本后增加以下两种:(7)volatile-lfu:从已设置过期时间的数据集(server.db[i].expires)中挑选最不经常使用的数据淘汰(8)allkeys-lfu:当内存不足以容纳新写入数据时,在键空间中,移除最不经常使用的keyredis默认的策略就是noeviction策略,如果想要配置的话,需要在配置文件中写这个配置:maxmemory-policy volatile-lruRedis 的 LRU 算法LRU是Least Recently Used 近期最少使用算法,很多缓存策略都使用了这种策略进行空间的释放,在学习操作系统的内存回收的时候也用到了这种机制进行内存的回收,类似的还有LFU(Least Frequently Used)最不经常使用算法,这种算法。我们在上面的描述中也可以了解到,redis使用的是一种类似LRU的算法进行内存溢出回收的,其算法的代码:/* volatile-lru and allkeys-lru policy */ else if (server.maxmemory_policy == REDIS_MAXMEMORY_ALLKEYS_LRU || server.maxmemory_policy == REDIS_MAXMEMORY_VOLATILE_LRU) { struct evictionPoolEntry *pool = db->eviction_pool; while(bestkey == NULL) { evictionPoolPopulate(dict, db->dict, db->eviction_pool); /* Go backward from best to worst element to evict. */ for (k = REDIS_EVICTION_POOL_SIZE-1; k >= 0; k--) { if (pool[k].key == NULL) continue; de = dictFind(dict,pool[k].key); /* Remove the entry from the pool. */ sdsfree(pool[k].key); /* Shift all elements on its right to left. */ memmove(pool+k,pool+k+1, sizeof(pool[0])*(REDIS_EVICTION_POOL_SIZE-k-1)); /* Clear the element on the right which is empty * since we shifted one position to the left. */ pool[REDIS_EVICTION_POOL_SIZE-1].key = NULL; pool[REDIS_EVICTION_POOL_SIZE-1].idle = 0; /* If the key exists, is our pick. Otherwise it is * a ghost and we need to try the next element. */ if (de) { bestkey = dictGetKey(de); break; } else { /* Ghost... */ continue; } } } }Redis会基于server.maxmemory_samples配置选取固定数目的key,然后比较它们的lru访问时间,然后淘汰最近最久没有访问的key,maxmemory_samples的值越大,Redis的近似LRU算法就越接近于严格LRU算法,但是相应消耗也变高。所以,频繁的进行这种内存回收是会降低redis性能的,主要是查找回收节点和删除需要回收节点的开销。所以一般我们在配置redis的时候,尽量不要让它进行这种内存溢出的回收操作,redis是可以配置maxmemory,used_memory指的是redis真实占用的内存,但是由于操作系统还有其他软件以及内存碎片还有swap区的存在,所以我们实际的内存应该比redis里面设置的maxmemory要大,具体大多少视系统环境和软件环境来定。maxmemory也要比used_memory大,一般由于碎片的存在需要做1~2个G的富裕。来源:https://cnblogs.com/niejunlei/p/12898225.html(十九):Redis Key 过期时间相关的命令、注意事项、回收策略既然是缓存,就会涉及过期时间以及过期后清理回收内存的过程;注意:实际上,redis的内存回收触发有 两种情况 ,上面说的是一种,也就是我们设置了 过期的对象到期的时候触发的到期释放的内存回收 ,还有一种是 内存使用达到maxmemory上限时候触发的溢出回收 。概念生存时间:(Time To Live, TTL) ,经过指定的秒/毫秒之后,服务器自动删除TTL为0的key过期时间:(expire time) ,时间戳,表示一个具体时间点,到这个时间点后,服务器会删除key相关命令设置生存时间TTLEXPIRE key ttl #设置ttl,单位s PEXPIRE key ttl #设置ttl,单位ms可以对一个已经带有生存时间的 key 执行 EXPIRE 命令,新指定的生存时间会取代旧的生存时间。EXPIRE key seconds为给定 key 设置生存时间,当 key 过期时(生存时间为 0 ),它会被自动删除。在 Redis 中,带有生存时间的 key 被称为『易失的』(volatile)。生存时间可以通过使用 DEL 命令来删除整个 key 来移除,或者被 SET 和 GETSET 命令覆写(overwrite),这意味着,如果一个命令只是修改(alter)一个带生存时间的 key 的值而不是用一个新的 key 值来代替(replace)它的话,那么生存时间不会被改变。比如说,对一个 key 执行 INCR 命令,对一个列表进行 LPUSH 命令,或者对一个哈希表执行 HSET 命令,这类操作都不会修改 key 本身的生存时间。另一方面,如果使用 RENAME 对一个 key 进行改名,那么改名后的 key 的生存时间和改名前一样。RENAME 命令的另一种可能是,尝试将一个带生存时间的 key 改名成另一个带生存时间的 another_key ,这时旧的 another_key (以及它的生存时间)会被删除,然后旧的 key 会改名为 another_key ,因此,新的 another_key 的生存时间也和原本的 key 一样。使用 PERSIST 命令可以在不删除 key 的情况下,移除 key 的生存时间,让 key 重新成为一个『持久的』(persistent) key 。PEXPIRE key milliseconds这个命令和 EXPIRE 命令的作用类似,但是它以毫秒为单位设置 key 的生存时间,而不像 EXPIRE 命令那样,以秒为单位。返回值:设置成功返回 1 。当 key 不存在或者不能为 key 设置生存时间时(比如在低于 2.1.3 版本的 Redis 中你尝试更新 key 的生存时间),返回 0 。示例:redis> SET cache_page "www.AA.com" OK redis> EXPIRE cache_page 30 # 设置过期时间为 30 秒 (integer) 1 redis> TTL cache_page # 查看剩余生存时间 (integer) 23 redis> EXPIRE cache_page 30000 # 更新过期时间 (integer) 1 redis> TTL cache_page (integer) 29996 设置过期时间 (指定过期的时间节点)EXPIREAT key timestamp #设置expire time,s PEXPIREAT key timestamp #设置exprie time,msEXPIREAT key timestampEXPIREAT 的作用和 EXPIRE 类似,都用于为 key 设置生存时间。不同在于 EXPIREAT 命令接受的时间参数是 UNIX 时间戳(unix timestamp)。PEXPIREAT key milliseconds-timestamp这个命令和 EXPIREAT 命令类似,但它以毫秒为单位设置 key 的过期 unix 时间戳。过期时间的精确度在 Redis 2.4 版本中,过期时间的延迟在 1 秒钟之内 —— 也即是,就算 key 已经过期,但它还是可能在过期之后一秒钟之内被访问到,而在新的 Redis 2.6 版本中,延迟被降低到 1 毫秒之内。以上4种命令虽然各有不同,但是其底层都是使用 PEXPIREAT 实现的!删除和更新PERSIST key #移除生存时间PERSIST key移除给定 key 的生存时间,将这个 key 从『易失的』(带生存时间 key )转换成『持久的』(一个不带生存时间、永不过期的 key )。DLE 命令可以删除key,也会删除其生存时间SET 和 GETSET 命令也可以覆写生存时间查看剩余存活时间TTL key #计算key的剩余生存时间,s PTTL key #计算key的剩余生存时间,msTTL key以秒为单位,返回给定 key 的剩余生存时间(TTL, time to live)。PTTL key这个命令类似于TTL命令,但它以毫秒为单位返回 key 的剩余生存时间。返回值:当 key 不存在时,返回 -2 。当 key 存在但没有设置剩余生存时间时,返回 -1 。否则,以秒为单位,返回 key 的剩余生存时间。原理:过期时间如何保存redisDb结果的expires字典中保存了数据库中的所有key的过期时间,redisDb的声明如下:/* Redis database representation. There are multiple databases identified * by integers from 0 (the default database) up to the max configured * database. The database number is the 'id' field in the structure. */ //每个数据库都是一个redisDb,id为数据库编号 typedef struct redisDb { dict *dict; //键空间,保存了数据中所有键值对 dict *expires; //过期字典,保存了数据库中所有键的过期时间 dict *blocking_keys; dict *ready_keys; dict *watched_keys; struct evictionPoolEntry *eviction_pool; int id; /* Database ID */ long long avg_ttl; /* Average TTL, just for stats */ } redisDb;expires 的键是一个指针,指向某个键对象,值是一个 long long 类型整数,保存了过期时间,是一个毫秒精度的UNIX时间戳可见,过期时间的保存是使用key来作为关联的,所以操作用,修改key均可以修改过期时间,而只修改key的value,是不是改变其过期时间的;如何计算过期时间?底层的处理方式也很简单,获取key的生存时间戳,减去当前时间戳即可;如果键不存在,则返回-2;如果键没有设置过期时间,则返回-1;同样可以使用此方法判断key是否过期,TTL/PTTL 结果小于0,则表示过去,大于0,则表示未过期;Redis的key过期删除策略有哪些过期删除策略?定时删除 :设置键的过期时间的同时,设置一个定时器,来删除键惰性删除 :放任过期键不管,每次从键空间取值时,检查是否过期,以决定是否删除;定期删除 :每隔一段时间,进行一次数据库检查,删除里面的过期键,至于,要删除多少过期键,以及要检查多少数据库,由算法决定;各自的利弊?定时删除定时删除是指在设置键的过期时间的同时,创建一个定时器,让定时器在键的过期时间来临时,立即执行对键的删除操作。定时删除策略对内存是最友好的:通过使用定时器,定时删除策略可以保证过期键会尽可能快的被删除,并释放过期键所占用的内存。定时删除策略的缺点是,他对CPU时间是最不友好的:再过期键比较多的情况下,删除过期键这一行为可能会占用相当一部分CPU时间。除此之外,创建一个定时器需要用到Redis服务器中的时间事件。而当前时间事件的实现方式----无序链表,查找一个事件的时间复杂度为O(N)----并不能高效地处理大量时间事件。惰性删除惰性删除是指放任键过期不管,但是每次从键空间中获取键时,都检查取得的键是否过期,如果过期的话就删除该键,如果没有过期就返回该键。惰性删除策略对CPU时间来说是最友好的,但对内存是最不友好的。如果数据库中有非常多的过期键,而这些过期键又恰好没有被访问到的话,那么他们也许永远也不会被删除。Redis 的惰性删除策略由 db.c/expireIfNeeded 函数实现,所有读写Redis的命令在执行之前都会调用expireIfNeeded 函数定期删除定期删除是指每隔一段时间,程序就对数据库进行一次检查,删除里面的过期键。定期删除策略是前两种策略的一种整合和折中:定期删除策略每隔一段时间执行一次删除过期键操作,并通过限制删除操作执行的时长和频率来减少删除操作对CPU时间的影响。除此之外,通过定期删除过期键,定期删除策略有效地减少了因为过期键带来的内存浪费。定期删除策略的难点是确定删除操作执行的时长和频率:如果删除操作执行的太频繁或者执行的时间太长,定期删除策略就会退化成定时删除策略,以至于将CPU时间过多的消耗在删除过期键上面。如果删除操作执行的太少,或者执行的时间太短,定期删除策略又会和惰性删除策略一样,出现浪费内存的情况。redis的过期删除策略?那你有没有想过一个问题,Redis里面如果有大量的key,怎样才能高效的找出过期的key并将其删除呢,难道是遍历每一个key吗?假如同一时期过期的key非常多,Redis会不会因为一直处理过期事件,而导致读写指令的卡顿。这里说明一下,Redis是单线程的,所以一些耗时的操作会导致Redis卡顿,比如当Redis数据量特别大的时候,使用keys * 命令列出所有的key。Redis所有的键都可以设置过期属性,内部保存在过期字典中。由于进程内保存大量的键,维护每个键精准的过期删除机制会导致消耗大量的 CPU,对于单线程的Redis来说成本过高。因此—— Redis服务器实际使用的是惰性删除和定期删除两种策略 :通过配合使用这两种删除策略,服务器可以很好的在合理使用CPU时间和避免浪费内存空间之间取得平衡。惰性删除 :顾名思义,指的是不主动删除,当用户访问已经过期的对象的时候才删除种方式看似很完美,在访问的时候检查key的过期时间,最大的优点是节省cpu的开销,不用另外的内存和TTL链表来维护删除信息。但是如果一个key已经过期了,如果长时间没有被访问,那么这个key就会一直存留在内存之中,严重消耗了内存资源。定时任务删除 :为了弥补第一种方式的缺点,redis内部还维护了一个定时任务,默认每秒运行10次。定时任务中删除过期逻辑采用了自适应算法,使用快、慢两种速率模式回收键。定期删除 :Redis会将所有设置了过期时间的key放入一个字典中,然后每隔一段时间从字典中随机一些key检查过期时间并删除已过期的key。Redis默认每秒进行10次过期扫描:从过期字典中随机20个key删除这20个key中已过期的如果超过25%的key过期,则重复第一步同时,为了保证不出现循环过度的情况,Redis还设置了扫描的时间上限,默认不会超过25ms。图示: 流程说明:定时任务在每个数据库空间随机检查20个键,当发现过期时删除对应的键。如果超过检查数25%的键过期,循环执行回收逻辑直到不足25%或 运行超时为止,慢模式下超时时间为25毫秒。如果之前回收键逻辑超时,则在Redis触发内部事件之前再次以快模 式运行回收过期键任务,快模式下超时时间为1毫秒且2秒内只能运行1次。快慢两种模式内部删除逻辑相同,只是执行的超时时间不同。定期删除策略的实现过期键的定期删除策略由函数redis.c/activeExpireCycle实现,每当Redis服务器周期性操作redis.c/serverCron函数执行时,activeExpireCycle函数就会被调用,它在规定的时间内分多次遍历服务器中的各个数据库,从数据库的expires字典中随机检查一部分键的过期时间,并删除其中的过期键。redis过期时间的注意事项DEL/SET/GETSET等命令会清除过期时间在使用DEL、SET、GETSET等会覆盖key对应value的命令操作一个设置了过期时间的key的时候,会导致对应的key的过期时间被清除。//设置mykey的过期时间为300s 127.0.0.1:6379> set mykey hello ex 300 OK //查看过期时间 127.0.0.1:6379> ttl mykey (integer) 294 //使用set命令覆盖mykey的内容 127.0.0.1:6379> set mykey olleh OK //过期时间被清除 127.0.0.1:6379> ttl mykey (integer) -1INCR/LPUSH/HSET等命令则不会清除过期时间而在使用INCR/LPUSH/HSET这种只是修改一个key的value,而不是覆盖整个value的命令,则不会清除key的过期时间。INCR://设置incr_key的过期时间为300s 127.0.0.1:6379> set incr_key 1 ex 300 OK 127.0.0.1:6379> ttl incr_key (integer) 291 //进行自增操作 127.0.0.1:6379> incr incr_key (integer) 2 127.0.0.1:6379> get incr_key "2" //查询过期时间,发现过期时间没有被清除 127.0.0.1:6379> ttl incr_key (integer) 277LPUSH://新增一个list类型的key,并添加一个为1的值 127.0.0.1:6379> LPUSH list 1 (integer) 1 //为list设置300s的过期时间 127.0.0.1:6379> expire list 300 (integer) 1 //查看过期时间 127.0.0.1:6379> ttl list (integer) 292 //往list里面添加值2 127.0.0.1:6379> lpush list 2 (integer) 2 //查看list的所有值 127.0.0.1:6379> lrange list 0 1 1) "2" 2) "1" //能看到往list里面添加值并没有使过期时间清除 127.0.0.1:6379> ttl list (integer) 252PERSIST命令会清除过期时间当使用PERSIST命令将一个设置了过期时间的key转变成一个持久化的key的时候,也会清除过期时间。127.0.0.1:6379> set persist_key haha ex 300 OK 127.0.0.1:6379> ttl persist_key (integer) 296 //将key变为持久化的 127.0.0.1:6379> persist persist_key (integer) 1 //过期时间被清除 127.0.0.1:6379> ttl persist_key (integer) -1使用RENAME命令,老key的过期时间将会转到新key上在使用例如:RENAME KEY_A KEY_B命令将KEY_A重命名为KEY_B,不管KEY_B有没有设置过期时间,新的key KEY_B将会继承KEY_A的所有特性。//设置key_a的过期时间为300s 127.0.0.1:6379> set key_a value_a ex 300 OK //设置key_b的过期时间为600s 127.0.0.1:6379> set key_b value_b ex 600 OK 127.0.0.1:6379> ttl key_a (integer) 279 127.0.0.1:6379> ttl key_b (integer) 591 //将key_a重命名为key_b 127.0.0.1:6379> rename key_a key_b OK //新的key_b继承了key_a的过期时间 127.0.0.1:6379> ttl key_b (integer) 248这里篇幅有限,我就不一一将key_a重命名到key_b的各个情况列出来,大家可以在自己电脑上试一下key_a设置了过期时间,key_b没设置过期时间这种情况。使用EXPIRE/PEXPIRE设置的过期时间为负数或者使用EXPIREAT/PEXPIREAT设置过期时间戳为过去的时间会导致key被删除EXPIRE:127.0.0.1:6379> set key_1 value_1 OK 127.0.0.1:6379> get key_1 "value_1" //设置过期时间为-1 127.0.0.1:6379> expire key_1 -1 (integer) 1 //发现key被删除 127.0.0.1:6379> get key_1 (nil)EXPIREAT:127.0.0.1:6379> set key_2 value_2 OK 127.0.0.1:6379> get key_2 "value_2" //设置的时间戳为过去的时间 127.0.0.1:6379> expireat key_2 10000 (integer) 1 //key被删除 127.0.0.1:6379> get key_2 (nil)EXPIRE命令可以更新过期时间对一个已经设置了过期时间的key使用expire命令,可以更新其过期时间。//设置key_1的过期时间为100s 127.0.0.1:6379> set key_1 value_1 ex 100 OK 127.0.0.1:6379> ttl key_1 (integer) 95 更新key_1的过期时间为300s 127.0.0.1:6379> expire key_1 300 (integer) 1 127.0.0.1:6379> ttl key_1 (integer) 295在Redis2.1.3以下的版本中,使用expire命令更新一个已经设置了过期时间的key的过期时间会失败。并且对一个设置了过期时间的key使用LPUSH/HSET等命令修改其value的时候,会导致Redis删除该key。来源:https://blog.csdn.net/minghao0508/article/details/123895525(二十):Redis 性能优化与问题排查前言你们是否遇到过以下这些场景:在 Redis 上执行同样的命令,为什么有时响应很快,有时却很慢?为什么 Redis 执行 SET、DEL 命令耗时也很久?为什么我的 Redis 突然慢了一波,之后又恢复正常了?为什么我的 Redis 稳定运行了很久,突然从某个时间点开始变慢了?Redis真的变慢了吗?首先,在开始之前,你需要弄清楚Redis是否真的变慢了?如果你发现你的业务服务 API 响应延迟变长,首先你需要先排查服务内部,究竟是哪个环节拖慢了整个服务。比较高效的做法是,在服务内部集成链路追踪(打印日志的方式也可以),也就是在服务访问外部依赖的出入口,记录每次请求外部依赖的响应延时。如果你发现确实是操作 Redis 的这条链路耗时变长了,那么此刻你需要把焦点关注在业务服务到 Redis 这条链路上。Redis这条链路变慢的原因可能也有 2 个:业务服务器到 Redis 服务器之间的网络存在问题,例如网络线路质量不佳,网络数据包在传输时存在延迟、丢包等情况;Redis 本身存在问题,需要进一步排查是什么原因导致 Redis 变慢。第一种情况发生的概率比较小,如果有,找网络运维。我们这篇文章,重点关注的是第二种情况。什么是基准性能?排除网络原因,如何确认你的 Redis 是否真的变慢了?首先,你需要对 Redis 进行基准性能测试,了解你的 Redis 在生产环境服务器上的基准性能。基准性能就是指 Redis 在一台负载正常的机器上,其最大的响应延迟和平均响应延迟分别是怎样的?方式一:redis-cli --intrinsic-latency方式二:redis-benchmark使用复杂度过高的命令你需要去查看一下 Redis 的慢日志slowlog(又不会!)。Redis 提供了慢日志命令的统计功能,它记录了有哪些命令在执行时耗时比较久。redis.config文件:慢日志的阈值:CONFIG SET slowlog-log-slower-than 5000只保留最近 500 条慢日志 : CONFIG SET slowlog-max-len 500127.0.0.1:6379> SLOWLOG get 5 1) 1) (integer) 32693 # 慢日志ID 2) (integer) 1593763337 # 执行时间戳 3) (integer) 5299 # 执行耗时(微秒) 4) 1) "LRANGE" # 具体执行的命令和参数 2) "user_list:2000" 3) "0" 4) "-1"通过查看慢日志,我们就可以知道在什么时间点,执行了哪些命令比较耗时。经常使用 O(N) 以上复杂度的命令,例如 keys、flushdb类命令。使用O(N) 复杂度的命令,但 N 的值非常大。如:hgetall、lrange、smembers、zrange等并非不能使用,但是需要明确N的值。第一种情况导致变慢的原因在于,Redis 在操作内存数据时,时间复杂度过高 ,要花费更多的 CPU 资源。第二种情况导致变慢的原因在于,Redis 一次需要返回给客户端的数据过多 ,更多时间 花费在数据协议的组装和网络传输过程 中。另外,如果你的应用程序操作 Redis 的QPS不是很大,但 Redis 实例的 CPU 使用率却很高,那么很有可能是使用了复杂度过高的命令导致的。Redis 是单线程处理客户端请求的,如果你经常使用以上命令,那么当Redis处理客户端请求时,一旦前面某个命令发生耗时,就会导致后面的请求发生排队,对于客户端来说,响应延迟也会变长。操作 bigkey你查询慢日志发现,并不是复杂度过高的命令导致的,而都是 SET / DEL 这种简单命令出现在慢日志中,那么你就要怀疑你的实例否写入了bigkey。Redis 在写入数据时,需要为新的数据分配内存,相对应的,当从 Redis 中删除数据时,它会释放对应的内存空间。如果一个 key 写入的 value 非常大,那么 Redis 在分配内存时就会比较耗时。同样的,当删除这个 key 时,释放内存也会比较耗时,这种类型的 key 我们一般称之为 bigkey。如何扫描出实例中 bigkey 的分布情况呢?第一种:Redis 提供了扫描 bigkey 的命令,执行以下命令就可以扫描出,一个实例中 bigkey的分布情况:$ redis-cli -h 127.0.0.1 -p 6379 --bigkeys -i 0.01 ... -------- summary ------- Sampled 829675 keys in the keyspace! Total key length in bytes is 10059825 (avg len 12.13) Biggest string found 'key:291880' has 10 bytes Biggest list found 'mylist:004' has 40 items Biggest set found 'myset:2386' has 38 members Biggest hash found 'myhash:3574' has 37 fields Biggest zset found 'myzset:2704' has 42 members 36313 strings with 363130 bytes (04.38% of keys, avg size 10.00) 787393 lists with 896540 items (94.90% of keys, avg size 1.14) 1994 sets with 40052 members (00.24% of keys, avg size 20.09) 1990 hashs with 39632 fields (00.24% of keys, avg size 19.92) 1985 zsets with 39750 members (00.24% of keys, avg size 20.03)每种 数据类型(5个基础类型,不是全部数据) 所占用的最大内存 / 拥有最多元素的 key 是哪一个,以及每种数据类型在整个实例中的占比和平均大小 / 元素数量。使用这个命令的原理,就是 Redis 在内部执行了 SCAN 命令,遍历整个实例中所有的 key,然后针对 key 的类型,分别执行 STRLEN、LLEN、HLEN、SCARD、ZCARD 命令,来获取 String 类型的长度、容器类型(List、Hash、Set、ZSet)的元素个数。当执行这个命令时,要注意 2 个问题:对线上实例进行 bigkey 扫描时,Redis 的 OPS ( 每秒操作次数)会突增,为了降低扫描过程中对 Redis 的影响,最好控制一下扫描的频率,指定-i参数即可,它表示扫描过程中每次扫描后休息的时间间隔,单位是秒。扫描结果中,对于容器类型(List、Hash、Set、ZSet)的 key,只能扫描出元素最多的 key。但一个 key 的元素多,不一定表示占用内存也多,你还需要根据业务情况,进一步评估内存占用情况。第二种:rdb_bigkeys工具,go写的一款工具,分析rdb文件,找出文件中的大key,直接导出到csv文件,方便查看,个人推荐使用该工具去查找大key。工具地址:https://github.com/weiyanwei412/rdb_bigkeys针对 bigkey 导致延迟的问题,有什么好的解决方案呢?拒绝bigkey(十分推荐)导致redis阻塞网络拥塞过期删除:设置了过期时间,当它过期后,会被删除,如果没有使用Redis 4.0的过期异步删除(lazyfree-lazy-expire yes),就会存在阻塞Redis的可能性。Redis 是 4.0 以上版本,用 UNLINK 命令替代 DEL,此命令可以把释放 key 内存的操作,放到后台线程中去执行,从而降低对 Redis 的影响;Redis 是 4.0 以上版本,可以开启 lazy-free 机制(lazyfree-lazy-user-del = yes),在执行 DEL 命令时,释放内存也会放到后台线程中执行。bigkey 在很多场景下,依旧会产生性能问题。例如,bigkey 在分片集群模式下,对于 数据的迁移 也会有性能影响, 数据过期、数据淘汰、透明大页 ,都会受到bigkey的影响。集中过期如果你发现,平时在操作 Redis 时,并没有延迟很大的情况发生,但在某个时间点突然出现一波延时,其现象表现为:变慢的时间点很有规律,每间隔多久就会发生一波延迟。如果是出现这种情况,那么你需要排查一下,业务代码中是否存在设置大量 key 集中过期的情况。如果有大量的key在某个固定时间点集中过期,在这个时间点访问 Redis 时,就有可能导致延时变大。Redis对于过期键有三种清除策略:被动删除:当读/写一个已经过期的key时,会触发惰性删除策略,直接删除掉这个过期key;主动删除:由于惰性删除策略无法保证冷数据被及时删掉,所以Redis会定期主动淘汰一批已过期的key,Redis 内部维护了一个定时任务,默认每隔 100 毫秒就会从全局的过期哈希表中随机取出 20 个 key,然后删除其中过期的 key,如果过期 key 的比例超过了 25%,则继续重复此过程,直到过期 key 的比例下降到 25% 以下,或者这次任务的执行耗时超过了 25 毫秒,才会退出循环。这个主动过期 key 的定时任务,是在 Redis 主线程中执行的。当前已用内存超过maxmemory限定时,触发内存淘汰策略。也就是说如果在执行主动过期的过程中,出现了需要大量删除过期 key 的情况,那么此时应用程序在访问Redis时,必须要等待这个过期任务执行结束,Redis 才可以服务这个客户端请求。此时需要过期删除的是一个 bigkey,那么这个耗时会更久。而且,这个操作延迟的命令并不会记录在慢日志中。慢日志中没有操作耗时的命令,但我们的应用程序却感知到了延迟变大,其实时间都花费在了删除过期 key 上,这种情况我们需要尤为注意。解决方法集中过期 key 增加一个随机过期时间,把集中过期的时间打散,降低 Redis 清理过期key的压力;Redis 是 4.0 以上版本,可以开启lazy-free机制,当删除过期 key 时,把释放内存的操作放到后台线程中执行,避免阻塞主线程。实例内存达到上限原因在于,当 Redis 内存达到 maxmemory 后,每次写入新的数据之前,Redis 必须先从实例中踢出一部分数据,让整个实例的内存维持在 maxmemory 之下,然后才能把新数据写进来。allkeys-lru:不管 key 是否设置了过期,淘汰最近最少访问的 keyvolatile-lru:只淘汰最近最少访问、并设置了过期时间的 keyallkeys-random:不管 key 是否设置了过期,随机淘汰 keyvolatile-random:只随机淘汰设置了过期时间的 keyallkeys-ttl:不管 key 是否设置了过期,淘汰即将过期的 keynoeviction:不淘汰任何 key,实例内存达到 maxmeory 后,再写入新数据直接返回错allkeys-lfu:不管 - key 是否设置了过期,淘汰访问频率最低的 key(4.0+版本支持)volatile-lfu:只淘汰访问频率最低、并设置了过期时间 key(4.0+版本支持)Redis 的淘汰数据的逻辑与删除过期 key 的一样,也是在命令真正执行之前执行的,也就是说它也会增加我们操作 Redis 的延迟,而且,写 OPS 越高,延迟也会越明显。Redis 实例中还存储了 bigkey,那么在淘汰删除 bigkey 释放内存时,也会耗时比较久。优化建议:避免存储 bigkey,降低释放内存的耗时;淘汰策略改为随机淘汰,随机淘汰比 LRU 要快很多(视业务情况调整);拆分实例,把淘汰key的压力分摊到多个实例上;如果使用的是 Redis 4.0 以上版本,开启 layz-free 机制,把淘汰 key 释放内存的操作放到后台线程中执行(配置 lazyfree-lazy-eviction = yes);持久化/同步影响fork耗时严重操作 Redis 延迟变大,都发生在 Redis 后台 RDB 和AOF rewrite 期间,那你就需要排查,在这期间有可能导致变慢的情况。当 Redis 开启了后台 RDB 和 AOF rewrite 后,在执行时,它们都需要主进程创建出一个子进程进行数据的持久化。主进程创建子进程,会调用操作系统提供的fork函数。而 fork 在执行过程中,主进程需要拷贝自己的内存页表给子进程,如果这个实例很大,那么这个拷贝的过程也会比较耗时。而且这个 fork 过程会消耗大量的CPU资源,在完成fork之前,整个 Redis 实例会被阻塞住,无法处理任何客户端请求。如果此时你的 CPU 资源本来就很紧张,那么 fork 的耗时会更长,甚至达到秒级,这会严重影响 Redis 的性能。你可以在 Redis 上执行 INFO 命令,查看 latest_fork_usec 项,单位微秒。数据持久化会生成 RDB 之外,当主从节点第一次建立数据同步时,主节点也创建子进程生成 RDB,然后发给从节点进行一次全量同步,所以,这个过程也会对 Redis 产生性能影响。优化控制 Redis 实例的内存:尽量在 10G 以下,执行 fork 的耗时与实例大小有关,实例越大,耗时越久合理配置数据持久化策略:在 slave 节点执行 RDB 备份,推荐在低峰期执行,而对于丢失数据不敏感的业务(例如把 Redis 当做纯缓存使用),可以关闭 AOF 和 AOF rewrite。Redis 实例不要部署在虚拟机上:fork 的耗时也与系统也有关,虚拟机比物理机耗时更久。降低主从库全量同步的概率:适当调大· repl-backlog-size· 参数,避免主从全量同步。开启内存大页什么是内存大页?我们都知道,应用程序向操作系统申请内存时,是按内存页进行申请的,而常规的内存页大小是 4KB 。Linux 内核从2.6.38开始 ,支持了 内存大页 机制,该机制允许应用程序以 2MB 大小为单位,向操作系统申请内存。应用程序每次向操作系统申请的内存单位变大了,但这也意味着申请内存的耗时变长。主进程fork子进程后,此时的主进程依旧是可以接收写请求的,而进来的写请求,会采用 Copy On Write(写时复制)的方式操作内存数据。 主进程一旦有数据需要修改,Redis 并不会直接修改现有内存中的数据,而是先将这块内存数据拷贝出来,再修改这块新内存的数据。 写时复制你也可以理解成,谁需要发生写操作,谁就需要先拷贝,再修改。注意,主进程在拷贝内存数据时,这个阶段就涉及到新内存的申请,如果此时操作系统开启了内存大页,那么在此期间,客户端即便只修改 10B 的数据,Redis 在申请内存时也会以 2MB 为单位向操作系统申请,申请内存的耗时变长,进而导致每个写请求的延迟增加,影响到 Redis 性能。如果这个写请求操作的是一个 bigkey,那主进程在拷贝这个 bigkey 内存块时,一次申请的内存会更大,时间也会更久。可见,bigkey 在这里又一次影响到了性能。开启AOFAOF 配置为 appendfsync always,那么 Redis 每处理一次写操作,都会把这个命令写入到磁盘中才返回,整个过程都是在主线程执行的,这个过程必然会加重Redis写负担。AOF 配置为appendfsync no,Redis 每次写操作只写内存,什么时候把内存中的数据刷到磁盘,交给操作系统决定,对 Redis 的性能影响最小,但当 Redis 宕机时,会丢失一部分数据,为了数据的安全性。AOF 配置为appendfsync everysec ,当 Redis 后台线程在执行 AOF 文件刷盘时,如果此时磁盘的IO负载很高,那这个后台线程在执行刷盘操作(fsync系统调用)时就会被阻塞住。此时的主线程依旧会接收写请求,紧接着,主线程又需要把数据写到文件内存中(write 系统调用),但此时的后台子线程由于磁盘负载过高,导致 fsync 发生阻塞,迟迟不能返回,那主线程在执行 write 系统调用时,也会被阻塞住,直到后台线程fsync执行完成后,主线程执行 write 才能成功返回。我总结了以下几种情况,你可以参考进行 问题排查 :子进程正在执行 AOF rewrite,这个过程会占用大量的磁盘 IO 资源;有其他应用程序在执行大量的写文件操作,也会占用磁盘 IO 资源;Redis 的 AOF 后台子线程刷盘操作,撞上了子进程 AOF rewrite!Redis 提供了一个配置项,当子进程在 AOF rewrite 期间,可以让后台子线程不执行刷盘(不触发 fsync 系统调用)操作。这相当于在 AOF rewrite期间,临时把 appendfsync 设置为了 none,配置如下:# AOF rewrite 期间,AOF 后台子线程不进行刷盘操作 # 相当于在这期间,临时把 appendfsync 设置为了 none no-appendfsync-on-rewrite yes开启这个配置项,在 AOF rewrite 期间,如果实例发生宕机,那么此时会丢失更多的数据,性能和数据安全性,你需要权衡后进行选择。碎片整理Redis 的数据都存储在内存中,当我们的应用程序频繁修改Redis中的数据时,就有可能会导致 Redis产生内存碎片。内存碎片会降低 Redis 的内存使用率,我们可以通过执行INFO命令,得到这个实例的内存碎片率:used_memory 表示 Redis 存储数据的内存大小,used_memory_rss 表示操作系统实际分配给 Redis 进程的大小。mem_fragmentation_ratio> 1.5,说明内存碎片率已经超过了 50%,这时我们就需要采取一些措施来降低内存碎片了。解决的方案一般如下:如果你使用的是 Redis 4.0 以下版本,只能通过重启实例来解决如果你使用的是 Redis 4.0 版本,它正好提供了自动碎片整理的功能,可以通过配置开启碎片自动整理但是,开启内存碎片整理,它也有可能会导致Redis性能下降。原因在于,Redis 的碎片整理工作是也在主线程中执行的,当其进行碎片整理时,必然会消耗CPU资源,产生更多的耗时,从而影响到客户端的请求。其他原因频繁短连接:你的业务应用,应该使用长连接操作 Redis,避免频繁的短连接。其它程序争抢资源:其它程序占用 CPU、内存、磁盘资源,导致分配给 Redis 的资源不足而受到影响。总结你应该也发现了,Redis 的性能问题,涉及到的知识点非常广,几乎涵盖了 CPU、内存、网络、甚至磁盘的方方面面,同时,你还需要了解计算机的体系结构,以及操作系统的各种机制。从 资源使用 角度来看,包含的知识点如下:CPU相关:使用复杂度过高命令、数据的持久化,都与耗费过多的 CPU 资源有关内存相关:bigkey 内存的申请和释放、数据过期、数据淘汰、碎片整理、内存大页、内存写时复制都与内存息息相关磁盘相关:数据持久化、AOF 刷盘策略,也会受到磁盘的影响网络相关:短连接、实例流量过载、网络流量过载,也会降低 Redis 性能计算机系统:CPU 结构、内存分配,都属于最基础的计算机系统知识操作系统:写时复制、内存大页、Swap、CPU 绑定,都属于操作系统层面的知识优化的一些建议1、尽量使用短的key当然在精简的同时,不要为了key的“见名知意”。对于value有些也可精简,比如性别使用0、1。2、避免使用keys *keys *, 这个命令是阻塞的,即操作执行期间,其它任何命令在你的实例中都无法执行。当redis中key数据量小时到无所谓,数据量大就很糟糕了。所以我们应该避免去使用这个命令。可以去使用SCAN,来代替。3、在存到Redis之前先把你的数据压缩下redis为每种数据类型都提供了两种内部编码方式,在不同的情况下redis会自动调整合适的编码方式。4、设置key有效期我们应该尽可能的利用key有效期。比如一些临时数据(短信校验码),过了有效期Redis就会自动为你清除!5、选择回收策略(maxmemory-policy)当Redis的实例空间被填满了之后,将会尝试回收一部分key。根据你的使用方式,强烈建议使用 volatile-lru(默认) 策略——前提是你对key已经设置了超时。但如果你运行的是一些类似于 cache 的东西,并且没有对 key 设置超时机制,可以考虑使用 allkeys-lru 回收机制,具体讲解查看 。maxmemory-samples 3 是说每次进行淘汰的时候 会随机抽取3个key 从里面淘汰最不经常使用的(默认选项)。maxmemory-policy 六种方式 :volatile-lru #只对设置了过期时间的key进行LRU(默认值)allkeys-lru #是从所有key里 删除 不经常使用的keyvolatile-random #随机删除即将过期keyallkeys-random #随机删除volatile-ttl #删除即将过期的noeviction #永不过期,返回错误6、使用bit位级别操作和byte字节级别操作来减少不必要的内存使用bit位级别操作:GETRANGE, SETRANGE, GETBIT and SETBITbyte字节级别操作:GETRANGE and SETRANGE7、尽可能地使用hashes哈希存储8、当业务场景不需要数据持久化时,关闭所有的持久化方式可以获得最佳的性能数据持久化时需要在持久化和延迟/性能之间做相应的权衡.9、想要一次添加多条数据的时候可以使用管道10、限制redis的内存大小(64位系统不限制内存,32位系统默认最多使用3GB内存)数据量不可预估,并且内存也有限的话,尽量限制下redis使用的内存大小,这样可以避免redis使用swap分区或者出现OOM错误。(使用swap分区,性能较低,如果限制了内存,当到达指定内存之后就不能添加数据了,否则会报OOM错误。可以设置maxmemory-policy,内存不足时删除数据)11、SLOWLOG [get/reset/len]slowlog-log-slower-than #它决定要对执行时间大于多少微秒(microsecond,1秒 = 1,000,000 微秒)的命令进行记录。 slowlog-max-len #它决定 slowlog 最多能保存多少条日志,当发现redis性能下降的时候可以查看下是哪些命令导致的。来源:https://blog.csdn.net/weixin_42128977/article/details/127622146(二十一):Redis 性能测试及相关工具使用为什么需要性能测试?性能测试可以让我们了解 Redis 服务器的性能优劣。在实际的业务场景中,性能测试是必不可少的。在业务系统上线之前,我们都需要清楚地了解 Redis 服务器的性能,从而避免发生某些意外情况,比如数据量过大会导致服务器宕机等。本文将介绍几种不同的方式对Redis的性能进行相关的测试,大家可以根据自己的实际使用需求来选择不同的工具。redis-benchmark 介绍为了解 Redis 在不同配置环境下的性能表现,Redis 提供了一种性能测试工具 redis-benchmark(也称压力测试工具),它通过同时执行多组命令实现对 Redis 的性能测试。语法格式redis-benchmark [option] [option value] option #可选参数。 option value #具体的参数值。注意:该命令是在 redis 的目录下执行的,而不是 redis 客户端的内部指令。参数说明Usage: redis-benchmark [-h ] [-p ] [-c ] [-n ] [-k ]-h #设置redis服务端 IP (default 127.0.0.1) -p #设置redis服务端 端口 (default 6379)-a #设置redis服务端 密码-c #设置多少个redis客户端并发连接redis服务端 (default 50)-d #设置每次SET/GET值的数据大小,默认3字节 “VXK” (default 3)-n #设置请求总数,若默认50个客户端,每个客户端只需要请求2000次 (default 100000)-q #只显示每种类型测试 读/写 的秒数(不会输出大片测试过程)-l #闭环模式,测试完后,循环上一次测试,(就是命令永远循环)-r #设置指定数量的键;对SET/GET/INCR使用随机键,对SADD使用随机值,ZADD的随机成员和分数。 # 注:-r会被应用到key和counter键,并且拼接12位后缀标识为多少个;如 "key:000000000008" 代表生成的第八个键-P #选项代表每个请求pipeline的数据量. Default 1 (no pipeline).-t #设置选择性的测试操作,它只会对我们指定的命令测试 如 -t SET,SPOP,LPUSH (低版本只能测试17个命令) 注:看下面的性能测试方式,选择其中指定方式测试-I #空闲模式。只需打开N个空闲连接并等待-s #Server socket (覆盖主机和端口)-k #1=保持活动状态 0=重新连接 (default 1) # 注:默认测试是一旦第一次连接,后面就不会断开,直到测试完成, # 注:若是 1 则代表每次请求完成则断开连接,下次请求再重新连接 # --csv #以CSV格式输出,方便我们统计Excel等处理--user #用于发送ACL样式的“验证用户名密码”。需要 -a。--dbnum #选择指定的数据库号进行测试,redis默认数据库为(0~16) (default 0)--threads #启动多线程模式来测试--cluster #启用集群模式来测试--enable-tracking #启动测试之前发送客户端跟踪--help #帮助文档--version #显示版本号测试案例①:连接redis服务器并测试以50个客户端并发(平分每个客户端2000次)访问100000次./redis-benchmark -h 127.0.0.1 -p 6379 -c 50 -n 100000 ②:不输出测试过程,只显示当前测试案例结束的时间./redis-benchmark -h 127.0.0.1 -p 6379 -c 50 -n 100000 -q④:综合上面,并设置每个请求的请求值的大小字节./redis-benchmark -h 127.0.0.1 -p 6379 -c 50 -n 100000 -q -d 5④:综合上面,并设置指定的测试案例./redis-benchmark -h 127.0.0.1 -p 6379 -c 50 -n 100000 -q -d 5 -t SET,SADD,ZADD,GET随机 set/get 100万条命令,1000 个并发./redis-benchmark -a 123456 -h 192.168.61.129 -p 6379 -t set,get -r 1000000 -n 1000000 -c 1000测试输出的格式说明:(以 SET 测试案例说明)./redis-benchmark -h 127.0.0.1 -p 6379 -c 1000 -n 1000000 -t SET ====== SET ====== 1000000 requests completed in 18.75 seconds -- 1000000 请求用时 18.75 秒 1000 parallel clients -- 每个客户端请求次数1000 3 bytes payload -- 每次测试请求字节大小为 3byte keep alive: 1 -- 保持活力模式 1 一直连接 (0则代表每次请求从新连接) host configuration "save": 3600 1 300 100 60 10000 host configuration "appendonly": no -- 上面两个主机配置 持久化方式关闭 multi-thread: no -- 不是多线程测试 Latency by percentile distribution: 0.000% <= 6.159 milliseconds (cumulative count 1) 50.000% <= 13.191 milliseconds (cumulative count 500348) 75.000% <= 15.687 milliseconds (cumulative count 750168) 98.438% <= 26.799 milliseconds (cumulative count 984415) 100.000% <= 49.151 milliseconds (cumulative count 1000000) 100.000% <= 49.151 milliseconds (cumulative count 1000000) Cumulative distribution of latencies: 0.000% <= 0.103 milliseconds (cumulative count 0) 1.234% <= 7.103 milliseconds (cumulative count 12338) 12.473% <= 9.103 milliseconds (cumulative count 124733) 49.156% <= 13.103 milliseconds (cumulative count 491559) 100.000% <= 48.127 milliseconds (cumulative count 999999) 100.000% <= 50.111 milliseconds (cumulative count 1000000) Summary: throughput summary: 53339.02 requests per second -- 吞吐量摘要:每秒53339.02个请求 latency summary (msec): --延迟摘要(毫秒) avg min p50 p95 p99 max 13.639 6.152 13.191 21.663 28.351 49.151 memtier_benchmark 的使用memtier_benchmark是Redis Labs推出的一款命令行工具。它可以根据需求生成多种结构的数据对数据库进行压力测试,以了解目标数据库的性能极限。其部分功能特性如下。支持Redis和Memcached数据库测试。支持多线程、多客户端测试。可设置测试中的读写比例(SET: GET Ratio)。可自定义测试中键的结构。支持设置随机过期时间。使用教程如下安装依赖memtier_benchmark的安装依赖以下依赖包:Git、libevent 2.0.10或更高版本、libpcre 8.x、autoconf、automake、GNU make、GCC C++ compiler。yum install git yum install autoconf automake make gcc-c++ yum install pcre-devel zlib-devel libmemcached-devel # 如您系统中的libevent库不符合要求,下载并安装libevent-2.0.21 wget https://github.com/downloads/libevent/libevent/libevent-2.0.21-stable.tar.gz tar xfz libevent-2.0.21-stable.tar.gz pushd libevent-2.0.21-stable ./configure make sudo make install popd # 设置PKG_CONFIG_PATH使configure能够发现前置步骤安装的库。 export PKG_CONFIG_PATH=/usr/local/lib/pkgconfig:${PKG_CONFIG_PATH}下载并编译memtier_benchmarkgit clone https://github.com/RedisLabs/memtier_benchmark.git cd memtier_benchmark autoreconf -ivf ./configure make make install测试方法使用示例:./memtier_benchmark -s r-XXXX.redis.rds.aliyuncs.com -p 6379 -a XXX -c 20 -d 32 --threads=10 --ratio=1:1 --test-time=1800 --select-db=10具体参数如下:-s #Redis数据库的连接地址-a #Redis数据库的密码-c #测试中模拟连接的客户端数量-d #测试使用的对象数据的大小--threads #测试中使用的线程数--ratio #测试命令的读写比率(SET:GET Ratio)--test-time #测试时长(单位:秒)--select-db #测试使用的DB数量python脚本对redis进行测试除了使用redis-benchmark和memtier_benchmark,我们也可以使用python脚本对Redis进行性能测试。首先需要安装python版本的Redis:pip install redis接着就可以编码连接Redis,并且进行测试:简单连接import redis # 创建Redis对象进行连接 # 参数:decode_responses是否解码返回值 r = redis.Redis(host = 'localhost', port = 6379, password = '123456', decode_responses = True) # 终端下的命令在代码中都是函数 r.set('name', 'xiaoming') print(r.get('name'))连接池多个redis对象使用同一个连接池进行连接,避免了多次连接、断开等操作的系统开销import redis # 创建连接池,减少了多次的连接、断开的开销 pool = redis.ConnectionPool(password = '123456', decode_responses = True) # 创建Redis对象 r = redis.Redis(connection_pool = pool) print(r.get('name'))使用管道管道可以记录多个操作,然后一次将操作发送至数据库,避免了多次向服务器发送少量的数据,多个操作可以依次进行保存,然后发送,也可以进行连贯操作。import redis # 创建连接池,减少了多次的连接、断开的开销 pool = redis.ConnectionPool(password = '123456', decode_responses = True) # 创建Redis对象 r = redis.Redis(connection_pool = pool) # 创建管道 pipe = r.pipeline() # 保存记录操作 #pipe.set('name', 'dahua') #pipe.set('age', 20) # 执行操作(发送到服务器),一次可以执行多个操作,可以避免多次的想服武器发送数据 #pipe.execute() # 也可以进行连贯操作 pipe.set('name', 'haha').set('age', 10).execute() print(r.get('name'))参考文章:https://cnblogs.com/uestc2007/p/16962523.html https://blog.csdn.net/MOU_IT/article/details/121522395(二十二):Redis 运维监控(指标、体系建设、工具使用)如何理解Redis监控呢Redis运维和监控的意义不言而喻,我认为主要从如下三方面去构建认知体系:首先是Redis自身提供了哪些状态信息,以及有哪些常见的命令可以获取Redis的监控信息;其次需要知道一些常见的UI工具可以可视化的监控Redis;最后需要理解Redis的监控体系;Redis用的好不好,如何让它更好,这是运维要做的;本文主要在 Redis自身状态及命令,可视化监控工具,以及Redis监控体系等方面帮助你构建对redis运维/监控体系的认知,它是性能优化的前提。Redis 自身状态及命令如果只是想简单看一下Redis的负载情况的话,完全可以用它提供的一些命令来完成。状态信息 - infoRedis提供的INFO命令不仅能够查看实时的吞吐量(ops/sec),还能看到一些有用的运行时信息。info查看所有状态信息[root@redis_test_vm ~]# redis-cli -h 127.0.0.1 127.0.0.1:6379> auth xxxxx OK 127.0.0.1:6379> info # Server redis_version:3.2.3 #redis版本号 redis_git_sha1:00000000 #git sha1摘要值 redis_git_dirty:0 #git dirty标识 redis_build_id:443e50c39cbcdbe0 #redis构建id redis_mode:standalone #运行模式:standalone、sentinel、cluster os:Linux 3.10.0-514.16.1.el7.x86_64 x86_64 #服务器宿主机操作系统 arch_bits:64 服务器宿主机CUP架构(32位/64位) multiplexing_api:epoll #redis IO机制 gcc_version:4.8.5 #编译 redis 时所使用的 GCC 版本 process_id:1508 #服务器进程的 PID run_id:b4ac0f9086659ce54d87e41d4d2f947e19c28401 #redis 服务器的随机标识符 (用于 Sentinel 和集群) tcp_port:6380 #redis服务监听端口 uptime_in_seconds:520162 #redis服务启动以来经过的秒数 uptime_in_days:6 #redis服务启动以来经过的天数 hz:10 #redis内部调度(进行关闭timeout的客户端,删除过期key等等)频率,程序规定serverCron每秒运行10次 lru_clock:16109450 #自增的时钟,用于LRU管理,该时钟100ms(hz=10,因此每1000ms/10=100ms执行一次定时任务)更新一次 executable:/usr/local/bin/redis-server config_file:/data/redis-6380/redis.conf 配置文件的路径 # Clients connected_clients:2 #已连接客户端的数量(不包括通过从属服务器连接的客户端) client_longest_output_list:0 #当前连接的客户端当中,最长的输出列表 client_biggest_input_buf:0 #当前连接的客户端当中,最大输入缓存 blocked_clients:0 #正在等待阻塞命令(BLPOP、BRPOP、BRPOPLPUSH)的客户端的数量 # Memory used_memory:426679232 #由 redis 分配器分配的内存总量,以字节(byte)为单位 used_memory_human:406.91M #以可读的格式返回 redis 分配的内存总量(实际是used_memory的格式化) used_memory_rss:443179008 #从操作系统的角度,返回 redis 已分配的内存总量(俗称常驻集大小)。这个值和 top 、 ps等命令的输出一致 used_memory_rss_human:422.65M # redis 的内存消耗峰值(以字节为单位) used_memory_peak:426708912 used_memory_peak_human:406.94M total_system_memory:16658403328 total_system_memory_human:15.51G used_memory_lua:37888 # Lua脚本存储占用的内存 used_memory_lua_human:37.00K maxmemory:0 maxmemory_human:0B maxmemory_policy:noeviction mem_fragmentation_ratio:1.04 # used_memory_rss/ used_memory mem_allocator:jemalloc-4.0.3 # Persistence loading:0 #服务器是否正在载入持久化文件,0表示没有,1表示正在加载 rdb_changes_since_last_save:3164272 #离最近一次成功生成rdb文件,写入命令的个数,即有多少个写入命令没有持久化 rdb_bgsave_in_progress:0 #服务器是否正在创建rdb文件,0表示否 rdb_last_save_time:1559093160 #离最近一次成功创建rdb文件的时间戳。当前时间戳 - rdb_last_save_time=多少秒未成功生成rdb文件 rdb_last_bgsave_status:ok #最近一次rdb持久化是否成功 rdb_last_bgsave_time_sec:-1 #最近一次成功生成rdb文件耗时秒数 rdb_current_bgsave_time_sec:-1 #如果服务器正在创建rdb文件,那么这个域记录的就是当前的创建操作已经耗费的秒数 aof_enabled:0 #是否开启了aof aof_rewrite_in_progress:0 #标识aof的rewrite操作是否在进行中 aof_rewrite_scheduled:0 #rewrite任务计划,当客户端发送bgrewriteaof指令,如果当前rewrite子进程正在执行,那么将客户端请求的bgrewriteaof变为计划任务,待aof子进程结束后执行rewrite aof_last_rewrite_time_sec:-1 #最近一次aof rewrite耗费的时长 aof_current_rewrite_time_sec:-1 #如果rewrite操作正在进行,则记录所使用的时间,单位秒 aof_last_bgrewrite_status:ok #上次bgrewriteaof操作的状态 aof_last_write_status:ok #上次aof写入状态 # Stats total_connections_received:10 #服务器已经接受的连接请求数量 total_commands_processed:9510792 #redis处理的命令数 instantaneous_ops_per_sec:1 #redis当前的qps,redis内部较实时的每秒执行的命令数 total_net_input_bytes:1104411373 #redis网络入口流量字节数 total_net_output_bytes:66358938 #redis网络出口流量字节数 instantaneous_input_kbps:0.04 #redis网络入口kps instantaneous_output_kbps:3633.35 #redis网络出口kps rejected_connections:0 #拒绝的连接个数,redis连接个数达到maxclients限制,拒绝新连接的个数 sync_full:0 #主从完全同步成功次数 sync_partial_ok:0 #主从部分同步成功次数 sync_partial_err:0 #主从部分同步失败次数 expired_keys:0 #运行以来过期的key的数量 evicted_keys:0 #运行以来剔除(超过了maxmemory后)的key的数量 keyspace_hits:87 #命中次数 keyspace_misses:17 #没命中次数 pubsub_channels:0 #当前使用中的频道数量 pubsub_patterns:0 #当前使用的模式的数量 latest_fork_usec:0 #最近一次fork操作阻塞redis进程的耗时数,单位微秒 migrate_cached_sockets:0 #是否已经缓存了到该地址的连接 # Replication role:master #实例的角色,是master or slave connected_slaves:0 #连接的slave实例个数 master_repl_offset:0 #主从同步偏移量,此值如果和上面的offset相同说明主从一致没延迟,与master_replid可被用来标识主实例复制流中的位置 repl_backlog_active:0 #复制积压缓冲区是否开启 repl_backlog_size:1048576 #复制积压缓冲大小 repl_backlog_first_byte_offset:0 #复制缓冲区里偏移量的大小 repl_backlog_histlen:0 #此值等于 master_repl_offset - repl_backlog_first_byte_offset,该值不会超过repl_backlog_size的大小 # CPU used_cpu_sys:507.00 #将所有redis主进程在核心态所占用的CPU时求和累计起来 used_cpu_user:280.48 #将所有redis主进程在用户态所占用的CPU时求和累计起来 used_cpu_sys_children:0.00 #将后台进程在核心态所占用的CPU时求和累计起来 used_cpu_user_children:0.00 #将后台进程在用户态所占用的CPU时求和累计起来 # Cluster cluster_enabled:0 # Keyspace db0:keys=5557407,expires=362,avg_ttl=604780497 db15:keys=1,expires=0,avg_ttl=0 code here...查看某个section的信息127.0.0.1:6379> info memory # Memory used_memory:1067440 used_memory_human:1.02M used_memory_rss:9945088 used_memory_rss_human:9.48M used_memory_peak:1662736 used_memory_peak_human:1.59M total_system_memory:10314981376 total_system_memory_human:9.61G used_memory_lua:37888 used_memory_lua_human:37.00K maxmemory:0 maxmemory_human:0B maxmemory_policy:noeviction mem_fragmentation_ratio:9.32 mem_allocator:jemalloc-4.0.3监控执行命令 - monitormonitor用来监视服务端收到的命令。127.0.0.1:6379> monitor OK 1616045629.853032 [10 192.168.0.101:37990] "PING" 1616045629.858214 [10 192.168.0.101:37990] "PING" 1616045632.193252 [10 192.168.0.101:37990] "EXISTS" "test_key_from_app" 1616045632.193607 [10 192.168.0.101:37990] "GET" "test_key_from_app" 1616045632.200572 [10 192.168.0.101:37990] "SET" "test_key_from_app" "1616045625017" 1616045632.200973 [10 192.168.0.101:37990] "SET" "test_key_from_app" "1616045622621"监控延迟监控延迟 - latency[root@redis_test_vm ~]# redis-cli --latency -h 127.0.0.1 min: 0, max: 1, avg: 0.21如果我们故意用DEBUG命令制造延迟,就能看到一些输出上的变化:[root@redis_test_vm ~]# redis-cli -h 127.0.0.1 127.0.0.1:6379> debug sleep 2 OK (2.00s) 127.0.0.1:6379> debug sleep 3 OK (3.00s)观测延迟[root@redis_test_vm ~]# redis-cli --latency -h 127.0.0.1 min: 0, max: 1995, avg: 1.60 (492 samples)客户端监控 - ping127.0.0.1:6379> ping PONG 127.0.0.1:6379> ping PONG同时monitor127.0.0.1:6379> monitor OK 1616045629.853032 [10 192.168.0.101:37990] "PING" 1616045629.858214 [10 192.168.0.101:37990] "PING"服务端 - 内部机制服务端内部的延迟监控稍微麻烦一些,因为延迟记录的默认阈值是0。尽管空间和时间耗费很小,Redis为了高性能还是默认关闭了它。所以首先我们要开启它,设置一个合理的阈值,例如下面命令中设置的100ms:127.0.0.1:6379> CONFIG SET latency-monitor-threshold 100 OK因为Redis执行命令非常快,所以我们用DEBUG命令人为制造一些慢执行命令:127.0.0.1:6379> debug sleep 2 OK (2.00s) 127.0.0.1:6379> debug sleep .15 OK 127.0.0.1:6379> debug sleep .5 OK下面就用LATENCY的各种子命令来查看延迟记录:LATEST:四列分别表示事件名、最近延迟的Unix时间戳、最近的延迟、最大延迟。HISTORY:延迟的时间序列。可用来产生图形化显示或报表。GRAPH:以图形化的方式显示。最下面以竖行显示的是指延迟在多久以前发生。RESET:清除延迟记录。127.0.0.1:6379> latency latest 1) 1) "command" 1) (integer) 1616058778 2) (integer) 500 3) (integer) 2000 127.0.0.1:6379> latency history command 1) 1) (integer) 1616058773 2) (integer) 2000 2) 1) (integer) 1616058776 2) (integer) 150 3) 1) (integer) 1616058778 2) (integer) 500 127.0.0.1:6379> latency graph command command - high 2000 ms, low 150 ms (all time high 2000 ms) -------------------------------------------------------------------------------- # | | |_# 666 mmm在执行一条DEBUG命令会发现GRAPH图的变化,多出一条新的柱状线,下面的时间2s就是指延迟刚发生两秒钟:127.0.0.1:6379> debug sleep 1.5 OK (1.50s) 127.0.0.1:6379> latency graph command command - high 2000 ms, low 150 ms (all time high 2000 ms) -------------------------------------------------------------------------------- # | # | | |_#| 2222 333s mmm 还有一个子命令DOCTOR,它能列出一些指导建议,例如开启慢日志进一步追查问题原因,查看是否有大对象被踢出或过期,以及操作系统的配置建议等。127.0.0.1:6379> latency doctor Dave, I have observed latency spikes in this Redis instance. You don't mind talking about it, do you Dave? 1. command: 3 latency spikes (average 883ms, mean deviation 744ms, period 210.00 sec). Worst all time event 2000ms. I have a few advices for you: - Check your Slow Log to understand what are the commands you are running which are too slow to execute. Please check http://redis.io/commands/slowlog for more information. - Deleting, expiring or evicting (because of maxmemory policy) large objects is a blocking operation. If you have very large objects that are often deleted, expired, or evicted, try to fragment those objects into multiple smaller objects. - I detected a non zero amount of anonymous huge pages used by your process. This creates very serious latency events in different conditions, especially when Redis is persisting on disk. To disable THP support use the command 'echo never > /sys/kernel/mm/transparent_hugepage/enabled', make sure to also add it into /etc/rc.local so that the command will be executed again after a reboot. Note that even if you have already disabled THP, you still need to restart the Redis process to get rid of the huge pages already created.如果你沒有配置 CONFIG SET latency-monitor-threshold ., 会返回如下信息。127.0.0.1:6379> latency doctor I'm sorry, Dave, I can't do that. Latency monitoring is disabled in this Redis instance. You may use "CONFIG SET latency-monitor-threshold <milliseconds>." in order to enable it. If we weren't in a deep space mission I'd suggest to take a look at http://redis.io/topics/latency-monitor.度量延迟Baseline - intrinsic-latency延迟中的一部分是来自环境的,比如操作系统内核、虚拟化环境等等。Redis提供了让我们度量这一部分延迟基线(Baseline)的方法:[root@redis_test_vm ~]# redis-cli --intrinsic-latency 100 -h 127.0.0.1 Could not connect to Redis at 127.0.0.1:6379: Connection refused Max latency so far: 1 microseconds. Max latency so far: 62 microseconds. Max latency so far: 69 microseconds. Max latency so far: 72 microseconds. Max latency so far: 102 microseconds. Max latency so far: 438 microseconds. Max latency so far: 5169 microseconds. Max latency so far: 9923 microseconds. 1435096059 total runs (avg latency: 0.0697 microseconds / 69.68 nanoseconds per run). Worst run took 142405x longer than the average latency.intrinsic-latency后面是测试的时长(秒),一般100秒足够了。Redis可视化监控工具在谈Redis可视化监控工具时,要分清工具到底是仅仅指标的可视化,还是可以融入监控体系(比如包含可视化,监控,报警等; 这是生产环境长期监控生态的基础)。只能可视化指标不能监控: redis-stat、RedisLive、redmon 等工具用于生产环境: 基于redis_exporter以及grafana可以做到指标可视化,持久化,监控以及报警等redis-statredis-stat【https://github.com/junegunn/redis-stat】是一个比较有名的redis指标可视化的监控工具,采用ruby开发,基于redis的info和monitor命令来统计,不影响redis性能。它提供了命令行彩色控制台展示模式和web模式RedisLiveRedisLive【https://github.com/nkrode/RedisLive】是采用python开发的redis的可视化及查询分析工具。docker运行docker run --name redis-live -p 8888:8888 -d snakeliwei/redislive运行实例图访问http://192.168.99.100:8888/index.htmlredmonredmon【https://github.com/steelThread/redmon】提供了cli、admin的web界面,同时也能够实时监控redis。docker运行docker run -p 4567:4567 -d vieux/redmon -r redis://192.168.99.100:6379监控cli动态更新配置redis_exporterredis_exporter【https://github.com/oliver006/redis_exporter】为Prometheus提供了redis指标的exporter,支持Redis 2.x, 3.x, 4.x, 5.x, and 6.x,配合Prometheus以及grafana的Prometheus Redis插件,可以在grafana进行可视化及监控运行实例图Redis监控体系那么上面我们谈到的监控体系到底应该考虑什么? redis这类敏感的纯内存、高并发和低延时的服务,一套完善的监控告警方案,是精细化运营的前提。我们从以下几个角度来理解:什么样的场景会谈到redis监控体系?构建Redis监控体系具备什么价值?监控体系化包含哪些维度?具体的监控指标有哪些呢?有哪些成熟的监控方案呢?什么样的场景会谈到redis监控体系?一个大型系统引入了Redis作为缓存中间件,具体描述如下:部署架构采用Redis-Cluster模式;后台应用系统有几十个,应用实例数超过二百个;所有应用系统共用一套缓存集群;集群节点数几十个,加上容灾备用环境,节点数量翻倍;集群节点内存配置较高。问题描述 系统刚开始关于Redis的一切都很正常,随着应用系统接入越来越多,应用系统子模块接入也越来越多,开始出现一些问题,应用系统有感知,集群服务端也有感知,如下描述:集群节点崩溃;集群节点假死;某些后端应用访问集群响应特别慢。其实问题的根源都是架构运维层面的欠缺,对于Redis集群服务端的运行监控其实很好做,上文也介绍了很多直接的命令方式,但只能看到服务端的一些常用指标信息,无法深入分析,治标不治本,对于Redis的内部运行一无所知,特别是对于业务应用如何使用Redis集群一无所知:Redis集群使用的热度问题?哪些应用占用的Redis内存资源多?哪些应用占用Redis访问数最高?哪些应用使用Redis类型不合理?应用系统模块使用Redis资源分布怎么样?应用使用Redis集群的热点问题?构建Redis监控体系具备什么价值?Redis监控告警的价值对每个角色都不同,重要的几个方面:redis故障快速通知,定位故障点;分析redis故障的Root causeredis容量规划和性能管理redis硬件资源利用率和成本redis故障快速发现,定位故障点和解决故障当redis出现故障时,运维人员应在尽可能短时间内发现告警;如果故障对服务是有损的(如大面积网络故障或程序BUG),需立即通知SRE和RD启用故障预案(如切换机房或启用emergency switch)止损。如果没完善监控告警; 假设由RD发现服务故障,再排查整体服务调用链去定位;甚于用户发现用问题,通过客服投诉,再排查到redis故障的问题;整个redis故障的发现、定位和解决时间被拉长,把一个原本的小故障被”无限”放大。分析redis故障的Root cause任何一个故障和性能问题,其根本“诱因”往往只有一个,称为这个故障的Root cause。一个故障从DBA发现、止损、分析定位、解决和以后规避措施;最重要一环就是DBA通过各种问题表象,层层分析到Root cause;找到问题的根据原因,才能根治这类问题,避免再次发生。完善的redis监控数据,是我们分析root cause的基础和证据。问题表现是综合情的,一般可能性较复杂,这里举2个例子:服务调用Redis响应时间变大的性能总是;可能网络问题,redis慢查询,redis QPS增高达到性能瓶颈,redis fork阻塞和请求排队,redis使用swap, cpu达到饱和(单核idle过低),aof fsync阻塞,网络进出口资源饱和等等redis使用内存突然增长,快达到maxmemory; 可能其个大键写入,键个数增长,某类键平均长度突增,fork COW, 客户端输入/输出缓冲区,lua程序占用等等Root cause是要直观的监控数据和证据,而非有技术支撑的推理分析。redis响应抖动,分析定位root casue是bgsave时fork导致阻塞200ms的例子。而不是分析推理:redis进程rss达30gb,响应抖动时应该有同步,fork子进程时,页表拷贝时要阻塞父进程,估计页表大小xx,再根据内存copy连续1m数据要xx 纳秒,分析出可能fork阻塞导致的。(要的不是这种分析)Redis容量规划和性能管理通过分析redis资源使用和性能指标的监控历史趋势数据;对集群进行合理扩容(Scale-out)、缩容(Scale-back);对性能瓶颈优化处理等。Redis资源使用饱和度监控,设置合理阀值;一些常用容量指标:redis内存使用比例,swap使用,cpu单核的饱和度等;当资源使用容量预警时,能及时扩容,避免因资源使用过载,导致故障。另一方面,如果资源利用率持续过低,及时通知业务,并进行redis集群缩容处理,避免资源浪费。进一步,容器化管理redis后,根据监控数据,系统能自动地弹性扩容和缩容。Redis性能监控管理,及时发现性能瓶颈,进行优化或扩容,把问题扼杀在”萌芽期“,避免它”进化“成故障。Redis硬件资源利用率和成本从老板角度来看,最关心的是成本和资源利用率是否达标。如果资源不达标,就得推进资源优化整合;提高硬件利用率,减少资源浪费。砍预算,减成本。资源利用率是否达标的数据,都是通过监控系统采集的数据。监控体系化包含哪些维度?监控的目的不仅仅是监控Redis本身,而是为了更好的使用Redis。传统的监控一般比较单一化,没有系统化,但对于Redis来说,个人认为至少包括:一是服务端,二是应用端,三是服务端与应用端联合分析。服务端服务端首先是操作系统层面,常用的CPU、内存、网络IO,磁盘IO,服务端运行的进程信息等;Redis运行进程信息,包括服务端运行信息、客户端连接数、内存消耗、持久化信息 、键值数量、主从同步、命令统计、集群信息等;Redis运行日志,日志中会记录一些重要的操作进程,如运行持久化时,可以有效帮助分析崩溃假死的程序。应用端应用端、获取应用端使用Redis的一些行为,具体哪些应用哪些模块最占用 Redis资源、哪些应用哪些模块最消耗Redis资源、哪些应用哪些模块用法有误等。联合分析联合分析结合服务端的运行与应用端使用的行为,如:一些造成服务端突然阻塞的原因,可能是应用端设置了一个很大的缓存键值,或者使用的键值列表,数据量超大造成阻塞。监控指标有哪些呢?redis 监控的数据采集,数据采集1分钟一次,分为下面几个方面:服务器系统数据采集Redis Server数据采集Redis响应时间数据采集Redis监控Screen详细的 Redis性能指标监控 !服务器系统监控数据采集服务器系统的数据采集,这部分包含数百个指标. 采集方式现在监控平台自带的agent都会支持我们从redis使用资源的特性,分析各个子系统的重要监控指标。服务器存活监控ping监控告警CPU平均负载 (Load Average): 综合负载指标(暂且归类cpu子系统),当系统的子系统出现过度使用时,平均负载会升高。可说明redis的处理性能下降(平均响应时间变长、吞吐量降低)。CPU整体利用率或饱和度 (cpu.busy): redis在高并发或时间复杂度高的指令,cpu整体资源饱和,导致redis性能下降,请求堆积。CPU单核饱和度 (cpu.core.idle/core=0): redis是单进程模式,常规情况只使用一个cpu core, 单某个实例出现cpu性能瓶颈,导致性能故障,但系统一般24线程的cpu饱和度却很低。所以监控cpu单核心利用率也同样重样。CPU上下文切换数 (cpu.switches):context swith过高xxxxxx内存和swap系统内存余量大小 (mem.memfree):redis是纯内存系统,系统内存必须保有足够余量,避免出现OOM,导致redis进程被杀,或使用swap导致redis性能骤降。系统swap使用量大小 (mem.swapused):redis的”热数据“只要进入swap,redis处理性能就会骤降; 不管swap分区的是否是SSD介质。OS对swap的使用材质还是disk store. 这也是作者早期redis实现VM,后来又放弃的原因。磁盘磁盘分区的使用率 (df.bytes.used.percent):磁盘空间使用率监控告警,确保有足磁盘空间用AOF/RDB, 日志文件存储。不过 redis服务器一般很少出现磁盘容量问题磁盘IOPS的饱和度(disk.io.util):如果有AOF持久化时,要注意这类情况。如果AOF持久化,每秒sync有堆积,可能导致写入stall的情况。 另外磁盘顺序吞吐量还是很重要,太低会导致复制同步RDB时,拉长同步RDB时间。(期待diskless replication)网络网络吞吐量饱和度(net.if.out.bytes/net.if.in.bytes):如果服务器是千兆网卡(Speed: 1000Mb/s),单机多实例情况,有异常的大key容量导致网卡流量打滿。redis整体服务等量下降,苦于出现故障切换。丢包率 :Redis服务响应质量受影响Redis Server监控数据采集通过redis实例的状态数据采集,采集监控数据的命令。ping,info all, slowlog get/len/reset/cluster info/config getRedis存活监控redis存活监控 (redis_alive):redis本地监控agent使用ping,如果指定时间返回PONG表示存活,否则redis不能响应请求,可能阻塞或死亡。redis uptime监控 (redis_uptime):uptime_in_secondsRedis 连接数监控连接个数 (connected_clients):客户端连接个数,如果连接数过高,影响redis吞吐量。常规建议不要超过5000.连接数使用率(connected_clients_pct): 连接数使用百分比,通过(connected_clients/macclients)计算;如果达到1,redis开始拒绝新连接创建。拒绝的连接个数(rejected_connections): redis连接个数达到maxclients限制,拒绝新连接的个数。新创建连接个数 (total_connections_received): 如果新创建连接过多,过度地创建和销毁连接对性能有影响,说明短连接严重或连接池使用有问题,需调研代码的连接设置。list阻塞调用被阻塞的连接个数 (blocked_clients): BLPOP这类命令没使用过,如果监控数据大于0,还是建议排查原因。Redis内存监控redis分配的内存大小 (used_memory): redis真实使用内存,不包含内存碎片;单实例的内存大小不建议过大,常规10~20GB以内。redis内存使用比例(used_memory_pct): 已分配内存的百分比,通过(used_memory/maxmemory)计算;对于redis存储场景会比较关注,未设置淘汰策略(maxmemory_policy)的,达到maxmemory限制不能写入数据。redis进程使用内存大小(used_memory_rss): 进程实际使用的物理内存大小,包含内存碎片;如果rss过大导致内部碎片大,内存资源浪费,和fork的耗时和cow内存都会增大。redis内存碎片率 (mem_fragmentation_ratio): 表示(used_memory_rss/used_memory),碎片率过大,导致内存资源浪费;Redis综合性能监控Redis Keyspace: redis键空间的状态监控键个数 (keys): redis实例包含的键个数。建议控制在1kw内;单实例键个数过大,可能导致过期键的回收不及时。设置有生存时间的键个数 (keys_expires): 是纯缓存或业务的过期长,都建议对键设置TTL; 避免业务的死键问题. (expires字段)估算设置生存时间键的平均寿命 (avg_ttl): redis会抽样估算实例中设置TTL键的平均时长,单位毫秒。如果无TTL键或在Slave则avg_ttl一直为0LRU淘汰的键个数 (evicted_keys): 因used_memory达到maxmemory限制,并设置有淘汰策略的实例;(对排查问题重要,可不设置告警)过期淘汰的键个数 (expired_keys): 删除生存时间为0的键个数;包含主动删除和定期删除的个数。Redis qpsredis处理的命令数 (total_commands_processed): 监控采集周期内的平均qps,redis单实例处理达数万,如果请求数过多,redis过载导致请求堆积。redis当前的qps (instantaneous_ops_per_sec): redis内部较实时的每秒执行的命令数;可和total_commands_processed监控互补。Redis cmdstat_xxx通过info all的Commandstats节采集数据.每类命令执行的次数 (cmdstat_xxx): 这个值用于分析redis抖动变化比较有用以下表示:每个命令执行次数,总共消耗的CPU时长(单个微秒),平均每次消耗的CPU时长(单位微秒)# Commandstats cmdstat_set:calls=6,usec=37,usec_per_call=6.17 cmdstat_lpush:calls=4,usec=32,usec_per_call=8.00 cmdstat_lpop:calls=4,usec=33,usec_per_call=8.25Redis Keysapce hit ratioredis键空间请求命中率监控,通过此监控来度量redis缓存的质量,如果未命中率或次数较高,可能因热点数据已大于redis的内存限制,导致请求落到后端存储组件,可能需要扩容redis缓存集群的内存容量。当然也有可能是业务特性导致。请求键被命中次数 (keyspace_hits): redis请求键被命中的次数请求键未被命中次数 (keyspace_misses): redis请求键未被命中的次数;当命中率较高如95%,如果请求量大,未命中次数也会很多。请求键的命中率 (keyspace_hit_ratio):使用keyspace_hits/(keyspace_hits+keyspace_misses)计算所得,是度量Redis缓存服务质量的标准Redis forkredis在执行BGSAVE,BGREWRITEAOF命令时,redis进程有 fork 操作。而fork会对redis进程有个短暂的卡顿,这个卡顿redis不能响应任务请求。所以监控fork阻塞时长,是相当重要。如果你的系统不能接受redis有500ms的阻塞,那么就要监控fork阻塞时长的变化,做好容量规划。最近一次fork阻塞的微秒数 (latest_fork_usec): 最近一次Fork操作阻塞redis进程的耗时数,单位微秒。 redis network traffic redis一般单机多实例部署,当服务器网络流量增长很大,需快速定位是网络流量被哪个redis实例所消耗了; 另外redis如果写流量过大,可能导致slave线程“客户端输出缓冲区”堆积,达到限制后被Maser强制断开连接,出现复制中断故障。所以我们需监控每个redis实例网络进出口流量,设置合适的告警值。说明:网络监控指标 ,需较高的版本才有,应该是2.8.2x以后redis网络入口流量字节数 (total_net_input_bytes)redis网络出口流量字节数 (total_net_output_bytes)redis网络入口kps (instantaneous_input_kbps)redis网络出口kps (instantaneous_output_kbps)前两者是累计值,根据监控平台1个采集周期(如1分钟)内平均每秒的流量字节数。Redis慢查询监控redis慢查询 是排查性能问题关键监控指标。因redis是单线程模型(single-threaded server),即一次只能执行一个命令,如果命令耗时较长,其他命令就会被阻塞,进入队列排队等待;这样对程序性能会较大。redis慢查询保存在内存中,最多保存slowlog-max-len(默认128)个慢查询命令,当慢查询命令日志达到128个时,新慢查询被加入前,会删除最旧的慢查询命令。因慢查询不能持久化保存,且不能实时监控每秒产生的慢查询个数。我们建议的慢查询监控方法:设置合理慢查询日志阀值,slowlog-log-slower-than, 建议1ms(如果平均1ms, redis qps也就只有1000) 设+ 置全理慢查询日志队列长度,slowlog-max-len建议大于1024个,因监控采集周期1分钟,建议,避免慢查询日志被删除;另外慢查询的参数过多时,会被省略,对内存消耗很小每次采集使用slowlog len获取慢查询日志个数每次彩集使用slowlog get 1024 获取所慢查询,并转存储到其他地方,如MongoDB或MySQL等,方便排查问题;并分析当前慢查询日志最长耗时微秒数。然后使用slowlog reset把慢查询日志清空,下个采集周期的日志长度就是最新产生的。redis慢查询的监控项:redis慢查询日志个数 (slowlog_len):每个采集周期出现慢查询个数,如1分钟出现10次大于1ms的慢查询redis慢查询日志最长耗时值 (slowlog_max_time):获取慢查询耗时最长值,因有的达10秒以下的慢查询,可能导致复制中断,甚至出来主从切换等故障。Redis持久化监控redis存储场景的集群,就得 redis持久化 保障数据落地,减少故障时数据丢失。这里分析redis rdb数据持久化的几个监控指标。最近一次rdb持久化是否成功 (rdb_last_bgsave_status):如果持久化未成功,建议告警,说明备份或主从复制同步不正常。或redis设置有”stop-writes-on-bgsave-error”为yes,当save失败后,会导致redis不能写入操作最近一次成功生成rdb文件耗时秒数 (rdb_last_bgsave_time_sec):rdb生成耗时反应同步时数据是否增长; 如果远程备份使用redis-cli –rdb方式远程备份rdb文件,时间长短可能影响备份线程客户端输出缓冲内存使用大小。离最近一次成功生成rdb文件,写入命令的个数 (rdb_changes_since_last_save):即有多少个写入命令没有持久化,最坏情况下会丢失的写入命令数。建议设置监控告警离最近一次成功rdb持久化的秒数 (rdb_last_save_time): 最坏情况丢失多少秒的数据写入。使用当前时间戳 - 采集的rdb_last_save_time(最近一次rdb成功持久化的时间戳),计算出多少秒未成功生成rdb文件Redis复制监控不论使用何种redis集群方案, redis复制 都会被使用。复制相关的监控告警项:redis角色 (redis_role):实例的角色,是master or slave复制连接状态 (master_link_status): slave端可查看它与master之间同步状态;当复制断开后表示down,影响当前集群的可用性。需设置监控告警。复制连接断开时间长度 (master_link_down_since_seconds):主从服务器同步断开的秒数,建议设置时长告警。主库多少秒未发送数据到从库 (master_last_io_seconds):如果主库超过repl-timeout秒未向从库发送命令和数据,会导致复制断开重连。 在slave端可监控,建议设置大于10秒告警从库多少秒未向主库发送REPLCONF命令 (slave_lag): 正常情况从库每秒都向主库,发送REPLCONF ACK命令;如果从库因某种原因,未向主库上报命令,主从复制有中断的风险。通过在master端监控每个slave的lag值。从库是否设置只读 (slave_read_only):从库默认只读禁止写入操作,监控从库只读状态;如果关闭从库只读,有写入数据风险。主库挂载的从库个数 (connected_slaves):主库至少保证一个从库,不建议设置超过2个从库。复制积压缓冲区是否开启 (repl_backlog_active):主库默认开启复制积压缓冲区,用于应对短时间复制中断时,使用 部分同步 方式。复制积压缓冲大小 (repl_backlog_size):主库复制积压缓冲大小默认1MB,因为是redis server共享一个缓冲区,建议设置100MB.说明: 关于根据实际情况,设置合适大小的复制缓冲区。可以通过master_repl_offset指标计算每秒写入字节数,同时乘以希望多少秒内闪断使用“部分同步”方式。Redis集群监控这里所写 redis官方集群方案 的监控指标数据基本通过cluster info和info命令采集。实例是否启用集群模式 (cluster_enabled): 通过info的cluster_enabled监控是否启用集群模式。集群健康状态 (clusster_state):如果当前redis发现有failed的slots,默认为把自己cluster_state从ok个性为fail, 写入命令会失败。如果设置cluster-require-full-coverage为NO,则无此限制。集群数据槽slots分配情况 (cluster_slots_assigned):集群正常运行时,默认16384个slots检测下线的数据槽slots个数 (cluster_slots_fail):集群正常运行时,应该为0. 如果大于0说明集群有slot存在故障。集群的分片数 (cluster_size):集群中设置的分片个数集群的节点数 (cluster_known_nodes):集群中redis节点的个数Redis响应时间监控响应时间 是衡量一个服务组件性能和质量的重要指标。使用redis的服务通常对响应时间都十分敏感,比如要求99%的响应时间达10ms以内。因redis的慢查询日志只计算命令的cpu占用时间,不会考虑排队或其他耗时。最长响应时间 (respond_time_max):最长响应时间的毫秒数99%的响应时间长度 (respond_time_99_max):99%的平均响应时间长度 (respond_time_99_avg):95%的响应时间长度 (respond_time_95_max):95%的平均响应时间长度 (respond_time_95_avg):常用哪些成熟方案呢?无论哪种,要体系化,必然要考虑如下几点。指标采集,即采集redis提供的metric指标,所以方案中有时候会出现Agent,比如metricBeat;监控的数据持久化,只有将监控数据放到数据库,才能对比和长期监控;时序化,因为很多场景都会按照时间序列去展示 - 所以通常是用时序库或者针对时间列优化;可视化,比如常见的kibana,grafana等按条件报警,因为运维不可能盯着看,只有引入报警配置,触发报警条件时即发出报警,比如短息提醒等;基于不同报警方式,平台可以提供插件支持等;ELK Stack经典的ELK采集agent: metricBeat收集管道:logstashDB: elasticSearchview和告警: kibana及插件fluent + Prometheus + Grafana推荐使用这种采集指标来源: redis-export收集管道:fluentdDB: Prometheusview和告警: Grafana及插件来源:https://www.pdai.tech/md/db/nosql-redis/db-redis-y-monitor.html(二十三):Redis 开发规范本文介绍了在使用阿里云Redis的开发规范,从键值设计、命令使用、客户端使用、相关工具等方面进行说明,通过本文的介绍可以减少使用Redis过程带来的问题。键值设计key名设计可读性和可管理性以业务名(或数据库名)为前缀(防止key冲突),用冒号分隔,比如业务名:表名:idugc:video:1简洁性保证语义的前提下,控制key的长度,当key较多时,内存占用也不容忽视,例如:user:{uid}:friends:messages:{mid}简化为u:{uid}不要包含特殊字符反例:包含空格、换行、单双引号以及其他转义字符value设计拒绝bigkey(防止网卡流量、慢查询)string类型控制在10KB以内,hash、list、set、zset元素个数不要超过5000。反例:一个包含200万个元素的list。非字符串的bigkey,不要使用del删除,使用hscan、sscan、zscan方式渐进式删除,同时要注意防止bigkey过期时间自动删除问题(例如一个200万的zset设置1小时过期,会触发del操作,造成阻塞,而且该操作不会不出现在慢查询中(latency可查)),查找方法和删除方法选择适合的数据类型例如:实体类型(要合理控制和使用数据结构内存编码优化配置,例如ziplist,但也要注意节省内存和性能之间的平衡)反例:set user:1:name tom set user:1:age 19 set user:1:favor football正例:hmset user:1 name tom age 19 favor football控制key的生命周期,redis不是垃圾桶。建议使用expire设置过期时间(条件允许可以打散过期时间,防止集中过期),不过期的数据重点关注idletime。命令使用O(N)命令关注N的数量例如hgetall、lrange、smembers、zrange、sinter等并非不能使用,但是需要明确N的值。有遍历的需求可以使用hscan、sscan、zscan代替。禁用命令禁止线上使用keys、flushall、flushdb等,通过redis的rename机制禁掉命令,或者使用scan的方式渐进式处理。合理使用selectredis的多数据库较弱,使用数字进行区分,很多客户端支持较差,同时多业务用多数据库实际还是单线程处理,会有干扰。使用批量操作提高效率原生命令:例如mget、mset。非原生命令:可以使用pipeline提高效率。但要注意控制一次批量操作的元素个数(例如500以内,实际也和元素字节数有关)。注意两者不同:原生是原子操作,pipeline是非原子操作。pipeline可以打包不同的命令,原生做不到pipeline需要客户端和服务端同时支持。不建议过多使用Redis事务功能Redis的事务功能较弱(不支持回滚),而且集群版本(自研和官方)要求一次事务操作的key必须在一个slot上(可以使用hashtag功能解决)。Redis集群版本在使用Lua上有特殊要求1、所有key都应该由 KEYS 数组来传递,redis.call/pcall 里面调用的redis命令,key的位置,必须是KEYS array, 否则直接返回error,"-ERR bad lua script for redis cluster, all the keys that the script uses should be passed using the KEYS arrayrn"2、所有key,必须在1个slot上,否则直接返回error, "-ERR eval/evalsha command keys must in same slotrn"monitor命令必要情况下使用monitor命令时,要注意不要长时间使用。客户端使用避免多个应用使用一个Redis实例正例:不相干的业务拆分,公共数据做服务化。使用带有连接池的数据库可以有效控制连接,同时提高效率,标准使用方式:Jedis jedis = null; try { jedis = jedisPool.getResource(); //具体的命令 jedis.executeCommand() } catch (Exception e) { logger.error("op key {} error: " + e.getMessage(), key, e); } finally { //注意这里不是关闭连接,在JedisPool模式下,Jedis会被归还给资源池。 if (jedis != null) jedis.close(); }熔断功能高并发下建议客户端添加熔断功能(例如netflix hystrix)合理的加密设置合理的密码,如有必要可以使用SSL加密访问(阿里云Redis支持)淘汰策略根据自身业务类型,选好maxmemory-policy(最大内存淘汰策略),设置好过期时间。默认策略是volatile-lru,即超过最大内存后,在过期键中使用lru算法进行key的剔除,保证不过期数据不被删除,但是可能会出现OOM问题。其他策略如下:allkeys-lru:根据LRU算法删除键,不管数据有没有设置超时属性,直到腾出足够空间为止。allkeys-random:随机删除所有键,直到腾出足够空间为止。volatile-random:随机删除过期键,直到腾出足够空间为止。volatile-ttl:根据键值对象的ttl属性,删除最近将要过期数据。如果没有,回退到noeviction策略。noeviction:不会剔除任何数据,拒绝所有写入操作并返回客户端错误信息"(error) OOM command not allowed when used memory",此时Redis只响应读操作。相关工具数据同步redis间数据同步可以使用:redis-portbig key搜索redis大key搜索工具热点key寻找内部实现使用monitor,所以建议短时间使用facebook的redis-faina 阿里云Redis已经在内核层面解决热点key问题删除bigkey下面操作可以使用pipeline加速。redis 4.0已经支持key的异步删除,欢迎使用。Hash删除: hscan + hdelpublic void delBigHash(String host, int port, String password, String bigHashKey) { Jedis jedis = new Jedis(host, port); if (password != null && !"".equals(password)) { jedis.auth(password); } ScanParams scanParams = new ScanParams().count(100); String cursor = "0"; do { ScanResult<Entry<String, String>> scanResult = jedis.hscan(bigHashKey, cursor, scanParams); List<Entry<String, String>> entryList = scanResult.getResult(); if (entryList != null && !entryList.isEmpty()) { for (Entry<String, String> entry : entryList) { jedis.hdel(bigHashKey, entry.getKey()); } } cursor = scanResult.getStringCursor(); } while (!"0".equals(cursor)); //删除bigkey jedis.del(bigHashKey); }List删除: ltrimpublic void delBigList(String host, int port, String password, String bigListKey) { Jedis jedis = new Jedis(host, port); if (password != null && !"".equals(password)) { jedis.auth(password); } long llen = jedis.llen(bigListKey); int counter = 0; int left = 100; while (counter < llen) { //每次从左侧截掉100个 jedis.ltrim(bigListKey, left, llen); counter += left; } //最终删除key jedis.del(bigListKey); }Set删除: sscan + srempublic void delBigSet(String host, int port, String password, String bigSetKey) { Jedis jedis = new Jedis(host, port); if (password != null && !"".equals(password)) { jedis.auth(password); } ScanParams scanParams = new ScanParams().count(100); String cursor = "0"; do { ScanResult<String> scanResult = jedis.sscan(bigSetKey, cursor, scanParams); List<String> memberList = scanResult.getResult(); if (memberList != null && !memberList.isEmpty()) { for (String member : memberList) { jedis.srem(bigSetKey, member); } } cursor = scanResult.getStringCursor(); } while (!"0".equals(cursor)); //删除bigkey jedis.del(bigSetKey); }SortedSet删除: zscan + zrempublic void delBigZset(String host, int port, String password, String bigZsetKey) { Jedis jedis = new Jedis(host, port); if (password != null && !"".equals(password)) { jedis.auth(password); } ScanParams scanParams = new ScanParams().count(100); String cursor = "0"; do { ScanResult<Tuple> scanResult = jedis.zscan(bigZsetKey, cursor, scanParams); List<Tuple> tupleList = scanResult.getResult(); if (tupleList != null && !tupleList.isEmpty()) { for (Tuple tuple : tupleList) { jedis.zrem(bigZsetKey, tuple.getElement()); } } cursor = scanResult.getStringCursor(); } while (!"0".equals(cursor)); //删除bigkey jedis.del(bigZsetKey); }来源:https://tinyurl.com/y3kky2xn
2023年09月19日
20 阅读
0 评论
0 点赞
2023-09-14
Google 代码审查指南
Google 代码审查指南简介代码审查是除了代码作者之外,其他人检查代码的过程。 Google 通过 Code Review 来维护代码和产品质量。此文档是 Google Code Review 流程和政策的规范说明。 Google Code Review有两套文档:代码审查者指南:针对代码审查者的详细指南。代码开发者指南:针对 CL 开发者的的详细指南。背景Google 有许多通用工程实践,几乎涵盖所有语言和项目。此文档为长期积累的最佳实践,是集体经验的结晶。我们尽可能地将其公之于众,您的组织和开源项目也会从中受益。术语文档中使用了 Google 内部术语,在此为外部读者澄清:CL:代表“变更列表(changelist)” ,表示已提交到版本控制或正在进行代码审查的自包含更改。有的组织会将其称为“变更(change)”或“补丁(patch)”。LGTM:意思是“我觉得不错(Looks Good to Me)” 。这是批准 CL 时代码审查者所说的。代码审查者应该关注哪些方面?代码审查时应该关注以下方面:设计:代码是否经过精心设计并适合您的系统?功能:代码的行为是否与作者的意图相同?代码是否可以正常响应用户的行为?复杂度:代码能更简单吗?将来其他开发人员能轻松理解并使用此代码吗?测试:代码是否具有正确且设计良好的自动化测试?命名:开发人员是否为变量、类、方法等选择了明确的名称?注释:评论是否清晰有用?风格:代码是否遵守了风格指南?文档:开发人员是否同时更新了相关文档?选择最合适审查者一般而言,您希望找到能在合理的时间内回复您的评论的最合适的审查者。最合适的审查者应该是 能彻底了解和审查您代码的人 。他们 通常是代码的所有者,可能是 OWNERS 文件中的人,也可能不是。 有时 CL 的不同部分可能需要不同的人审查。如果您找到了理想的审查者但他们又没空,那您也至少要抄送他们。面对面审查如果您与有资格做代码审查的人一起结对编程了一段代码,那么该代码将被视为已审查。您还可以进行面对面的代码审查,审查者提问,CL 的开发人员作答。代码审查者指南这是基于过往经验编写的 Code Review 最佳方式建议。其中分为了很多独立的部分,共同组成完整的文档。虽然您不必阅读文档,但通读一遍会对您自己和团队很有帮助。Code Review 标准代码审查的主要目的是确保逐步改善 Google 代码库的整体健康状况。代码审查的所有工具和流程都是为此而设计的。为了实现此目标,必须做出一系列权衡。首先,开发人员必须能够对任务进行 改进 。如果开发者从未向代码库提交过代码,那么代码库的改进也就无从谈起。此外,如果审核人员对代码吹毛求疵,那么开发人员以后也很难再做出改进。另外,审查者有责任确保随着时间的推移,CL 的质量不会使代码库的整体健康状况下降。这可能很棘手,因为通常情况下,代码库健康状况会随着时间的而下降,特别是在对团队有严格的时间要求时,团队往往会采取捷径来达成他们的目标。此外,审查者应对正在审核的代码负责并拥有所有权。审查者希望确保代码库保持一致、可维护及 Code Review 要点 中所提及的所有其他内容。因此,我们将以下规则作为 Code Review 中期望的标准:一般来说,审核人员应该倾向于批准 CL,只要 CL 确实可以提高系统的整体代码健康状态,即使 CL 并不完美。 这是所有 Code Review 指南中的 高级原则 。当然,也有一些限制。例如,如果 CL 添加了审查者认为系统中不需要的功能,那么即使代码设计良好,审查者依然可以拒绝批准它。此处有一个关键点就是没有“完美”的代码,只有 更好 的代码。审查者不该要求开发者在批准程序前仔细清理、润色 CL 每个角落。相反,审查者应该在变更的重要性与取得进展之间取得平衡。审查者不应该追求完美,而应是追求持续改进。不要因为一个 CL不是“完美的”,就将可以提高系统的可维护性、可读性和可理解性的 CL 延迟数天或数周才批准。审核者应该随时在可以改善的地方留下审核评论,但如果评论不是很重要,请在评论语句前加上“Nit:”之类的内容,让开发者知道这条评论是用来指出可以润色的地方,而他们可以选择是否忽略。注意:本文档中没有任何内容证明检查 CL 肯定会使系统的整体代码健康状况恶化。您会做这中事情应该只有在 紧急情况 时。指导代码审查具有向开发人员传授语言、框架或通用软件设计原则新内容的重要功能。留下评论可以帮助开发人员学习新东西 ,这总归是很好的。分享知识是随着长年累月改善系统代码健康状况的一部分。请记住, 如果您的评论纯粹是教育性的,且对于本文档中描述的标准并不重要,请在其前面添加“Nit:”或以其他方式表明作者不必在此 CL 中解决它。原则基于技术事实和数据否决意见和个人偏好。关于代码风格问题, 风格指南 是绝对权威。任何不在风格指南中的纯粹风格点(例如空白等)都是个人偏好的问题。代码风格应该与风格指南中的一致。如果没有以前的风格,请接受作者的风格。软件设计方面几乎不是纯粹的风格或个人偏好问题。软件设计基于基本原则且应该权衡这些原则,而不仅仅是个人意见。有时候会有多种有效的选择。如果作者可以证明(通过数据或基于可靠的工程原理)该方法同样有效,那么审查者应该接受作者的偏好。否则,就要取决于软件设计的标准原则。如果没有其他适用规则,则审查者可以要求作者与当前代码库中的内容保持一致,只要不恶化系统的整体代码健康状况即可。解决冲突如果在代码审查过程中有任何冲突,第一步应该始终是开发人员和审查者根据本文档中的 开发者指南和审查者指南 达成共识。当达成共识变得特别困难时,审阅者和开发者可以进行面对面的会议,或者有 VC 参与调停,而不仅仅是试着通过代码审查评论来解决冲突。 (但是,如果您这样做了,请确保在 CL 的评论中记录讨论结果,以供将来的读者使用。)如果这样还不能解决问题,那么解决该问题最常用方法是将问题升级。通常是将问题升级为更广泛的团队讨论,有一个 TL 权衡,要求维护人员对代码作出决定,或要求工程经理的帮助。 不要因为 CL 的开发者和审查者不能达成一致,就让 CL 在那里卡壳。Code Review 要点注意:在考虑这些要点时,请谨记 “ Code Review 标准 ”。设计审查中最重要的是 CL 的 整体设计。CL 中各种代码的交互是否有意义?此变更是属于您的代码库(codebase)还是属于库(library)?它是否与您系统的其他部分很好地集成?现在是添加此功能的好时机吗?功能这个 CL 是否符合开发者的意图?开发者的意图对代码的用户是否是好的? “用户”通常都是最终用户(当他们受到变更影响时)和开发者(将来必须“使用”此代码)。 大多数情况下,我们希望开发者能够很好地测试 CL,以便在审查时代码能够正常工作。但是,作为审查者,仍然应该考虑边缘情况,寻找并发问题,尝试像用户一样思考,并确保您单纯透过阅读方式审查时,代码没有包含任何 bug。当要检查 CL 的行为会对用户有重大影响时,验证 CL 的变化将变得十分重要。例如 UI 变更。当您只是阅读代码时,很难理解某些变更会如何影响用户。如果在 CL 中打 patch 或自行尝试这样的变更太不方便,您可以让开发人员为您提供功能演示。另一个在代码审查期间特别需要考虑功能的时机,就是如果 CL 中存在某种并行编程,理论上可能导致死锁或竞争条件。通过运行代码很难检测到这些类型的问题,并且通常需要某人(开发者和审查者)仔细思考它们以确保不会引入问题。 (请注意,这也是在可能出现竞争条件或死锁的情况下,不使用并发模型的一个很好的理由——它会使代码审查或理解代码变得非常复杂。)复杂度CL 是否已经超过它原本所必须的复杂度?针对任何层级的 CL 请务必确认这点——每行程序是否过于复杂? 功能太复杂了吗?类太复杂了吗? “太复杂”通常意味着“阅读代码的人无法快速理解。”也可能意味着“开发者在尝试调用或修改此代码时可能会引入错误。” 其中一种复杂性就是过度工程(over-engineering),如开发人员使代码过度通用,超过它原本所需的,或者添加系统当前不需要的功能 。审查者应特别警惕过度工程。未来的问题应该在它实际到达后解决,且届时才能更清晰的看到其真实样貌及在现实环境里的需求,鼓励开发人员解决他们现在需要解决的问题,而不是开发人员推测可能需要在未来解决的问题。测试将要求单元、集成或端到端测试视为应该做的适当变更。 通常,除非 CL 处理紧急情况,否则应在与生产代码相同的 CL 中添加测试。确保 CL 中的测试正确,合理且有用。测试并非用来测试自己本身,且我们很少为测试编写测试——人类必须确保测试有效。当代码被破坏时,测试是否真的会失败? 如果代码发生变化时,它们会开始产生误报吗? 每个测试都会做出简单而有用的断言吗? 不同测试方法的测试是否适当分开?请记住,测试也是必须维护的代码。不要仅仅因为它们不是主二进制文件的一部分而接受测试中的复杂性。命名开发人员是否为所有内容选择了好名字? 一个好名字应该足够长,可以完全传达项目的内容或作用,但又不会太长,以至于难以阅读。注释开发者是否用可理解的英语撰写了清晰的注释?所有注释都是必要的吗?通常,注释解释为什么某些代码存在时很有用,且不应该用来解释某些代码正在做什么。如果代码无法清楚到去解释自己时,那么代码应该变得更简单。有一些例外(正则表达式和复杂算法通常会从解释他们正在做什么事情的注释中获益很多),但大多数注释都是针对代码本身可能无法包含的信息,例如决策背后的推理。 查看此 CL 之前的注释也很有帮助。 也许有一个 TODO 现在可以删除,一个注释建议不要进行这种改变,等等。请注意,注释与类、模块或函数的文档不同,它们应该代表一段代码的目的,如何使用它,以及使用时它的行为方式。风格Google 提供了所有主要语言的风格指南,甚至包括大多数小众语言。确保 CL 遵循适当的风格指南。如果您想改进风格指南中没有的一些样式点,请在评论前加上“Nit:”,让开发人员知道这是您认为可改善代码的小瑕疵,但不是强制性的。不要仅根据个人风格偏好阻止提交 CL。CL 的作者不应在主要风格变更中,包括与其他种类的变更。它会使得很难看到 CL 中的变更了什么,使合并和回滚更复杂,并导致其他问题。例如,如果作者想要重新格式化整个文件,让他们只将重新格式化变为一个 CL,其后再发送另一个包含功能变更的 CL。文档如果 CL 变更了用户构建、测试、交互或发布代码的方式,请检查相关文档是否有更新,包括 README、g3doc 页面和任何生成的参考文档。 如果 CL 删除或弃用代码,请考虑是否也应删除文档。 如果缺少文档,请询问。每一行查看分配给您审查的每行代码。 有时如数据文件、生成的代码或大型数据结构等东西,您可以快速扫过。但不要快速扫过人类编写的类、函数或代码块,并假设其中的内容是 OK 的。显然,某些代码需要比其他代码更仔细的审查——这是您必须做出的判断——但您至少应该确定您理解所有代码正在做什么。如果您觉得这些代码太难以阅读了并减慢您审查的速度,您应该在您尝试继续审核前要让开发者知道这件事,并等待他们为程序做出解释、澄清。在 Google,我们聘请了优秀的软件工程师,您就是其中之一。如果您无法理解代码,那么很可能其他开发人员也不会。因此,当您要求开发人员澄清此代码时,您也会帮助未来的开发人员理解这些代码。如果您了解代码但觉得没有资格做某些部分的审查,请确保 CL 上有一个合格的审查人,特别是对于安全性、并发性、可访问性、国际化等复杂问题。上下文在广泛的上下文下查看 CL 通常很有帮助。 通常,代码审查工具只会显示变更的部分的周围的几行。有时您必须查看整个文件以确保变更确实有意义。例如,您可能只看到添加了四行新代码,但是当您查看整个文件时,您会看到这四行是添加在一个 50 行的方法里,现在确实需要将它们分解为更小的方法。在整个系统的上下文中考虑 CL 也很有用。 这个 CL 是否改善了系统的代码健康状况,还是使整个系统更复杂,测试更少等等?不要接受降低系统代码运行状况的 CL。大多数系统通过许多小的变化而变得复杂,因此防止新变更引入即便很小的复杂性也非常重要。好的事情如果您在 CL 中看到一些不错的东西,请告诉开发者,特别是当他们以一种很好的方式解决了您的的一个评论时。 代码审查通常只关注错误,但也应该为良好实践提供鼓励。在指导方面,比起告诉他们他们做错了什么,有时更有价值的是告诉开发人员他们做对了什么。总结在进行代码审查时,您应该确保:代码设计精良。该功能对代码用户是有好处的。任何 UI 变更都是合理的且看起来是好的。其中任何并行编程都是安全的。代码并不比它需要的复杂。开发人员没有实现他们将来可能需要,但不知道他们现在是否需要的东西。代码有适当的单元测试。测试精心设计。开发人员使用了清晰的名称。评论清晰有用,且大多用来解释为什么而不是做什么。代码有适当记录成文件(通常在 g3doc 中)。代码符合我们的风格指南。确保查看您被要求查看的每一行代码,查看上下文,确保您提高代码健康状况,并赞扬开发人员所做的好事。查看 CL 的步骤总结现在您已经知道了 Code Review 要点 ,那么管理分布在多个文件中的评论的最有效方法是什么?变更是否有意义?它有很好的描述吗?首先看一下变更中最重要的部分。整体设计得好吗?以适当的顺序查看 CL 的其余部分。第一步:全面了解变更查看 CL 描述 和 CL 大致上用来做什么事情。这种变更是否有意义?如果在最初不应该发生这样的变更,请立即回复,说明为什么不应该进行变更。当您拒绝这样的变更时,向开发人员建议应该做什么也是一个好主意。例如,您可能会说“看起来你已经完成一些不错的工作,谢谢!但实际上,我们正朝着删除您在这里修改的 FooWidget 系统的方向演进,所以我们不想对它进行任何新的修改。不过,您来重构下新的 BarWidget 类怎么样?“请注意,审查者不仅拒绝了当前的 CL 并提供了替代建议,而且他们保持礼貌地这样做。这种礼貌很重要,因为我们希望表明,即使不同意,我们也会相互尊重。如果您获得了多个您不想变更的 CL,您应该考虑重整开发团队的开发过程或外部贡献者的发布过程,以便在编写CL之前有更多的沟通。最好在他们完成大量工作之前说“不”,避免已经投入心血的工作现在必须被抛弃或彻底重写。第二步:检查 CL 的主要部分查找作为此 CL “主要”部分的文件。通常,包含大量的逻辑变更的文件就是 CL 的主要部分。先看看这些主要部分。这有助于为 CL 的所有较小部分提供上下文,并且通常可以加速代码审查。如果 CL 太大而无法确定哪些部分是主要部分,请向开发人员询问您应该首先查看的内容,或者要求他们将 CL 拆分为多个 CL(即后文“小型CL”) 。如果在该部分发现存在一些主要的设计问题时,即使没有时间立即查看 CL 的其余部分,也应立即留下评论告知此问题。因为事实上,因为该设计问题足够严重的话,继续审查其余部分很可能只是浪费宝贵的时间,因为其他正在审查的程序可能都将无关或消失。立即发送这些主要设计评论非常重要,有两个主要原因:通常开发者在发出 CL 后,在等待审查时立即开始基于该 CL 的新工作。如果您正在审查的 CL 中存在重大设计问题,那么他们以后的 CL 也必须要返工。您应该赶在他们在有问题的设计上做了太多无用功之前通知他们。主要的设计变更比起小的变更来说需要更长的时间才能完成。开发人员基本都有截止日期;为了完成这些截止日期并且在代码库中仍然保有高质量代码,开发人员需要尽快开始 CL 的任何重大工作。第三步:以适当的顺序查看 CL 的其余部分一旦您确认整个 CL 没有重大的设计问题,试着找出一个逻辑顺序来查看文件,同时确保您不会错过查看任何文件。 通常在查看主要文件之后,最简单的方法是按照代码审查工具向您提供的顺序浏览每个文件。有时在阅读主代码之前先阅读测试也很有帮助,因为这样您就可以了解该变更应当做些什么。Code Review 速度为什么尽快进行 Code Review?在Google,我们优化了开发团队共同开发产品的速度,而不是优化单个开发者编写代码的速度。个人开发的速度很重要,它并不如整个团队的速度那么重要。当代码审查很慢时,会发生以下几件事:整个团队的速度降低了。 是的,对审查没有快速响应的个人的确完成了其他工作。但是,对于团队其他人来说重要的新功能与缺陷修復将会被延迟数天、数周甚至数月,只因为每个 CL 正在等待审查和重新审查。开发者开始抗议代码审查流程。 如果审查者每隔几天只响应一次,但每次都要求对 CL 进行重大更改,那么开发者可能会变得沮丧。通常,开发者将表达对审查者过于“严格”的抱怨。如果审查者请求相同实质性更改(确实可以改善代码健康状况),但每次开发者进行更新时都会快速响应,则抱怨会逐渐消失。大多数关于代码审查流程的投诉实际上是通过加快流程来解决的。代码健康状况可能会受到影响。 如果审查速度很慢,则造成开发者提交不尽如人意的 CL 的压力会越来越大。审查太慢还会阻止代码清理、重构以及对现有 CL 的进一步改进。Code Review 应该有多快?如果您没有处于重点任务的中,那么您应该在收到代码审查后尽快开始。一个工作日是应该响应代码审查请求所需的最长时间(即第二天早上的第一件事)。遵循这些指导意味着典型的 CL 应该在一天内进行多轮审查(如果需要)。速度 vs. 中断有一种情况下个人速度胜过团队速度。 如果您正处于重点任务中,例如编写代码,请不要打断自己进行代码审查。 研究表明,开发人员在被打断后需要很长时间才能恢复到顺畅的开发流程中。因此,编写代码时打断自己实际上比让另一位开发人员等待代码审查的代价更加昂贵。相反,在回复审查请求之前,请等待工作中断点。可能是当你的当前编码任务完成,午餐后,从会议返回,从厨房回来等等。快速响应当我们谈论代码审查的速度时,我们关注的是响应时间,而不是 CL 需要多长时间才能完成整个审查并提交。理想情况下,整个过程也应该是快速的, 快速的个人响应比整个过程快速发生更为重要。 即使有时需要很久才能完成整个审查流程,但在整个过程中获得审查者的快速响应可以显着减轻开发人员对“慢速”代码审查感到的挫败感。如果您太忙而无法对 CL 进行全面审查,您仍然可以发送快速回复,让开发人员知道您什么时候可以开始,或推荐其他能够更快回复的审查人员,或者 提供一些大体的初步评论 。 (注意:这并不意味着您应该中断编码,即使发送这样的响应,也要在工作中的合理断点处发出响应。)重要的是,审查人员要花足够的时间进行审查,确信他们的“LGTM”意味着“此代码符合我们的标准。”但是,理想情况下,个人反应仍然应该很快。跨时区审查在处理时区差异时,尝试在他们还在办公室时回复作者。 如果他们已经下班回家了,那么请确保在第二天回到办公室之前完成审查。带评论的 LGTM为了加快代码审查,在某些情况下,即使他们也在 CL 上留下未解决的评论,审查者也应该给予 LGTM/Approval,这可以是以下任何一种情况:审查者确信开发人员将适当地处理所有审查者的剩余评论。其余的更改很小,不必由开发者完成。如果不清楚的话,审查者应该指定他们想要哪些选项。当开发者和审查者处于不同的时区时,带评论的 LGTM 尤其值得考虑,否则开发者将等待一整天才能获得 “LGTM,Approval”。大型 CL如果有人向您发送了代码审查太大,您不确定何时有时间查看,那么您应该要求开发者将 CL 拆分为几个较小的 CL 而不是一次审查的一个巨大的 CL。这通常可行,对审查者非常有帮助,即使需要开发人员的额外工作。如果 CL 无法分解为较小的 CL,并且您没有时间快速查看整个内容,那么至少要对 CL 的整体设计写一些评论并将其发送回开发人员以进行改进。作为审查者,您的目标之一应该在不牺牲代码健康状况的前提下,始终减少开发者能够快速采取某种进一步的操作的阻力。代码审查随时间推移而改进如果您遵循这些准则,并且您对代码审查非常严格,那么您应该会发现整个代码审核流程会随着时间的推移而变得越来越快。开发者可以了解健康代码所需的内容,并向您发送从一开始就很棒的 CL,且需要的审查时间越来越短。审查者学会快速响应,而不是在审查过程中添加不必要的延迟。但是,从长远来看, 不要为了提高想象中的代码审查速度,而在代码审查标准或质量方面妥协,实际上这样做对于长期来说不会有任何帮助。紧急情况还有一些紧急情况,CL 必须非常快速地通过整个审查流程,并且质量准则将放宽。请查看什么是紧急情况? 中描述的哪些情况属于紧急情况,哪些情况不属于紧急情况。如何撰写 Code Review 评论总结保持友善。解释你的推理。在给出明确的指示与只指出问题并让开发人员自己决定间做好平衡。鼓励开发人员简化代码或添加代码注释,而不仅仅是向你解释复杂性。礼貌一般而言,对于那些正在被您审查代码的人, 除了保持有礼貌且尊重以外,重要的是还要确保您(的评论)是非常清楚且有帮助的。 你并不总是必须遵循这种做法,但在说出可能令人不安或有争议的事情时你绝对应该使用它。 例如:糟糕的示例:“为什么这里你使用了线程,显然并发并没有带来什么好处?”好的示例:“这里的并发模型增加了系统的复杂性,但没有任何实际的性能优势,因为没有性能优势,最好是将这些代码作为单线程处理而不是使用多线程。”解释为什么关于上面的“好”示例,您会注意到的一件事是,它可以帮助开发人员理解您发表评论的原因。 并不总是需要您在审查评论中包含此信息,但有时候提供更多解释,对于表明您的意图,您在遵循的最佳实践,或为您建议如何提高代码健康状况是十分恰当的。给予指导一般来说,修复 CL 是开发人员的责任,而不是审查者。 您无需为开发人员详细设计解决方案或编写代码。但这并不意味着审查者应该没有帮助。一般来说,您应该在指出问题和提供直接指导之间取得适当的平衡。指出问题并让开发人员做出决定通常有助于开发人员学习,并使代码审查变得更容易。它还可能产生更好的解决方案,因为开发人员比审查者更接近代码。但是,有时直接说明,建议甚至代码会更有帮助。 代码审查的主要目标是尽可能获得最佳 CL。第二个目标是提高开发人员的技能,以便他们随着时间的推移需要的审查越来越少。接受解释如果您要求开发人员解释一段您不理解的代码,那通常会导致他们更清楚地重写代码。 偶尔,在代码中添加注释也是一种恰当的响应,只要它不仅仅是解释过于复杂的代码。仅在代码审查工具中编写的解释对未来的代码阅读者没有帮助。这仅在少数情况下是可接受的,例如当您查看一个您不熟悉的领域时,开发人员会用来向您解释普通读者已经知道的内容。处理 Code Review 中的抵触有时开发人员会拖延(Pushback)代码审查。他们要么不同意您的建议,要么抱怨您太严格。谁是对的?当开发人员不同意您的建议时,请先花点时间考虑一下是否正确。通常,他们比你更接近代码,所以他们可能真的对它的某些方面有更好的洞察力。他们的论点有意义吗?从代码健康的角度来看它是否有意义?如果是这样,让他们知道他们是对的,把问题解决。但是,开发人员并不总是对的。在这种情况下,审查人应进一步解释为什么认为他们的建议是正确的。好的解释在描述对开发人员回复的理解的同时,还会解释为什么请求更改。特别是,当审查人员认为他们的建议会改善代码健康状况时,他们应该继续提倡更改,如果他们认为最终的代码质量改进能够证明所需的额外工作是合理的。提高代码健康状况往往只需很小的几步。有时需要几轮解释一个建议才能才能让对方真正理解你的用意。只要确保始终保持礼貌,让开发人员知道你有听到他们在说什么,只是你不同意该论点而已。沮丧的开发者审查者有时认为,如果审查者人坚持改进,开发人员会感到不安。有时候开发人员会感到很沮丧,但这样的感觉通常只会持续很短的时间,后来他们会非常感谢您在提高代码质量方面给他们的帮助。通常情况下,如果您在评论中表现得很有礼貌,开发人员实际上根本不会感到沮丧,这些担忧都仅存在于审核者心中而已。开发者感到沮丧通常更多地与评论的写作方式有关,而不是审查者对代码质量的坚持。稍后清理开发人员拖延的一个常见原因是开发人员(可以理解)希望完成任务。他们不想通过另一轮审查来完成该 CL。所以他们说会在以后的 CL 中清理一些东西,所以您现在应该 LGTM 这个 CL。一些开发人员非常擅长这一点,并会立即编写一个修复问题的后续 CL。但是,经验表明,在开发人员编写原始 CL 后,经过越长的时间这种清理发生的可能性就越小。实际上,通常除非开发人员在当前 CL 之后立即进行清理,否则它就永远不会发生。这不是因为开发人员不负责任,而是因为他们有很多工作要做,清理工作在其他工作中被丢失或遗忘。因此, 在代码进入代码库并“完成”之前,通常最好坚持让开发人员现在清理他们的 CL。让人们“稍后清理东西”是代码库质量退化的常见原因。 如果 CL 引入了新的复杂性,除非是紧急情况,否则必须在提交之前将其清除。如果 CL 暴露了相关的问题并且现在无法解决,那么开发人员应该将 bug 记录下来并分配给自己,避免后续被遗忘。又或者他们可以选择在程序中留下 TODO 的注释并连结到刚记录下的 bug。关于严格性的抱怨如果您以前有相当宽松的代码审查,并转而进行严格的审查,一些开发人员会抱怨得非常大声。通常提高代码审查的速度会让这些抱怨逐渐消失。有时,这些投诉可能需要数月才会消失,但最终开发人员往往会看到严格的代码审查的价值,因为他们会看到代码审查帮助生成的优秀代码。而且一旦发生某些事情时,最响亮的抗议者甚至可能会成为你最坚定的支持者,因为他们会看到审核变严格后所带来的价值。解决冲突如果上述所有操作仍无法解决您与开发人员之间的冲突,请参阅 “ Code Review 标准 ”以获取有助于解决冲突的指导和原则。代码开发者指南本节页面的内容为开发人员进行代码审查的最佳实践。这些指南可帮助您更快地完成审核并获得更高质量的结果。您不必全部阅读它们,但它们适用于每个 Google 开发人员,并且许阅读全文通常会很有帮助。写好 CL 描述CL 描述是进行了哪些更改以及为何更改的公开记录。 CL 将作为版本控制系统中的永久记录,可能会在长时期内被除审查者之外的数百人阅读。开发者将来会根据描述搜索您的 CL。有人可能会仅凭有关联性的微弱印象,但没有更多具体细节的情况下,来查找你的改动。如果所有重要信息都在代码而不是描述中,那么会让他们更加难以找到你的 CL 。首行正在做什么的简短摘要。完整的句子,使用祈使句。后面跟一个空行。CL 描述的第一行应该是关于这个 CL 是做什么的简短摘要,后面跟一个空白行。这是将来大多数的代码搜索者在浏览代码的版本控制历史时,最常被看到的内容,因此第一行应该提供足够的信息,以便他们不必阅读 CL 的整个描述就可以获得这个 CL 实际上是做了什么的信息。按照传统,CL 描述的第一行应该是一个完整的句子,就好像是一个命令(一个命令句)。例如,“Delete the FizzBuzz RPC and replace it with the new system.”而不是“Deleting the FizzBuzz RPC and replacing it with the new system.“ 但是,您不必把其余的描述写成祈使句。Body 是信息丰富的其余描述应该是提供信息的。可能包括对正在解决的问题的简要描述,以及为什么这是最好的方法。如果方法有任何缺点,应该提到它们。如果相关,请包括背景信息,例如错误编号,基准测试结果以及设计文档的链接。即使是小型 CL 也需要注意细节。在 CL 描述中提供上下文以供参照。糟糕的 CL 描述“Fix bug ”是一个不充分的 CL 描述。什么 bug?你做了什么修复?其他类似的不良描述包括:“Fix build.”“Add patch.”“Moving code from A to B.”“Phase 1.”“Add convenience functions.”“kill weird URLs.”其中一些是真正的 CL 描述。他们的作者可能认为自己提供了有用的信息,却没有达到 CL 描述的目的。好的 CL 描述以下是一些很好的描述示例。功能更新rpc:删除 RPC 服务器消息 freelist 上的大小限制。像 FIzzBuzz 这样的服务器有非常大的消息,并且可以从重用中受益。增大 freelist,添加一个 goroutine,缓慢释放 freelist 条目,以便空闲服务器最终释放所有 freelist 条目。前几个词描述了CL实际上做了什么。其余的描述讨论了正在解决的问题,为什么这是一个很好的解决方案,以及有关具体实现的更多信息。重构Construct a Task with a TimeKeeper to use its TimeStr and Now methods.Add a Now method to Task, so the borglet() getter method can be removed (which was only used by OOMCandidate to call borglet’s Now method). This replaces the methods on Borglet that delegate to a TimeKeeper.Allowing Tasks to supply Now is a step toward eliminating the dependency on Borglet. Eventually, collaborators that depend on getting Now from the Task should be changed to use a TimeKeeper directly, but this has been an accommodation to refactoring in small steps.Continuing the long-range goal of refactoring the Borglet Hierarchy.第一行描述了 CL 的作用以及改变。其余的描述讨论了具体的实现,CL 的背景,解决方案并不理想,以及未来的可能方向。它还解释了为什么正在进行此更改。需要上下文的 小 CLCreate a Python3 build rule for status.py.This allows consumers who are already using this as in Python3 to depend on a rule that is next to the original status build rule instead of somewhere in their own tree. It encourages new consumers to use Python3 if they can, instead of Python2, and significantly simplifies some automated build file refactoring tools being worked on currently.第一句话描述实际做了什么。其余的描述解释了为什么正在进行更改并为审查者提供了大量背景信息。在提交 CL 前审查描述CL 在审查期间可能会发生重大变更。在提交 CL 之前检查 CL 描述是必要的,以确保描述仍然反映了 CL 的作用。小型 CL为什么提交小型 CL?小且简单的 CL 是指审查更快。审查者更容易抽多次五分钟时间来审查小型 CL,而不是留出 30 分钟来审查一个大型 CL。审查得更彻底。如果是大的变更,审查者和提交者往往会因为大量细节的讨论翻来覆去而感到沮丧——有时甚至到了重要点被遗漏或丢失的程度。不太可能引入错误。 由于您进行的变更较少,您和您的审查者可以更轻松有效地推断 CL 的影响,并查看是否已引入错误。如果被拒绝,减少浪费的工作。 如果您写了一个巨大的 CL,您的评论者说整个 CL 的方向都错误了,你就浪费了很多精力和时间。更容易合并。 处理大型 CL 需要很长时间,在合并时会出现很多冲突,并且必须经常合并。更容易设计好。 打磨一个小变更的设计和代码健康状况比完善一个大变更的所有细节要容易得多。减少对审查的阻碍。 发送整体变更的自包含部分可让您在等待当前 CL 审核时继续编码。更简单的回滚。 大型 CL 更有可能触及在初始 CL 提交和回滚 CL 之间更新的文件,从而使回滚变得复杂(中间的 CL 也可能需要回滚)。请注意,审查者可以仅凭 CL 过大而自行决定完全拒绝您的变更。通常他们会感谢您的贡献,但要求您以某种方式将其 CL 改成一系列较小的变更。在您编写完变更后,或者需要花费大量时间来讨论为什么审查者应该接受您的大变更,这可能需要做很多工作。首先编写小型 CL 更容易。什么是小型 CL?一般来说, CL 的正确大小是自包含的变更 。这意味着:CL 进行了一项最小的变更,只解决了一件事。通常只是功能的一部分,而不是一个完整的功能。一般来说,因为编写过小的 CL 而犯错也比过大的 CL 犯错要好。与您的审查者讨论以确定可接受的大小。审查者需要了解的关于 CL 的所有内容(除了未来的开发)都在 CL 的描述、现有的代码库或已经审查过的 CL 中。对其用户和开发者来说,在签入 CL 后系统能继续良好的工作。CL 不会过小以致于其含义难以理解。如果您添加新 API,则应在同一 CL 中包含 API 的用法,以便审查者可以更好地了解 API 的使用方式。这也可以防止签入未使用的 API。关于多大算“太大”没有严格的规则。对于 CL 来说,100 行通常是合理的大小,1000 行通常太大,但这取决于您的审查者的判断。变更中包含的文件数也会影响其“大小”。一个文件中的 200 行变更可能没问题,但是分布在 50 个文件中通常会太大。请记住,尽管从开始编写代码开始就您就已经密切参与了代码,但审查者通常不清楚背景信息。对您来说,看起来像是一个可接受的大小的 CL 对您的审查者来说可能是压倒性的。如有疑问,请编写比您认为需要编写的要小的 CL。审查者很少抱怨收到过小的 CL 提交。什么时候大 CL 是可以的?在某些情况下,大变更也是可以接受的:您通常可以将整个文件的删除视为一行变更,因为审核人员不需要很长时间审核。有时一个大的 CL 是由您完全信任的自动重构工具生成的,而审查者的工作只是检查并确定想要这样的变更。但这些 CL 可以更大,尽管上面的一些警告(例如合并和测试)仍然适用。按文件拆分拆分 CL 的另一种方法是对文件进行分组,这些文件需要不同的审查者,否则就是自包含的变更。例如:您发送一个 CL 以修改协议缓冲区,另一个 CL 发送变更使用该原型的代码。您必须在代码 CL 之前提交 proto CL,但它们都可以同时进行审查。如果这样做,您可能希望通知两组审查者您编写的其他 CL,以便他们对您的变更具有更充足的上下文。另一个例子:你发送一个 CL 用于代码更改,另一个用于使用该代码的配置或实验;如果需要,这也更容易回滚,因为配置/实验文件有时会比代码变更更快地推向生产。分离出重构通常最好在功能变更或错误修复的单独 CL 中进行重构。例如,移动和重命名类应该与修复该类中的错误的 CL 不同。审查者更容易理解每个 CL 在单独时引入的更改。但是,修复本地变量名称等小清理可以包含在功能变更或错误修复 CL 中。如果重构大到包含在您当前的 CL 中,会使审查更加困难的话,需要开发者和审查者一起判断是否将其拆开。将相关的测试代码保存在同一个 CL 中避免将测试代码拆分为单独的 CL。验证代码修改的测试应该进入相同的 CL,即使它增加了代码行数。但是,独立的测试修改可以首先进入单独的 CL,类似于重构指南。包括:使用新测试验证预先存在的已提交代码。重构测试代码(例如引入辅助函数)。引入更大的测试框架代码(例如集成测试)。不要破坏构建如果您有几个相互依赖的 CL,您需要找到一种方法来确保在每次提交 CL 后整个系统能够继续运作。否则可能会在您的 CL 提交的几分钟内打破所有开发人员的构建(如果您之后的 CL 提交意外出错,时间可能会甚至更长)。如果不能让它足够小有时你会遇到看起来您的 CL 必须如此庞大,但这通常很少是正确的。习惯于编写小型 CL 的提交者几乎总能找到将功能分解为一系列小变更的方法。在编写大型 CL 之前,请考虑在重构 CL 之前是否可以为更清晰的实现铺平道路。与你的同伴聊聊,看看是否有人想过如何在小型 CL 中实现这些功能。如果以上的努力都失败了(这应该是非常罕见的),那么请在事先征得审查者的同意后提交大型 CL,以便他们收到有关即将发生的事情的警告。在这种情况下,做好完成审查过程需要很长一段时间的准备,对不引入错误保持警惕,并且在编写测试时要更下功夫。如何处理审查者的评论当您发送 CL 进行审查时,您的审查者可能会对您的 CL 发表一些评论。以下是处理审查者评论的一些有用信息。不是针对您审查的目标是保持代码库和产品的质量。当审查者对您的代码提出批评时,请将其视为在帮助您、代码库和 Google,而不是对您或您的能力的个人攻击。有时,审查者会感到沮丧并在评论中表达他们的挫折感。对于审查者来说,这不是一个好习惯,但作为开发人员,您应该为此做好准备。问问自己,“审查者试图与我沟通的建设性意见是什么?”然后像他们实际说的那样操作。永远不要愤怒地回应代码审查评论。这严重违反了专业礼仪且将永远存在于代码审查工具中。如果您太生气或恼火而无法好好的回应,那么请离开电脑一段时间,或者做一些别的事情,直到您感到平静,可以礼貌地回答。一般来说,如果审查者没有以建设性和礼貌的方式提供反馈,请亲自向他们解释。如果您无法亲自或通过视频通话与他们交谈,请向他们发送私人电子邮件。以友善的方式向他们解释您不喜欢的东西以及您希望他们以怎样不同的方式来做些什么。如果他们也以非建设性的方式回复此私人讨论,或者没有预期的效果,那么请酌情上报给您的经理。修复代码如果审查者说他们不了解您的代码中的某些内容,那么您的第一反应应该是澄清代码本身。 如果无法澄清代码,请添加代码注释,以解释代码存在的原因。 只有在想增加的注释看起来毫无意义时,您才能在代码审查工具中进行回复与解释。如果审查者不理解您的某些代码,那么代码的未来读者可能也不会理解。在代码审查工具中回复对未来的代码读者没有帮助,但澄清代码或添加代码注释确可以实实在在得帮助他们。自我反思编写 CL 可能需要做很多工作。在终于发送一个 CL 用于审查后,我们通常会感到满足的,认为它已经完成,并且非常确定不需要进一步的工作。这通常是令人满意的。因此,当审查者回复对可以改进的事情的评论时,很容易本能地认为评论是错误的,审查者正在不必要地阻止您,或者他们应该让您提交 CL。但是,无论您目前多么确定,请花一点时间退一步,考虑审查者是否提供有助于对代码库和对 Google 的有价值的反馈。您首先应该想到的应该是,“审查者是否正确?”如果您无法回答这个问题,那么审查者可能需要澄清他们的意见。如果您已经考虑过并且仍然认为自己是正确的,请随时回答一下为什么您的方法对代码库、用户和/或 Google 更好。通常,审查者实际上是在提供建议,他们希望您自己思考什么是最好的。您可能实际上对审阅者不知道的用户、代码库或 CL 有所了解。所以提供并告诉他们更多的上下文。通常,您可以根据技术事实在自己和审查者之间达成一些共识。解决冲突解决冲突的第一步应该是尝试与审查者达成共识。 如果您无法达成共识,请参阅“ 代码审查标准 ”,该标准提供了在这种情况下遵循的原则。
2023年09月14日
15 阅读
0 评论
0 点赞
2023-08-30
SQL 语法速成手册
SQL 语法速成手册一、基本概念数据库术语数据库(database) - 保存有组织的数据的容器(通常是一个文件或一组文件)。数据表(table) - 某种特定类型数据的结构化清单。模式(schema) - 关于数据库和表的布局及特性的信息。模式定义了数据在表中如何存储,包含存储什么样的数据,数据如何分解,各部分信息如何命名等信息。数据库和表都有模式。列(column) - 表中的一个字段。所有表都是由一个或多个列组成的。行(row) - 表中的一个记录。主键(primary key) - 一列(或一组列),其值能够唯一标识表中每一行。SQL 语法SQL(Structured Query Language),标准 SQL 由 ANSI 标准委员会管理,从而称为 ANSI SQL。各个 DBMS 都有自己的实现,如 PL/SQL、Transact-SQL 等。SQL 语法结构SQL 语法结构包括:子句 - 是语句和查询的组成成分。(在某些情况下,这些都是可选的。)表达式 - 可以产生任何标量值,或由列和行的数据库表谓词 - 给需要评估的 SQL 三值逻辑(3VL)(true/false/unknown)或布尔真值指定条件,并限制语句和查询的效果,或改变程序流程。查询 - 基于特定条件检索数据。这是 SQL 的一个重要组成部分。语句 - 可以持久地影响纲要和数据,也可以控制数据库事务、程序流程、连接、会话或诊断。SQL 语法要点SQL 语句不区分大小写 ,但是数据库表名、列名和值是否区分,依赖于具体的 DBMS 以及配置。例如:SELECT 与 select 、Select 是相同的。多条 SQL 语句必须以分号(;)分隔 。处理 SQL 语句时, 所有空格都被忽略 。SQL 语句可以写成一行,也可以分写为多行。-- 一行 SQL 语句 UPDATE user SET username='robot', password='robot' WHERE username = 'root'; -- 多行 SQL 语句 UPDATE user SET username='robot', password='robot' WHERE username = 'root';SQL 支持三种注释## 注释1 -- 注释2 /* 注释3 */SQL 分类数据定义语言(DDL)数据定义语言(Data Definition Language,DDL)是 SQL 语言集中负责数据结构定义与数据库对象定义的语言。DDL 的主要功能是 定义数据库对象。 DDL 的核心指令是 CREATE、ALTER、DROP。数据操纵语言(DML)数据操纵语言(Data Manipulation Language, DML)是用于数据库操作,对数据库其中的对象和数据运行访问工作的编程语句。DML 的主要功能是 访问数据,因此其语法都是以读写数据库为主。 DML 的核心指令是 INSERT、UPDATE、DELETE、SELECT。这四个指令合称 CRUD(Create, Read, Update, Delete),即增删改查。事务控制语言(TCL)事务控制语言 (Transaction Control Language, TCL) 用于 管理数据库中的事务 。这些用于管理由 DML 语句所做的更改。它还允许将语句分组为逻辑事务。TCL 的核心指令是 COMMIT、ROLLBACK 。数据控制语言(DCL)数据控制语言 (Data Control Language, DCL) 是一种可 对数据访问权进行控制的指令,它可以控制特定用户账户对数据表、查看表、预存程序、用户自定义函数等数据库对象的控制权。 DCL 的核心指令是 GRANT、REVOKE。DCL 以控制用户的访问权限为主,因此其指令作法并不复杂,可利用 DCL 控制的权限有: CONNECT、SELECT、INSERT、UPDATE、DELETE、EXECUTE、USAGE、REFERENCES。 根据不同的 DBMS 以及不同的安全性实体,其支持的权限控制也有所不同。二、增删改查(以下为 DML 语句用法)增删改查,又称为 CRUD,数据库基本操作中的基本操作。插入数据INSERT INTO 语句用于向表中插入新记录。插入完整的行INSERT INTO user VALUES (10, 'root', 'root', 'xxxx@163.com');插入行的一部分INSERT INTO user(username, password, email) VALUES ('admin', 'admin', 'xxxx@163.com');插入查询出来的数据INSERT INTO user(username) SELECT name FROM account;更新数据UPDATE 语句用于更新表中的记录。UPDATE user SET username='robot', password='robot' WHERE username = 'root';删除数据DELETE 语句用于删除表中的记录。 TRUNCATE TABLE 可以清空表,也就是删除所有行。删除表中的指定数据DELETE FROM user WHERE username = 'robot';清空表中的数据TRUNCATE TABLE user;查询数据SELECT 语句用于从数据库中查询数据。DISTINCT 用于返回唯一不同的值。它作用于所有列,也就是说所有列的值都相同才算相同。LIMIT 限制返回的行数。可以有两个参数,第一个参数为起始行,从 0 开始;第二个参数为返回的总行数。ASC :升序(默认)DESC :降序查询单列SELECT prod_name FROM products;查询多列SELECT prod_id, prod_name, prod_price FROM products;查询所有列ELECT * FROM products;查询不同的值SELECT DISTINCT vend_id FROM products;限制查询结果-- 返回前 5 行 SELECT * FROM mytable LIMIT 5; SELECT * FROM mytable LIMIT 0, 5; -- 返回第 3 ~ 5 行 SELECT * FROM mytable LIMIT 2, 3;三、子查询子查询是 嵌套在较大查询中的 SQL 查询。子查询也称为内部查询或内部选择,而包含子查询的语句也称为外部查询或外部选择。子查询可以嵌套在 SELECT,INSERT,UPDATE 或 DELETE 语句内或另一个子查询中。子查询通常会在另一个 SELECT 语句的 WHERE 子句中添加。您可以使用比较运算符,如 >,<,或 =。比较运算符也可以是多行运算符,如 IN,ANY 或 ALL。子查询必须被圆括号 () 括起来。内部查询首先在其父查询之前执行,以便可以将内部查询的结果传递给外部查询。执行过程可以参考下图:子查询的子查询SELECT cust_name, cust_contact FROM customers WHERE cust_id IN (SELECT cust_id FROM orders WHERE order_num IN ( SELECT order_num FROM orderitems WHERE prod_id = 'RGAN01' ) );WHEREWHERE 子句用于过滤记录,即缩小访问数据的范围。WHERE 后跟一个返回 true 或 false 的条件。WHERE 可以与 SELECT,UPDATE 和 DELETE 一起使用。可以在 WHERE 子句中使用的操作符SELECT 语句中的 WHERE 子句SELECT * FROM Customers WHERE cust_name = 'Kids Place';UPDATE 语句中的 WHERE 子句UPDATE Customers SET cust_name = 'Jack Jones' WHERE cust_name = 'Kids Place';DELETE 语句中的 WHERE 子句DELETE FROM Customers WHERE cust_name = 'Kids Place';IN 和 BETWEENIN 操作符在 WHERE 子句中使用,作用是在指定的几个特定值中任选一个值。BETWEEN 操作符在 WHERE 子句中使用,作用是选取介于某个范围内的值。IN 示例SELECT * FROM products WHERE vend_id IN ('DLL01', 'BRS01');BETWEEN 示例SELECT * FROM products WHERE prod_price BETWEEN 3 AND 5;AND、OR、NOTAND、OR、NOT 是用于对过滤条件的逻辑处理指令。AND 优先级高于 OR,为了明确处理顺序,可以使用 ()。AND 操作符表示左右条件都要满足。OR 操作符表示左右条件满足任意一个即可。NOT 操作符用于否定一个条件。AND 示例SELECT prod_id, prod_name, prod_price FROM products WHERE vend_id = 'DLL01' AND prod_price <= 4;OR 示例SELECT prod_id, prod_name, prod_priceFROM productsWHERE vend_id = 'DLL01' OR vend_id = 'BRS01';NOT 示例SELECT * FROM products WHERE prod_price NOT BETWEEN 3 AND 5;LIKELIKE 操作符在 WHERE 子句中使用,作用是确定字符串是否匹配模式。只有字段是文本值时才使用 LIKE。LIKE 支持两个通配符匹配选项:% 和 _。不要滥用通配符,通配符位于开头处匹配会非常慢。% 表示任何字符出现任意次数。_ 表示任何字符出现一次。拓展:mysql使用like% 示例SELECT prod_id, prod_name, prod_price FROM products WHERE prod_name LIKE '%bean bag%';_ 示例SELECT prod_id, prod_name, prod_price FROM products WHERE prod_name LIKE '__ inch teddy bear';四、连接和组合连接(JOIN)如果一个 JOIN 至少有一个公共字段并且它们之间存在关系,则该 JOIN 可以在两个或多个表上工作。连接用于连接多个表,使用 JOIN 关键字,并且条件语句使用 ON 而不是 WHERE。JOIN 保持基表(结构和数据)不变。JOIN 有两种连接类型:内连接和外连接内连接又称等值连接,使用 INNER JOIN 关键字。在没有条件语句的情况下返回笛卡尔积。自连接可以看成内连接的一种,只是连接的表是自身而已。自然连接是把同名列通过 = 测试连接起来的,同名列可以有多个。内连接 vs 自然连接内连接提供连接的列,而自然连接自动连接所有同名列。外连接返回一个表中的所有行,并且仅返回来自次表中满足连接条件的那些行,即两个表中的列是相等的。外连接分为左外连接、右外连接、全外连接(Mysql 不支持)。左外连接就是保留左表没有关联的行。右外连接就是保留右表没有关联的行。连接 vs 子查询连接可以替换子查询,并且比子查询的效率一般会更快。内连接(INNER JOIN)SELECT vend_name, prod_name, prod_price FROM vendors INNER JOIN products ON vendors.vend_id = products.vend_id;自连接select st2.name, st2.grade from new_student st1, new_student st2 where st1.name='小明' and st1.grade < st2.grade;SQL中 JOIN 的两种连接类型 - 自连接自然连接(NATURAL JOIN)SELECT * FROM Products NATURAL JOIN Customers;左连接(LEFT JOIN)SELECT customers.cust_id, orders.order_num FROM customers LEFT JOIN orders ON customers.cust_id = orders.cust_id;右连接(RIGHT JOIN)SELECT customers.cust_id, orders.order_num FROM customers RIGHT JOIN orders ON customers.cust_id = orders.cust_id;组合(UNION)UNION 运算符将两个或更多查询的结果组合起来,并生成一个结果集,其中包含来自 UNION 中参与查询的提取行。UNION 基本规则所有查询的列数和列顺序必须相同。每个查询中涉及表的列的数据类型必须相同或兼容。通常返回的列名取自第一个查询。默认会去除相同行,如果需要保留相同行,使用 UNION ALL。只能包含一个 ORDER BY 子句,并且必须位于语句的最后。应用场景在一个查询中从不同的表返回结构数据。对一个表执行多个查询,按一个查询返回数据。组合查询SELECT cust_name, cust_contact, cust_email FROM customers WHERE cust_state IN ('IL', 'IN', 'MI') UNION SELECT cust_name, cust_contact, cust_email FROM customers WHERE cust_name = 'Fun4All';JOIN vs UNIONJOIN 中连接表的列可能不同,但在 UNION 中,所有查询的列数和列顺序必须相同。UNION 将查询之后的行放在一起(垂直放置),但 JOIN 将查询之后的列放在一起(水平放置),即它构成一个笛卡尔积。五、函数拓展:mysql文本处理🔔 注意:不同数据库的函数往往各不相同,因此不可移植。本节主要以 Mysql 的函数为例。其中, SOUNDEX() 可以将一个字符串转换为描述其语音表示的字母数字模式。SELECT * FROM mytable WHERE SOUNDEX(col1) = SOUNDEX('apple')日期和时间处理日期格式:YYYY-MM-DD时间格式:HH:MM:SSmysql> SELECT NOW(); 2018-4-14 20:25:11AVG() 会忽略 NULL 行。使用 DISTINCT 可以让汇总函数值汇总不同的值。SELECT AVG(DISTINCT col1) AS avg_colFROM mytable六、排序和分组ORDER BYORDER BY 用于对结果集进行排序。ASC :升序(默认)DESC :降序可以按多个列进行排序,并且为每个列指定不同的排序方式 指定多个列的排序方向SELECT * FROM products ORDER BY prod_price DESC, prod_name ASC; GROUP BY GROUP BY 子句将记录分组到汇总行中。 GROUP BY 为每个组返回一个记录。 GROUP BY 通常还涉及聚合:COUNT,MAX,SUM,AVG 等。 GROUP BY 可以按一列或多列进行分组。 GROUP BY 按分组字段进行排序后,ORDER BY 可以以汇总字段来进行排序。分组SELECT cust_name, COUNT(cust_address) AS addr_num FROM Customers GROUP BY cust_name;分组后排序SELECT cust_name, COUNT(cust_address) AS addr_num FROM Customers GROUP BY cust_name ORDER BY cust_name DESC;HAVINGHAVING 用于对汇总的 GROUP BY 结果进行过滤。HAVING 要求存在一个 GROUP BY 子句。WHERE 和 HAVING 可以在相同的查询中。HAVING vs WHEREWHERE 和 HAVING 都是用于过滤。HAVING 适用于汇总的组记录;而 WHERE 适用于单个记录。使用 WHERE 和 HAVING 过滤数据SELECT cust_name, COUNT(*) AS num FROM Customers WHERE cust_email IS NOT NULL GROUP BY cust_name HAVING COUNT(*) >= 1;七、数据定义(以下为 DDL 语句用法)DDL 的主要功能是定义数据库对象(如:数据库、数据表、视图、索引等)。数据库(DATABASE)创建数据库CREATE DATABASE test;删除数据库DROP DATABASE test;选择数据库USE test;数据表(TABLE)创建数据表普通创建CREATE TABLE user ( id int(10) unsigned NOT NULL COMMENT 'Id', username varchar(64) NOT NULL DEFAULT 'default' COMMENT '用户名', password varchar(64) NOT NULL DEFAULT 'default' COMMENT '密码', email varchar(64) NOT NULL DEFAULT 'default' COMMENT '邮箱') COMMENT='用户表';根据已有的表创建新表CREATE TABLE vip_user AS SELECT * FROM user;删除数据表DROP TABLE user;修改数据表添加列ALTER TABLE user ADD age int(3);删除列ALTER TABLE user DROP COLUMN age;修改列ALTER TABLE `user` MODIFY COLUMN age tinyint;添加主键ALTER TABLE user ADD PRIMARY KEY (id);删除主键ALTER TABLE user DROP PRIMARY KEY;视图(VIEW)MySQL高级篇之View视图定义视图是基于 SQL 语句的结果集的可视化的表。视图是虚拟的表,本身不包含数据,也就不能对其进行索引操作。对视图的操作和对普通表的操作一样。作用简化复杂的 SQL 操作,比如复杂的联结;只使用实际表的一部分数据;通过只给用户访问视图的权限,保证数据的安全性;更改数据格式和表示。创建视图CREATE VIEW top_10_user_view AS SELECT id, username FROM user WHERE id < 10;删除视图DROP VIEW top_10_user_view;索引(INDEX)作用通过索引可以更加快速高效地查询数据。用户无法看到索引,它们只能被用来加速查询。注意更新一个包含索引的表需要比更新一个没有索引的表花费更多的时间,这是由于索引本身也需要更新。因此,理想的做法是仅仅在常常被搜索的列(以及表)上面创建索引。唯一索引唯一索引表明此索引的每一个索引值只对应唯一的数据记录。创建索引CREATE INDEX user_indexON user (id);创建唯一索引CREATE UNIQUE INDEX user_indexON user (id);删除索引ALTER TABLE userDROP INDEX user_index;约束SQL 约束用于规定表中的数据规则。如果存在违反约束的数据行为,行为会被约束终止。约束可以在创建表时规定(通过 CREATE TABLE 语句),或者在表创建之后规定(通过 ALTER TABLE 语句)。约束类型NOT NULL - 指示某列不能存储 NULL 值。UNIQUE - 保证某列的每行必须有唯一的值。PRIMARY KEY - NOT NULL 和 UNIQUE 的结合。确保某列(或两个列多个列的结合)有唯一标识,有助于更容易更快速地找到表中的一个特定的记录。FOREIGN KEY - 保证一个表中的数据匹配另一个表中的值的参照完整性。CHECK - 保证列中的值符合指定的条件。DEFAULT - 规定没有给列赋值时的默认值。创建表时使用约束条件:CREATE TABLE Users ( Id INT(10) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '自增Id', Username VARCHAR(64) NOT NULL UNIQUE DEFAULT 'default' COMMENT '用户名', Password VARCHAR(64) NOT NULL DEFAULT 'default' COMMENT '密码', Email VARCHAR(64) NOT NULL DEFAULT 'default' COMMENT '邮箱地址', Enabled TINYINT(4) DEFAULT NULL COMMENT '是否有效', PRIMARY KEY (Id)) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COMMENT='用户表';八、事务处理(以下为 TCL 语句用法)不能回退 SELECT 语句,回退 SELECT 语句也没意义;也不能回退 CREATE 和 DROP 语句。MySQL 默认是隐式提交,每执行一条语句就把这条语句当成一个事务然后进行提交。当出现 START TRANSACTION 语句时,会关闭隐式提交;当 COMMIT 或 ROLLBACK 语句执行后,事务会自动关闭,重新恢复隐式提交。通过 set autocommit=0 可以取消自动提交,直到 set autocommit=1 才会提交;autocommit 标记是针对每个连接而不是针对服务器的。指令START TRANSACTION - 指令用于标记事务的起始点。SAVEPOINT - 指令用于创建保留点。ROLLBACK TO - 指令用于回滚到指定的保留点;如果没有设置保留点,则回退到 START TRANSACTION 语句处。COMMIT - 提交事务。-- 开始事务START TRANSACTION;-- 插入操作 A START TRANSACTION; -- 插入操作 AINSERT INTO `user` VALUES (1, 'root1', 'root1', 'xxxx@163.com'); -- 创建保留点 updateA SAVEPOINT updateA; -- 插入操作 B INSERT INTO `user` VALUES (2, 'root2', 'root2', 'xxxx@163.com'); -- 回滚到保留点 updateA ROLLBACK TO updateA; -- 提交事务,只有操作 A 生效 COMMIT;九、权限控制(以下为 DCL 语句用法)GRANT 和 REVOKE 可在几个层次上控制访问权限:整个服务器,使用 GRANT ALL 和 REVOKE ALL;整个数据库,使用 ON database.*;特定的表,使用 ON database.table;特定的列;特定的存储过程。新创建的账户没有任何权限。账户用 username@host 的形式定义,username@% 使用的是默认主机名。MySQL 的账户信息保存在 mysql 这个数据库中。USE mysql;SELECT user FROM user;创建账户CREATE USER myuser IDENTIFIED BY 'mypassword';修改账户名UPDATE user SET user='newuser' WHERE user='myuser'; FLUSH PRIVILEGES;删除账户DROP USER myuser;查看权限SHOW GRANTS FOR myuser;授予权限GRANT SELECT, INSERT ON *.* TO myuser;删除权限REVOKE SELECT, INSERT ON *.* FROM myuser;更改密码SET PASSWORD FOR myuser = 'mypass';十、存储过程存储过程可以看成是对一系列 SQL 操作的批处理;使用存储过程的好处代码封装,保证了一定的安全性;代码复用;由于是预先编译,因此具有很高的性能。创建存储过程命令行中创建存储过程需要自定义分隔符,因为命令行是以 ; 为结束符,而存储过程中也包含了分号,因此会错误把这部分分号当成是结束符,造成语法错误。包含 in、out 和 inout 三种参数。给变量赋值都需要用 select into 语句。每次只能给一个变量赋值,不支持集合的操作。创建存储过程DROP PROCEDURE IF EXISTS `proc_adder`; DELIMITER ;; CREATE DEFINER=`root`@`localhost` PROCEDURE `proc_adder`(IN a int, IN b int, OUT sum int) BEGIN DECLARE c int; if a is null then set a = 0; end if; if b is null then set b = 0; end if; set sum = a + b; END; DELIMITER;使用存储过程set @b=5; call proc_adder(2,@b,@s); select @s as sum;十一、游标游标(cursor)是一个存储在 DBMS 服务器上的数据库查询,它不是一条 SELECT 语句,而是被该语句检索出来的结果集。在存储过程中使用游标可以对一个结果集进行移动遍历。游标主要用于交互式应用,其中用户需要对数据集中的任意行进行浏览和修改。使用游标的四个步骤:声明游标,这个过程没有实际检索出数据;打开游标;取出数据;关闭游标;DELIMITER $ CREATE PROCEDURE getTotal() BEGIN DECLARE total INT; -- 创建接收游标数据的变量 DECLARE sid INT; DECLARE sname VARCHAR(10); -- 创建总数变量 DECLARE sage INT; -- 创建结束标志变量 DECLARE done INT DEFAULT false; -- 创建游标 DECLARE cur CURSOR FOR SELECT id,name,age from cursor_table where age>30; -- 指定游标循环结束时的返回值 DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = true; SET total = 0; OPEN cur; FETCH cur INTO sid, sname, sage; WHILE(NOT done) DO SET total = total + 1; FETCH cur INTO sid, sname, sage; END WHILE; CLOSE cur; SELECT total; END $ DELIMITER ; -- 调用存储过程 call getTotal();十二、触发器触发器是一种与表操作有关的数据库对象,当触发器所在表上出现指定事件时,将调用该对象,即表的操作事件触发表上的触发器的执行。可以使用触发器来进行审计跟踪,把修改记录到另外一张表中。MySQL 不允许在触发器中使用 CALL 语句 ,也就是不能调用存储过程。BEGIN 和 END当触发器的触发条件满足时,将会执行 BEGIN 和 END 之间的触发器执行动作。🔔 注意:在 MySQL 中,分号 ; 是语句结束的标识符,遇到分号表示该段语句已经结束,MySQL 可以开始执行了。因此,解释器遇到触发器执行动作中的分号后就开始执行,然后会报错,因为没有找到和 BEGIN 匹配的 END。这时就会用到 DELIMITER 命令(DELIMITER 是定界符,分隔符的意思)。它是一条命令,不需要语句结束标识,语法为:DELIMITER new_delemiter。new_delemiter 可以设为 1 个或多个长度的符号,默认的是分号 ;,我们可以把它修改为其他符号,如 $ - DELIMITER $ 。在这之后的语句,以分号结束,解释器不会有什么反应,只有遇到了 $,才认为是语句结束。注意,使用完之后,我们还应该记得把它给修改回来。NEW 和 OLDMySQL 中定义了 NEW 和 OLD 关键字,用来表示触发器的所在表中,触发了触发器的那一行数据。在 INSERT 型触发器中,NEW 用来表示将要(BEFORE)或已经(AFTER)插入的新数据;在 UPDATE 型触发器中,OLD 用来表示将要或已经被修改的原数据,NEW 用来表示将要或已经修改为的新数据;在 DELETE 型触发器中,OLD 用来表示将要或已经被删除的原数据;使用方法: NEW.columnName (columnName 为相应数据表某一列名)创建触发器提示:为了理解触发器的要点,有必要先了解一下创建触发器的指令。CREATE TRIGGER 指令用于创建触发器。语法:CREATE TRIGGER trigger_name trigger_time trigger_event ON table_name FOR EACH ROW BEGIN trigger_statements END;说明:trigger_name:触发器名trigger_time: 触发器的触发时机。取值为 BEFORE 或 AFTER。trigger_event: 触发器的监听事件。取值为 INSERT、UPDATE 或 DELETE。table_name: 触发器的监听目标。指定在哪张表上建立触发器。FOR EACH ROW: 行级监视,Mysql 固定写法,其他 DBMS 不同。trigger_statements: 触发器执行动作。是一条或多条 SQL 语句的列表,列表内的每条语句都必须用分号 ; 来结尾。示例:DELIMITER $ CREATE TRIGGER `trigger_insert_user` AFTER INSERT ON `user` FOR EACH ROW BEGIN INSERT INTO `user_history`(user_id, operate_type, operate_time) VALUES (NEW.id, 'add a user', now()); END $ DELIMITER ;查看触发器SHOW TRIGGERS;删除触发器DROP TRIGGER IF EXISTS trigger_insert_user;
2023年08月30日
17 阅读
0 评论
0 点赞
2023-08-30
进程、线程、进程池、进程三态、同步、异步、并发、并行、串行
进程、线程、进程池、进程三态、同步、异步、并发、并行、串行一.进程, 线程1.🌵进程🍹什么是进程?开发写的代码我们称为程序,那么将开发的代码运行起来。我们称为进程。明白点: 当我们运行一个程序,那么我们将运行的程序叫进程。👉精简重点👈进程是申请一块内存空间,将数据放到内存空间中去, 是申请数据的过程是最小的资源管理单元进程是线程的容器🍹程序与进程的区别程序是数据和指令的集合, 是一个静态的概念, 就是一堆代码, 可以长时间的保存在系统中进程是程序运行的过程, 是一个动态的概念, 进程存在着生命周期, 也就是说进程会随着程序的终止而销毁, 不会永久存在系统中🍹进程之间交互进程之间通过 TCP/IP 端口实现2.🌵线程🍹什么是线程线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。👉精简重点👈是进程的一条流水线, 只用来执行程序,而不涉及到申请资源, 是程序的实际执行者最小的执行单元🍹线程之间交互多个线程 共享同一块内存 ,通过共享的内存空间来进行交互3.🌵进程与线程的关系🍹例子我们打开一个聊天软件,这就是开启了一个进程当我们在软件里面打开一些功能,比如空间, 扫一扫, 设置...,这些操作就是线程所以可以说 "进程" 包含 "线程", "线程" 是 "进程" 的子集🍹进程是线程的容器工厂流水线例子4.🌵总结进程:指在系统中正在运行的一个应用程序;程序一旦运行就是进程; 进程——资源分配的最小单位。线程:系统分配处理器时间资源的基本单元,或者说进程之内独立执行的一个单元执行流。 线程——程序执行的最小单位。进程要分配一大部分的内存,而线程只需要分配一部分栈就可以了.一个程序至少有一个进程,一个进程至少有一个线程.一个线程可以创建和撤销另一个线程,同一个进程中的多个线程之间可以并发执行.二.并行, 并发, 串行并发: 多个任务看起来是同时进行, 这是一种假并行单核下使用多道技术实现并行: 多个任务同时进行并行必须有多核才能实现,否则只能实现并发(伪并行)串行: 一个程序完完整整的运行完,再运行下一个进程三.任务运行的三种状态进程在运行的过程中不断地改变其运行状态 通常一个运行的进程必须具有三种状态:就绪态, 运行态, 阻塞态1.就绪态 (Ready)当进程已分配到除CPU以外的所有必要的资源后,只要再获得CPU, 便可执行程序, 进程这时的状态就称为就绪态,在一个系统中处于就绪态的进程可能有多个 , 通常将他们排成一个队列, 这就叫 就绪队列2.运行态 (Running)当进程已经获得CPU操作权限, 其程序正在运行, 着就叫做运行态在单核操作系统中, 只有一个进程处于运行态 , 多核操作系统有多个进程处于运行态3.阻塞态 (Blocked)(sleep)正在执行的进程, 由于等待某个事件而无法执行时, 便被操作系统剥夺了cpu的操作时间, 这是就是阻塞态引起阻塞的 原因 多种, 例如: 等待I/O操作, 更高优先级的任务抢走了CPU权限等.4.进程三种状态 间的转换一个进程在运行期间, 会不断地在一种状态切换到另一只种状态 他可以是多次处于就绪态和运行态, 也可以多次处于阻塞态, 下图是三种状态的转换图就绪态➠➠运行态处于就绪态的进程, 当进程调度程序为之分配了CPU的时间片后, 该进程就会由就绪态转变成运行态运行态➠➠就绪态处于运行态的进程在运行过程中, 因为分配的时间片用完了, 于是失去了CPU的使用权限, 运行态就会重新转为就绪态运行态➠➠阻塞态正在运行的进程由于遇到I/O操作或被更高优先级的任务抢走CPU使用权限而无法继续执行, 便从运行态转为阻塞态阻塞态➠➠就绪态处于阻塞态的进程, 若其等待的事情已经处理完毕, 于是进程从阻塞态转为就绪态四.任务提交的两种方式1.同步同步是指发送方发送数据后, 等接收方发回响应后才发下一个数据报的通讯方式同步是指 两个程序的运行是相关的 , 其中一个线程在阻塞需要等待状态, 那另一个线程才运行2.异步异步是指发送方发出数据后, 不等接收方发回响应, 接着就发下个数据报的通讯方式异步是指 两个线程毫无相关 , 自己运行自己的3.例子同步你叫我去吃饭, 我听到了就立即和你去吃饭, 如果没有听到, 你就不停的叫, 直到我告诉你听到了, 才一起去吃饭打电话好比同步, 两边是同时进行不能再打给另一个人异步你叫我去吃饭, 然后自己去吃饭了, 我得到消息后可能立即走, 也可能过会儿走发消息好比异步, 和一个人发完消息就可能和另一个人发消息五.进程池1.什么是进程池?👉 进程池是资源进程, 管理进程组成的技术的应用.2.为什么要有进程池?忙时会有成千上万的任务需要被执行,闲时可能只有零星任务。 那么在成千上万个任务需要被执行的时候,我们就需要去创建成千上万个进程么? 首先,创建进程需要消耗时间,销毁进程也需要消耗时间。第二即便开启了成千上万的进程,操作系统也不能让他们同时执行,这样反而会影响程序的效率。因此我们不能无限制的根据任务去开启或者结束进程。那么我们要怎么做呢?3.进程池的概念定义一个池子,在里面放上固定数量的进程,有需求来了,就拿一个池中的进程来处理任务 等到处理完毕,进程并不关闭,而是将进程再放回进程池中继续等待任务 如果有很多任务需要执行,池中的进程数量不够,任务就要等待之前的进程执行任务完毕归来,拿到空闲进程才能继续执行。重点来了 ,也就是说, 进池中进程的数量是固定的,那么同一时间最多有固定数量的进程在运行 这样不会增加操作系统的调度难度,还节省了开关进程的时间,也一定程度上能够实现 并发 效果。4.资源进程预先创建好的空闲进程 ,管理进程(好比池子🏊)会把工作分发到空闲进程来处理。5.管理进程🏊管理进程 负责创建资源进程,把工作交给空闲资源进程处理,回收已经处理完工作的资源进程。资源进程与管理进程的交互管理进程如何有效的管理资源进程,分配任务给资源进程? 通过IPC,信号,信号量,消息队列,管道等进行交互。
2023年08月30日
16 阅读
0 评论
0 点赞
1
...
4
5
6
...
20