理解lisp中的cons

列表是Lisp的核心数据结构及语法。在Lisp里面,列表的存储方式比较特别,一个列表通常由1个或N个cons来组成的。那个list又与cons是什么关系呢?搞清楚之前,我混乱了许久,读过很多文档及实作才豁然开朗,概要记录一下,看似内容不多,但要理解的东西多了去,整理出来的文字比起之前做的笔记要少多了 :)

关于Cons

cons是一种简单的数据结构,一个cons是由头尾两个元素构成:如下创建一个cons对象:
> (setf x (cons 1 2) )
> x
(1 . 2 )
这创建了一个头是1,尾是2的cons对象。
对cons的操作有两个,car及cdr,分别是读取cons的头和尾元素:
> (car x)
1
> (cdr x)
2

cons的元素可以是任意类型的,当元素也是cons时:
> (setf y (cons 1 (cons 2 (cons 3 4))))
> y
(1 2 3 . 4)
y值可以这样理解:它由三个(点之前的元素个数)cons组成,最后一个元素是4
> (car y)
1
> (cdr y)
(2 3 . 4)

Cons与List

我们再来创建一个特别的cons:
> (setf z (cons 1 (cons 2 (cons 3 (cons 4 nil)))))
> z
(1 2 3 4)
z值可以这样理解:它由4个cons组成,最后一个元素是nil 
z也等价于:
> (list 1 2 3 4)

1、像y这样的列表,我们称之为点列表,其特征是cons的最后元素是一个原子,非nil,非空列表。
2、而像z这样的列表,我们称之为真列表(list) ,其特重是cons的最后元素是nil,即空列表。真列表打印输出时,会隐藏掉最后的nil值及点。
3、所以,list是一种特殊的cons,即真列表是一种特殊的点列表。
4、真列表(list)是可以使用append函数的,而点列表不行:
> (append z 5)
(1 2 3 4 5)
> (append y 5)
*** Eval error ***  Wrong type argument: listp, 4
错误指出,4(y中的最后一个元素) 不是一个list,所以不能append。
真列表(list) 最后的元素是nil,即空列表,所以是可以加进新元素。

说起来简单,实际上要真理解的确是得费点工夫和心思。

Cons与其他数据结构

cons除了可以表示list,还可以表示其他复杂的数据结构,如树,栈等,这就留待后面再做功课了。

Posted in 技术 | Tagged , , , | Leave a comment

Emacs打开python文件时加载ropemacs

用Emacs作为日常的编辑器有些日子了,各种爽,只是之前的配置会让Emacs在启动的时候自动加载ropemacs,这个东西只有在编辑python文件时才有用,启动时加载会让Emacs的整个启动速度变慢。于是对此作了一下微调,使用eval-after-load宏,在python-mode完成加载后(即打开一个python文件后)才开始加载ropemacs。

我的init.el里面,关于python的配置修改如下:

BTW:因为感冒才有借口不务正业一番,又重温了一下lisp。lisp太美了,决定以后就拿学习lisp当休闲,虽然短时间内工作中用不上lisp,但我已经相信熟悉lisp能让自己的编程水平更上一层楼,至少在视野上。

Posted in 技术 | Tagged , | 2 Comments

我的2011 — 创业前记

由于苦逼地埋头苦干的缘故,我已经快有半年时间没有更新博客,这些时间里,我欠下的文字债累累:
- 参加了gurudigger第一季的城市越野活动,欠下Mike一篇总结文字。
- 我升级当老爸了,我没有为蓝蓝妈妈或者是蓝蓝写下半片博文。
- 离开了工作近七年的Botwave,我竟然连一个字儿的离职信都没有。
- 创业了,关于产品,关于技术,关于团队,心中每日都有千百种感想,却始终只是闷声加速。

最近思考过多过快,没有停下来整理思路,沟通的时候嘴巴往往跟不上头脑的速度了。善哉。

萝卜闷在坑里会烂,话儿闷在心里会淡。今天晚上以“家属”的身份参加了Botwave的年会,席上和旧同事聊得很有感触,趁有感觉,顺便把今年的事儿都马克一下,不然放着放着就会忘记了。同时,希望这篇文字可以一次性把欠下的债还了。@peter @treeinthewall @onedear @nino @kevin @steven @blue @ken @sven @rocket,
@mike @cngump @vera @anson @linluxiang @michael @forever @bill @bigbigtooth,由于涉及范围较多并且发散,大家就挑感兴趣的来看吧。原谅我的婆妈吧。

Botwave,外星人和他的朋友们

博汇(botwave)是我迄今为止,我除了自己的家以外,所呆的最长的地方了,我在这里生活了7年的时间,从一个懵懂的程序员成长至产品研发总监,从一个小屁孩升级为人夫,继而为人父。这里就是我第二个家。我偶尔还暗暗为自己7年前的选择庆幸,我没有受邀去某上市公司当钉子,而是留在了一家不起眼的创业小公司,饱经磨练,修来周身刀,现在自己创业大派用场。

