Android – AsyncTask与Handler 的关系

前面的文章已经分析了HandlerAsyncTask的原理,现在说说他们的异同点:

1. 相同点:AsyncTask就是封装了Thread+Handler,来简单实现做异步任务同时又能更新UI

2. 不同点: 在android 3.0之后的AsyncTask中的任务默认是串行执行的,如果你有多个异步任务要并发执行,应该使用Thread(Pool)来替代。 当然AsyncTask也是考虑到了这一点,所以提供了一个executeOnExcutor方法,可以传入我们自定义的executor来进行并发执行。AsyncTask内部实现的Executor是SerialExecutor, 是串行执行的,来看代码他是如何串行化的:1

调用execute的时候,会先把异步任务封装为一个runnable放到一个队列里面,然后再判断要不要执行该任务。
判断的依据很简单,如果当前已经有任务在跑了,那么就不跑了。那么,这个任务会被丢弃吗?不会!当前任务执行后的finally块里面会执行下一个任务。这里的mActive变量设计的很巧妙,当mTasks.poll()得到的下一个任务为空的时候, 就不会再往下执行了, 所以可以保证所有任务都能被执行到,而且任务都是串行执行的。这里execute和scheduleNext两个方法都加上了synchronized关键字,所以也不会有线程安全的问题。
看下AsyncTask的execute方法:
2
这里用的是默认的Executor, 而这个默认的Executor就是SerialExecutor, 而且是单例的
3
看到sDefaultExecutor前面有个volatile关键字就说明了这个Executor是可以被更改的,果不其然, AsyncTask提供了这么一个方法:
4
这样一来,我们通过更改默认的Executor就能达到AsyncTask里面的任务并发执行的目的。
另外,回到AsyncTask的execute方法,是通过executeOnExecutor来提交任务的,而恰巧,executeOnExecutor这个方法是public的,说明我们也可以通过executeOnExecutor这个方法来指定我们自定义的Executor来执行任务,从而达到并发执行的目的。
5
同时,AsyncTask也定义了一个Executor常量供我们使用(实际上他的SerialExecutor也在用),就是
6
所以我们调用AsyncTask.executeOnExecutor的时候,可以把AsyncTask.THREAD_POOL_EXECUTOR作为第一个参数传过去即可,也省去了我们自己定义(如果实在需要还是真的要自己定义)的代码了。
如果用Thread+Handler来实现类似AsyncTask类似的功能,可以用Handler的post方法,在Thread中处理任务的过程中,如果想要更新UI线程,有几种方法,一种是post,一种是sendMessage。如果从用法上来看,post应该说更实用一点,sendMessage的话你还要去封装一个消息,然后接受到消息的时候还要再把数据拿出来,进行UI的更新,例如:
7
3. 最后画一幅图来总结一下AsyncTask和Handler之间的关系:
9
绿色部分是Android框架定义的方法,我们无需去重载,而粉色部分的方法我们可以去重载来实现自己的业务逻辑。
注意:在doInBackground被调用之前会有一个线程池的调度过程,以及会先执行onPreExecute这个方法,这里省略了。

Android – AsyncTask 源码分析

AsyncTask,看名字直接翻译就是异步任务的意思,顾名思义,是用来处理异步的任务的,那么什么任务需要异步处理呢,那些需要比较耗时的计算和资源获取都需要异步处理。如果不异步处理的话,处理这个任务的UI线程就会出现卡顿的情况。

1. AsyncTask是什么?
先看源码中的javadoc:
1
简单翻译一下: AsyncTask能够适当和简单地使用UI线程。可以处理后台操作以及发送操作结果到UI线程,而不需要操作Thread和Handler。AsyncTask就是设计用来简化Thread和Handler的使用的工具类,在做一些短操作的时候(最多几秒)应该观念性的想到AsyncTask。如果你需要保持线程在后台跑一段时间,那么强烈建议你使用JUC里面系统的一些并发相关的API,比如Executor,ThreadPoolExecutor和FutureTask等类。一个异步任务是由一个计算逻辑来定义的,跑在后台,在跑完之后将结果反馈给UI线程进行处理。定义一个异步任务需要三个泛型类型(Params,Progress和Result)以及4个步骤(onPreExecute,doInBackground,onProgressUpdate和onPostExecute)。
翻译太差,看得云里雾里有木有,其实简单的说就是异步任务(AsyncTask)是为了简化Thread和Handler的配合使用而定义的一种工具类,实现他,只要制定好几个参数的泛型类型以及覆盖几个步骤的方法(事件)就可以了。其底层还是通过线程(池)和Handler来实现的,后面会提到。

2.定义一个AsyncTask
可以通过匿名内部类的方式,也可以通过类继承的方式,定义一个AsyncTask,但是都必须实现doInBackground这个方法。(吐槽:这个方法的参数类型就是泛型定义里面的Params,但是为什么是不定数组,这个比较奇怪,不知道这个接口的设计者怎么想的。)后面再将实现机制的时候在说说这个方法要怎么实现。

