深入贯彻闭包思想,全面理解JS闭包形成过程

谈起闭包,它可是JavaScript两个核心技术之一(异步和闭包),在面试以及实际应用当中,我们都离不开它们,甚至可以说它们是衡量js工程师实力的一个重要指标。下面我们就罗列闭包的几个常见问题,从回答问题的角度来理解和定义你们心中的闭包

问题如下:

1.什么是闭包?

2.闭包的原理可不可以说一下? 

3.你是怎样使用闭包的?

闭包的介绍

我们先看看几本书中的大致介绍:

1.闭包是指有权访问另一个函数作用域中的变量的函数

2.函数对象可以通过作用域关联起来,函数体内的变量都可以保存在函数作用域内,这在计算机科学文献中称为“闭包”,所有的javascirpt函数都是闭包

3.闭包是基于词法作用域书写代码时所产生的必然结果

4.. 函数可以通过作用域链相互关联起来,函数内部的变量可以保存在其他函数作用域内,这种特性在计算机科学文献中称为闭包

可见,它们各有各自的定义,但要说明的意思大同小异。笔者在这之前对它是知其然而不知其所以然,最后用了一天的时间从词法作用域到作用域链的概念再到闭包的形成做了一次总的梳理,发现做人好清晰了…。

下面让我们抛开这些抽象而又晦涩难懂的表述,从头开始理解,内化最后总结出自己的一段关于闭包的句子。我想这对面试以及充实开发者自身的理论知识非常有帮助。

闭包的构成

词法作用域

要理解词法作用域,我们不得不说起JS的编译阶段,大家都知道JS是弱类型语言,所谓弱类型是指不用预定义变量的储存类型,并不能完全概括JS或与其他语言的区别,在这里我们引用黄皮书(《你不知道的javascript》)上的给出的解释编译语言

编译语言

编译语言在执行之前必须要经历三个阶段,这三个阶段就像过滤器一样,把我们写的代码转换成语言内部特定的可执行代码。就比如我们写的代码是var a = 1;,而JS引擎内部定义的格式是var,a,=,1 那在编译阶段就需要把它们进行转换。这只是一个比喻,而事实上这只是在编译阶段的第一个阶段所做的事情。下面我们概括一下,三个阶段分别做了些什么。

  1. 分词/词法分析(Tokenizing/Lexing)
    这就是我们上面讲的一样,其实我们写的代码就是字符串,在编译的第一个阶段里,把这些字符串转成词法单元(toekn)词法单元我们可以想象成我们上面分解的表达式那样。(注意这个步骤有两种可能性,当前这属于分词,而词法分析,会在下面和词法作用域一起说。)

  2. 解析/语法分析(Parsing)
    在有了词法单元之后,JS还需要继续分解代码中的语法以便为JS引擎减轻负担(总不能在引擎运行的过程中让它承受这么多轮的转换规则吧?) ,通过词法单元生成了一个抽象语法树(Abstract Syntax Tree),它的作用是为JS引擎构造出一份程序语法树,我们简称为AST。这时我们不禁联想到Dom树(扯得有点远),没错它们都是,以var,a,=,1为例,它会以为单元划分他们,例如: 顶层有一个 stepA 里面包含着 “v”,stepA下面有一个stepB,stepB中含有 “a”,就这样一层一层嵌套下去….

  3. 代码生成(raw code)
    这个阶段主要做的就是拿AST来生成一份JS语言内部认可的代码(这是语言内部制定的,并不是二进制哦),在生成的过程中,编译器还会询问作用域的问题,还是以 var a = 1;为例,编译器首先会询问作用域,当前有没有变量a,如果有则忽略,否则在当前作用域下创建一个名叫a的变量.

词法阶段

哈哈,终于到了词法阶段,是不是看了上面的三大阶段,甚是懵逼,没想到js还会有这样繁琐的经历? 其实,上面的概括只是所有编译语言的最基本的流程,对于我们的JS而言,它在编译阶段做的事情可不仅仅是那些,它会提前为js引擎做一些性能优化等工作,总之,编译器把所有脏活累活全干遍了

要说到词法阶段这个概念,我们还要结合上面未结的分词/词法分析阶段.来说…

词法作用域是发生在编译阶段的第一个步骤当中,也就是分词/词法分析阶段。它有两种可能,分词和词法分析,分词是无状态的,而词法分析是有状态的。

那我们如何判断有无状态呢?以 var a = 1为例,如果词法单元生成器在判断a是否为一个独立的词法单元时,调用的是有状态的解析规则(生成器不清楚它是否依赖于其他词法单元,所以要进一步解析)。反之,如果它不用生成器判断,是一条不用被赋予语意的代码(暂时可以理解为不涉及作用域的代码,因为js内部定义什么样的规则我们并不清楚),那就被列入分词中了。

这下我们知道,如果词法单元生成器拿不准当前词法单元是否为独立的,就进入词法分析,否则就进入分词阶段。

没错,这就是理解词法作用域及其名称来历的基础。

简单的说,词法作用域就是定义在词法阶段的作用域。词法作用域就是你编写代码时,变量和块级作用域写在哪里决定的。当词法解析器(这里只当作是解析词法的解析器,后续会有介绍)处理代码时,会保持作用域不变(除动态作用域)。

在这一小节中,我们只需要了解:

  1. 词法作用域是什么?

  2. 词法阶段中 分词/词法分析的概念?

  3. 它们对词法作用域的形成有哪些影响?

这节有两个个忽略掉的知识点(词法解析器,动态作用域),因主题限制没有写出来,以后有机会为大家介绍。下面开始作用域。

作用域链

1. 执行环境

执行环境定义了变量或函数有权访问的其他数据。

环境栈可以暂时理解为一个数组(JS引擎的一个储存栈)。

在web浏览器中,全局环境即window是最外层的执行环境,而每个函数也都有自己的执行环境,当调用一个函数的时候,函数会被推入到一个环境栈中,当他以及依赖成员都执行完毕之后,栈就将其环境弹出,

先看一个图 !

环境栈也有人称做它为函数调用栈(都是一回事,只不过后者的命名方式更倾向于函数),这里我们统称为栈。位于环境栈中最外层是 window , 它只有在关闭浏览器时才会从栈中销毁。而每个函数都有自己的执行环境,

到这里我们应该知道:

  1. 每个函数都有一个与之对应的执行环境。

  2. 当函数执行时,会把当前函数的环境押入环境栈中,把当前函数执行完毕,则摧毁这个环境。

  3. window 全局对象时栈中对外层的(相对于图片来说,就是最下面的)。

  4. 函数调用栈与环境栈的区别 。 这两者就好像是 JS中原始类型和基础类型 | 引用类型与对象类型与复合类型 汗!

2. 变量对象与活动对象

执行环境,所谓环境我们不难联想到房子这一概念。没错,它就像是一个大房子,它不是独立的,它会为了完成更多的任务而携带或关联其他的概念。

每个执行环境都有一个表示变量的对象——-变量对象,这个对象里储存着在当前环境中所有的变量和函数

变量对象对于执行环境来说很重要,它在函数执行之前被创建。它包含着当前函数中所有的参数变量函数。这个创建变量对象的过程实际就是函数内数据(函数参数、内部变量、内部函数)初始化的过程。

在没有执行当前环境之前,变量对象中的属性都不能访问!但是进入执行阶段之后,变量对象转变为了活动对象,里面的属性都能被访问了,然后开始进行执行阶段的操作。所以活动对象实际就是变量对象在真正执行时的另一种形式。

 function fun (a){
    var n = 12;
    function toStr(a){
        return String(a);
    }
 }

在 fun 函数的环境中,有三个变量对象(压入环境栈之前),首先是arguments,变量n 与 函数 toStr ,压入环境栈之后(在执行阶段),他们都属于fun的活动对象。 活动对象在最开始时,只包含一个变量,即argumens对象。

到这里我们应该知道:

  1. 每个执行环境有一个与之对应的变量对象

  2. 环境中定义的所有变量和函数都保存在这个对象里。

  3. 对于函数,执行前的初始化阶段叫变量对象,执行中就变成了活动对象

3. 作用域链

当代码在一个环境中执行时,会创建变量对象的一个作用域链。用数据格式表达作用域链的结构如下。

[{当前环境的变量对象},{外层变量对象},{外层的外层的变量对象}, {window全局变量对象}] 每个数组单元就是作用域链的一块,这个块就是我们的变量对象。

作用于链的前端,始终都是当前执行的代码所在环境的变量对象。全局执行环境的变量对象也始终都是链的最后一个对象。

    function foo(){
        var a = 12;
        fun(a);
        function fun(a){
             var b = 8;
              console.log(a + b);
        }
    }  
    
   foo();

再来看上面这个简单的例子,我们可以先思考一下,每个执行环境下的变量对象都是什么? 这两个函数它们的变量对象分别都是什么?