botwave是一片自由的土壤,在这里,只有你认为自己有能力把控好一样技术,你就可以把它引入进来,我在第2年就获得为公司最大的项目设计架构的机会、以及在以后的时间里影响整个核心技术部的技术格局。在产品方面同样,有好的想法和执行能力就可以放手去干,试手机网的诞生与落幕让我获益良多,从此与产品结下不解之缘。

botwave是一个有爱的家:Boss们绝对亲民随和,peter混在员工里面,很难认出是CEO,扫地老僧Bruise把技术人的幽默感在技术上挥洒得淋漓尽致,长发披肩的jas可以男扮女装和我走catwalk;你可以终年拖鞋短裤地上班,只要你能完成任务,睡到中午12点来上班也没问题。最重要的是,这是一家正直的公司,引用Peter一句话:我们要站着把钱赚了。

2010年我的创业计划流产后,继续留在公司,带领外星人团队(EasyTrace团队,简称ET)进行研发工作。我给团队的目标是快速响应市场需求,推进研发的过程、每个队员全面发展(包括技术以外的能力,如沟通能力、产品感觉)并术业有专攻。这一年多以来,ET团队阵容空前稳定、每个人都在团队中找到了自己的位置并独当一面,产品也顺利地往前推进。在这个时候,我知道ET更多需要的是来自市场扑面而来的动力和紧迫感,我也到了离开、为自己的梦想去奋斗一把的时候了。

在Botwave很快乐,我享受技术难题带来的挑战,享受产品失败给我带来的痛的体验,享受和你们在一起的晨会、推心置腹的交谈、面红耳赤的争论。我为团队成员的成长而感到愉悦,我更为曾经是一名botwaver而光荣。我爱你,botwave。

一起,爱朋四友和图睿和下一个产品

2011年初,作为珠三角技术沙龙的哎呀主席,我在主办过N次的线下沙龙之后,发现了一个相对小众,但是又比较强烈的需求:基于活动的社交服务。于是某一天,我快速地在evernote上写下几行文字,发给了anson,老甘,小林,没想到大家都对此非常兴奋并一拍即合。在2011年3月份的某一天,爱朋四友(iapp4you)在维多利星巴克进行了第一次正式会晤,然后便马不停蹄地展开了产品的开发。对了,这个产品起初名叫eventking。

如果你用webQQ,现在你应该可以在应用排行榜前100位找到一个叫点歌台的应用,该应用由上线之日起,用户数就一路飙升,最终超越豆瓣电台、音悦台、虾米电台等大牌应用,稳稳当当地挤进应用排行前100。它出自一个名叫图睿的团队。团队主要有三个核心成员:michael, forever, bill.一个产品达人,一个开放平台的周伯通,一个一流的UI设计师。

在2011年的3月至7月,爱朋四友利用大部份的业余时间做出了eventking的第一个版本,在正式登录appstore之时,eventking改名为一起,与此同时,因缘际会地,我们接触到了图睿团队,她与四友的阵容互补,加上其leader michael与我较早之前已认识并惺惺相惜已久,遂在anson的牵线之下两个团队正式合并,团队仍叫图睿。一起在发布第二个版本时参加了36kr深圳开放日的演示,然后,一切就开始了加速。那时,两款同类型的产品也面世了,它们是幸会和在这儿IM。很巧的是,他们的创始人也一样是高频率的活动主办者。

感谢36kr和Beta Friends为我们打开了与移动互联网业界的投资人、创业者们沟通的门,开放日回来后,我们从很多投资人、成功的创业者们那里得到了很多重要的反馈意见以及批评,我们更加深刻地体会到了做产品的看似简单的几个重要法则:简单专注、直指需求痛处、易用,并尝试着在一起上面应用这些法则。

2011年10月24日,老甘、forever和我作为先头部队,率先全职创业。小林由于有肉身翻墙要务在身暂时不能掺和,anson这时也拥有了自己的产品团队,推出了几个地市的实时公交查询app。而其他人则会在稍后的时间全职加入。

全职部队对一起做了一次转型,由活动社交转向话题社交——我们较早地遇到了线下活动增长缓慢的问题,用户更喜欢把活动当作一个话题来玩。幸会和在这儿也同样遇到了相同的问题,这也体现在他们的产品形态上:都纷纷加上话题或发贴功能,以至于产品的定位进一步扩散。尽管这两款应用最近的版本都看起来不错,我仍然建议他们要克制住自己,大胆砍掉多余的功能,让产品更简单。

一起的转型有我们自己的心中并不及格,所以2.0版本出来了,我们也没有去推广。但一起的历史任务已经完成了,让创业团队走到一起来,磨合团队,给团队结结实实地上了一堂创业学前课。最终老甘选择了离开,成立自己的ios外包工作室,图睿团队则继续坚毅地前行,展开了新产品的征程。

团队是创业的根本。从年初到年末,一直在调整的是团队,有更多合拍的新伙伴慢慢走到一起,也有老拍档因为现实和自己的梦想而离开。现在的团队在价值观和追求上保持一致,并且在合作过程中都不由自主地为团队大局着想。我很开心,也有信心大家可以坚持着走很远很远。