3.实例化AsyncTask
new一个AsyncTask的时候,发生了什么?直接看他的构造方法就知道了:
2
这里mWorker就是一个Callable对象,执行的时候会调用到他的call方法,这里call方法会调用doInBackground方法,所以这个mWorker实际上就是把doInBackground做了封装,保证在执行的时候会调用到这个后台方法。而mFutrueTask就是吧mWorker再封装成FutureTask对象,在任务完成的时候执行postResult方法,把数据发给内部的Handler进行处理。这里内部的handler是在类加载的时候定义好的:
3
看看他的实现:
4
很简单, 根据发过来的消息类型(what),去执行对应的那个方法。注意这里有finish和onProgressUpdate两个路径,先看看onProgressUpdate是用来干嘛的:
我们在定义AsyncTask的时候,可以覆盖其onProgressUpdate方法,这个方法可以更新UI,而且不是等AsyncTask执行结束的时候, 那么是在什么时候触发呢,是在doInBackground中调用了publishProgress这个方法的时候,就会触发这个事件。这个可以干嘛用呢?最直接的一个例子就是进度条(下载,听歌播放等)。进度条一般是有一个进度的过程,不仅仅是开始和结束两个状态,所以我们处理了部分数据之后,为了更及时地反馈给用户,需要更新进度条的进度,所以需要在doInBackground里面调用 publishProgress方法。这就是为什么定义一个AsyncTask需要第三个泛型参数,这个参数就是为了进度中的数据传过来的。他的实现也很简单:
5
把当前的AsyncTask包装成AsyncTaskResult,加上当前进度的参数,发送到内部的handler去处理。
再回头来看handler中,对于消息的类型(what)是MESSAGE_POST_PROGRESS的消息会触发onProgressUpdate事件,从而实现线程的更新。
那么什么时候触发finish事件呢?很显然上面提到的mFutureTask的结束方法里面会postResult,而postResult就是给handler发送一个what等于MESSAGE_POST_RESULT的消息,这时就会触发finish事件(其实不是finish事件,后面会分析到)了。
所以这里几个参数是对应的
execute(Params...)
doInbackground(Parmas...)
publishProgress(Progress ...)
onProgressUpdate(Progress...)
finish(Result)
画个图来简单理解一下吧:
6
其中, 绿色的部分是我们不需要实现的, 粉色的部分是我们可以实现的一些事件。这里已经把整个AsyncTask的生命周期画出来了。

4.启动AsyncTask
调用AsyncTask的execute(Params...)方法就可以了, 这个方法是直接调用了内部的另一个方法,executeOnExecutor(Executor, Params...),这里传入的executor参数是AsyncTask内部自己实现的SerialExecutor,用法就是一个线程池。当然我们可以在外部直接调用这个executeOnExecutor方法,然后指定我们自己实现(or定义)的Executor就可以了。不过既然人家提供了,不用白不用你说是不?
7
看吧,这里会先触发onPreExecute事件,然后把参数交给mWorker定义的mParams,然后才丢到池里面进行处理。这个SerialExecutor实现也不复杂:
8
先把runnable对象放到队列里面(mTasks.off),然后再出列交给ThreadPoolExecutor处理,这的实现就是实现了异步任务的串行化处理,先来先执行。

5. 更新UI
在执行过程或者结果的时候需要更新UI,那么在哪里更新呢?不可能直接在doInBackground方法里面更新,因为这时候还不是在UI线程里面,只有在几个on*的事件方法内可以执行UI的更新,因为这几个事件确实是在UI线程中执行的。其中onPreExecute是在启动时还没有进入线程池之前的更新,其他几个都是通过handler来实现的。那么,我们在doInBackground中要如何更新UI呢?很简单,调用publishProgress方法就可以了,看他javadoc里面提供的代码:
9
调用publishProgress的时候把当前处理的进度(Progress的含义)传过去,然后在onProgressUpdate事件里面就可以使用这个进度的结果,然后更新UI.

6. 任务结束
任务结束,也会触发事件, 一种是正常结束, 会调用onPostExecute;另一种是用户自己取消(调用AsyncTask的cancel方法), 则会调用onCancelled事件.在结束这里我们也可以执行UI操作,比如结束进度条神马的, 你看这个AsyncTask简直就是为了类似进度条的UI的完美设计啊.
10

7.分析总结
最后来分析一下AsyncTask是如何搭建起Thread和Handler之间的桥梁的。首先,先从AsyncTask的入口方法,execute入手,他最终是提交任务给了本地的线程池去处理,这就打通了Thread的这一层。然后他提供了publishProgress这个API来发送消息到Handler里面,就打通了Handler的这一层,如此而为之的目的是为了具备线程的异步执行的特性同时又有更新UI的能力。如下图
11