我们以fun为例,当我们调用它时,会创建一个包含 arguments,a,b的活动对象,对于函数而言,在执行的最开始阶段它的活动对象里只包含一个变量,即arguments(当执行流进入,再创建其他的活动对象)。

在活动对象中,它依然表示当前参数集合。对于函数的活动对象,我们可以想象成两部分,一个是固定的arguments对象,另一部分是函数中的局部变量。而在此例中,a和b都被算入是局部变量中,即便a已经包含在了arguments中,但他还是属于。

有没有发现在环境栈中,所有的执行环境都可以组成相对应的作用域链。我们可以在环境栈中非常直观的拼接成一个相对作用域链。

下面我们大致说下这段代码的执行流程:

  1. 在创建foo的时候,作用域链已经预先包含了一个全局对象,并保存在内部属性[[ Scope ]]当中。

  2. 执行foo函数,创建执行环境与活动对象后,取出函数的内部属性[[Scope]]构建当前环境的作用域链(取出后,只有全局变量对象,然后此时追加了一个它自己的活动对象)。

  3. 执行过程中遇到了fun,从而继续对fun使用上一步的操作。

  4. fun执行结束,移出环境栈。foo因此也执行完毕,继续移出。

  5. javscript 监听到foo没有被任何变量所引用,开始实施垃圾回收机制,清空占用内存。

作用域链其实就是引用了当前执行环境的变量对象的指针列表,它只是引用,但不是包含。,因为它的形状像链条,它的执行过程也非常符合,所以我们都称之为作用域,而当我们弄懂了这其中的奥秘,就可以抛开这种形式上的束缚,从原理上出发。

到这里我们应该知道:

  1. 什么是作用域链。

  2. 作用域链的形成流程。

  3. 内部属性 [[Scope]] 的概念。

使用闭包

从头到尾,我们把涉及到的技术点都过了一遍,写的不太详细也有些不准确,因为没有经过事实的论证,我们只大概了解了这个过程概念。

涉及的理论充实了,那么现在我们就要使用它了。 先上几个最简单的计数器例子:

 var counter = (!function(){
    var num = 0;
    return function(){ return  ++num; }
 }())

 function counter(){
        var num = 0;
        return {
            reset:function(){
                num = 0;
            },
            count:function(){
                return num++;    
            }
        }
 }
 
 function counter_get (n){
    return {
        get counte(){
        return ++n;
        },
        set counte(m){
            if(m<n){ throw Error("error: param less than value"); }
            else {
                n = m; return n;
            }
        }
    }    
 }

相信看到这里,很多同学都预测出它们执行的结果。它们都有一个小特点,就是实现的过程都返回一个函数对象,返回的函数中带有对外部变量的引用

为什么非要返回一个函数呢 ?
因为函数可以提供一个执行环境,在这个环境中引用其它环境的变量对象时,后者不会被js内部回收机制清除掉。从而当你在当前执行环境中访问它时,它还是在内存当中的。这里千万不要把环境栈垃圾回收这两个很重要的过程搞混了,环境栈通俗点就是调用栈,调用移入,调用后移出,垃圾回收则是监听引用。

为什么可以一直递增呢 ?
上面已经说了,返回的匿名函数构成了一个执行环境,这个执行环境的作用域链下的变量对象并不是它自己的,而是其他环境中的。正因为它引用了别人,js才不会对它进行垃圾回收。所以这个值一直存在,每次执行都会对他进行递增。

性能会不会有损耗 ?
就拿这个功能来说,我们为了实现它使用了闭包,但是当我们使用结束之后呢? 不要忘了还有一个变量对其他变量对象的引用。这个时候我们为了让js可以正常回收它,可以手动赋值为null;

以第一个为例:

  var counter = (!function(){
    var num = 0;
    return function(){ return  ++num; }
 }())
 var n = counter();
 n(); n();
 
 n = null;  // 清空引用,等待回收
 

我们再来看上面的代码,第一个是返回了一个函数,后两个类似于方法,他们都能非常直接的表明闭包的实现,其实更值得我们注意的是闭包实现的多样性。

闭包面试题

一. 用属性的存取器实现一个闭包计时器

见上例;

二. 看代码,猜输出

function fun(n,o) {
  console.log(o);
  return {
    fun:function(m){
      return fun(m,n);
    }
  };
}

var a = fun(0); a.fun(1); a.fun(2); a.fun(3);//undefined,?,?,?
var b = fun(0).fun(1).fun(2).fun(3);//undefined,?,?,?
var c = fun(0).fun(1); c.fun(2); c.fun(3);//undefined,?,?,?

这道题的难点除了闭包,还有递归等过程,笔者当时答这道题的时候也答错了,真是恶心。下面我们来分析一下。

首先说闭包部分,fun返回了一个可用.操作符访问的fun方法(这样说比较好理解)。在返回的方法中它的活动对象可以分为 [arguments[m],m,n,fun]。在问题中,使用了变量引用(接收了返回的函数)了这些活动对象。

在返回的函数中,有一个来自外部的实参m,拿到实参后再次调用并返回fun函数。这次执行fun时附带了两个参数,第一个是刚才的外部实参(也就是调用时自己赋的),注意第二个是上一次的fun第一个参数

第一个,把返回的fun赋给了变量a,然后再单独调用返回的fun,在返回的fun函数中第二个参数n正好把我们上一次通过调用外层fun的参数又拿回来了,然而它并不是链式的,可见我们调用了四次,但这四次,只有第一次调用外部的fun时传进去的,后面通过a调用的内部fun并不会影响到o的输出,所以仔细琢磨一下不难看出最后结果是undefine 0,0,0。

第二个是链式调用,乍一看,和第一个没有区别啊,只不过第一个是多了一个a的中间变量,可千万不要被眼前的所迷惑呀!!!

    // 第一个的调用方式 a.fun(1) a.fun(2) a.fun(3)
    {
        fun:function(){
              return fun()  // 外层的fun 
        }
    }
    
    //第二个的调用方式 fun(1).fun(2).fun(3)
    //第一次调用返回和上面的一模一样
    //第二次以后有所不同
    return fun()  //直接返回外部的fun
    

看上面的返回,第二的不同在于,第二次调用它再次接收了{fun:return fun}的返回值,然而在第三次调用时候它就是外部的fun函数了。理解了第一个和第二个我相信就知道了第三个。最后的结果就不说了,可以自己测一下。

三. 看代码,猜输出

   for (var i = 1; i <= 5; i++) {
  setTimeout( function timer() {
      console.log(i);  
  }, 1000 );
  }

 for (var i = 1; i <= 5; i++) {
    (function(i){
        setTimeout( function () {
              console.log(i);
          },  1000 );
    })(i);
 }

上例中两段代码,第一个我们在面试过程中一定碰到过,这是一个异步的问题,它不是一个闭包,但我们可以通过闭包的方式解决。

第二段代码会输出 1- 5 ,因为每循环一次回调中都引用了参数i(也就是活动对象),而在上一个循环中,每个回调引用的都是一个变量i,其实我们还可以用其他更简便的方法来解决。

       for (let i = 1; i <= 5; i++) {
               setTimeout( function timer() {
                          console.log(i);  
              }, 1000 );
  }

let为我们创建局部作用域,它和我们刚才使用的闭包解决方案是一样的,只不过这是js内部创建临时变量,我们不用担心它引用过多造成内存溢出问题。

总结

我们知道了

本章涉及的范围稍广,主要是想让大家更全面的认识闭包,那么到现在你知道了什么呢?我想每个人心中都有了答案。

1.什么是闭包?

闭包是依据词法作用域产生的必然结果。通过变相引用函数的活动对象导致其不能被回收,然而形成了依然可以用引用访问其作用域链的结果。

    (function(w,d){
            var s = "javascript";
    }(window,document))

有些说法把这种方式称之为闭包,并说闭包可以避免全局污染,首先大家在这里应该有一个自己的答案,以上这个例子是一个闭包吗?

避免全局污染不假,但闭包谈不上,它最多算是在全局执行环境之上新建了一个二级作用域,从而避免了在全局上定义其他变量。切记它不是真正意义的闭包。

2.闭包的原理可不可以说一下?

结合我们上面讲过的,它的根源起始于词法阶段,在这个阶段中形成了词法作用域。最终根据调用环境产生的环境栈来形成了一个由变量对象组成的作用域链,当一个环境没有被js正常垃圾回收时,我们依然可以通过引用来访问它原始的作用域链。

3.你是怎样使用闭包的?

使用闭包的场景有很多,笔者最近在看函数式编程,可以说在js中闭包其实就是函数式的一个重要基础,举个不完全函数的栗子.

  function calculate(a,b){
    return a + b;
 }

 function fun(){
    var ars = Array.from(arguments);
  
    
    return function(){
        var arguNum = ars.concat(Array.from(arguments))
        
        return arguNum.reduce(calculate)
    }
}

var n = fun(1,2,3,4,5,6,7);