创业是一种快乐的痛苦,显而易见的,除了吃喝拉撤和逗女儿玩的时间,都用在了工作上,但做着自己喜欢的事儿,是快乐的,无法形容的。

我们的新产品已经在密锣紧鼓地开发当中了,希望它可以成为您生活中的好帮手,敬请期待。伙伴们加油!

你曾经拥有一个英雄的梦想

我和老甘在07年的一场python会课上认识,老甘的招牌微笑让我感到亲切(相信很多在社区的朋友有同感),所以原本有社交障碍的我因此放下忐忑的心情融入了社区,并在随后的珠三角技术沙龙上共同出力至今。到今天,我们也可谓是老朋友了。

老甘曾是爱朋四友和图睿的团队成员,一起的大部份客户端的开发工作由他包办了。在兼职创业期间,我们风雨不改地在星期四晚上和周末外出coding和喝咖啡,我们合作得很有默契,在上海的Gurudigger第一季城市越野赛上,凭我们默契的合作,在严重落后的情况下逆转比赛。 我们都曾共同地认为,以后出来全职创业,一定要有对方。后来我们都因为对方在而果断离职创业。

也许当一起坐到办公室为产品奋斗的时候,才使得大家的思考加快,包括思考自己想要什么。一天,与我合作最长时间的伙伴老甘说出了他的真实的梦想,就是成立甘果ios专业外包工作室,亲身去体验外包行业的一切的一切。

有梦想的人是伟大的。尽管老甘的离去于我犹如失去右臂,我作为朋友,会衷心地祝福你。同时,我也坚信现在的团队将会是我坚强有力的后盾。

我们都因为对方而离职创业,以后也因为对方而各自努力,希望我们的梦想都可以在前方为我们指路。

创业的路上很多不测风云,产品问题,团队问题,早晚会摊上,只是我比较早摊上罢了,摊上了,就快速调整吧。

祝福所有在路上的创业者。

蓝蓝

我的女儿、这个世界上第四个对我最重要的女人蓝蓝在10月28号呱呱坠地,我正式升级为奶爸。快乐在不言中。

谢谢我的娘子,你把最可爱的蓝蓝带到了世界上。
谢谢我的丈母娘,从娘子怀孕到现在,全赖有您无微不至的照料。
谢谢我的妈妈,在家里照顾好爷爷奶奶。
全因为有你们,我才可以有足够充裕时间为创业作准备,才能够安心地全身而出。我爱你们。

Posted in 感悟, 技术, 生活 | 7 Comments

移动应用开发的终极武器

不,我并没有在讲HTML5,也不是讲PhoneGap这类号称跨平台的FrameWork。我讲的是我的一些iDea和YC的一个即将发布的产品Parse。

我在做iOS应用的时候,前至手机端的一个像素,后至服务端的某条推送服务的守护进程,我统统都结结实实地打过交道。作为一个自命有追求的研发工作者,我总结出,在ios应用开发的很多个环节可以抽象出来进行重用,并且再进一步包装的话,可以做成面向开发者的第三方服务,如下面我提出的三个:

推送服务

在服务器端搭建和维护一个稳定靠谱的推送服务不简单。当我完成了我的产品的推送服务时,我马上就想到可以做一个叫“代客推送”的生意。但是有更聪明的人想到了,并把生意做了。

应用内聊天(in-app chat)

现在好像不管什么应用,都指望给自己的应用加上一个能让用户点对点的发消息功能,有消息功能后,又想要及时一点的聊天。嗯,这又是一个可以做的生意呀。如果有一个服务给你提供一份SDK,可以三行代码实现用户应用内实时聊天,外带推送,200个使用用户内免费,你会考虑吗?

用户及关系中心(SocialCenter)

这样说吧,你有一个应用A,允许用户绑定微博,twitter,facebook的帐号。然后有下面的场景:

  1. 用户在一台终端绑定三种帐号。
  2. 用户在另一台终端再次登录,无需重新绑定即可直接访问三种帐号的资源。

好了,作为开发者的你,又开发了应用B,与应用A一样有着上面一样的需求,你可以把应用A里实现的那套代码(包括前端和服务器)照抄一遍,也可以继续思考:

  1. 针对于绑定用户这个功能而言,应用A和应用B的服务端Host为同一个,会怎样呢?那就只需要维护一套代码咯,但这还不是全部。
  2. 对于用户来讲,更神奇的事情发生了,我在A应用绑定过三个帐号后,在B应用使用微博帐号登录,居然那三个帐号的绑定在B应用里也生效了,这说明什么?用户在所有的应用里有可能不断重复绑定社交网络的动作,其实可以简化为绑定一次!

其他的就不多讲了,它是一个类似about.me和gameCenter结合的东西,我认为基于它之上可以挖掘的东西太多了。

歇一歇

