什么是 MySQL?
MySQL 是一种使用 C 和 C++ 语言编写的关系型数据库管理系统。
递归(英语:Recursion),又译为递回,在数学与计算机科学中,是指在函数的定义中使用函数自身的方法。递归一词还较常用于描述以自相似方法重复事物的过程。例如,当两面镜子相互之间近似平行时,镜中嵌套的图像是以无限递归的形式出现的。也可以理解为自我复制的过程。
简单来讲,递归就是函数反复调用自身的过程。
你肯定听过这个故事:从前有座山,山里有座庙,庙里有个老和尚,正在给小和尚讲故事呢!故事是什么呢?“从前有座山,山里有座庙,庙里有个老和尚,正在给小和尚讲故事呢!故事是什么呢?‘从前有座山,山里有座庙,庙里有个老和尚,正在给小和尚讲故事呢!故事是什么呢?……’”
上面的故事即是递归思想的体现。
十种常见排序算法可以分为两大类:


相关概念:
选择排序(Selection-sort):首先在乱序序列中找到最小(大)元素存放到排序序列的起始位置,然后再从剩余乱序序列中继续寻找最小(大)元素,放到已排序序列的末尾。以此类推,直到所有元素排序完毕。
n 个记录的直接选择排序可经过 n-1 趟直接选择排序得到有序结果。具体算法描述如下:
1 | public class SelectSort { |
表现最稳定的排序算法之一,因为无论什么数据进去都是 O($n^2$) 的时间复杂度,所以用到它的时候,数据规模越小越好。
选择排序唯一的好处可能就是不占用额外的内存空间。理论上讲,选择排序可能也是平时排序一般最容易想到的排序方式了。
冒泡排序(Bubble Sort):重复地走访过要排序的数列,一次比较两个元素,若它们的顺序错误就把它们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。
这个算法的名字由来是因为越小的元素会经由交换慢慢“浮”到数列的顶端。
冒泡排序的步骤如下:

1 | public class BubbleSort { |
插入排序(Insertion Sort):通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入,类似于玩扑克牌。
一般来说,插入排序都采用 in-place 在数组上实现。具体算法描述如下:

1 | public class InsertionSort { |
插入排序在实现上,通常采用 in-place 排序(即只需用到 O(1) 的额外空间的排序),因而在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。
希尔排序(Shell Sort)是 1959 年由 Shell 发明,第一个突破 O(n2) 的排序算法,是简单插入排序的改进版。它与插入排序的不同之处在于,它会优先比较距离较远的元素。
希尔排序也叫缩小增量排序。
先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,具体算法描述:

1 | function shellSort(arr) { |
希尔排序的核心在于间隔序列的设定。既可以提前设定好间隔序列,也可以动态的定义间隔序列。动态定义间隔序列的算法是《算法(第4版)》的合著者Robert Sedgewick提出的。
快速排序的基本思想:通过一趟排序将待排记录分隔成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序。
快速排序使用分治法来把一个串(list)分为两个子串(sub-lists)。具体算法描述如下:

1 | public class QuickSort { |
归并排序是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为2-路归并。

1 | function mergeSort(arr) { |
归并排序是一种稳定的排序方法。和选择排序一样,归并排序的性能不受输入数据的影响,但表现比选择排序好的多,因为始终都是 O(nlogn)的时间复杂度。代价是需要额外的内存空间。
堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。

1 | var len; // 因为声明的多个函数都需要数据长度,所以把len设置成为全局变量 |
计数排序不是基于比较的排序算法,其核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。 作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。

1 | function countingSort(arr, maxValue) { |
计数排序是一个稳定的排序算法。当输入的元素是 n 个 0到 k 之间的整数时,时间复杂度是 O(n+k) ,空间复杂度也是O(n+k),其排序速度快于任何比较排序算法。当k不是很大并且序列比较集中时,计数排序是一个很有效的排序算法。
桶排序是计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。桶排序 (Bucket sort)的工作的原理:假设输入数据服从均匀分布,将数据分到有限数量的桶里,每个桶再分别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排)。

1 | function bucketSort(arr, bucketSize) { |
桶排序最好情况下使用线性时间O(n),桶排序的时间复杂度,取决与对各个桶之间数据进行排序的时间复杂度,因为其它部分的时间复杂度都为O(n)。很显然,桶划分的越小,各个桶之间的数据越少,排序所用的时间也会越少。但相应的空间消耗就会增大。
基数排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序。最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。
1 | var counter = []; |
基数排序基于分别排序,分别收集,所以是稳定的。但基数排序的性能比桶排序要略差,每一次关键字的桶分配都需要O(n)的时间复杂度,而且分配之后得到新的关键字序列又需要O(n)的时间复杂度。假如待排数据可以分为d个关键字,则基数排序的时间复杂度将是O(d*2n) ,当然d要远远小于n,因此基本上还是线性级别的。
基数排序的空间复杂度为 O(n+k),其中 k 为桶的数量。一般来说 n>>k ,因此额外空间需要大概n个左右。
1 | public class BinarySearch { |
| 时间 | 说明 |
|---|---|
| 2018-12-29 | 初稿 |
index属性设置为false,提高 ES 性能,比如说用户图片的地址就不需要进行搜索,可以这么设置1 | { |
磁盘在现代服务器.上通常都是瓶颈。Elasticsearch重度使用磁盘,你的磁盘能处理的吞吐量越大,你的节点就越稳定。这里有一些优化磁盘I/0 的技巧:
分片和副本的设计为 ES 提供了支持分布式和故障转移的特性,但并不意味着分片和副本是可以无限分配的。而且由于索引主分片的路由机制,一旦主分片完成分配后,无法重新修改主分片的数量。
可能有人会说,我不知道这个索引将来会变得多大,并且过后我也不能更改索引的大小,所以为了保险起见,还是给它设为 100 个分片吧。这样并不合理,因为配置分片时并不是没有代价的:
一个业务索引具体需要分配多少分片可能需要架构师和技术人员对业务的增长有个预先的判断,横向扩展应当分阶段进行, 为下一阶段准备好足够的资源。 只有当你进入到下一个阶段,你才有时间思考需要作出哪些改变来达到这个阶段。一般来说, 我们遵循一些原则
控制每个分片占用的硬盘容量不超过 ES 的最大 JVM 的堆空间设置(一般设置不超过 32G,参考下文的 JVM 设置原则),因此,如果索引的总容量在 500G 左右,那分片大小在 16 个左右即可;当然,最好同时考虑原则
考虑一下node数量,一般一个节点有时候就是一台物理机, 如果分片数过多,大大超过了节点数,很可能会导致一个节点上存在多个分片,一旦该节点故障,即使保持了1个以上的副本,同样有可能会导致数据丢失,集群无法恢复。所以,一般都设置分片数不超过节点数的 3 倍
主分片,副本和节点最大数之间数量,我们分配的时候可以参考以下关系:节点数<=主分片数*(副本数+1)
对于节点瞬时中断的问题,默认情况, 集群会等待一分钟来查看节点是否会重新加入,如果节点在此期间重新加入, 重新加入的节点会保持其现有的分片数据,不会触发新的分片分配。这样就可以减少 ES 在自动再平衡可用分片时所带来的极大开销。
通过修改参数 delayed timeout,可以延长再均衡的时间,可以全局设置也可以在索引级别进行修改:
1 | PUT /_all/_settings |
当我们查询文档的时候,Elasticsearch 如何知道一个文档应该存放到哪个分片中呢?它其实是通过下面这个公式来计算出来:
1 | shard = hash(routing) % number_of primary_shards |
routing 默认值是文档的 id, 也可以采用自定义值,比如用户 id。
在查询的时候因为不知道要查询的数据具体在哪个分片上, 所以整个过程分为 2 个步骤:
查询的时候,可以直接根据 routing 信息定位到某个分配查询, 不需要查询所有的分配,经过协调节点排序。
向上面自定义的用户查询,如果 routing 设置为 userid 的话, 就可以直接查询出数据来,效率提升很多。
ES 的默认配置,是综合了数据可靠性、写入速度、搜索实时性等因素。
实际使用时, 我们需要根据公司要求,进行偏向性的优化。
针对于搜索性能要求不高,但是对写入要求较高的场景,我们需要尽可能的选择恰当写优化策略。
综合来说,可以考虑以下几个方面来提升写索引的性能:
ES 提供了 Bulk API 工支持批量操作,当我们有大量的写任务时,可以使用 Bulk 来进行批量写入。
通用的策略如下:Bulk 默认设置批量提交的数据量不能超过 100M。数据条数一般是根据文档的大小和服务器性能而定的,但是单次批处理的数据大小应从 5MB~15MB 逐渐增加,当性能没有提升时,把这个数据量作为最大值。
ES 是一种密集使用磁盘的应用,在段合并的时候会频繁操作磁盘,所以对磁盘要求较高,当磁盘速度提升之后,集群的整体性能会大幅度提高。
Lucene以段的形式存储数据。当有新的数据写入索引时,Lucene 就会自动创建一个新的段。
随着数据量的变化,段的数量会越来越多,消耗的多文件句柄数及CPU就越多,查询效率就会下降。
由于Lucene 段合并的计算量庞大,会消耗大量的I/O, 所以ES默认采用较保守的策略,让后台定期进行段合并
Lucene在新增数据时,用了延迟写入的策略,默认情况下索引的refiesh interval 为1秒。
Lucene将待写入的数据先写到内存中,超过1秒(默认)时就会触发一次 Refesh, 然后Refresh会把内存中的的数据刷新到操作系统的文件缓存系统中。
如果我们对搜索的实效性要求不高,可以将Refresh 周期延长,例如30秒。
这样还可以有效地减少段刷新次数,提高写的效率,但这同时意味着需要消耗更多的 Heap 内存。
Flush的主要目的是把文件缓存系统中的段持久化到硬盘,当Translog 的数据量达到512MB或者30分钟时,会触发一次Flush.
index.translog. flush threshold size参数的默认值是 512MB,我们进行修改。
增加参数值意味着文件缓存系统中可能需要存储更多的数据,所以我们需要为操作系统的文件缓存系统留下足够的空间。
ES 为了保证集群的可用性,提供了Replica(副本)支持,由于每个副本也会执行分析、索引及可能的合并过程,所以 Replica 的数量会影响写索引的效率。
当写索引时,除了把数据写入主分片节点中,还会并行将数据写入到所有副本分片节点,副本节点越多,写索引的效率就越慢。
因此,如果我们需要大批量进行写入操作,可以先禁止 Replica 复制,设置index.number.of_replicas: 0关闭副本;在写入完成后,再将 Replica 修改回正常的状态。
ES 默认安装后设置的内存是基于服务器总内存设置的,如果安装 ES 的机器还存在其他应用,那么会影响到它们。
对于解压安装的 ES,则其中包含一个jvm.option配置文件,可通过以下参数设置 ES 堆大小:1
2
3# Xms 表示堆的初始大小,Xmx 表示可分配的最大内存
-Xms4g
-Xmx4g
确保 Xmx 和 Xms 的大小是相同的,其目的是为了能够在 Java 垃圾回收机制清理完堆区后不需要重新分隔计算堆区的大小而浪费资源,可以减轻伸缩堆大小带来的压力。
假设你有一个 64G 内存的机器,按照正常思维思考,你可能会认为把 64G 内存都给ES比较好,但现实是这样吗,越大越好?
虽然内存对 ES 来说至关重要,但是答案是否定的!因为 ES 堆内存的分配需要满足以下两个原则:
最终我们都会采用 31G 设置
1 | -Xms 31g |
ES版本5.6,数据量在3000万左右,数据更新频率比较频繁,总共的更新速度大概是1w/s-5w/s。
最新的数据先进kafka,再由flink消费写入ES。
目前发现在默认的ES配置下,bulk update或者upsert的速度始终上不去,所有节点的cpu使用率才25%左右,速度最高只能到1w/s左右。
如果改成这些数据全部是插入,不做更新操作,那么cpu可以跑满,而且kafka的消费绝对没有积压。
试过增加节点,效果很小,速度看起来有一点点的增加。试过增加分片数,有效果,但仍然不是很明显。
请问是什么原因导致这个现象呢,增加分片对写入速度有提升又是为什么呢?
像这种场景,有没有办法直接的提升update/upsert速率,或者间接解决,比如全部用插入的方式写入,查询时同一个id的记录取时间最近的数据。那么查询方式还有删除旧数据这块怎么设计比较好
1.update是先get再insert然后再delete(标记删除)旧的文档,和insert相比,肯定update耗时多
2.由于一次操作完成时长多,线程池数量有限,导致cpu只有25%(猜测…)
3.适当增加ES分片,对写入是有一点提高,因为相当于多出来了lucene进程,可以接收的请求多了,付出的代价就是分片多了之后同步数据是需要消耗性能的,然后查询更是会性能降低
4.客户端使用层面:可以全部使用insert提高性能,然后定时去delete,定时(低峰期)合并segment,优化数据结构
5.集群本身层面:可以控制refresh的频率,translog设置,副本可以先干掉(写完再补回来),线程池参数修改——-这些都是危险操作,评估后再进行实践
在上面基础上补充:
在不改业务逻辑的情况下,只能指定id来做更新。
改线程池大小、设置translog异步写、加大refresh interval、index buffer size这些以前测试过都没有明显提高,或者说几乎没有提高。
改业务逻辑的话,不知道怎么样保持像不改业务逻辑那样的效果。例如有10条数据,9条更新频繁,1条几天都不更新一次。那么如果我每次都是新写入,查询时取时间最近的记录,这个好办,但删旧数据时,怎么办呢,如果根据时间删掉当天之前的全部数据,那么原来的10条数据可能只剩下9条了。
集群规模的评估主要评估以下三个方面:
1 | PUT /user_gift_202401 |
哈希表(Hash table,也叫哈希表),是根据键(Key)而直接访问在内存存储位置的数据结构。也就是说,它通过计算一个关于键值的函数,将所需查询的数据映射到表中一个位置来访问记录,这加快了查找速度。这个映射函数称做哈希函数,存放记录的数组称做哈希表。
一个通俗的例子是,为了查找电话簿中某人的号码,可以创建一个按照人名首字母顺序排列的表(即建立人名 x 到首字母 F(x) 的一个函数关系),在首字母为 W 的表中查找“王”姓的电话号码,显然比直接查找就要快得多。这里使用人名作为关键字,“取首字母”是这个例子中哈希函数的函数法则 F(x) ,存放首字母的表对应哈希表。关键字和函数法则理论上可以任意确定。