var k = n(8,9,10);

delete n;

上面这个栗子,就是保留对 fun函数的活动对象(arguments[]),当然在我们日常开发中还有更复杂的情况,这需要很多函数块,到那个时候,才能显出我们闭包的真正威力.

文章到这里大概讲完了,都是我自己的薄见和书上的一些内容,希望能对大家有点影响吧,当然这是正面的…如果哪里文中有描述不恰当或大家有更好的见解还望指出,谢谢。

题外话:

读一篇文章或者看几页书,也不过是几分钟的事情。但是要理解的话需要个人内化的过程,从输入 到 理解 到 内化 再到输出,这是一个非常合理的知识体系。我想不仅仅对于闭包,它对任何知识来说都是一样的重要,当某些知识融入到我们身体时,需要把他输出出去,告诉别人。这不仅仅是“奉献”精神,也是自我提高的过程。

Mycat 数据库分库分表中间件 解决大数据高并发难题

目前最流行的替代阿里巴巴开源数据库中间件Cobar的最佳方案

基于阿里开源的Cobar产品而研发,Cobar的稳定性、可靠性、优秀的架构和性能以及众多成熟的使用案例使得MYCAT一开始就拥有一个很好的起点,站在巨人的肩膀上,我们能看到更远。业界优秀的开源项目和创新思路被广泛融入到MYCAT的基因中,使得MYCAT在很多方面都领先于目前其他一些同类的开源项目,甚至超越某些商业产品。

MYCAT背后有一支强大的技术团队,其参与者都是5年以上资深软件工程师、架构师、DBA等,优秀的技术团队保证了MYCAT的产品质量。

MYCAT并不依托于任何一个商业公司,因此不像某些开源项目,将一些重要的特性封闭在其商业产品中,使得开源项目成了一个摆设。

什么是MYCAT

  • 一个彻底开源的,面向企业应用开发的大数据库集群
  • 支持事务、ACID、可以替代MySQL的加强版数据库
  • 一个可以视为MySQL集群的企业级数据库,用来替代昂贵的Oracle集群
  • 一个融合内存缓存技术、NoSQL技术、HDFS大数据的新型SQL Server
  • 结合传统数据库和新型分布式数据仓库的新一代企业级数据库产品
  • 一个新颖的数据库中间件产品

关键特性

 

  • 支持SQL92标准
  • 支持MySQL、Oracle、DB2、SQL Server、PostgreSQL等DB的常见SQL语法
  • 遵守Mysql原生协议,跨语言,跨平台,跨数据库的通用中间件代理。
  • 基于心跳的自动故障切换,支持读写分离,支持MySQL主从,以及galera cluster集群。
  • 支持Galera for MySQL集群,Percona Cluster或者MariaDB cluster
  • 基于Nio实现,有效管理线程,解决高并发问题。
  • 支持数据的多片自动路由与聚合,支持sum,count,max等常用的聚合函数,支持跨库分页。
  • 支持单库内部任意join,支持跨库2表join,甚至基于caltlet的多表join。
  • 支持通过全局表,ER关系的分片策略,实现了高效的多表join查询。
  • 支持多租户方案。
  • 支持分布式事务(弱xa)。
  • 支持XA分布式事务(1.6.5)。
  • 支持全局序列号,解决分布式下的主键生成问题。
  • 分片规则丰富,插件化开发,易于扩展。
  • 强大的web,命令行监控。
  • 支持前端作为MySQL通用代理,后端JDBC方式支持Oracle、DB2、SQL Server 、 mongodb 、巨杉。
  • 支持密码加密
  • 支持服务降级
  • 支持IP白名单
  • 支持SQL黑名单、sql注入攻击拦截
  • 支持prepare预编译指令(1.6)
  • 支持非堆内存(Direct Memory)聚合计算(1.6)
  • 支持PostgreSQL的native协议(1.6)
  • 支持mysql和oracle存储过程,out参数、多结果集返回(1.6)
  • 支持zookeeper协调主从切换、zk序列、配置zk化(1.6)
  • 支持库内分表(1.6)
  • 集群基于ZooKeeper管理,在线升级,扩容,智能优化,大数据处理(2.0开发版)。

 

 

MYCAT监控

 

  • 支持对Mycat、Mysql性能监控
  • 支持对Mycat的JVM内存提供监控服务
  • 支持对线程的监控
  • 支持对操作系统的CPU、内存、磁盘、网络的监控

用户案例

运行在安智账户系统中,数据量单表总量6KW,20多张表,上亿条数据。 运行良好,高并发下偶尔出现sql操作有缓存延迟的现象

公安某项目已上线,主要使用mycat分库分表服务于web系统做代理统计查询,数据总计目前20个表,30亿数据,选取适合的业务使用mycat,而非所有业务都依托于mycat.

某电影票务行业系统,支撑线下1200家影院POS设备的刷卡及券类验证,使用Mycat-server-1.2.2做为数据库访问中间件,一期已上线并稳定运行2月余

联通某系统已上线Mycat10个月左右,从1.1版本开始到现在的1.3,采用分库+按日分区的方式,大概15个表左右,只保留一个月数据量,超期迁走,其中几个大表单表数据量约保持1.5亿左右。总数据量没统计,估算在7亿左右,运行稳定。

移动医疗产品,使用技术架构(spring+spring mvc+myibatis+mycat(mysql集群)+redis+nginx+Haproxy),使用mycat主要应用于采集数据的分布式存储,实现分库分表和读写分离,目前数据量在1.5亿左右,并且在不断增长中,预计今年将突破5亿。

大型零售系统,支持全国2万家以上门店使用,预计全面上线后每月新增订单1千万左右。最大的表会1年2亿左右。产品目前开发阶段,2015年4月8日上线初始版本,全面上线支持2w+门店的阶段还需要一定的时间

征信辅助系统,数据量单表总量10KW, 目前系统运行良好,使用mycat之后,不像以前要经常时不时的去看数据库是否正常了。

天狮集团B2C系统在数据库层面,系统使用Mysql数据库,并将产品库、用户库、订单库、日志库、归档库部署到不同的Mysql实例中,同时对于存在高并发写入的订单库进行分片集群设计。对于查询量比较大的产品库、用户库进行一主多从设计,系统对数据库的读写都通过Mycat数据库中间件完成。

 

生产环境下MySQL 高可用浅析

对于多数应用来说,MySQL都是作为最关键的数据存储中心的,所以,如何让MySQL提供HA服务,是我们不得不面对的一个问题。当master当机的时候,我们如何保证数据尽可能的不丢失,如何保证快速的获知master当机并进行相应的故障转移处理,都是需要我们好好思考的。这里,笔者将结合这段时间做的MySQL proxy以及toolsets相关工作,说说我们现阶段以及后续会在项目中采用的MySQL HA方案。

Replication

要保证MySQL数据不丢失,replication是一个很好的解决方案,而MySQL也提供了一套强大的replication机制。只是我们需要知道,为了性能考量,replication是采用的asynchronous模式,也就是写入的数据并不会同步更新到slave上面,如果这时候master当机,我们仍然可能会面临数据丢失的风险。

为了解决这个问题,我们可以使用semi-synchronous replication,semi-synchronous replication的原理很简单,当master处理完一个事务,它会等待至少一个支持semi-synchronous的slave确认收到了该事件并将其写入relay-log之后,才会返回。这样即使master当机,最少也有一个slave获取到了完整的数据。

但是,semi-synchronous并不是100%的保证数据不会丢失,如果master在完成事务并将其发送给slave的时候崩溃,仍然可能造成数据丢失。只是相比于传统的异步复制,semi-synchronous replication能极大地提升数据安全。更为重要的是,它并不慢,MHA的作者都说他们在facebook的生产环境中使用了semi-synchronous,所以我觉得真心没必要担心它的性能问题,除非你的业务量级已经完全超越了facebook或者google。MySQL 5.7之后已经使用了Loss-Less Semi-Synchronous replication,所以丢数据的概率已经很小了。

如果真的想完全保证数据不会丢失,现阶段一个比较好的办法就是使用gelera,一个MySQL集群解决方案,它通过同时写三份的策略来保证数据不会丢失。笔者没有任何使用gelera的经验,只是知道业界已经有公司将其用于生产环境中,性能应该也不是问题。但gelera对MySQL代码侵入性较强,可能对某些有代码洁癖的同学来说不合适了:-)

我们还可以使用drbd来实现MySQL数据复制,MySQL官方文档有一篇文档有详细介绍,但笔者并未采用这套方案。

在后续的项目中,笔者会优先使用semi-synchronous replication的解决方案,如果数据真的非常重要,则会考虑使用gelera。

Monitor

前面我们说了使用replication机制来保证master当机之后尽可能的数据不丢失,但是我们不能等到master当了几分钟才知道出现问题了。所以一套好的监控工具是必不可少的。

当master当掉之后,monitor能快速的检测到并做后续处理,譬如邮件通知管理员,或者通知守护程序快速进行failover。