以上三个东西是我一直嚷着要做的第三方服务,原先我的想法是继续多做几个应用,经过几个应用的沉淀,它们自然就会作为副产品沉淀出来。其实这三个想法都有一个一致的理念:为开发者提供更方便的基础服务,降低开发成本(虽然第三个看起来理想更远大)。我深信这个方向是可以产生价值的。一直到今天,我看到了YC的一个未发布的产品Parse,我更加坚定了我的想法。

Parse

Parse是一个完整的 iOS,android 后端支持平台,它可以让开发者完成忘掉服务器端的事情(parse透明地为你提供服务端的支持),全情投入在客户端的开发上面。还有人把它类比成手机开发中的Rails。上面我提到的三点想法里面,Parse提供了其中两点:

  1. 推送服务
  2. 用户、社交网络连接(含twitter,facebook)

此外,Parse还提供了本地数据与服务端数据同步的服务,开发者只需要对本地的数据进行操作就行,多舒服啊。

有这样好的SDK和服务提供给你时,做一个iOS或andoird应用变得更加容易了。如今这样的第三方SDK和服务越来越多,原来在Web2.0里面出现的第三方服务,如评论,用户反馈托管(如userVoice),表单等己经开始全面移植到移动互联网的世界。可以想像,未来的移动应用也可以简单的MashUp出来。

现在你知道为什么我说的终极武器并不是哪一门子的技术了,丰富而强大的基础服务才是。

关于我的idea们

在没有成熟的类似的第三方服务出来之前,我会选择在以后的应用里面(包括“一起”)继续打磨这些基础套件,如果打磨得好用,我再考虑作为独立产品发布出来。如果有朋友现在就感兴趣和有时间,那请你们赶紧做吧,我一定会成为你的客户。

Posted in 技术 | Tagged , , | 9 Comments

使用VitrualEnvWrapper隔离python项目的库依赖

是什么

VirtualEnv用于在一台机器上创建多个独立的python运行环境,VirtualEnvWrapper为前者提供了一些便利的命令行上的封装。

为什么要用

- 隔离项目之间的第三方包依赖,如A项目依赖django1.2.5,B项目依赖django1.3。
- 为部署应用提供方便,把开发环境的虚拟环境打包到生产环境即可,不需要在服务器上再折腾一翻。

怎么用

安装

- pip install virtualenvwrapper
- 把下面这句加到~/.bash_profile里面,如不嫌麻烦,也可以每次都手动执行。
source /usr/local/bin/virtualenvwrapper.sh

常用命令

创新的虚拟环境
- mkvirtualenv [env1]
该命令会帮我们创建一个新环境,默认情况下,环境的目录是.virtualenv/en1,创建过程中它会自动帮我们安装pip,以后我们要安装新依赖时可直接使用pip命令。
创建完之后,自动切换到该环境下工作,可看到提示符变为:
(env1)$
在这个环境下安装的依赖不会影响到其他的环境
- lssitepackages 显示该环境中所安装的包

切换环境
- workon [env]
随时使用“workon 环境名”可以进行环境切换,如果不带环境名参数,则显示当前使用的环境
- deactivate
在某个环境中使用,切换到系统的python环境

其他命令
- showvirtualenv [env] 显示指定环境的详情。
- rmvirtualenv [env] 移除指定的虚拟环境,移除的前提是当前没有在该环境中工作。如在该环境工作,先使用deactivate退出。
- cpvirtualenv [source] [dest] 复制一份虚拟环境。
- cdvirtualenv [subdir] 把当前工作目录设置为所在的环境目录。
- cdsitepackages [subdir] 把当前工作目录设置为所在环境的sitepackages路径。
- add2virtualenv [dir] [dir] 把指定的目录加入当前使用的环境的path中,这常使用于在多个project里面同时使用一个较大的库的情况。
- toggleglobalsitepackages -q 控制当前的环境是否使用全局的sitepackages目录。

Posted in 技术 | Tagged , | 2 Comments

用Redis实现分布式锁

Redis有一系列的命令,特点是以NX结尾,NX是Not eXists的缩写,如SETNX命令就应该理解为:SET if Not eXists。这系列的命令非常有用,这里讲使用SETNX来实现分布式锁。

用SETNX实现分布式锁

利用SETNX非常简单地实现分布式锁。例如:某客户端要获得一个名字foo的锁,客户端使用下面的命令进行获取:

SETNX lock.foo <current Unix time + lock timeout + 1>

  •  如返回1,则该客户端获得锁,把lock.foo的键值设置为时间值表示该键已被锁定,该客户端最后可以通过DEL lock.foo来释放该锁。
  •  如返回0,表明该锁已被其他客户端取得,这时我们可以先返回或进行重试等对方完成或等待锁超时。

解决死锁

上面的锁定逻辑有一个问题:如果一个持有锁的客户端失败或崩溃了不能释放锁,该怎么解决?我们可以通过锁的键对应的时间戳来判断这种情况是否发生了,如果当前的时间已经大于lock.foo的值,说明该锁已失效,可以被重新使用。

