精华内容
下载资源
问答
  • 协同数据交换平台利用企业服务总线、数据抽取ETL、消息中间件、大文件传输等相关技术,包括文件...从设计、生产、质量、财务以及人员等几个方面进行院内部数据应用集成场景的梳理,实现相关系统之间的数据集成与共享
  • 常用数据科学方法总结梳理笔记

    千次阅读 2019-05-10 16:27:06
    常用数据科学方法 【未经允许,不得转载】 ...

                                           常用数据科学方法

                                                     【未经允许,不得转载】

                                                                                                                                                                                                                                                ——沂水寒城

    一、数据缺失值处理

    对于数据挖掘和分析人员来说,数据准备(Data Preparation,包括数据的抽取、清洗、转换和集成)常常占据了70%左右的工作量。而在数据准备的过程中,数据质量差又是最常见而且令人头痛的问题,诸如数据缺失值、特殊值等问题,都需要专门的处理方法才行,本文档整理了常用的序列数据的处理方法,为数据挖掘、机器学习等工作提供数据处理基础。

    我们所说的缺失值,不仅包括数据库中的NULL值,也包括用于表示数值缺失的特殊数值(比如,在系统中用-999来表示数值不存在)。如果我们仅有数据库的数据模型,而缺乏相关说明,常常需要花费更多的精力来发现这些数值的特殊含义。而如果我们漠视这些数值的特殊性,直接拿来进行挖掘,那么很可能会得到错误的结论。

    还有一种数值缺失的情况,是因为我们要求统计的时间窗口并非对所有数据都适合。例如,我们希望计算出“客户在以前六个月内的最大存款余额”,对于那些建立账户尚不满六个月的客户来说,统计出来的数值与我们想要得到的就可能存在差距。

    一般来说,对缺失值的填充方法有多种,用某个常数来填充常常不是一个好方法。最好建立一些模型,根据数据的分布来填充一个更恰当的数值。(例如根据其它变量对记录进行数据分箱,然后选择该记录所在分箱的相应变量的均值或中位数,来填充缺失值,效果会更好一些)

    数据缺失的原因

    在各种使用的数据库中,属性值缺失的情况经常发全甚至是不可避免的。因此,在大多数情况下,信息系统是不完备的,或者说存在某种程度的不完备。

    缺失值的产生的原因多种多样,主要分为机械原因和人为原因。机械原因是由于机械原因导致的数据收集或保存的失败造成的数据缺失,比如数据存储的失败,存储器损坏,机械故障导致某段时间数据未能收集(对于定时数据采集而言)。人为原因是由于人的主观失误、历史局限或有意隐瞒造成的数据缺失,比如,在市场调查中被访人拒绝透露相关问题的答案,或者回答的问题是无效的,数据录入人员失误漏录了数据

    造成数据缺失的原因是多方面的,主要可能有以下几种:

    1)有些信息暂时无法获取。例如在医疗数据库中,并非所有病人的所有临床检验结果都能在给定的时间内得到,就致使一部分属性值空缺出来。又如在申请表数据中,对某些问题的反映依赖于对其他问题的回答。

    2)有些信息是被遗漏的。可能是因为输入时认为不重要、忘记填写了或对数据理解错误而遗漏,也可能是由于数据采集设备的故障、存储介质的故障、传输媒体的故障、一些人为因素等原因而丢失了。

    3)有些对象的某个或某些属性是不可用的。也就是说,对于这个对象来说,该属性值是不存在的,如一个未婚者的配偶姓名、一个儿童的固定收入状况等。

    4)有些信息(被认为)是不重要的。如一个属性的取值与给定语境是无关的,或训练数据库的设计者并不在乎某个属性的取值(称为dont-care value)。

    5)获取这些信息的代价太大。

    6)系统实时性能要求较高,即要求得到这些信息前迅速做出判断或决策。

    数据缺失机制

    在对缺失数据进行处理前,了解数据缺失的机制和形式是十分必要的。将数据集中不含缺失值的变量(属性)称为完全变量,数据集中含有缺失值的变量称为不完全变量,Little 和 Rubin定义了以下三种不同的数据缺失机制:

    1)完全随机缺失(Missing Completely at Random,MCAR)。数据的缺失与不完全变量以及完全变量都是无关的。

    2)随机缺失(Missing at Random,MAR)。数据的缺失仅仅依赖于完全变量。

    3)非随机、不可忽略缺失(Not Missing at Random,NMAR,or nonignorable)。不完全变量中数据的缺失依赖于不完全变量本身,这种缺失是不可忽略的。

    从缺失值的所属属性上讲,如果所有的缺失值都是同一属性,那么这种缺失成为单值缺失,如果缺失值属于不同的属性,称为任意缺失。另外对于时间序列类的数据,可能存在随着时间的缺失,这种缺失称为单调缺失。

    空值语义

    对于某个对象的属性值未知的情况,我们称它在该属性的取值为空值(null value)。空值的来源有许多种,因此现实世界中的空值语义也比较复杂。总的说来,可以把空值分成以下三类:

    1)不存在型空值。即无法填入的值,或称对象在该属性上无法取值,如一个未婚者的配偶姓名等。

    2)存在型空值。即对象在该属性上取值是存在的,但暂时无法知道。一旦对象在该属性上的实际值被确知以后,人们就可以用相应的实际值来取代原来的空值,使信息趋于完全。存在型空值是不确定性的一种表征,该类空值的实际值在当前是未知的。但它有确定性的一面,诸如它的实际值确实存在,总是落在一个人们可以确定的区间内。一般情况下,空值是指存在型空值。

    3)占位型空值。即无法确定是不存在型空值还是存在型空值,这要随着时间的推移才能够清楚,是最不确定的一类。这种空值除填充空位外,并不代表任何其他信息。

    空值处理的重要性和复杂性

    数据缺失在许多研究领域都是一个复杂的问题。对数据挖掘来说,空值的存在,造成了以下影响:

    第一,系统丢失了大量的有用信息;

    第二,系统中所表现出的不确定性更加显著,系统中蕴涵的确定性成分更难把握;

    第三,包含空值的数据会使挖掘过程陷入混乱,导致不可靠的输出。

    数据挖掘算法本身更致力于避免数据过分适合所建的模型,这一特性使得它难以通过自身的算法去很好地处理不完整数据。因此,空缺的数据需要通过专门的方法进行推导、填充等,以减少数据挖掘算法与实际应用之间的差距。

    常用缺失值处理方法的分析比较

    处理不完备数据集的方法主要有以下三大类:

    (一)删除元组

    也就是将存在遗漏信息属性值的对象(元组,记录)删除,从而得到一个完备的信息表。这种方法简单易行,在对象有多个属性缺失值、被删除的含缺失值的对象与信息表中的数据量相比非常小的情况下是非常有效的,类标号(假设是分类任务)缺少时通常使用。然而,这种方法却有很大的局限性。它是以减少历史数据来换取信息的完备,会造成资源的大量浪费,丢弃了大量隐藏在这些对象中的信息。在信息表中本来包含的对象很少的情况下,删除少量对象就足以严重影响到信息表信息的客观性和结果的正确性;当每个属性空值的百分比变化很大时,它的性能非常差。因此,当遗漏数据所占比例较大,特别当遗漏数据非随机分布时,这种方法可能导致数据发生偏离,从而引出错误的结论。

    (二)数据补齐

    这类方法是用一定的值去填充空值,从而使信息表完备化。通常基于统计学原理,根据决策表中其余对象取值的分布情况来对一个空值进行填充,譬如用其余属性的平均值来进行补充等。

    数据挖掘中常用的有以下几种补齐方法:

    (1)人工填写(filling manually

    由于最了解数据的还是用户自己,因此这个方法产生数据偏离最小,可能是填充效果最好的一种。然而一般来说,该方法很费时,当数据规模很大、空值很多的时候,该方法是不可行的。

    (2)特殊值填充(Treating Missing Attribute values as Special values

    将空值作为一种特殊的属性值来处理,它不同于其他的任何属性值。如所有的空值都用“unknown”填充。这样将形成另一个有趣的概念,可能导致严重的数据偏离,一般不推荐使用。

    (3)平均值填充(Mean/Mode Completer

    将信息表中的属性分为数值属性和非数值属性来分别进行处理。如果空值是数值型的,就根据该属性在其他所有对象的取值的平均值来填充该缺失的属性值;如果空值是非数值型的,就根据统计学中的众数原理,用该属性在其他所有对象的取值次数最多的值(即出现频率最高的值)来补齐该缺失的属性值。另外有一种与其相似的方法叫条件平均值填充法(Conditional Mean Completer)。在该方法中,缺失属性值的补齐同样是靠该属性在其他对象中的取值求平均得到,但不同的是用于求平均的值并不是从信息表所有对象中取,而是从与该对象具有相同决策属性值的对象中取得。这两种数据的补齐方法,其基本的出发点都是一样的,以最大概率可能的取值来补充缺失的属性值,只是在具体方法上有一点不同。与其他方法相比,它是用现存数据的多数信息来推测缺失值。

    (4)热卡填充(Hot deck imputation,或就近补齐)

    对于一个包含空值的对象,热卡填充法在完整数据中找到一个与它最相似的对象,然后用这个相似对象的值来进行填充。不同的问题可能会选用不同的标准来对相似进行判定。该方法概念上很简单,且利用了数据间的关系来进行空值估计。这个方法的缺点在于难以定义相似标准,主观因素较多。

    (5)K最近距离邻法(K-means clustering

    先根据欧式距离或相关分析来确定距离具有缺失数据样本最近的K个样本,将这K个值加权平均来估计该样本的缺失数据。同均值插补的方法都属于单值插补,不同的是,它用层次聚类模型预测缺失变量的类型,再以该类型的均值插补。假设X=(X1,X2…Xp)为信息完全的变量,Y为存在缺失值的变量,那么首先对X或其子集行聚类,然后按缺失个案所属类来插补不同类的均值。如果在以后统计分析中还需以引入的解释变量和Y做分析,那么这种插补方法将在模型中引入自相关,给分析造成障碍。

    (6)使用所有可能的值填充(Assigning All Possible values of the Attribute

    这种方法是用空缺属性值的所有可能的属性取值来填充,能够得到较好的补齐效果。但是,当数据量很大或者遗漏的属性值较多时,其计算的代价很大,可能的测试方案很多。另有一种方法,填补遗漏属性值的原则是一样的,不同的只是从决策相同的对象中尝试所有的属性值的可能情况,而不是根据信息表中所有对象进行尝试,这样能够在一定程度上减小原方法的代价。

    (7)组合完整化方法(Combinatorial Completer

    这种方法是用空缺属性值的所有可能的属性取值来试,并从最终属性的约简结果中选择最好的一个作为填补的属性值。这是以约简为目的的数据补齐方法,能够得到好的约简结果;但是,当数据量很大或者遗漏的属性值较多时,其计算的代价很大。另一种称为条件组合完整化方法(Conditional Combinatorial Complete),填补遗漏属性值的原则是一样的,不同的只是从决策相同的对象中尝试所有的属性值的可能情况,而不是根据信息表中所有对象进行尝试。条件组合完整化方法能够在一定程度上减小组合完整化方法的代价。在信息表包含不完整数据较多的情况下,可能的测试方案将巨增。

    (8)回归(Regression

    基于完整的数据集,建立回归方程(模型)。对于包含空值的对象,将已知属性值代入方程来估计未知属性值,以此估计值来进行填充。当变量不是线性相关或预测变量高度相关时会导致有偏差的估计。

    (9)期望值最大化方法(Expectation maximizationEM

    在缺失类型为随机缺失的条件下,假设模型对于完整的样本是正确的,那么通过观测数据的边际分布可以对未知参数进行极大似然估计(Little and Rubin)。这种方法也被称为忽略缺失值的极大似然估计,对于极大似然的参数估计实际中常采用的计算方法是期望值最大化(Expectation Maximization,EM)。该方法比删除个案和单值插补更有吸引力,它一个重要前提:适用于大样本。有效样本的数量足够以保证ML估计值是渐近无偏的并服从正态分布。但是这种方法可能会陷入局部极值,收敛速度也不是很快,并且计算很复杂。

    EM算法是一种在不完全数据情况下计算极大似然估计或者后验分布的迭代算法。在每一迭代循环过程中交替执行两个步骤:E步(Excepctaion step,期望步),在给定完全数据和前一次迭代所得到的参数估计的情况下计算完全数据对应的对数似然函数的条件期望;M步(Maximzation step,极大化步),用极大化对数似然函数以确定参数的值,并用于下步的迭代。算法在E步和M步之间不断迭代直至收敛,即两次迭代之间的参数变化小于一个预先给定的阈值时结束。该方法可能会陷入局部极值,收敛速度也不是很快,并且计算很复杂。

    (10)多重填补(Multiple ImputationMI

    多值插补的思想来源于贝叶斯估计,认为待插补的值是随机的,它的值来自于已观测到的值。具体实践上通常是估计出待插补的值,然后再加上不同的噪声,形成多组可选插补值。根据某种选择依据,选取最合适的插补值。

    多重填补方法分为三个步骤:;为每个空值产生一套可能的填补值,这些值反映了无响应模型的不确定性;每个值都被用来填补数据集中的缺失值,产生若干个完整数据集合。;每个填补数据集合都用针对完整数据集的统计方法进行统计分析。;对来自各个填补数据集的结果进行综合,产生最终的统计推断,这一推断考虑到了由于数据填补而产生的不确定性。该方法将空缺值视为随机样本,这样计算出来的统计推断可能受到空缺值的不确定性的影响。该方法的计算也很复杂。

    多重插补方法分为三个步骤:①为每个空值产生一套可能的插补值,这些值反映了无响应模型的不确定性;每个值都可以被用来插补数据集中的缺失值,产生若干个完整数据集合。②每个插补数据集合都用针对完整数据集的统计方法进行统计分析。③对来自各个插补数据集的结果,根据评分函数进行选择,产生最终的插补值。

    假设一组数据,包括三个变量Y1,Y2,Y3,它们的联合分布为正态分布,将这组数据处理成三组,A组保持原始数据,B组仅缺失Y3,C组缺失Y1和Y2。在多值插补时,对A组将不进行任何处理,对B组产生Y3的一组估计值(作Y3关于Y1,Y2的回归),对C组作产生Y1和Y2的一组成对估计值(作Y1,Y2关于Y3的回归)。

    当用多值插补时,对A组将不进行处理,对B、C组将完整的样本随机抽取形成为m组(m为可选择的m组插补值),每组个案数只要能够有效估计参数就可以了。对存在缺失值的属性的分布作出估计,然后基于这m组观测值,对于这m组样本分别产生关于参数的m组估计值,给出相应的预测即,这时采用的估计方法为极大似然法,在计算机中具体的实现算法为期望最大化法(EM)。对B组估计出一组Y3的值,对C将利用 Y1,Y2,Y3它们的联合分布为正态分布这一前提,估计出一组(Y1,Y2)。

    上例中假定了Y1,Y2,Y3的联合分布为正态分布。这个假设是人为的,但是已经通过验证(Graham和Schafer于1999),非正态联合分布的变量,在这个假定下仍然可以估计到很接近真实值的结果。

    多重插补和贝叶斯估计的思想是一致的,但是多重插补弥补了贝叶斯估计的几个不足:

    (1)贝叶斯估计以极大似然的方法估计,极大似然的方法要求模型的形式必须准确,如果参数形式不正确,将得到错误得结论,即先验分布将影响后验分布的准确性。而多重插补所依据的是大样本渐近完整的数据的理论,在数据挖掘中的数据量都很大,先验分布将极小的影响结果,所以先验分布的对结果的影响不大。

    (2)贝叶斯估计仅要求知道未知参数的先验分布,没有利用与参数的关系。而多重插补对参数的联合分布作出了估计,利用了参数间的相互关系。

    (11) C4.5方法

    通过寻找属性间的关系来对遗失值填充。它寻找之间具有最大相关性的两个属性,其中没有遗失值的一个称为代理属性,另一个称为原始属性,用代理属性决定原始属性中的遗失值。这种基于规则归纳的方法只能处理基数较小的名词型属性。

    (三)不处理

    补齐处理只是将未知值补以我们的主观估计值,不一定完全符合客观事实,在对不完备信息进行补齐处理的同时,我们或多或少地改变了原始的信息系统。而且,对空值不正确的填充往往将新的噪声引入数据中,使挖掘任务产生错误的结果。因此,在许多情况下,我们还是希望在保持原始信息不发生变化的前提下对信息系统进行处理。这就是第三种方法:

    直接在包含空值的数据上进行数据挖掘。这类方法包括贝叶斯网络和人工神经网络等。

    贝叶斯网络是用来表示变量间连接概率的图形模式,它提供了一种自然的表示因果信息的方法,用来发现数据间的潜在关系。在这个网络中,用节点表示变量,有向边表示变量间的依赖关系。贝叶斯网络仅适合于对领域知识具有一定了解的情况,至少对变量间的依赖关系较清楚的情况。否则直接从数据中学习贝叶斯网的结构不但复杂性较高(随着变量的增加,指数级增加),网络维护代价昂贵,而且它的估计参数较多,为系统带来了高方差,影响了它的预测精度。当在任何一个对象中的缺失值数量很大时,存在指数爆炸的危险。

    人工神经网络可以有效的对付空值,但人工神经网络在这方面的研究还有待进一步深入展开。人工神经网络方法在数据挖掘应用中的局限性

    缺失值填充方法总结分析:

    就几种基于统计的方法而言,删除元组法和平均值法差于hot deck、EM和MI;回归是比较好的一种方法,但仍比不上hot deck和EM;EM缺少MI包含的不确定成分。值得注意的是,这些方法直接处理的是模型参数的估计而不是空缺值预测本身。它们合适于处理无监督学习的问题,而对有监督学习来说,情况就不尽相同了。譬如,你可以删除包含空值的对象用完整的数据集来进行训练,但预测时你却不能忽略包含空值的对象。另外,C4.5和使用所有可能的值填充方法也有较好的补齐效果,人工填写和特殊值填充则是一般不推荐使用的。

    大多数数据挖掘系统都是在数据挖掘之前的数据预处理阶段采用第一、第二类方法来对空缺数据进行处理。并不存在一种处理空值的方法可以适合于任何问题。无论哪种方式填充,都无法避免主观因素对原系统的影响,并且在空值过多的情形下将系统完备化是不可行的。从理论上来说,贝叶斯考虑了一切,但是只有当数据集较小或满足某些条件(如多元正态分布)时完全贝叶斯分析才是可行的。而现阶段人工神经网络方法在数据挖掘中的应用仍很有限。值得一提的是,采用不精确信息处理数据的不完备性已得到了广泛的研究。不完备数据的表达方法所依据的理论主要有可信度理论、概率论、模糊集合论、可能性理论,D-S的证据理论等。

     

     

    二:数据平滑处理

    在实际应用中,由于数据质量参差不齐对于数据挖掘工作的影响不能够忽略不计,往往70%-80%左右的时间都需要花在数据准备的阶段里面,上面的部分简单介绍和总结了一下常用的缺失值处理方法,这部分介绍一下常用的数据平滑处理方法。

    数据平滑最根本的目的就是:降低高概率,提高低概率。

    常用的数据平滑处理方法包括:拉普拉斯数据平滑(Laplace Smoothing)、古德-图灵(Good-Turing)平滑和简单移动平均平滑。

     

     

     

    (一)拉普拉斯数据平滑

    拉普拉斯平滑(Laplace Smoothing)又被称为加 1 平滑,是比较常用的平滑方法。平滑方法的存在时为了解决零概率问题。所谓零概率问题,就是在计算新实例的概率时,如果某个分量在训练集中从没出现过,会导致整个实例的概率计算结果为0,。针对文本分类问题就是当一个词语在训练集中没有出现过,那么该词语的概率就为0,使用连乘法计算文本出现的概率时,整个文本出现的概率也为0,这显然是不合理的,因为不能因为一个事件没有观测到就判断该事件的概率为0.

    拉普拉斯计算方法总结:分子加1,分母加K,K代表类别数目

     

     

    (二)古德-图灵(Good-Turing)平滑

    在统计语言模型章节中,我们谈到了N元语法模型不可避免的一个问题,就是数据稀疏,其原因是大规模语料统计与有限语料的矛盾。根据齐普夫(Zipf)法则,我们能够推测知零概率问题不可避免。数据稀疏问题的解决办法就是进行平滑处理。平滑处理的算法有很多,本文将介绍众多算法中的佼佼者:古德-图灵(Good-Turing)平滑算法。

    古德-图灵(Good-Turing)估计法是很多平滑技术的核心,于1953年有古德(I.J.Good)引用图灵(Turing)的方法而提出来的。其基本思想是:利用频率的类别信息来平滑频率。对于任何发生r次数的n元语法,都假设它发生了r*次。

    其中,nr是训练语料中正好发生r次的N元组的个数。也就是说,发生r次的N元组的调整由发生r次的N元组与发生r+1次的N元组两个类别共同决定。统计数为r*次的N元组,其概率为:

    我们注意到: 也就是说,N等于这个分布中最初的统计。那样,样本中所有事件的概率之和为

    因此,可以这样说我们把你n1/N的概率剩量分配给未见事件。为了更好地理解古德-图灵(Good-Turing)估计法,以一个例子来讲解。

    训练集合:T={<s>what is it what is small?}|T|=8

    验证集合:V={what is it small ? <s> flying birds are a bird.}, |V|=12

    在训练集合上,我们得到:p(<s>)=p(it)=p(small)=p(?)=0.125, p(what)=p(is)=0.25,其他为0

    如果不经过平滑处理,则验证集上两句子的概率分别为:p(what is it?)=(0.25*2)*(0.125*2)≈0.001  p(it is flying.)=0.125*0.25*(0*2)=0

    现在用古德-图灵算法进行平滑处理,如下:

    首先计算,各发生r次N元组类别的数目,依次为 N(0)=6,N(1)=4,N(2)=2,N(i)=0 ,i>2:

    其次,重新估计各概率值。

    对于发生0次的事件概率:Pr(.)=p(flying)=p(birds)=p(are)=p(bird)=p(a)= (0+1)*N(0+1)/(8*N(0))=1*4/(8*6)≈0.083

    对于发生1次的时间概率:Pr(it)=p(<s>)=p(small)=p(?)=(1+1)*N(1+1)/(8*N(1))=2*2 /(8*4)=0.125

    对于发生两次的时间概率:Pr(what)=Pr(is)=(2+1)*N(2+1)/(8*N(2))=3*0/(8*2)=0: 保持原值0.25

    归一化处理,6*P0+4*P1+2*P2=1.5。.

    所以,归一化处理后,p’(it)=p’(<s>)=p’ (small)=p’(?)= 0.125/1.5 ≈0.08,  p’(what)=p’(is)= 0.25/1.5 ≈0.17, 

    p’(.)=p’(birds)=p’(are)=p’(bird)=p’(a) = 0.083/1.5  ≈0.06

    因此:p’(what is it?)=(0175*2)*(0.08*2)≈0.0002   p’(it is flying.) ≈ 0.08*0.17*(0.06*2)≈0.00004

     

     

    (三)简单移动平均平滑

    简单移动平均平滑是计算与等权重的指示函数的卷积,也可以不等权重.

    数据平滑示意图如下图所示:

        其中,蓝线是原始的数据曲线,绿线是经过数据平滑处理后的曲线。

    1.用ones函数创建一个元素均为1的数组,然后对整个数组除以N,得到等权重.

    2.使用权值,调用convolve函数.

    3.从convolve函数分安徽的数组中取出中间的长度为N的部分(即两者作卷积运算时完全重叠的区域.)

    4.使用matplotlib绘图

     

     

     

     

    二:常见概率分布

        概率基础是机器学习、深度学习等众多智能领域的核心基础,了解常用的一些概率分布对于了解模型内在的工作机理是很有帮助的。

    (一)长尾效应

    长尾分布,或者说长尾理论是一个与互联网发展分不开的概念。说到这里就不得不先提一下传统商业中的帕累托法则(Pareto principle),又称为二八定律。比如80%的财富集中在20%的人手里,图书馆里20%的书可以满足80%的顾客。于是大家往往只关注在PDF图中最左面的20%的顾客,以期满足80%,如下图绿色的部分,来实现效益的最大化。

    根据维基百科,长尾(The Long Tail)这一概念是由“连线”杂志主编克里斯·安德森(Chris Anderson)在2004年十月的“长尾” 一文中最早提出,用来描述诸如亚马逊和Netflix之类网站的商业和经济模式。“长尾”实际上是统计学中幂律(Power Laws)和帕累托分布(Pareto)特征的一个口语化表达。简单的长尾分布如下图所示:

        举例说明:卖一辆大众汽车利润只有几万元,卖一辆兰博基尼利润则达到几十万,翻了几十倍!然而买大众汽车的人却比买兰博基尼的人多百倍、千倍!这样一来,大众的成功便不难解释了。

    Google和阿里巴巴的成功,也在于他们找到了一条长尾,把许许多多的小市场拼合在一起,成就了一个大市场。阿里巴巴从那些不被其他商家关注的中小企业、小微企业入手,把网下的贸易搬到了网上,以较低的门槛吸引他们在网上展开贸易,而这些处于长尾的小微企业,也通过阿里巴巴找到了更多的机会和财富,这些长长的尾巴聚集起来,也就铸造了阿里巴巴的成功,铺就了马云的财富金路。

    产品的“二八”市场呈现“长尾”分布,二者对比如下:

    长尾理论是蓝海战略的延续,长尾理论的基本原理是聚沙成塔,创造市场规模。 长尾价值重构目的是满足个性需求,通过创意和网络,提供一些更具价值内容,更个性化的东西,二者对比如下:

    (二)肥尾分布(Fat-tailed distribution

    从建模的角度来看,肥尾分布就是针对那些罕见事件虽然发生的概率低,但也必须要考虑到的情况。比如一个保险公司考虑灾害的发生和保险的定价,那么像自然灾害这种情况,如果不考虑的话就可能面临真的发生时要赔很多的情况。因为正如肥尾分布的名字所体现的,即使在远离峰值的远端,那些罕见事件还是有相当的概率会发生的。虽然我们常常用正态分布对很多时间进行建模,但当一个事件的本质是肥尾分布而我们误用了正态分布或指数分布时,就存在着对“小概率事件真的发生”这种危险的低估。据说美国股市历史上的黑色星期五,千禧年的互联网泡沫破灭,以及2008年前后的金融危机都是这种错误的真实案例(来源:Wikipedia, Fat-tailed distribution)。

    肥尾分布的数学定义为:

    limx→∞Pr[X>x]∼x−α,α>0

    也就是说,在 x较大的地方,肥尾分布趋于0的速度是明显慢于指数分布和正态分布的。柯西分布(Cauchy distribution)就是一类有名的肥尾分布。关于柯西分布,有几个有趣的性质,首先它是稳定的(stable),也有着显式的PDF和CDF,但是它的均值和方差确是无法定义的(undefined)。于是中心极限定理在这里就不适用了。如果试着做一下仿真,我们也可以发现,随着实验次数的增大,样本的均值并不会逐渐收敛到某个值上,而出现这种情况的原因就是时不时出现的“异常大值”会明显改变样本的均值。

    (三)重尾分布(Heavy-tailed distribution

    指数分布在 x→∞的时候是以指数的速度趋近于0,那么以指数分布为分界线,我们把 x→∞时下降速度更快的称为 Thin-tailed distribution,比如正态分布。也就是说,在远离峰值的尾部区域,时间发生的概率更低一些。所以正态分布用来对那些主流事件发生较多,非主流事件发生较少的情况进行建模更为合适。与此相对的,把 x→∞时下降速度慢于指数分布的成为重尾分布(Heavy-tailed distribution)。其数学定义为:

    limx→∞eλxF¯(x)=∞,for all λ>0

    其中,F¯(x)≡Pr(X>x)F¯(x)≡Pr(X>x) 是所谓的尾分布函数。

    重尾分布更适用于对那些离峰值较远的稀有事件也会有相当的概率发生的情况。重尾分布作为一个大的类别,还包含三个重要的子类别,分别是肥尾分布(Fat-tailed distribution),长尾分布(Long-tailed distribution)和次指数分布(Subexponential distribution)。

    (四)随机游走(Random walk

    所谓随机游走,是统计学中一个很广泛的概念,包含了很多内容。我没能找到一个统一的数学模型来描述随机游走,但大意就是在任意维度的空间里,一个点随机地向任意方向前进随机长度的距离,然后重复这一步骤的过程。有一个有名的醉汉回家问题就是一个典型的一维随机游走的问题。

    Lévy flight 是随即游走的一种,它的每一步方向完全随机而各向同性,但步长的分布是重尾分布(heavy-tailed)。Brownian motion(好像)也算是随即游走的一种,它的步长的分布取正态分布。下面两张图来自Wikipedia,分别描述了1000次的基于柯西分布的 Lévy flight (左)和基于正态分布的Brownian motion(右)。

    从这张图上也可以比较明显地看出 Lévy flight 出现大跨步的频率确实要比 Brownian motion 要多一些。已经有相当多的研究表明很多动物的移动模式可以用 Lévy flight 来描述。而近些年通过对人类的移动数据(通话记录、出租车等)的挖掘,我们惊奇地发现人类的移动模式也和 Lévy flight 高度吻合。也就是说,虽然我们每个人急功近利地去追求自己的目标,但在宏观的尺度上,我们和山里的猴子没什么区别。

    (五)连续型随机分布

    统计学中连续型随机分布主要包括:正态分布、均匀分布、指数分布、对数正态分布、柯西分布、Gamma分布、瑞利分布和韦伯分布。

     

    1)正态分布(Normal Distribution

    正态分布应该是实际使用中,接触最多,也是假设或者使用最多的一种连续型数据分布形态了,通常又称为高斯分布。

    2)均匀分布(Uniform Distribution

        均匀分布应该是最简单的一种概率分布函数了,概率分布函数f(x)曲线如下图所示:

    X落在(a,b)中任意等长度的子区间内的概率都是相同的,即它落在子区间的概率只依赖于子区间的长度,而与子区间的位置无关。

       常见应用情形:

       1、在数值计算中,由于四舍五入,小数点后某一位小数引入的误差;

       2、公交线路上两辆公共汽车前后通过某汽车停车站的时间,即:乘客的候车时间。(等等)

    3)指数分布(Exponential Distribution

        指数分布的使用也是很广泛的,在实际的应用中,往往采用指数分来来对时间特性进行描述。

     

     

    常见应用情形:

    主要用于描述独立事件发生的时间间隔。自然界中有很多“寿命”都可以用指数分布来进行描述。如:电子元件的寿命、动物的寿命、电话的童话时间、服务系统地服务时间等。

    4)对数分布(Log Distribution

    如果一个随机变量的对数服从正态分布,那么就称该随机变量服从于对数正态分布。

    常见应用情形:

    金融保险业、投资收益计算等。

    5)柯西分布(Cauchy Distribution

    柯西分布被称为是正态分布的孪生兄弟,它和正态分布的曲线是极为相似的也是很容易混淆的。

    常见应用情形:

    主要应用于物理学中,是描述受迫共振的微分方程的解。在光谱学中,它用来描述被共振或者其他机制加宽的谱线的形状。

    6Gamma分布

    Gamma分布又称为伽马分布,多用于描述随机事件的发生时间间隔。

    常见应用情形:

    用于描述随机变量X等到第K件事发生所需等待的时间。

    7)瑞利分布(Rayleigh Distribution

    当一个随二维向量的两个分量呈独立的、有着相同的方差的正态分布时,这个向量的模呈瑞利分布。

    常见应用情形:

    常用与描述平坦衰落信号接收包络或独立多径分量接受包络统计时变特性。如两个正交高斯噪声信号之和的包络服从瑞利分布。

    8)韦伯分布(Weibull Distribution

    韦氏分布或者威布尔分布,是可靠性分析和寿命检验的理论基础。

    常见应用情形:

    可靠性和失效分析、极值理论。

    (六)离散型随机分布

          统计学中的常用离散型随机分布主要包括:二项分布、几何分布、超几何分布、泊松分布。

    1)二项分布(Bernoulli Distribution

    2)负二项分布(Negative Bernoulli Distribution

    2)几何分布(Geometric Distribution

    3)超几何分布(Geometric Distribution

    4)泊松分布(Poisson Distribution

     

    (七)三大抽样分布

          统计学中包含的三大抽样分布分别为:卡方分布、F分布和t分布。

    1)卡方分布

    2F分布

    2t分布

    分布之间的关系:

     

     

     

     

     

    展开全文
  • 软件需求分析阶段研究的对象是软件项目的用户要求,如何准确表达用户的要求,怎 样与用户共同明确将要开发的是一个什么样的系统,是需求分析要解决的主要问题。也就 是说需求阶段的任务并不是确定系统怎样完成工作,...

           软件需求分析阶段研究的对象是软件项目的用户要求,如何准确表达用户的要求,怎 样与用户共同明确将要开发的是一个什么样的系统,是需求分析要解决的主要问题。也就 是说需求阶段的任务并不是确定系统怎样完成工作,而仅仅是确定系统必须完成哪些工作, 即对目标系统提出完整、准确、清晰、具体的要求。需求分析阶段所要完成的任务是以软 件计划阶段确定的软件工作范围为指南,通过分析综合建立分析模型,编制出软件需求规 格说明书。

     软件需求分析在软件工程生存期的阶段:

           

    软件需求分析的具体内容、步骤如下:

    生成的文档主要有软件需求规格说明书,内容如下:

     

         个人理解(如有不足还望指正):需求分析阶段中的分析建模主要是---将用户的需求抽象为概念模式。而这种概念模式有多种表达方式,可以通过E-R图表现,也可以通过DFD数据流图表现,还可以通过STD状态-变迁图表现,以上三种图都是以DD数据字典为基础进行的。

         软件工程第一步的【软件计划】阶段确定了方案是否可行,第二个【需求分析】阶段,则是考虑软件运行的功能、性能以及对环境要求的一个宏观把控、宏观梳理。

    展开全文
  • 平时我也买了一些Android开发相关的,虽然每本书只看了一部分的内容(这个是我个人的问题,不能怨作者,所以说大家不要自己买,而要劝别人买,买完后借过来读,这样效率还高些),不过遇到问题时还是会重新翻翻...

    之所以写下这篇文章,是因为我最近在Android应用开发过程中常会产生很多疑问,有些甚至是很简单很基础的问题。平时我也买了一些Android开发相关的书,虽然每本书只看了一部分的内容(这个是我个人的问题,不能怨作者,所以说大家不要自己买书,而要劝别人买,买完后借过来读,这样效率还高些),不过遇到问题时还是会重新翻翻。

    不过应该是我脑袋不灵光的原因,读这些书对我的问题没有实质性的帮助。看着它们就好像看着作者对着我露出深深地鄙视:这种问题还要说明?你照着我写的做,能用就行了。

    好吧,幸好有Android API文档以及网上各路大神们的解答,这才为我的智商充了一点值。我想现在可能也有一小撮和我一样的弱智分子,我们对Android了解得越多,越发现自己对它不了解。

    因此才有了这种将我对Android系统所了解内容进行梳理的想法,如果真的有哪位大神无意中看见了这篇文章或者后续文章中存在的错误,还请随手指出,不才感激万分。

     

    好了,不再废话了,下面针对AndroidManifest.xml文件进行说明。

    AndroidManifest.xml是一个应用中的配置文件,我们在手机上应用信息中能够查看的内容多数是在这个文件中定义的。比如:应用的名称、应用的版本、应用中有开启了哪些服务、应用申请了哪些权限等等。同时它还包含了一些用户看不到但是对应用来说至关重要的信息,比如用来唯一区别应用的标识、应用适配的Android系统版本等。

    下面是一个从Android API官方文档中扒下来的结构说明:

    <?xml version="1.0" encoding="utf-8"?>
    
    <manifest>
    
        <uses-permission />
        <permission />
        <permission-tree />
        <permission-group />
        <instrumentation />
        <uses-sdk />
        <uses-configuration />
        <uses-feature />
        <supports-screens />
        <compatible-screens />
        <supports-gl-texture />
        
        <application>
            
            <activity>
                <intent-filter>
                    <action />
                    <category />
                    <data />
                </intent-filter>
                <meta-data />
            </activity>
            
            <activity-alias>
                <intent-filter> . . . </intent-filter>
                <meta-data />
            </activity-alias>
            
            <service>
                <intent-filter> . . . </intent-filter>
                <meta-data/>
            </service>
            
            <receiver>
                <intent-filter> . . . </intent-filter>
                <meta-data />
            </receiver>
            
            <provider>
                <grant-uri-permission />
                <meta-data />
            </provider>
            
            <uses-library />
    
        </application>
    
    </manifest>
    


    从上面的结构中可以看出,这是一个典型的xml文件,每个xml标签表示一项具体信息,下面我们来对每个标签内容进行说明。

    先放上一个最简单的AndroidManifest.xml文件:

    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
        package="com.oldsun.test"
        android:versionCode="1"
        android:versionName="1.0" >
    
        <uses-sdk
            android:minSdkVersion="9"
            android:targetSdkVersion="14" />
        <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    
        <application
            android:allowBackup="true"
            android:icon="@drawable/ic_launcher"
            android:label="@string/app_name"
            android:theme="@style/AppTheme" >
            
            <activity
                android:name="com.oldsun.test.MainActivity"
                android:label="@string/app_name" >
    
                <intent-filter>
                    <action android:name="android.intent.action.MAIN" />
                    <category android:name="android.intent.category.LAUNCHER" />
                </intent-filter>
            </activity>
        </application>
    
    </manifest>
    

    <manifest>标签说明

    首先xmlns:android="http://schemas.android.com/apk/res/android"定义了一个xml的命名空间,也就是说它表示该标签包括的内容可以使用该schemas文件进行约束。其中xmlns是xmlnamespace的缩写,而android表示命名空间的别名,后续引用这个命名空间时可以用android:+属性名来表示,例如android:versionCode,而后面的http://schemas.android.com/apk/res/android是android系统本身为我们创建的命名空间,如果使用eclipse等开发工具,可以查看到该命名空间的定义。关于命名空间,后续再进行讨论。

    接下来的属性:package="com.oldsun.test",顾名思义指的是应用开发时的包信息,由于Java中为了防止包名重复,开始时将域名颠倒过来作为包名信息,因此沿袭过来的Android同样采用该方式,并且指定该值为应用的唯一标识,它同样还是应用进程的默认名称。

    也就是说,如果有两个应用存在同样的包名,在Android系统中就会认为这是同一个应用,顶多存在前后版本的关系。当然了,大家也不用害怕另外一个人开发的应用会将我们的应用替换掉,因为在覆盖安装时,除了包名一致外,还需要安装文件的证书签名一致,关于这部分以后再讨论。

    工作上的关系,有时候别人经常向我问一个apk安装文件的标识是什么,有没有一种方法可以不通过反编译文件直接得到包名。实际上,还真有方法。由于apk本质上就是一个zip格式的压缩文件(因此有的手机上下载apk时会自动转换为zip),我们可以通过winrar或者7zip等工具将其进行解压缩,得到AndroidManifest.xml文件。注意,大家不要高兴得太早,这个文件可是一个二进制文件,里面并非xml格式。这样做当然是为了能够让Android系统更有效率地进行操作,不过却给我们造成了部分困扰。

    不过没关系,如果只是要查看包名是什么,还是比较容易的。即使将一个文本格式的xml进行二进制转换,里面也不会对数据内容进行编码转换,用UE打开这个文件,可以看到如下内容:


    实际上这就是包名信息,只不过每个字节间都插入了00字节,如果使用winrar进行查看,由于00为不可见字符,显示信息就更明显了:


    当然了,还有一种最准确的方式是将xml文件重新转换为文档文件,这种反编译的处理后续再进行讨论。

    android:versionCode="1"android:versionName="1.0"用来表示应用的版本信息,其中android:versionCode是用来真正进行版本识别的,也可以给一些应用商店用来进行版本比较,它的值是int类型,越大的值表示版本越高;而android:versionName只是单纯的用于显示使用其中可以为任意字符,一般是数字的点分格式,也有一些包含beta等内容。

    其它manifest标签下内容还包括android:sharedUserIdandroid:sharedUserLabelandroid:installLocation

    其中sharedUserId是给应用分配的用户空间,如果设置该项,默认每个应用都在不同的用户空间,这样可以防止不同应用间互相干扰;sharedUserLabel是给sharedUserId设置一个用户可读的标签,只有sharedUserId设置时才有效。关于用户空间的内容后续再讨论。

    installLocation是Android2.2系统引入的,它用来设置程序的默认安装位置,按照API文档中的描述,有三个选项可供选择:

    android:installLocation=["auto"| "internalOnly" | "preferExternal"]

    "internalOnly"应用只能安装在内存中,如果手机内存为空,将无法安装这个应用,应用永远无法安装到外部存储卡中,如果没有installLocation属性,则默认设置该值。

    "auto"这种时候应用可能会安装到外部存储中。不过默认情况下还是优先安装到手机内存中,一旦手机内存满了,系统会自动安装到存储卡中。这种应用即使在手机内存中安装了,也可以由用户手工转移到外部存储卡中。

    "preferExternal"这种应用优先安装到外部存储卡中,当存储卡不可用时,系统会自动安装到手机内存中。同样的,应用安装后,用户可以手工将应用在手机内存与外部存储卡中互相转移。

    <uses-permission>标签说明

    这个标签主要是为了声明应用中需要使用的权限,比如网络访问的权限,存储卡访问的权限等,一个应用中所有需要的权限声明信息可以在安装过程中看见(我怀疑大家从来不看吧,下次一定要看看,否则不知不觉中被扣款了就不好玩了)。

    假定应用未声明这些权限,但是使用过程中有相应操作,这时会怎么样?这种情况下,我会负责任地告诉他,操作过程中会失败。这时发现Permission denied错误,该知道怎么办了吧?

    API文档中定义语法如下:

    <uses-permission android:name="string" android:maxSdkVersion="integer" />

     

    < permission >标签说明

    这个是权限的定义标签,其中声明了<uses-permission>需要的权限信息。这个标签通常情况下用处不多,主要是为了为其他应用提供可以使用的代码或者数据信息。它还可以与<permission-group>以及<permission-tree>配合使用来构造更有层次的、更有针对性权限系统。

    使用举例:如果我们需要一个邮箱进行邮件操作,但是还有一个应用希望能够查询到部分邮件,但是又不能干扰邮箱的处理过程,就可以在邮箱中定义这个权限,然后在其它应用中申请这个权限。当然这种需求在地图应用中也是比较常见。

    API文档中定义语法如下:

    <permissionandroid:description="string resource"
       android:icon="drawable resource"
        android:label="stringresource"
        android:name="string"
       android:permissionGroup="string"
       android:protectionLevel=["normal" | "dangerous" |"signature" | "signatureOrSystem"] />

    关于权限定义更完善的处理,我们后续再进行讨论。

    <instrumentation >标签说明

    按照描述信息来说,这个标签是用来声明Instrumentation测试类来监控Android应用的行为并应用到相关的功能测试中。看起来是一个挺有意思的用法,平时对这个标签没有使用过,需要仔细地研究下这个标签的使用方法,下次对这个标签进行详细说明。
    展开全文
  • 资料搜集-JAVA系统梳理知识

    千次阅读 2020-01-06 08:09:07
    <!-- TOC --> - [Java](#java) - [基础](#基础) - [并发](#并发) - [JVM](#jvm) - [Java8 新特性](#java8-新特性) - [代码优化](#代码优化)...- [数据结构与算法](#数据结构与算法) - [数据库](#数据库)...
    <!-- TOC -->
    
    - [Java](#java)
        - [基础](#基础)
        - [并发](#并发)
        - [JVM](#jvm)
        - [Java8 新特性](#java8-新特性)
        - [代码优化](#代码优化)
    - [网络](#网络)
    - [操作系统](#操作系统)
    - [数据结构与算法](#数据结构与算法)
    - [数据库](#数据库)
    - [系统设计](#系统设计)
        - [设计模式](#设计模式)
        - [常用框架](#常用框架)
        - [网站架构](#网站架构)
        - [软件底层](#软件底层)
    - [其他](#其他)
    
    <!-- /TOC -->
    ## Java
    
    ### 基础
    
    - [《Head First Java》](https://book.douban.com/subject/2000732/)(推荐,豆瓣评分 8.7,1.0K+人评价):  可以说是我的 Java 启蒙书籍了,特别适合新手读当然也适合我们用来温故 Java 知识点。
    - [《Java 核心技术卷 1+卷 2》](https://book.douban.com/subject/25762168/)(推荐): 很棒的两本书,建议有点 Java 基础之后再读,介绍的还是比较深入的,非常推荐。这两本书我一般也会用来巩固知识点,是两本适合放在自己身边的好书。
    - [《JAVA 网络编程 第 4 版》](https://book.douban.com/subject/26259017/):  可以系统的学习一下网络的一些概念以及网络编程在 Java 中的使用。
    - [《Java 编程思想 (第 4 版)》](https://book.douban.com/subject/2130190/)(推荐,豆瓣评分 9.1,3.2K+人评价):大部分人称之为Java领域的圣经,但我不推荐初学者阅读,有点劝退的味道。稍微有点基础后阅读更好。
    - [《Java性能权威指南》](https://book.douban.com/subject/26740520/)(推荐,豆瓣评分 8.2,0.1K+人评价):O'Reilly 家族书,性能调优的入门书,我个人觉得性能调优是每个 Java 从业者必备知识,这本书的缺点就是太老了,但是这本书可以作为一个实战书,尤其是 JVM 调优!不适合初学者。前置书籍:《深入理解 Java 虚拟机》
    
    ### 并发
    
    - [《Java 并发编程之美》](<https://book.douban.com/subject/30351286/>) (推荐):2018 年 10 月出版的一本书,个人感觉非常不错,对每个知识点的讲解都很棒。
    - [《Java 并发编程的艺术》](https://book.douban.com/subject/26591326/)(推荐,豆瓣评分 7.2,0.2K+人评价): 这本书不是很适合作为 Java 并发入门书籍,需要具备一定的 JVM 基础。我感觉有些东西讲的还是挺深入的,推荐阅读。
    - [《实战 Java 高并发程序设计》](https://book.douban.com/subject/26663605/)(推荐,豆瓣评分 8.3): 书的质量没的说,推荐大家好好看一下。
    - [《Java 高并发编程详解》](https://book.douban.com/subject/30255689/)(豆瓣评分 7.6): 2018 年 6 月出版的一本书,内容很详细,但可能又有点过于啰嗦,不过这只是我的感觉。
    
    ### JVM
    
    -  [《深入理解 Java 虚拟机(第 2 版)周志明》](https://book.douban.com/subject/24722612/)(推荐,豆瓣评分 8.9,1.0K+人评价):建议多刷几遍,书中的所有知识点可以通过 JAVA 运行时区域和 JAVA 的内存模型与线程两个大模块罗列完全。 
    - [《实战 JAVA 虚拟机》](https://book.douban.com/subject/26354292/)(推荐,豆瓣评分 8.0,1.0K+人评价):作为入门的了解 Java 虚拟机的知识还是不错的。
    
    ### Java8 新特性
    
    - [《Java 8 实战》](https://book.douban.com/subject/26772632/) (推荐,豆瓣评分 9.2 ):面向 Java 8 的技能升级,包括 Lambdas、流和函数式编程特性。实战系列的一贯风格让自己快速上手应用起来。Java 8 支持的 Lambda 是精简表达在语法上提供的支持。Java 8 提供了 Stream,学习和使用可以建立流式编程的认知。
    - [《Java 8 编程参考官方教程》](https://book.douban.com/subject/26556574/) (推荐,豆瓣评分 9.2):也还不错吧。
    
    ### 代码优化
    
    -  [《重构_改善既有代码的设计》](https://book.douban.com/subject/4262627/)(推荐):豆瓣 9.1 分,重构书籍的开山鼻祖。
    -  [《Effective java 》](https://book.douban.com/subject/3360807/)(推荐,豆瓣评分 9.0,1.4K+人评价):本书介绍了在 Java 编程中 78 条极具实用价值的经验规则,这些经验规则涵盖了大多数开发人员每天所面临的问题的解决方案。通过对 Java 平台设计专家所使用的技术的全面描述,揭示了应该做什么,不应该做什么才能产生清晰、健壮和高效的代码。本书中的每条规则都以简短、独立的小文章形式出现,并通过例子代码加以进一步说明。本书内容全面,结构清晰,讲解详细。可作为技术人员的参考用书。
    -  [《代码整洁之道》](https://book.douban.com/subject/5442024/)(推荐,豆瓣评分 9.1):虽然是用 Java 语言作为例子,全篇都是在阐述 Java 面向对象的思想,但是其中大部分内容其它语言也能应用到。
    -  **阿里巴巴 Java 开发手册(详尽版)** [https://github.com/alibaba/p3c/blob/master/阿里巴巴 Java 开发手册(详尽版).pdf](https://github.com/alibaba/p3c/blob/master/%E9%98%BF%E9%87%8C%E5%B7%B4%E5%B7%B4Java%E5%BC%80%E5%8F%91%E6%89%8B%E5%86%8C%EF%BC%88%E8%AF%A6%E5%B0%BD%E7%89%88%EF%BC%89.pdf)
    -  **Google Java 编程风格指南:** <http://www.hawstein.com/posts/google-java-style.html>
    
    
    ## 网络
    
    - [《图解 HTTP》](https://book.douban.com/subject/25863515/)(推荐,豆瓣评分 8.1 , 1.6K+人评价): 讲漫画一样的讲 HTTP,很有意思,不会觉得枯燥,大概也涵盖也 HTTP 常见的知识点。因为篇幅问题,内容可能不太全面。不过,如果不是专门做网络方向研究的小伙伴想研究 HTTP 相关知识的话,读这本书的话应该来说就差不多了。
    - [《HTTP 权威指南》](https://book.douban.com/subject/10746113/) (推荐,豆瓣评分 8.6):如果要全面了解 HTTP 非此书不可!
    
    ## 操作系统
    
    - [《鸟哥的 Linux 私房菜》](https://book.douban.com/subject/4889838/)(推荐,,豆瓣评分 9.1,0.3K+人评价):本书是最具知名度的 Linux 入门书《鸟哥的 Linux 私房菜基础学习篇》的最新版,全面而详细地介绍了 Linux 操作系统。全书分为 5 个部分:第一部分着重说明 Linux 的起源及功能,如何规划和安装 Linux 主机;第二部分介绍 Linux 的文件系统、文件、目录与磁盘的管理;第三部分介绍文字模式接口 shell 和管理系统的好帮手 shell 脚本,另外还介绍了文字编辑器 vi 和 vim 的使用方法;第四部分介绍了对于系统安全非常重要的 Linux 账号的管理,以及主机系统与程序的管理,如查看进程、任务分配和作业管理;第五部分介绍了系统管理员 (root) 的管理事项,如了解系统运行状况、系统服务,针对登录文件进行解析,对系统进行备份以及核心的管理等。
    
    ## 数据结构与算法
    
    - [《大话数据结构》](https://book.douban.com/subject/6424904/)(推荐,豆瓣评分 7.9 , 1K+人评价):入门类型的书籍,读起来比较浅显易懂,适合没有数据结构基础或者说数据结构没学好的小伙伴用来入门数据结构。
    - [《数据结构与算法分析:C 语言描述》](https://book.douban.com/subject/1139426/)(推荐,豆瓣评分 8.9,1.6K+人评价):本书是《Data Structures and Algorithm Analysis in C》一书第 2 版的简体中译本。原书曾被评为 20 世纪顶尖的 30 部计算机著作之一,作者 Mark Allen Weiss 在数据结构和算法分析方面卓有建树,他的数据结构和算法分析的著作尤其畅销,并受到广泛好评.已被世界 500 余所大学用作教材。
    - [《算法图解》](https://book.douban.com/subject/26979890/)(推荐,豆瓣评分 8.4,0.6K+人评价):入门类型的书籍,读起来比较浅显易懂,适合没有算法基础或者说算法没学好的小伙伴用来入门。示例丰富,图文并茂,以让人容易理解的方式阐释了算法.读起来比较快,内容不枯燥!
    - [《算法 第四版》](https://book.douban.com/subject/10432347/)(推荐,豆瓣评分 9.3,0.4K+人评价):Java 语言描述,算法领域经典的参考书,全面介绍了关于算法和数据结构的必备知识,并特别针对排序、搜索、图处理和字符串处理进行了论述。书的内容非常多,可以说是 Java 程序员的必备书籍之一了。
    
    ## 数据库
    
    -  [《高性能 MySQL》](https://book.douban.com/subject/23008813/)(推荐,豆瓣评分 9.3,0.4K+人评价):mysql 领域的经典之作,拥有广泛的影响力。不但适合数据库管理员(dba)阅读,也适合开发人员参考学习。不管是数据库新手还是专家,相信都能从本书有所收获。
    -  [《Redis 实战》](https://book.douban.com/subject/26612779/):如果你想了解 Redis 的一些概念性知识的话,这本书真的非常不错。
    -  [《Redis 设计与实现》](https://book.douban.com/subject/25900156/)(推荐,豆瓣评分 8.5,0.5K+人评价):也还行吧!
    -  [《MySQL 技术内幕-InnoDB 存储引擎》](<https://book.douban.com/subject/24708143/>)(推荐,豆瓣评分 8.7):了解 InnoDB 存储引擎底层原理必备的一本书,比较深入。
    
    ## 系统设计
    
    ### 设计模式
    
    - [《设计模式 : 可复用面向对象软件的基础》](https://book.douban.com/subject/1052241/) (推荐,豆瓣评分 9.1):设计模式的经典!
    - [《Head First 设计模式(中文版)》](https://book.douban.com/subject/2243615/) (推荐,豆瓣评分 9.2):相当赞的一本设计模式入门书籍。用实际的编程案例讲解算法设计中会遇到的各种问题和需求变更(对的,连需求变更都考虑到了!),并以此逐步推导出良好的设计模式解决办法。
    - [《大话设计模式》](https://book.douban.com/subject/2334288/) (推荐,豆瓣评分 8.3):本书通篇都是以情景对话的形式,用多个小故事或编程示例来组织讲解GOF(即《设计模式 : 可复用面向对象软件的基础》这本书)),但是不像《设计模式 : 可复用面向对象软件的基础》难懂。但是设计模式只看书是不够的,还是需要在实际项目中运用,结合[设计模式](docs/system-design/设计模式.md)更佳!
    
    ### 常用框架
    
    - [《深入分析 Java Web 技术内幕》](https://book.douban.com/subject/25953851/):  感觉还行,涉及的东西也蛮多。
    - [《Netty 实战》](https://book.douban.com/subject/27038538/)(推荐,豆瓣评分 7.8,92 人评价):内容很细,如果想学 Netty 的话,推荐阅读这本书!
    - [《从 Paxos 到 Zookeeper》](https://book.douban.com/subject/26292004/)(推荐,豆瓣评分 7.8,0.3K 人评价):简要介绍几种典型的分布式一致性协议,以及解决分布式一致性问题的思路,其中重点讲解了 Paxos 和 ZAB 协议。同时,本书深入介绍了分布式一致性问题的工业解决方案——ZooKeeper,并着重向读者展示这一分布式协调框架的使用方法、内部实现及运维技巧,旨在帮助读者全面了解 ZooKeeper,并更好地使用和运维 ZooKeeper。
    - [《Spring 实战(第 4 版)》](https://book.douban.com/subject/26767354/)(推荐,豆瓣评分 8.3,0.3K+人评价):不建议当做入门书籍读,入门的话可以找点国人的书或者视频看。这本定位就相当于是关于 Spring 的新华字典,只有一些基本概念的介绍和示例,涵盖了 Spring 的各个方面,但都不够深入。就像作者在最后一页写的那样:“学习 Spring,这才刚刚开始”。
    - [《RabbitMQ 实战指南》](https://book.douban.com/subject/27591386/):《RabbitMQ 实战指南》从消息中间件的概念和 RabbitMQ 的历史切入,主要阐述 RabbitMQ 的安装、使用、配置、管理、运维、原理、扩展等方面的细节。如果你想浅尝 RabbitMQ 的使用,这本书是你最好的选择;如果你想深入 RabbitMQ 的原理,这本书也是你最好的选择;总之,如果你想玩转 RabbitMQ,这本书一定是最值得看的书之一
    - [《Spring Cloud 微服务实战》](https://book.douban.com/subject/27025912/):从时下流行的微服务架构概念出发,详细介绍了 Spring Cloud 针对微服务架构中几大核心要素的解决方案和基础组件。对于各个组件的介绍,《Spring Cloud 微服务实战》主要以示例与源码结合的方式来帮助读者更好地理解这些组件的使用方法以及运行原理。同时,在介绍的过程中,还包含了作者在实践中所遇到的一些问题和解决思路,可供读者在实践中作为参考。
    - [《第一本 Docker 书》](https://book.douban.com/subject/26780404/):Docker 入门书籍!
    - [《Spring Boot编程思想(核心篇)》](https://book.douban.com/subject/33390560/)(推荐,豆瓣评分 6.2):SpringBoot深入书,不适合初学者。书尤其的厚,评分低的的理由是书某些知识过于拖沓,评分高的理由是书中对SpringBoot内部原理讲解很清楚。作者小马哥:Apache Dubbo PMC、Spring Cloud Alibaba项目架构师。B站作者地址:https://space.bilibili.com/327910845?from=search&seid=17095917016893398636。
    
    ### 网站架构
    
    -  [《大型网站技术架构:核心原理与案例分析+李智慧》](https://book.douban.com/subject/25723064/)(推荐):这本书我读过,基本不需要你有什么基础啊~读起来特别轻松,但是却可以学到很多东西,非常推荐了。另外我写过这本书的思维导图,关注我的微信公众号:“Java 面试通关手册”回复“大型网站技术架构”即可领取思维导图。
    - [《亿级流量网站架构核心技术》](https://book.douban.com/subject/26999243/)(推荐):一书总结并梳理了亿级流量网站高可用和高并发原则,通过实例详细介绍了如何落地这些原则。本书分为四部分:概述、高可用原则、高并发原则、案例实战。从负载均衡、限流、降级、隔离、超时与重试、回滚机制、压测与预案、缓存、池化、异步化、扩容、队列等多方面详细介绍了亿级流量网站的架构核心技术,让读者看后能快速运用到实践项目中。
    
    ### 软件底层
    
    - [《深入剖析 Tomcat》](https://book.douban.com/subject/10426640/)(推荐,豆瓣评分 8.4,0.2K+人评价):本书深入剖析 Tomcat 4 和 Tomcat 5 中的每个组件,并揭示其内部工作原理。通过学习本书,你将可以自行开发 Tomcat 组件,或者扩展已有的组件。 读完这本书,基本可以摆脱背诵面试题的尴尬。
    - [《深入理解 Nginx(第 2 版)》](https://book.douban.com/subject/26745255/):作者讲的非常细致,注释都写的都很工整,对于 Nginx 的开发人员非常有帮助。优点是细致,缺点是过于细致,到处都是代码片段,缺少一些抽象。
    
    ## 其他
    
    - [《黑客与画家》](https://read.douban.com/ebook/387525/?dcs=subject-rec&dcm=douban&dct=2243615):这本书是硅谷创业之父,Y Combinator 创始人 Paul Graham 的文集。之所以叫这个名字,是因为作者认为黑客(并非负面的那个意思)与画家有着极大的相似性,他们都是在创造,而不是完成某个任务。
    - [《图解密码技术》](https://book.douban.com/subject/26265544/)(推荐,豆瓣评分 9.1,0.3K+人评价):本书以**图配文**的形式,第一部分讲述了密码技术的历史沿革、对称密码、分组密码模式(包括ECB、CBC、CFB、OFB、CTR)、公钥、混合密码系统。第二部分重点介绍了认证方面的内容,涉及单向散列函数、消息认证码、数字签名、证书等。第三部分讲述了密钥、随机数、PGP、SSL/TLS 以及密码技术在现实生活中的应用。关键字:JWT 前置知识、区块链密码技术前置知识。属于密码知识入门书籍。
    
    
    最近经常被读者问到有没有 Spring Boot 实战项目可以学习,于是,我就去 Github 上找了 10 个我觉得还不错的实战项目。对于这些实战项目,有部分是比较适合 Spring Boot 刚入门的朋友学习的,还有一部分可能要求你对 Spring Boot 相关技术比较熟悉。需要的朋友可以根据个人实际情况进行选择。如果你对 Spring Boot 不太熟悉的话,可以看我最近开源的 springboot-guide:https://github.com/Snailclimb/springboot-guide 入门(还在持续更新中)。
    
    ### mall
    
    - **Github地址**: [https://github.com/macrozheng/mall](https://github.com/macrozheng/mall)
    - **star**: 22.9k
    - **介绍**: mall项目是一套电商系统,包括前台商城系统及后台管理系统,基于SpringBoot+MyBatis实现。 前台商城系统包含首页门户、商品推荐、商品搜索、商品展示、购物车、订单流程、会员中心、客户服务、帮助中心等模块。 后台管理系统包含商品管理、订单管理、会员管理、促销管理、运营管理、内容管理、统计报表、财务管理、权限管理、设置等模块。
    
    ### jeecg-boot
    
    - **Github地址**:[https://github.com/zhangdaiscott/jeecg-boot](https://github.com/zhangdaiscott/jeecg-boot)
    - **star**: 6.4k
    - **介绍**: 一款基于代码生成器的JAVA快速开发平台!采用最新技术,前后端分离架构:SpringBoot 2.x,Ant Design&Vue,Mybatis,Shiro,JWT。强大的代码生成器让前后端代码一键生成,无需写任何代码,绝对是全栈开发福音!! JeecgBoot的宗旨是提高UI能力的同时,降低前后分离的开发成本,JeecgBoot还独创在线开发模式,No代码概念,一系列在线智能开发:在线配置表单、在线配置报表、在线设计流程等等。 
    
    ### eladmin
    
    - **Github地址**:[https://github.com/elunez/eladmin](https://github.com/elunez/eladmin)
    - **star**: 3.9k
    - **介绍**: 项目基于 Spring Boot 2.1.0 、 Jpa、 Spring Security、redis、Vue的前后端分离的后台管理系统,项目采用分模块开发方式, 权限控制采用 RBAC,支持数据字典与数据权限管理,支持一键生成前后端代码,支持动态路由。
    
    ### paascloud-master
    
    - **Github地址**:[https://github.com/paascloud/paascloud-master](https://github.com/paascloud/paascloud-master)
    - **star**: 5.9k
    - **介绍**:  spring cloud + vue + oAuth2.0全家桶实战,前后端分离模拟商城,完整的购物流程、后端运营平台,可以实现快速搭建企业级微服务项目。支持微信登录等三方登录。
    
    ### vhr
    
    - **Github地址**:[https://github.com/lenve/vhr](https://github.com/lenve/vhr)
    - **star**: 10.6k
    - **介绍**:  微人事是一个前后端分离的人力资源管理系统,项目采用SpringBoot+Vue开发。
    
    ### One mall
    
    - **Github地址**:[https://github.com/YunaiV/onemall](https://github.com/YunaiV/onemall)
    - **star**: 1.2k
    - **介绍**:  mall 商城,基于微服务的思想,构建在 B2C 电商场景下的项目实战。核心技术栈,是 Spring Boot + Dubbo 。未来,会重构成 Spring Cloud Alibaba 。
    
    ### Guns
    
    - **Github地址**:[https://github.com/stylefeng/Guns](https://github.com/stylefeng/Guns)
    - **star**: 2.3k
    - **介绍**:  Guns基于SpringBoot 2,致力于做更简洁的后台管理系统,完美整合springmvc + shiro + mybatis-plus + beetl!Guns项目代码简洁,注释丰富,上手容易,同时Guns包含许多基础模块(用户管理,角色管理,部门管理,字典管理等10个模块),可以直接作为一个后台管理系统的脚手架!
    
    ### SpringCloud
    
    - **Github地址**:[https://github.com/YunaiV/onemall](https://github.com/YunaiV/onemall)
    - **star**: 1.2k
    - **介绍**:  mall 商城,基于微服务的思想,构建在 B2C 电商场景下的项目实战。核心技术栈,是 Spring Boot + Dubbo 。未来,会重构成 Spring Cloud Alibaba 。
    
    ### SpringBoot-Shiro-Vue
    
    - **Github地址**:[https://github.com/Heeexy/SpringBoot-Shiro-Vue](https://github.com/Heeexy/SpringBoot-Shiro-Vue)
    - **star**: 1.8k
    - **介绍**: 提供一套基于Spring Boot-Shiro-Vue的权限管理思路.前后端都加以控制,做到按钮/接口级别的权限。
    
    ### newbee-mall
    
    最近开源的一个商城项目。
    
    - **Github地址**:[https://github.com/newbee-ltd/newbee-mall](https://github.com/newbee-ltd/newbee-mall)
    - **star**: 50
    - **介绍**: newbee-mall 项目是一套电商系统,包括 newbee-mall 商城系统及 newbee-mall-admin 商城后台管理系统,基于 Spring Boot 2.X 及相关技术栈开发。 前台商城系统包含首页门户、商品分类、新品上线、首页轮播、商品推荐、商品搜索、商品展示、购物车、订单结算、订单流程、个人订单管理、会员中心、帮助中心等模块。 后台管理系统包含数据面板、轮播图管理、商品管理、订单管理、会员管理、分类管理、设置等模块。
    
    
    点击关注[公众号](#公众号)及时获取笔主最新更新文章,并可免费领取本文档配套的《Java面试突击》以及Java工程师必备学习资源。
    
    - [书籍推荐](#书籍推荐)
    - [文字教程推荐](#文字教程推荐)
    - [视频教程推荐](#视频教程推荐)
    - [常见问题总结](#常见问题总结)
        - [什么是MySQL?](#什么是mysql)
        - [存储引擎](#存储引擎)
            - [一些常用命令](#一些常用命令)
            - [MyISAM和InnoDB区别](#myisam和innodb区别)
        - [字符集及校对规则](#字符集及校对规则)
        - [索引](#索引)
        - [查询缓存的使用](#查询缓存的使用)
        - [什么是事务?](#什么是事务)
        - [事物的四大特性(ACID)](#事物的四大特性acid)
        - [并发事务带来哪些问题?](#并发事务带来哪些问题)
        - [事务隔离级别有哪些?MySQL的默认隔离级别是?](#事务隔离级别有哪些mysql的默认隔离级别是)
        - [锁机制与InnoDB锁算法](#锁机制与innodb锁算法)
        - [大表优化](#大表优化)
            - [1. 限定数据的范围](#1-限定数据的范围)
            - [2. 读/写分离](#2-读写分离)
            - [3. 垂直分区](#3-垂直分区)
            - [4. 水平分区](#4-水平分区)
        - [一条SQL语句在MySQL中如何执行的](#一条sql语句在mysql中如何执行的)
        - [MySQL高性能优化规范建议](#mysql高性能优化规范建议)
        - [一条SQL语句执行得很慢的原因有哪些?](#一条sql语句执行得很慢的原因有哪些)
    
    <!-- /TOC -->
    
    ## 书籍推荐
    
    - 《SQL基础教程(第2版)》 (入门级)
    - 《高性能MySQL : 第3版》 (进阶)
    
    ## 文字教程推荐
    
    - [SQL Tutorial](https://www.w3schools.com/sql/default.asp) (SQL语句学习,英文)、[SQL Tutorial](https://www.w3school.com.cn/sql/index.asp)(SQL语句学习,中文)、[SQL语句在线练习](https://www.w3schools.com/sql/exercise.asp) (非常不错)
    - [Github-MySQL入门教程(MySQL tutorial book)](https://github.com/jaywcjlove/mysql-tutorial) (从零开始学习MySQL,主要是面向MySQL数据库管理系统初学者)
    - [官方教程](https://dev.mysql.com/doc/refman/5.7/)
    - [MySQL 教程(菜鸟教程)](http://www.runoob.com/MySQL/MySQL-tutorial.html)
    
    ## 相关资源推荐
    
    - [中国5级行政区域mysql库](https://github.com/kakuilan/china_area_mysql)
    
    ## 视频教程推荐
    
    **基础入门:** [与MySQL的零距离接触-慕课网](https://www.imooc.com/learn/122)
    
    **MySQL开发技巧:** [MySQL开发技巧(一)](https://www.imooc.com/learn/398)  [MySQL开发技巧(二)](https://www.imooc.com/learn/427)  [MySQL开发技巧(三)](https://www.imooc.com/learn/449)
    
    **MySQL5.7新特性及相关优化技巧:** [MySQL5.7版本新特性](https://www.imooc.com/learn/533)  [性能优化之MySQL优化](https://www.imooc.com/learn/194)
    
    [MySQL集群(PXC)入门](https://www.imooc.com/learn/993)  [MyCAT入门及应用](https://www.imooc.com/learn/951)
    
    ## 常见问题总结
    
    ### 什么是MySQL?
    
    MySQL 是一种关系型数据库,在Java企业级开发中非常常用,因为 MySQL 是开源免费的,并且方便扩展。阿里巴巴数据库系统也大量用到了 MySQL,因此它的稳定性是有保障的。MySQL是开放源代码的,因此任何人都可以在 GPL(General Public License) 的许可下下载并根据个性化的需要对其进行修改。MySQL的默认端口号是**3306**。
    
    ### 存储引擎
    
    #### 一些常用命令
    
    **查看MySQL提供的所有存储引擎**
    
    ```sql
    mysql> show engines;
    ```
    
    ![查看MySQL提供的所有存储引擎](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/mysql-engines.png)
    
    从上图我们可以查看出 MySQL 当前默认的存储引擎是InnoDB,并且在5.7版本所有的存储引擎中只有 InnoDB 是事务性存储引擎,也就是说只有 InnoDB 支持事务。
    
    **查看MySQL当前默认的存储引擎**
    
    我们也可以通过下面的命令查看默认的存储引擎。
    
    ```sql
    mysql> show variables like '%storage_engine%';
    ```
    
    **查看表的存储引擎**
    
    ```sql
    show table status like "table_name" ;
    ```
    
    ![查看表的存储引擎](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/查看表的存储引擎.png)
    
    #### MyISAM和InnoDB区别
    
    MyISAM是MySQL的默认数据库引擎(5.5版之前)。虽然性能极佳,而且提供了大量的特性,包括全文索引、压缩、空间函数等,但MyISAM不支持事务和行级锁,而且最大的缺陷就是崩溃后无法安全恢复。不过,5.5版本之后,MySQL引入了InnoDB(事务性数据库引擎),MySQL 5.5版本后默认的存储引擎为InnoDB。
    
    大多数时候我们使用的都是 InnoDB 存储引擎,但是在某些情况下使用 MyISAM 也是合适的比如读密集的情况下。(如果你不介意 MyISAM 崩溃恢复问题的话)。
    
    **两者的对比:**
    
    1. **是否支持行级锁** : MyISAM 只有表级锁(table-level locking),而InnoDB 支持行级锁(row-level locking)和表级锁,默认为行级锁。
    2. **是否支持事务和崩溃后的安全恢复: MyISAM** 强调的是性能,每次查询具有原子性,其执行速度比InnoDB类型更快,但是不提供事务支持。但是**InnoDB** 提供事务支持事务,外部键等高级数据库功能。 具有事务(commit)、回滚(rollback)和崩溃修复能力(crash recovery capabilities)的事务安全(transaction-safe (ACID compliant))型表。
    3. **是否支持外键:** MyISAM不支持,而InnoDB支持。
    4. **是否支持MVCC** :仅 InnoDB 支持。应对高并发事务, MVCC比单纯的加锁更高效;MVCC只在 `READ COMMITTED` 和 `REPEATABLE READ` 两个隔离级别下工作;MVCC可以使用 乐观(optimistic)锁 和 悲观(pessimistic)锁来实现;各数据库中MVCC实现并不统一。推荐阅读:[MySQL-InnoDB-MVCC多版本并发控制](https://segmentfault.com/a/1190000012650596)
    5. ......
    
    《MySQL高性能》上面有一句话这样写到:
    
    > 不要轻易相信“MyISAM比InnoDB快”之类的经验之谈,这个结论往往不是绝对的。在很多我们已知场景中,InnoDB的速度都可以让MyISAM望尘莫及,尤其是用到了聚簇索引,或者需要访问的数据都可以放入内存的应用。
    
    一般情况下我们选择 InnoDB 都是没有问题的,但是某些情况下你并不在乎可扩展能力和并发能力,也不需要事务支持,也不在乎崩溃后的安全恢复问题的话,选择MyISAM也是一个不错的选择。但是一般情况下,我们都是需要考虑到这些问题的。
    
    ### 字符集及校对规则
    
    字符集指的是一种从二进制编码到某类字符符号的映射。校对规则则是指某种字符集下的排序规则。MySQL中每一种字符集都会对应一系列的校对规则。
    
    MySQL采用的是类似继承的方式指定字符集的默认值,每个数据库以及每张数据表都有自己的默认值,他们逐层继承。比如:某个库中所有表的默认字符集将是该数据库所指定的字符集(这些表在没有指定字符集的情况下,才会采用默认字符集) PS:整理自《Java工程师修炼之道》
    
    详细内容可以参考:   [MySQL字符集及校对规则的理解](https://www.cnblogs.com/geaozhang/p/6724393.html#MySQLyuzifuji)
    
    ### 索引
    
    MySQL索引使用的数据结构主要有**BTree索引** 和 **哈希索引** 。对于哈希索引来说,底层的数据结构就是哈希表,因此在绝大多数需求为单条记录查询的时候,可以选择哈希索引,查询性能最快;其余大部分场景,建议选择BTree索引。
    
    MySQL的BTree索引使用的是B树中的B+Tree,但对于主要的两种存储引擎的实现方式是不同的。
    
    - **MyISAM:** B+Tree叶节点的data域存放的是数据记录的地址。在索引检索的时候,首先按照B+Tree搜索算法搜索索引,如果指定的Key存在,则取出其 data 域的值,然后以 data 域的值为地址读取相应的数据记录。这被称为“非聚簇索引”。
    - **InnoDB:** 其数据文件本身就是索引文件。相比MyISAM,索引文件和数据文件是分离的,其表数据文件本身就是按B+Tree组织的一个索引结构,树的叶节点data域保存了完整的数据记录。这个索引的key是数据表的主键,因此InnoDB表数据文件本身就是主索引。这被称为“聚簇索引(或聚集索引)”。而其余的索引都作为辅助索引,辅助索引的data域存储相应记录主键的值而不是地址,这也是和MyISAM不同的地方。**在根据主索引搜索时,直接找到key所在的节点即可取出数据;在根据辅助索引查找时,则需要先取出主键的值,再走一遍主索引。** **因此,在设计表的时候,不建议使用过长的字段作为主键,也不建议使用非单调的字段作为主键,这样会造成主索引频繁分裂。** PS:整理自《Java工程师修炼之道》
    
    **更多关于索引的内容可以查看文档首页MySQL目录下关于索引的详细总结。**
    
    ### 查询缓存的使用
    
    > 执行查询语句的时候,会先查询缓存。不过,MySQL 8.0 版本后移除,因为这个功能不太实用
    
    my.cnf加入以下配置,重启MySQL开启查询缓存
    ```properties
    query_cache_type=1
    query_cache_size=600000
    ```
    
    MySQL执行以下命令也可以开启查询缓存
    
    ```properties
    set global  query_cache_type=1;
    set global  query_cache_size=600000;
    ```
    如上,**开启查询缓存后在同样的查询条件以及数据情况下,会直接在缓存中返回结果**。这里的查询条件包括查询本身、当前要查询的数据库、客户端协议版本号等一些可能影响结果的信息。因此任何两个查询在任何字符上的不同都会导致缓存不命中。此外,如果查询中包含任何用户自定义函数、存储函数、用户变量、临时表、MySQL库中的系统表,其查询结果也不会被缓存。
    
    缓存建立之后,MySQL的查询缓存系统会跟踪查询中涉及的每张表,如果这些表(数据或结构)发生变化,那么和这张表相关的所有缓存数据都将失效。
    
    **缓存虽然能够提升数据库的查询性能,但是缓存同时也带来了额外的开销,每次查询后都要做一次缓存操作,失效后还要销毁。** 因此,开启缓存查询要谨慎,尤其对于写密集的应用来说更是如此。如果开启,要注意合理控制缓存空间大小,一般来说其大小设置为几十MB比较合适。此外,**还可以通过sql_cache和sql_no_cache来控制某个查询语句是否需要缓存:**
    ```sql
    select sql_no_cache count(*) from usr;
    ```
    
    ### 什么是事务?
    
    **事务是逻辑上的一组操作,要么都执行,要么都不执行。**
    
    事务最经典也经常被拿出来说例子就是转账了。假如小明要给小红转账1000元,这个转账会涉及到两个关键操作就是:将小明的余额减少1000元,将小红的余额增加1000元。万一在这两个操作之间突然出现错误比如银行系统崩溃,导致小明余额减少而小红的余额没有增加,这样就不对了。事务就是保证这两个关键操作要么都成功,要么都要失败。
    
    ### 事物的四大特性(ACID)
    
    ![事物的特性](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/事务特性.png)
    
    1. **原子性(Atomicity):** 事务是最小的执行单位,不允许分割。事务的原子性确保动作要么全部完成,要么完全不起作用;
    2. **一致性(Consistency):** 执行事务前后,数据保持一致,多个事务对同一个数据读取的结果是相同的;
    3. **隔离性(Isolation):** 并发访问数据库时,一个用户的事务不被其他事务所干扰,各并发事务之间数据库是独立的;
    4. **持久性(Durability):** 一个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库发生故障也不应该对其有任何影响。
    
    ### 并发事务带来哪些问题?
    
    在典型的应用程序中,多个事务并发运行,经常会操作相同的数据来完成各自的任务(多个用户对同一数据进行操作)。并发虽然是必须的,但可能会导致以下的问题。
    
    - **脏读(Dirty read):** 当一个事务正在访问数据并且对数据进行了修改,而这种修改还没有提交到数据库中,这时另外一个事务也访问了这个数据,然后使用了这个数据。因为这个数据是还没有提交的数据,那么另外一个事务读到的这个数据是“脏数据”,依据“脏数据”所做的操作可能是不正确的。
    - **丢失修改(Lost to modify):** 指在一个事务读取一个数据时,另外一个事务也访问了该数据,那么在第一个事务中修改了这个数据后,第二个事务也修改了这个数据。这样第一个事务内的修改结果就被丢失,因此称为丢失修改。    例如:事务1读取某表中的数据A=20,事务2也读取A=20,事务1修改A=A-1,事务2也修改A=A-1,最终结果A=19,事务1的修改被丢失。
    - **不可重复读(Unrepeatableread):** 指在一个事务内多次读同一数据。在这个事务还没有结束时,另一个事务也访问该数据。那么,在第一个事务中的两次读数据之间,由于第二个事务的修改导致第一个事务两次读取的数据可能不太一样。这就发生了在一个事务内两次读到的数据是不一样的情况,因此称为不可重复读。
    - **幻读(Phantom read):** 幻读与不可重复读类似。它发生在一个事务(T1)读取了几行数据,接着另一个并发事务(T2)插入了一些数据时。在随后的查询中,第一个事务(T1)就会发现多了一些原本不存在的记录,就好像发生了幻觉一样,所以称为幻读。
    
    **不可重复读和幻读区别:**
    
    不可重复读的重点是修改比如多次读取一条记录发现其中某些列的值被修改,幻读的重点在于新增或者删除比如多次读取一条记录发现记录增多或减少了。
    
    ### 事务隔离级别有哪些?MySQL的默认隔离级别是?
    
    **SQL 标准定义了四个隔离级别:**
    
    - **READ-UNCOMMITTED(读取未提交):** 最低的隔离级别,允许读取尚未提交的数据变更,**可能会导致脏读、幻读或不可重复读**。
    - **READ-COMMITTED(读取已提交):** 允许读取并发事务已经提交的数据,**可以阻止脏读,但是幻读或不可重复读仍有可能发生**。
    - **REPEATABLE-READ(可重复读):**  对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,**可以阻止脏读和不可重复读,但幻读仍有可能发生**。
    - **SERIALIZABLE(可串行化):** 最高的隔离级别,完全服从ACID的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,**该级别可以防止脏读、不可重复读以及幻读**。
    
    ------
    
    |     隔离级别     | 脏读 | 不可重复读 | 幻影读 |
    | :--------------: | :--: | :--------: | :----: |
    | READ-UNCOMMITTED |  √   |     √      |   √    |
    |  READ-COMMITTED  |  ×   |     √      |   √    |
    | REPEATABLE-READ  |  ×   |     ×      |   √    |
    |   SERIALIZABLE   |  ×   |     ×      |   ×    |
    
    MySQL InnoDB 存储引擎的默认支持的隔离级别是 **REPEATABLE-READ(可重读)**。我们可以通过`SELECT @@tx_isolation;`命令来查看
    
    ```sql
    mysql> SELECT @@tx_isolation;
    +-----------------+
    | @@tx_isolation  |
    +-----------------+
    | REPEATABLE-READ |
    +-----------------+
    ```
    
    这里需要注意的是:与 SQL 标准不同的地方在于 InnoDB 存储引擎在 **REPEATABLE-READ(可重读)** 
    事务隔离级别下使用的是Next-Key Lock 锁算法,因此可以避免幻读的产生,这与其他数据库系统(如 SQL Server)
    是不同的。所以说InnoDB 存储引擎的默认支持的隔离级别是 **REPEATABLE-READ(可重读)** 已经可以完全保证事务的隔离性要求,即达到了
     SQL标准的 **SERIALIZABLE(可串行化)** 隔离级别。因为隔离级别越低,事务请求的锁越少,所以大部分数据库系统的隔离级别都是 **READ-COMMITTED(读取提交内容)** ,但是你要知道的是InnoDB 存储引擎默认使用 **REPEAaTABLE-READ(可重读)** 并不会有任何性能损失。
    
    InnoDB 存储引擎在 **分布式事务** 的情况下一般会用到 **SERIALIZABLE(可串行化)** 隔离级别。
    
    ### 锁机制与InnoDB锁算法
    
    **MyISAM和InnoDB存储引擎使用的锁:**
    
    - MyISAM采用表级锁(table-level locking)。
    - InnoDB支持行级锁(row-level locking)和表级锁,默认为行级锁
    
    **表级锁和行级锁对比:**
    
    - **表级锁:** MySQL中锁定 **粒度最大** 的一种锁,对当前操作的整张表加锁,实现简单,资源消耗也比较少,加锁快,不会出现死锁。其锁定粒度最大,触发锁冲突的概率最高,并发度最低,MyISAM和 InnoDB引擎都支持表级锁。
    - **行级锁:** MySQL中锁定 **粒度最小** 的一种锁,只针对当前操作的行进行加锁。 行级锁能大大减少数据库操作的冲突。其加锁粒度最小,并发度高,但加锁的开销也最大,加锁慢,会出现死锁。 
    
    详细内容可以参考: MySQL锁机制简单了解一下:[https://blog.csdn.net/qq_34337272/article/details/80611486](https://blog.csdn.net/qq_34337272/article/details/80611486)
    
    **InnoDB存储引擎的锁的算法有三种:**
    
    - Record lock:单个行记录上的锁
    - Gap lock:间隙锁,锁定一个范围,不包括记录本身
    - Next-key lock:record+gap 锁定一个范围,包含记录本身
    
    **相关知识点:**
    
    1. innodb对于行的查询使用next-key lock
    2. Next-locking keying为了解决Phantom Problem幻读问题
    3. 当查询的索引含有唯一属性时,将next-key lock降级为record key
    4. Gap锁设计的目的是为了阻止多个事务将记录插入到同一范围内,而这会导致幻读问题的产生
    5. 有两种方式显式关闭gap锁:(除了外键约束和唯一性检查外,其余情况仅使用record lock) A. 将事务隔离级别设置为RC B. 将参数innodb_locks_unsafe_for_binlog设置为1
    
    ### 大表优化
    
    当MySQL单表记录数过大时,数据库的CRUD性能会明显下降,一些常见的优化措施如下:
    
    #### 1. 限定数据的范围
    
    务必禁止不带任何限制数据范围条件的查询语句。比如:我们当用户在查询订单历史的时候,我们可以控制在一个月的范围内;
    
    #### 2. 读/写分离
    
    经典的数据库拆分方案,主库负责写,从库负责读;
    
    #### 3. 垂直分区
    
     **根据数据库里面数据表的相关性进行拆分。** 例如,用户表中既有用户的登录信息又有用户的基本信息,可以将用户表拆分成两个单独的表,甚至放到单独的库做分库。
    
     **简单来说垂直拆分是指数据表列的拆分,把一张列比较多的表拆分为多张表。** 如下图所示,这样来说大家应该就更容易理解了。
     ![数据库垂直分区](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/数据库垂直分区.png)
    
    - **垂直拆分的优点:** 可以使得列数据变小,在查询时减少读取的Block数,减少I/O次数。此外,垂直分区可以简化表的结构,易于维护。
    - **垂直拆分的缺点:** 主键会出现冗余,需要管理冗余列,并会引起Join操作,可以通过在应用层进行Join来解决。此外,垂直分区会让事务变得更加复杂;
    
    #### 4. 水平分区
    
    **保持数据表结构不变,通过某种策略存储数据分片。这样每一片数据分散到不同的表或者库中,达到了分布式的目的。 水平拆分可以支撑非常大的数据量。** 
    
     水平拆分是指数据表行的拆分,表的行数超过200万行时,就会变慢,这时可以把一张的表的数据拆成多张表来存放。举个例子:我们可以将用户信息表拆分成多个用户信息表,这样就可以避免单一表数据量过大对性能造成影响。
    
    ![数据库水平拆分](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/数据库水平拆分.png)
    
    水平拆分可以支持非常大的数据量。需要注意的一点是:分表仅仅是解决了单一表数据过大的问题,但由于表的数据还是在同一台机器上,其实对于提升MySQL并发能力没有什么意义,所以 **水平拆分最好分库** 。
    
    水平拆分能够 **支持非常大的数据量存储,应用端改造也少**,但 **分片事务难以解决**  ,跨节点Join性能较差,逻辑复杂。《Java工程师修炼之道》的作者推荐 **尽量不要对数据进行分片,因为拆分会带来逻辑、部署、运维的各种复杂度** ,一般的数据表在优化得当的情况下支撑千万以下的数据量是没有太大问题的。如果实在要分片,尽量选择客户端分片架构,这样可以减少一次和中间件的网络I/O。
    
    **下面补充一下数据库分片的两种常见方案:**
    
    - **客户端代理:**  **分片逻辑在应用端,封装在jar包中,通过修改或者封装JDBC层来实现。** 当当网的 **Sharding-JDBC** 、阿里的TDDL是两种比较常用的实现。
    - **中间件代理:** **在应用和数据中间加了一个代理层。分片逻辑统一维护在中间件服务中。** 我们现在谈的 **Mycat** 、360的Atlas、网易的DDB等等都是这种架构的实现。
    
    详细内容可以参考: MySQL大表优化方案: [https://segmentfault.com/a/1190000006158186](https://segmentfault.com/a/1190000006158186)
    
    ### 解释一下什么是池化设计思想。什么是数据库连接池?为什么需要数据库连接池?
    
    池话设计应该不是一个新名词。我们常见的如java线程池、jdbc连接池、redis连接池等就是这类设计的代表实现。这种设计会初始预设资源,解决的问题就是抵消每次获取资源的消耗,如创建线程的开销,获取远程连接的开销等。就好比你去食堂打饭,打饭的大妈会先把饭盛好几份放那里,你来了就直接拿着饭盒加菜即可,不用再临时又盛饭又打菜,效率就高了。除了初始化资源,池化设计还包括如下这些特征:池子的初始值、池子的活跃值、池子的最大值等,这些特征可以直接映射到java线程池和数据库连接池的成员属性中。——这篇文章对[池化设计思想](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247485679&idx=1&sn=57dbca8c9ad49e1f3968ecff04a4f735&chksm=cea24724f9d5ce3212292fac291234a760c99c0960b5430d714269efe33554730b5f71208582&token=1141994790&lang=zh_CN#rd)介绍的还不错,直接复制过来,避免重复造轮子了。
    
    数据库连接本质就是一个 socket 的连接。数据库服务端还要维护一些缓存和用户权限信息之类的 所以占用了一些内存。我们可以把数据库连接池是看做是维护的数据库连接的缓存,以便将来需要对数据库的请求时可以重用这些连接。为每个用户打开和维护数据库连接,尤其是对动态数据库驱动的网站应用程序的请求,既昂贵又浪费资源。**在连接池中,创建连接后,将其放置在池中,并再次使用它,因此不必建立新的连接。如果使用了所有连接,则会建立一个新连接并将其添加到池中。**连接池还减少了用户必须等待建立与数据库的连接的时间。
    
    ### 分库分表之后,id 主键如何处理?
    
    因为要是分成多个表之后,每个表都是从 1 开始累加,这样是不对的,我们需要一个全局唯一的 id 来支持。
    
    生成全局 id 有下面这几种方式:
    
    - **UUID**:不适合作为主键,因为太长了,并且无序不可读,查询效率低。比较适合用于生成唯一的名字的标示比如文件的名字。
    - **数据库自增 id** : 两台数据库分别设置不同步长,生成不重复ID的策略来实现高可用。这种方式生成的 id 有序,但是需要独立部署数据库实例,成本高,还会有性能瓶颈。
    - **利用 redis 生成 id :** 性能比较好,灵活方便,不依赖于数据库。但是,引入了新的组件造成系统更加复杂,可用性降低,编码更加复杂,增加了系统成本。
    - **Twitter的snowflake算法** :Github 地址:https://github.com/twitter-archive/snowflake。
    - **美团的[Leaf](https://tech.meituan.com/2017/04/21/mt-leaf.html)分布式ID生成系统** :Leaf 是美团开源的分布式ID生成器,能保证全局唯一性、趋势递增、单调递增、信息安全,里面也提到了几种分布式方案的对比,但也需要依赖关系数据库、Zookeeper等中间件。感觉还不错。美团技术团队的一篇文章:https://tech.meituan.com/2017/04/21/mt-leaf.html 。
    - ......
    
    ### 一条SQL语句在MySQL中如何执行的
    
    [一条SQL语句在MySQL中如何执行的](<https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247485097&idx=1&sn=84c89da477b1338bdf3e9fcd65514ac1&chksm=cea24962f9d5c074d8d3ff1ab04ee8f0d6486e3d015cfd783503685986485c11738ccb542ba7&token=79317275&lang=zh_CN#rd>)
    
    ### MySQL高性能优化规范建议
    
    [MySQL高性能优化规范建议](<https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247485117&idx=1&sn=92361755b7c3de488b415ec4c5f46d73&chksm=cea24976f9d5c060babe50c3747616cce63df5d50947903a262704988143c2eeb4069ae45420&token=79317275&lang=zh_CN#rd>)
    
    ### 一条SQL语句执行得很慢的原因有哪些?
    
    [腾讯面试:一条SQL语句执行得很慢的原因有哪些?---不看后悔系列](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247485185&idx=1&sn=66ef08b4ab6af5757792223a83fc0d45&chksm=cea248caf9d5c1dc72ec8a281ec16aa3ec3e8066dbb252e27362438a26c33fbe842b0e0adf47&token=79317275&lang=zh_CN#rd)
    
    ## 公众号
    
    如果大家想要实时关注我更新的文章以及分享的干货的话,可以关注我的公众号。
    
    **《Java面试突击》:** 由本文档衍生的专为面试而生的《Java面试突击》V2.0 PDF 版本[公众号](#公众号)后台回复 **"Java面试突击"** 即可免费领取!
    
    **Java工程师必备学习资源:** 一些Java工程师常用学习资源公众号后台回复关键字 **“1”** 即可免费无套路获取。 
    
    ![我的公众号](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/167598cd2e17b8ec.png)
    
    # 思维导图-索引篇
    
    > 系列思维导图源文件(数据库+架构)以及思维导图制作软件—XMind8 破解安装,公众号后台回复:**“思维导图”** 免费领取!(下面的图片不是很清楚,原图非常清晰,另外提供给大家源文件也是为了大家根据自己需要进行修改)
    
    ![【思维导图-索引篇】](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-10-2/70973487.jpg)
    
    > **下面是我补充的一些内容**
    
    # 为什么索引能提高查询速度
    
    > 以下内容整理自:
    >  地址: https://juejin.im/post/5b55b842f265da0f9e589e79
    >  作者 :Java3y
    
    ### 先从 MySQL 的基本存储结构说起
    
    MySQL的基本存储结构是页(记录都存在页里边):
    
    ![MySQL的基本存储结构是页](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-10-2/28559421.jpg)
    
    ![](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-10-2/82053134.jpg)
    
     - **各个数据页可以组成一个双向链表**
     -   **每个数据页中的记录又可以组成一个单向链表**
           - 每个数据页都会为存储在它里边儿的记录生成一个页目录,在通过主键查找某条记录的时候可以在页目录中使用二分法快速定位到对应的槽,然后再遍历该槽对应分组中的记录即可快速找到指定的记录
           - 以其他列(非主键)作为搜索条件:只能从最小记录开始依次遍历单链表中的每条记录。
    
    所以说,如果我们写select * from user where indexname = 'xxx'这样没有进行任何优化的sql语句,默认会这样做:
    
    1. **定位到记录所在的页:需要遍历双向链表,找到所在的页**
    2. **从所在的页内中查找相应的记录:由于不是根据主键查询,只能遍历所在页的单链表了**
    
    很明显,在数据量很大的情况下这样查找会很慢!这样的时间复杂度为O(n)。
    
    
    ### 使用索引之后
    
    索引做了些什么可以让我们查询加快速度呢?其实就是将无序的数据变成有序(相对):
    
    ![](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-10-2/5373082.jpg)
    
    要找到id为8的记录简要步骤:
    
    ![](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-10-2/89338047.jpg)
    
    很明显的是:没有用索引我们是需要遍历双向链表来定位对应的页,现在通过 **“目录”** 就可以很快地定位到对应的页上了!(二分查找,时间复杂度近似为O(logn))
    
    其实底层结构就是B+树,B+树作为树的一种实现,能够让我们很快地查找出对应的记录。
    
    # 关于索引其他重要的内容补充
    
    > 以下内容整理自:《Java工程师修炼之道》
    
    
    ### 最左前缀原则
    
    MySQL中的索引可以以一定顺序引用多列,这种索引叫作联合索引。如User表的name和city加联合索引就是(name,city),而最左前缀原则指的是,如果查询的时候查询条件精确匹配索引的左边连续一列或几列,则此列就可以被用到。如下:        
    
    ```                                                                                       
    select * from user where name=xx and city=xx ; //可以命中索引
    select * from user where name=xx ; // 可以命中索引
    select * from user where city=xx ; // 无法命中索引            
    ```                                                          
    这里需要注意的是,查询的时候如果两个条件都用上了,但是顺序不同,如 `city= xx and name =xx`,那么现在的查询引擎会自动优化为匹配联合索引的顺序,这样是能够命中索引的。
    
    由于最左前缀原则,在创建联合索引时,索引字段的顺序需要考虑字段值去重之后的个数,较多的放前面。ORDER BY子句也遵循此规则。
    
    ### 注意避免冗余索引
    
    冗余索引指的是索引的功能相同,能够命中 就肯定能命中 ,那么 就是冗余索引如(name,city )和(name )这两个索引就是冗余索引,能够命中后者的查询肯定是能够命中前者的 在大多数情况下,都应该尽量扩展已有的索引而不是创建新索引。
    
    MySQL 5.7 版本后,可以通过查询 sys 库的 `schema_redundant_indexes` 表来查看冗余索引             
    
    ### Mysql如何为表字段添加索引???
    
    1.添加PRIMARY KEY(主键索引)
    
    ```
    ALTER TABLE `table_name` ADD PRIMARY KEY ( `column` ) 
    ```
    2.添加UNIQUE(唯一索引) 
    
    ```
    ALTER TABLE `table_name` ADD UNIQUE ( `column` ) 
    ```
     
    3.添加INDEX(普通索引) 
    
    ```
    ALTER TABLE `table_name` ADD INDEX index_name ( `column` )
    ```
     
    4.添加FULLTEXT(全文索引) 
    
    ```
    ALTER TABLE `table_name` ADD FULLTEXT ( `column`) 
    ```
     
    5.添加多列索引
    
    ```
    ALTER TABLE `table_name` ADD INDEX index_name ( `column1`, `column2`, `column3` )
    ```
    
    
    # 参考
    
    - 《Java工程师修炼之道》
    - 《MySQL高性能书籍_第3版》
    - https://juejin.im/post/5b55b842f265da0f9e589e79
    
    > 作者: 听风,原文地址: <https://www.cnblogs.com/huchong/p/10219318.html>。JavaGuide 已获得作者授权。
    
    <!-- TOC -->
    
    - [数据库命令规范](#数据库命令规范)
    - [数据库基本设计规范](#数据库基本设计规范)
        - [1. 所有表必须使用 Innodb 存储引擎](#1-所有表必须使用-innodb-存储引擎)
        - [2. 数据库和表的字符集统一使用 UTF8](#2-数据库和表的字符集统一使用-utf8)
        - [3. 所有表和字段都需要添加注释](#3-所有表和字段都需要添加注释)
        - [4. 尽量控制单表数据量的大小,建议控制在 500 万以内。](#4-尽量控制单表数据量的大小建议控制在-500-万以内)
        - [5. 谨慎使用 MySQL 分区表](#5-谨慎使用-mysql-分区表)
        - [6.尽量做到冷热数据分离,减小表的宽度](#6尽量做到冷热数据分离减小表的宽度)
        - [7. 禁止在表中建立预留字段](#7-禁止在表中建立预留字段)
        - [8. 禁止在数据库中存储图片,文件等大的二进制数据](#8-禁止在数据库中存储图片文件等大的二进制数据)
        - [9. 禁止在线上做数据库压力测试](#9-禁止在线上做数据库压力测试)
        - [10. 禁止从开发环境,测试环境直接连接生成环境数据库](#10-禁止从开发环境测试环境直接连接生成环境数据库)
    - [数据库字段设计规范](#数据库字段设计规范)
        - [1. 优先选择符合存储需要的最小的数据类型](#1-优先选择符合存储需要的最小的数据类型)
        - [2. 避免使用 TEXT,BLOB 数据类型,最常见的 TEXT 类型可以存储 64k 的数据](#2-避免使用-textblob-数据类型最常见的-text-类型可以存储-64k-的数据)
        - [3. 避免使用 ENUM 类型](#3-避免使用-enum-类型)
        - [4. 尽可能把所有列定义为 NOT NULL](#4-尽可能把所有列定义为-not-null)
        - [5. 使用 TIMESTAMP(4 个字节) 或 DATETIME 类型 (8 个字节) 存储时间](#5-使用-timestamp4-个字节-或-datetime-类型-8-个字节-存储时间)
        - [6. 同财务相关的金额类数据必须使用 decimal 类型](#6-同财务相关的金额类数据必须使用-decimal-类型)
    - [索引设计规范](#索引设计规范)
        - [1. 限制每张表上的索引数量,建议单张表索引不超过 5 个](#1-限制每张表上的索引数量建议单张表索引不超过-5-个)
        - [2. 禁止给表中的每一列都建立单独的索引](#2-禁止给表中的每一列都建立单独的索引)
        - [3. 每个 Innodb 表必须有个主键](#3-每个-innodb-表必须有个主键)
        - [4. 常见索引列建议](#4-常见索引列建议)
        - [5.如何选择索引列的顺序](#5如何选择索引列的顺序)
        - [6. 避免建立冗余索引和重复索引(增加了查询优化器生成执行计划的时间)](#6-避免建立冗余索引和重复索引增加了查询优化器生成执行计划的时间)
        - [7. 对于频繁的查询优先考虑使用覆盖索引](#7-对于频繁的查询优先考虑使用覆盖索引)
        - [8.索引 SET 规范](#8索引-set-规范)
    - [数据库 SQL 开发规范](#数据库-sql-开发规范)
        - [1. 建议使用预编译语句进行数据库操作](#1-建议使用预编译语句进行数据库操作)
        - [2. 避免数据类型的隐式转换](#2-避免数据类型的隐式转换)
        - [3. 充分利用表上已经存在的索引](#3-充分利用表上已经存在的索引)
        - [4. 数据库设计时,应该要对以后扩展进行考虑](#4-数据库设计时应该要对以后扩展进行考虑)
        - [5. 程序连接不同的数据库使用不同的账号,进制跨库查询](#5-程序连接不同的数据库使用不同的账号进制跨库查询)
        - [6. 禁止使用 SELECT * 必须使用 SELECT <字段列表> 查询](#6-禁止使用-select--必须使用-select-字段列表-查询)
        - [7. 禁止使用不含字段列表的 INSERT 语句](#7-禁止使用不含字段列表的-insert-语句)
        - [8. 避免使用子查询,可以把子查询优化为 join 操作](#8-避免使用子查询可以把子查询优化为-join-操作)
        - [9. 避免使用 JOIN 关联太多的表](#9-避免使用-join-关联太多的表)
        - [10. 减少同数据库的交互次数](#10-减少同数据库的交互次数)
        - [11. 对应同一列进行 or 判断时,使用 in 代替 or](#11-对应同一列进行-or-判断时使用-in-代替-or)
        - [12. 禁止使用 order by rand() 进行随机排序](#12-禁止使用-order-by-rand-进行随机排序)
        - [13. WHERE 从句中禁止对列进行函数转换和计算](#13-where-从句中禁止对列进行函数转换和计算)
        - [14. 在明显不会有重复值时使用 UNION ALL 而不是 UNION](#14-在明显不会有重复值时使用-union-all-而不是-union)
        - [15. 拆分复杂的大 SQL 为多个小 SQL](#15-拆分复杂的大-sql-为多个小-sql)
    - [数据库操作行为规范](#数据库操作行为规范)
        - [1. 超 100 万行的批量写 (UPDATE,DELETE,INSERT) 操作,要分批多次进行操作](#1-超-100-万行的批量写-updatedeleteinsert-操作要分批多次进行操作)
        - [2. 对于大表使用 pt-online-schema-change 修改表结构](#2-对于大表使用-pt-online-schema-change-修改表结构)
        - [3. 禁止为程序使用的账号赋予 super 权限](#3-禁止为程序使用的账号赋予-super-权限)
        - [4. 对于程序连接数据库账号,遵循权限最小原则](#4-对于程序连接数据库账号遵循权限最小原则)
    
    <!-- /TOC -->
    
    ## 数据库命令规范
    
    - 所有数据库对象名称必须使用小写字母并用下划线分割
    - 所有数据库对象名称禁止使用 MySQL 保留关键字(如果表名中包含关键字查询时,需要将其用单引号括起来)
    - 数据库对象的命名要能做到见名识意,并且最后不要超过 32 个字符
    - 临时库表必须以 tmp_为前缀并以日期为后缀,备份表必须以 bak_为前缀并以日期 (时间戳) 为后缀
    - 所有存储相同数据的列名和列类型必须一致(一般作为关联列,如果查询时关联列类型不一致会自动进行数据类型隐式转换,会造成列上的索引失效,导致查询效率降低)
    
    ------
    
    ## 数据库基本设计规范
    
    ### 1. 所有表必须使用 Innodb 存储引擎
    
    没有特殊要求(即 Innodb 无法满足的功能如:列存储,存储空间数据等)的情况下,所有表必须使用 Innodb 存储引擎(MySQL5.5 之前默认使用 Myisam,5.6 以后默认的为 Innodb)。
    
    Innodb 支持事务,支持行级锁,更好的恢复性,高并发下性能更好。
    
    ### 2. 数据库和表的字符集统一使用 UTF8
    
    兼容性更好,统一字符集可以避免由于字符集转换产生的乱码,不同的字符集进行比较前需要进行转换会造成索引失效,如果数据库中有存储 emoji 表情的需要,字符集需要采用 utf8mb4 字符集。
    
    ### 3. 所有表和字段都需要添加注释
    
    使用 comment 从句添加表和列的备注,从一开始就进行数据字典的维护
    
    ### 4. 尽量控制单表数据量的大小,建议控制在 500 万以内。
    
    500 万并不是 MySQL 数据库的限制,过大会造成修改表结构,备份,恢复都会有很大的问题。
    
    可以用历史数据归档(应用于日志数据),分库分表(应用于业务数据)等手段来控制数据量大小
    
    ### 5. 谨慎使用 MySQL 分区表
    
    分区表在物理上表现为多个文件,在逻辑上表现为一个表;
    
    谨慎选择分区键,跨分区查询效率可能更低;
    
    建议采用物理分表的方式管理大数据。
    
    ### 6.尽量做到冷热数据分离,减小表的宽度
    
    > MySQL 限制每个表最多存储 4096 列,并且每一行数据的大小不能超过 65535 字节。
    
    减少磁盘 IO,保证热数据的内存缓存命中率(表越宽,把表装载进内存缓冲池时所占用的内存也就越大,也会消耗更多的 IO);
    
    更有效的利用缓存,避免读入无用的冷数据;
    
    经常一起使用的列放到一个表中(避免更多的关联操作)。
    
    ### 7. 禁止在表中建立预留字段
    
    预留字段的命名很难做到见名识义。
    
    预留字段无法确认存储的数据类型,所以无法选择合适的类型。
    
    对预留字段类型的修改,会对表进行锁定。
    
    ### 8. 禁止在数据库中存储图片,文件等大的二进制数据
    
    通常文件很大,会短时间内造成数据量快速增长,数据库进行数据库读取时,通常会进行大量的随机 IO 操作,文件很大时,IO 操作很耗时。
    
    通常存储于文件服务器,数据库只存储文件地址信息
    
    ### 9. 禁止在线上做数据库压力测试
    
    ### 10. 禁止从开发环境,测试环境直接连接生产环境数据库
    
    ------
    
    ## 数据库字段设计规范
    
    ### 1. 优先选择符合存储需要的最小的数据类型
    
    **原因:**
    
    列的字段越大,建立索引时所需要的空间也就越大,这样一页中所能存储的索引节点的数量也就越少也越少,在遍历时所需要的 IO 次数也就越多,索引的性能也就越差。
    
    **方法:**
    
    **a.将字符串转换成数字类型存储,如:将 IP 地址转换成整形数据**
    
    MySQL 提供了两个方法来处理 ip 地址
    
    - inet_aton 把 ip 转为无符号整型 (4-8 位)
    - inet_ntoa 把整型的 ip 转为地址
    
    插入数据前,先用 inet_aton 把 ip 地址转为整型,可以节省空间,显示数据时,使用 inet_ntoa 把整型的 ip 地址转为地址显示即可。
    
    **b.对于非负型的数据 (如自增 ID,整型 IP) 来说,要优先使用无符号整型来存储**
    
    **原因:**
    
    无符号相对于有符号可以多出一倍的存储空间
    
    ```
    SIGNED INT -2147483648~2147483647
    UNSIGNED INT 0~4294967295
    ```
    
    VARCHAR(N) 中的 N 代表的是字符数,而不是字节数,使用 UTF8 存储 255 个汉字 Varchar(255)=765 个字节。**过大的长度会消耗更多的内存。**
    
    ### 2. 避免使用 TEXT,BLOB 数据类型,最常见的 TEXT 类型可以存储 64k 的数据
    
    **a. 建议把 BLOB 或是 TEXT 列分离到单独的扩展表中**
    
    MySQL 内存临时表不支持 TEXT、BLOB 这样的大数据类型,如果查询中包含这样的数据,在排序等操作时,就不能使用内存临时表,必须使用磁盘临时表进行。而且对于这种数据,MySQL 还是要进行二次查询,会使 sql 性能变得很差,但是不是说一定不能使用这样的数据类型。
    
    如果一定要使用,建议把 BLOB 或是 TEXT 列分离到单独的扩展表中,查询时一定不要使用 select * 而只需要取出必要的列,不需要 TEXT 列的数据时不要对该列进行查询。
    
    **2、TEXT 或 BLOB 类型只能使用前缀索引**
    
    因为[MySQL](http://mp.weixin.qq.com/s?__biz=MzI4Njc5NjM1NQ==&mid=2247487885&idx=1&sn=65b1bf5f7d4505502620179669a9c2df&chksm=ebd62ea1dca1a7b7bf884bcd9d538d78ba064ee03c09436ca8e57873b1d98a55afd6d7884cfc&scene=21#wechat_redirect) 对索引字段长度是有限制的,所以 TEXT 类型只能使用前缀索引,并且 TEXT 列上是不能有默认值的
    
    ### 3. 避免使用 ENUM 类型
    
    修改 ENUM 值需要使用 ALTER 语句
    
    ENUM 类型的 ORDER BY 操作效率低,需要额外操作
    
    禁止使用数值作为 ENUM 的枚举值
    
    ### 4. 尽可能把所有列定义为 NOT NULL
    
    **原因:**
    
    索引 NULL 列需要额外的空间来保存,所以要占用更多的空间
    
    进行比较和计算时要对 NULL 值做特别的处理
    
    ### 5. 使用 TIMESTAMP(4 个字节) 或 DATETIME 类型 (8 个字节) 存储时间
    
    TIMESTAMP 存储的时间范围 1970-01-01 00:00:01 ~ 2038-01-19-03:14:07
    
    TIMESTAMP 占用 4 字节和 INT 相同,但比 INT 可读性高
    
    超出 TIMESTAMP 取值范围的使用 DATETIME 类型存储
    
    **经常会有人用字符串存储日期型的数据(不正确的做法)**
    
    - 缺点 1:无法用日期函数进行计算和比较
    - 缺点 2:用字符串存储日期要占用更多的空间
    
    ### 6. 同财务相关的金额类数据必须使用 decimal 类型
    
    - 非精准浮点:float,double
    - 精准浮点:decimal
    
    Decimal 类型为精准浮点数,在计算时不会丢失精度
    
    占用空间由定义的宽度决定,每 4 个字节可以存储 9 位数字,并且小数点要占用一个字节
    
    可用于存储比 bigint 更大的整型数据
    
    ------
    
    ## 索引设计规范
    
    ### 1. 限制每张表上的索引数量,建议单张表索引不超过 5 个
    
    索引并不是越多越好!索引可以提高效率同样可以降低效率。
    
    索引可以增加查询效率,但同样也会降低插入和更新的效率,甚至有些情况下会降低查询效率。
    
    因为 MySQL 优化器在选择如何优化查询时,会根据统一信息,对每一个可以用到的索引来进行评估,以生成出一个最好的执行计划,如果同时有很多个索引都可以用于查询,就会增加 MySQL 优化器生成执行计划的时间,同样会降低查询性能。
    
    ### 2. 禁止给表中的每一列都建立单独的索引
    
    5.6 版本之前,一个 sql 只能使用到一个表中的一个索引,5.6 以后,虽然有了合并索引的优化方式,但是还是远远没有使用一个联合索引的查询方式好。
    
    ### 3. 每个 Innodb 表必须有个主键
    
    Innodb 是一种索引组织表:数据的存储的逻辑顺序和索引的顺序是相同的。每个表都可以有多个索引,但是表的存储顺序只能有一种。
    
    Innodb 是按照主键索引的顺序来组织表的
    
    - 不要使用更新频繁的列作为主键,不适用多列主键(相当于联合索引)
    - 不要使用 UUID,MD5,HASH,字符串列作为主键(无法保证数据的顺序增长)
    - 主键建议使用自增 ID 值
    
    ------
    
    ### 4. 常见索引列建议
    
    - 出现在 SELECT、UPDATE、DELETE 语句的 WHERE 从句中的列
    - 包含在 ORDER BY、GROUP BY、DISTINCT 中的字段
    - 并不要将符合 1 和 2 中的字段的列都建立一个索引, 通常将 1、2 中的字段建立联合索引效果更好
    - 多表 join 的关联列
    
    ------
    
    ### 5.如何选择索引列的顺序
    
    建立索引的目的是:希望通过索引进行数据查找,减少随机 IO,增加查询性能 ,索引能过滤出越少的数据,则从磁盘中读入的数据也就越少。
    
    - 区分度最高的放在联合索引的最左侧(区分度=列中不同值的数量/列的总行数)
    - 尽量把字段长度小的列放在联合索引的最左侧(因为字段长度越小,一页能存储的数据量越大,IO 性能也就越好)
    - 使用最频繁的列放到联合索引的左侧(这样可以比较少的建立一些索引)
    
    ------
    
    ### 6. 避免建立冗余索引和重复索引(增加了查询优化器生成执行计划的时间)
    
    - 重复索引示例:primary key(id)、index(id)、unique index(id)
    - 冗余索引示例:index(a,b,c)、index(a,b)、index(a)
    
    ------
    
    ### 7. 对于频繁的查询优先考虑使用覆盖索引
    
    > 覆盖索引:就是包含了所有查询字段 (where,select,ordery by,group by 包含的字段) 的索引
    
    **覆盖索引的好处:**
    
    - **避免 Innodb 表进行索引的二次查询:** Innodb 是以聚集索引的顺序来存储的,对于 Innodb 来说,二级索引在叶子节点中所保存的是行的主键信息,如果是用二级索引查询数据的话,在查找到相应的键值后,还要通过主键进行二次查询才能获取我们真实所需要的数据。而在覆盖索引中,二级索引的键值中可以获取所有的数据,避免了对主键的二次查询 ,减少了 IO 操作,提升了查询效率。
    - **可以把随机 IO 变成顺序 IO 加快查询效率:** 由于覆盖索引是按键值的顺序存储的,对于 IO 密集型的范围查找来说,对比随机从磁盘读取每一行的数据 IO 要少的多,因此利用覆盖索引在访问时也可以把磁盘的随机读取的 IO 转变成索引查找的顺序 IO。
    
    ------
    
    ### 8.索引 SET 规范
    
    **尽量避免使用外键约束**
    
    - 不建议使用外键约束(foreign key),但一定要在表与表之间的关联键上建立索引
    - 外键可用于保证数据的参照完整性,但建议在业务端实现
    - 外键会影响父表和子表的写操作从而降低性能
    
    ------
    
    ## 数据库 SQL 开发规范
    
    ### 1. 建议使用预编译语句进行数据库操作
    
    预编译语句可以重复使用这些计划,减少 SQL 编译所需要的时间,还可以解决动态 SQL 所带来的 SQL 注入的问题。
    
    只传参数,比传递 SQL 语句更高效。
    
    相同语句可以一次解析,多次使用,提高处理效率。
    
    ### 2. 避免数据类型的隐式转换
    
    隐式转换会导致索引失效如:
    
    ```
    select name,phone from customer where id = '111';
    ```
    
    ### 3. 充分利用表上已经存在的索引
    
    避免使用双%号的查询条件。如:`a like '%123%'`,(如果无前置%,只有后置%,是可以用到列上的索引的)
    
    一个 SQL 只能利用到复合索引中的一列进行范围查询。如:有 a,b,c 列的联合索引,在查询条件中有 a 列的范围查询,则在 b,c 列上的索引将不会被用到。
    
    在定义联合索引时,如果 a 列要用到范围查找的话,就要把 a 列放到联合索引的右侧,使用 left join 或 not exists 来优化 not in 操作,因为 not in 也通常会使用索引失效。
    
    ### 4. 数据库设计时,应该要对以后扩展进行考虑
    
    ### 5. 程序连接不同的数据库使用不同的账号,禁止跨库查询
    
    - 为数据库迁移和分库分表留出余地
    - 降低业务耦合度
    - 避免权限过大而产生的安全风险
    
    ### 6. 禁止使用 SELECT * 必须使用 SELECT <字段列表> 查询
    
    **原因:**
    
    - 消耗更多的 CPU 和 IO 以网络带宽资源
    - 无法使用覆盖索引
    - 可减少表结构变更带来的影响
    
    ### 7. 禁止使用不含字段列表的 INSERT 语句
    
    如:
    
    ```
    insert into values ('a','b','c');
    ```
    
    应使用:
    
    ```
    insert into t(c1,c2,c3) values ('a','b','c');
    ```
    
    ### 8. 避免使用子查询,可以把子查询优化为 join 操作
    
    通常子查询在 in 子句中,且子查询中为简单 SQL(不包含 union、group by、order by、limit 从句) 时,才可以把子查询转化为关联查询进行优化。
    
    **子查询性能差的原因:**
    
    子查询的结果集无法使用索引,通常子查询的结果集会被存储到临时表中,不论是内存临时表还是磁盘临时表都不会存在索引,所以查询性能会受到一定的影响。特别是对于返回结果集比较大的子查询,其对查询性能的影响也就越大。
    
    由于子查询会产生大量的临时表也没有索引,所以会消耗过多的 CPU 和 IO 资源,产生大量的慢查询。
    
    ### 9. 避免使用 JOIN 关联太多的表
    
    对于 MySQL 来说,是存在关联缓存的,缓存的大小可以由 join_buffer_size 参数进行设置。
    
    在 MySQL 中,对于同一个 SQL 多关联(join)一个表,就会多分配一个关联缓存,如果在一个 SQL 中关联的表越多,所占用的内存也就越大。
    
    如果程序中大量的使用了多表关联的操作,同时 join_buffer_size 设置的也不合理的情况下,就容易造成服务器内存溢出的情况,就会影响到服务器数据库性能的稳定性。
    
    同时对于关联操作来说,会产生临时表操作,影响查询效率,MySQL 最多允许关联 61 个表,建议不超过 5 个。
    
    ### 10. 减少同数据库的交互次数
    
    数据库更适合处理批量操作,合并多个相同的操作到一起,可以提高处理效率。
    
    ### 11. 对应同一列进行 or 判断时,使用 in 代替 or
    
    in 的值不要超过 500 个,in 操作可以更有效的利用索引,or 大多数情况下很少能利用到索引。
    
    ### 12. 禁止使用 order by rand() 进行随机排序
    
    order by rand() 会把表中所有符合条件的数据装载到内存中,然后在内存中对所有数据根据随机生成的值进行排序,并且可能会对每一行都生成一个随机值,如果满足条件的数据集非常大,就会消耗大量的 CPU 和 IO 及内存资源。
    
    推荐在程序中获取一个随机值,然后从数据库中获取数据的方式。
    
    ### 13. WHERE 从句中禁止对列进行函数转换和计算
    
    对列进行函数转换或计算时会导致无法使用索引
    
    **不推荐:**
    
    ```
    where date(create_time)='20190101'
    ```
    
    **推荐:**
    
    ```
    where create_time >= '20190101' and create_time < '20190102'
    ```
    
    ### 14. 在明显不会有重复值时使用 UNION ALL 而不是 UNION
    
    - UNION 会把两个结果集的所有数据放到临时表中后再进行去重操作
    - UNION ALL 不会再对结果集进行去重操作
    
    ### 15. 拆分复杂的大 SQL 为多个小 SQL
    
    - 大 SQL 逻辑上比较复杂,需要占用大量 CPU 进行计算的 SQL
    - MySQL 中,一个 SQL 只能使用一个 CPU 进行计算
    - SQL 拆分后可以通过并行执行来提高处理效率
    
    ------
    
    ## 数据库操作行为规范
    
    ### 1. 超 100 万行的批量写 (UPDATE,DELETE,INSERT) 操作,要分批多次进行操作
    
    **大批量操作可能会造成严重的主从延迟**
    
    主从环境中,大批量操作可能会造成严重的主从延迟,大批量的写操作一般都需要执行一定长的时间,
    而只有当主库上执行完成后,才会在其他从库上执行,所以会造成主库与从库长时间的延迟情况
    
    **binlog 日志为 row 格式时会产生大量的日志**
    
    大批量写操作会产生大量日志,特别是对于 row 格式二进制数据而言,由于在 row 格式中会记录每一行数据的修改,我们一次修改的数据越多,产生的日志量也就会越多,日志的传输和恢复所需要的时间也就越长,这也是造成主从延迟的一个原因
    
    **避免产生大事务操作**
    
    大批量修改数据,一定是在一个事务中进行的,这就会造成表中大批量数据进行锁定,从而导致大量的阻塞,阻塞会对 MySQL 的性能产生非常大的影响。
    
    特别是长时间的阻塞会占满所有数据库的可用连接,这会使生产环境中的其他应用无法连接到数据库,因此一定要注意大批量写操作要进行分批
    
    ### 2. 对于大表使用 pt-online-schema-change 修改表结构
    
    - 避免大表修改产生的主从延迟
    - 避免在对表字段进行修改时进行锁表
    
    对大表数据结构的修改一定要谨慎,会造成严重的锁表操作,尤其是生产环境,是不能容忍的。
    
    pt-online-schema-change 它会首先建立一个与原表结构相同的新表,并且在新表上进行表结构的修改,然后再把原表中的数据复制到新表中,并在原表中增加一些触发器。把原表中新增的数据也复制到新表中,在行所有数据复制完成之后,把新表命名成原表,并把原来的表删除掉。把原来一个 DDL 操作,分解成多个小的批次进行。
    
    ### 3. 禁止为程序使用的账号赋予 super 权限
    
    - 当达到最大连接数限制时,还运行 1 个有 super 权限的用户连接
    - super 权限只能留给 DBA 处理问题的账号使用
    
    ### 4. 对于程序连接数据库账号,遵循权限最小原则
    
    - 程序使用数据库账号只能在一个 DB 下使用,不准跨库
    - 程序使用的账号原则上不准有 drop 权限
    
    > 原文地址:https://shockerli.net/post/1000-line-mysql-note/ ,JavaGuide 对本文进行了简答排版,新增了目录。
    > 作者:格物
    
    非常不错的总结,强烈建议保存下来,需要的时候看一看。
    
    <!-- TOC -->
    - [基本操作](#基本操作)
    - [数据库操作](#数据库操作)
    - [表的操作](#表的操作)
    - [数据操作](#数据操作)
    - [字符集编码](#字符集编码)
    - [数据类型(列类型)](#数据类型列类型)
    - [列属性(列约束)](#列属性列约束)
    - [建表规范](#建表规范)
    - [SELECT](#select)
    - [UNION](#union)
    - [子查询](#子查询)
    - [连接查询(join)](#连接查询join)
    - [TRUNCATE](#truncate)
    - [备份与还原](#备份与还原)
    - [视图](#视图)
    - [事务(transaction)](#事务transaction)
    - [锁表](#锁表)
    - [触发器](#触发器)
    - [SQL编程](#sql编程)
    - [存储过程](#存储过程)
    - [用户和权限管理](#用户和权限管理)
    - [表维护](#表维护)
    - [杂项](#杂项)
    
    <!-- /TOC -->
    
    ### 基本操作
    
    ```mysql
    /* Windows服务 */
    -- 启动MySQL
        net start mysql
    -- 创建Windows服务
        sc create mysql binPath= mysqld_bin_path(注意:等号与值之间有空格)
    /* 连接与断开服务器 */
    mysql -h 地址 -P 端口 -u 用户名 -p 密码
    SHOW PROCESSLIST -- 显示哪些线程正在运行
    SHOW VARIABLES -- 显示系统变量信息
    ```
    
    ### 数据库操作
    
    ```mysql
    /* 数据库操作 */ ------------------
    -- 查看当前数据库
        SELECT DATABASE();
    -- 显示当前时间、用户名、数据库版本
        SELECT now(), user(), version();
    -- 创建库
        CREATE DATABASE[ IF NOT EXISTS] 数据库名 数据库选项
        数据库选项:
            CHARACTER SET charset_name
            COLLATE collation_name
    -- 查看已有库
        SHOW DATABASES[ LIKE 'PATTERN']
    -- 查看当前库信息
        SHOW CREATE DATABASE 数据库名
    -- 修改库的选项信息
        ALTER DATABASE 库名 选项信息
    -- 删除库
        DROP DATABASE[ IF EXISTS] 数据库名
            同时删除该数据库相关的目录及其目录内容
    ```
    
    ### 表的操作 
    
    ```mysql
    -- 创建表
        CREATE [TEMPORARY] TABLE[ IF NOT EXISTS] [库名.]表名 ( 表的结构定义 )[ 表选项]
            每个字段必须有数据类型
            最后一个字段后不能有逗号
            TEMPORARY 临时表,会话结束时表自动消失
            对于字段的定义:
                字段名 数据类型 [NOT NULL | NULL] [DEFAULT default_value] [AUTO_INCREMENT] [UNIQUE [KEY] | [PRIMARY] KEY] [COMMENT 'string']
    -- 表选项
        -- 字符集
            CHARSET = charset_name
            如果表没有设定,则使用数据库字符集
        -- 存储引擎
            ENGINE = engine_name
            表在管理数据时采用的不同的数据结构,结构不同会导致处理方式、提供的特性操作等不同
            常见的引擎:InnoDB MyISAM Memory/Heap BDB Merge Example CSV MaxDB Archive
            不同的引擎在保存表的结构和数据时采用不同的方式
            MyISAM表文件含义:.frm表定义,.MYD表数据,.MYI表索引
            InnoDB表文件含义:.frm表定义,表空间数据和日志文件
            SHOW ENGINES -- 显示存储引擎的状态信息
            SHOW ENGINE 引擎名 {LOGS|STATUS} -- 显示存储引擎的日志或状态信息
        -- 自增起始数
           AUTO_INCREMENT = 行数
        -- 数据文件目录
            DATA DIRECTORY = '目录'
        -- 索引文件目录
            INDEX DIRECTORY = '目录'
        -- 表注释
            COMMENT = 'string'
        -- 分区选项
            PARTITION BY ... (详细见手册)
    -- 查看所有表
        SHOW TABLES[ LIKE 'pattern']
        SHOW TABLES FROM  库名
    -- 查看表结构
        SHOW CREATE TABLE 表名 (信息更详细)
        DESC 表名 / DESCRIBE 表名 / EXPLAIN 表名 / SHOW COLUMNS FROM 表名 [LIKE 'PATTERN']
        SHOW TABLE STATUS [FROM db_name] [LIKE 'pattern']
    -- 修改表
        -- 修改表本身的选项
            ALTER TABLE 表名 表的选项
            eg: ALTER TABLE 表名 ENGINE=MYISAM;
        -- 对表进行重命名
            RENAME TABLE 原表名 TO 新表名
            RENAME TABLE 原表名 TO 库名.表名 (可将表移动到另一个数据库)
            -- RENAME可以交换两个表名
        -- 修改表的字段机构(13.1.2. ALTER TABLE语法)
            ALTER TABLE 表名 操作名
            -- 操作名
                ADD[ COLUMN] 字段定义       -- 增加字段
                    AFTER 字段名          -- 表示增加在该字段名后面
                    FIRST               -- 表示增加在第一个
                ADD PRIMARY KEY(字段名)   -- 创建主键
                ADD UNIQUE [索引名] (字段名)-- 创建唯一索引
                ADD INDEX [索引名] (字段名) -- 创建普通索引
                DROP[ COLUMN] 字段名      -- 删除字段
                MODIFY[ COLUMN] 字段名 字段属性     -- 支持对字段属性进行修改,不能修改字段名(所有原有属性也需写上)
                CHANGE[ COLUMN] 原字段名 新字段名 字段属性      -- 支持对字段名修改
                DROP PRIMARY KEY    -- 删除主键(删除主键前需删除其AUTO_INCREMENT属性)
                DROP INDEX 索引名 -- 删除索引
                DROP FOREIGN KEY 外键    -- 删除外键
    -- 删除表
        DROP TABLE[ IF EXISTS] 表名 ...
    -- 清空表数据
        TRUNCATE [TABLE] 表名
    -- 复制表结构
        CREATE TABLE 表名 LIKE 要复制的表名
    -- 复制表结构和数据
        CREATE TABLE 表名 [AS] SELECT * FROM 要复制的表名
    -- 检查表是否有错误
        CHECK TABLE tbl_name [, tbl_name] ... [option] ...
    -- 优化表
        OPTIMIZE [LOCAL | NO_WRITE_TO_BINLOG] TABLE tbl_name [, tbl_name] ...
    -- 修复表
        REPAIR [LOCAL | NO_WRITE_TO_BINLOG] TABLE tbl_name [, tbl_name] ... [QUICK] [EXTENDED] [USE_FRM]
    -- 分析表
        ANALYZE [LOCAL | NO_WRITE_TO_BINLOG] TABLE tbl_name [, tbl_name] ...
    ```
    
    ### 数据操作
    
    ```mysql
    /* 数据操作 */ ------------------
    -- 增
        INSERT [INTO] 表名 [(字段列表)] VALUES (值列表)[, (值列表), ...]
            -- 如果要插入的值列表包含所有字段并且顺序一致,则可以省略字段列表。
            -- 可同时插入多条数据记录!
            REPLACE 与 INSERT 完全一样,可互换。
        INSERT [INTO] 表名 SET 字段名=值[, 字段名=值, ...]
    -- 查
        SELECT 字段列表 FROM 表名[ 其他子句]
            -- 可来自多个表的多个字段
            -- 其他子句可以不使用
            -- 字段列表可以用*代替,表示所有字段
    -- 删
        DELETE FROM 表名[ 删除条件子句]
            没有条件子句,则会删除全部
    -- 改
        UPDATE 表名 SET 字段名=新值[, 字段名=新值] [更新条件]
    ```
    
    ### 字符集编码
    
    ```mysql
    /* 字符集编码 */ ------------------
    -- MySQL、数据库、表、字段均可设置编码
    -- 数据编码与客户端编码不需一致
    SHOW VARIABLES LIKE 'character_set_%'   -- 查看所有字符集编码项
        character_set_client        客户端向服务器发送数据时使用的编码
        character_set_results       服务器端将结果返回给客户端所使用的编码
        character_set_connection    连接层编码
    SET 变量名 = 变量值
        SET character_set_client = gbk;
        SET character_set_results = gbk;
        SET character_set_connection = gbk;
    SET NAMES GBK;  -- 相当于完成以上三个设置
    -- 校对集
        校对集用以排序
        SHOW CHARACTER SET [LIKE 'pattern']/SHOW CHARSET [LIKE 'pattern']   查看所有字符集
        SHOW COLLATION [LIKE 'pattern']     查看所有校对集
        CHARSET 字符集编码     设置字符集编码
        COLLATE 校对集编码     设置校对集编码
    ```
    
    ### 数据类型(列类型)
    
    ```mysql
    /* 数据类型(列类型) */ ------------------
    1. 数值类型
    -- a. 整型 ----------
        类型         字节     范围(有符号位)
        tinyint     1字节    -128 ~ 127      无符号位:0 ~ 255
        smallint    2字节    -32768 ~ 32767
        mediumint   3字节    -8388608 ~ 8388607
        int         4字节
        bigint      8字节
        int(M)  M表示总位数
        - 默认存在符号位,unsigned 属性修改
        - 显示宽度,如果某个数不够定义字段时设置的位数,则前面以0补填,zerofill 属性修改
            例:int(5)   插入一个数'123',补填后为'00123'
        - 在满足要求的情况下,越小越好。
        - 1表示bool值真,0表示bool值假。MySQL没有布尔类型,通过整型0和1表示。常用tinyint(1)表示布尔型。
    -- b. 浮点型 ----------
        类型             字节     范围
        float(单精度)     4字节
        double(双精度)    8字节
        浮点型既支持符号位 unsigned 属性,也支持显示宽度 zerofill 属性。
            不同于整型,前后均会补填0.
        定义浮点型时,需指定总位数和小数位数。
            float(M, D)     double(M, D)
            M表示总位数,D表示小数位数。
            M和D的大小会决定浮点数的范围。不同于整型的固定范围。
            M既表示总位数(不包括小数点和正负号),也表示显示宽度(所有显示符号均包括)。
            支持科学计数法表示。
            浮点数表示近似值。
    -- c. 定点数 ----------
        decimal -- 可变长度
        decimal(M, D)   M也表示总位数,D表示小数位数。
        保存一个精确的数值,不会发生数据的改变,不同于浮点数的四舍五入。
        将浮点数转换为字符串来保存,每9位数字保存为4个字节。
    2. 字符串类型
    -- a. char, varchar ----------
        char    定长字符串,速度快,但浪费空间
        varchar 变长字符串,速度慢,但节省空间
        M表示能存储的最大长度,此长度是字符数,非字节数。
        不同的编码,所占用的空间不同。
        char,最多255个字符,与编码无关。
        varchar,最多65535字符,与编码有关。
        一条有效记录最大不能超过65535个字节。
            utf8 最大为21844个字符,gbk 最大为32766个字符,latin1 最大为65532个字符
        varchar 是变长的,需要利用存储空间保存 varchar 的长度,如果数据小于255个字节,则采用一个字节来保存长度,反之需要两个字节来保存。
        varchar 的最大有效长度由最大行大小和使用的字符集确定。
        最大有效长度是65532字节,因为在varchar存字符串时,第一个字节是空的,不存在任何数据,然后还需两个字节来存放字符串的长度,所以有效长度是64432-1-2=65532字节。
        例:若一个表定义为 CREATE TABLE tb(c1 int, c2 char(30), c3 varchar(N)) charset=utf8; 问N的最大值是多少? 答:(65535-1-2-4-30*3)/3
    -- b. blob, text ----------
        blob 二进制字符串(字节字符串)
            tinyblob, blob, mediumblob, longblob
        text 非二进制字符串(字符字符串)
            tinytext, text, mediumtext, longtext
        text 在定义时,不需要定义长度,也不会计算总长度。
        text 类型在定义时,不可给default值
    -- c. binary, varbinary ----------
        类似于char和varchar,用于保存二进制字符串,也就是保存字节字符串而非字符字符串。
        char, varchar, text 对应 binary, varbinary, blob.
    3. 日期时间类型
        一般用整型保存时间戳,因为PHP可以很方便的将时间戳进行格式化。
        datetime    8字节    日期及时间     1000-01-01 00:00:00 到 9999-12-31 23:59:59
        date        3字节    日期         1000-01-01 到 9999-12-31
        timestamp   4字节    时间戳        19700101000000 到 2038-01-19 03:14:07
        time        3字节    时间         -838:59:59 到 838:59:59
        year        1字节    年份         1901 - 2155
    datetime    YYYY-MM-DD hh:mm:ss
    timestamp   YY-MM-DD hh:mm:ss
                YYYYMMDDhhmmss
                YYMMDDhhmmss
                YYYYMMDDhhmmss
                YYMMDDhhmmss
    date        YYYY-MM-DD
                YY-MM-DD
                YYYYMMDD
                YYMMDD
                YYYYMMDD
                YYMMDD
    time        hh:mm:ss
                hhmmss
                hhmmss
    year        YYYY
                YY
                YYYY
                YY
    4. 枚举和集合
    -- 枚举(enum) ----------
    enum(val1, val2, val3...)
        在已知的值中进行单选。最大数量为65535.
        枚举值在保存时,以2个字节的整型(smallint)保存。每个枚举值,按保存的位置顺序,从1开始逐一递增。
        表现为字符串类型,存储却是整型。
        NULL值的索引是NULL。
        空字符串错误值的索引值是0。
    -- 集合(set) ----------
    set(val1, val2, val3...)
        create table tab ( gender set('男', '女', '无') );
        insert into tab values ('男, 女');
        最多可以有64个不同的成员。以bigint存储,共8个字节。采取位运算的形式。
        当创建表时,SET成员值的尾部空格将自动被删除。
    ```
    
    ### 列属性(列约束)
    
    ```mysql
    /* 列属性(列约束) */ ------------------
    1. PRIMARY 主键
        - 能唯一标识记录的字段,可以作为主键。
        - 一个表只能有一个主键。
        - 主键具有唯一性。
        - 声明字段时,用 primary key 标识。
            也可以在字段列表之后声明
                例:create table tab ( id int, stu varchar(10), primary key (id));
        - 主键字段的值不能为null。
        - 主键可以由多个字段共同组成。此时需要在字段列表后声明的方法。
            例:create table tab ( id int, stu varchar(10), age int, primary key (stu, age));
    2. UNIQUE 唯一索引(唯一约束)
        使得某字段的值也不能重复。
    3. NULL 约束
        null不是数据类型,是列的一个属性。
        表示当前列是否可以为null,表示什么都没有。
        null, 允许为空。默认。
        not null, 不允许为空。
        insert into tab values (null, 'val');
            -- 此时表示将第一个字段的值设为null, 取决于该字段是否允许为null
    4. DEFAULT 默认值属性
        当前字段的默认值。
        insert into tab values (default, 'val');    -- 此时表示强制使用默认值。
        create table tab ( add_time timestamp default current_timestamp );
            -- 表示将当前时间的时间戳设为默认值。
            current_date, current_time
    5. AUTO_INCREMENT 自动增长约束
        自动增长必须为索引(主键或unique)
        只能存在一个字段为自动增长。
        默认为1开始自动增长。可以通过表属性 auto_increment = x进行设置,或 alter table tbl auto_increment = x;
    6. COMMENT 注释
        例:create table tab ( id int ) comment '注释内容';
    7. FOREIGN KEY 外键约束
        用于限制主表与从表数据完整性。
        alter table t1 add constraint `t1_t2_fk` foreign key (t1_id) references t2(id);
            -- 将表t1的t1_id外键关联到表t2的id字段。
            -- 每个外键都有一个名字,可以通过 constraint 指定
        存在外键的表,称之为从表(子表),外键指向的表,称之为主表(父表)。
        作用:保持数据一致性,完整性,主要目的是控制存储在外键表(从表)中的数据。
        MySQL中,可以对InnoDB引擎使用外键约束:
        语法:
        foreign key (外键字段) references 主表名 (关联字段) [主表记录删除时的动作] [主表记录更新时的动作]
        此时需要检测一个从表的外键需要约束为主表的已存在的值。外键在没有关联的情况下,可以设置为null.前提是该外键列,没有not null。
        可以不指定主表记录更改或更新时的动作,那么此时主表的操作被拒绝。
        如果指定了 on update 或 on delete:在删除或更新时,有如下几个操作可以选择:
        1. cascade,级联操作。主表数据被更新(主键值更新),从表也被更新(外键值更新)。主表记录被删除,从表相关记录也被删除。
        2. set null,设置为null。主表数据被更新(主键值更新),从表的外键被设置为null。主表记录被删除,从表相关记录外键被设置成null。但注意,要求该外键列,没有not null属性约束。
        3. restrict,拒绝父表删除和更新。
        注意,外键只被InnoDB存储引擎所支持。其他引擎是不支持的。
    
    ```
    
    ### 建表规范
    
    ```mysql
    /* 建表规范 */ ------------------
        -- Normal Format, NF
            - 每个表保存一个实体信息
            - 每个具有一个ID字段作为主键
            - ID主键 + 原子表
        -- 1NF, 第一范式
            字段不能再分,就满足第一范式。
        -- 2NF, 第二范式
            满足第一范式的前提下,不能出现部分依赖。
            消除复合主键就可以避免部分依赖。增加单列关键字。
        -- 3NF, 第三范式
            满足第二范式的前提下,不能出现传递依赖。
            某个字段依赖于主键,而有其他字段依赖于该字段。这就是传递依赖。
            将一个实体信息的数据放在一个表内实现。
    ```
    
    ### SELECT 
    
    ```mysql
    /* SELECT */ ------------------
    SELECT [ALL|DISTINCT] select_expr FROM -> WHERE -> GROUP BY [合计函数] -> HAVING -> ORDER BY -> LIMIT
    a. select_expr
        -- 可以用 * 表示所有字段。
            select * from tb;
        -- 可以使用表达式(计算公式、函数调用、字段也是个表达式)
            select stu, 29+25, now() from tb;
        -- 可以为每个列使用别名。适用于简化列标识,避免多个列标识符重复。
            - 使用 as 关键字,也可省略 as.
            select stu+10 as add10 from tb;
    b. FROM 子句
        用于标识查询来源。
        -- 可以为表起别名。使用as关键字。
            SELECT * FROM tb1 AS tt, tb2 AS bb;
        -- from子句后,可以同时出现多个表。
            -- 多个表会横向叠加到一起,而数据会形成一个笛卡尔积。
            SELECT * FROM tb1, tb2;
        -- 向优化符提示如何选择索引
            USE INDEX、IGNORE INDEX、FORCE INDEX
            SELECT * FROM table1 USE INDEX (key1,key2) WHERE key1=1 AND key2=2 AND key3=3;
            SELECT * FROM table1 IGNORE INDEX (key3) WHERE key1=1 AND key2=2 AND key3=3;
    c. WHERE 子句
        -- 从from获得的数据源中进行筛选。
        -- 整型1表示真,0表示假。
        -- 表达式由运算符和运算数组成。
            -- 运算数:变量(字段)、值、函数返回值
            -- 运算符:
                =, <=>, <>, !=, <=, <, >=, >, !, &&, ||,
                in (not) null, (not) like, (not) in, (not) between and, is (not), and, or, not, xor
                is/is not 加上ture/false/unknown,检验某个值的真假
                <=>与<>功能相同,<=>可用于null比较
    d. GROUP BY 子句, 分组子句
        GROUP BY 字段/别名 [排序方式]
        分组后会进行排序。升序:ASC,降序:DESC
        以下[合计函数]需配合 GROUP BY 使用:
        count 返回不同的非NULL值数目  count(*)、count(字段)
        sum 求和
        max 求最大值
        min 求最小值
        avg 求平均值
        group_concat 返回带有来自一个组的连接的非NULL值的字符串结果。组内字符串连接。
    e. HAVING 子句,条件子句
        与 where 功能、用法相同,执行时机不同。
        where 在开始时执行检测数据,对原数据进行过滤。
        having 对筛选出的结果再次进行过滤。
        having 字段必须是查询出来的,where 字段必须是数据表存在的。
        where 不可以使用字段的别名,having 可以。因为执行WHERE代码时,可能尚未确定列值。
        where 不可以使用合计函数。一般需用合计函数才会用 having
        SQL标准要求HAVING必须引用GROUP BY子句中的列或用于合计函数中的列。
    f. ORDER BY 子句,排序子句
        order by 排序字段/别名 排序方式 [,排序字段/别名 排序方式]...
        升序:ASC,降序:DESC
        支持多个字段的排序。
    g. LIMIT 子句,限制结果数量子句
        仅对处理好的结果进行数量限制。将处理好的结果的看作是一个集合,按照记录出现的顺序,索引从0开始。
        limit 起始位置, 获取条数
        省略第一个参数,表示从索引0开始。limit 获取条数
    h. DISTINCT, ALL 选项
        distinct 去除重复记录
        默认为 all, 全部记录
    ```
    
    ###  UNION
    
    ```mysql
    /* UNION */ ------------------
        将多个select查询的结果组合成一个结果集合。
        SELECT ... UNION [ALL|DISTINCT] SELECT ...
        默认 DISTINCT 方式,即所有返回的行都是唯一的
        建议,对每个SELECT查询加上小括号包裹。
        ORDER BY 排序时,需加上 LIMIT 进行结合。
        需要各select查询的字段数量一样。
        每个select查询的字段列表(数量、类型)应一致,因为结果中的字段名以第一条select语句为准。
    ```
    
    ### 子查询
    
    ```mysql
    /* 子查询 */ ------------------
        - 子查询需用括号包裹。
    -- from型
        from后要求是一个表,必须给子查询结果取个别名。
        - 简化每个查询内的条件。
        - from型需将结果生成一个临时表格,可用以原表的锁定的释放。
        - 子查询返回一个表,表型子查询。
        select * from (select * from tb where id>0) as subfrom where id>1;
    -- where型
        - 子查询返回一个值,标量子查询。
        - 不需要给子查询取别名。
        - where子查询内的表,不能直接用以更新。
        select * from tb where money = (select max(money) from tb);
        -- 列子查询
            如果子查询结果返回的是一列。
            使用 in 或 not in 完成查询
            exists 和 not exists 条件
                如果子查询返回数据,则返回1或0。常用于判断条件。
                select column1 from t1 where exists (select * from t2);
        -- 行子查询
            查询条件是一个行。
            select * from t1 where (id, gender) in (select id, gender from t2);
            行构造符:(col1, col2, ...) 或 ROW(col1, col2, ...)
            行构造符通常用于与对能返回两个或两个以上列的子查询进行比较。
        -- 特殊运算符
        != all()    相当于 not in
        = some()    相当于 in。any 是 some 的别名
        != some()   不等同于 not in,不等于其中某一个。
        all, some 可以配合其他运算符一起使用。
    ```
    
    ### 连接查询(join)
    
    ```mysql
    /* 连接查询(join) */ ------------------
        将多个表的字段进行连接,可以指定连接条件。
    -- 内连接(inner join)
        - 默认就是内连接,可省略inner。
        - 只有数据存在时才能发送连接。即连接结果不能出现空行。
        on 表示连接条件。其条件表达式与where类似。也可以省略条件(表示条件永远为真)
        也可用where表示连接条件。
        还有 using, 但需字段名相同。 using(字段名)
        -- 交叉连接 cross join
            即,没有条件的内连接。
            select * from tb1 cross join tb2;
    -- 外连接(outer join)
        - 如果数据不存在,也会出现在连接结果中。
        -- 左外连接 left join
            如果数据不存在,左表记录会出现,而右表为null填充
        -- 右外连接 right join
            如果数据不存在,右表记录会出现,而左表为null填充
    -- 自然连接(natural join)
        自动判断连接条件完成连接。
        相当于省略了using,会自动查找相同字段名。
        natural join
        natural left join
        natural right join
    select info.id, info.name, info.stu_num, extra_info.hobby, extra_info.sex from info, extra_info where info.stu_num = extra_info.stu_id;
    ```
    
    ### TRUNCATE 
    
    ```mysql
    /* TRUNCATE */ ------------------
    TRUNCATE [TABLE] tbl_name
    清空数据
    删除重建表
    区别:
    1,truncate 是删除表再创建,delete 是逐条删除
    2,truncate 重置auto_increment的值。而delete不会
    3,truncate 不知道删除了几条,而delete知道。
    4,当被用于带分区的表时,truncate 会保留分区
    ```
    
    ### 备份与还原
    
    ```mysql
    /* 备份与还原 */ ------------------
    备份,将数据的结构与表内数据保存起来。
    利用 mysqldump 指令完成。
    -- 导出
    mysqldump [options] db_name [tables]
    mysqldump [options] ---database DB1 [DB2 DB3...]
    mysqldump [options] --all--database
    1. 导出一张表
      mysqldump -u用户名 -p密码 库名 表名 > 文件名(D:/a.sql)
    2. 导出多张表
      mysqldump -u用户名 -p密码 库名 表1 表2 表3 > 文件名(D:/a.sql)
    3. 导出所有表
      mysqldump -u用户名 -p密码 库名 > 文件名(D:/a.sql)
    4. 导出一个库
      mysqldump -u用户名 -p密码 --lock-all-tables --database 库名 > 文件名(D:/a.sql)
    可以-w携带WHERE条件
    -- 导入
    1. 在登录mysql的情况下:
      source  备份文件
    2. 在不登录的情况下
      mysql -u用户名 -p密码 库名 < 备份文件
    ```
    
    ### 视图
    
    ```mysql
    什么是视图:
        视图是一个虚拟表,其内容由查询定义。同真实的表一样,视图包含一系列带有名称的列和行数据。但是,视图并不在数据库中以存储的数据值集形式存在。行和列数据来自由定义视图的查询所引用的表,并且在引用视图时动态生成。
        视图具有表结构文件,但不存在数据文件。
        对其中所引用的基础表来说,视图的作用类似于筛选。定义视图的筛选可以来自当前或其它数据库的一个或多个表,或者其它视图。通过视图进行查询没有任何限制,通过它们进行数据修改时的限制也很少。
        视图是存储在数据库中的查询的sql语句,它主要出于两种原因:安全原因,视图可以隐藏一些数据,如:社会保险基金表,可以用视图只显示姓名,地址,而不显示社会保险号和工资数等,另一原因是可使复杂的查询易于理解和使用。
    -- 创建视图
    CREATE [OR REPLACE] [ALGORITHM = {UNDEFINED | MERGE | TEMPTABLE}] VIEW view_name [(column_list)] AS select_statement
        - 视图名必须唯一,同时不能与表重名。
        - 视图可以使用select语句查询到的列名,也可以自己指定相应的列名。
        - 可以指定视图执行的算法,通过ALGORITHM指定。
        - column_list如果存在,则数目必须等于SELECT语句检索的列数
    -- 查看结构
        SHOW CREATE VIEW view_name
    -- 删除视图
        - 删除视图后,数据依然存在。
        - 可同时删除多个视图。
        DROP VIEW [IF EXISTS] view_name ...
    -- 修改视图结构
        - 一般不修改视图,因为不是所有的更新视图都会映射到表上。
        ALTER VIEW view_name [(column_list)] AS select_statement
    -- 视图作用
        1. 简化业务逻辑
        2. 对客户端隐藏真实的表结构
    -- 视图算法(ALGORITHM)
        MERGE       合并
            将视图的查询语句,与外部查询需要先合并再执行!
        TEMPTABLE   临时表
            将视图执行完毕后,形成临时表,再做外层查询!
        UNDEFINED   未定义(默认),指的是MySQL自主去选择相应的算法。
    ```
    
    ### 事务(transaction) 
    
    ```mysql
    事务是指逻辑上的一组操作,组成这组操作的各个单元,要不全成功要不全失败。
        - 支持连续SQL的集体成功或集体撤销。
        - 事务是数据库在数据完整性方面的一个功能。
        - 需要利用 InnoDB 或 BDB 存储引擎,对自动提交的特性支持完成。
        - InnoDB被称为事务安全型引擎。
    -- 事务开启
        START TRANSACTION; 或者 BEGIN;
        开启事务后,所有被执行的SQL语句均被认作当前事务内的SQL语句。
    -- 事务提交
        COMMIT;
    -- 事务回滚
        ROLLBACK;
        如果部分操作发生问题,映射到事务开启前。
    -- 事务的特性
        1. 原子性(Atomicity)
            事务是一个不可分割的工作单位,事务中的操作要么都发生,要么都不发生。
        2. 一致性(Consistency)
            事务前后数据的完整性必须保持一致。
            - 事务开始和结束时,外部数据一致
            - 在整个事务过程中,操作是连续的
        3. 隔离性(Isolation)
            多个用户并发访问数据库时,一个用户的事务不能被其它用户的事物所干扰,多个并发事务之间的数据要相互隔离。
        4. 持久性(Durability)
            一个事务一旦被提交,它对数据库中的数据改变就是永久性的。
    -- 事务的实现
        1. 要求是事务支持的表类型
        2. 执行一组相关的操作前开启事务
        3. 整组操作完成后,都成功,则提交;如果存在失败,选择回滚,则会回到事务开始的备份点。
    -- 事务的原理
        利用InnoDB的自动提交(autocommit)特性完成。
        普通的MySQL执行语句后,当前的数据提交操作均可被其他客户端可见。
        而事务是暂时关闭“自动提交”机制,需要commit提交持久化数据操作。
    -- 注意
        1. 数据定义语言(DDL)语句不能被回滚,比如创建或取消数据库的语句,和创建、取消或更改表或存储的子程序的语句。
        2. 事务不能被嵌套
    -- 保存点
        SAVEPOINT 保存点名称 -- 设置一个事务保存点
        ROLLBACK TO SAVEPOINT 保存点名称 -- 回滚到保存点
        RELEASE SAVEPOINT 保存点名称 -- 删除保存点
    -- InnoDB自动提交特性设置
        SET autocommit = 0|1;   0表示关闭自动提交,1表示开启自动提交。
        - 如果关闭了,那普通操作的结果对其他客户端也不可见,需要commit提交后才能持久化数据操作。
        - 也可以关闭自动提交来开启事务。但与START TRANSACTION不同的是,
            SET autocommit是永久改变服务器的设置,直到下次再次修改该设置。(针对当前连接)
            而START TRANSACTION记录开启前的状态,而一旦事务提交或回滚后就需要再次开启事务。(针对当前事务)
    
    ```
    
    ### 锁表
    
    ```mysql
    /* 锁表 */
    表锁定只用于防止其它客户端进行不正当地读取和写入
    MyISAM 支持表锁,InnoDB 支持行锁
    -- 锁定
        LOCK TABLES tbl_name [AS alias]
    -- 解锁
        UNLOCK TABLES
    ```
    
    ### 触发器
    
    ```mysql
    /* 触发器 */ ------------------
        触发程序是与表有关的命名数据库对象,当该表出现特定事件时,将激活该对象
        监听:记录的增加、修改、删除。
    -- 创建触发器
    CREATE TRIGGER trigger_name trigger_time trigger_event ON tbl_name FOR EACH ROW trigger_stmt
        参数:
        trigger_time是触发程序的动作时间。它可以是 before 或 after,以指明触发程序是在激活它的语句之前或之后触发。
        trigger_event指明了激活触发程序的语句的类型
            INSERT:将新行插入表时激活触发程序
            UPDATE:更改某一行时激活触发程序
            DELETE:从表中删除某一行时激活触发程序
        tbl_name:监听的表,必须是永久性的表,不能将触发程序与TEMPORARY表或视图关联起来。
        trigger_stmt:当触发程序激活时执行的语句。执行多个语句,可使用BEGIN...END复合语句结构
    -- 删除
    DROP TRIGGER [schema_name.]trigger_name
    可以使用old和new代替旧的和新的数据
        更新操作,更新前是old,更新后是new.
        删除操作,只有old.
        增加操作,只有new.
    -- 注意
        1. 对于具有相同触发程序动作时间和事件的给定表,不能有两个触发程序。
    -- 字符连接函数
    concat(str1,str2,...])
    concat_ws(separator,str1,str2,...)
    -- 分支语句
    if 条件 then
        执行语句
    elseif 条件 then
        执行语句
    else
        执行语句
    end if;
    -- 修改最外层语句结束符
    delimiter 自定义结束符号
        SQL语句
    自定义结束符号
    delimiter ;     -- 修改回原来的分号
    -- 语句块包裹
    begin
        语句块
    end
    -- 特殊的执行
    1. 只要添加记录,就会触发程序。
    2. Insert into on duplicate key update 语法会触发:
        如果没有重复记录,会触发 before insert, after insert;
        如果有重复记录并更新,会触发 before insert, before update, after update;
        如果有重复记录但是没有发生更新,则触发 before insert, before update
    3. Replace 语法 如果有记录,则执行 before insert, before delete, after delete, after insert
    ```
    
    ### SQL编程
    
    ```mysql
    /* SQL编程 */ ------------------
    --// 局部变量 ----------
    -- 变量声明
        declare var_name[,...] type [default value]
        这个语句被用来声明局部变量。要给变量提供一个默认值,请包含一个default子句。值可以被指定为一个表达式,不需要为一个常数。如果没有default子句,初始值为null。
    -- 赋值
        使用 set 和 select into 语句为变量赋值。
        - 注意:在函数内是可以使用全局变量(用户自定义的变量)
    --// 全局变量 ----------
    -- 定义、赋值
    set 语句可以定义并为变量赋值。
    set @var = value;
    也可以使用select into语句为变量初始化并赋值。这样要求select语句只能返回一行,但是可以是多个字段,就意味着同时为多个变量进行赋值,变量的数量需要与查询的列数一致。
    还可以把赋值语句看作一个表达式,通过select执行完成。此时为了避免=被当作关系运算符看待,使用:=代替。(set语句可以使用= 和 :=)。
    select @var:=20;
    select @v1:=id, @v2=name from t1 limit 1;
    select * from tbl_name where @var:=30;
    select into 可以将表中查询获得的数据赋给变量。
        -| select max(height) into @max_height from tb;
    -- 自定义变量名
    为了避免select语句中,用户自定义的变量与系统标识符(通常是字段名)冲突,用户自定义变量在变量名前使用@作为开始符号。
    @var=10;
        - 变量被定义后,在整个会话周期都有效(登录到退出)
    --// 控制结构 ----------
    -- if语句
    if search_condition then
        statement_list   
    [elseif search_condition then
        statement_list]
    ...
    [else
        statement_list]
    end if;
    -- case语句
    CASE value WHEN [compare-value] THEN result
    [WHEN [compare-value] THEN result ...]
    [ELSE result]
    END
    -- while循环
    [begin_label:] while search_condition do
        statement_list
    end while [end_label];
    - 如果需要在循环内提前终止 while循环,则需要使用标签;标签需要成对出现。
        -- 退出循环
            退出整个循环 leave
            退出当前循环 iterate
            通过退出的标签决定退出哪个循环
    --// 内置函数 ----------
    -- 数值函数
    abs(x)          -- 绝对值 abs(-10.9) = 10
    format(x, d)    -- 格式化千分位数值 format(1234567.456, 2) = 1,234,567.46
    ceil(x)         -- 向上取整 ceil(10.1) = 11
    floor(x)        -- 向下取整 floor (10.1) = 10
    round(x)        -- 四舍五入去整
    mod(m, n)       -- m%n m mod n 求余 10%3=1
    pi()            -- 获得圆周率
    pow(m, n)       -- m^n
    sqrt(x)         -- 算术平方根
    rand()          -- 随机数
    truncate(x, d)  -- 截取d位小数
    -- 时间日期函数
    now(), current_timestamp();     -- 当前日期时间
    current_date();                 -- 当前日期
    current_time();                 -- 当前时间
    date('yyyy-mm-dd hh:ii:ss');    -- 获取日期部分
    time('yyyy-mm-dd hh:ii:ss');    -- 获取时间部分
    date_format('yyyy-mm-dd hh:ii:ss', '%d %y %a %d %m %b %j'); -- 格式化时间
    unix_timestamp();               -- 获得unix时间戳
    from_unixtime();                -- 从时间戳获得时间
    -- 字符串函数
    length(string)          -- string长度,字节
    char_length(string)     -- string的字符个数
    substring(str, position [,length])      -- 从str的position开始,取length个字符
    replace(str ,search_str ,replace_str)   -- 在str中用replace_str替换search_str
    instr(string ,substring)    -- 返回substring首次在string中出现的位置
    concat(string [,...])   -- 连接字串
    charset(str)            -- 返回字串字符集
    lcase(string)           -- 转换成小写
    left(string, length)    -- 从string2中的左边起取length个字符
    load_file(file_name)    -- 从文件读取内容
    locate(substring, string [,start_position]) -- 同instr,但可指定开始位置
    lpad(string, length, pad)   -- 重复用pad加在string开头,直到字串长度为length
    ltrim(string)           -- 去除前端空格
    repeat(string, count)   -- 重复count次
    rpad(string, length, pad)   --在str后用pad补充,直到长度为length
    rtrim(string)           -- 去除后端空格
    strcmp(string1 ,string2)    -- 逐字符比较两字串大小
    -- 流程函数
    case when [condition] then result [when [condition] then result ...] [else result] end   多分支
    if(expr1,expr2,expr3)  双分支。
    -- 聚合函数
    count()
    sum();
    max();
    min();
    avg();
    group_concat()
    -- 其他常用函数
    md5();
    default();
    --// 存储函数,自定义函数 ----------
    -- 新建
        CREATE FUNCTION function_name (参数列表) RETURNS 返回值类型
            函数体
        - 函数名,应该合法的标识符,并且不应该与已有的关键字冲突。
        - 一个函数应该属于某个数据库,可以使用db_name.funciton_name的形式执行当前函数所属数据库,否则为当前数据库。
        - 参数部分,由"参数名"和"参数类型"组成。多个参数用逗号隔开。
        - 函数体由多条可用的mysql语句,流程控制,变量声明等语句构成。
        - 多条语句应该使用 begin...end 语句块包含。
        - 一定要有 return 返回值语句。
    -- 删除
        DROP FUNCTION [IF EXISTS] function_name;
    -- 查看
        SHOW FUNCTION STATUS LIKE 'partten'
        SHOW CREATE FUNCTION function_name;
    -- 修改
        ALTER FUNCTION function_name 函数选项
    --// 存储过程,自定义功能 ----------
    -- 定义
    存储存储过程 是一段代码(过程),存储在数据库中的sql组成。
    一个存储过程通常用于完成一段业务逻辑,例如报名,交班费,订单入库等。
    而一个函数通常专注与某个功能,视为其他程序服务的,需要在其他语句中调用函数才可以,而存储过程不能被其他调用,是自己执行 通过call执行。
    -- 创建
    CREATE PROCEDURE sp_name (参数列表)
        过程体
    参数列表:不同于函数的参数列表,需要指明参数类型
    IN,表示输入型
    OUT,表示输出型
    INOUT,表示混合型
    注意,没有返回值。
    ```
    
    ### 存储过程
    
    ```mysql
    /* 存储过程 */ ------------------
    存储过程是一段可执行性代码的集合。相比函数,更偏向于业务逻辑。
    调用:CALL 过程名
    -- 注意
    - 没有返回值。
    - 只能单独调用,不可夹杂在其他语句中
    -- 参数
    IN|OUT|INOUT 参数名 数据类型
    IN      输入:在调用过程中,将数据输入到过程体内部的参数
    OUT     输出:在调用过程中,将过程体处理完的结果返回到客户端
    INOUT   输入输出:既可输入,也可输出
    -- 语法
    CREATE PROCEDURE 过程名 (参数列表)
    BEGIN
        过程体
    END
    ```
    
    ### 用户和权限管理
    
    ```mysql
    /* 用户和权限管理 */ ------------------
    -- root密码重置
    1. 停止MySQL服务
    2.  [Linux] /usr/local/mysql/bin/safe_mysqld --skip-grant-tables &
        [Windows] mysqld --skip-grant-tables
    3. use mysql;
    4. UPDATE `user` SET PASSWORD=PASSWORD("密码") WHERE `user` = "root";
    5. FLUSH PRIVILEGES;
    用户信息表:mysql.user
    -- 刷新权限
    FLUSH PRIVILEGES;
    -- 增加用户
    CREATE USER 用户名 IDENTIFIED BY [PASSWORD] 密码(字符串)
        - 必须拥有mysql数据库的全局CREATE USER权限,或拥有INSERT权限。
        - 只能创建用户,不能赋予权限。
        - 用户名,注意引号:如 'user_name'@'192.168.1.1'
        - 密码也需引号,纯数字密码也要加引号
        - 要在纯文本中指定密码,需忽略PASSWORD关键词。要把密码指定为由PASSWORD()函数返回的混编值,需包含关键字PASSWORD
    -- 重命名用户
    RENAME USER old_user TO new_user
    -- 设置密码
    SET PASSWORD = PASSWORD('密码')  -- 为当前用户设置密码
    SET PASSWORD FOR 用户名 = PASSWORD('密码') -- 为指定用户设置密码
    -- 删除用户
    DROP USER 用户名
    -- 分配权限/添加用户
    GRANT 权限列表 ON 表名 TO 用户名 [IDENTIFIED BY [PASSWORD] 'password']
        - all privileges 表示所有权限
        - *.* 表示所有库的所有表
        - 库名.表名 表示某库下面的某表
        GRANT ALL PRIVILEGES ON `pms`.* TO 'pms'@'%' IDENTIFIED BY 'pms0817';
    -- 查看权限
    SHOW GRANTS FOR 用户名
        -- 查看当前用户权限
        SHOW GRANTS; 或 SHOW GRANTS FOR CURRENT_USER; 或 SHOW GRANTS FOR CURRENT_USER();
    -- 撤消权限
    REVOKE 权限列表 ON 表名 FROM 用户名
    REVOKE ALL PRIVILEGES, GRANT OPTION FROM 用户名   -- 撤销所有权限
    -- 权限层级
    -- 要使用GRANT或REVOKE,您必须拥有GRANT OPTION权限,并且您必须用于您正在授予或撤销的权限。
    全局层级:全局权限适用于一个给定服务器中的所有数据库,mysql.user
        GRANT ALL ON *.*和 REVOKE ALL ON *.*只授予和撤销全局权限。
    数据库层级:数据库权限适用于一个给定数据库中的所有目标,mysql.db, mysql.host
        GRANT ALL ON db_name.*和REVOKE ALL ON db_name.*只授予和撤销数据库权限。
    表层级:表权限适用于一个给定表中的所有列,mysql.talbes_priv
        GRANT ALL ON db_name.tbl_name和REVOKE ALL ON db_name.tbl_name只授予和撤销表权限。
    列层级:列权限适用于一个给定表中的单一列,mysql.columns_priv
        当使用REVOKE时,您必须指定与被授权列相同的列。
    -- 权限列表
    ALL [PRIVILEGES]    -- 设置除GRANT OPTION之外的所有简单权限
    ALTER   -- 允许使用ALTER TABLE
    ALTER ROUTINE   -- 更改或取消已存储的子程序
    CREATE  -- 允许使用CREATE TABLE
    CREATE ROUTINE  -- 创建已存储的子程序
    CREATE TEMPORARY TABLES     -- 允许使用CREATE TEMPORARY TABLE
    CREATE USER     -- 允许使用CREATE USER, DROP USER, RENAME USER和REVOKE ALL PRIVILEGES。
    CREATE VIEW     -- 允许使用CREATE VIEW
    DELETE  -- 允许使用DELETE
    DROP    -- 允许使用DROP TABLE
    EXECUTE     -- 允许用户运行已存储的子程序
    FILE    -- 允许使用SELECT...INTO OUTFILE和LOAD DATA INFILE
    INDEX   -- 允许使用CREATE INDEX和DROP INDEX
    INSERT  -- 允许使用INSERT
    LOCK TABLES     -- 允许对您拥有SELECT权限的表使用LOCK TABLES
    PROCESS     -- 允许使用SHOW FULL PROCESSLIST
    REFERENCES  -- 未被实施
    RELOAD  -- 允许使用FLUSH
    REPLICATION CLIENT  -- 允许用户询问从属服务器或主服务器的地址
    REPLICATION SLAVE   -- 用于复制型从属服务器(从主服务器中读取二进制日志事件)
    SELECT  -- 允许使用SELECT
    SHOW DATABASES  -- 显示所有数据库
    SHOW VIEW   -- 允许使用SHOW CREATE VIEW
    SHUTDOWN    -- 允许使用mysqladmin shutdown
    SUPER   -- 允许使用CHANGE MASTER, KILL, PURGE MASTER LOGS和SET GLOBAL语句,mysqladmin debug命令;允许您连接(一次),即使已达到max_connections。
    UPDATE  -- 允许使用UPDATE
    USAGE   -- “无权限”的同义词
    GRANT OPTION    -- 允许授予权限
    ```
    
    ### 表维护
    
    ```mysql
    /* 表维护 */
    -- 分析和存储表的关键字分布
    ANALYZE [LOCAL | NO_WRITE_TO_BINLOG] TABLE 表名 ...
    -- 检查一个或多个表是否有错误
    CHECK TABLE tbl_name [, tbl_name] ... [option] ...
    option = {QUICK | FAST | MEDIUM | EXTENDED | CHANGED}
    -- 整理数据文件的碎片
    OPTIMIZE [LOCAL | NO_WRITE_TO_BINLOG] TABLE tbl_name [, tbl_name] ...
    ```
    
    ### 杂项
    
    ```mysql
    /* 杂项 */ ------------------
    1. 可用反引号(`)为标识符(库名、表名、字段名、索引、别名)包裹,以避免与关键字重名!中文也可以作为标识符!
    2. 每个库目录存在一个保存当前数据库的选项文件db.opt。
    3. 注释:
        单行注释 # 注释内容
        多行注释 /* 注释内容 */
        单行注释 -- 注释内容     (标准SQL注释风格,要求双破折号后加一空格符(空格、TAB、换行等))
    4. 模式通配符:
        _   任意单个字符
        %   任意多个字符,甚至包括零字符
        单引号需要进行转义 \'
    5. CMD命令行内的语句结束符可以为 ";", "\G", "\g",仅影响显示结果。其他地方还是用分号结束。delimiter 可修改当前对话的语句结束符。
    6. SQL对大小写不敏感
    7. 清除已有语句:\c
    ```
    
    
    本文来自[木木匠](https://github.com/kinglaw1204)投稿。
    
    <!-- TOC -->
    
    - [一 MySQL 基础架构分析](#一-mysql-基础架构分析)
        - [1.1 MySQL 基本架构概览](#11-mysql-基本架构概览)
        - [1.2 Server 层基本组件介绍](#12-server-层基本组件介绍)
            - [1) 连接器](#1-连接器)
            - [2) 查询缓存(MySQL 8.0 版本后移除)](#2-查询缓存mysql-80-版本后移除)
            - [3) 分析器](#3-分析器)
            - [4) 优化器](#4-优化器)
            - [5) 执行器](#5-执行器)
    - [二 语句分析](#二-语句分析)
        - [2.1 查询语句](#21-查询语句)
        - [2.2 更新语句](#22-更新语句)
    - [三 总结](#三-总结)
    - [四 参考](#四-参考)
    
    <!-- /TOC -->
    
    本篇文章会分析下一个 sql 语句在 MySQL 中的执行流程,包括 sql 的查询在 MySQL 内部会怎么流转,sql 语句的更新是怎么完成的。
    
    在分析之前我会先带着你看看 MySQL 的基础架构,知道了 MySQL 由那些组件组成已经这些组件的作用是什么,可以帮助我们理解和解决这些问题。
    
    ## 一 MySQL 基础架构分析
    
    ### 1.1 MySQL 基本架构概览
    
    下图是 MySQL  的一个简要架构图,从下图你可以很清晰的看到用户的 SQL 语句在 MySQL 内部是如何执行的。
    
    先简单介绍一下下图涉及的一些组件的基本作用帮助大家理解这幅图,在 1.2 节中会详细介绍到这些组件的作用。
    
    - **连接器:** 身份认证和权限相关(登录 MySQL 的时候)。
    - **查询缓存:**  执行查询语句的时候,会先查询缓存(MySQL 8.0 版本后移除,因为这个功能不太实用)。
    - **分析器:**  没有命中缓存的话,SQL 语句就会经过分析器,分析器说白了就是要先看你的 SQL 语句要干嘛,再检查你的 SQL 语句语法是否正确。
    - **优化器:**  按照 MySQL 认为最优的方案去执行。
    - **执行器:** 执行语句,然后从存储引擎返回数据。
    
    ![](https://user-gold-cdn.xitu.io/2019/3/23/169a8bc60a083849?w=950&h=1062&f=jpeg&s=38189)
    
    简单来说 MySQL  主要分为 Server 层和存储引擎层:
    
    - **Server 层**:主要包括连接器、查询缓存、分析器、优化器、执行器等,所有跨存储引擎的功能都在这一层实现,比如存储过程、触发器、视图,函数等,还有一个通用的日志模块 binglog 日志模块。
    - **存储引擎**: 主要负责数据的存储和读取,采用可以替换的插件式架构,支持 InnoDB、MyISAM、Memory 等多个存储引擎,其中 InnoDB 引擎有自有的日志模块 redolog 模块。**现在最常用的存储引擎是 InnoDB,它从 MySQL 5.5.5 版本开始就被当做默认存储引擎了。**
    
    ### 1.2 Server 层基本组件介绍
    
    #### 1) 连接器
    
    连接器主要和身份认证和权限相关的功能相关,就好比一个级别很高的门卫一样。
    
    主要负责用户登录数据库,进行用户的身份认证,包括校验账户密码,权限等操作,如果用户账户密码已通过,连接器会到权限表中查询该用户的所有权限,之后在这个连接里的权限逻辑判断都是会依赖此时读取到的权限数据,也就是说,后续只要这个连接不断开,即时管理员修改了该用户的权限,该用户也是不受影响的。
    
    #### 2) 查询缓存(MySQL 8.0 版本后移除)
    
    查询缓存主要用来缓存我们所执行的 SELECT 语句以及该语句的结果集。
    
    连接建立后,执行查询语句的时候,会先查询缓存,MySQL 会先校验这个 sql 是否执行过,以 Key-Value 的形式缓存在内存中,Key 是查询预计,Value 是结果集。如果缓存 key 被命中,就会直接返回给客户端,如果没有命中,就会执行后续的操作,完成后也会把结果缓存起来,方便下一次调用。当然在真正执行缓存查询的时候还是会校验用户的权限,是否有该表的查询条件。
    
    MySQL 查询不建议使用缓存,因为查询缓存失效在实际业务场景中可能会非常频繁,假如你对一个表更新的话,这个表上的所有的查询缓存都会被清空。对于不经常更新的数据来说,使用缓存还是可以的。
    
    所以,一般在大多数情况下我们都是不推荐去使用查询缓存的。
    
    MySQL 8.0 版本后删除了缓存的功能,官方也是认为该功能在实际的应用场景比较少,所以干脆直接删掉了。
    
    #### 3) 分析器
    
    MySQL 没有命中缓存,那么就会进入分析器,分析器主要是用来分析 SQL 语句是来干嘛的,分析器也会分为几步:
    
    **第一步,词法分析**,一条 SQL 语句有多个字符串组成,首先要提取关键字,比如 select,提出查询的表,提出字段名,提出查询条件等等。做完这些操作后,就会进入第二步。
    
    **第二步,语法分析**,主要就是判断你输入的 sql 是否正确,是否符合 MySQL 的语法。
    
    完成这 2 步之后,MySQL 就准备开始执行了,但是如何执行,怎么执行是最好的结果呢?这个时候就需要优化器上场了。
    
    #### 4) 优化器 
    
    优化器的作用就是它认为的最优的执行方案去执行(有时候可能也不是最优,这篇文章涉及对这部分知识的深入讲解),比如多个索引的时候该如何选择索引,多表查询的时候如何选择关联顺序等。
    
    可以说,经过了优化器之后可以说这个语句具体该如何执行就已经定下来。
    
    #### 5) 执行器
    
    当选择了执行方案后,MySQL 就准备开始执行了,首先执行前会校验该用户有没有权限,如果没有权限,就会返回错误信息,如果有权限,就会去调用引擎的接口,返回接口执行的结果。
    
    ## 二 语句分析 
    
    ### 2.1 查询语句
    
    说了以上这么多,那么究竟一条 sql 语句是如何执行的呢?其实我们的 sql 可以分为两种,一种是查询,一种是更新(增加,更新,删除)。我们先分析下查询语句,语句如下:
    
    ```sql
    select * from tb_student  A where A.age='18' and A.name=' 张三 ';
    ```
    
    结合上面的说明,我们分析下这个语句的执行流程:
    
    * 先检查该语句是否有权限,如果没有权限,直接返回错误信息,如果有权限,在 MySQL8.0 版本以前,会先查询缓存,以这条 sql 语句为 key 在内存中查询是否有结果,如果有直接缓存,如果没有,执行下一步。
    * 通过分析器进行词法分析,提取 sql 语句的关键元素,比如提取上面这个语句是查询 select,提取需要查询的表名为 tb_student,需要查询所有的列,查询条件是这个表的 id='1'。然后判断这个 sql 语句是否有语法错误,比如关键词是否正确等等,如果检查没问题就执行下一步。
    * 接下来就是优化器进行确定执行方案,上面的 sql 语句,可以有两种执行方案:
      
            a.先查询学生表中姓名为“张三”的学生,然后判断是否年龄是 18。
            b.先找出学生中年龄 18 岁的学生,然后再查询姓名为“张三”的学生。
        那么优化器根据自己的优化算法进行选择执行效率最好的一个方案(优化器认为,有时候不一定最好)。那么确认了执行计划后就准备开始执行了。
    
    * 进行权限校验,如果没有权限就会返回错误信息,如果有权限就会调用数据库引擎接口,返回引擎的执行结果。
    
    ### 2.2 更新语句
    
    以上就是一条查询 sql 的执行流程,那么接下来我们看看一条更新语句如何执行的呢?sql 语句如下:
    
    ```
    update tb_student A set A.age='19' where A.name=' 张三 ';
    ```
    我们来给张三修改下年龄,在实际数据库肯定不会设置年龄这个字段的,不然要被技术负责人打的。其实条语句也基本上会沿着上一个查询的流程走,只不过执行更新的时候肯定要记录日志啦,这就会引入日志模块了,MySQL 自带的日志模块式 **binlog(归档日志)** ,所有的存储引擎都可以使用,我们常用的 InnoDB 引擎还自带了一个日志模块 **redo log(重做日志)**,我们就以 InnoDB 模式下来探讨这个语句的执行流程。流程如下:
    
    * 先查询到张三这一条数据,如果有缓存,也是会用到缓存。
    * 然后拿到查询的语句,把 age 改为 19,然后调用引擎 API 接口,写入这一行数据,InnoDB 引擎把数据保存在内存中,同时记录 redo log,此时 redo log 进入 prepare 状态,然后告诉执行器,执行完成了,随时可以提交。
    * 执行器收到通知后记录 binlog,然后调用引擎接口,提交 redo log 为提交状态。
    * 更新完成。
    
    **这里肯定有同学会问,为什么要用两个日志模块,用一个日志模块不行吗?**
    
    这是因为最开始 MySQL 并没与 InnoDB 引擎( InnoDB 引擎是其他公司以插件形式插入 MySQL 的) ,MySQL 自带的引擎是 MyISAM,但是我们知道 redo log 是 InnoDB 引擎特有的,其他存储引擎都没有,这就导致会没有 crash-safe 的能力(crash-safe 的能力即使数据库发生异常重启,之前提交的记录都不会丢失),binlog 日志只能用来归档。
    
    并不是说只用一个日志模块不可以,只是 InnoDB 引擎就是通过 redo log 来支持事务的。那么,又会有同学问,我用两个日志模块,但是不要这么复杂行不行,为什么 redo log 要引入 prepare 预提交状态?这里我们用反证法来说明下为什么要这么做?
    
    * **先写 redo log 直接提交,然后写 binlog**,假设写完 redo log 后,机器挂了,binlog 日志没有被写入,那么机器重启后,这台机器会通过 redo log 恢复数据,但是这个时候 bingog 并没有记录该数据,后续进行机器备份的时候,就会丢失这一条数据,同时主从同步也会丢失这一条数据。
    * **先写 binlog,然后写 redo log**,假设写完了 binlog,机器异常重启了,由于没有 redo log,本机是无法恢复这一条记录的,但是 binlog 又有记录,那么和上面同样的道理,就会产生数据不一致的情况。
    
    如果采用 redo log 两阶段提交的方式就不一样了,写完 binglog 后,然后再提交 redo log 就会防止出现上述的问题,从而保证了数据的一致性。那么问题来了,有没有一个极端的情况呢?假设 redo log 处于预提交状态,binglog 也已经写完了,这个时候发生了异常重启会怎么样呢?
    这个就要依赖于 MySQL 的处理机制了,MySQL 的处理过程如下:
    
    * 判断 redo log 是否完整,如果判断是完整的,就立即提交。
    * 如果 redo log 只是预提交但不是 commit 状态,这个时候就会去判断 binlog 是否完整,如果完整就提交 redo log, 不完整就回滚事务。
    
    这样就解决了数据一致性的问题。
    
    ## 三 总结
    
    * MySQL 主要分为 Server 层和引擎层,Server 层主要包括连接器、查询缓存、分析器、优化器、执行器,同时还有一个日志模块(binlog),这个日志模块所有执行引擎都可以共用,redolog 只有 InnoDB 有。
    * 引擎层是插件式的,目前主要包括,MyISAM,InnoDB,Memory 等。
    * 查询语句的执行流程如下:权限校验(如果命中缓存)---》查询缓存---》分析器---》优化器---》权限校验---》执行器---》引擎
    * 更新语句执行流程如下:分析器----》权限校验----》执行器---》引擎---redo log(prepare 状态---》binlog---》redo log(commit状态)
    
    ## 四 参考
    
    * 《MySQL 实战45讲》
    * MySQL 5.6参考手册:<https://dev.MySQL.com/doc/refman/5.6/en/>
    
    > 本文由 [SnailClimb](https://github.com/Snailclimb) 和 [BugSpeak](https://github.com/BugSpeak) 共同完成。
    <!-- TOC -->
    
    - [事务隔离级别(图文详解)](#事务隔离级别图文详解)
        - [什么是事务?](#什么是事务)
        - [事务的特性(ACID)](#事务的特性acid)
        - [并发事务带来的问题](#并发事务带来的问题)
        - [事务隔离级别](#事务隔离级别)
        - [实际情况演示](#实际情况演示)
            - [脏读(读未提交)](#脏读读未提交)
            - [避免脏读(读已提交)](#避免脏读读已提交)
            - [不可重复读](#不可重复读)
            - [可重复读](#可重复读)
            - [防止幻读(可重复读)](#防止幻读可重复读)
        - [参考](#参考)
    
    <!-- /TOC -->
    
    ## 事务隔离级别(图文详解)
    
    ### 什么是事务?
    
    事务是逻辑上的一组操作,要么都执行,要么都不执行。
    
    事务最经典也经常被拿出来说例子就是转账了。假如小明要给小红转账1000元,这个转账会涉及到两个关键操作就是:将小明的余额减少1000元,将小红的余额增加1000元。万一在这两个操作之间突然出现错误比如银行系统崩溃,导致小明余额减少而小红的余额没有增加,这样就不对了。事务就是保证这两个关键操作要么都成功,要么都要失败。
    
    ### 事务的特性(ACID)
    
    ![事务的特性](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/事务特性.png)
    
    
    1.  **原子性:** 事务是最小的执行单位,不允许分割。事务的原子性确保动作要么全部完成,要么完全不起作用;
    2.  **一致性:** 执行事务前后,数据保持一致,例如转账业务中,无论事务是否成功,转账者和收款人的总额应该是不变的;
    3.  **隔离性:** 并发访问数据库时,一个用户的事务不被其他事务所干扰,各并发事务之间数据库是独立的;
    4.  **持久性:** 一个事务被提交之后。它对数据库中数据的改变是持久的,即使数据库发生故障也不应该对其有任何影响。
    
    ### 并发事务带来的问题
    
    在典型的应用程序中,多个事务并发运行,经常会操作相同的数据来完成各自的任务(多个用户对统一数据进行操作)。并发虽然是必须的,但可能会导致以下的问题。
    
    - **脏读(Dirty read):** 当一个事务正在访问数据并且对数据进行了修改,而这种修改还没有提交到数据库中,这时另外一个事务也访问了这个数据,然后使用了这个数据。因为这个数据是还没有提交的数据,那么另外一个事务读到的这个数据是“脏数据”,依据“脏数据”所做的操作可能是不正确的。
    - **丢失修改(Lost to modify):** 指在一个事务读取一个数据时,另外一个事务也访问了该数据,那么在第一个事务中修改了这个数据后,第二个事务也修改了这个数据。这样第一个事务内的修改结果就被丢失,因此称为丢失修改。    例如:事务1读取某表中的数据A=20,事务2也读取A=20,事务1修改A=A-1,事务2也修改A=A-1,最终结果A=19,事务1的修改被丢失。
    - **不可重复读(Unrepeatableread):** 指在一个事务内多次读同一数据。在这个事务还没有结束时,另一个事务也访问该数据。那么,在第一个事务中的两次读数据之间,由于第二个事务的修改导致第一个事务两次读取的数据可能不太一样。这就发生了在一个事务内两次读到的数据是不一样的情况,因此称为不可重复读。
    - **幻读(Phantom read):** 幻读与不可重复读类似。它发生在一个事务(T1)读取了几行数据,接着另一个并发事务(T2)插入了一些数据时。在随后的查询中,第一个事务(T1)就会发现多了一些原本不存在的记录,就好像发生了幻觉一样,所以称为幻读。
    
    **不可重复度和幻读区别:**
    
    不可重复读的重点是修改,幻读的重点在于新增或者删除。
    
    例1(同样的条件, 你读取过的数据, 再次读取出来发现值不一样了 ):事务1中的A先生读取自己的工资为     1000的操作还没完成,事务2中的B先生就修改了A的工资为2000,导        致A再读自己的工资时工资变为  2000;这就是不可重复读。
    
     例2(同样的条件, 第1次和第2次读出来的记录数不一样 ):假某工资单表中工资大于3000的有4人,事务1读取了所有工资大于3000的人,共查到4条记录,这时事务2 又插入了一条工资大于3000的记录,事务1再次读取时查到的记录就变为了5条,这样就导致了幻读。
    
    ### 事务隔离级别
    
    **SQL 标准定义了四个隔离级别:**
    
    - **READ-UNCOMMITTED(读取未提交):** 最低的隔离级别,允许读取尚未提交的数据变更,**可能会导致脏读、幻读或不可重复读**。
    - **READ-COMMITTED(读取已提交):** 允许读取并发事务已经提交的数据,**可以阻止脏读,但是幻读或不可重复读仍有可能发生**。
    - **REPEATABLE-READ(可重复读):**  对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,**可以阻止脏读和不可重复读,但幻读仍有可能发生**。
    - **SERIALIZABLE(可串行化):** 最高的隔离级别,完全服从ACID的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,**该级别可以防止脏读、不可重复读以及幻读**。
    
    ----
    
    | 隔离级别 | 脏读 | 不可重复读 | 幻影读 |
    | :---: | :---: | :---:| :---: |
    | READ-UNCOMMITTED | √ | √ | √ |
    | READ-COMMITTED | × | √ | √ |
    | REPEATABLE-READ | × | × | √ |
    | SERIALIZABLE | × | × | × |
    
    MySQL InnoDB 存储引擎的默认支持的隔离级别是 **REPEATABLE-READ(可重读)**。我们可以通过`SELECT @@tx_isolation;`命令来查看,MySQL 8.0 该命令改为`SELECT @@transaction_isolation;`
    
    ```sql
    mysql> SELECT @@tx_isolation;
    +-----------------+
    | @@tx_isolation  |
    +-----------------+
    | REPEATABLE-READ |
    +-----------------+
    ```
    
    这里需要注意的是:与 SQL 标准不同的地方在于InnoDB 存储引擎在 **REPEATABLE-READ(可重读)**事务隔离级别下使用的是Next-Key Lock 锁算法,因此可以避免幻读的产生,这与其他数据库系统(如 SQL Server)是不同的。所以说InnoDB 存储引擎的默认支持的隔离级别是 **REPEATABLE-READ(可重读)** 已经可以完全保证事务的隔离性要求,即达到了 SQL标准的**SERIALIZABLE(可串行化)**隔离级别。
    
    因为隔离级别越低,事务请求的锁越少,所以大部分数据库系统的隔离级别都是**READ-COMMITTED(读取提交内容):**,但是你要知道的是InnoDB 存储引擎默认使用 **REPEATABLE-READ(可重读)**并不会有任何性能损失。
    
    InnoDB 存储引擎在 **分布式事务** 的情况下一般会用到**SERIALIZABLE(可串行化)**隔离级别。
    
    ### 实际情况演示
    
    在下面我会使用 2 个命令行mysql ,模拟多线程(多事务)对同一份数据的脏读问题。
    
    MySQL 命令行的默认配置中事务都是自动提交的,即执行SQL语句后就会马上执行 COMMIT 操作。如果要显式地开启一个事务需要使用命令:`START TARNSACTION`。
    
    我们可以通过下面的命令来设置隔离级别。
    
    ```sql
    SET [SESSION|GLOBAL] TRANSACTION ISOLATION LEVEL [READ UNCOMMITTED|READ COMMITTED|REPEATABLE READ|SERIALIZABLE]
    ```
    
    我们再来看一下我们在下面实际操作中使用到的一些并发控制语句:
    
    - `START TARNSACTION` |`BEGIN`:显式地开启一个事务。
    - `COMMIT`:提交事务,使得对数据库做的所有修改成为永久性。
    - `ROLLBACK`:回滚会结束用户的事务,并撤销正在进行的所有未提交的修改。
    
    #### 脏读(读未提交)
    
    <div align="center">  
    <img src="https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-31-1脏读(读未提交)实例.jpg" width="800px"/>
    </div>
    
    #### 避免脏读(读已提交)
    
    <div align="center">  
    <img src="https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-31-2读已提交实例.jpg" width="800px"/>
    </div>
    
    #### 不可重复读
    
    还是刚才上面的读已提交的图,虽然避免了读未提交,但是却出现了,一个事务还没有结束,就发生了 不可重复读问题。
    
    <div align="center">  
    <img src="https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-32-1不可重复读实例.jpg"/>
    </div>
    
    #### 可重复读
    
    <div align="center">  
    <img src="https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-33-2可重复读.jpg"/>
    </div>
    
    #### 防止幻读(可重复读)
    
    <div align="center">  
    <img src="https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-33防止幻读(使用可重复读).jpg"/>
    </div>
    
    一个事务对数据库进行操作,这种操作的范围是数据库的全部行,然后第二个事务也在对这个数据库操作,这种操作可以是插入一行记录或删除一行记录,那么第一个是事务就会觉得自己出现了幻觉,怎么还有没有处理的记录呢? 或者 怎么多处理了一行记录呢?
    
    幻读和不可重复读有些相似之处 ,但是不可重复读的重点是修改,幻读的重点在于新增或者删除。
    
    ### 参考
    
    - 《MySQL技术内幕:InnoDB存储引擎》
    - <https://dev.mysql.com/doc/refman/5.7/en/>
    - [Mysql 锁:灵魂七拷问](https://tech.youzan.com/seven-questions-about-the-lock-of-mysql/)
    - [Innodb 中的事务隔离级别和锁的关系](https://tech.meituan.com/2014/08/20/innodb-lock.html)
    
    - 公众号和Github待发文章:[数据库:数据库连接池原理详解与自定义连接池实现](https://www.fangzhipeng.com/javainterview/2019/07/15/mysql-connector-pool.html)
    - [基于JDBC的数据库连接池技术研究与应用](http://blog.itpub.net/9403012/viewspace-111794/)
    - [数据库连接池技术详解](https://juejin.im/post/5b7944c6e51d4538c86cf195)
    
    数据库连接本质就是一个 socket 的连接。数据库服务端还要维护一些缓存和用户权限信息之类的 所以占用了一些内存
    
    连接池是维护的数据库连接的缓存,以便将来需要对数据库的请求时可以重用这些连接。为每个用户打开和维护数据库连接,尤其是对动态数据库驱动的网站应用程序的请求,既昂贵又浪费资源。**在连接池中,创建连接后,将其放置在池中,并再次使用它,因此不必建立新的连接。如果使用了所有连接,则会建立一个新连接并将其添加到池中。**连接池还减少了用户必须等待建立与数据库的连接的时间。
    
    操作过数据库的朋友应该都知道数据库连接池这个概念,它几乎每天都在和我们打交道,但是你真的了解 **数据库连接池** 吗?
    
    ### 没有数据库连接池之前
    
    我相信你一定听过这样一句话:**Java语言中,JDBC(Java DataBase Connection)是应用程序与数据库沟通的桥梁**。
    
    
    
    # 阿里巴巴Java开发手册数据库部分的一些最佳实践总结
    
    ## 模糊查询
    
    对于模糊查询阿里巴巴开发手册这样说到:
    
    > 【强制】页面搜索严禁左模糊或者全模糊,如果需要请走搜索引擎来解决。
    >
    > 说明:索引文件具有 B-Tree 的最左前缀匹配特性,如果左边的值未确定,那么无法使用此索引。
    
    ## 外键和级联
    
    对于外键和级联,阿里巴巴开发手册这样说到:
    
    >【强制】不得使用外键与级联,一切外键概念必须在应用层解决。
    >
    >说明:以学生和成绩的关系为例,学生表中的 student_id 是主键,那么成绩表中的 student_id 则为外键。如果更新学生表中的 student_id,同时触发成绩表中的 student_id 更新,即为级联更新。外键与级联更新适用于单机低并发,不适合分布式、高并发集群;级联更新是强阻塞,存在数据库更新风暴的风 险;外键影响数据库的插入速度
    
    为什么不要用外键呢?大部分人可能会这样回答:
    
    > 1. **增加了复杂性:** a.每次做DELETE 或者UPDATE都必须考虑外键约束,会导致开发的时候很痛苦,测试数据极为不方便;b.外键的主从关系是定的,假如那天需求有变化,数据库中的这个字段根本不需要和其他表有关联的话就会增加很多麻烦。
    > 2. **增加了额外工作**: 数据库需要增加维护外键的工作,比如当我们做一些涉及外键字段的增,删,更新操作之后,需要触发相关操作去检查,保证数据的的一致性和正确性,这样会不得不消耗资源;(个人觉得这个不是不用外键的原因,因为即使你不使用外键,你在应用层面也还是要保证的。所以,我觉得这个影响可以忽略不计。)
    > 3. 外键还会因为需要请求对其他表内部加锁而容易出现死锁情况;
    > 4. **对分不分表不友好** :因为分库分表下外键是无法生效的。
    > 5. ......
    
    我个人觉得上面这种回答不是特别的全面,只是说了外键存在的一个常见的问题。实际上,我们知道外键也是有很多好处的,比如:
    
    1. 保证了数据库数据的一致性和完整性;
    2. 级联操作方便,减轻了程序代码量;
    3. ......
    
    所以说,不要一股脑的就抛弃了外键这个概念,既然它存在就有它存在的道理,如果系统不涉及分不分表,并发量不是很高的情况还是可以考虑使用外键的。
    
    我个人是不太喜欢外键约束,比较喜欢在应用层去进行相关操作。
    
    ## 关于@Transactional注解
    
    对于`@Transactional`事务注解,阿里巴巴开发手册这样说到:
    
    >【参考】@Transactional事务不要滥用。事务会影响数据库的QPS,另外使用事务的地方需要考虑各方面的回滚方案,包括缓存回滚、搜索引擎回滚、消息补偿、统计修正等。
    
    点击关注[公众号](#公众号)及时获取笔主最新更新文章,并可免费领取本文档配套的《Java面试突击》以及Java工程师必备学习资源。
    
    <!-- TOC -->
    
    - [redis 简介](#redis-简介)
    - [为什么要用 redis/为什么要用缓存](#为什么要用-redis为什么要用缓存)
    - [为什么要用 redis 而不用 map/guava 做缓存?](#为什么要用-redis-而不用-mapguava-做缓存)
    - [redis 和 memcached 的区别](#redis-和-memcached-的区别)
    - [redis 常见数据结构以及使用场景分析](#redis-常见数据结构以及使用场景分析)
        - [1.String](#1string)
        - [2.Hash](#2hash)
        - [3.List](#3list)
        - [4.Set](#4set)
        - [5.Sorted Set](#5sorted-set)
    - [redis 设置过期时间](#redis-设置过期时间)
    - [redis 内存淘汰机制(MySQL里有2000w数据,Redis中只存20w的数据,如何保证Redis中的数据都是热点数据?)](#redis-内存淘汰机制mysql里有2000w数据redis中只存20w的数据如何保证redis中的数据都是热点数据)
    - [redis 持久化机制(怎么保证 redis 挂掉之后再重启数据可以进行恢复)](#redis-持久化机制怎么保证-redis-挂掉之后再重启数据可以进行恢复)
    - [redis 事务](#redis-事务)
    - [缓存雪崩和缓存穿透问题解决方案](#缓存雪崩和缓存穿透问题解决方案)
    - [如何解决 Redis 的并发竞争 Key 问题](#如何解决-redis-的并发竞争-key-问题)
    - [如何保证缓存与数据库双写时的数据一致性?](#如何保证缓存与数据库双写时的数据一致性)
    
    <!-- /TOC -->
    
    ### redis 简介
    
    简单来说 redis 就是一个数据库,不过与传统数据库不同的是 redis 的数据是存在内存中的,所以读写速度非常快,因此 redis 被广泛应用于缓存方向。另外,redis 也经常用来做分布式锁。redis 提供了多种数据类型来支持不同的业务场景。除此之外,redis 支持事务 、持久化、LUA脚本、LRU驱动事件、多种集群方案。 
    
    ### 为什么要用 redis/为什么要用缓存
    
    主要从“高性能”和“高并发”这两点来看待这个问题。
    
    **高性能:**
    
    假如用户第一次访问数据库中的某些数据。这个过程会比较慢,因为是从硬盘上读取的。将该用户访问的数据存在缓存中,这样下一次再访问这些数据的时候就可以直接从缓存中获取了。操作缓存就是直接操作内存,所以速度相当快。如果数据库中的对应数据改变的之后,同步改变缓存中相应的数据即可!
    
    ![](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-9-24/54316596.jpg)
    
    
    **高并发:**
    
    直接操作缓存能够承受的请求是远远大于直接访问数据库的,所以我们可以考虑把数据库中的部分数据转移到缓存中去,这样用户的一部分请求会直接到缓存这里而不用经过数据库。
    
    
    ![](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-9-24/85146760.jpg)
    
    
    ### 为什么要用 redis 而不用 map/guava 做缓存?
    
    
    >下面的内容来自 segmentfault 一位网友的提问,地址:https://segmentfault.com/q/1010000009106416
    
    缓存分为本地缓存和分布式缓存。以 Java 为例,使用自带的 map 或者 guava 实现的是本地缓存,最主要的特点是轻量以及快速,生命周期随着 jvm 的销毁而结束,并且在多实例的情况下,每个实例都需要各自保存一份缓存,缓存不具有一致性。
    
    使用 redis 或 memcached 之类的称为分布式缓存,在多实例的情况下,各实例共用一份缓存数据,缓存具有一致性。缺点是需要保持 redis 或  memcached服务的高可用,整个程序架构上较为复杂。
    
    ### redis 的线程模型
    
    > 参考地址:https://www.javazhiyin.com/22943.html
    
    redis 内部使用文件事件处理器 `file event handler`,这个文件事件处理器是单线程的,所以 redis 才叫做单线程的模型。它采用 IO 多路复用机制同时监听多个 socket,根据 socket 上的事件来选择对应的事件处理器进行处理。
    
    文件事件处理器的结构包含 4 个部分:
    
    - 多个 socket
    - IO 多路复用程序
    - 文件事件分派器
    - 事件处理器(连接应答处理器、命令请求处理器、命令回复处理器)
    
    多个 socket 可能会并发产生不同的操作,每个操作对应不同的文件事件,但是 IO 多路复用程序会监听多个 socket,会将 socket 产生的事件放入队列中排队,事件分派器每次从队列中取出一个事件,把该事件交给对应的事件处理器进行处理。
    
    
    ###  redis 和 memcached 的区别
    
    对于 redis 和 memcached 我总结了下面四点。现在公司一般都是用 redis 来实现缓存,而且 redis 自身也越来越强大了!
    
    1. **redis支持更丰富的数据类型(支持更复杂的应用场景)**:Redis不仅仅支持简单的k/v类型的数据,同时还提供list,set,zset,hash等数据结构的存储。memcache支持简单的数据类型,String。
    2. **Redis支持数据的持久化,可以将内存中的数据保持在磁盘中,重启的时候可以再次加载进行使用,而Memecache把数据全部存在内存之中。**
    3. **集群模式**:memcached没有原生的集群模式,需要依靠客户端来实现往集群中分片写入数据;但是 redis 目前是原生支持 cluster 模式的.
    4. **Memcached是多线程,非阻塞IO复用的网络模型;Redis使用单线程的多路 IO 复用模型。**
    
    
    > 来自网络上的一张图,这里分享给大家!
    
    ![redis 和 memcached 的区别](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-9-24/61603179.jpg)
    
    
    ### redis 常见数据结构以及使用场景分析
    
    #### 1.String
    
    > **常用命令:**  set,get,decr,incr,mget 等。
    
    
    String数据结构是简单的key-value类型,value其实不仅可以是String,也可以是数字。 
    常规key-value缓存应用; 
    常规计数:微博数,粉丝数等。
    
    #### 2.Hash
    > **常用命令:** hget,hset,hgetall 等。
    
    hash 是一个 string 类型的 field 和 value 的映射表,hash 特别适合用于存储对象,后续操作的时候,你可以直接仅仅修改这个对象中的某个字段的值。 比如我们可以 hash 数据结构来存储用户信息,商品信息等等。比如下面我就用 hash 类型存放了我本人的一些信息:
    
    ```
    key=JavaUser293847
    value={
      “id”: 1,
      “name”: “SnailClimb”,
      “age”: 22,
      “location”: “Wuhan, Hubei”
    }
    
    ```
    
    
    #### 3.List
    > **常用命令:** lpush,rpush,lpop,rpop,lrange等
    
    list 就是链表,Redis list 的应用场景非常多,也是Redis最重要的数据结构之一,比如微博的关注列表,粉丝列表,消息列表等功能都可以用Redis的 list 结构来实现。
    
    Redis list 的实现为一个双向链表,即可以支持反向查找和遍历,更方便操作,不过带来了部分额外的内存开销。
    
    另外可以通过 lrange 命令,就是从某个元素开始读取多少个元素,可以基于 list 实现分页查询,这个很棒的一个功能,基于 redis 实现简单的高性能分页,可以做类似微博那种下拉不断分页的东西(一页一页的往下走),性能高。
    
    #### 4.Set
    
    > **常用命令:**
    sadd,spop,smembers,sunion 等
    
    set 对外提供的功能与list类似是一个列表的功能,特殊之处在于 set 是可以自动排重的。
    
    当你需要存储一个列表数据,又不希望出现重复数据时,set是一个很好的选择,并且set提供了判断某个成员是否在一个set集合内的重要接口,这个也是list所不能提供的。可以基于 set 轻易实现交集、并集、差集的操作。
    
    比如:在微博应用中,可以将一个用户所有的关注人存在一个集合中,将其所有粉丝存在一个集合。Redis可以非常方便的实现如共同关注、共同粉丝、共同喜好等功能。这个过程也就是求交集的过程,具体命令如下:
    
    ```
    sinterstore key1 key2 key3     将交集存在key1内
    ```
    
    #### 5.Sorted Set
    > **常用命令:** zadd,zrange,zrem,zcard等
    
    
    和set相比,sorted set增加了一个权重参数score,使得集合中的元素能够按score进行有序排列。
    
    **举例:** 在直播系统中,实时排行信息包含直播间在线用户列表,各种礼物排行榜,弹幕消息(可以理解为按消息维度的消息排行榜)等信息,适合使用 Redis 中的 Sorted Set 结构进行存储。
    
    
    ### redis 设置过期时间
    
    Redis中有个设置时间过期的功能,即对存储在 redis 数据库中的值可以设置一个过期时间。作为一个缓存数据库,这是非常实用的。如我们一般项目中的 token 或者一些登录信息,尤其是短信验证码都是有时间限制的,按照传统的数据库处理方式,一般都是自己判断过期,这样无疑会严重影响项目性能。
    
    我们 set key 的时候,都可以给一个 expire time,就是过期时间,通过过期时间我们可以指定这个 key 可以存活的时间。
    
    如果假设你设置了一批 key 只能存活1个小时,那么接下来1小时后,redis是怎么对这批key进行删除的?
    
    **定期删除+惰性删除。**
    
    通过名字大概就能猜出这两个删除方式的意思了。
    
    - **定期删除**:redis默认是每隔 100ms 就**随机抽取**一些设置了过期时间的key,检查其是否过期,如果过期就删除。注意这里是随机抽取的。为什么要随机呢?你想一想假如 redis 存了几十万个 key ,每隔100ms就遍历所有的设置过期时间的 key 的话,就会给 CPU 带来很大的负载!
    - **惰性删除** :定期删除可能会导致很多过期 key 到了时间并没有被删除掉。所以就有了惰性删除。假如你的过期 key,靠定期删除没有被删除掉,还停留在内存里,除非你的系统去查一下那个 key,才会被redis给删除掉。这就是所谓的惰性删除,也是够懒的哈!
    
    
    但是仅仅通过设置过期时间还是有问题的。我们想一下:如果定期删除漏掉了很多过期 key,然后你也没及时去查,也就没走惰性删除,此时会怎么样?如果大量过期key堆积在内存里,导致redis内存块耗尽了。怎么解决这个问题呢? **redis 内存淘汰机制。**
    
    ### redis 内存淘汰机制(MySQL里有2000w数据,Redis中只存20w的数据,如何保证Redis中的数据都是热点数据?)
    
    redis 配置文件 redis.conf 中有相关注释,我这里就不贴了,大家可以自行查阅或者通过这个网址查看: [http://download.redis.io/redis-stable/redis.conf](http://download.redis.io/redis-stable/redis.conf)
    
    **redis 提供 6种数据淘汰策略:**
    
    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**:当内存不足以容纳新写入数据时,在键空间中,移除最不经常使用的key
    
    **备注: 关于 redis 设置过期时间以及内存淘汰机制,我这里只是简单的总结一下,后面会专门写一篇文章来总结!**
    
    
    ### redis 持久化机制(怎么保证 redis 挂掉之后再重启数据可以进行恢复)
    
    很多时候我们需要持久化数据也就是将内存中的数据写入到硬盘里面,大部分原因是为了之后重用数据(比如重启机器、机器故障之后恢复数据),或者是为了防止系统故障而将数据备份到一个远程位置。
    
    Redis不同于Memcached的很重一点就是,Redis支持持久化,而且支持两种不同的持久化操作。**Redis的一种持久化方式叫快照(snapshotting,RDB),另一种方式是只追加文件(append-only file,AOF)**。这两种方法各有千秋,下面我会详细这两种持久化方法是什么,怎么用,如何选择适合自己的持久化方法。
    
    **快照(snapshotting)持久化(RDB)**
    
    Redis可以通过创建快照来获得存储在内存里面的数据在某个时间点上的副本。Redis创建快照之后,可以对快照进行备份,可以将快照复制到其他服务器从而创建具有相同数据的服务器副本(Redis主从结构,主要用来提高Redis性能),还可以将快照留在原地以便重启服务器的时候使用。
    
    快照持久化是Redis默认采用的持久化方式,在redis.conf配置文件中默认有此下配置:
    
    ```conf
    
    save 900 1           #在900秒(15分钟)之后,如果至少有1个key发生变化,Redis就会自动触发BGSAVE命令创建快照。
    
    save 300 10          #在300秒(5分钟)之后,如果至少有10个key发生变化,Redis就会自动触发BGSAVE命令创建快照。
    
    save 60 10000        #在60秒(1分钟)之后,如果至少有10000个key发生变化,Redis就会自动触发BGSAVE命令创建快照。
    ```
    
    **AOF(append-only file)持久化**
    
    与快照持久化相比,AOF持久化 的实时性更好,因此已成为主流的持久化方案。默认情况下Redis没有开启AOF(append only file)方式的持久化,可以通过appendonly参数开启:
    
    ```conf
    appendonly yes
    ```
    
    开启AOF持久化后每执行一条会更改Redis中的数据的命令,Redis就会将该命令写入硬盘中的AOF文件。AOF文件的保存位置和RDB文件的位置相同,都是通过dir参数设置的,默认的文件名是appendonly.aof。
    
    在Redis的配置文件中存在三种不同的 AOF 持久化方式,它们分别是:
    
    ```conf
    appendfsync always    #每次有数据修改发生时都会写入AOF文件,这样会严重降低Redis的速度
    appendfsync everysec  #每秒钟同步一次,显示地将多个写命令同步到硬盘
    appendfsync no        #让操作系统决定何时进行同步
    ```
    
    为了兼顾数据和写入性能,用户可以考虑 appendfsync everysec选项 ,让Redis每秒同步一次AOF文件,Redis性能几乎没受到任何影响。而且这样即使出现系统崩溃,用户最多只会丢失一秒之内产生的数据。当硬盘忙于执行写入操作的时候,Redis还会优雅的放慢自己的速度以便适应硬盘的最大写入速度。
    
    **Redis 4.0 对于持久化机制的优化**
    
    Redis 4.0 开始支持 RDB 和 AOF 的混合持久化(默认关闭,可以通过配置项 `aof-use-rdb-preamble` 开启)。
    
    如果把混合持久化打开,AOF 重写的时候就直接把 RDB 的内容写到 AOF 文件开头。这样做的好处是可以结合 RDB 和 AOF 的优点, 快速加载同时避免丢失过多的数据。当然缺点也是有的, AOF 里面的 RDB 部分是压缩格式不再是 AOF 格式,可读性较差。
    
    **补充内容:AOF 重写**
    
    AOF重写可以产生一个新的AOF文件,这个新的AOF文件和原有的AOF文件所保存的数据库状态一样,但体积更小。
    
    AOF重写是一个有歧义的名字,该功能是通过读取数据库中的键值对来实现的,程序无须对现有AOF文件进行任何读入、分析或者写入操作。
    
    在执行 BGREWRITEAOF 命令时,Redis 服务器会维护一个 AOF 重写缓冲区,该缓冲区会在子进程创建新AOF文件期间,记录服务器执行的所有写命令。当子进程完成创建新AOF文件的工作之后,服务器会将重写缓冲区中的所有内容追加到新AOF文件的末尾,使得新旧两个AOF文件所保存的数据库状态一致。最后,服务器用新的AOF文件替换旧的AOF文件,以此来完成AOF文件重写操作
    
    **更多内容可以查看我的这篇文章:**
    
    - [Redis持久化](Redis持久化.md)
    
    
    ### redis 事务
    
    Redis 通过 MULTI、EXEC、WATCH 等命令来实现事务(transaction)功能。事务提供了一种将多个命令请求打包,然后一次性、按顺序地执行多个命令的机制,并且在事务执行期间,服务器不会中断事务而改去执行其他客户端的命令请求,它会将事务中的所有命令都执行完毕,然后才去处理其他客户端的命令请求。
    
    在传统的关系式数据库中,常常用 ACID 性质来检验事务功能的可靠性和安全性。在 Redis 中,事务总是具有原子性(Atomicity)、一致性(Consistency)和隔离性(Isolation),并且当 Redis 运行在某种特定的持久化模式下时,事务也具有持久性(Durability)。
    
    补充内容:
    
    > 1. redis同一个事务中如果有一条命令执行失败,其后的命令仍然会被执行,没有回滚。(来自[issue:关于Redis事务不是原子性问题](https://github.com/Snailclimb/JavaGuide/issues/452) )
    
    ### 缓存雪崩和缓存穿透问题解决方案
    
    #### **缓存雪崩** 
    
    **什么是缓存雪崩?**
    
    简介:缓存同一时间大面积的失效,所以,后面的请求都会落到数据库上,造成数据库短时间内承受大量请求而崩掉。
    
    **有哪些解决办法?**
    
    (中华石杉老师在他的视频中提到过,视频地址在最后一个问题中有提到):
    
    - 事前:尽量保证整个 redis 集群的高可用性,发现机器宕机尽快补上。选择合适的内存淘汰策略。
    - 事中:本地ehcache缓存 + hystrix限流&降级,避免MySQL崩掉
    - 事后:利用 redis 持久化机制保存的数据尽快恢复缓存
    
    ![](http://my-blog-to-use.oss-cn-beijing.aliyuncs.com/18-9-25/6078367.jpg)
    
    #### **缓存穿透** 
    
    **什么是缓存穿透?**
    
    缓存穿透说简单点就是大量请求的 key 根本不存在于缓存中,导致请求直接到了数据库上,根本没有经过缓存这一层。举个例子:某个黑客故意制造我们缓存中不存在的 key 发起大量请求,导致大量请求落到数据库。下面用图片展示一下(这两张图片不是我画的,为了省事直接在网上找的,这里说明一下):
    
    **正常缓存处理流程:**
    
    <img src="https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/正常缓存处理流程-redis.png" style="zoom:50%;" />
    
    **缓存穿透情况处理流程:**
    
    <img src="https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/缓存穿透处理流程-redis.png" style="zoom:50%;" />
    
    一般MySQL 默认的最大连接数在 150 左右,这个可以通过 `show variables like '%max_connections%'; `命令来查看。最大连接数一个还只是一个指标,cpu,内存,磁盘,网络等无力条件都是其运行指标,这些指标都会限制其并发能力!所以,一般 3000 个并发请求就能打死大部分数据库了。
    
    **有哪些解决办法?**
    
    最基本的就是首先做好参数校验,一些不合法的参数请求直接抛出异常信息返回给客户端。比如查询的数据库 id 不能小于 0、传入的邮箱格式不对的时候直接返回错误消息给客户端等等。
    
    **1)缓存无效 key** : 如果缓存和数据库都查不到某个 key 的数据就写一个到 redis 中去并设置过期时间,具体命令如下:`SET key value EX 10086`。这种方式可以解决请求的 key 变化不频繁的情况,如何黑客恶意攻击,每次构建的不同的请求key,会导致 redis 中缓存大量无效的 key 。很明显,这种方案并不能从根本上解决此问题。如果非要用这种方式来解决穿透问题的话,尽量将无效的 key 的过期时间设置短一点比如 1 分钟。
    
    另外,这里多说一嘴,一般情况下我们是这样设计 key 的: `表名:列名:主键名:主键值`。
    
     如果用 Java 代码展示的话,差不多是下面这样的:
    
    ```java
    public Object getObjectInclNullById(Integer id) {
        // 从缓存中获取数据
        Object cacheValue = cache.get(id);
        // 缓存为空
        if (cacheValue != null) {
            // 从数据库中获取
            Object storageValue = storage.get(key);
            // 缓存空对象
            cache.set(key, storageValue);
            // 如果存储数据为空,需要设置一个过期时间(300秒)
            if (storageValue == null) {
                // 必须设置过期时间,否则有被攻击的风险
                cache.expire(key, 60 * 5);
            }
            return storageValue;
        }
        return cacheValue;
    }
    ```
    
    **2)布隆过滤器:**布隆过滤器是一个非常神奇的数据结构,通过它我们可以非常方便地判断一个给定数据是否存在与海量数据中。我们需要的就是判断 key 是否合法,有没有感觉布隆过滤器就是我们想要找的那个“人”。具体是这样做的:把所有可能存在的请求的值都存放在布隆过滤器中,当用户请求过来,我会先判断用户发来的请求的值是否存在于布隆过滤器中。不存在的话,直接返回请求参数错误信息给客户端,存在的话才会走下面的流程。总结一下就是下面这张图(这张图片不是我画的,为了省事直接在网上找的):
    
    <img src="https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/布隆过滤器-缓存穿透-redis.png" style="zoom:50%;" />
    
    更多关于布隆过滤器的内容可以看我的这篇原创:[《不了解布隆过滤器?一文给你整的明明白白!》](https://github.com/Snailclimb/JavaGuide/blob/master/docs/dataStructures-algorithms/data-structure/bloom-filter.md) ,强烈推荐,个人感觉网上应该找不到总结的这么明明白白的文章了。
    
    ### 如何解决 Redis 的并发竞争 Key 问题
    
    所谓 Redis 的并发竞争 Key 的问题也就是多个系统同时对一个 key 进行操作,但是最后执行的顺序和我们期望的顺序不同,这样也就导致了结果的不同!
    
    推荐一种方案:分布式锁(zookeeper 和 redis 都可以实现分布式锁)。(如果不存在 Redis 的并发竞争 Key 问题,不要使用分布式锁,这样会影响性能)
    
    基于zookeeper临时有序节点可以实现的分布式锁。大致思想为:每个客户端对某个方法加锁时,在zookeeper上的与该方法对应的指定节点的目录下,生成一个唯一的瞬时有序节点。 判断是否获取锁的方式很简单,只需要判断有序节点中序号最小的一个。 当释放锁的时候,只需将这个瞬时节点删除即可。同时,其可以避免服务宕机导致的锁无法释放,而产生的死锁问题。完成业务流程后,删除对应的子节点释放锁。
    
    在实践中,当然是从以可靠性为主。所以首推Zookeeper。
    
    参考:
    
    - https://www.jianshu.com/p/8bddd381de06
    
    ### 如何保证缓存与数据库双写时的数据一致性?
    
    > 一般情况下我们都是这样使用缓存的:先读缓存,缓存没有的话,就读数据库,然后取出数据后放入缓存,同时返回响应。这种方式很明显会存在缓存和数据库的数据不一致的情况。
    
    你只要用缓存,就可能会涉及到缓存与数据库双存储双写,你只要是双写,就一定会有数据一致性的问题,那么你如何解决一致性问题?
    
    一般来说,就是如果你的系统不是严格要求缓存+数据库必须一致性的话,缓存可以稍微的跟数据库偶尔有不一致的情况,最好不要做这个方案,读请求和写请求串行化,串到一个内存队列里去,这样就可以保证一定不会出现不一致的情况
    
    串行化之后,就会导致系统的吞吐量会大幅度的降低,用比正常情况下多几倍的机器去支撑线上的一个请求。
    
    更多内容可以查看:https://github.com/doocs/advanced-java/blob/master/docs/high-concurrency/redis-consistence.md
    
    **参考:** Java工程师面试突击第1季(可能是史上最好的Java面试突击课程)-中华石杉老师!公众号后台回复关键字“1”即可获取该视频内容。
    
    ### 参考
    
    - 《Redis开发与运维》
    - Redis 命令总结:http://redisdoc.com/string/set.html
    
    ## 公众号
    
    如果大家想要实时关注我更新的文章以及分享的干货的话,可以关注我的公众号。
    
    **《Java面试突击》:** 由本文档衍生的专为面试而生的《Java面试突击》V2.0 PDF 版本[公众号](#公众号)后台回复 **"Java面试突击"** 即可免费领取!
    
    **Java工程师必备学习资源:** 一些Java工程师常用学习资源公众号后台回复关键字 **“1”** 即可免费无套路获取。 
    
    ![我的公众号](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/167598cd2e17b8ec.png)
    非常感谢《redis实战》真本书,本文大多内容也参考了书中的内容。非常推荐大家看一下《redis实战》这本书,感觉书中的很多理论性东西还是很不错的。
    
    为什么本文的名字要加上春夏秋冬又一春,哈哈 ,这是一部韩国的电影,我感觉电影不错,所以就用在文章名字上了,没有什么特别的含义,然后下面的有些配图也是电影相关镜头。
    
    ![春夏秋冬又一春](https://user-gold-cdn.xitu.io/2018/6/13/163f97071d71f6de?w=1280&h=720&f=jpeg&s=205252)
    
    **很多时候我们需要持久化数据也就是将内存中的数据写入到硬盘里面,大部分原因是为了之后重用数据(比如重启机器、机器故障之后回复数据),或者是为了防止系统故障而将数据备份到一个远程位置。**
    
    Redis不同于Memcached的很重一点就是,**Redis支持持久化**,而且支持两种不同的持久化操作。Redis的一种持久化方式叫**快照(snapshotting,RDB)**,另一种方式是**只追加文件(append-only file,AOF)**.这两种方法各有千秋,下面我会详细这两种持久化方法是什么,怎么用,如何选择适合自己的持久化方法。
    
    
    ## 快照(snapshotting)持久化
    
    Redis可以通过创建快照来获得存储在内存里面的数据在某个时间点上的副本。Redis创建快照之后,可以对快照进行备份,可以将快照复制到其他服务器从而创建具有相同数据的服务器副本(Redis主从结构,主要用来提高Redis性能),还可以将快照留在原地以便重启服务器的时候使用。
    
    
    ![春夏秋冬又一春](https://user-gold-cdn.xitu.io/2018/6/13/163f97568281782a?w=600&h=329&f=jpeg&s=88616)
    
    **快照持久化是Redis默认采用的持久化方式**,在redis.conf配置文件中默认有此下配置:
    ```
    
    save 900 1              #在900秒(15分钟)之后,如果至少有1个key发生变化,Redis就会自动触发BGSAVE命令创建快照。
    
    save 300 10            #在300秒(5分钟)之后,如果至少有10个key发生变化,Redis就会自动触发BGSAVE命令创建快照。
    
    save 60 10000        #在60秒(1分钟)之后,如果至少有10000个key发生变化,Redis就会自动触发BGSAVE命令创建快照。
    ```
    
    根据配置,快照将被写入dbfilename选项指定的文件里面,并存储在dir选项指定的路径上面。如果在新的快照文件创建完毕之前,Redis、系统或者硬件这三者中的任意一个崩溃了,那么Redis将丢失最近一次创建快照写入的所有数据。
    
    举个例子:假设Redis的上一个快照是2:35开始创建的,并且已经创建成功。下午3:06时,Redis又开始创建新的快照,并且在下午3:08快照创建完毕之前,有35个键进行了更新。如果在下午3:06到3:08期间,系统发生了崩溃,导致Redis无法完成新快照的创建工作,那么Redis将丢失下午2:35之后写入的所有数据。另一方面,如果系统恰好在新的快照文件创建完毕之后崩溃,那么Redis将丢失35个键的更新数据。
    
    **创建快照的办法有如下几种:**
    
    - **BGSAVE命令:** 客户端向Redis发送 **BGSAVE命令** 来创建一个快照。对于支持BGSAVE命令的平台来说(基本上所有平台支持,除了Windows平台),Redis会调用fork来创建一个子进程,然后子进程负责将快照写入硬盘,而父进程则继续处理命令请求。
    - **SAVE命令:** 客户端还可以向Redis发送 **SAVE命令** 来创建一个快照,接到SAVE命令的Redis服务器在快照创建完毕之前不会再响应任何其他命令。SAVE命令不常用,我们通常只会在没有足够内存去执行BGSAVE命令的情况下,又或者即使等待持久化操作执行完毕也无所谓的情况下,才会使用这个命令。
    - **save选项:** 如果用户设置了save选项(一般会默认设置),比如 **save 60 10000**,那么从Redis最近一次创建快照之后开始算起,当“60秒之内有10000次写入”这个条件被满足时,Redis就会自动触发BGSAVE命令。
    - **SHUTDOWN命令:**  当Redis通过SHUTDOWN命令接收到关闭服务器的请求时,或者接收到标准TERM信号时,会执行一个SAVE命令,阻塞所有客户端,不再执行客户端发送的任何命令,并在SAVE命令执行完毕之后关闭服务器。
    - **一个Redis服务器连接到另一个Redis服务器:** 当一个Redis服务器连接到另一个Redis服务器,并向对方发送SYNC命令来开始一次复制操作的时候,如果主服务器目前没有执行BGSAVE操作,或者主服务器并非刚刚执行完BGSAVE操作,那么主服务器就会执行BGSAVE命令
    
    如果系统真的发生崩溃,用户将丢失最近一次生成快照之后更改的所有数据。因此,快照持久化只适用于即使丢失一部分数据也不会造成一些大问题的应用程序。不能接受这个缺点的话,可以考虑AOF持久化。
    
    
    
    ## **AOF(append-only file)持久化**
    与快照持久化相比,AOF持久化 的实时性更好,因此已成为主流的持久化方案。默认情况下Redis没有开启AOF(append only file)方式的持久化,可以通过appendonly参数开启:
    ```
    appendonly yes
    ```
    
    开启AOF持久化后每执行一条会更改Redis中的数据的命令,Redis就会将该命令写入硬盘中的AOF文件。AOF文件的保存位置和RDB文件的位置相同,都是通过dir参数设置的,默认的文件名是appendonly.aof。
    
    ![春夏秋冬又一春](https://user-gold-cdn.xitu.io/2018/6/13/163f976818876166?w=400&h=219&f=jpeg&s=91022)
    
    **在Redis的配置文件中存在三种同步方式,它们分别是:**
    
    ```
    
    appendfsync always     #每次有数据修改发生时都会写入AOF文件,这样会严重降低Redis的速度
    appendfsync everysec  #每秒钟同步一次,显示地将多个写命令同步到硬盘
    appendfsync no      #让操作系统决定何时进行同步
    ```
    
    **appendfsync always** 可以实现将数据丢失减到最少,不过这种方式需要对硬盘进行大量的写入而且每次只写入一个命令,十分影响Redis的速度。另外使用固态硬盘的用户谨慎使用appendfsync always选项,因为这会明显降低固态硬盘的使用寿命。
    
    为了兼顾数据和写入性能,用户可以考虑 **appendfsync everysec选项** ,让Redis每秒同步一次AOF文件,Redis性能几乎没受到任何影响。而且这样即使出现系统崩溃,用户最多只会丢失一秒之内产生的数据。当硬盘忙于执行写入操作的时候,Redis还会优雅的放慢自己的速度以便适应硬盘的最大写入速度。
    
    
    **appendfsync no**  选项一般不推荐,这种方案会使Redis丢失不定量的数据而且如果用户的硬盘处理写入操作的速度不够的话,那么当缓冲区被等待写入的数据填满时,Redis的写入操作将被阻塞,这会导致Redis的请求速度变慢。
    
    **虽然AOF持久化非常灵活地提供了多种不同的选项来满足不同应用程序对数据安全的不同要求,但AOF持久化也有缺陷——AOF文件的体积太大。**
    
    ## 重写/压缩AOF
    
    AOF虽然在某个角度可以将数据丢失降低到最小而且对性能影响也很小,但是极端的情况下,体积不断增大的AOF文件很可能会用完硬盘空间。另外,如果AOF体积过大,那么还原操作执行时间就可能会非常长。
    
    为了解决AOF体积过大的问题,用户可以向Redis发送 **BGREWRITEAOF命令** ,这个命令会通过移除AOF文件中的冗余命令来重写(rewrite)AOF文件来减小AOF文件的体积。BGREWRITEAOF命令和BGSAVE创建快照原理十分相似,所以AOF文件重写也需要用到子进程,这样会导致性能问题和内存占用问题,和快照持久化一样。更糟糕的是,如果不加以控制的话,AOF文件的体积可能会比快照文件大好几倍。
    
    **文件重写流程:**
    
    ![文件重写流程](https://user-gold-cdn.xitu.io/2018/6/13/163f97f9bd0eea50?w=380&h=345&f=jpeg&s=14501)
    和快照持久化可以通过设置save选项来自动执行BGSAVE一样,AOF持久化也可以通过设置
    
    ```
    auto-aof-rewrite-percentage
    ```
    
    选项和
    
    ```
    auto-aof-rewrite-min-size
    ```
    
    选项自动执行BGREWRITEAOF命令。举例:假设用户对Redis设置了如下配置选项并且启用了AOF持久化。那么当AOF文件体积大于64mb,并且AOF的体积比上一次重写之后的体积大了至少一倍(100%)的时候,Redis将执行BGREWRITEAOF命令。
    
    ```
    auto-aof-rewrite-percentage 100  
    auto-aof-rewrite-min-size 64mb
    ```
    
    无论是AOF持久化还是快照持久化,将数据持久化到硬盘上都是非常有必要的,但除了进行持久化外,用户还必须对持久化得到的文件进行备份(最好是备份到不同的地方),这样才能尽量避免数据丢失事故发生。如果条件允许的话,最好能将快照文件和重新重写的AOF文件备份到不同的服务器上面。
    
    随着负载量的上升,或者数据的完整性变得 越来越重要时,用户可能需要使用到复制特性。
    
    ## Redis 4.0 对于持久化机制的优化
    Redis 4.0 开始支持 RDB 和 AOF 的混合持久化(默认关闭,可以通过配置项 `aof-use-rdb-preamble` 开启)。
    
    如果把混合持久化打开,AOF 重写的时候就直接把 RDB 的内容写到 AOF 文件开头。这样做的好处是可以结合 RDB 和 AOF 的优点, 快速加载同时避免丢失过多的数据。当然缺点也是有的, AOF 里面的 RDB 部分就是压缩格式不再是 AOF 格式,可读性较差。
    
    参考:
    
    《Redis实战》
    
    [深入学习Redis(2):持久化](https://www.cnblogs.com/kismetv/p/9137897.html)
    
    
    
    相关阅读:
    
    - [史上最全Redis高可用技术解决方案大全](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247484850&idx=1&sn=3238360bfa8105cf758dcf7354af2814&chksm=cea24a79f9d5c36fb2399aafa91d7fb2699b5006d8d037fe8aaf2e5577ff20ae322868b04a87&token=1082669959&lang=zh_CN&scene=21#wechat_redirect)
    - [Raft协议实战之Redis Sentinel的选举Leader源码解析](http://weizijun.cn/2015/04/30/Raft%E5%8D%8F%E8%AE%AE%E5%AE%9E%E6%88%98%E4%B9%8BRedis%20Sentinel%E7%9A%84%E9%80%89%E4%B8%BELeader%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90/)
    
    目录:
    
    <!-- TOC -->
    
    - [Redis 集群以及应用](#redis-集群以及应用)
        - [集群](#集群)
            - [主从复制](#主从复制)
                - [主从链(拓扑结构)](#主从链拓扑结构)
                - [复制模式](#复制模式)
                - [问题点](#问题点)
            - [哨兵机制](#哨兵机制)
                - [拓扑图](#拓扑图)
                - [节点下线](#节点下线)
                - [Leader选举](#Leader选举)
                - [故障转移](#故障转移)
                - [读写分离](#读写分离)
                - [定时任务](#定时任务)
            - [分布式集群(Cluster)](#分布式集群cluster)
                - [拓扑图](#拓扑图)
                - [通讯](#通讯)
                    - [集中式](#集中式)
                    - [Gossip](#gossip)
                - [寻址分片](#寻址分片)
                    - [hash取模](#hash取模)
                    - [一致性hash](#一致性hash)
                    - [hash槽](#hash槽)
        - [使用场景](#使用场景)
            - [热点数据](#热点数据)
            - [会话维持 Session](#会话维持-session)
            - [分布式锁 SETNX](#分布式锁-setnx)
            - [表缓存](#表缓存)
            - [消息队列 list](#消息队列-list)
            - [计数器 string](#计数器-string)
        - [缓存设计](#缓存设计)
            - [更新策略](#更新策略)
            - [更新一致性](#更新一致性)
            - [缓存粒度](#缓存粒度)
            - [缓存穿透](#缓存穿透)
                - [解决方案](#解决方案)
            - [缓存雪崩](#缓存雪崩)
                - [出现后应对](#出现后应对)
                - [请求过程](#请求过程)
    
    <!-- /MarkdownTOC -->
    
    # Redis 集群以及应用
    
    ## 集群
    
    ### 主从复制
    
    #### 主从链(拓扑结构)
    
    ![主从](https://user-images.githubusercontent.com/26766909/67539461-d1a26c00-f714-11e9-81ae-61fa89faf156.png)
    
    ![主从](https://user-images.githubusercontent.com/26766909/67539485-e0891e80-f714-11e9-8980-d253239fcd8b.png)
    
    #### 复制模式
    - 全量复制:Master 全部同步到 Slave
    - 部分复制:Slave 数据丢失进行备份
    
    #### 问题点
    - 同步故障
        - 复制数据延迟(不一致)
        - 读取过期数据(Slave 不能删除数据)
        - 从节点故障
        - 主节点故障
    - 配置不一致
        - maxmemory 不一致:丢失数据
        - 优化参数不一致:内存不一致.
    - 避免全量复制
        - 选择小主节点(分片)、低峰期间操作.
        - 如果节点运行 id 不匹配(如主节点重启、运行 id 发送变化),此时要执行全量复制,应该配合哨兵和集群解决.
        - 主从复制挤压缓冲区不足产生的问题(网络中断,部分复制无法满足),可增大复制缓冲区( rel_backlog_size 参数).
    - 复制风暴
    
    ### 哨兵机制
    
    #### 拓扑图
    
    ![image](https://user-images.githubusercontent.com/26766909/67539495-f0086780-f714-11e9-9eab-c11a163ac6c0.png)
    
    #### 节点下线
    
    - 主观下线
        - 即 Sentinel 节点对 Redis 节点失败的偏见,超出超时时间认为 Master 已经宕机。
        - Sentinel 集群的每一个 Sentinel 节点会定时对 Redis 集群的所有节点发心跳包检测节点是否正常。如果一个节点在 `down-after-milliseconds` 时间内没有回复 Sentinel 节点的心跳包,则该 Redis 节点被该 Sentinel 节点主观下线。
    - 客观下线
        - 所有 Sentinel 节点对 Redis 节点失败要达成共识,即超过 quorum 个统一。
        - 当节点被一个 Sentinel 节点记为主观下线时,并不意味着该节点肯定故障了,还需要 Sentinel 集群的其他 Sentinel 节点共同判断为主观下线才行。
        - 该 Sentinel 节点会询问其它 Sentinel 节点,如果 Sentinel 集群中超过 quorum 数量的 Sentinel 节点认为该 Redis 节点主观下线,则该 Redis 客观下线。
    
    #### Leader选举
    
    - 选举出一个 Sentinel 作为 Leader:集群中至少有三个 Sentinel 节点,但只有其中一个节点可完成故障转移.通过以下命令可以进行失败判定或领导者选举。
    - 选举流程
        1. 每个主观下线的 Sentinel 节点向其他 Sentinel 节点发送命令,要求设置它为领导者.
        2. 收到命令的 Sentinel 节点如果没有同意通过其他 Sentinel 节点发送的命令,则同意该请求,否则拒绝。
        3. 如果该 Sentinel 节点发现自己的票数已经超过 Sentinel 集合半数且超过 quorum,则它成为领导者。
        4. 如果此过程有多个 Sentinel 节点成为领导者,则等待一段时间再重新进行选举。
    
    #### 故障转移
    
    - 转移流程
        1. Sentinel 选出一个合适的 Slave 作为新的 Master(slaveof no one 命令)。
        2. 向其余 Slave 发出通知,让它们成为新 Master 的 Slave( parallel-syncs 参数)。
        3. 等待旧 Master 复活,并使之称为新 Master 的 Slave。
        4. 向客户端通知 Master 变化。
    - 从 Slave 中选择新 Master 节点的规则(slave 升级成 master 之后)
        1. 选择 slave-priority 最高的节点。
        2. 选择复制偏移量最大的节点(同步数据最多)。
        3. 选择 runId 最小的节点。
    
    >Sentinel 集群运行过程中故障转移完成,所有 Sentinel 又会恢复平等。Leader 仅仅是故障转移操作出现的角色。
    
    #### 读写分离
    
    #### 定时任务
    
    - 每 1s 每个 Sentinel 对其他 Sentinel 和 Redis 执行 ping,进行心跳检测。
    - 每 2s 每个 Sentinel 通过 Master 的 Channel 交换信息(pub - sub)。
    - 每 10s 每个 Sentinel 对 Master 和 Slave 执行 info,目的是发现 Slave 节点、确定主从关系。
    
    ### 分布式集群(Cluster)
    
    #### 拓扑图
    
    ![image](https://user-images.githubusercontent.com/26766909/67539510-f8f93900-f714-11e9-9d8d-08afdecff95a.png)
    
    #### 通讯
    
    ##### 集中式
    
    > 将集群元数据(节点信息、故障等等)几种存储在某个节点上。
    - 优势
        1. 元数据的更新读取具有很强的时效性,元数据修改立即更新
    - 劣势
        1. 数据集中存储
    
    ##### Gossip
    
    ![image](https://user-images.githubusercontent.com/26766909/67539546-16c69e00-f715-11e9-9891-1e81b6af624c.png)
    
    - [Gossip 协议](https://www.jianshu.com/p/8279d6fd65bb)
    
    #### 寻址分片
    
    ##### hash取模
    
    - hash(key)%机器数量
    - 问题
        1. 机器宕机,造成数据丢失,数据读取失败
        1. 伸缩性
    
    ##### 一致性hash
    
    - ![image](https://user-images.githubusercontent.com/26766909/67539595-352c9980-f715-11e9-8e4a-9d9c04027785.png)
    
    - 问题
        1. 一致性哈希算法在节点太少时,容易因为节点分布不均匀而造成缓存热点的问题。
            - 解决方案
                - 可以通过引入虚拟节点机制解决:即对每一个节点计算多个 hash,每个计算结果位置都放置一个虚拟节点。这样就实现了数据的均匀分布,负载均衡。
    
    ##### hash槽
    
    - CRC16(key)%16384
    - 
    ![image](https://user-images.githubusercontent.com/26766909/67539610-3fe72e80-f715-11e9-8e0d-ea58bc965795.png)
    
    ## 使用场景
    
    ### 热点数据
    
    存取数据优先从 Redis 操作,如果不存在再从文件(例如 MySQL)中操作,从文件操作完后将数据存储到 Redis 中并返回。同时有个定时任务后台定时扫描 Redis 的 key,根据业务规则进行淘汰,防止某些只访问一两次的数据一直存在 Redis 中。
    >例如使用 Zset 数据结构,存储 Key 的访问次数/最后访问时间作为 Score,最后做排序,来淘汰那些最少访问的 Key。  
      
    如果企业级应用,可以参考:[阿里云的 Redis 混合存储版][1]
    
    ### 会话维持 Session
    
    会话维持 Session 场景,即使用 Redis 作为分布式场景下的登录中心存储应用。每次不同的服务在登录的时候,都会去统一的 Redis 去验证 Session 是否正确。但是在微服务场景,一般会考虑 Redis + JWT 做 Oauth2 模块。
    >其中 Redis 存储 JWT 的相关信息主要是留出口子,方便以后做统一的防刷接口,或者做登录设备限制等。
    
    ### 分布式锁 SETNX
    
    命令格式:`SETNX key value`:当且仅当 key 不存在,将 key 的值设为 value。若给定的 key 已经存在,则 SETNX 不做任何动作。
    
    1. 超时时间设置:获取锁的同时,启动守护线程,使用 expire 进行定时更新超时时间。如果该业务机器宕机,守护线程也挂掉,这样也会自动过期。如果该业务不是宕机,而是真的需要这么久的操作时间,那么增加超时时间在业务上也是可以接受的,但是肯定有个最大的阈值。
    2. 但是为了增加高可用,需要使用多台 Redis,就增加了复杂性,就可以参考 Redlock:[Redlock分布式锁](Redlock分布式锁.md#怎么在单节点上实现分布式锁)
    
    ### 表缓存
    
    Redis 缓存表的场景有黑名单、禁言表等。访问频率较高,即读高。根据业务需求,可以使用后台定时任务定时刷新 Redis 的缓存表数据。
    
    ### 消息队列 list
    
    主要使用了 List 数据结构。  
    List 支持在头部和尾部操作,因此可以实现简单的消息队列。
    1. 发消息:在 List 尾部塞入数据。
    2. 消费消息:在 List 头部拿出数据。
    
    同时可以使用多个 List,来实现多个队列,根据不同的业务消息,塞入不同的 List,来增加吞吐量。
    
    ### 计数器 string
    
    主要使用了 INCR、DECR、INCRBY、DECRBY 方法。
    
    INCR key:给 key 的 value 值增加一 
    DECR key:给 key 的 value 值减去一
    
    ## 缓存设计
    
    ### 更新策略
    
    - LRU、LFU、FIFO 算法自动清除:一致性最差,维护成本低。
    - 超时自动清除(key expire):一致性较差,维护成本低。
    - 主动更新:代码层面控制生命周期,一致性最好,维护成本高。
    
    在 Redis 根据在 redis.conf 的参数 `maxmemory` 来做更新淘汰策略:
    1. noeviction: 不删除策略, 达到最大内存限制时, 如果需要更多内存, 直接返回错误信息。大多数写命令都会导致占用更多的内存(有极少数会例外, 如 DEL 命令)。
    2. allkeys-lru: 所有 key 通用; 优先删除最近最少使用(less recently used ,LRU) 的 key。
    3. volatile-lru: 只限于设置了 expire 的部分; 优先删除最近最少使用(less recently used ,LRU) 的 key。
    4. allkeys-random: 所有key通用; 随机删除一部分 key。
    5. volatile-random: 只限于设置了 expire 的部分; 随机删除一部分 key。
    6. volatile-ttl: 只限于设置了 expire 的部分; 优先删除剩余时间(time to live,TTL) 短的key。
    
    ### 更新一致性
    
    - 读请求:先读缓存,缓存没有的话,就读数据库,然后取出数据后放入缓存,同时返回响应。
    - 写请求:先删除缓存,然后再更新数据库(避免大量地写、却又不经常读的数据导致缓存频繁更新)。
    
    ### 缓存粒度
    
    - 通用性:全量属性更好。
    - 占用空间:部分属性更好。
    - 代码维护成本。
    
    ### 缓存穿透
    
    > 当大量的请求无命中缓存、直接请求到后端数据库(业务代码的 bug、或恶意攻击),同时后端数据库也没有查询到相应的记录、无法添加缓存。  
    > 这种状态会一直维持,流量一直打到存储层上,无法利用缓存、还会给存储层带来巨大压力。
    
    #### 解决方案
    
    1. 请求无法命中缓存、同时数据库记录为空时在缓存添加该 key 的空对象(设置过期时间),缺点是可能会在缓存中添加大量的空值键(比如遭到恶意攻击或爬虫),而且缓存层和存储层数据短期内不一致;
    2. 使用布隆过滤器在缓存层前拦截非法请求、自动为空值添加黑名单(同时可能要为误判的记录添加白名单).但需要考虑布隆过滤器的维护(离线生成/ 实时生成)。
    
    ### 缓存雪崩
    
    > 缓存崩溃时请求会直接落到数据库上,很可能由于无法承受大量的并发请求而崩溃,此时如果只重启数据库,或因为缓存重启后没有数据,新的流量进来很快又会把数据库击倒。
    
    #### 出现后应对
    
    - 事前:Redis 高可用,主从 + 哨兵,Redis Cluster,避免全盘崩溃。
    - 事中:本地 ehcache 缓存 + hystrix 限流 & 降级,避免数据库承受太多压力。
    - 事后:Redis 持久化,一旦重启,自动从磁盘上加载数据,快速恢复缓存数据。
    
    #### 请求过程
    
    1. 用户请求先访问本地缓存,无命中后再访问 Redis,如果本地缓存和 Redis 都没有再查数据库,并把数据添加到本地缓存和 Redis;
    2. 由于设置了限流,一段时间范围内超出的请求走降级处理(返回默认值,或给出友情提示)。
    
    [1]: https://promotion.aliyun.com/ntms/act/redishybridstorage.html?spm=5176.54432.1380373.5.41921cf20pcZrZ&aly_as=ArH4VaEb
    
    这篇文章主要是对 Redis 官方网站刊登的 [Distributed locks with Redis](https://redis.io/topics/distlock) 部分内容的总结和翻译。
    
    ## 什么是 RedLock
    
    Redis 官方站这篇文章提出了一种权威的基于 Redis 实现分布式锁的方式名叫 *Redlock*,此种方式比原先的单节点的方法更安全。它可以保证以下特性:
    
    1. 安全特性:互斥访问,即永远只有一个 client 能拿到锁
    2. 避免死锁:最终 client 都可能拿到锁,不会出现死锁的情况,即使原本锁住某资源的 client crash 了或者出现了网络分区
    3. 容错性:只要大部分 Redis 节点存活就可以正常提供服务
    
    ## 怎么在单节点上实现分布式锁
    
    > SET resource_name my_random_value NX PX 30000
    
    主要依靠上述命令,该命令仅当 Key 不存在时(NX保证)set 值,并且设置过期时间 3000ms (PX保证),值 my_random_value 必须是所有 client 和所有锁请求发生期间唯一的,释放锁的逻辑是:
    
    ```lua
    if redis.call("get",KEYS[1]) == ARGV[1] then
        return redis.call("del",KEYS[1])
    else
        return 0
    end
    ```
    
    上述实现可以避免释放另一个client创建的锁,如果只有 del 命令的话,那么如果 client1 拿到 lock1 之后因为某些操作阻塞了很长时间,此时 Redis 端 lock1 已经过期了并且已经被重新分配给了 client2,那么 client1 此时再去释放这把锁就会造成 client2 原本获取到的锁被 client1 无故释放了,但现在为每个 client 分配一个 unique 的 string 值可以避免这个问题。至于如何去生成这个 unique string,方法很多随意选择一种就行了。
    
    ## Redlock 算法
    
    算法很易懂,起 5 个 master 节点,分布在不同的机房尽量保证可用性。为了获得锁,client 会进行如下操作:
    
    1. 得到当前的时间,微秒单位
    2. 尝试顺序地在 5 个实例上申请锁,当然需要使用相同的 key 和 random value,这里一个 client 需要合理设置与 master 节点沟通的 timeout 大小,避免长时间和一个 fail 了的节点浪费时间
    3. 当 client 在大于等于 3 个 master 上成功申请到锁的时候,且它会计算申请锁消耗了多少时间,这部分消耗的时间采用获得锁的当下时间减去第一步获得的时间戳得到,如果锁的持续时长(lock validity time)比流逝的时间多的话,那么锁就真正获取到了。
    4. 如果锁申请到了,那么锁真正的 lock validity time 应该是 origin(lock validity time) - 申请锁期间流逝的时间
    5. 如果 client 申请锁失败了,那么它就会在少部分申请成功锁的 master 节点上执行释放锁的操作,重置状态
    
    ## 失败重试
    
    如果一个 client 申请锁失败了,那么它需要稍等一会在重试避免多个 client 同时申请锁的情况,最好的情况是一个 client 需要几乎同时向 5 个 master 发起锁申请。另外就是如果 client 申请锁失败了它需要尽快在它曾经申请到锁的 master 上执行 unlock 操作,便于其他 client 获得这把锁,避免这些锁过期造成的时间浪费,当然如果这时候网络分区使得 client 无法联系上这些 master,那么这种浪费就是不得不付出的代价了。
    
    ## 放锁
    
    放锁操作很简单,就是依次释放所有节点上的锁就行了
    
    ## 性能、崩溃恢复和 fsync
    
    如果我们的节点没有持久化机制,client 从 5 个 master 中的 3 个处获得了锁,然后其中一个重启了,这是注意 **整个环境中又出现了 3 个 master 可供另一个 client 申请同一把锁!** 违反了互斥性。如果我们开启了 AOF 持久化那么情况会稍微好转一些,因为 Redis 的过期机制是语义层面实现的,所以在 server 挂了的时候时间依旧在流逝,重启之后锁状态不会受到污染。但是考虑断电之后呢,AOF部分命令没来得及刷回磁盘直接丢失了,除非我们配置刷回策略为 fsnyc = always,但这会损伤性能。解决这个问题的方法是,当一个节点重启之后,我们规定在 max TTL 期间它是不可用的,这样它就不会干扰原本已经申请到的锁,等到它 crash 前的那部分锁都过期了,环境不存在历史锁了,那么再把这个节点加进来正常工作。
    
    本文是对 [Martin Kleppmann](https://martin.kleppmann.com/) 的文章 [How to do distributed locking](https://martin.kleppmann.com/2016/02/08/how-to-do-distributed-locking.html) 部分内容的翻译和总结,上次写 Redlock 的原因就是看到了 Martin 的这篇文章,写得很好,特此翻译和总结。感兴趣的同学可以翻看原文,相信会收获良多。
    
    开篇作者认为现在 Redis 逐渐被使用到数据管理领域,这个领域需要更强的数据一致性和耐久性,这使得他感到担心,因为这不是 Redis 最初设计的初衷(事实上这也是很多业界程序员的误区,越来越把 Redis 当成数据库在使用),其中基于 Redis 的分布式锁就是令人担心的其一。
    
    Martin 指出首先你要明确你为什么使用分布式锁,为了性能还是正确性?为了帮你区分这二者,在这把锁 fail 了的时候你可以询问自己以下问题: 
    1. **要性能的:** 拥有这把锁使得你不会重复劳动(例如一个 job 做了两次),如果这把锁 fail 了,两个节点同时做了这个 Job,那么这个 Job 增加了你的成本。
    2. **要正确性的:** 拥有锁可以防止并发操作污染你的系统或者数据,如果这把锁 fail 了两个节点同时操作了一份数据,结果可能是数据不一致、数据丢失、file 冲突等,会导致严重的后果。
    
    上述二者都是需求锁的正确场景,但是你必须清楚自己是因为什么原因需要分布式锁。
    
    如果你只是为了性能,那没必要用 Redlock,它成本高且复杂,你只用一个 Redis 实例也够了,最多加个从防止主挂了。当然,你使用单节点的 Redis 那么断电或者一些情况下,你会丢失锁,但是你的目的只是加速性能且断电这种事情不会经常发生,这并不是什么大问题。并且如果你使用了单节点 Redis,那么很显然你这个应用需要的锁粒度是很模糊粗糙的,也不会是什么重要的服务。
    
    那么是否 Redlock 对于要求正确性的场景就合适呢?Martin 列举了若干场景证明 Redlock 这种算法是不可靠的。
    
    ## 用锁保护资源
    这节里 Martin 先将 Redlock 放在了一边而是仅讨论总体上一个分布式锁是怎么工作的。在分布式环境下,锁比 mutex 这类复杂,因为涉及到不同节点、网络通信并且他们随时可能无征兆的 fail 。
    Martin 假设了一个场景,一个 client 要修改一个文件,它先申请得到锁,然后修改文件写回,放锁。另一个 client 再申请锁 ... 代码流程如下:
    
    ```java
    // THIS CODE IS BROKEN
    function writeData(filename, data) {
        var lock = lockService.acquireLock(filename);
        if (!lock) {
            throw 'Failed to acquire lock';
        }
    
        try {
            var file = storage.readFile(filename);
            var updated = updateContents(file, data);
            storage.writeFile(filename, updated);
        } finally {
            lock.release();
        }
    }
    ```
    
    可惜即使你的锁服务非常完美,上述代码还是可能跪,下面的流程图会告诉你为什么:
    
    ![](https://martin.kleppmann.com/2016/02/unsafe-lock.png)
    
    上述图中,得到锁的 client1 在持有锁的期间 pause 了一段时间,例如 GC 停顿。锁有过期时间(一般叫租约,为了防止某个 client 崩溃之后一直占有锁),但是如果 GC 停顿太长超过了锁租约时间,此时锁已经被另一个 client2 所得到,原先的 client1 还没有感知到锁过期,那么奇怪的结果就会发生,曾经 HBase 就发生过这种 Bug。即使你在 client1 写回之前检查一下锁是否过期也无助于解决这个问题,因为 GC 可能在任何时候发生,即使是你非常不便的时候(在最后的检查与写操作期间)。
    如果你认为自己的程序不会有长时间的 GC 停顿,还有其他原因会导致你的进程 pause。例如进程可能读取尚未进入内存的数据,所以它得到一个 page fault 并且等待 page 被加载进缓存;还有可能你依赖于网络服务;或者其他进程占用 CPU;或者其他人意外发生 SIGSTOP 等。
    
    ... .... 这里 Martin 又增加了一节列举各种进程 pause 的例子,为了证明上面的代码是不安全的,无论你的锁服务多完美。
    
    ## 使用 Fencing (栅栏)使得锁变安全
    修复问题的方法也很简单:你需要在每次写操作时加入一个 fencing token。这个场景下,fencing token 可以是一个递增的数字(lock service 可以做到),每次有 client 申请锁就递增一次:
    
    ![](https://martin.kleppmann.com/2016/02/fencing-tokens.png)
    
    client1 申请锁同时拿到 token33,然后它进入长时间的停顿锁也过期了。client2 得到锁和 token34 写入数据,紧接着 client1 活过来之后尝试写入数据,自身 token33 比 34 小因此写入操作被拒绝。注意这需要存储层来检查 token,但这并不难实现。如果你使用 Zookeeper 作为 lock service 的话那么你可以使用 zxid 作为递增数字。
    但是对于 Redlock 你要知道,没什么生成 fencing token 的方式,并且怎么修改 Redlock 算法使其能产生 fencing token 呢?好像并不那么显而易见。因为产生 token 需要单调递增,除非在单节点 Redis 上完成但是这又没有高可靠性,你好像需要引进一致性协议来让 Redlock 产生可靠的 fencing token。
    
    ## 使用时间来解决一致性
    Redlock 无法产生 fencing token 早该成为在需求正确性的场景下弃用它的理由,但还有一些值得讨论的地方。
    
    学术界有个说法,算法对时间不做假设:因为进程可能pause一段时间、数据包可能因为网络延迟延后到达、时钟可能根本就是错的。而可靠的算法依旧要在上述假设下做正确的事情。
    
    对于 failure detector 来说,timeout 只能作为猜测某个节点 fail 的依据,因为网络延迟、本地时钟不正确等其他原因的限制。考虑到 Redis 使用 gettimeofday,而不是单调的时钟,会受到系统时间的影响,可能会突然前进或者后退一段时间,这会导致一个 key 更快或更慢地过期。
    
    可见,Redlock 依赖于许多时间假设,它假设所有 Redis 节点都能对同一个 Key 在其过期前持有差不多的时间、跟过期时间相比网络延迟很小、跟过期时间相比进程 pause 很短。
    
    ## 用不可靠的时间打破 Redlock 
    这节 Martin 举了个因为时间问题,Redlock 不可靠的例子。
    
    1. client1 从 ABC 三个节点处申请到锁,DE由于网络原因请求没有到达
    2. C节点的时钟往前推了,导致 lock 过期
    3. client2 在CDE处获得了锁,AB由于网络原因请求未到达
    4. 此时 client1 和 client2 都获得了锁
    
    **在 Redlock 官方文档中也提到了这个情况,不过是C崩溃的时候,Redlock 官方本身也是知道 Redlock 算法不是完全可靠的,官方为了解决这种问题建议使用延时启动,相关内容可以看之前的[这篇文章](https://zhuanlan.zhihu.com/p/40915772)。但是 Martin 这里分析得更加全面,指出延时启动不也是依赖于时钟的正确性的么?**
    
    接下来 Martin 又列举了进程 Pause 时而不是时钟不可靠时会发生的问题:
    
    1. client1 从 ABCDE 处获得了锁
    2. 当获得锁的 response 还没到达 client1 时 client1 进入 GC 停顿
    3. 停顿期间锁已经过期了
    4. client2 在 ABCDE 处获得了锁
    5. client1 GC 完成收到了获得锁的 response,此时两个 client 又拿到了同一把锁
    
    **同时长时间的网络延迟也有可能导致同样的问题。**
    
    ## Redlock 的同步性假设
    这些例子说明了,仅有在你假设了一个同步性系统模型的基础上,Redlock 才能正常工作,也就是系统能满足以下属性:
    
    1. 网络延时边界,即假设数据包一定能在某个最大延时之内到达
    2. 进程停顿边界,即进程停顿一定在某个最大时间之内
    3. 时钟错误边界,即不会从一个坏的 NTP 服务器处取得时间
    
    ## 结论
    Martin 认为 Redlock 实在不是一个好的选择,对于需求性能的分布式锁应用它太重了且成本高;对于需求正确性的应用来说它不够安全。因为它对高危的时钟或者说其他上述列举的情况进行了不可靠的假设,如果你的应用只需要高性能的分布式锁不要求多高的正确性,那么单节点 Redis 够了;如果你的应用想要保住正确性,那么不建议 Redlock,建议使用一个合适的一致性协调系统,例如 Zookeeper,且保证存在 fencing token。
    

     

     

     

    展开全文
  • 推荐书目电子版下载 HDFS知识梳理 应用背景 简介 基本原理 优点 缺点 设计 概念 读写流程 文件写入 文件读取 命令行接口 Java接口 连接Hadoop集群 Hadoop分布式文件系统Java接口详细版 简介 文件系统 接口
  • 在构建多通道数据采集与分析系统时,需要考虑很多因素和挑战:比如根据测试对象的不同,如何选择不同平台的数采设备;...本资源将结合实际应用,帮您梳理构建大型多通道数据采集系统的要素,应对大型项目挑战。
  • 此外,买了N多,基本有时间就会去看,补补基础,什么操作系统数据结构与算法、MySQL、JDK之类的源码,基本都好好温习了(文末会列一下自己看过的和一些好的资料)。**我深知基础就像“木桶效应”的短板,决定...
  • 教材:王珊 萨师煊 编著 数据库系统概论(第5版) 高等教育出版社 注:文档高清截图在后 第3章 关系数据库标准语言SQL 3.1 SQL概述 1、结构化查询语言(Structured Query Language,SQL)是关系数据库的标准语言,也...
  • 案例引入(1) 一元多项式的运算(2) 稀疏多项式的运算(3) 图书信息管理系统二、线性表的类型定义三、线性表的顺序表示和实现1.初始化顺序表2.顺序表取值3.顺序表查找4.顺序表的插入5.顺序表的删除6.基本操作补充7.顺序...
  • 6. 如果对比成功,说明消息完整,并来自与正确的发送者 ### 3.2.3. 数字签名 消息认证码的缺点在于**无法防止否认**,因为共享秘钥被 client、server 两端拥有,server 可以伪造 client 发送给自己的消息(自己给...
  •  找工作时(IT行业),除了常见的软件开发以外,机器学习岗位也可以当作是一个选择,不少计算机方向的研究生都会接触这个,如果你的研究方向是机器学习/数据挖掘之类,且又对其非常感兴趣的话,可以考虑考虑该岗位...
  • 数据可视化(全彩)(大数据丛书,首次全面细致地梳理了可视化理论,方法、工具与应用案例。马匡六教授、石教英教授鼎力推荐,十二五国家重点图书出版规划项目) 陈为 沈则潜等 编著 ISBN 978-7-121-21154-6 ...
  • 说明书只对整个项目的框架进行梳理,对于一些词汇不进行详细的解释,如果要深入研究可联系博主获得更详细的资料,或者自行百度。 项目背景介绍 数据运营平台的建设是为了解决公司 营销分析断层、产品迭代无法量化...
  • 各种系统架构图与详细说明

    万次阅读 多人点赞 2018-09-15 17:49:59
    如上图所示为本次共享资源平台逻辑架构图,上图整体展现说明包括以下几个方面: 1 应用系统建设 本次项目的一项重点就是实现原有应用系统的全面升级以及新的应用系统的开发,从而建立行业的全面的应用系统架构群。...
  • 处理机调度与死锁 处理机调度与死锁 ...它的调度对象为作业,只适用于多道批处理系统中,不适合实时和分时系统。 低级调度。 又称进程调度或短程调度。它的调度对象为进程或内核级线程,适用于所有类型...
  • 机器学习&数据挖掘笔记(常见面试之机器学习算法思想简单梳理) 作者:tornadomeet 出处:http://www.cnblogs.com/tornadomeet 找工作时(IT行业),除了常见的软件开发以外,机器学习岗位也可以当作是一个...
  • 目录前言项目概述项目背景建设目标建设内容需求概述业务流程总体架构用户角色功能权限...规格说明书还是蛮重要的,因为它是项目管理的起点,规格说明书梳理清楚功能点,需要有经验的程序员依据功能点来估算工作量,而
  • 业务梳理方法论

    千次阅读 2020-08-19 15:33:25
    数通畅联致力提供开放、敏捷的集成产品和解决方案,公司多年耕耘于应用系统集成领域,公司现已进入集成范畴的深水区,如:基础数据治理、数据治理分析、深度业务集成、系统流程整合等,对于此类集成类项目对于典型...
  • 订单系统需求分析说明

    千次阅读 2021-01-18 14:00:08
    本文主要讲述了在传统电商企业中,订单系统应承载的角色,就订单系统所包含的主要功能模块梳理了设计思路,并对订单系统未来的发展做了一些思考。 1. 订单系统在企业中的角色 在搭建企业订单系统之前,需要先梳理...
  • 背景其实从编制架构设计说明书的角度来看,也可以阐述具体如何编写架构设计说明书就像高考作文一样,评审总是有些采分点的嘛,那么对于编制架构设计说明书来说哪些是我们应该准备的采分点呢?我们在编...
  • 腾讯安全正式发布了企业级数据安全能力图谱,图谱中将数据安全能力分为四层六大模块,为使各企事业单位能够更好的理解和运用图谱,腾讯安全专家咨询中心联合腾讯安全内部核心部门,对当今重点的数据安全问题进行梳理,...
  • 这是一个系列文章,沉淀了我在数据治理领域的一些实践和思考。共分为5篇。分别是: 一、大数据治理:那些年,我们一起踩过的坑 主要讲讲数据治理工作中常见的一些误区。 二、要打仗,你手里先得有张地图:数据...
  • 机器学习&数据挖掘笔记_16(常见面试之机器学习算法思想简单梳理) http://www.cnblogs.com/tornadomeet/p/3395593.html  博主现在在网易工作 大牛    前言:  找工作时(IT行业),除了常见的软件开发...
  •  《数据仓库原理》系列博文,是笔者在学习数据仓库与商业智能时的读书笔记,现重新梳理思路,分享在这里,希望读者批评指正。  本系列主要包括以下几部分内容:  [1].数据库与数据仓库  为什么有了数据库还...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 13,253
精华内容 5,301
关键字:

系统数据梳理说明书