薛定谔的风口猪

站在巨人的肩膀上学习,猪都能看得很远

浅谈网站性能优化

今天简单从上层的角度聊一下如何对有一个网站进行优化。

从用户在浏览器敲下回车,到数据回来,至少可以分为三个路径

  1. 在浏览器端,发送用户请求,并且接受服务器返回的响应数据进行页面渲染。
  2. 请求数据在网络进行传输,发送到服务器。服务器把响应数据在网络传输返回。
  3. 服务器端进行数据解析处理(访问文件,数据库等),最后返回响应数据。

我们把第一路径简要称为“前端”,第三路径称为“后端”,看看能在这三层如何对网站的性能做出优化。

前端的过程

1 . 本地DNS解析域名,得到IP地址(并将IP地址缓存起来),向目标IP发送请求(通常为HTTP)

以上过程可以优化的地方主要依靠减少DNS解析的次数。如果用户的浏览器设置了缓存,那么第二次访问相同域名的时候就不会请求DNS服务器了,而是直接用缓存中的IP发送请求。这主要依靠浏览器的相关设置,但我们也可以在页面告知浏览器需要做DNS的预取:

<meta http-equiv="x-dns-prefetch-control" content="on" />

2 . 浏览器得到相应数据做出渲染计算

在这阶段,浏览器主要做的是解析相应数据,创建DOM树,下载CSS样式应用到DOM树中,下载JS文件开始解析。

为了提高页面的访问速度,我们应该尽可能让CSS样式放到<head>中并且让下载js的语句放到<body>的末尾,这样就可以使得页面先渲染起来再执行js脚本,用户的等待时间将减小。

注:HTML5支持async属性支持脚本的异步执行,如:

<script type="text/javascript" src="demo_async.js" async="async"></script>

同时,我们可以设置浏览器的缓存,让浏览器下次防蚊时从缓存中读取内容,减小HTTP请求。

网络传输(第二阶段)

这是阶段的速度取决于网络情况,由于用户的请求的数据很小但往往接受的响应数据很大,所以这要求企业的网络带宽要有快的上行速度,这和用户的带宽是相反的。

后端过程

后端是主要可以发挥的地方,这里包括处理请求,访问数据库等资源的过程。

我们主要可以优化的地方有:

1 . 使用缓存,减缓数据库压力

我们应该尽可能使用缓存提高常用数据的读取数据,减少访问数据库的次数。现在分布式的场景下,可以使用Memcached搭建起分布式缓存。

2 . 使用异步操作代替同步操作,避免阻塞的等待时间,提高性能。

在高并发的情况,同步的请求操作(如数据库插入)会对数据库造成很大压力,同时也会导致用户的等待时间增长,我们应该尽可能使用异步的请求操作代替同步操作,以提高整体服务的响应速度,具体的操作将进入消息队列处理。最终的结果可以使用其他的方式告知用户,如邮件提醒的方式。

如何在两台机器上使用octopress

在现在云端时代,在多台机器上操作同一份文档是十分常见的需求。而需要多台机器上使用octopress,git提供了很好的支持。今天分享下如何使用git在多台机器上使用octopress.

准备工作:

之前写的“如何开始使用octopress”一样,要先安装相应的软件

  1. 安装Git
  2. 安装ruby,例如:Ruby 1.9.3-p194,并配置环境变量PATH 到rubyhome/bin
  3. 安装 Development Kit.如 DevKit-tdm-32-4.5.2-20111229-1559-sfx.exe and 解压到文件夹 C:/RubyDevKit.
  4. 建立一个文件夹,例如在C:/github

克隆项目

接下来我们需要把已经建好的博客项目clone下来。

克隆source分支

$ git clone -b source git@github.com:username/username.github.com.git octopress ##octopress 为你的项目文件夹

克隆master分支

$ cd octopress ##进入项目
$ git clone git@github.com:username/username.github.com.git _deploy ##克隆master分支到_deploy 

配置环境

$ gem install bundler
$ rbenv rehash    # If you use rbenv, rehash to be able to run the bundle command
$ bundle install
$ rake setup_github_pages

然后它会询问你的项目仓库的URL:

Enter the read/write url for your repository (For example, ‘git@github.com:your_username/your_username.github.com)

输入仓库的URL,这样你就完成了全新的一个本地博客副本。

更新变化(重要)

每次使用前,先确保拿到最新的文件

$ cd octopress  #进入项目目录
$ git pull origin source  # 更新本地source branch
$ cd ./_deploy  #进入_deploy目录
$ git pull origin master  # 更新本地master branch

提交