发生这种情况时,可不能简单的通过DEL来删除锁,然后再SETNX一次,当多个客户端检测到锁超时后都会尝试去释放它,这里就可能出现一个竞态条件,让我们模拟一下这个场景:

  1.  C0操作超时了,但它还持有着锁,C1和C2读取lock.foo检查时间戳,先后发现超时了。
  2.  C1 发送DEL lock.foo
  3.  C1 发送SETNX lock.foo 并且成功了。
  4.  C2 发送DEL lock.foo
  5.  C2 发送SETNX lock.foo 并且成功了。

这样一来,C1,C2都拿到了锁!问题大了!

幸好这种问题是可以避免D,让我们来看看C3这个客户端是怎样做的:

  1. C3发送SETNX lock.foo 想要获得锁,由于C0还持有锁,所以Redis返回给C3一个0
  2. C3发送GET lock.foo 以检查锁是否超时了,如果没超时,则等待或重试。
  3. 反之,如果已超时,C3通过下面的操作来尝试获得锁:
    GETSET lock.foo <current Unix time + lock timeout + 1>
  4. 通过GETSET,C3拿到的时间戳如果仍然是超时的,那就说明,C3如愿以偿拿到锁了。
  5. 如果在C3之前,有个叫C4的客户端比C3快一步执行了上面的操作,那么C3拿到的时间戳是个未超时的值,这时,C3没有如期获得锁,需要再次等待或重试。留意一下,尽管C3没拿到锁,但它改写了C4设置的锁的超时值,不过这一点非常微小的误差带来的影响可以忽略不计。

注意:为了让分布式锁的算法更稳键些,持有锁的客户端在解锁之前应该再检查一次自己的锁是否已经超时,再去做DEL操作,因为可能客户端因为某个耗时的操作而挂起,操作完的时候锁因为超时已经被别人获得,这时就不必解锁了。

示例伪代码

根据上面的代码,我写了一小段Fake代码来描述使用分布式锁的全过程:

  1. # get lock
  2. lock = 0
  3. while lock != 1:
  4.     timestamp = current Unix time + lock timeout + 1
  5.     lock = SETNX lock.foo timestamp
  6.     if lock == 1 or (now() > (GET lock.foo) and now() > (GETSET lock.foo timestamp)):
  7.         break;
  8.     else:
  9.         sleep(10ms)
  10.  
  11. # do your job
  12. do_job()
  13.  
  14. # release
  15. if now() < GET lock.foo:
  16.     DEL lock.foo

是的,要想这段逻辑可以重用,使用python的你马上就想到了Decorator,而用Java的你是不是也想到了那谁?AOP + annotation?行,怎样舒服怎样用吧,别重复代码就行。

Posted in 技术 | Tagged , , , | 12 Comments

Redis点滴

最近试验在产品中使用Redis来完成以前MongoDB做的一些工作,发现在大量消息采集的场景下(咱们这次不谈查询什么的),redis比mongoDB表现更好──这里主要是指编程更简便、逻辑更清晰。下面我举一些小例子说说Redis都为我们解决了什么问题,技术上下文关键字:高并发、分布式。

插入与更新操作的无差别性

Redis的所有SET(包括MSET,HMSET)操作都是:存在则更新,不存在则插入,即insert if not exists。所以在编程的时候开发人员不需要关心所做的操作属于更新还是插入,减免了判断,因此也避免了判断操作可能带来的锁定。

MongoDB也有同样的操作,update操作的upsert参数调为True即可,不过经过测试,MongoDB为查询条件为了索引后使用update with upsert来代替insert操作,效率比光insert要低5倍以上,而redis的HMSET操作的效率要胜出。

GETSET的妙用

上一个经验虽说可以解决这条数据该“插入还是更新”的问题,但需要知道当前操作是否针对某数据的首次操作的需求还不少。例如我的程序会在不同时间接收到同一条消息的不同分片信息包,我需要在收到该消息的首个信息包(发送是无序的)时做些特殊处理。

早些时候的做法是为消息在MongoDB维护一个状态对象,有信息包来的时候就走“上锁->检查消息状态->根据状态决定做不做特殊操作->解锁” 这个流程,虽然同事已经把锁的粒度控制得非常细了,但有锁的程序遇上多实例部署就歇了。

Redis的GETSET是解决这个问题的终极武器,只需要把当前信息包的唯一标识对指定的状态属性进行一次GETSET操作,再判断返回值是否为空则知道是否首次操作。GETSET替我们把两次读写的操作封装成了原子操作,V5啊。

山寨版数据过期策略

我曾经想过要写服务器端的脚本来扩展redis,试图要拿到数据过期的事件,用来做一些回调来处理过期数据,但很快我发现这个不现实。于是我选择通过使用排序集合(SORTEDSET)来实现一个山寨的数据过期策略:需要定时过期的数据,统一添加到一个排序集合:ZADD expiringKey timestamp data。在这里我使用了时间值(毫秒为单位的长整型)作为数据的分数,那么很自然的,早期的数据总会排在集合前面;然后我写一个程序会定时地过来打理这些过期的数据就好了。