通常,对于一个服务的监控,我们采用keepalived或者heartbeat的方式,这样当master当机之后,我们能很方便的切换到备机上面。但他们仍然不能很即时的检测到服务不可用。笔者的公司现阶段使用的是keepalived的方式,但后续笔者更倾向于使用zookeeper来解决整个MySQL集群的monitor以及failover。

对于任何一个MySQL实例,我们都有一个对应的agent程序,agent跟该MySQL实例放到同一台机器上面,并且定时的对MySQL实例发送ping命令检测其可用性,同时该agent通过ephemeral的方式挂载到zookeeper上面。这样,我们可以就能知道MySQL是否当机,主要有以下几种情况:

  1. 机器当机,这样MySQL以及agent都会当掉,agent与zookeeper连接自然断开
  2. MySQL当掉,agent发现ping不通,主动断开与zookeeper的连接
  3. Agent当掉,但MySQL未当

上面三种情况,我们都可以认为MySQL机器出现了问题,并且zookeeper能够立即感知。agent与zookeeper断开了连接,zookeeper触发相应的children changed事件,监控到该事件的管控服务就可以做相应的处理。譬如如果是上面前两种情况,管控服务就能自动进行failover,但如果是第三种,则可能不做处理,等待机器上面crontab或者supersivord等相关服务自动重启agent。

使用zookeeper的好处在于它能很方便的对整个集群进行监控,并能即时的获取整个集群的变化信息并触发相应的事件通知感兴趣的服务,同时协调多个服务进行相关处理。而这些是keepalived或者heartbeat做不到或者做起来太麻烦的。

使用zookeeper的问题在于部署起来较为复杂,同时如果进行了failover,如何让应用程序获取到最新的数据库地址也是一个比较麻烦的问题。

对于部署问题,我们要保证一个MySQL搭配一个agent,幸好这年头有了docker,所以真心很简单。而对于第二个数据库地址更改的问题,其实并不是使用了zookeeper才会有的,我们可以通知应用动态更新配置信息,VIP,或者使用proxy来解决。

虽然zookeeper的好处很多,但如果你的业务不复杂,譬如只有一个master,一个slave,zookeeper可能并不是最好的选择,没准keepalived就够了。

Failover

通过monitor,我们可以很方便的进行MySQL监控,同时在MySQL当机之后通知相应的服务做failover处理,假设现在有这样的一个MySQL集群,a为master,b,c为其slave,当a当掉之后,我们需要做failover,那么我们选择b,c中的哪一个作为新的master呢?

原则很简单,哪一个slave拥有最近最多的原master数据,就选哪一个作为新的master。我们可以通过show slave status这个命令来获知哪一个slave拥有最新的数据。我们只需要比较两个关键字段Master_Log_File以及Read_Master_Log_Pos,这两个值代表了slave读取到master哪一个binlog文件的哪一个位置,binlog的索引值越大,同时pos越大,则那一个slave就是能被提升为master。这里我们不讨论多个slave可能会被提升为master的情况。

在前面的例子中,假设b被提升为master了,我们需要将c重新指向新的master b来开始复制。我们通过CHANGE MASTER TO来重新设置c的master,但是我们怎么知道要从b的binlog的哪一个文件,哪一个position开始复制呢?

GTID

为了解决这一个问题,MySQL 5.6之后引入了GTID的概念,即uuid:gid,uuid为MySQL server的uuid,是全局唯一的,而gid则是一个递增的事务id,通过这两个东西,我们就能唯一标示一个记录到binlog中的事务。使用GTID,我们就能非常方便的进行failover的处理。

仍然是前面的例子,假设b此时读取到的a最后一个GTID为3E11FA47-71CA-11E1-9E33-C80AA9429562:23,而c的为3E11FA47-71CA-11E1-9E33-C80AA9429562:15,当c指向新的master b的时候,我们通过GTID就可以知道,只要在b中的binlog中找到GTID为3E11FA47-71CA-11E1-9E33-C80AA9429562:15这个event,那么c就可以从它的下一个event的位置开始复制了。虽然查找binlog的方式仍然是顺序查找,稍显低效暴力,但比起我们自己去猜测哪一个filename和position,要方便太多了。

google很早也有了一个Global Transaction ID的补丁,不过只是使用的一个递增的整形,LedisDB就借鉴了它的思路来实现failover,只不过google貌似现在也开始逐步迁移到MariaDB上面去了。

MariaDB的GTID实现跟MySQL 5.6是不一样的,这点其实比较麻烦,对于我的MySQL工具集go-mysql来说,意味着要写两套不同的代码来处理GTID的情况了。后续是否支持MariaDB再看情况吧。

Pseudo GTID

GTID虽然是一个好东西,但是仅限于MySQL 5.6+,当前仍然有大部分的业务使用的是5.6之前的版本,笔者的公司就是5.5的,而这些数据库至少长时间也不会升级到5.6的。所以我们仍然需要一套好的机制来选择master binlog的filename以及position。

最初,笔者打算研究MHA的实现,它采用的是首先复制relay log来补足缺失的event的方式,但笔者不怎么信任relay log,同时加之MHA采用的是perl,一个让我完全看不懂的语言,所以放弃了继续研究。

幸运的是,笔者遇到了orchestrator这个项目,这真的是一个非常神奇的项目,它采用了一种Pseudo GTID的方式,核心代码就是这个

create database if not exists meta;drop event if exists meta.create_pseudo_gtid_view_event;delimiter ;;create event if not exists

meta.create_pseudo_gtid_view_event  on schedule every 10 second starts current_timestamp

on completion preserve

enable

do

begin

set @pseudo_gtid := uuid();

set @_create_statement := concat(‘create or replace view meta.pseudo_gtid_view as select \”, @pseudo_gtid, ‘\’ as pseudo_gtid_unique_val from dual’);

PREPARE st FROM @_create_statement;

EXECUTE st;

DEALLOCATE PREPARE st;

end;;

delimiter ;set global event_scheduler := 1;

它在MySQL上面创建了一个事件,每隔10s,就将一个uuid写入到一个view里面,而这个是会记录到binlog中的,虽然我们仍然不能像GTID那样直接定位到一个event,但也能定位到一个10s的区间了,这样我们就能在很小的一个区间里面对比两个MySQL的binlog了。

继续上面的例子,假设c最后一次出现uuid的位置为s1,我们在b里面找到该uuid,位置为s2,然后依次对比后续的event,如果不一致,则可能出现了问题,停止复制。当遍历到c最后一个binlog event之后,我们就能得到此时b下一个event对应的filename以及position了,然后让c指向这个位置开始复制。

使用Pseudo GTID需要slave打开log-slave-update的选项,考虑到GTID也必须打开该选项,所以个人感觉完全可以接受。

后续,笔者自己实现的failover工具,将会采用这种Pseudo GTID的方式实现。

在《MySQL High Availability》这本书中,作者使用了另一种GTID的做法,每次commit的时候,需要在一个表里面记录gtid,然后就通过这个gtid来找到对应的位置信息,只是这种方式需要业务MySQL客户端的支持,笔者不很喜欢,就不采用了。

后记

MySQL HA一直是一个水比较深的领域,笔者仅仅列出了一些最近研究的东西,有些相关工具会尽量在go-mysql中实现。

更新

经过一段时间的思考与研究,笔者又有了很多心得与收获,设计的MySQL HA跟先前有了很多不一样的地方。后来发现,自己设计的这套HA方案,跟facebook的一篇文章几乎一样,加之最近跟facebook的人聊天听到他们也正在大力实施,所以感觉自己方向是对了。

新的HA,我会完全拥抱GTID,比较这玩意的出现就是为了解决原先replication那一堆问题的,所以我不会考虑非GTID的低版本MySQL了。幸运的是,我们项目已经将MySQL全部升级到5.6,完全支持GTID了。

不同于fb那篇文章将mysqlbinlog改造支持semi-sync replication协议,我是将go-mysql的replication库支持semi-sync replication协议,这样就能实时的将MySQL的binlog同步到一台机器上面。这可能就是我和fb方案的唯一区别了。

只同步binlog速度铁定比原生slave要快,毕竟少了执行binlog里面event的过程了,而另外真正的slaves,我们仍然使用最原始的同步方式,不使用semi-sync replication。然后我们通过MHA监控整个集群以及进行故障转移处理。

以前我总认为MHA不好理解,但其实这是一个非常强大的工具,而且真正看perl,发现也还是看的懂得。MHA已经被很多公司用于生产环境,经受了检验,直接使用绝对比自己写一个要划算。所以后续我也不会考虑zookeeper,考虑自己写agent了。

不过,虽然设想的挺美好,但这套HA方案并没有在项目中实施,主要原因在于笔者打算近期离职,如果现在贸然实施,后续出问题了就没人维护了。:-)

PHP应用性能优化指南

PHP简史