提交的时候,由于需要多台机器协作,需要把source分支push到origin中,这样另外一台机器才能拿到最新的源文件。

$ rake generate
$ git add .
$ git commit -am "提交评论" 
$ git push origin source  # 更新远程 source branch 
$ rake deploy             # 更新远程 master branch,并部署博文

另外的机器更新变化

在另外的机器上,就可以获取到相应的变化。

$ cd octopress  #进入项目目录
$ git pull origin source  # 更新本地source branch
$ cd ./_deploy  #进入_deploy目录
$ git pull origin master  # 更新本地master branch

谈谈Java同步机制

在多线程中,有两个核心的问题需要解决。一个是多个线程对于资源的竞争问题,我们需要同步,另外一个则是多个线程之间的交互。 今天简单谈谈Java的同步机制。

我们以一个十分简单的例子讲起。

int i=0
public int getNextI(){
    return ++i;
}

这是一个极为简单的方法,但是在多线程的环境中则增加了许多其他复杂的因素。

首先,++i不是一个原子操作。进行i+1的操作,是分多步的,最后重新赋值写给i。

其次,在多线程的环境中,每个线程都会有一个working memory, 所以如果我们启动一个新线程操作getNextI(),i的操作是出现在working memory 中的,最后操作完成后一段时间才会重新写入main memory.

这样第一个问题就是,如何保证i的可见性。也就是说,假如两个线程T1,T2。T1对i进行操作后把i变成了2,但在T1把2这个值写入main memory之前,T2是读取不到2这个值的,也就是说他读到的是老的数据。

volatile

这时候,我们就可以把i用volatile修饰。 volatile的变量,线程不会复制到working memory,而是直接在main memory上操作。