存储结构化数据

例如有“通讯录”这样的数据,包含有”name”,”city”,”gender”等8个属性,使用mongoDB保存就很简单,创建一个Document,设置属性后存储即可,而Redis本身并非Document型的DB而是Key Value DB,要存储这种数据,还得在Key上面花一点功夫:使用contact:id:name,contact:id:city,contact:id:gender之类的Key来存储其对应的值。当然,这只是使用redis存储结构化数据最原始的办法,更建议的办法是使用Hash存储,如 hmset contact:id name jeff contact xx@gmail.com gender male。相对set操作而言,hmset既节省了存储空间又提高了存储效率。

使用MongoDB来存储这些数据是小菜一碟,但鉴于第一点经验,我还是愿意使用Redis。

比较可惜的是,目前Redis的Hash存储仅支持字符类型的值,不支持其他数据结构,我非常期待它日后会支持其他数据结构,甚至支持Hash的嵌套。关于这点,@wuvist 同学认为十分有可能。

小结

上面这些Case都只是Redis牛刀小用,但实际上给程序带来的便利是非常明显的,最明显的就是可以把原来的程序上使用的锁都抛弃掉,甚至直接支持分布式运行和水平扩展了。

顺便在此小结一点高并发分布式应用程序编写的一些推荐的注意事项吧,当然这是我的个人偏好并结合了一些特定业务领域的性质:

1. 程序对资源最好是只读或只写,明确分工。不要在一个程序里同时对资源进行读写,除非是原子操作,如GETSET。
2. 写操作中,插入与更新最好是无差别的,避免程序对此进行行判断,破坏操作的原子性。
3. 更新过程中尽最不要对更新值和原值进行比较,还是关乎操作的原子性,如果真要进行比较,有两种方案供参考。
1). 更新时,为字段追加新数据,使用集合(如果是数值使用排序集合更好)来存储;比较的逻辑交给读取的程序处理。
2). 使用CAS,类似乐观锁,实现多进程数据安全控制。如果目标资源的服务器支持最佳。
4. 还是那一句,避免在程序里面使用锁。逼不得已就用分布式锁吧。
5. 多线程是万恶之源,要慎用,一条线程能把CPU跑满才是真牛,多核、扩容时可考虑多进程。

Posted in 技术 | Tagged , , , | 3 Comments

Autoforms使用指南

Autoforms是Jeff开源的一个基于Django的自定义表单引擎,可用于调查、投票、信息收集甚至是整合至工作流程引擎和PAAS平台当中。目前AutoForms主要用于支持珠三角地区几个技术社区的线下活动报名、反馈收集等。jeff在本站上也搭建了一个autoforms的实例,可以免费为大家提供简单的在线表单服务,有兴趣者,可联系我开通帐号。最近对AutoForms的使用反馈越来越多,所以更新会相对频繁一些,嗯,有人使用就有动力呀!

AutoForms(今天下午发布最新版本为0.4)的主要特性:

  • 支持14种字段类型13种html输入控件(包括日期选择组件),可以快速定制大部份常见的表单。
  • 保存用户提交的表单数据,并向表单作者展示,同时提供数据导出功能。
  • 支持表单继承,需要周期性地使用相同的表单时,一点也不费力。
  • 用户填写表单后,邮件通知表单作者。
  • 提供了丰富的API,容易与其他应用程序作整合。

关于AutoForms的安装请参考github上面的说明,本文主要的内容是指导表单用户如何使用Autoforms。

登录

