精华内容
下载资源
问答
  • 注:本次为参加datawhale的打卡活动~详细资料在team-learning-rs 核心系列内容: 协同过滤算法: 包括基于用户的协同过滤(UserCF)和基于商品的协同过滤(ItemCF),这是入门推荐系统的人必看的内容,因为这些算法可以让...

    注:本次为参加datawhale的打卡活动~详细资料在team-learning-rs

    核心系列内容:

    • 协同过滤算法: 包括基于用户的协同过滤(UserCF)和基于商品的协同过滤(ItemCF),这是入门推荐系统的人必看的内容,因为这些算法可以让初学者更加容易的理解推荐算法的思想。
    • 矩阵分解算法: 矩阵分解算法通过引入了隐向量的概念,加强了模型处理稀疏矩阵的能力,也为后续深度学习推荐系统算法中Embedding的使用打下了基础。
    • FM(Factorization Machines): 该算法属于对逻辑回归(LR)算法应用在推荐系统上的一个改进,在LR模型的基础上加上了特征交叉项,该思想不仅在传统的推荐算法中继续使用过,在深度学习推荐算法中也对其进行了改进与应用。
    • GBDT+LR: 该模型仍然是对LR模型的改进,使用树模型做特征交叉,相比于FM的二阶特征交叉,树模型可以对特征进行深度的特征交叉,充分利用了特征之间的相关性。
    • Wide&Deep: 从深度学习推荐系统的演化图中可以看出Wide&Deep模型处在最中间的位置,可以看出该模型在推荐系统发展中的重要地位,此外该算法模型的思想与实现都比较的简单,非常适合初学深度学习推荐系统的学习者们去学习。

    协同过滤算法

    协同过滤(Collaborative Filtering)推荐算法是最经典、最常用的推荐算法。

    所谓协同过滤, 基本思想是根据用户之前的喜好以及其他兴趣相近的用户的选择来给用户推荐物品(基于对用户历史行为数据的挖掘发现用户的喜好偏向, 并预测用户可能喜好的产品进行推荐),一般是仅仅基于用户的行为数据(评价、购买、下载等), 而不依赖于项的任何附加信息(物品自身特征)或者用户的任何附加信息(年龄, 性别等)。目前应用比较广泛的协同过滤算法是基于邻域的方法, 而这种方法主要有下面两种算法:

    • 基于用户的协同过滤算法(UserCF): 给用户推荐和他兴趣相似的其他用户喜欢的产品
    • 基于物品的协同过滤算法(ItemCF): 给用户推荐和他之前喜欢的物品相似的物品

    不管是UserCF还是ItemCF算法, 非常重要的步骤之一就是计算用户和用户或者物品和物品之间的相似度, 所以下面先整理常用的相似性度量方法, 然后再对每个算法的具体细节进行展开。

    相似性度量方法

    1. 杰卡德(Jaccard)相似系数 这个是衡量两个集合的相似度一种指标。两个用户 u u u v v v交互商品交集的数量占这两个用户交互商品并集的数量的比例,称为两个集合的杰卡德相似系数,用符号 s i m u v sim_{uv} simuv表示,其中 N ( u ) , N ( v ) N(u),N(v) N(u),N(v)分别表示用户 u u u和用户 v v v交互商品的集合。 s i m u v = ∣ N ( u ) ∩ N ( v ) ∣ ∣ N ( u ) ∣ ∪ ∣ N ( v ) ∣ sim_{uv}=\frac{|N(u) \cap N(v)|}{{|N(u)| \cup|N(v)|}} simuv=N(u)N(v)N(u)N(v) 由于杰卡德相似系数一般无法反映具体用户的评分喜好信息, 所以常用来评估用户是否会对某商品进行打分, 而不是预估用户会对某商品打多少分。

      • 实例:
        如集合N(u)={1,2,3,4};N(v)={3,4,5,6};
        那么他们的J(X,Y)=1{3,4}/1{1,2,3,4,5,6}=1/3;
    #当两个集合元素个数相同
    from numpy import *
    import scipy.spatial.distance as dist  
    matV = mat([[1,1,0,1,0,1,0,0,1],[0,1,1,0,0,0,1,1,1]])
    print ("dist.jaccard:", dist.pdist(matV,'jaccard'))
    #dist.jaccard: [0.75]
    
    #当集合元素个数不同
    def correlation(set_a,set_b):
        unions = len(set_a.union(set_b))
        intersections = len(set_a.intersection(set_b))
        return 1. * intersections / unions
    
    set_a=set([1,1,0,1,0,1,0,0,1])
    set_b=set([0,1,1,0,0,0,1,1])
    correlation(set_a,set_b)
    #1.0
    
    1. 余弦相似度 余弦相似度衡量了两个向量的夹角,夹角越小越相似。首先从集合的角度描述余弦相似度,相比于Jaccard公式来说就是分母有差异,不是两个用户交互商品的并集的数量,而是两个用户分别交互的商品数量的乘积,公式如下: s i m u v = ∣ N ( u ) ∣ ∩ ∣ N ( v ) ∣ ∣ N ( u ) ∣ ⋅ ∣ N ( v ) ∣ sim_{uv}=\frac{|N(u)| \cap |N(v)|}{\sqrt{|N(u)|\cdot|N(v)|}} simuv=N(u)N(v) N(u)N(v) 从向量的角度进行描述,令矩阵 A A A为用户-商品交互矩阵(因为是TopN推荐并不需要用户对物品的评分,只需要知道用户对商品是否有交互就行),即矩阵的每一行表示一个用户对所有商品的交互情况,有交互的商品值为1没有交互的商品值为0,矩阵的列表示所有商品。若用户和商品数量分别为 m , n m,n m,n的话,交互矩阵 A A A就是一个 m m m n n n列的矩阵。此时用户的相似度可以表示为(其中 u ⋅ v u\cdot v uv指的是向量点积): s i m u v = c o s ( u , v ) = u ⋅ v ∣ u ∣ ⋅ ∣ v ∣ sim_{uv} = cos(u,v) =\frac{u\cdot v}{|u|\cdot |v|} simuv=cos(u,v)=uvuv 上述用户-商品交互矩阵在现实情况下是非常的稀疏了,为了避免存储这么大的稀疏矩阵,在计算用户相似度的时候一般会采用集合的方式进行计算。理论上向量之间的相似度计算公式都可以用来计算用户之间的相似度,但是会根据实际的情况选择不同的用户相似度度量方法。

    这个在具体实现的时候, 可以使用cosine_similarity进行实现:

    from sklearn.metrics.pairwise import cosine_similarity
    i = [1, 0, 0, 0]
    j = [1, 0.5, 0.5, 0]
    cosine_similarity([i,j])
    '''
    array([[1.        , 0.81649658],
           [0.81649658, 1.        ]])
           '''
    
    1. 皮尔逊相关系数
      皮尔逊相关系数的公式与余弦相似度的计算公式非常的类似,首先对于上述的余弦相似度的计算公式写成求和的形式,其中 r u i , r v i r_{ui},r_{vi} rui,rvi分别表示用户 u u u和用户 v v v对商品 i i i是否有交互(或者具体的评分值): s i m u v = ∑ i r u i ∗ r v i ∑ i r u i 2 ∑ i r v i 2 sim_{uv} = \frac{\sum_i r_{ui}*r_{vi}}{\sqrt{\sum_i r_{ui}^2}\sqrt{\sum_i r_{vi}^2}} simuv=irui2 irvi2 iruirvi 如下是皮尔逊相关系数计算公式,其中 r u i , r v i r_{ui},r_{vi} rui,rvi分别表示用户 u u u和用户 v v v对商品 i i i是否有交互(或者具体的评分值), r ˉ u , r ˉ v \bar r_u, \bar r_v rˉu,rˉv分别表示用户 u u u和用户 v v v交互的所有商品交互数量或者具体评分的平均值。 s i m ( u , v ) = ∑ i ∈ I ( r u i − r ˉ u ) ( r v i − r ˉ v ) ∑ i ∈ I ( r u i − r ˉ u ) 2 ∑ i ∈ I ( r v i − r ˉ v ) 2 sim(u,v)=\frac{\sum_{i\in I}(r_{ui}-\bar r_u)(r_{vi}-\bar r_v)}{\sqrt{\sum_{i\in I }(r_{ui}-\bar r_u)^2}\sqrt{\sum_{i\in I }(r_{vi}-\bar r_v)^2}} sim(u,v)=iI(ruirˉu)2 iI(rvirˉv)2 iI(ruirˉu)(rvirˉv) 所以相比余弦相似度,皮尔逊相关系数通过使用用户的平均分对各独立评分进行修正,减小了用户评分偏置的影响。具体实现, 我们也是可以调包, 这个计算方式很多, 下面是其中的一种:
    from scipy.stats import pearsonr
    
    i = [1, 0, 0, 0]
    j = [1, 0.5, 0.5, 0]
    pearsonr(i, j)
    #(0.816496580927726, 0.18350341907227397)
    

    基于用户的协同过滤

    基于用户的协同过滤(以下用UserCF表示),思想其实比较简单,当一个用户A需要个性化推荐的时候, 我们可以先找到和他有相似兴趣的其他用户, 然后把那些用户喜欢的, 而用户A没有听说过的物品推荐给A。
    在这里插入图片描述

    UserCF算法主要包括两个步骤:

    • 找到和目标用户兴趣相似的集合
    • 找到这个集合中的用户喜欢的, 且目标用户没有听说过的物品推荐给目标用户。

    上面的两个步骤中, 第一个步骤里面, 我们会基于前面给出的相似性度量的方法找出与目标用户兴趣相似的用户, 而第二个步骤里面, 如何基于相似用户喜欢的物品来对目标用户进行推荐呢? 这个要依赖于目标用户对相似用户喜欢的物品的一个喜好程度, 那么如何衡量这个程度大小呢? 为了更好理解上面的两个步骤, 下面用一个具体的例子把两个步骤具体化。

    以下图为例,此例将会用于本文各种算法中

    在这里插入图片描述

    给用户推荐物品的过程可以形象化为一个猜测用户对商品进行打分的任务,上面表格里面是5个用户对于5件物品的一个打分情况,就可以理解为用户对物品的喜欢程度

    应用UserCF算法的两个步骤:

    • 首先根据前面的这些打分情况(或者说已有的用户向量)计算一下Alice和用户1, 2, 3, 4的相似程度, 找出与Alice最相似的n个用户
    • 根据这n个用户对物品5的评分情况和与Alice的相似程度会猜测出Alice对物品5的评分, 如果评分比较高的话, 就把物品5推荐给用户Alice, 否则不推荐。

    关于第一个步骤, 上面已经给出了计算两个用户相似性的方法, 这里不再过多赘述, 这里主要解决第二个问题, 如何产生最终结果的预测。

    最终结果的预测

    根据上面的几种方法, 我们可以计算出向量之间的相似程度, 也就是可以计算出Alice和其他用户的相近程度, 这时候我们就可以选出与Alice最相近的前n个用户, 基于他们对物品5的评价猜测出Alice的打分值, 那么是怎么计算的呢?

    这里常用的方式之一是利用用户相似度和相似用户的评价加权平均获得用户的评价预测, 用下面式子表示:

    R u , p = ∑ s ∈ S ( w u , s ⋅ R s , p ) ∑ s ∈ S w u , s R_{\mathrm{u}, \mathrm{p}}=\frac{\sum_{\mathrm{s} \in S}\left(w_{\mathrm{u}, \mathrm{s}} \cdot R_{\mathrm{s}, \mathrm{p}}\right)}{\sum_{\mathrm{s} \in S} w_{\mathrm{u}, \mathrm{s}}} Ru,p=sSwu,ssS(wu,sRs,p) 这个式子里面, 权重 w u , s w_{u,s} wu,s是用户 u u u和用户 s s s的相似度, R s , p R_{s,p} Rs,p是用户 s s s对物品 p p p的评分。

    还有一种方式如下, 这种方式考虑的更加全面, 依然是用户相似度作为权值, 但后面不单纯的是其他用户对物品的评分, 而是该物品的评分与此用户的所有评分的差值进行加权平均, 这时候考虑到了有的用户内心的评分标准不一的情况, 即有的用户喜欢打高分, 有的用户喜欢打低分的情况。

    P i , j = R ˉ i + ∑ k = 1 n ( S i , k ( R k , j − R ˉ k ) ) ∑ k = 1 n S j , k P_{i, j}=\bar{R}{i}+\frac{\sum{k=1}^{n}\left(S_{i, k}\left(R_{k, j}-\bar{R}{k}\right)\right)}{\sum{k=1}^{n} S_{j, k}} Pi,j=Rˉi+k=1nSj,kk=1n(Si,k(Rk,jRˉk))

    P i j P_{i j} Pij 表示的是用户 i i i 对物品 j j j 的评分 , R ˉ i , \quad \bar{R}_{i} ,Rˉi 表示的是用户 i i i 的所有评分的平均值, n n n 表示的是与用户 i i i 相 似的 n n n 个用户 , S i , k , \quad S_{i, k} ,Si,k 表示的是用户 i i i 和用户 k k k 的相似度, R k , j R_{k, j} Rk,j 表示的是用户 k k k 对物品 j j j 的评分 , R ˉ k , \quad \bar{R}_{k} ,Rˉk 表 示的是用户 k k k 的所有评分的平均值。所以这一种计算方式更为推荐。下面的计算将使用这个方式。

    在获得用户 u u u对不同物品的评价预测后, 最终的推荐列表根据预测评分进行排序得到。 至此,基于用户的协同过滤算法的推荐过程完成。

    根据上面的问题, 下面手算一下:

    Aim: 猜测Alice对物品5的得分:

    1. 计算Alice与其他用户的相似度(这里使用皮尔逊相关系数)
      用户向量:Alice ( 5 , 3 , 4 , 4 ) , (5,3,4,4), (5,3,4,4), user 1 ( 3 , 1 , 2 , 3 ) , 1(3,1,2,3), 1(3,1,2,3), user 2 ( 4 , 3 , 4 , 3 ) , 2(4,3,4,3), 2(4,3,4,3), user 3 ( 3 , 3 , 1 , 5 ) , 3(3,3,1,5), 3(3,3,1,5), user 4(1,5,5,2)
      这里计算Alice与user1的余弦相似性:
      sim ⁡ (  Alice, user  1 ) = cos ⁡ (  Alice, user  1 ) = 15 + 3 + 8 + 12 sqrt ⁡ ( 25 + 9 + 16 + 16 ) ∗ sqrt ⁡ ( 9 + 1 + 4 + 9 ) = 0.975 \operatorname{sim}(\text { Alice, user } 1)=\cos (\text { Alice, user } 1)=\frac{15+3+8+12}{\operatorname{sqrt}(25+9+16+16){*\operatorname{sqrt}(9+1+4+9)}}=0.975 sim( Alice, user 1)=cos( Alice, user 1)=sqrt(25+9+16+16)sqrt(9+1+4+9)15+3+8+12=0.975
      计算Alice与user1皮尔逊相关系数:
      Alice_ave = 4 =4 \quad =4 user1_ave = 2.25 =2.25 =2.25
      向量减去均值:
       Alice  ( 1 , − 1 , 0 , 0 )  user  1 ( 0.75 , − 1.25 , − 0.25 , 0.75 ) \text { Alice }(1,-1,0,0) \quad \text { user } 1(0.75,-1.25,-0.25,0.75)  Alice (1,1,0,0) user 1(0.75,1.25,0.25,0.75)
    a = [5, 3, 4, 4]
    b = [3, 1, 2, 3]
    pearsonr(a, b)
    #(0.8528028654224415, 0.14719713457755845)
    

    计算这lia俩新向量的余弦相似度和上面计算过程一致,结果是0.852

    这里我们使用皮尔逊相关系数, 也就是Alice与用户1的相似度是0.85。同样的方式, 我们就可以计算与其他用户的相似度, 这里可以使用numpy的相似度函数得到用户的相似性矩阵:

    users = np.array([[5, 3, 4, 4],[3, 1, 2, 3],[4,3,4,3],[3,3,1,5],[1,5,5,2]])
    cosine_similarity(users)#余弦相似度
    '''
    array([[1.        , 0.9753213 , 0.99224264, 0.89072354, 0.79668736],
           [0.9753213 , 1.        , 0.94362852, 0.91160719, 0.67478587],
           [0.99224264, 0.94362852, 1.        , 0.85280287, 0.85811633],
           [0.89072354, 0.91160719, 0.85280287, 1.        , 0.67082039],
           [0.79668736, 0.67478587, 0.85811633, 0.67082039, 1.        ]])
    '''
    
    np.corrcoef(users)#皮尔逊相关系数
    '''
    array([[ 1.        ,  0.85280287,  0.70710678,  0.        , -0.79211803],
           [ 0.85280287,  1.        ,  0.30151134,  0.42640143, -0.88662069],
           [ 0.70710678,  0.30151134,  1.        , -0.70710678, -0.14002801],
           [ 0.        ,  0.42640143, -0.70710678,  1.        , -0.59408853],
           [-0.79211803, -0.88662069, -0.14002801, -0.59408853,  1.        ]])
    '''       
    

    从上面看出, Alice用户和用户2,用户3,用户4的相似度是0.7,0, -0.79。 所以如果n=2, 找到与Alice最相近的两个用户是用户1, 和Alice的相似度是0.85, 用户2, 和Alice相似度是0.7

    根据相似度用户计算Alice对物品5的最终得分 用户1对物品5的评分是3, 用户2对物品5的打分是5, 那么根据上面的计算公式, 可以计算出Alice对物品5的最终得分是 P A l i c e , 物 品 5 = R ˉ A l i c e + ∑ k = 1 2 ( S A l i c e , u s e r k ( R u s e r k , 物 品 5 − R ˉ u s e r k ) ) ∑ k = 1 2 S A l i c e , u s e r k = 4 + 0.85 ∗ ( 3 − 2.4 ) + 0.7 ∗ ( 5 − 3.8 ) 0.85 + 0.7 = 4.87 P_{Alice, 物品5}=\bar{R}{Alice}+\frac{\sum{k=1}^{2}\left(S_{Alice,user k}\left(R_{userk, 物品5}-\bar{R}{userk}\right)\right)}{\sum{k=1}^{2} S_{Alice, userk}}=4+\frac{0.85*(3-2.4)+0.7*(5-3.8)}{0.85+0.7}=4.87 PAlice,5=RˉAlice+k=12SAlice,userkk=12(SAlice,userk(Ruserk,5Rˉuserk))=4+0.85+0.70.85(32.4)+0.7(53.8)=4.87

    其他计算大同小异,之后根据用户评分对用户进行推荐 这时候, 我们就得到了Alice对物品5的得分是4.87, 根据Alice的打分对物品排个序从大到小: 物 品 1 > 物 品 5 > 物 品 3 = 物 品 4 > 物 品 2 物品1>物品5>物品3=物品4>物品2 1>5>3=4>2 这时候,如果要向Alice推荐2款产品的话, 我们就可以推荐物品1和物品5给Alice

    至此, 基于用户的协同过滤算法原理介绍完毕。

    UserCF编程实现

    这里简单的通过编程实现上面的案例,为后面的大作业做一个热身, 梳理一下上面的过程其实就是三步: 计算用户相似性矩阵、得到前n个相似用户、计算最终得分

    所以我们下面的程序也是分为这三步:

    1. 首先, 先把数据表给建立起来 这里我采用了字典的方式, 之所以没有用pandas, 是因为上面举得这个例子其实是个个例, 在真实情况中, 我们知道, 用户对物品的打分情况并不会这么完整, 会存在大量的空值, 所以矩阵会很稀疏, 这时候用DataFrame, 会有大量的NaN。故这里用字典的形式存储。
      用两个字典, 第一个字典是物品-用户的评分映射, 键是物品1-5, 用A-E来表示, 每一个值又是一个字典, 表示的是每个用户对该物品的打分。 第二个字典是用户-物品的评分映射, 键是上面的五个用户, 用1-5表示, 值是该用户对每个物品的打分。
    # 定义数据集, 也就是那个表格, 注意这里我们采用字典存放数据, 因为实际情况中数据是非常稀疏的, 很少有情况是现在这样
    import pandas as pd
    def loadData():
        items={'A': {1: 5, 2: 3, 3: 4, 4: 3, 5: 1},
               'B': {1: 3, 2: 1, 3: 3, 4: 3, 5: 5},
               'C': {1: 4, 2: 2, 3: 4, 4: 1, 5: 5},
               'D': {1: 4, 2: 3, 3: 3, 4: 5, 5: 2},
               'E': {2: 3, 3: 5, 4: 4, 5: 1}
              }
        users={1: {'A': 5, 'B': 3, 'C': 4, 'D': 4},
               2: {'A': 3, 'B': 1, 'C': 2, 'D': 3, 'E': 3},
               3: {'A': 4, 'B': 3, 'C': 4, 'D': 3, 'E': 5},
               4: {'A': 3, 'B': 3, 'C': 1, 'D': 5, 'E': 4},
               5: {'A': 1, 'B': 5, 'C': 5, 'D': 2, 'E': 1}
              }
        return items,users
    
    items, users = loadData()
    item_df = pd.DataFrame(items).T
    user_df = pd.DataFrame(users).T
    
    item_df
    '''
    	1	2	3	4	5
    A	5.0	3.0	4.0	3.0	1.0
    B	3.0	1.0	3.0	3.0	5.0
    C	4.0	2.0	4.0	1.0	5.0
    D	4.0	3.0	3.0	5.0	2.0
    E	NaN	3.0	5.0	4.0	1.0
    '''
    
    user_df
    '''
    	A	B	C	D	E
    1	5.0	3.0	4.0	4.0	NaN
    2	3.0	1.0	2.0	3.0	3.0
    3	4.0	3.0	4.0	3.0	5.0
    4	3.0	3.0	1.0	5.0	4.0
    5	1.0	5.0	5.0	2.0	1.0
    '''
    
    1. 计算用户相似性矩阵 这个是一个共现矩阵, 5*5,行代表每个用户, 列代表每个用户, 值代表用户和用户的相关性,这里的思路是这样, 因为要求用户和用户两两的相关性, 所以需要用双层循环遍历用户-物品评分数据, 当不是同一个用户的时候, 我们要去遍历物品-用户评分数据, 在里面去找这两个用户同时对该物品评过分的数据放入到这两个用户向量中。
      因为正常情况下会存在很多的NAN, 即可能用户并没有对某个物品进行评分过, 这样的不能当做用户向量的一部分, 没法计算相似性(为空可以看做去掉)。
    """计算用户相似性矩阵"""
    similarity_matrix = pd.DataFrame(np.zeros((len(users), len(users))), index=[1, 2, 3, 4, 5], columns=[1, 2, 3, 4, 5])
    
    # 遍历每条用户-物品评分数据
    for userID in users:
        for otheruserId in users:
            vec_user = []
            vec_otheruser = []
            if userID != otheruserId:
                for itemId in items:   # 遍历物品-用户评分数据
                    itemRatings = items[itemId]        # 这也是个字典  每条数据为所有用户对当前物品的评分
                    if userID in itemRatings and otheruserId in itemRatings:  # 说明两个用户都对该物品评过分
                        vec_user.append(itemRatings[userID])
                        vec_otheruser.append(itemRatings[otheruserId])
                # 这里可以获得相似性矩阵(共现矩阵)
                similarity_matrix[userID][otheruserId] = np.corrcoef(np.array(vec_user), np.array(vec_otheruser))[0][1]
                #similarity_matrix[userID][otheruserId] = cosine_similarity(np.array(vec_user), np.array(vec_otheruser))[0][1]
    
    similarity_matrix#用户相似性矩阵
    ‘‘’
    		1			2			3			4			5
    1	0.000000	0.852803	0.707107	0.000000	-0.792118
    2	0.852803	0.000000	0.467707	0.489956	-0.900149
    3	0.707107	0.467707	0.000000	-0.161165	-0.466569
    4	0.000000	0.489956	-0.161165	0.000000	-0.641503
    5	-0.792118	-0.900149	-0.466569	-0.641503	0.000000
    ‘’’
    

    有了相似性矩阵, 我们就可以得到与Alice最相关的前n个用户。

    1. 计算前n个相似的用户
    """计算前n个相似的用户"""
    n = 2
    similarity_users = similarity_matrix[1].sort_values(ascending=False)[:n].index.tolist()    # [2, 3]   也就是用户1和用户2
    
    similarity_users
    #[2, 3]
    
    1. 计算最终得分 这里就是上面的那个公式了。
    """计算最终得分"""
    base_score = np.mean(np.array([value for value in users[1].values()]))
    weighted_scores = 0.
    corr_values_sum = 0.
    for user in similarity_users:  # [2, 3]
        corr_value = similarity_matrix[1][user]            # 两个用户之间的相似性
        mean_user_score = np.mean(np.array([value for value in users[user].values()]))    # 每个用户的打分平均值
        weighted_scores += corr_value * (users[user]['E']-mean_user_score)      # 加权分数
        corr_values_sum += corr_value
    final_scores = base_score + weighted_scores / corr_values_sum
    print('用户Alice对物品5的打分: ', final_scores)
    user_df.loc[1]['E'] = final_scores
    user_df
    '''
    用户Alice对物品5的打分:  4.871979899370592
    A	B	C	D	E
    1	5.0	3.0	4.0	4.0	4.87198
    2	3.0	1.0	2.0	3.0	3.00000
    3	4.0	3.0	4.0	3.0	5.00000
    4	3.0	3.0	1.0	5.0	4.00000
    5	1.0	5.0	5.0	2.0	1.00000
    '''
    

    至此, 我们就用代码完成了上面的小例子, 有了这个评分, 我们其实就可以对该用户做推荐了。 这其实就是微型版的UserCF的工作过程了。

    注意:基于用户协同过滤的完整代码参考源代码文件中的UserCF.py

    UserCF优缺点

    User-based算法存在两个重大问题:

    1. 数据稀疏性。 一个大型的电子商务推荐系统一般有非常多的物品,用户可能买的其中不到1%的物品,不同用户之间买的物品重叠性较低,导致算法无法找到一个用户的邻居,即偏好相似的用户。这导致UserCF不适用于那些正反馈获取较困难的应用场景(如酒店预订, 大件商品购买等低频应用)
    2. 算法扩展性。 基于用户的协同过滤需要维护用户相似度矩阵以便快速的找出Topn相似用户, 该矩阵的存储开销非常大,存储空间随着用户数量的增加而增加,不适合用户数据量大的情况使用。

    由于UserCF技术上的两点缺陷, 导致很多电商平台并没有采用这种算法, 而是采用了ItemCF算法实现最初的推荐系统。

    基于物品的协同过滤

    基于物品的协同过滤(ItemCF)的基本思想是预先根据所有用户的历史偏好数据计算物品之间的相似性,然后把与用户喜欢的物品相类似的物品推荐给用户。比如物品a和c非常相似,因为喜欢a的用户同时也喜欢c,而用户A喜欢a,所以把c推荐给用户A。

    ItemCF算法并不利用物品的内容属性计算物品之间的相似度, 主要通过分析用户的行为记录计算物品之间的相似度, 该算法认为, 物品a和物品c具有很大的相似度是因为喜欢物品a的用户大都喜欢物品c。
    在这里插入图片描述
    基于物品的协同过滤算法主要分为两步

    • 计算物品之间的相似度
    • 根据物品的相似度和用户的历史行为给用户生成推荐列表(购买了该商品的用户也经常购买的其他商品)

    基于物品的协同过滤算法和基于用户的协同过滤算法很像, 所以我们这里直接还是拿上面Alice的那个例子来看。
    在这里插入图片描述

    如果想知道Alice对物品5打多少分, 基于物品的协同过滤算法会这么做:

    • 首先计算一下物品5和物品1, 2, 3, 4之间的相似性(它们也是向量的形式, 每一列的值就是它们的向量表示, 因为ItemCF认为物品a和物品c具有很大的相似度是因为喜欢物品a的用户大都喜欢物品c, 所以就可以基于每个用户对该物品的打分或者说喜欢程度来向量化物品)
    • 找出与物品5最相近的n个物品
    • 根据Alice对最相近的n个物品的打分去计算对物品5的打分情况

    下面我们就可以具体计算一下,首先是步骤1:

    物品向量 :物品1 (3,4,3,1) \quad 物品2 (1,3,3,5) \quad 物品3 (2,4,1,5) \quad 物品4 (3,3,5,2) 物品5 5(3,5,41)

    下面计算物品5和物品1之间的余弦相似性:
    sim ⁡ (  物品1,   物品5)  = cosine ⁡ (  物品1  ,  物品5  ) = 9 + 20 + 12 + 1 sqrt ⁡ ( 9 + 16 + 9 + 1 ) + sqrt ⁡ ( 9 + 25 + 16 + 1 ) \operatorname{sim}\left(\text { 物品1, } \quad\right. \text { 物品5) }=\operatorname{cosine}(\text { 物品1 }, \quad \text { 物品5 })=\frac{9+20+12+1}{\operatorname{sqrt}(9+16+9+1)+\operatorname{sqrt}(9+25+16+1)} sim( 物品1  物品5) =cosine( 物品, 物品)=sqrt(9+16+9+1)+sqrt(9+25+16+1)9+20+12+1
    皮尔逊相关系数依然是向量先各自减去均值,然后求余弦相似性,

    python计算:

    items = np.array([[3, 4, 3, 1],[1, 3, 3, 5],[2,4,1,5],[3,3,5,2],[3,5,4,1]])
    cols = ['item'+str(i) for i in range(1,6)]
    pd.DataFrame(np.corrcoef(items),columns=cols, index=cols)#皮尔逊相关系数
    '''
    			item1		item2		item3		item4		item5
    item1	1.000000	-0.648886	-0.435286	0.473684	0.969458
    item2	-0.648886	1.000000	0.670820	-0.324443	-0.478091
    item3	-0.435286	0.670820	1.000000	-0.870572	-0.427618
    item4	0.473684	-0.324443	-0.870572	1.000000	0.581675
    item5	0.969458	-0.478091	-0.427618	0.581675	1.000000
    '''
    
    pd.DataFrame(cosine_similarity(items),columns=cols, index=cols)#余弦相似度
    '''
    		item1		item2		item3		item4		item5
    item1	1.000000	0.738988	0.747667	0.936916	0.994100
    item2	0.738988	1.000000	0.933564	0.813629	0.738851
    item3	0.747667	0.933564	1.000000	0.709718	0.722610
    item4	0.936916	0.813629	0.709718	1.000000	0.939558
    item5	0.994100	0.738851	0.722610	0.939558	1.000000
    '''
    

    根据皮尔逊相关系数, 可以找到与物品5最相似的2个物品是item1和item4(n=2), 下面基于上面的公式计算最终得分:

    P A l i c e , 物 品 5 = R ˉ 物 品 5 + ∑ k = 1 2 ( S 物 品 5 , 物 品 k ( R A l i c e , 物 品 k − R ˉ 物 品 k ) ) ∑ k = 1 2 S 物 品 k , 物 品 5 = 13 4 + 0.97 ∗ ( 5 − 3.2 ) + 0.58 ∗ ( 4 − 3.4 ) 0.97 + 0.58 = 4.6 P_{Alice, 物品5}=\bar{R}{物品5}+\frac{\sum{k=1}^{2}\left(S_{物品5,物品 k}\left(R_{Alice, 物品k}-\bar{R}{物品k}\right)\right)}{\sum{k=1}^{2} S_{物品k, 物品5}}=\frac{13}{4}+\frac{0.97*(5-3.2)+0.58*(4-3.4)}{0.97+0.58}=4.6 PAlice,5=Rˉ5+k=12Sk,5k=12(S5,k(RAlice,kRˉk))=413+0.97+0.580.97(53.2)+0.58(43.4)=4.6

    这时候依然可以向Alice推荐物品5。下面也是python编程实现一下, 和之前例子的差不多:

    注:此处的items仍旧为字典形式,并非array

    """计算物品的相似矩阵"""
    similarity_matrix = pd.DataFrame(np.ones((len(items), len(items))), index=['A', 'B', 'C', 'D', 'E'], columns=['A', 'B', 'C', 'D', 'E'])
    #users = np.array([[5, 3, 4, 4],[3, 1, 2, 3],[4,3,4,3],[3,3,1,5],[1,5,5,2]])
    
    # 遍历每条物品-用户评分数据
    for itemId in items:
        for otheritemId in items:
            vec_item = []         # 定义列表, 保存当前两个物品的向量值
            vec_otheritem = []
            #userRagingPairCount = 0     # 两件物品均评过分的用户数
            #print(itemId)
            #print(otheritemId)
            if itemId != otheritemId:    # 物品不同
                for userId in users:    # 遍历用户-物品评分数据
                    userRatings = users[userId]    # 每条数据为该用户对所有物品的评分, 这也是个字典
                    
                    if itemId in userRatings and otheritemId in userRatings:   # 用户对这两个物品都评过分
                        #userRagingPairCount += 1
                        #print(userRatings)
                        #print(itemId)
                        vec_item.append(userRatings[itemId])
                        vec_otheritem.append(userRatings[otheritemId])
                
                # 这里可以获得相似性矩阵(共现矩阵)
                similarity_matrix[itemId][otheritemId] = np.corrcoef(np.array(vec_item), np.array(vec_otheritem))[0][1]
                #similarity_matrix[itemId][otheritemId] = cosine_similarity(np.array(vec_item), np.array(vec_otheritem))[0][1]
    
    similarity_matrix
    '''
    	A			B			C			D			E
    A	1.000000	-0.476731	-0.123091	0.532181	0.969458
    B	-0.476731	1.000000	0.645497	-0.310087	-0.478091
    C	-0.123091	0.645497	1.000000	-0.720577	-0.427618
    D	0.532181	-0.310087	-0.720577	1.000000	0.581675
    E	0.969458	-0.478091	-0.427618	0.581675	1.000000
    '''
    

    然后也是得到与物品5相似的前n个物品, 计算出最终得分来。

    """得到与物品5相似的前n个物品"""
    n = 2
    similarity_items = similarity_matrix['E'].sort_values(ascending=False)[:n].index.tolist()       # ['A', 'D']
    
    """计算最终得分"""
    base_score = np.mean(np.array([value for value in items['E'].values()]))
    weighted_scores = 0.
    corr_values_sum = 0.
    for item in similarity_items:  # ['A', 'D']
        corr_value = similarity_matrix['E'][item]            # 两个物品之间的相似性
        mean_item_score = np.mean(np.array([value for value in items[item].values()]))    # 每个物品的打分平均值
        weighted_scores += corr_value * (users[1][item]-mean_item_score)      # 加权分数
        corr_values_sum += corr_value
    final_scores = base_score + weighted_scores / corr_values_sum
    print('用户Alice对物品5的打分: ', final_scores)
    user_df.loc[1]['E'] = final_scores
    user_df
    

    完整代码参见:itemcf.py

    算法评估

    由于UserCF和ItemCF结果评估部分是共性知识点, 所以在这里统一标识。 这里介绍评测指标:

    1. 召回率

    对用户u推荐N个物品记为 R ( u ) R(u) R(u), 令用户u在测试集上喜欢的物品集合为 T ( u ) T(u) T(u), 那么召回率定义为: Recall ⁡ = ∑ u ∣ R ( u ) ∩ T ( u ) ∣ ∑ u ∣ T ( u ) ∣ \operatorname{Recall}=\frac{\sum_{u}|R(u) \cap T(u)|}{\sum_{u}|T(u)|} Recall=uT(u)uR(u)T(u) 这个意思就是说, 在用户真实购买或者看过的影片里面, 我模型真正预测出了多少, 这个考察的是模型推荐的一个全面性。

    1. 准确率 准确率定义为: Precision ⁡ = ∑ u ∣ R ( u ) ∩ T ( u ) ∣ ∑ u ∣ R ( u ) ∣ \operatorname{Precision}=\frac{\sum_{u} \mid R(u) \cap T(u)|}{\sum_{u}|R(u)|} Precision=uR(u)uR(u)T(u) 这个意思再说, 在我推荐的所有物品中, 用户真正看的有多少, 这个考察的是我模型推荐的一个准确性。 为了提高准确率, 模型需要把非常有把握的才对用户进行推荐, 所以这时候就减少了推荐的数量, 而这往往就损失了全面性, 真正预测出来的会非常少,所以实际应用中应该综合考虑两者的平衡。

    2. 覆盖率 覆盖率反映了推荐算法发掘长尾的能力, 覆盖率越高, 说明推荐算法越能将长尾中的物品推荐给用户。  Coverage  = ∣ ⋃ u ∈ U R ( u ) ∣ ∣ I ∣ \text { Coverage }=\frac{\left|\bigcup_{u \in U} R(u)\right|}{|I|}  Coverage =IuUR(u)

    3. 该覆盖率表示最终的推荐列表中包含多大比例的物品。如果所有物品都被给推荐给至少一个用户, 那么覆盖率是100%。

    4. 新颖度 用推荐列表中物品的平均流行度度量推荐结果的新颖度。 如果推荐出的物品都很热门, 说明推荐的新颖度较低。 由于物品的流行度分布呈长尾分布, 所以为了流行度的平均值更加稳定, 在计算平均流行度时对每个物品的流行度取对数。

    协同过滤算法的权重改进

    算法1:
    w i j = ∣ N ( i ) ∩ N ( j ) ∣ ∣ N ( i ) ∣ w_{i j}=\frac{|N(i) \cap N(j)|}{|N(i)|} wij=N(i)N(i)N(j)

    算法2:
    w i j = ∣ N ( i ) ∩ N ( j ) ∣ ∣ N ( i ) ∣ ∣ N ( j ) ∣ w_{i j}=\frac{|N(i) \cap N(j)|}{\sqrt{|N(i)||N(j)|}} wij=N(i)N(j) N(i)N(j)

    算法3:
    w i j = ∣ N ( i ) ∩ N ( j ) ∣ ∣ N ( i ) ∣ ∣ − α ∣ N ( j ) ∣ α w_{i j}=\frac{|N(i) \cap N(j)|}{|N(i)|^{\mid-\alpha}|N(j)|^{\alpha}} wij=N(i)αN(j)αN(i)N(j)

    算法4:
    w i j = ∑ ν ∈ N ( i ) ∩ N ( j ) 1 log ⁡ 1 + ∣ N ( u ) ∣ ∣ N ( i ) ∣ 1 − α ∣ N ( j ) ∣ α w_{i j}=\frac{\sum_{\nu \in N(i) \cap N(j)} \frac{1}{\log 1+|N(u)|}}{|N(i)|^{1-\alpha}|N(j)|^{\alpha}} wij=N(i)1αN(j)ανN(i)N(j)log1+N(u)1

    • 基础算法 1为最简单的计算物品相关度的公式, 分子为同时喜好itemi和itemj的用户数
    • 对热门物品的惩罚 图1存在一个问题, 如果 item-j 是很热门的商品,导致很多喜欢 item-i 的用户都喜欢 item-j,这时 w i j w_{ij} wij 就会非常大。同样,几乎所有的物品都和 item-j 的相关度非常高,这显然是不合理的。所以算法2中分母通过引入 N ( j ) N(j) N(j) 来对 item-j 的热度进行惩罚。如果物品很热门, 那么 N ( j ) N(j) N(j)就会越大, 对应的权重就会变小。
    • 对热门物品的进一步惩罚 如果 item-j 极度热门,上面的算法还是不够的。举个例子,《Harry Potter》非常火,买任何一本书的人都会购买它,即使通过算法2的方法对它进行了惩罚,但是《Harry Potter》仍然会获得很高的相似度。这就是推荐系统领域著名的 Harry Potter Problem。
    • 如果需要进一步对热门物品惩罚,可以继续修改公式为如算法3所示,通过调节参数 α α α,$α $越大,惩罚力度越大,热门物品的相似度越低,整体结果的平均热门程度越低。
    • 对活跃用户的惩罚 同样的,Item-based CF 也需要考虑活跃用户(即一个活跃用户(专门做刷单)可能买了非常多的物品)的影响,活跃用户对物品相似度的贡献应该小于不活跃用户。算法4为集合了该权重的算法。

    协同过滤算法的问题分析

    协同过滤算法存在的问题之一就是泛化能力弱, 即协同过滤无法将两个物品相似的信息推广到其他物品的相似性上。 导致的问题是热门物品具有很强的头部效应, 容易跟大量物品产生相似, 而尾部物品由于特征向量稀疏, 导致很少被推荐。 比如下面这个例子:
    在这里插入图片描述

    A, B, C, D是物品, 看右边的物品共现矩阵, 可以发现物品D与A、B、C的相似度比较大, 所以很有可能将D推荐给用过A、B、C的用户。 但是物品D与其他物品相似的原因是因为D是一件热门商品, 系统无法找出A、B、C之间相似性的原因是其特征太稀疏, 缺乏相似性计算的直接数据。 所以这就是协同过滤的天然缺陷:推荐系统头部效应明显, 处理稀疏向量的能力弱。

    为了解决这个问题, 同时增加模型的泛化能力,2006年,矩阵分解技术(Matrix Factorization,MF)被提出, 该方法在协同过滤共现矩阵的基础上, 使用更稠密的隐向量表示用户和物品, 挖掘用户和物品的隐含兴趣和隐含特征, 在一定程度上弥补协同过滤模型处理稀疏矩阵能力不足的问题。详细参见下一个博客~

    Question

    1.什么时候使用UserCF,什么时候使用ItemCF?为什么?

    答案来自:项亮推荐系统实践

    • UserCF 由于是基于用户相似度进行推荐, 所以具备更强的社交特性, 这样的特点非常适于用户少, 物品多, 时效性较强的场合, 比如新闻推荐场景, 因为新闻本身兴趣点分散, 相比用户对不同新闻的兴趣偏好, 新闻的及时性,热点性往往更加重要, 所以正好适用于发现热点,跟踪热点的趋势。

      另外还具有推荐新信息的能力, 更有可能发现惊喜, 因为看的是人与人的相似性, 推出来的结果可能更有惊喜,可以发现用户潜在但自己尚未察觉的兴趣爱好。

    对于用户较少, 要求时效性较强的场合, 就可以考虑UserCF。

    • ItemCF 这个更适用于兴趣变化较为稳定的应用, 更接近于个性化的推荐, 适合物品少,用户多,用户兴趣固定持久, 物品更新速度不是太快的场合, 比如推荐艺术品, 音乐, 电影。

    2.协同过滤在计算上有什么缺点?有什么比较好的思路可以解决(缓解)?

    较差的稀疏向量处理能力

    第一个问题就是泛化能力弱, 即协同过滤无法将两个物品相似的信息推广到其他物品的相似性上。 导致的问题是热门物品具有很强的头部效应, 容易跟大量物品产生相似, 而尾部物品由于特征向量稀疏, 导致很少被推荐。 比如下面这个例子:
    图片

    A, B, C, D是物品, 看右边的物品共现矩阵, 可以发现物品D与A、B、C的相似度比较大, 所以很有可能将D推荐给用过A、B、C的用户。 但是物品D与其他物品相似的原因是因为D是一件热门商品, 系统无法找出A、B、C之间相似性的原因是其特征太稀疏, 缺乏相似性计算的直接数据。 所以这就是协同过滤的天然缺陷:推荐系统头部效应明显, 处理稀疏向量的能力弱。

    为了解决这个问题, 同时增加模型的泛化能力,2006年,矩阵分解技术(Matrix Factorization,MF)被提出, 该方法在协同过滤共现矩阵的基础上, 使用更稠密的隐向量表示用户和物品, 挖掘用户和物品的隐含兴趣和隐含特征, 在一定程度上弥补协同过滤模型处理稀疏矩阵能力不足的问题。

    3.上面介绍的相似度计算方法有什么优劣之处?

    cosine相似度还是比较常用的, 一般效果也不会太差, 但是对于评分数据不规范的时候, 也就是说, 存在有的用户喜欢打高分, 有的用户喜欢打低分情况的时候,有的用户喜欢乱打分的情况, 这时候consine相似度算出来的结果可能就不是那么准确了, 比如下面这种情况:

    x Y z d 4 4 5 e 1 1 2 f 4 1 5 \begin{array}{|l|l|l|l|} \hline & x & Y & z \\ \hline d & 4 & 4 & 5 \\ \hline e & 1 & 1 & 2 \\ \hline f & 4 & 1 & 5 \\ \hline \end{array} defx414Y411z525

    这时候, 如果用余弦相似度进行计算, 会发现用户d和用户f比较相似, 而实际上, 如果看这个商品喜好的一个趋势的话, 其实d和e比较相近, 只不过e比较喜欢打低分, d比较喜欢打高分。 所以对于这种用户评分偏置的情况, 余弦相似度就不是那么好了, 可以考虑使用下面的皮尔逊相关系数。

    4.协同过滤还存在其他什么缺陷?有什么比较好的思路可以解决(缓解)?

    • 协同过滤的特点就是完全没有利用到物品本身或者是用户自身的属性, 仅仅利用了用户与物品的交互信息就可以实现推荐,比较简单高效, 但这也是它的一个短板所在, 由于无法有效的引入用户年龄, 性别,商品描述,商品分类,当前时间,地点等一系列用户特征、物品特征和上下文特征, 这就造成了有效信息的遗漏,不能充分利用其它特征数据。

    • 为了解决这个问题, 在推荐模型中引用更多的特征,推荐系统慢慢的从以协同过滤为核心到了以逻辑回归模型为核心, 提出了能够综合不同类型特征的机器学习模型。
      在这里插入图片描述

    推荐阅读:推荐系统实战(二)

    展开全文
  • 权限梳理 Oracle数据库和mysql数据库都有自己的权限管理。Mysql数据库的权限管理相对oracle数据库的权限管理会更简洁。 Mysql的权限认证是通过查询权限表直接验证的,不过mysql 的权限表是有等级的。在验证的过程...

    权限梳理

    Oracle数据库和mysql数据库都有自己的权限管理。Mysql数据库的权限管理相对oracle数据库的权限管理会更简洁。

    Mysql的权限认证是通过查询权限表直接验证的,不过mysql 的权限表是有等级的。在验证的过程中,先验证等级较高的权限表,如果通过验证则放行,不通过会继续验证等级次高的权限表,如果最低的权限表都没有通过,那么系统会判定该用户没有执行这条命令的权限。

    Oracle数据库有一套基于权限-角色-用户的权限系统。即:权限可以赋予角色,角色可以赋予用户,用户就可以得到该角色的的全部权限。当然也可以单独为用户添加某个权限。

    Mysql和oracle数据库的用户和表的关系也是不一样的。Mysql一个用户,多个数据库,每个数据库拥有各自的表,而Oracle一个数据库,多个用户,每个用户拥有各自的表(数据库对象)

    Mysql数据库的权限表都放在mysql库中,其中

    1. mysql.user表存储全局权限,适用于一个给定服务器中的所有数据库
    2. mysql.dbmysql.host表存储数据库权限,适用于一个给定数据库中的所有目标。
    3. mysql.tables_priv表存储表权限,适用于一个给定表中的所有列
    4. mysql.columns_priv表存储列权限,适用于一个给定表中的单一列。这些权限存储在中。

    ORACLE系统提供三种权限:Object 对象级、System 系统级、Role 角色级。

    1. Oracle 的角色存放在表 dba_roles
    2. 某角色包含的系统权限存放在  表dba_sys_privs
    3. 包含的对象权限存放在 表dba_tab_privs

    具体的权限这里就不列举了

    oracle可以参考官网给出的列表:https://docs.oracle.com/en/database/oracle/oracle-database/19/sqlrf/GRANT.html#GUID-20B4E2C0-A7F8-4BC8-A5E8-BE61BDC41AC3  表18-1和表18-2

    mydql可以参考官网给出的列表:https://dev.mysql.com/doc/refman/5.7/en/grant-tables.html#grant-tables-user-db

    系统表梳理

    系统表的梳理主要关注的是有关于元数据的系统表,至于其他的系统表这里暂不做梳理。

    Mysql数据库的information_schema是一个元数据库。它就像物业公司的信息库,对管理的每栋大厦有多少电梯、电梯型号、每个房间的长宽高等等了如指掌。

    常见的元信息表有:

    1. SCHEMATA提供数据库信息,有哪些数据库,字符集是GBK还是UTF-8等等。
    2. TABLES  提供表的信息,数据库有哪些表,是什么存储引擎等等。
    3. COLUMNS   提供字段的信息,有哪些字段字段类型是什么等等。
    4. STATISTICS  提供索引信息 表中有那些索引,索引的字段、类型等。
    5. TABLE_CONSTRAINTS   提供表的约束情况
    6. KEY_COLUMN_USAGE  提供主键、外键、唯一约束等信息
    7. ROUTINES 提供函数和存储过程的信息
    8. VIEWS 提供数据库下所有视图信息
    9. TRIGGERS  提供所有的触发器情况

    Oracle数据库将元数据放到静态数据字典视图。Oracle元数据获取可以通过静态数据字典视图来获取。

    Oracle数据库的元数据表同种类型的一般有三张表,前缀分别是:DBA 表示数据库中所有的 ALL 表示当前用户可访问的所有 USER表示当前用户拥有的

    【参考:https://docs.oracle.com/cd/B19306_01/server.102/b14237/statviews_2094.htm

    1. XX_TAB_COMMENTS  显示表和视图的元数据
    2. XX_TAB_COLUMNS 显示表、视图和集群的列
    3. XX_CONS_COLUMNS 显示约束的元数据

    突发奇想

    当面对一个数据表很大的时候,我们的业务需求需要查询一些数据,但是这些数据并不是数据表里的数据,或许是一些统计数据,例如想拿到一张表的数据行数、数据表中的字节数等数据。我们去查询数据表效果是不理想的,因为查询的数据表如果数据量很大,那么我们会在这个查询中消耗较长的时间。

    建议如下:如果我们想拿到的数据并不是数据库表中的数据,那么我们可以查找数据库的元数据来拿到我们想要的数据,比如想拿到数据库中的行数,我们可以查询数据库中table表的元数据,里面会有行数据的统计。相对于使用count来查询表,效率和时间都会有很大的提升。

    展开全文
  • # 定义数据集, 也就是那个表格, 注意这里我们采用字典存放数据, 因为实际情况中数据是非常稀疏的, 很少有情况是现在这样 def loadData(): rating_data={1: {'A': 5, 'B': 3, 'C': 4, 'D': 4}, 2: {'A': 3, 'B'...
  • 资料搜集-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。
    

     

     

     

    展开全文
  • 数据管理注意事项: • 您能否在 CDP 中构建和维护数据字典? • 自动执行数据标准有哪些选择? • CDP 是否有版本控制? • CDP 是否有工具包来识别和修复不良数据? • 您是否需要为自定义流程或编码工具导出跟踪...

    商业的未来愈发数字化,也愈发复杂化。客户与品牌互动的方式比以往任何时候都多,为了管理所有这些互动数据,您需要一套可以不断扩展的工具。谁能够快速将这些数据转化为洞察,并最终营造卓越的客户体验,谁就可以在市场上创造持久的优势。

    客户数据平台 (CDP)可帮助企业收集、标准化、统一和激活其客户数据,更全面地了解客户,并使整个组织能够做出数据驱动的决策,改善客户体验,并在竞争中保持领先地位。然而,市场上有许多 CDP,产品优劣各异,各有特点,选择合适的 CDP 是一项十分艰巨的任务。

    企业立项提出需求时,有一个专业术语叫做RFP,就是英文“request for proposal”的首字母缩写,意为需求说明书。我们已经帮助了数百家公司梳理出了反映他们需求的RFP,本文中,我们将带您了解创建CDP RFP 有效框架和最佳做法。

    Step 1  确定目标

    您选择的 CDP 将成为您组织数据基础架构的基础。 CDP 提供的可靠数据可以作为当前和未来技术栈以及您的数据策略的构建模块。

    在编写 RFP 之前,定义公司的目标、期望的结果以及组织需要的 CDP 类型至关重要。只要这些方面确定了,CDP提供商的选择思路也就基本确定了。

    a.定义公司上线CDP的目标

    首先,明确定义实施 CDP 的总体目标。这些目标通常来源于影响业务增长的特定痛点。经过评估数百家公司的CDP需求,我们发现最常见的三个共同目标是:

    • 借助营销技术获得快速响应和创新的能力

    设定这一目标背后的痛点一般是,您用来创建和衡量客户体验的工具套件不够灵活或者难以实现大规模管理。

    • 帮助团队做出更好的、数据驱动的决策

    设定这个目标,您的痛点可能是您的业务团队(营销人员、销售人员和高管)需要很长时间才能获得他们需要的数据或者洞察。这些非技术员工可能需要等待数天、数周甚至数月,来让技术人员或一线人员完成收集数据。

    • 分渠道个性化客户体验

    激发这一目标的三个主要痛点往往是:

    • 基本没有实现个性化。

    • 无法在已完成的个性化触达之间创造无缝的客户体验。

    • 对于一些劳动力和成本密集型渠道,希望制定一些数据监测用以计算投资回报率。

    这些目标看上去很宽泛,但它们实实在在来源于许多影响市场运营人员日常工作的真实的痛点。所制定的目标应该既有较广的覆盖面,又源自需要解决的明确问题。

    b.定义所期待的业务成果

    对于您描述的每个目标,确定您希望在实现所述目标后看到的具体业务成果。仔细考虑您的 CDP 应该改进的成功指标、效率和 KPI。让我们以刚刚概述的共同目标为基础,探索每个目标的共同业务成果:

    • 借助营销技术获得快速响应和创新的能力

    相关业务成果:

    • 提高投资回报率,并加快技术投资的价值实现。

    • 通过消除繁琐的工作来提高工程团队的生产力。

    • 通过避免转换成本和供应商锁定来节省资金。

    • 降低隐私和安全风险。

    • 通过从一个平台自动收集、清理、管理和激活所有数据和用户事件来提高效率。

    • 帮助团队做出更好的、数据驱动的决策

    相关业务成果:

    • 提高跨团队的生产力、效率和数据驱动的决策。

    • 提高整个公司的客户数据可访问性。

    • 分渠道个性化客户体验

    相关业务成果:

    • 改进整个客户旅程的指标,包括关键收入、客户获取和保留目标。

    • 改进营销和广告 KPI,例如客户获取成本 (CAC)、广告支出回报率 (ROAS) 等。

    • 提高客户参与度(customer engagement)指标,例如全生命周期价值 (LTV)、客户满意度 (CSAT)、净推荐值 (NPS) 和实时互动情况。

    c.确定可帮助您实现目标的 CDP 类型

    下一步,就是确定,为了实现企业核心目标,该选哪种类型的 CDP呢?对不同类型的 CDP 进行评估,对于确保所有内部团队都能获得所需的可信数据来说,至关重要。市场上有许多不同类型的CDP,我们分析大概分属四类:

    • 数据导向的 CDP:专注于确保企业的每个团队(营销、产品、分析等职能)都可以依靠客户数据来实现增长目标。他们非常重视从数百个来源收集和管理数据。凭借高保真高精细级别的数据质量,他们可以帮助您在整个组织和所有工具中激活数据,以解决复杂的挑战,例如全渠道个性化和多点触控归因等等。
    • 营销campaign导向 CDP:仅将客户数据用于营销活动。它们通常专门围绕营销人员的需求设计,解决一些特定营销应用场景问题。campaign导向型 CDP 通常不是企业的基础设施,不是为整个组织收集和管理数据的。
    • 特定应用场景 CDP:专注于特定应用场景的数据收集和管理。许多分析套件和客户关系管理 (CRM) 平台提供类似 CDP 的功能,可帮助公司更好地利用其平台。但是,与前两类 CDP 相比,这些 CRM 和分析工具的范围和功能往往有限。
    • 特殊 CDP: 专注于解决特殊行业或不常见应用场景的问题。

    Step 2 定义应用场景和技术要求

    CDP 可以解决许多团队遇到的许多挑战。选择合适的供应商的重要一步,就是要征求这些团队关于CDP的使用场景和技术要求的意见。

    将跨职能的应用场景和需求清晰地表达出来,可以缩小您的 CDP 选择范围。那么如何梳理跨团队的应用场景和需求呢?

    a.组建跨职能的需求整合团队,并指定项目负责人

    CDP 是一种跨部门的工具,因此选择关键利益相关者组成的多元化团队将有助于确保 CDP 实施的长期成功。除了这个团队,您还需要确定谁是项目的负责人(很有可能就是你哦)!

    最理想的需求整合团队包括项目负责人,市场营销代表、数字/客户体验团队代表、数据/技术架构团队代表以及其他技术利益相关方。

    这些团队成员对于确定完整的应用场景和技术要求,甚至对于CDP的成功落地实施至关重要。如果不寻求他们的帮助,您可能会错失关键的评估标准、技术考虑因素和企业级别的应用场景。

    根据该团队确定的优先需求,您还可以将来自以下利益相关方的反馈考虑在内:

    •      产品部

    •      工程部

    •   数据分析部门

    •   客户成功部门

    •      信息安全部

    •      合规部

    •   其他面向客户的团队

    在您组建了 RFP 团队之后,是时候开始合作来定义您的不可协商的应用场景和技术要求了。

    b.定义应用场景

    征求好上文提到的每个利益相关者的需求之后,需要为这些所有应用场景做出优先级排序。下面,我们来列出一些服务过的所有客户中最常见的速赢场景,供您参考:

    借助营销技术获得快速响应和创新的能力

    速赢场景示例:

    • 加快技术栈适配,以及投资回报速度;

    • 自动化设置您的技术栈和新工具;

    • 减少数据集成、数据提取(ETL)与数据准备的工作量;

    • 从一个平台收集、清洗、管理和激活用户数据;

    • 使用可信且一致的数据支持您的技术栈;

    • 提升「赋能增长项目」的速度和效率;

    • 以更加高频、高效的方式跨团队进行试验。

    帮助团队做出更好的、数据驱动的决策

    速赢场景示例:

    • 将数据的收集实现自动化标准化

    • 为团队打造统一共享的数据词典。

    • 帮助团队深入梳理完整的客户旅程。

    • 实现高级分析和可信的数据报告。

    • 使用高级的产品分析与归因分析,以了解新上线的功能或新产品对客户行为、转化率和留存率的影响。

    • 分析和优化营销转化漏斗。

    • 识别定位支持需求,为支持需求排序优先级,并优化支持需求完成的价值。

    • 衡量产品/市场契合度(PMF)。

    • 打破销售和客户支持团队之间的数据孤岛。

    分渠道个性化客户体验

    速赢场景示例:

    • 通过统一跨所有平台、应用程序、第三方工具和渠道的客户接触点,创建单一的客户视图。

    • 识别匿名和追踪用户。

    • 在合适的时间瞄准正确的用户推送正确的信息。

    • 补充丰富用户画像。

    • 跨工具轻松创建和激活受众。

    • 个性化定制跨所有平台的全生命周期客户体验。 这部分场景可能包括以下几个细分场景:

    • 个性化营销内容信息。
    • 个性化生命周期活动(Campaign)。
    • 优化营销和广告支出。
    • 个性化客户服务体验。
    • 个性化产品体验以推动更好的用户参与和产品交叉销售。

    c.定义技术要求

    技术要求这方面的需求内容,指的是您希望 CDP 执行以实现您的目标的具体功能。您需要梳理定义好这些要求,并做出优先级排序。

    技术要求一般来讲可能会涉及下列9个方面,我们也列出了每个方面您需要考虑到的注意事项:

    数据采集​​,指平台跨渠道、跨平台、跨第三方工具采集用户行为数据的能力。

    数据收集注意事项:

    • CDP 需要从您的哪些应用程序、网站、平台、工具、后端资源或服务中(线上和线下)收集数据?

    • CDP 如何防止数据丢失?

    • CDP 如何确保数据可靠性?

    • 通过软件开发工具包 (SDK) 收集数据的流程是什么?

    数据激活是 CDP 向您的团队使用的所有工具和系统提供数据的能力。

    数据激活注意事项:

    • CDP支持哪些第三方工具目标用于数据输出?

    • 哪些集成支持双向数据流动?

    • CDP是否有能力在激活新工具时重放数据?

    • 您的数据仓库和数据湖与 CDP 集成的难易程度如何?

    • CDP是否可以通过附加组件和附加功能进行扩展?

    个性化和身份解析是该平台支持和丰富统一用户配置文件的能力,以便以后用于为每个客户定制化触达。

    个性化和身份解析注意事项:

    • 是否易于将受众划分细分群体?

    • 您是否有能力通过API 构建和联合实时访问受众和用户资料?

    • CDP 是否支持您自定义身份管理规则?

    • 您是否能够使用来自任何数据源的数据来丰富用户画像?

    数据管理包括数据纯净度、数据标准化和Schema约束等方面。这是平台验证、清洗和标准化数据的能力。

    数据管理注意事项:

    • 您能否在 CDP 中构建和维护数据字典?

    • 自动执行数据标准有哪些选择?

    • CDP 是否有版本控制?

    • CDP 是否有工具包来识别和修复不良数据?

    • 您是否需要为自定义流程或编码工具导出跟踪计划?

    隐私、安全和合规是解决平台集中监管合规和隐私规则能力的类别。

    隐私、安全和合规性注意事项:

    • 您是否需要CDP 自动检测和分类PII?

    • 您是否需要能够控制对特定工具的 PII 访问?

    • CPD 是否尊重最终用户的偏好,而不管用户位于何处?

    • 您可以设置和控制用户权限吗?

    平台设计是包括平台的可扩展性、正常运行时间、易用性和其他与平台相关的功能的类别

    设计注意事项:

    • 您是否需要能够通过 API 和命令代码配置和管理 CDP?

    • 您是否需要直观的界面和易于使用的工具?

    • CDP 是否有经过实战检验的数据模型?

    • CDP 处理流量高峰的能力如何?

    • 调试过程是怎样的?

    集成包括平台内的预构建集成,以及使用户能够构建或自定义集成的工具,如 API、SDK 和库。

    集成注意事项:

    • CDP 有哪些现成可用的集成?

    • 您是否需要在标准集成之外扩展平台的能力?

    • 您的所有合作伙伴是否都可以轻松地与该平台集成?

    • 哪些工具和内部系统需要连接到您的 CDP?

    服务是可用于帮助客户长期实施、管理或使用平台的内部或第三方专业服务网络。

    服务注意事项:

    • 实施该平台需要哪些资源?

    • 长期管理CDP 需要哪些资源?

    • 是否可以使用服务选项来加快价值实现和/或平台成功?

    创新力是产品发布和思想领导的步伐。此类别还包括 CDP 供应商帮助您采用新数据策略并跟上变化步伐的能力。

    创新考虑:

    • CDP 的产品路线图是什么?

    • CDP 供应商如何与行业趋势保持一致?

    根据我们总结出的RFP梳理思路,需求主要分为上述这九类。您的类别可能略有不同,但无论您使用哪种类别,您的 RFP 团队都应考虑好您的要求,并列出相关问题。 然后由CDP供应商来仔细检查并为每个类别提供答案。 这种格式将帮助您的供应商更彻底、更有效地回答您的问题,您还可以同时比较不同供应商的给出的答复。

    Step 3 编写您的 RFP

    在这里,我们将简要介绍每个板块的核心目的和主要部分:

     a.公司及项目概况

    贵公司的摘要以及您希望通过此 RFP 流程实现的目标。具体包括以下细节:

    • CDP 评估项目的执行摘要及其重要性

    • 您的目标和您正在寻找 CDP 来解决的挑战

    • 您正在寻找的业务成果以及用于评估这些成果的成功指标

    • 您当前的数据和营销堆栈

    b. RFP 要求

    为完成 RFP 提供清晰、简明的说明列表,例如:

    • 整个 RFP 流程

    • 主要成果

    • 提案格式

    • 主要联系人

    • 如何提问

    • 时间表和重要日期

     c.供应商概览

    为 CDP 供应商提供一个部分,以标准格式包含有关其公司、产品和客户的相关信息。比如:

    • 供应商简介

    • 办公室和团队

    • 他们的使命愿景

    • 产品和服务概览

    • 客户参考和相关客户案例

    • 行业认可

    • 客户团队简介

     d. CDP 应用场景

    列出您的应用场景,并让供应商针对每个应用场景,做出响应。尤其要关注平台如何满足每个应用场景的细节。

    • 技术要求

    列出您的所有技术要求,并为供应商提供响应每个要求的机会。此步骤将确保满足您的主要购买标准。我们还建议要求供应商提供所有可用的开箱即用集成、API 和 SDK 的列表,以确保访问您需要的工具和集成。您也可以要求您的供应商提供相关的架构图。

    • 总体成本

    为供应商提供一个部分,以提供有关其平台总拥有成本的详细信息。这应包括与其平台相关的所有成本,例如平台许可和其他定价机制、设置或实施成本、定制成本、专业服务、培训成本、帐户管理资源和持续支持。

    Step 4 选择合适的长期供应商

    编写好RFP,将其发送给各个供应商并收到回复后,就可以进行评估了。这些最后的步骤旨在指引您缩小最终平台的范围,确认技术要求,选择合适的供应商,并开始您的 CDP 之旅。

     a.回答供应商问题

    您的供应商将花费时间和资源对您的 RFP 进行彻底的响应。在此期间,他们可能会向您询问一些澄清问题。满足他们的要求的最简单方法是在提案的截止日期前与每个供应商进行沟通。

    b.查看供应商的回复并评估

    重组您的 RFP 团队,并安排时间审查 RFP的回复。创建评分流程,并为每个供应商的回答分配分数,以缩小到比较理想的两三个竞争者的范围。

    c. 完整的技术验证

    一旦缩小了决赛入围者的范围,您应该确认他们的平台可以满足您的技术要求。为此,请向您的最终供应商申请一系列深入的演示、概念验证测试 (POC) 或与您的技术团队和利益相关者的试用。

    d.认真审视您的首选供应商的方案细节

    在完全承诺给一个供应商之前,安排时间审查最终细节,例如:

    • 任何悬而未决的问题

    • 最终定价

    • 实施和持续服务

    • 采购流程

    e.做出选择,并制定衡量成功的计划

    在缩小顶级竞争者的范围后,进行选择,并开始准备跨职能团队以进行实施。在实施 CDP 的同时,您还应该与您的客户团队会面以设定季度里程碑,确保实现您的业务目标,并为成功的长期合作伙伴关系奠定基础。

    f.与您的 CDP 供应商继续合作

    CDP不是一个部署好了就再也不用管了的工具。您的 CDP 将成为您整个技术栈中的关键平台,并对您的长期业务增长产生重大影响。您组织中的每个人(营销人员、开发人员到销售人员)都会感受到 CDP 对其日常工作的影响。因此,您与 CDP 供应商的关系应被视为持续的战略业务关系。

    一旦您启动并运行您的 CDP,就应该依靠您的供应商帮助您了解不断变化的市场、树立专业的思想领导力,达到关键 KPI、最大化投资回报率、采用新的应用场景和功能,并继续发展您的数据策略。CDP是一项巨大的长期投资,因此请确保您选择的供应商是您喜欢与之合作的供应商。

    祝您拥有愉快的CDP探索之旅,如果您有任何疑问,欢迎扫描以下二维码联系我们,期待能为您的业务出一份力!

     

    展开全文
  • python编程操作系统篇知识点详细梳理 进程的概念:(Process) 进程就是正在运行的程序,它是操作系统中资源分配的最小单位。 资源分配:操作系统分配的CPU时间片、内存、磁盘空间端口等等资源。 进程号(process ...
  • 分布式系统架构常见知识点梳理

    千次阅读 2020-10-26 10:54:06
    这是从每一个微服务搭建成了分布式的系统来考虑的。 实现原理:Ribbon会从 Eureka Client里获取到对应的服务注册表,也就知道了所有的服务都部署在了哪些机器上,在监听哪些端口号;然后Ribbon就可以使用默认的Round...
  • 区块链字典

    2019-01-21 12:16:15
    「维京资本」与「甲子光年」系统梳理了区块链领域的多个概念 涵盖基本定义、区块 链基础技术、数字货币和法律监管等多个方面。词典由维京研究院和甲子光年旗下研究 院甲子智库历时一个月合作完成。
  • 新入手一个新系统,如何重构和梳理

    千次阅读 2017-07-19 16:58:01
    另外一篇文章,重点是熟悉,写文档https://blog.csdn.net/fei33423/article/details/79762540... (流水系统变存储为业务系统) 2. 两个流程拆分后. 至少会出现1.上游 -- 边界转换类 -- 2.下游支持接口.(业务无关.) ...
  • 教材:王珊 萨师煊 编著 数据库系统概论(第5版) 高等教育出版社 注:文档高清截图在后 7.3 概念设计 1、在需求分析阶段得到的应用需求应首先抽象为信息世界的结构——概念模型,才能更好更准确地运用DBMS实现...
  • 用数据流图和数据字典来分析表达用户需求(结构化分析方法),以数据流图和数据字典作为这个阶段的成果; 概念结构设计:(用结构化的分析方法)抽象数据并设计局部视图,建立分E-R图;集成局部视图,合成总E-R图,...
  • 1、数据库应用系统,通常是指使用数据库的各类信息系统,比如以数据库为基础的各种管理信息系统、办公自动化系统、地理信息系统(GIS)、电子政务系统、电子商务系统等。 2、广义的数据库设计指数据库及其应用系统的...
  • 数据字典汇总

    2015-05-26 18:39:47
    select * from dictionary;   --数据字典 ...数据字典是Oracle存放有关数据库信息的地方,其用途是用来描述数据的。... 比如一个表的创建者信息,...当用户在对数据库中的数据进行操作时遇到困难就可以访问数据字典
  • 教材:王珊 萨师煊 编著 数据库系统概论(第5版) 高等教育出版社 注:文档高清截图在后 第9章 关系查询处理和查询优化 9.1 关系数据库系统的查询处理 1、RDBMS的查询处理分为四个阶段: 【1】查询分析。 对...
  • 本章将对Redis的系统状态信息(info命令结果)和Redis的所有配置 (包括Standalone、Sentinel、Cluster三种模式)做一个全面的梳理,希望本章能够成为 Redis配置统计字典,协助大家分析和解决日常开发和运维中遇到的...
  • 数仓概念梳理

    2021-06-10 10:20:42
    数仓概念梳理 数仓分层 常见分层思路、案例 案例1:互联网金融 ODL层 (Operational Data Layer):操作数据层 ​ 外部数据什么样,该层数据就是什么样(关系型数据库、JSON格式等),部分关系型数据可以直接转IDL层 ...
  • 梳理重点

    2015-10-10 19:24:02
    项目整体管理重点梳理项目整体管理过程负责管理项目的需求、范围、进度、成本、质量、人力资源、沟通、风险和采购。一、启动过程组制定项目章程制定项目范围初步说明书Δ项目整体管理的过程包括如下内容⑴项目启动⑵...
  • 决策树算法梳理

    2019-03-04 14:53:54
    决策树算法梳理1、 信息论基础(熵 联合熵 条件熵 信息增益 基尼不纯度)信息熵:联合熵(Joint Entropy):条件熵:信息增益:基尼不纯度:2、决策树的不同分类算法(ID3算法、C4.5、CART分类树)的原理及应用场景3...
  • 1 功能点字典 基准数据库中,功能点字典也是一种可以建立基准的数据...如何梳理形成软件系统字典库?如何在行业成本度量规范标准的基础上建立快速、分级的软件成本度量?如何将甲乙双方争论的焦点从费用转移到...
  • 数据字典知识整理

    2012-07-02 14:33:11
    数据字典(英语:data dictionary)是一个自动的或手动的存储数据元的定义和属性的文档。 数据字典 - 数据库的重要部分是数据字典。它存放有数据库所用的有关信息,对用户来说是一组只读的表。数据字典内容包括...
  • 这是学习笔记的第1788篇文章如果对MySQL做一些巡检,那么巡检工作该怎么做,当然我们可以想到内核参数,系统配置,数据库参数配置等。这些巡检工作其实对于业务同学来说...
  • Oracle梳理

    2009-04-09 13:35:12
    Oracle服务器包括:Oracle例程和Oracle数据库。 Oracle例程包括:SGA(系统全局区)和后台进程。 系统全局区:数据库高速缓冲区(db_cache_size),重做日志缓冲区(log_buffer),共享...
  • 面向对象中所有的内容的重新梳理,其实面向对象的知识早在一个多月前就学习过并整理过,但是发现还是有所欠缺,故在此以极其简介的语言风格重新梳理一遍 面向对象详细介绍:...
  • 本文是对Python中的列表、元组、字典、集合知识的梳理总结,将Python的重要知识点梳理成条,通过一个简单的员工管理系统(EMS)实战,可以深入的了解Python的基本知识。本文基本上涵盖了在日常使用过程中的语法,没有...
  • 教材:王珊 萨师煊 编著 数据库系统概论(第5版) 高等教育出版社 注:文档高清截图在后 第4章 数据库安全性 4.1 数据库安全性概述 1、数据库的安全性是指:保护数据库,防止不合法的使用造成数据泄露、更改或毁坏。...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 5,749
精华内容 2,299
关键字:

系统字典梳理