Android – Handler 源码分析

android开发我也是在学习阶段,由于喜欢看源码,所以顺便把经常用到的一些类都看看顺便分析一下他们的工作机制。

Handler在android开发中应该比较常用,主要用于接收异步处理结果的消息并更新UI,比如做了一个天气应用,用户点击刷新的按钮的时候,为了不让界面卡住,这时候应该起一个异步任务去获取数据,然后等数据获取完成之后才更新UI的结果。不同于js更新web上的元素,android的UI更新操作都不是线程安全的,所以需要在同一个线程下才能更新UI,而js是没有多线程的概念的,所有操作都在同一个线程中,所以不会有线程安全的问题。来看看Handler的官方注释:handler.comment

简单翻译一下:一个Handler可以让你用来发送和处理消息(Message),以及消息上附带的Runnable对象,整个是跟消息队列(MessageQueue)一起使用的。每一个Handler实例会关联到一个唯一的线程和该线程的MessageQueue。如果你创建了一个Handler,他将会跟创建这个Handler的线程和该线程的消息队列绑定在一起。也就是说通过把消息发往这个队列和在出列的时候处理他们。Handler一般有两种应用产景,(1)就是调度消息和runnable对象在未来的某个时间点执行(归纳起来就是消息的发送);(2)可以把消息发送到其他线程里面。再简单概括一下就是Handler是跟创建他的线程绑定在一起的,然后通过消息队列方式,实现线程安全的操作。

1. Handler的创建

当你在一个Activity(或者其他)里面new一个Handler的时候,他最终会执行下面这个构造函数:handler.create

第一段if可以无视,是判断Handler是不是静态的,如果不是给你一个警告说可能会内存泄露。Looper.myLooper()其实就是在当前线程的ThreadLocal中获得Looper对象,同时从looper中获取MessageQueue。这里的callback其跟覆盖handleMessage的机制差不多,估计是为了某些地方使用了这种方式,然后一直兼容到现在。

2. 消息的发送机制

当我们调用handler的send()/post()系列的方法的时候(这些方法的调用一般发生在异步处理的任务完成之后,像触发线程的更新操作),handler做了什么操作?他们实际上殊途同归,最终都调用了这个方法:handler.sendMessage

再看看enqueueMessage方法:handler.enqueue

第一句把msg.target设置为当前的handler本身,这一步很重要,因为在这之后,就跟handler没关系了,后面会分析;接着就调用消息队列的入列方法把消息体丢到队列里面排队等待执行。

可以看到,不管是什么方式,最终都是把消息(runnable最后也是包装成消息)丢到消息队列里面。

3. Looper的工作机制

Looper的工作机制就跟他的名字一样,就是一个循环器,作为MessageQueue的消费者,进行消息的出列和处理操作。其核心代码就在loop()方法中,来看看looper

看到for(;;)我们就能猜到这是一个典型的生产者消费者模型了,在for循环里面,looper不断的订阅消息队列的下一个元素(next()方法),然后调用Handler的dispatchMessage方式分发消息给handler进行处理。上面说到设置msg.target那一步很重要就重要在这里。消息处理完之后还可以回收再利用,这点不细说了(如果你的Message是new出来的,回收了也没用,科学的使用方式是使用Message.obtain() 系列的方法来创建消息。)这里消息一个一个处理完之后才会处理下一个,是单线程串行执行的,而且跟创建handler的线程是同一个线程,所以完美的避免的线程安全的问题。那么你的疑问会是,究竟是谁来调用这个loop方法的,不是会卡住吗?是的,这就是精髓所在,这里的looper是UI线程在初始化完所有的UI操作之后调用的,这样一来,就不会有卡住的问题了。

4. 消息分发机制

上面Looper拿到消息之后,会调用Handler的dispatchMessage进行消息的分发操作,这里分发不是简单的调用Handler中的handleMessage方法,而是有其他逻辑在里面的,来看handler.dispatchMessage

惊呆了有木有,handleMessage方法是优先级最低的!为什么呢,因为有Handler有个post方法呀。post方法的参数是一个Runnable对象,然后通过创建一个Message,再把message的callback设置为这个runnable,然后再发送到消息队列里面。post方法的场景是你更新UI的时候需要知道获取到了什么新的数据,然后直接更新。而handleMessage方法可以不需要知道更新了哪些数据,就仅仅更新UI就可以了(当然,神奇的Message里面还配置了bandle,可以传数据的,所以其实都差不多,哈哈)。至于Handler的mCallback成员则是回调函数的通用写法,跟handleMassage的方式差别也不大。到这里就能明白为什么handleMessage在定义的时候不是abstract protected void handleMessage()了吧,因为他确实不是必须的。。。