PHP是由拉斯姆斯·勒多夫于1995年开始开发的。起初,它只是勒多夫为了要维护个人网页,而用c语言开发的一些CGI工具程序集,我们从PHP这个缩写最初的来源“Personal Home Page”(个人主页)就可以看出这一点。然而,随着勒多夫不断地扩充它的功能,PHP逐渐成为了现在的“PHP:超文本预处理器”。

在过去的20年中,PHP的开发团队一直致力于提升PHP的性能,最引人瞩目的是于1999年引入的Zend语法解释器引擎。2000年发布的PHP 4,包含了一个內建的编译器和执行器模型,使得PHP开始有能力开发动态的Web应用。2015年PHP发布了里程碑式的版本PHP 7.0,极大的提升了Zend引擎的性能,并降低了PHP的整体内存使用率。截止到本文发稿为止,目前最新的PHP版本是7.1.4,有兴趣的话可以看看这篇文章PHP7 新特性,改变变化

怎样才算是高性能的PHP应用?

性能和速度不是一对同义词。实现最佳性能通常需要在速度、准确性和可扩展性之间进行权衡。例如,在开发Web应用时,如果你优先考虑速度,你可能会编写一个将所有内容都载入内存的脚本,而如果从可扩展性出发,可能你就会编写以块为单位将数据载入内存的脚本。

基于phpLens的研究,下图展示了速度与可扩展性之间理论上的权衡关系。

红线表示针对速度进行了优化的脚本,蓝线是可扩展性优先的脚本。当并发连接数低时,红线运行速度更快; 然而,随着并发连接数量的增加,红线变慢。当并发连接数上升时,蓝线也减慢;然而,下降并不那么剧烈,因此,在一定阈值后,速度优先的脚本会比可扩展性优先的脚本慢。然而,在现实当中,一些脚本可能随着运行环境的变化而表现出前后不同的性能差异。你需要仔细的观察用户的使用情况,以及应用的并发请求数量,来适时调整合适的优化策略。

PHP代码优化最佳实践

编写好的PHP代码是创建快速稳定Web应用的关键一步。从一开始就遵循一些最佳实践技巧将节省后期填坑的时间。

1. 尽可能的使用PHP的内置方法

只要可以尽可能的使用PHP的内置方法,而不是自己编写相同功能的方法。花点时间去熟悉和学习PHP的内置方法,不但可以帮助你更快的编写代码,而且可以使你编写的代码更高效的运行。

2. 使用Json替代xml

json_encode()json_decode() 等PHP的内置方法,运行速度都非常快,所有应该优先使用Json。如果你无法避免使用xml,那么请务必使用正则表达式而不是DOM操作来进行解析。

3. 使用缓存技术

Memcache特别适用于减少数据库负载,而像APCOPcache这样的字节码缓存引擎在脚本编译时可节省执行时间。

4. 减少不必要的计算

当一个变量会被多次使用时,一开始就计算好,肯定要比每次使用时都计算一遍要更高效。

5. 使用isset()和empty()

与count()、strlen()和sizeof()函数相比,isset()empty()对于检测一个变量是否为空等场景更加简单和高效。

6. 减少不必要的类

如果你不打算重复使用一个类或者方法,那么它就没什么存在的价值。而如果你必须要定义和使用一个类,则需要合理规划类中的方法,对于不是特别公用的方法,尽量将他们放到子类中去,因为调用子类中的方法,比调用父类方法速度更快。

7. 在生产环境关闭用作调试的相关代码及错误报告

开发时打开错误报告,可以让你避免很多潜藏的Bug,而一些调试代码也有助于你定位Bug,但是当代码部署到生产环境后,这些错误报告和调试代码会拖慢你的程序速度,而且将一些错误报告直接显示给用户,也具有相当的安全风险。因此,在生产环境请关闭它们。

8. 关闭数据库连接

当使用完毕后,注销变量和关闭数据库连接,可以释放珍贵的内存资源。

9. 使用聚合函数减少数据库查询

查询数据库时,使用聚合函数,可以减少检索数据库的频率,并且使程序运行的更快。

10. 使用强大的字符串操作函数

举个例子,str_replace()比preg_replace()要快,而strtr()函数则比str_replace()函数快四倍。

11. 尽量使用单引号

如果可能,尽量使用单引号替代双引号。程序运行时,会检查双引号中的变量,这会拖慢程序的性能。

12. 尝试使用恒等运算符

由于“===”仅检查闭合范围,因此比使用“==”进行比较速度更快。

PHP代码之外的性能瓶颈因素

优化代码当然能够提高PHP的性能。但是,还有一些代码之外的因素也会成为PHP的性能瓶颈。这就是为什么程序员需要了解代码部署的整个服务器环境,这有助于他们在编写代码时有一定的心理准备,并能够在性能出现问题时,快速识别和定位性能瓶颈。以下是你遇到性能瓶颈时需要检查的点。

1. 网络带宽

如果网络带宽不够,其传输的总数据量将会受到严重影响,使其成为最明显的性能瓶颈。

2. CPU

如果只是传输一些纯静态的HTML,则不需要消耗很多CPU资源,但是PHP毕竟创建的是动态的应用程序,根据应用的需要,你可能至少需要一台具备多核处理器的服务器来提升PHP代码的运行效率。

3. 共享内存

缺少共享内存可能会影响进程间通信,从而影响程序性能。

4. 文件系统

随着时间推移,你的文件系统可能会出现大量磁盘碎片。如果内存足够,利用内存作为文件缓存可以加快磁盘的访问速度。

5. 进程管理

检查服务器的进程,确保里面没有非必要的进程。移除哪些不需要的网络协议、病毒扫描软件、邮件服务以及硬件驱动。将PHP代码运行在多线程模式,也能提高程序的响应时间。

6. 相关的其它服务

如果你的应用程序还依赖于一些外部服务,那这些外部服务的性能瓶颈也有可能拖慢你的应用。虽然这种情况下你能做的事情不多,但你仍然可以通过你这一边的操作来减轻外部服务性能瓶颈对你的影响,例如切换到备用服务上等。

更多PHP性能优化建议

1. 发挥OPCache的优势

由于默认情况下,PHP代码在执行时都会重新编译为可执行的中间代码OPCode,因此可以及时看到修改的代码所带来的变化,而不必频繁的重启PHP服务。不幸的是,如果每次在你的网站上运行时,都重新编译相同的代码会严重影响服务器的性能,这就是为什么opcode缓存或OPCache 非常有用。

OPCache是一个将编译好的代码保存到内存中的扩展。因此,下一次代码执行时,PHP将检查时间戳和文件大小,以确定源文件是否已更改。如果没有,则直接运行缓存的代码。

下图显示了运行无缓存的PHP应用程序,OPcache和eAccelerator(另一个PHP缓存工具)三者的执行时间和内存使用情况的差异。

图片来源: Prestashop

2. 识别数据库响应延迟

如上所述,性能问题并不总是由代码引起的。大多数瓶颈都出现在应用程序必须访问资源的时候。由于PHP应用程序的数据访问层可能占用最高90%的执行时间,因此你应该采取的第一步是查看代码中访问数据库的所有实例。

确保打开SQL的慢日志,以帮助你识别和处理慢SQL,然后评估这些查询的执行效率。如果你发现查询过多,或者在单次执行过程中发现相同的查询被多次进行,你可以通过减少数据库访问时间进行调整,从而提高应用程序的性能。

3. 清理文件系统

清理文件系统,并确保没有使用文件系统来存储Session。最重要的是,请注意file_exists(),filesize()或filetime()等触发文件统计信息的代码。将任何这些功能置于循环中可能会导致性能问题。

4. 监控外部API接口

大部分对外部系统有依赖关系的应用都会调用远程API。虽然这些远程API接口你无法直接控制,但你仍可以采取一些措施来减轻源自远程API的性能问题。例如,你可以缓存API输出的数据,或者可以在后台调用这些API。为API请求设置合理的超时时间,并且如果可能的话,随时做好API没有响应的情况下的显示输出。

5. 使用工具评估检测你的PHP代码

使用OPcache和监控外部API接口应该足以使大多数应用程序运行顺利;但是,如果你发现系统负载不断增加,那么可能需要使用工具来对你的PHP代码进行检测评估。完整的PHP代码检测评估虽然可能很耗时,但它可以为你提供有关应用程序性能的深入信息。幸运的是,有几个开源程序可以用于分析你的PHP代码,如Xdebug

监控PHP性能的重要性

如果你没有做好准备,你的Web应用可能前一分钟还在正常运行,但是下一分钟,一波突然激增的流量就会导致你的应用程序崩溃。 当然,优化和重构总是需要时间、精力和资金,而且投入是否值得的也很难说。因此,做出明智决策的最佳方式是不断收集数据

PHP性能监控软件可以帮助你立即测量所做的任何更改的影响。当然,知道要监测什么同样重要。速度和内存使用被认为是性能的最佳指标,因为它们影响到页面加载时间,这对Web应用程序至关重要。