首先登录至管理界面(http://f.jeffkit.info/admin/ 帐号为 form/form),会看到Autoforms的应用,该应用有两个模型可供管理:

  1. 表单管理提供快速创建、修改表单;预览表单,查看表单数据以及生成某入式表单代表等功能。
  2. 表单域管理则提供对某些表单域的高级设置功能。

创建表单

下面我通过创建一个“AutoForm用户反馈表单”为例,示范一下如何使用AutoForms来创建表单。

首先,我们点击表单管理的增加按钮开始,打开增加表单的页面,表单上半部份是表单的基本信息,下半部分是定义该表单所包括的字段(表单域)。下图是填写AutoForm用户反馈表单的示例数据:

补充说明一下上面页面一些容易迷惑的地方:

父表单:如果指定某个表单为父表单,那么所创建的表单将自动获得父表单的所有字段。

启用:如果处于勾选状态,则表单可供填写,否则表单只能查看而不能提交。

表单域里的组件:组件是指在页面上展示表单时使用的HTML控件,如你的字段是密码类的,你最好选择“密码输入框”组件。一般情况下,使用默认的组件已经足够。

接下来,点击”保存并继续编辑”按钮,这时在页面的面上角出现了一些快捷按钮:

  1. 预览,让您预览所创建的表单的展示效果。
  2. 数据,用户通过表单提交数据后,表单作者可以在此看到所有提交的数据并可将数据导出为CSV格式。
  3. 嵌入,autoforms允许用户通过嵌入页面的方式展现在第三方网站上面,点击该按钮可获得嵌入的HTML代码。
  4. 历史,Django的内置的数据为更记录。
  5. 在网站上查看,点击后打开填写表单页面。

预览表单

我们先点击预览按钮,看看刚才创建的表单的效果如何!结果有点意外,“评分”和“在哪了解到autoforms”两个字段光秃秃的!

其实并不意外,我们在表单是创建了两个选项/多选值类型的字段,而我们还没有为这两个字段提供选项数据,所以它们看上去是光秃秃的,那么下一步,我们就是要为选项类型的字段加上选项数据。

编辑字段

返回管理首页,点击表单域进入表单域列表页面,在这里可以找到刚在才创建表单时生成的几个字段,点击“在哪了解到autoforms”进入该字段的编辑页面,页面由一个展开区域和(字段基础信息),三个隐藏区域(高级设置、选项、错误信息)组成。高级设置暂略过不理,选项区就是我们要为选项类型的字段提供数据的地方,而错误信息则允许我们自定议错误提示内容。

下面我要为“在哪了解到autoforms”字段提供一些选项数据,如下图这般添加一些数据:

保存字段的修改,再依法泡制一下“评分”字段,一个表单就完成了。最终结果就是下面的表单了,各位使用过autoforms的同学,来这里给我一些建议吧。

收集数据

表单定义好了,是时候发出去给大家填写了,收集上一段时间后,可以通过“数据”页面来查看已经收集好的数据,如果你愿意,可以点击右上角的导出按钮,以CVS的格式导出数据:

好了,AutoForms的基本使用就这么一些,如果你是开发人员,对API感兴趣,欢迎阅读代码,如果你有些空闲时间,希望掺和一些开源项目,AutoForms也欢迎你加入到开发者行列。

Posted in 技术 | Tagged , , , | 7 Comments

初品RabbitMQ

《taste rabbitmq》是我在珠三角技术沙龙4月份深圳场分享的一个主题,由于时间紧迫,我在赶往深圳的和谐号上还努力跑着测试程序,但数据始终不好看,直至昨晚,才有时间把测试重新进一行遍,更新了一下slide,现在发布出来。废话不说,大家直接看slide和代码。录音稍后奉上。

我还没有正式记录持久化模式的性能测试,因为该模式下的数据极其不稳定,但与非持久化模式对比起来:
1、持久模式的入队列速度会稍慢,大概可能有10%左右的下降(当然消息体积大小也会有影响)。虽然rabbitmq是异步处理持久化事宜的。
2、持久模式的出队列速度明显慢很多。如果是完全从硬盘消费消息,那速度更是让人抓狂,尽管这种情况只会在重启服务器时才发生。
3、持久模式,rabbitmq的服务进程长期活跃着,占用着不少CPU资源。
如果你使用持久模式,rabbitmq会很负责任地保证你的数据安全,如果你的应用对消息消费者的性能要求不高,可以考虑使用。

最后模拟一次实际使用的场景:消费者一直处于待消费状态,生产者往队列里喂消息,结果是生产者消息入队列的速度与单测入队速度没明显差异,而消费者则有稍变慢,rabbitmq是尽量地让消息生产者始终保持高效啊。

我的测试代码放在了github上面。README有测试方法和参数说明,感兴趣的同学可以玩玩。

Posted in 技术 | Tagged , | 6 Comments

解一道SQL问题:找出成绩优秀的学生

今天中午,珠三角技术沙龙官方群(103903642)的Crazy同学给大家出了一道SQL的题目,据说来自某个微群:

“SQL开发的一道小问题,一个学校的老师需要评选一组学生作为优秀学生,条件为最多只有2个科目在80-85之间,其他科目在85分以上或者所有科目成绩都在85分以上,表中包含StuId,SubjectId,Score,求最简单且效率最高的语句。 ”

我有点无聊加手欠,就试着解了一下,建立测试用数据表及数据如下,一共有5位同学,4个科目,共20条数据。我的测试环境是老式macbook 402,4G内存,mysql 5.1.42,MyISAM引擎。

我们首先把非优秀学生的条件重新整理一下:

1、凡有一科分数低于80分的,都不能称为优秀学生
2、凡有两科以上分数在80至85分之间的,也不能称为优秀学生
这两个条件是或的关系。

同时整理一下优秀学生的条件:

1、称得上优秀学生的,必须至少所有科目都在80以上。
2、称得上优秀学生的,分数在80至85分的科目必须小于2科。
这两个条件是与的关系。

先找出非优秀学生还是先找出优秀学生,是两种不同的解题思路,下面我分别从这两种思路出发解决问题:

思路一:先找出非优秀的学生,反过来得到优秀的学生

1、首先找出有个别科目分数低于80分的学生
select StuID,count(*) from tb_score where Score < 80 group by StuID

2、然后找出有科目分数在80至85分超过两科的学生
select StuID,count(*) from tb_score where Score between 80 and 85 group by StuID
having count(*) >2

而未出现在结果集1及2中的学生,则可视为是优秀学生,把上面两句整合到一起,最后得到一句完整的SQL:

select distinct(StuID) from tb_score where StuId not in
(select StuID from tb_score where Score < 80 group by StuID
union
select StuID from tb_score where Score between 80 and 85 group by StuID having Count(score) > 2)

执行结果得出优秀学生为:vera、Selina。

效率测试结果:反复执行10次,最快2.1ms,最慢3.7ms,平均执行时间2.42ms, explain 一下,该SQL对表进行了3次查询,每次都是全表扫描:

实际上,这条SQL语句可以稍优化一下,先distinct出StuID的结果集,把该结果集去与子查询的结果进行not in操作,会比把所有包括重复的StuID直接进行not in操作效率高不少。对原SQL做了一下调整:

select dst.StuID from (select distinct (StuID) from tb_score) dst where StuId not in
(select StuID from tb_score where Score < 80 group by StuID
union
select StuID from tb_score where Score between 80 and 85 group by StuID having Count(score) > 2)

再次测试得到相同的结果,效率测试的结果:反复执行10次,最快1.2ms,最慢1.7ms,平均执行时间1.36ms,较原SQL有44%的提升!再explain一下,新语句用1,2两步来代替旧语句的步骤1,实际上多执行了一次查询。但是这两次查询的代价开销加起来比原语句的远远要小。

根据思路一给出结果后,King兄说使用in的效率还是不理想。好吧,我再试一试第二种思路。

思路二:直接找出成绩优秀的学生

即,要先找出全部科目分数均在80分以上的学生
(select StuID,count(score) from tb_score where Score > 80 group by StuID having count(score) = 4

注意,这里HardCode了一个值4,这是所有科目的总数,这也是本方法的一个瑕疵。

然后计算所有学生分数在80-85分之间的科目总数
select StuID,
count(
case
when score between 80 and 85 then score
else NULL
end
) bs from tb_score group by StuID

最后把他们放在一起,由于两者是与的关系,我使用innner join来表达:

select A.StuID from
(select StuID,count(score) from tb_score where Score > 80 group by StuID having count(score) = 4) as A
inner join
(select StuID,
count(
case
when score between 80 and 85 then score
else NULL
end
) bs from tb_score group by StuID) as B
on A.StuID = B.StuID where B.bs <= 2

测试结果与思路一的结果一致,执行效率测试结果:反复执行10次,最快0.8ms,最慢1.0ms,平均执行速度为0.81ms,再次获得40%以上的性能提升。explain结果如下,该语句减少了一次对物理表(tb_score)的查询,是查询性能提升的直接原因。

思路三:再简单点!再高效点!

经caoxg同学提醒,应该可以再减少一次查询得出结果,我回想一下思路二,其实再往前走一步就可以变成只查询一次了。遂花了两分钟再次码出新版SQL:

select stuID from
(select stuID,count(case when score < 80 then score else null end ) as low_count,
count(case when score between 80 and 85 then score else null end ) as median_count
from tb_score group by stuID)tmp
where tmp.low_count = 0 and tmp.median_count <=2

这一次的平均执行效率在0.8ms以下。再explain一下,结果好看多了。

嗯,事情还没完呢。caoxg同学不满足于where从句那里有两个条件,好吧,我说,条件是可以转移的,使用having从句在子查询里过滤掉分数为80分以下的人就可以把tmp.low_count=0这个条件去掉,相当于把条件提前了,不过这样一来会影响主查询语句的查询基数,理论上是可以带来一定程度的提升,下面是修改过后的语句:

select stuID from
(select stuID,count( case when score between 80 and 85 then score else null end) as median_count from tb_score group by stuID
having count(case when score < 80 then score else null end) = 0) tmp
where tmp.median_count <=2

这一次平均执行效率再次获得0.1ms的提升,正如前面说的,主查询语句的查询基数由5变成了3,见explain的结果:

嗯,现在看起来很不错了,如果说这已经是最简单的查询就错了,King胸在群里回复说:@jeff,我在你的语句基础上回复了一下,用一句SQL搞定。

看了King给的版本,是把select从句中的Count再次合并到having从句中,最终变成了只需要select一次,同时,把Case从句换成IF函数,这样语句看起来更短了:

select StuId from tb_score group by StuId
having count(if(score < 80, score, NULL)) = 0
and count(if(score between 80 and 85, score, NULL)) <= 2

执行效率与上面的语句差不多,但精简程度是更进一步了。explain一下,结果只有一次查询了。帅!

目前效率最高的查询语句还是有having从句,having从句在解决复杂问题时非常有用,常与DB打交道的同学一定不陌生的,having即相当于针对聚合函数的where从句啊。

ps. 挺长时间没动手用纯SQL做事了,这次一开始有明显的简单问题复杂化的嫌疑,不给力呀!面壁学习去。

如果还有更简单高效的方法,请一定要提醒俺 :)

Posted in 技术 | Tagged , | 18 Comments