分析到这里,可以看出,handler之所以可以更新UI,不是系统做了什么神奇般的兼容,而是因为他跟UI线程使用的本来就是同一个线程,UI线程通过Looper.loop来等待消息的分发,handler发送消息后把消息放到消息队列里面,而Looper负责从消息队列里面拿数据,又交给handler进行处理,最终实现了UI的异步更新操作。这是个生产者消费者模型典型应用,其中消息队列的功劳巨大,我们来看看他有哪些功能。如果我们开发中也需要实现这种类似的生产者消费者模型,可以使用这一套机制。需要注意的是,MessageQueue我们不能单独定义来使用,因为其核心API的可访问修饰符都是包级别的,我们不能把代码定义到android.os这个包下面,所以他要通过配合Looper来使用。Looper的核心API都是开放的。

5. 消息队列

enqueueMessage入列,可以把消息放到队列里面,这里队列的底层是android的本地代码实现的,其实可以参照juc里面的DelayQueue的实现,机制差不多,都支持延时出列的,这不过实现方式不同罢了。

next()出列,消息出列,队列的出列优先级是入列的时候定义的时间来决定的,时间值越小(长整形)优先级越高。

RESTful与伪RESTful

REST

关于RESTful,前几年就开始火了,其崛起是跟各大互联网开放平台的OPENAPI息息相关的,twitter的API就是声称完全RESTful的,不过其实呢,还是有些是伪RESTful的。那么,什么是RESTful呢?

REST很早(2000年)就有个博士( Roy Fielding,HTTP规范的主要编写者之一)提出来了,英文全称是REpresentational State Transfer,直译过来就是表述性状态转移,看起来更像物理学概念有木有?其核心理念就是把提供服务的所有事物定义一个ID,而这个ID在HTTP服务中通常是一个URI,然后使用标准的HTTP方法(HTTP Method,比如GET, POST, PUT, DELETE, HEAD,TRACE等)来提供同一个资源的不同服务(通常就是CRUD)。至于无状态这一点,HTTP都是无状态了,实际上也可以不用去深究。

那么,RESTful究竟是怎样一种风格呢?看几个例子:

http://www.zhihu.com/people/jiacheo

http://weibo.com/1790181393/AxqXitFF6

http://weibo.com/1790181393

看出来了没有,这里的URI都是 资源URI+资源ID组成的,也就是说,我们要访问一个资源的某种服务的时候,可以直接附上ID,就能获取了,如果要修改资源呢?HTTP方法中有POST, PUT, DELETE等可以对应用来新增/修改/删除资源的操作,但是现实中,我们大多数人用的浏览器都仅支持GET和POST两个方法,所以这个时候RESTful的理念在WEB站点中就无法完美实现了。上面说到,RESTful火起来那段时间刚好是各大开放平台开始崛起的时间,因为他们的OPENAPI大多数是RESTful风格的,由于提供API比展示页面要简单很多,仅提供CRUD操作就可以实现了,这么一来,HTTP METHOD就足够用了,而且各种编程语言提供的httpclient基本也都支持这几个方法,所以RESTful能在OPENAPI中大行其道跟API的简洁性是分不开的。

不能简单的认为没有后缀名的URI就是RESTful的,也不能认为有后缀名的URI就不是RESTful的,RESTful风格不在于你有没有后缀名(RESTful的API接口中,后缀名通常是为了实现区分返回的数据格式,如xml/json等),而是在于你的URI定义是不是对应到一个资源(这里通常是个名词),而不是一个操作(通常是动词)。由于浏览器不支持更多的HTTP方法,所以伪RESTful诞生了。

怎样的URI是伪RESTful呢?比如说这个:

http://www.zhihu.com/people/edit

这个是以动词结尾的URI,定位到的不是一个资源,而是一个动作(编辑),这个页面主要是用于编辑个人资料的时候使用的,如果要RESTful化,该怎么设计呢?

http://www.zhihu.com/people?to=edit

谁说RESTful不能有URL参数的?没有参数你如何实现复杂的查询(query)?

上面说到twitter的api中也有伪RESTful的,咱们来瞧瞧:

http://api.twitter.com/users/show.format

这里的URI是定位到show这个动作,然后传入的参数是用户的ID, 实际上, 如果遵循RESTful风格的话,可以这么搞

http://api.twitter.com/users/id.format 或者 http://api.twitter.com/users.format?id=

伪RESTful也是有存在的价值的,由于浏览器的原因,仅通过HTTP方法无法支持所有的资源的操作,那么何不把对资源的操作也定义一个URI呢?这样一来,伪RESTful其实对网站的开发者更友好,因为他一看这个URI就能知道对应后台的controller(MVC中的C)中的哪一个处理器,在写代码,调试的时候也有优势,不用去看POST的参数中对应的action(动作)究竟是谁,直接就知道了。