虽然数据收集很重要,但是当你不需要监控系统时,你应该关闭监控系统,因为大量日志同样也会对性能造成影响。当然,这样的日志可以提供有关如何提高性能的有用信息,因此你应该在高峰期间定期监控。

未来的PHP性能

PHP仍在不断进化中,在目前正在开发的PHP 8版本中,最新的功能是即时编译或JIT,它将可以为我们创建更快的Web应用。随着技术的不断进步,用户的期望也随之增加。因此,开发人员必须始终关注未来的变化。

在构建Web应用程序时,请记住,今年的工作可能在明年不起作用。你可能需要进行调整才能持续保持优秀的PHP性能。在开发过程中,应该持续重点关注如何构建适用于高并发场景的Web应用和网站,保证它们的高可用性。

大流量网站WEB缓存探究

前言

由于项目越来越大,即使了使用代码压缩工具减少文件大小,js文件还是不可避免的越变越大。
而对于用户来说每次重新下载都有可能会消耗大量时间,让我们的首屏展示有较长时间的空白。
为了提升网站性能,有效利用缓存能够提升用户体验,提高访问效率。

浏览器缓存

HTML中的Meta标签

http-equiv属性,相当于http的文件头中的参数,而content的内容则是对应参数的值

<!-- 告诉浏览器不缓存当前页面 -->
<meta http-equiv="pragma" content="no-cache">

然而设置pragma: no-cache并不能应用于HTTP1.1及以上规范,
而且因为这个方法太老了,如果你不需要估计那些史前客户的感受,完全可以不加👆

当然可以不用太方,还有其他的参数可以选择使用

<meta http-equiv="Cache-Control" content="no-cache" /> <!-- HTTP1.1 在1.1中优先于expires-->
<meta http-equiv="pragma" content="no-cache" /> <!-- HTTP1.0 -->
<meta http-equiv="Expires" content="0" /> <!-- 示意到期时间 HTTP1.0 & 1.1 -->

但是使用meta标签设置的参数优先级低于http请求中声明的,如果你同时设置了http头,那么就没有必要加上meta标签了。

当然,最后还有一个重要的一点,就是根据叉烧包的实验,meta制定这些内容可以说基本没有什么卵用:)
悲伤的故事……当然可能你的浏览器还可以用哦

Header参数

最保险的显然是配置Header参数来保证资源的缓存

  1. Cache-Control
    Cache-Control 标头是在 HTTP/1.1 规范中定义的,取代了之前用来定义响应缓存策略的标头例如 Expires。
    所有现代浏览器都支持 Cache-Control。

    • max-age 指从请求的时间开始,允许缓存有效的最长时间(单位是s)
    • public 可被任何对象缓存。它不是必须的,因为明确的缓存信息已表示响应是可以缓存的
    • private 通常只为单个用户缓存,不允许任何中间缓存对其进行缓存
    • no-cache 表示必须先与服务器确认返回的响应是否发生了变化
    • no-store 禁止浏览器以及所有中间缓存存储任何版本的返回响应,每次请求必须重新下载
  2. 借用谷歌爸爸的一张图来展示一下Cache-Control的选择策略
  3. Expires
    它代表一个缓存过期的绝对时间,在HTTP/1.0中实现,在HTTP/1.1中优先级低于Cache-Control。

它的缺点就是如果服务器与客户端误差较大,那么它的误差也会变大

  1. Last-Modified
    标记的是资源的最后修改时间,需要配合Cache-Control使用。只能精确到秒级,如果某些文件在1秒内修改多次,则无法及时更新
  2. ETag
    相当于验证令牌。通过它可以可实现高效的资源更新检查:资源未发生变化时不会传送任何数据。

ETag通常是服务器生成的文件内容的哈希值或某个其他指纹。如果请求时指纹仍然相同,则表示资源未发生变化,则可跳过下载。

参数弃用小指南

  • 如果你不考虑ie6和HTTP 1.0客户端,那么你可以无视Pragma
Cache-Control: no-store, must-revalidate
Expires: 0
  • 如果你也不打算管HTTP 1.0代理,那么你可以无视Expires
Cache-Control: no-store, must-revalidate
  • 如果服务器自动包含有效的Date标头,则理论上也可以省略Cache-Control,并仅依赖于Expires。不过如果客户端和服务端时间有差别,就可能会失败哦
Date: Wed, 24 Aug 2016 18:32:02 GMT
Expires: 0
  • 总的来说还是使用Cache-Control最妥妥的(如果不打算考虑HTTP 1.0)

项目实践

更新文件&弃用缓存

在项目中,当我们使用本地缓存后又会遇到另一个问题——如何更新文件、弃用缓存。
通常,我们通过对文件名加入指纹来实现。

以webpack为例,
写配置文件时

{
    output: {
        filename: "bundle.[hash].js"
    }
}

为打包后的文件名加上hash,使文件更新之后会生成新的hash,以达到弃用原来缓存的效果。

定制缓存策略

可以为不同类型的文件定义不同的缓存策略,以达到最高效的结果

  1. 将HTML被标记为“no-cache”,使浏览器在每次请求时都始终会重新验证文档,并在内容变化时能够及时获取最新版本,即使下载新资源。
  2. 允许浏览器和中间缓存(如CDN)缓存CSS,并将CSS设置为1年后到期,超长的缓存时间可以让用户避免每次都从服务端获取响应。同时不要忘记给文件名加上指纹,以便及时更新改动
  3. JavaScript同样设置为1年后到期,但标记为private,因为它可能会包含某些用户私人数据,这是CDN不应缓存的。
  4. 图像缓存时不包含版本或唯一指纹,并设置为1天后到期。

其他技巧

  1. 减少对Cookie的依赖,因为每次HTTP请求都会带上Cookie,这回增大传输流量(当然将静态资源挂载在其他域名下,也可以达到cookie free的效果)

mysql全局唯一ID生成方案(一)

一旦数据库被切分到多个物理结点上,我们将不能再依赖数据库自身的主键生成机制。一方面,某个分区数据库自生成的ID无法保证在全局上是唯一的;另一方面,应用程序在插入数据之前需要先获得ID,以便进行SQL路由。

目前几种可行的主键生成策略有:

1. UUID:使用UUID作主键是最简单的方案,但是缺点也是非常明显的。由于UUID非常的长,除占用大量存储空间外,最主要的问题是在索引上,在建立索引和基于索引进行查询时都存在性能问题。(UUID是指在一台机器上生成的数字,它保证对在同一时空中的所有机器都是唯一的。通常平台会提供生成的API。)

2. 结合数据库维护一个Sequence表:此方案的思路也很简单,在数据库中建立一个Sequence表,表的结构类似于:

    CREATE TABLE `SEQUENCE` (  
        `tablename` varchar(30) NOT NULL,  
        `nextid` bigint(20) NOT NULL,  
        PRIMARY KEY (`tablename`)  
    ) ENGINE=InnoDB

每当需要为某个表的新纪录生成ID时就从Sequence表中取出对应表的nextid,并将nextid的值加1后更新到数据库中以备下次使用。此方案也较简单,但缺点同样明显:由于所有插入任何都需要访问该表,该表很容易成为系统性能瓶颈,同时它也存在单点问题,一旦该表数据库失效,整个应用程序将无法工作。有人提出使用Master-Slave进行主从同步,但这也只能解决单点问题,并不能解决读写比为1:1的访问压力问题。

3. flickr提供了一个扩展的更好的方案: 他们建了一个专门用作生成 uid 的表,例如取名叫 uid_sequence,并拆成若干的子表(假设两个),自增步长设置为2(机器数目),这两张表可以放在不同的物理机器上。 其中一个表负责生成奇数uid,另一个负责生成偶数uid

11.png

uid_sequence 表的设计

#server1:
CREATE TABLE `uid_sequence` (  
  `id` bigint(20) unsigned NOT NULL auto_increment,  
  `stub` char(1) NOT NULL default '',  
  PRIMARY KEY  (`id`),  
  UNIQUE KEY `stub` (`stub`)  
) ENGINE=MyISAM AUTO_INCREMENT=1;
#server2:
CREATE TABLE `uid_sequence` (  
  `id` bigint(20) unsigned NOT NULL auto_increment,  
  `stub` char(1) NOT NULL default '',  
  PRIMARY KEY  (`id`),  
  UNIQUE KEY `stub` (`stub`)  
) ENGINE=MyISAM AUTO_INCREMENT=2;

在每个数据库配置文件中添加下边的配置,设置自动增长的步长为2

#Server1:  my.conf
auto-increment-increment = 2
#Server2: my.conf
auto-increment-increment = 2

SELECT * from uid_sequence 输出:

+——————-+——+
| id                | stub |
+——————-+——+
| 72157623227190423 |    a |

如果我需要一个全局的唯一的64位uid,则执行:

REPLACE INTO uid_sequence (stub) VALUES ('a');  
SELECT LAST_INSERT_ID();

用 REPLACE INTO 代替 INSERT INTO 的好处是避免表行数太大,还要另外定期清理。
stub 字段要设为唯一索引,这个 sequence 表只有一条纪录,但也可以同时为多张表生成全局主键,例如 user_ship_id。除非你需要表的主键是连续的,那么就另建一个 user_ship_id_sequence 表。
经过实际对比测试,使用 MyISAM 比 Innodb 有更高的性能。

这里flickr使用两台数据库作为自增序列生成,通过这两台机器做主备和负载均衡。因为flickr的数据库ID生成服务器是专用服务器,服务器上只有一个数据库,数据库中表都是用于生成Sequence的,所以配置了全局范围的auto-increment-offset和auto-increment-increment,这会影响到数据库中的每一个表。

MySQL 中 last_insert_id() 的并发问题

因为是两条SQL语句,所以这两条语句之间会不会有并发问题?
答案是不会,因为 last_insert_id() 是 Connection 级别的,是单个连接客户端里执行的insert语句最近一条,客户端之间是不会影响,没必要锁定和事务处理。

4. Redis生成ID

当使用数据库来生成ID性能不够要求的时候,我们可以尝试使用redis来生成ID。这主要依赖于Redis是单线程的,所以也可以用于生成全局唯一的ID。可以用Redis的原子操作 INCR和INCRBY来实现。

可以使用Redis集群来获取更高的吞吐量。假如一个集群中有5台Redis A,B,C,D,E。可以初始化每台Redis的值分别是1,2,3,4,5,然后步长都是5。各个Redis生成的ID为:

A:1,6,11,16,21
B:2,7,12,17,22
C:3,8,13,18,23
D:4,9,14,19,24
E:5,10,15,20,25

使用Redis集群也可以防止单点故障的问题。使用Redis来生成每天从0开始的流水号比较适合。比如订单号=日期+当日自增长号。可以每天在Redis中生成一个Key,使用INCR进行累加。

优点:
1)不依赖于数据库,灵活方便,且性能优于数据库。
2)数字ID天然排序,对分页或者需要排序的结果很有帮助。

缺点:
1)如果系统中没有Redis,还需要引入新的组件,增加系统复杂度。
2)需要编码和配置的工作量比较大。

.

如何让你的知识内化?

前言

是否,你浏览过无数文章,但是转眼就忘?

是否,你收藏过无数文章,但是很少再看?

是否,你感觉自己很努力学习了,但还是收获甚微?

其实,这几个问题也一直深深地困扰着我,一直渴望着提升,却又摸不到方向,感觉一直在努力,然而貌似并没什么卵用。

直到,我接触到知识管理这个概念,才领悟到:学习是一个系统工程,每一次的阅读、收藏、实践,其实都是这个系统的自我更新。唯有运用工程思维,才能更好的解决这几个问题!

关于知识管理,我视之为我人生中最重要的技能,我将不断探索、不断优化,以成就一个更完善的自己。

下面,我将从一个程序员的视角来讨论知识管理,主要包括以下几个方面:

  1. 什么是知识管理?
  2. 为什么要管理知识?
  3. 如何管理知识?

2

什么是知识管理?

个人知识管理(Personal Knowledge Management):一般指个人通过工具建立知识体系并不断完善,进行知识的收集、消化吸收和创新的过程。

3

为什么要管理知识?

核心目的:搭建自己的知识体系

计算机行业的一个特点是新技术更新特别快,意味着程序员需要不停学习,才能跟上行业的发展。所以,知识管理对程序员非常重要。有意识,成体系地管理知识能够:

  1. 更快速的入门
    如果我们已经建立好一个技术知识体系,新的技术也只是在其他技术上建立起来的,有了坚实基础,学习新技术就会更有效,毕竟原理总是类似的。
  2. 更全面的掌握
    看过那么多的博客,如果没有经过自己的整理,终究总是一块块记忆碎片,难成体系!使用合适的工具,正确的方法,才能更好地掌握知识,让知识凝固在脑海,形成一个整体的脉络。
  3. 更高效的检索
    程序员经常遇到同样的问题,例如说部署开发环境的时候,如果有把解决方案记录下来,就能省去重新解决问题的时间。毕竟重复的谷歌,也是耗时操作。

4

如何管理知识?

1.收集

确定主线,建立信源,链式反应,广泛收藏

“生也有涯,知也无涯”,我们永远不可能通晓所有的知识!

一开始我看到好的技术文章时,都是加书签存起来。后面发现这样不能离线访问,而且链接可能会失效。

后来,接触了印象笔记、为知笔记等云笔记软件,于是我可以把文章保存到笔记中,随时可以翻出来看。这是我的知识管理之路的开始。

可以说,笔记软件给知识积累带来了极大方便,而且,还可以在自己的所有笔记中快速搜索某个关键词。

有时候,我们明明记得自己以前看过某篇文章,但就是想不起来具体细节了,这个时候,只要我们之前保存过,一搜即可。这比再用搜索引擎去搜索,显然更高效一些。

记得以前,我为解决某个问题,谷歌了很久终于找到一篇文章解决了问题。解决之后并没有记录下来,结果下一次遇到同样问题,我又浪费了很多时间去再次搜索解决方案。重复多次之后,我意识到这是个严重的问题。所以逐渐养成了保存各种文章的习惯。这些网上积累下来的文章,成为了我构建知识体系的土壤。

  1. 不是收集好的知识,而是收集对自己有用的知识
  2. 你缺的不是知识,而是整合知识的能力
  3. 主题阅读,不以读完一书一文为目的,能提取到想要的知识即可
  4. 读书为纲,上网为目。唯有纲举,方可目张
  5. 设定一个自己的长期学习规划
  6. 建立自己的常用信源清单
  7. 从一个关键词的解释中,提取到一堆关键词,链式反应
  8. 印象剪藏时不必纠结分类,多用关键词搜索

2.整理

合并同类,编织脑图,定期整理,持续更新

“小马过河,深浅自知”,别人的文章永远是别人的知识!

上面收集的那些知识碎片是我们有初步印象的知识,只不过因为太过碎片,尚不能够形成完整的知识体系。很多人以为把看到的文章保存到自己的笔记里面,就有一种已经掌握它的错觉,结果保存了成千上百的文章,却一篇都没回顾过。

别人的文章是他的知识沉淀,并不是自己的。定期回顾,并提取出文章中的精华,再经过自己的实践、思考、整理,才能形成自己的知识体系。这是一个很漫长的积累过程,而我们能做的唯有:坚持到底!

  1. 分类自底而上,先有了大量碎片,而后才有细致分类。
  2. 多用思维导图,整体把握脉络
  3. 定时整理笔记,归纳相似主题
  4. 印象笔记做摘录,为知笔记写原创
  5. 书写,不只是为了记录,更是一种思考方式
  6. 原始积累,越快越好,先求量大,再求质优
  7. 使用工具,而不是被工具使用
  8. 构建知识体系,服从于个人职业发展
  9. 预判使用场景,布局技术未来
  10. 以写论文的方式来整理

3.分享

寻找同好,讨论反思,自我激励,打造品牌

“常与同好争高下,不共傻瓜论短长”,教是最好的学习方式!

一方面,自己以为理解了不是真理解,把别人讲理解了才是真理解!如果能有读者和自己互动,那肯定比自己一个人闭门造车,更有积极性一点,人毕竟是社会性的,我们也渴望着别人的认可。一个人默默写笔记,可能能坚持写个十几篇,如果是公开写博客,有读者的反馈和认同,那可能更容易坚持下去一些。毕竟,写作,是一段孤独的旅程。

另一方面,在这个时代,对于生活中的绝大多数人来说,拓宽朋友圈子的途径几乎只有一个,通过网络,而如何在网络中寻找到气味相投的朋友,如何判断别人和自己是否有共同语言?显然,通过天天在SNS上碎碎念的那些日志是难以做到的。我很佩服那些长期用博客记录想法的人,因此,即使和他们素未谋面,也算是神交已久。

  1. 打造个人品牌,增加自己的影响力
  2. 跨时空的交流方式
  3. 记录自己的经历和成长
  4. 锻炼自己的表达能力

参考清单:

  • 程序员的知识管理
  • 个人知识管理的方法
  • 提炼后的知识才是力量
  • 时寒冰谈读书:如何最快地汲取营养
  • 献给写作者的 Markdown 新手指南
  • 书写是为了更好的思考
  • 为什么你应该写博客
  • 如何建立自己的知识体系?
  • 如何构建自己的笔记系统?
  • 你的知识管理体系是如何的?
  • 如何提高影响力,为自己代言
  • 我为什么坚持写博客?
  • 方法论-有意识的学习

MySQL5.7 group by新特性,报错1055