volotile特别适用于多线程环境中某个循环的结束条件。 如 while(condition){//do someting}, 这里面的condition应该声明为volatile,这样每一个线程对condition的修改都会立刻被其他线程读取到。

synchronized

volatile只能保证变量的可见性,并不能保证i++的原子性。如果我们需要串行化的处理这个方法,我们需要谨慎使用volatile,而使用synchronized

假如T1,T2同时进入getNextI(),然后T1和T2都读到了1,然后分别的进行++i,最后有可能i只是变成了2。我们希望T1和T2是有秩序的访问这个方法,这时候我们要使用到Synchronized机制了。

我们可以改成

public synchronized int getNextI(){
    return i++;
}

这样在每个线程进入getNextI()之前,都会尝试去获取当前对象的intrinsic锁,并且只有一个线程可以获取。当结束该方法时候,就会释放,这样其他线程就可以获取到,这样就可以就可以保证T1和T2对方法操作是串行的。

注: 如果当前方法为静态方法,则锁是打在当前类的Class对象,而非对象本身。所以静态方法的控制和实例方法的控制是区分开来的。

但有些时候整个方法都加锁会影响性能,因为我们可能很多操作都不涉及共享资源,也就没有资源竞争的问题存在,所以synchronized 除了可以修饰方法,还可以修饰一段代码块,以便最小粒度的限制加锁的范围。当修饰代码块时候,必须要指定获取intrinsic锁的对象。如:

public  int getNextI(){
    synchronized(this){
        return i++;
    }
}

更多文档可参考:

http://docs.oracle.com/javase/tutorial/essential/concurrency/locksync.html

初探SOA

有些系统或者产品当发展得越来越大后,就会有很多子模块,子产品。但这些模块中却有着很多相同的需要解决的问题,即有许多共有的业务逻辑。例如,可能豆瓣系统下面的豆瓣读书,豆瓣电影,豆瓣小组等等的每个模块虽然都有着独特的功能,但都有对于用户信息的读写,或者用户评价的相关逻辑。

这时候,假如每个系统都要独自的单独维护着这些逻辑,会出现很多重复性的代码和逻辑,这就会导致这一部分共有的逻辑需要修改的时候,每个模块下的对应逻辑都要作相应的更改,会导致系统的维护成本大大上升。

为了解决的这一类问题,可采用提取公共逻辑去划分不同的系统的方法来提高系统的可重用性和可维护性。

还有另外一个问题是,随着系统的访问量逐步上升,由于单一系统需要处理所有的逻辑,必然会导致当访问量、数据量上升到一定程度后出现性能问题。这时候,我们可以把系统进行分解,以让独立的子系统分工处理其中某些具体的任务。

无论哪种问题,在我们把系统分解成了几个子系统之后,都要有一个最明显的问题需要解决:系统之间如何进行通信/交互。

显而易见的,系统可以通过网络进行通信:如基于HTTP,TCP+NIO,RMI或者WebService等等进行通信。同时,具体应该是同步还是异步通信,这也是一个需要考虑的问题。

那么问题就来了,当子系统越来越多,由于系统间的通信没有统一的标准,这将导致开发人员每需要访问一个子系统时,都可能需要使用/学习不同的交互方式,这大大增加了维护成本。

这时候,SOA就出现了,它是为了解决统一交互方式这一问题出出现的。

SOA(Service-oriented architecture),强调的是系统之间使用一个统一标准的服务方式进行交互,各个系统可以采用不同的语言,不同的框架实现,而交互则全部通过服务的方式进行。

SOA所带来的挑战如下:

  • 服务多级调用所带来的延时。

任何一个分布式的应用由于需要通过网络进行调用,除了成功和失败之外,都会多出一种状态:超时。而这在服务多级调用的时候会带来不少挑战:例如:A—>B—->C的过程中,可能会带啦大幅度的延时。为了解决高性能交互,需要完善调用的过程,例如当B服务执行的时如果已经超时,就没有必要调用C服务了,而应该直接抛出超时异常给A。

  • 调试/跟踪困难

如 A–>B–>C的过程,假如B报错了,调用者可能会认为B服务的问题,而B服务的开发人员则可能认为C的问题。而C则又可能认为这是网络的问题。这就导致了出现问题时候调试/跟踪困难

  • 更高的安全/检测要求

未拆分系统时候,安全和检测只需要在一个系统出现,而拆分后,每个系统都需要相应的安全控制和监测。

  • 现有应用移植

这是推广SOA的大挑战。应用越多,难度和时间越大

  • QoS(Quality of Service)的支持

每个服务提供者能支撑的访问量是有限的,因此设定服务的QoS非常重要,并且应该尽可能利用流量控制、机器资源分配等保障QoS

  • 高可用和高可伸缩的挑战

这是互联网应用必须做到的,而SOA由于承担了所有服务的交互,因此其在这两个指标上影响重大

  • 多版本和依赖管理

由于服务多起来后,需要对服务的依赖关系进行管理,以便升级时做相应的安排。

JVM垃圾回收算法

GC的原理是,找到不再被使用的对象(没有被引用),然后回收这些对象所占的内存。通常使用的是收集器的方式实现GC,主要有引用计数收集器和跟踪收集器

  • 引用计数收集器

顾名思义,引用计数收集器做的事是记录每个对象现在被引用的数量,而当计数器下降到0的时候,证明已经没有被引用了。就可以被回收了:如下图所示

引用计数收集器

当A释放了B的引用后,就可以回收B的所占用的内存。

但是引用计数需要对每一个对象赋值时进行计数器的增减,这有一定的消耗。更重要的是,他无法实现循环引用的场景。例如如果B和C相互引用,那么即使A释放了B和C的引用,也无法回收B和C。

- 跟踪收集器

采用集中的管理方式,全局记录数据的引用状态。基于既定的条件触发(如定时或者空间不足),执行时候需要从跟集合扫描对象的引用关系,这要有复制(Copying)、标记-清除(Mark-Sweep)和标记-压缩(Mark-Compact)三种实现算法。

ADB Server Didn’t ACK ,failed to Start Daemon 解决方法(小心风行客户端)

最近重新学习Android,但这两天遇到了极为奇怪的问题,突然之间,启动ADT的时候就报出下面的错误:

[2014-10-03 12:15:43 - adb] ADB server didn't ACK
[2014-10-03 12:15:43 - adb] * failed to start daemon *
[2014-10-03 12:15:43 - ddms] 'E:\Programming\ADT\adt-bundle-windows-x86_64-20140702\sdk\platform-tools\adb.exe,start-server' failed -- run manually if necessary

查了一下stackoverflow, 大多数的解决方案都是:

  1. 关掉eclipse
  2. 在任务管理器中把adb.exe关掉
  3. 进入adb所在目录,然后执行adb start-server,成功执行则问题解决

问题应该就解决了。但我的问题是, adb start-server 启动不起来!

最后发现了是端口占用的原因导致。

解决方法如下:

1.adb nodaemon server

查看不能执行的原因,输出:

cannot bind ‘tcp:5037’

2.定位到了是端口的问题!是5037端口被占用了!

3.netstat -ano | findstr 5037

查找谁占用了5037的进程,得到进程pid.

4.杀死该进程。

可以在任务管理器中杀死,或者使用命令: taskkill /pid 端口号 -f

最后发现是tfadb.exe这个程序占用了该端口。查询签名发现这是风行客户端!!

真是无比坑爹啊,安装了风行的同学真的要注意下。

数据库事务的隔离级别与并发控制

数据库拥有ACID四个基本属性。

其中的I(隔离性)要求并发事务中,事务的中间状态是无法被别的事务查看的。例如A账户转到B账户的事务中,不能让别的事务在B账户扣除了100块前查看到A事务增加了100块这个中间状态。

但出于性能的考虑,许多数据库都允许使用者配置隔离级别牺牲一定的隔离性换取并发性。

SQL定义了四种隔离级别:

  1. Read Uncommitted: 读取未提交的数据,即可读取其他事务修改了但是没有提交的数据。这是最低的隔离级别(会导致脏读)
  2. Read Committed:只能读取已经提交的数据。解决了脏读的问题但是没有解决“不可重复读”,即一个事务中多次读取的数据的内容可能是不一样的。例如,事务2在事务1开始后修改了A账户为150,而第一次读到A账户有100块,然后事务2提交,第二次读的时候就变成了150块。
  3. Repeatable Read:保证可以事务多次读取到的数据行的内容是一致的。但还是有可能导致幻读,即同一事务中,第二次读到的数据行中拥有第一次没有读取到的。
  4. Serializable:最高级别的隔离级别,即事务是可串行执行的,就像一个事务执行的时候没有别的事务执行一样。

并发控制

事务的锁分为读锁和写锁。允许为同一个元素增加多个读锁,但只允许加一个写锁,且写事务会阻塞读事务。

由于互联网的业务属性决定,读事务远远比写事务多得多。而加锁一定程度上阻碍了读的性能,对于读性能的优化是一个刚需。

现在有以下两种方法可以大大提高读取的效率而不需要加锁

写时复制(Copy-On-Write)

读操作不需要加锁,而当需要写操作的时候,以B+树为例:

1 拷贝:将从叶子到根的所有节点拷贝出来

2 对拷贝的内容进行修改。

3 提交后,原子地切换根节点指向新的节点。

这样读操作并不需要加锁,并不会被写操作所阻塞,但问题是写的时候需要拷贝结点,而且多个写操作是互斥的,一个时刻只能允许一个写操作

多版本并发控制(Multi-Version Concurrency Control,MVCC)

对于读操作也不需要加锁,原理是对于每行的数据维护多个数据版本。MySQL InnoDB的存储引擎为例,InnoDB对每行数据隐式地维护了两列——“最近被修改的事务号”和“被删除事务号”。

SELECT: 需要满足以下两个条件才能返回

  1. 行的修改版本号小于当前事务号。(证明事务开始前就被提交了)
  2. 行的删除号不存在,或者大于该事务号。(没有被删除,或者事务开始后才被删除的,保证可重复读) 在可重复读的隔离级别下,后开始的事务对数据的影响不应该被先前的事务看到,所以应该忽略后面事务的操作。

INSERT

直接把修改的事务号改为当前事务号

DELETE

直接把删除的事务号改为当前事务号。而不是真正的删除

UPDATE

更新行的时候,复制一份数据并修改最近修改的事务号为当前事务。

MVCC在读取数据时候不需要加锁,会通过对应的事务号返回需要的记录,大大提高了并发性。但由于维护了多个版本的数据,需要定期清理不再使用的数据。

怎么在Spring Controller里面返回404

由于大多的客户端和服务端是独立的(可能用不同语言编写),客户端无法获知服务端的异常,所以普通的异常处理并不足以提示客户端。而基于HTTP协议的服务,我们则需要按照服务端的异常而返回特定的状态码给客户端。

以返回404状态码为例,在Spring 的Controller里面我们可以有以下3种方式处理:

  1. 自定义异常+@ResponseStatus注解:

     //定义一个自定义异常,抛出时返回状态码404
     @ResponseStatus(value = HttpStatus.NOT_FOUND)
     public class ResourceNotFoundException extends RuntimeException {
         ...
     }
    
     //在Controller里面直接抛出这个异常
     @Controller
     public class SomeController {
         @RequestMapping(value="/video/{id}",method=RequestMethod.GET)
         public @ResponseBody Video getVidoeById(@PathVariable long id){
             if (isFound()) {
                 // 做该做的逻辑
             }
             else {
                 throw new ResourceNotFoundException();//把这个异常抛出 
             }
         }
     }
    
  2. 使用Spring的内置异常

    默认情况下,Spring 的DispatcherServlet注册了DefaultHandlerExceptionResolver,这个resolver会处理标准的Spring MVC异常来表示特定的状态码

      Exception                                   HTTP Status Code
      ConversionNotSupportedException             500 (Internal Server Error)
      HttpMediaTypeNotAcceptableException         406 (Not Acceptable)
      HttpMediaTypeNotSupportedException          415 (Unsupported Media Type)
      HttpMessageNotReadableException             400 (Bad Request)
      HttpMessageNotWritableException             500 (Internal Server Error)
      HttpRequestMethodNotSupportedException      405 (Method Not Allowed)
      MissingServletRequestParameterException     400 (Bad Request)
      NoSuchRequestHandlingMethodException        404 (Not Found)
      TypeMismatchException                       400 (Bad Request)
    
  3. 在Controller方法中通过HttpServletResponse参数直接设值

     //任何一个RequestMapping 的函数都可以接受一个HttpServletResponse类型的参数
     @Controller
     public class SomeController {
         @RequestMapping(value="/video/{id}",method=RequestMethod.GET)
         public @ResponseBody Video getVidoeById(@PathVariable long id ,HttpServletResponse response){
             if (isFound()) {
                 // 做该做的逻辑
             }
             else {
                 response.setStatus(HttpServletResponse.SC_NOT_FOUND);//设置状态码
             }
             return ....
         }
     }
    

更多详情: Spring MVC 文档

什么时候应该使用“静态类”而不使用单例?

经常我们都会使用单例,而有时候我们又会贪图方便而使用一个类然后全部都使用静态方法。那么,到底什么时候我们才应该使用这种都是静态方法的类呢(注:java没有静态类)?

  1. 所有方法都是一些工具类的方法,如Math
  2. 不希望被gc回收又不想自己去处理实例。
  3. 很确定这个类将来也不会是有状态的(stateful)而且你确定你不需要多个实例。

注:

如果我们使用单例模式的话,将来假如我们需要多个实例,将非常轻松的改变,但是使用static方法的类就不行。而且使用单例的话,将很好地利用继承、多态等方法。

stackoverflow相关讨论:

为什么java8引进了Lambda表达式

我们借用官方文档的例子:http://docs.oracle.com/javase/tutorial/java/javaOO/lambdaexpressions.html#approach1

看看Lambda 怎么样让我们的代码更具有可读性,而且更为简洁。

我们假如需要输出一个人的信息,我们可能有以下的类

public static void printPersons(
    List<Person> roster, CheckPerson tester) {
    for (Person p : roster) {
        if (tester.test(p)) {
            p.printPerson();
        }
    }
} 



interface CheckPerson {//接口,用于对于Person对象的验证
    boolean test(Person p);
}



class CheckPersonEligibleForSelectiveService implements CheckPerson {具体的CheckPerson实现 
    public boolean test(Person p) {
        return p.gender == Person.Sex.MALE &&
            p.getAge() >= 18 &&
            p.getAge() <= 25;
    }
}

然后当我们调用的时候,我们会写:

printPersons(roster, new CheckPersonEligibleForSelectiveService());

显然,Person的确是一个重要的对象,但是CheckPersonCheckPersonEligibleForSelectiveService呢? 是不是一定需要一个新建一个对象来只调用一次他的方法?其实,我们只是需要这样做(面向对象中,必须要有一个类,然后新建他的实例(非静态的话),才能调用他的方法),所以才这样做。

那假如封装的只是一个函数,然后我们可以简单的传递这样一个函数就能做到这些的话,是不是就很好了呢?这样的话,我们就不需要又创建一个实现类,又自己说动新建一个对象来仅仅只是调用一个函数了。

这时候,我们就需要Lambda了。

printPersons(
    roster,
    (Person p) -> p.getGender() == Person.Sex.MALE
        && p.getAge() >= 18
        && p.getAge() <= 25
);

这样的代码极为的简洁而且具有可读性。而且,我们不需要手动去新建一个CheckPersonEligibleForSelectiveService实现类,而且也不需要手动去新建这样的对象,lambda都帮我们做好了。

当然,我们原本也可以使用匿名类去解决这个问题

printPersons(
    roster,
    new CheckPerson() {
        public boolean test(Person p) {
            return p.getGender() == Person.Sex.MALE
                && p.getAge() >= 18
                && p.getAge() <= 25;
        }
    }
);

但,我们问问自己,我们真的有必要创建一个对象就仅仅为了去做Person验证吗?这样非常的反直觉。

如果我们需要的仅仅是一个函数而不是一整个对象,那么我们就应该关注在这个函数本身,而不是为了这个函数而衍生出一个类,一个对象。

更多本问题讨论请参考:http://stackoverflow.com/questions/23097484/why-lamda-expression-are-introduced-in-java8