那么RESTful与伪RESTful孰优孰劣呢?其实没有什么定论,适合就好,管他是不是RESTful呢(你是处女座?当我没说)!但是当你在对外宣称你提供的API是标准的RESTful风格的时候,就要好好审视一下自己的API是不是标准的RESTful风格了。

国内的网站RESTful风格的也有,比如新浪微博,他的访问页面基本上都是标准的RESTful风格的(修改数据的URL我没研究,不能下定论)所以关于RESTful与伪RESTful,你是怎么看的呢?

apache & tomcat https配置

出于安全的考虑,一是确实能带来一点安全性提升,二是让用户看起来安全,一些网站开始要使用https协议来提供服务(web,api),刚好我们新产品是面向企业级的用户,需要提供https服务和接口,所以就遇到了配置问题。

首先这篇文章是在你申请好了https(ssl)证书之后该干的事。本文介绍是linux系统下的apache2的https配置,windows下的基本相同,就是一些文件的路径写法不一样还有模块的安装不一样。

使用apache2的话,他自己内建了(build-in)mod_ssl 模块,不需要额外编译和安装,如果你用的apache1.*系列的,可以使用 sudo yum install mod_ssl 来给apache安装mod_ssl

如果是内建的mod_ssl, 不需要再httpd.conf里面配置LoadModule,默认是可以直接用的,如果是自己安装的,则需要LoadModule一下。下面讲具体配置

1. 首先你申请到https证书后,服务提供商会给你如下几个文件。

server.crt

server.key

ca.pem

sub.class1.server.ca.pem

说明一下, server.crt 是服务提供商颁发给你的证书,是一个纯文本文件,一般以

-----BEGIN CERTIFICATE-----

开头,以

-----END CERTIFICATE-----

结尾,前后都没有多余的空格。

server.key就是这个证书的私钥文件,用来解密用的,也是一个纯文本文件,以

-----BEGIN RSA PRIVATE KEY-----

开头,以

-----END RSA PRIVATE KEY-----

结尾,首位也都不带空格。

另外两个*.pem文件,是CA证书,其中ca.pem是root证书,sub.class1.server.ca.pem 则是颁发证书的网站给你的的这个https的证书的服务商证书,也就是说你要认证你的https(ssl)证书必须先通过这个sub.class1.server.ca.pem再通过ca.pem来完成整个链路的验证.

2. 接下来就是apache配置证书文件的路径了

先把上述文件都统一放到一个目录,要注意保障文件的安全,不要被盗用或者删除。这里我们放在 /var/www/ssl 目录下面