项目中本来使用的是mysql5.6进行开发,切换到5.7之后,突然发现原来的一些sql运行都报错,错误编码1055,错误信息和sql_mode中的“only_full_group_by“有关,到网上看了原因,说是mysql5.7中only_full_group_by这个模式是默认开启的
解决办法大致有两种:
一:在sql查询语句中不需要group by的字段上使用any_value()函数
当然,这种对于已经开发了不少功能的项目不太合适,毕竟要把原来的sql都给修改一遍

二:修改my.cnf(windows下是my.ini)配置文件,删掉only_full_group_by这一项
我们项目的MySQL安装在ubuntu上面,找到这个文件打开一看,里面并没有sql_mode这一配置项,想删都没得删。
当然,还有别的办法,打开mysql命令行,执行命令

select @@sql_mode
  • 1
  • 1

这样就可以查出sql_mode的值,复制这个值,在my.cnf中添加配置项(把查询到的值删掉only_full_group_by这个选项,其他的都复制过去):

sql_mode=STRICT_TRANS_TABLES,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION
  • 1
  • 1

如果 [mysqld] 这行被注释掉的话记得要打开注释。然后重重启mysql服务

注:使用命令

set sql_mode=STRICT_TRANS_TABLES,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION
  • 1
  • 1

这样可以修改一个会话中的配置项,在其他会话中是不生效的。

MySQL备份命令mysqldump参数说明与示例

 

1. 语法选项说明

  • -h, --host=name
    主机名
  • -P[ port_num], --port=port_num
    用于连接MySQL服务器的的TCP/IP端口号
  • --master-data
    这个选项可以把binlog的位置和文件名添加到输出中,如果等于1,将会打印成一个CHANGE MASTER命令;如果等于2,会加上注释前缀。并且这个选项会自动打开--lock-all-tables,除非同时设置了--single-transaction(这种情况下,全局读锁只会在开始dump的时候加上一小段时间,不要忘了阅读--single-transaction的部分)。在任何情况下,所有日志中的操作都会发生在导出的准确时刻。这个选项会自动关闭--lock-tables
  • -x, --lock-all-tables
    锁定所有库中所有的表。这是通过在整个dump的过程中持有全局读锁来实现的。会自动关闭--single-transaction--lock-tables
  • --single-transaction
    通过将导出操作封装在一个事务内来使得导出的数据是一个一致性快照。只有当表使用支持MVCC的存储引擎(目前只有InnoDB)时才可以工作;其他引擎不能保证导出是一致的。当导出开启了--single-transaction选项时,要确保导出文件有效(正确的表数据和二进制日志位置),就要保证没有其他连接会执行如下语句:ALTER TABLE, DROP TABLE, RENAME TABLE, TRUNCATE TABLE,这会导致一致性快照失效。这个选项开启后会自动关闭--lock-tables
  • -l, --lock-tables
    对所有表加读锁。(默认是打开的,用--skip-lock-tables来关闭,上面的选项会把关闭-l选项)
  • -F, --flush-logs
    在开始导出前刷新服务器的日志文件。注意,如果你一次性导出很多数据库(使用 -databases=--all-databases选项),导出每个库时都会触发日志刷新。例外是当使用了--lock-all-tables--master-data时:日志只会被刷新一次,那个时候所有表都会被锁住。所以如果你希望你的导出和日志刷新发生在同一个确定的时刻,你需要使用--lock-all-tables,或者--master-data配合--flush-logs
  • --delete-master-logs
    备份完成后删除主库上的日志。这个选项会自动打开“–master-data`。
  • --opt
    -add-drop-table, --add-locks, --create-options, --quick, --extended-insert, --lock-tables, --set-charset, --disable-keys。(默认已开启,--skip-opt关闭表示这些选项保持它的默认值)应该给你为读入一个MySQL服务器的尽可能最快的导出,--compact差不多是禁用上面的选项。
  • -q, --quick
    不缓冲查询,直接导出至stdout。(默认打开,用--skip-quick来关闭)该选项用于转储大的表。
  • --set-charset
    SET NAMES default_character_set加到输出中。该选项默认启用。要想禁用SET NAMES语句,使用--skip-set-charset
  • --add-drop-tables
    在每个CREATE TABLE语句前添加DROP TABLE语句。默认开启。
  • --add-locks
    在每个表导出之前增加LOCK TABLES并且之后UNLOCK TABLE。(为了使得更快地插入到MySQL)。默认开启。
  • --create-option
    在CREATE TABLE语句中包括所有MySQL表选项。默认开启,使用--skip-create-options来关闭。
  • -e, --extended-insert
    使用全新多行INSERT语法,默认开启(给出更紧缩并且更快的插入语句)
  • -d, --no-data
    不写入表的任何行信息。如果你只想得到一个表的结构的导出,这是很有用的。
  • --add-drop-database
    在create数据库之前先DROP DATABASE,默认关闭,所以一般在导入时需要保证数据库已存在。
  • --default-character-set=
    使用的默认字符集。如果没有指定,mysqldump使用utf8。
  • -B, --databases
    转储几个数据库。通常情况,mysqldump将命令行中的第1个名字参量看作数据库名,后面的名看作表名。使用该选项,它将所有名字参量看作数据库名。CREATE DATABASE IF NOT EXISTS db_nameUSE db_name语句包含在每个新数据库前的输出中。
  • --tables
    覆盖--database选项。选项后面的所有参量被看作表名。
  • -u[ name], --user=
    连接服务器时使用的MySQL用户名。
  • -p[password], --password[=password]
    连接服务器时使用的密码。如果你使用短选项形式(-p),不能在选项和密码之间有一个空格。如果在命令行中,忽略了--password-p选项后面的 密码值,将提示你输入一个。

2. 示例

导出一个数据库:

$ mysqldump -h localhost -uroot -ppassword \
--master-data=2 --single-transaction --add-drop-table --create-options --quick \
--extended-insert --default-character-set=utf8 \
--databases discuz > backup-file.sql

导出一个表:

$ mysqldump -u pak -p --opt --flush-logs pak t_user > pak-t_user.sql

将备份文件压缩:

$ mysqldump -hhostname -uusername -ppassword --databases dbname | gzip > backup-file.sql.gz
对应的还原动作为
gunzip < backup-file.sql.gz | mysql -uusername -ppassword dbname

导入数据库:

mysql> use target_dbname
mysql> source /mysql/backup/path/backup-file.sql
或
$ mysql target_dbname <backup-file.sql

导入还有一个mysqlimport命令,暂未研究。

直接从一个数据库向另一个数据库转储:

mysqldump -u用户名 -p --opt dbname | mysql --host remote_host -C dbname2

关于增量备份与恢复请参考:MySQL增量备份与恢复实例

参考

印度IT工程师毕业生质量堪忧 会写正确程序者不足5%

  提到工程技术人才,人们很容易想到IT服务业人才大国印度。但是印度的IT专业毕业生质量却堪忧。 据美国“石英”网4月21日报道,印度的高校可能是世界上批量生产了最多工程技术毕业生的国家,但这些毕业生的技术水平并不是很高。2011年,据印度国家软件和服务公司协会估计,在印度仅有25%的信息技术和工程专业毕业生符合最低雇佣要求,有任职资格。

src=”http://n.sinaimg.cn/tech/crawl/20170421/C35z-fyeqcac1064463.jpg” alt=”印度IT工程师毕业生质量堪忧 会写正确程序者不足5%”>

  六年来,信息技术和工程方面的人才储备情况仍然没有获得改善。

  印度求职测验机构“有志者”近日对印度3.6万名工程类学生进行调查研究,结果显示:“只有4.77%的学生可以写出正确的程序逻辑,而写出正确的程序逻辑却是任何编程工作对求职者的最基本的要求。”印度有500多个高校开设有IT相关专业,就业能力评估公司通过自动机(一款软件开发技能学习评估机器)对这些学校的学生进行测验。

  测验结果显示:“IT行业需要能书写维护性代码,这样会保护系统避免出现漏洞,让其具有可读性、可重复使用、也可扩充。只有1.4%的编程员能创建功能上正确有效的代码,意味着只有这1.4%的人可以迅速可靠地做好他们应该做的事。”

  据报道,来自印度前100所大学的应聘者中,超过三分之二的求职者可以编写“编译代码”,或者在将代码编译成机器可读代码时不会出错。其它学校中,只有31%的学生会写编译代码。

  报道说,出现这种情况,原因之一是印度高校中缺乏优秀教师、高校没有匹配的课程。瓦鲁纳?阿加沃尔(Varun Aggarwal)是“有志者”的创始人和首席技术官,他说:“现在的高校课程将注意力主要集中在了微软字处理软件、PPT制作和Excel表格等方面,却不是用于Basic 和Logo之类的简单编程语言进行编程教学方面,这也是出现目前这种状况的罪魁祸首。”

原文地址:http://tech.sina.com.cn/i/2017-04-21/doc-ifyepsch2312667.shtml