打开httpd-ssl.conf文件(一般在%apache安装目录%/conf/extra下面

首先配置证书文件:

SSLCertificateFile "/var/www/ssl/server.crt"
找到相应的项,有的话就覆盖,没有的话就新建一个,指定证书文件的位置
接下来是配置证书私钥文件:
SSLCertificateKeyFile "/var/www/ssl/server.key"
配置方法同上
最后配置证书链路文件路径
上面说到,我们证书要先通过sub....pem然后在通过root认证,所以需要把这两个文件合并。
cat  sub.class1.server.ca.pem  ca.pem  > ca.chain.pem.crt
然后定位到这个ca.chain.pem文件
SSLCertificateChainFile "/www/var/ssl/ca.chain.pem.crt"
至此,整个证书的配置就OK了,可以重启下apache,访问以下你的https://域名/ 就能看到已经生效。
3. 配置完证书后,配置一下apache请求转发给tomcat,有多种方式,可以用mod_jk,也可以用ProxyPass,我这里用ProxyPass。
还是编辑同一个文件httpd-ssl.conf
在<VirtualHost _default_:443>定义块的里面,添加ProxyPass

ProxyPass / ajp://localhost:7204/
ProxyPassReverse / ajp://localhost:7204/

我这里是把请求转发给ajp去处理,tomcat默认都会有ajp的端口,因为我应用比较多,为了区分开了,所以我自己使用了7204端口,如果默认的话,应该是8009端口,是在不清楚可以看下tomcat的配置(%tomcat安装目录%/conf/server.xml内的Connector protocol="AJP/1.3" 这个配置项)
如果我这里使用ProxyPass / http://localhost:8080/ 可不可以,答案是可以的,但是你的tomcat在处理request的时候,无法分辨用户访问的url是不是https的(当然apache可以配置保留用户的原始请求url给tomcat)
4. 大部分的https的ssl证书是颁布给某个子域名的,如果你的所有子域名都需要上https,建议使用购买一个可以多次使用的。对了,一开始可以申请startssl来用一下,可以免费使用30天,测试完了,可以再买高端的。
运行起来了,但遇到如下情况,该怎么办
nosecure
不要慌, 这时候你的https已经生效了,唯一的问题是你的页面请求的资源使用了其他非https的资源,比如cdn的图片,css等的引用,如果是js资源,建议都要使用https,不然浏览器会认为很不安全的哦。

通过apache proxy访问tomcat ERR_CONNECTION_RESET 问题排查

0. 这是个钓鱼贴你造么,看到后面你就造了。

1. 现象, 通过浏览器访问 www.qipeng.com 返回页面链接被重置

查看apache-access 日志, 是有发起请求的, 说明在proxy那里存在问题

2. ping www.qipeng.com 结果是DNS已经生效的
1
3. 改用mod_jk, 而不是proxy来处理请求转发, 发现服务器身上是可以curl通的
2
注意这台curl的机器不是企朋的服务器本身, 而是在服务器上随便找的
4. 然后用本地访问依旧是
3
5. 怀疑是DNS问题, 于是本地配置了hosts, 把www.qipeng.com 强制指向 223.4.49.236
结果还是一样, 本地配置别的域名 weike.taovip.com 指向 223.4.49.236, 访问这个域名, 结果是正确的, 我嚓, 这个正是我的企朋的页面!
4
6. 那么到底他妈的问题出在哪里呢,难道我被我自己墙了?找其他同事, 结果也是一样的- -||
找了半天, 突然想起老大好像昨天刚申请域名备案, 于是开始往这方面靠, 结果真正的原因是域名正在备案中, 期间访问不了, 呵呵! 天朝, 我又涨姿势了!
7. 解决方案
7.1 其实不需要解决, 产品也是刚做, 还没啥用户, 自己本地配置个其他域名先顶一阵等备案好了就OK了.
7.2 真的必须要访问的话可以找个国外的服务器, 先跑一跑, 前提是你的程序没有太多其他的依赖, 否则得不偿失.
7.3 知乎某知友的对策: http://www.zhihu.com/question/19794926

 

Google Java Style 中文翻译

Google Java Style 终于在前不久发布了,抽空学习了一下,同时生成了一个java代码注释风格的中文解释,如果你有兴趣可以看看。

java source code 在此: github下载

代码中的注释基本上已经把Google Java Style中提到的一些要求写进去了,而且是出现在该出现的地方。主要是缩进的地方我跟google要求的2个字符的缩进没有保持一致, Google的这篇文档中要求的是2个字符,而我个人一直以来都是习惯使用tab来缩进,所以这个恕难从命哈。

基本上跟以前的java编码习惯差不多,就把一些Java 7之后的新的语法特性也提到了,更之前的广为流传的Sun的Java Style区别不大。如果你之前是跟Sun的保持一致的,基本上风格上不需要大改的。

其实编码风格无所谓好坏,最重要的是能让别人更容易的读懂你的代码,这并不是说每一行代码都要写注释,注释不能解决一切问题,更重要的是各种变量和名称的命名规范,这个很重要。如果代码本身就能自解释,那才是极好的!

用mongodb构建延时队列

延时队列(DelayQueue)的使用场景有很多,比如订单类的系统,用户创建订单后一段时间内如果没有付款,那么要把用户的这个订单关闭掉,同时把库存还原回去。解决的方案有很多,一种是用定时任务,定时去扫描符合条件的数据出来进行出来,还有一种就是把这个丢到延时队列里面,等时间到了自动出列之后处理。

由于我们业务场景需要实时的延时队列,也就是必须准时处理,如果通过定时扫描的话,如果时间间隔短,会任务太多处理不过来,如果时间间隔长,会导致中间的有一些延时出列了。之前有调研过一些已有的产品,像twitter的beanstalkd,就是用来处理延时的任务的。不过当时由于在测试环境跑起来了,但是在线上却一直不可用,另外也没有人熟悉这个产品,怕后期带来运维的问题,所以也没有用起来,于是决定自己开发了。google一把,很多人在吹嘘用mongodb替换消息队列(MQ),看起来也很简单,于是我们准备试一下,用来做延时队列。这一试,就定下来了,然后在线上跑了1年多了。

先说一下我们部署mongodb的机器配置,4核CPU,8G内存的VM。你没看错,我们所有数据库都部署在淘宝聚石塔的虚拟机上面。这里不是做广告,如果你要做应用,建议不要用聚石塔,哈哈。我们是做淘宝业务,有“安全性”的限制,所以才用。

之所以用mongodb做“队列”,是因为mongodb有一个叫findAndModify的操作,这个操作是原子性的,也就是你可以修改一条记录的同时把老的记录返回。基于这个操作,我们可以在把一条记录标记为处理中的同时获取到这条数据,这样别人如果同时也是进行这样的处理,由于这个操作具备原子性,你们处理的任务不会重复,所以简单实现了出列的问题。所以简单的, 我们可以这样设计我们的延时队列里面的字段设计如下:

timestamp:出列的时间戳,这样我们可以根据系统当前的时间判断该记录要不要出列

status:状态,用来标记该记录目前是可以出列还是已经出列。0、1就可以搞定了

data:数据,表示你要保存到队列中的数据,出列后根据这个数据来处理任务。

定义好了,出列的操作就是:


//获取当前时间戳
long current = getCurrentTimeMillis();
//0是初始化状态,表示可以出列, 1表示已经出列过了.
DBObject obj = collection.findAndModify({status:0, timestamp:{$lte: current}},{$set:{status: 1}});
//没有命中, 说明当前不需要出列
if(obj == null){
return null;
}
//获取数据, 序列化为二进制的话, 可以兼容各种各样的数据格式
byte[] data = obj.get("data");
//反序列化
return SerializableUtil.deserial(data);

上面是伪代码, 既不是java的也不是javascript的.

是不是很简单,我们的队列搞定了。
如果故事到这里就结束了,那我就不写这篇文章了,呵呵呵呵呵。

由于业务的增长,我们在延时队列里的数据越来越多,处理任务的机器不够用了,不得不剥离成多台机来处理这些任务,这时候作为一个“队列”的挑战才刚刚开始。

由于多台机,你必须要让每台机处理的数据没有重复。上面的findAndModify已经很好的满足需求了是不是?是的,确实满足了。但是findAndModify有个致命的弱点,就是不支持批量findAndModify。由于每天要处理上千万的延时数据,如果全部请求都压到mongodb上,我们的mongodb吃不消,关键我们也不想多加机器。所以问题来了,我们要批量的findAndModify。于是乎,我们顺延这findAndModify的特性,自己弄了一套批量操作的findAndModify,代价是引入分布式锁,在批量find和批量modify的这段时间加分布式锁,这样能保证数据不会被重复处理。由于分布式锁我们用了zookeeper来实现,zookeeper的链路稳定性是我见过最差了,没有之一。时不时客户端就会断开连接,时不时客户端就会session expired。而面对这些问题,zookeeper是没有直接提供接口给你解决的,因为他的接口实在是太底层了,估计写那套client的人以前是搞底层系统开发的,所有问题都要调用方来处理,真不是一般的难用。zookeeper的问题始终是可以解决的,但接下来一个更大的问题是队列的吞吐量(qps)。由于改成了批量处理之后,mongodb平时的负载确实变低了好多。而平时一次出列1000条数据,不会带来拥堵的问题,直到那一天。

没错,那一天就是2013年的天猫双十一,我们系统之前为此做了扩容,机器数量从平时的20台加到了50+台。我们预估当天的数据量会达到平日的6倍。但实际上那天的量,远超我们想象。订单量达到了平时的10倍以上,在双十一前10分钟,我们的延时队列就被冲垮了。我着急了,一看,堵了100+W的数据了。过了一个小时之后,这个堵的数据量达到了800+W。眼看着这么多订单,白花花的就溜走了。直到第二天10点,我们终于解决了这个问题。

解决方案很简单粗暴,就是把批量findAndModify这套方案直接干掉,由于总共有6台任务处理机器,我们直接使用单机的模式,一台机器使用一个队列,把分布式锁抛弃掉。也是用批量的方式,批量出列,处理数据,然后批量删除。效率极高,在后面的几次高峰期都顶住了压力,“顺利”度过了双十一。这种模式也带来一个问题,如果处理数据的机器下线了怎么办?这涉及到了数据迁移的问题,好解决,我们写了个小工具,可以很快的把遗留的数据迁移到另外的机器上。

另外的解决方案是把架构搞成C/S的,做成一个单独的server来访问mongodb,任务处理系统都来订阅这个server。这样也不会有锁的问题,只不过当业务量继续增加,需要扩展服务器的时候,同样的问题还是会来临。上面的单机解决方案看起来是很简单,但实际上效果也是很不错的。当你真正贴近业务的时候,你会发现不是要做一个架构多么牛逼的系统,而是应该做一个能简单维护却能很好解决问题的系统。

如果你想设计延时队列,可以先参考java的延时队列的实现:

java.util.concurrent.DelayQueue

如果可以直接在上面加一层持久层,就可以简单实现延时队列了。

一些总结:能不用锁的地方一定不要用锁,锁会把你的性能耗尽。能不用分布式锁的地方一定不要用,如果你的系统需要使用分布式锁,可以想想有没有其他的简单粗暴的方案,也许会有更好的搜获。分布式系统的一个特点是高并发,如果架上了分布式锁,很可能让你的系统变成了串行处理的系统了,这样就违背分布式系统的初衷了。

附 beanstalkd 项目地址:http://kr.github.io/beanstalkd/

 

关于用户研究

昨天晚上,黄猿师兄(@小猪和蜜桃的故事)过来给我们分享了关于用户研究的一些心得,感觉很不错,所以也分享一下。

1. 用户研究是隶属于UED的,在产品的各个阶段都需要用到他。

在产品未出来的时候,用户研究需要先行,进行用户调研,市场调研,竞品分析等等。在产品出来之后需要进行收集用户反馈,可用性测试,用户数据分析来改善产品的用户体验,最后当积累了一定量的大数据的时候,可以通过挖掘用户数据来推断出接下来做什么产品会受用户欢迎。大数据不仅仅是属于后端同学使用的,实际上数据的分析在改善产品方面有很大的作用。

2. 用户体验的五大层面。

从上到下是表现成,框架层,结构层,功能层和战略层。最顶层是用户直接能感受到的东西,而最底层是用户感知不到的,看起来很虚,但却很影响深远的一层。产品的形成,往往是由战略层开始,通过市场调研,发现做某类产品有市场,于是交给下游的产品经理做功能的需求设计,产品经理最后把功能和结构层框架层跟交互设计师一起确定,把产品的交互稿定下来,接下来就是UI表现层了。所以一个产品的产生往往是由战略层来决定的,他一开始影响深远,但随着时间的推移,他的影响会越来越小,直到战略方向要大调整的时候, 整个产品也即将面临大改版了。如下图

用户体验5要素的影响和时间的关系
用户体验5要素的影响和时间的关系

也可以看出,如果一个产品一开始的战略定位不对,后面调整起来,就是全盘都要调整了。(大公司的表现是,当产品换了一个新的老大,往往一个新的老大会想表现自己的‘杰出才能’,调整产品的战略,导致之做的很多工作要重新调整,最终反馈到用户的是:“哇塞,你们又升级啦!”“尼玛!这个按钮怎么没了?”“卧槽,排序算法又变了?” 等等)

3. 用户研究的手段

分类起来可以归结为两种:一种是面向人的定性研究,另一种是面向数据的定量研究

在做面向人的定性研究的时候,必须要把人物角色和使用场景都考虑到:

人物角色:好比说一个产品的使用者,是一个什么样的人?一个淘宝卖家客服,还是白领消费者,还是公司CEO,这些都必须要归类出来。然后对这些人分别进行定性研究。“你不能拿着你的社交APP去问一个还不回走路的小孩他的感觉是什么样的。” 也就是说你要先定位你要分析的人群, 然后再给这些人群归类,通过提取共性,抽象出人物角色。再进行分析。

使用场景:这是一个非常重要的因素却往往容易被忽视。比如你办公司的宽带是20M的,你不能把你的用户请过来你的办公司,让他体验你的产品顺不顺畅,而是应该走到你的用户所在的环境中去对用户进行调研。还有一些土豪公司,在自己的办公楼搞了一个XX产品用户体验式,里面是豪华的设备装修,8G16核的机器,然后邀请一批用户到你这里,给他们冲上一杯香浓的咖啡,然后让他们来体验产品,这种做法是跟用户的真正使用场景完全脱节的,这时候用户给你的反馈,一点用处都没有。

另外必须要注意的是,用户真正的需求,往往不是他们说出来的,而是通过观察感受出来的。人往往会有虚伪的一面,你问他,直接的反馈不一定是真实的反馈。(比如你要做一个男性成人用品的产品,需要知道用户的JJ长度分布情况,然后你调查的结果肯定会比大多数人实际长度长一些,无法做到客观。)

定量分析:

根据数据来做定量分析的结果,一般是比较客观的(数据足够多的时候)。但是数据只是反馈了客观的数据,你不能加上主观的猜测然后刚好符合数据的表现就说明是这样的。比如,你一个功能做上去好久发现没人用,然后数据分析的结果证实了这一点,如果这时候你说这个功能没用是不客观的,你要看这个功能有没有入口,然后在用户的使用场景下,找到这个入口困不困难等等。数据是客观的,但不能为了‘佐证’你某个观点而滥用数据。

数据分析一般是用来验证你产品新功能的决策正不正确。比如你通过ABtest发现新功能的用户活跃度和消费度更高了,说明这是一个好的功能可以上。反之,应该继续改进产品。

数据挖掘不同于数据分析,他问问可以挖出用户潜在的需求,这是用于帮助产品新功能或者说新产品的决策的。比如美国著名的视频网站netflix就是通过大数据分析他们的用户平时都喜欢看什么类型的电视剧,最后发现用户喜欢看剧情片,所以出钱投资拍摄了美剧《纸牌屋》,现在已经第二季了。当然数据挖掘的难度比数据分析更大,更有创造性。

题外话:netflix我用过他们的开源zookeeper客户端curator, 确实把zookeeper那些shi一样的接口包装得容易用了许多。(不过还是有少许bug, session expired后有些时候没有自动重连)如果你要用到zookeeper,可以试试他们的框架。