Archive for 一月, 2012

我们的支付宝悬赏项目今天上线了,整理一下开发过程中遇到的问题,以及相应的解决方法。我们线上项目地址:http://yingyong.taobao.com/show.htm?app_id=155001

测试环境的ip限制

支付宝项目大多涉及到money,所以对安全性要求较高。支付宝一般要求第三方提供固定ip的白名单,仅允许白名单ip访问其系统接口(注:测试开发阶段,访问页面接口,也需通过固定ip)。

测试开发阶段,也同样需要固定ip的服务器,并在这些服务器上配置支付宝提供的hosts。我们的测试、开发服务器出口ip不固定,但是可以通过ssh的端口转发、本地代理等方式解决。以下,将称呼该固定ip服务器为代理服务器。

为chrome安装proxy switch,并用putty建立一个到代理服务器的ssh代理通道,指定给proxy switch。在本地开发机上,将hosts配置为淘宝所需。这样就可以访问其页面接口了。下面通过linux curl函数代理解决固定ip访问系统接口的问题。

Linux curl函数代理

绑定帐号之类的页面接口访问,可以通过浏览器代理实现。但是之后的创建悬赏任务等,都是系统接口,在server端执行的,需要设置linux下curl函数的sock代理。原理一样,也是通过ssh代理实现:ssh -TnNv -D local-port -p ssh-proxy-port  user@ssh-proxy-ip # 如果需要后台执行,则ssh -qTfnN 。

之后,在代码里设置代理信息:

curl_setopt($process, CURLOPT_PROXY, ‘127.0.0.1:7070′);
curl_setopt($process, CURLOPT_PROXYTYPE, CURLPROXY_SOCKS5);

ssh代理建立完成后,可以写一些小测试代码,检查代理是否正确。这时,有些情况会没有响应,这时可以换一个目标url试试。比如我之前用baidu测试,就没有响应,但是退出ssh代理时可以看到:

debug1: channel 2: free: direct-tcpip: listening port 7070 for 119.75.218.77 port 80, connect from 127.0.0.1 port 21856, nchannels 3
debug1: channel 3: free: direct-tcpip: listening port 7070 for 119.75.217.56 port 80, connect from 127.0.0.1 port 21857, nchannels 2
debug1: channel 4: free: direct-tcpip: listening port 7070 for 119.75.217.56 port 80, connect from 127.0.0.1 port 21864, nchannels 1

说明还是连接上了,但是baidu没给影响而已。

这样就彻底解决了固定IP的问题,以下正式开发。

绑定帐号

我们的功能需求是,定期给我们的用户打款,人和金额都不定。

由于支付宝接口中,用户都是用支付宝唯一用户号(2088开头,16位纯数字)来标识的,可以通过用户绑定的方式获取其支付宝id号(也有其他途径,比如taobao.user.get)。针对悬赏系列接口而言,仅创建任务者需要通过该接口绑定。其他如收款者,只需要获取到支付宝唯一号即可,不需要强制绑定。

例如我们的用户flykobe登录之后,希望绑定支付宝帐号,则页面会由站内跳转到alipay,flykobe在那里输入支付宝的帐号、密码手工授权,授权成功后,会跳转到return_url指定的我们的站内页面来,提示授权成功,并且该return_url会将绑定的结果(包括支付宝唯一用户号、部分屏蔽的支付宝帐号、绑定key)记录在我们的本地存储里,留待后用。

sns.account.bind参数和返回值解释:

Args:sns_user_id – 第三方站内用户id;sns_user_name – 第三方站内用户名; type – 我使用的是common,因为不清楚buyer、seller、common之后的区别。注意:之后悬赏接口的outer_account_name和outer_account_id就对应这里的sns_user_name和sns_user_id。

Returns:alipay_login_id – 支付宝帐号,可能是邮箱、手机号等;key – 在解除绑定时需要,之后的悬赏接口未见使用;alipay_card_no – 支付宝用户号,16位纯数字,目前以2088开头。

注意:在测试阶段,使用的支付宝帐号是测试帐号,需要请支付宝帮忙生成,否则就会一直报“请求参数不正确。”

注意2:对支付宝返回值进行sign verify的时候,仅需要将支付宝提供的参数作为verify的对象,而return_url等中自带的参数不需要!否则就会verify失败!

创建任务

可以在https://shang.alipay.com/qiugou/index.htm看看支付宝的悬赏任务。我的理解是,由一些商家或者组织,发布任务提供赏金(即冻结这部分钱),一旦用户完成了这些任务,并由商家人工审核通过后,就可以标记用户,并自动完成支付赏金的过程。创建任务接口,顾名思义,就是创建一个新的悬赏任务。

在支付宝提供的悬赏接口中分为页面接口和系统接口。前者实际上是带着第三方提供的url参数跳转到支付宝里,需要人工输入一些密码、验证码等信息来完成的;后者则是可以完全代码后台运行,不需要人工干预的。

创建任务是一个页面接口,解释一下参数和返回值。

Args:outer_task_id – 第三方的任务id,由第三方保证其唯一性;outer_task_freeze_no – 第三方流水号,也由第三方保证唯一性;task_amount – 赏金金额(总额);task_type – 目前测试阶段,由支付宝提供了一个字符串作为我们的任务类型,不清楚之后应该传入什么值;task_title – 标题;task_expired_time – 截止时间;outer_account_name – 绑定的第三方用户名;outer_account_id – 绑定的第三方用户id。未使用增值服务,所以增值字段都置为空了。

Returns:仅多返回了paid_time – 支付时间。其他还返回了outer_task_id、outer_task_freeze_no、task_amount、additional_profit_amount、additional_profit_transfer_no。所以,具体支付的细节,需要在调用该create接口前,第三方自己记录下来。

注意:在测试环境下,验证码都是“8888”!

注意2:线上环境里,任务的创建者必须是与支付宝签约的帐号,否则在进行后续的pay动作时,会报错:PLATFORM_AUTHORITY_ILLEGAL

注意3:线上环境,刚上线时,创建的任务截止时间不要太长、金额不要太大。如果用户有未结束的创建任务,则无法解除绑定。非签约用户可创建任务,但该任务之后无法支付赏金,也无法结束。

打款

alipay.witkey.task.pay.by.platform接口是真正付款的接口,是一个系统接口。可以针对不同的用户,支付不同的金额。需要创建者的sns_user_id或者支付宝id,以及收款用户的支付宝id和sns_user_name,收款用户可以不绑定帐号。支付宝id可以通过taobao.user.get获取(app授权后可用)。

在测试时,开始少传递了return_url和notify_url,则返回ILLEGAL_ARGUMENT错误。

该接口调用时,有以下几点需要注意的:

  1. 请求参数必须使用POST方式传递
  2. 返回分为同步和异步两种:
    1. 同步返回:仅表示支付宝是否成功受理该请求,若成功,第三方可以修改支付为“处理中”的状态;若失败,则代表参数有问题,或者金额不足等,需要自行处理
    2. 异步返回:至notify_url。表示赏金是否支付成功,第三方可修改支付为“已支付”状态。并且,第三方必须打印“success”作为响应,否则支付宝会多次调用notify_url,直至超时。

解释下参数:

Args:outer_task_id – 对应create里的outer_task_id;outer_account_id – 创建者的第三方用户id;alipay_user_id – 创建者的支付宝id;transfer_detail – 字符串,放款明细。记录数不超过1000。 多条记录之间用(*@|$)分割, 记录内部属性之间用(~*@^)分割。 具体内容: 赏金分配流水号~*@^打款金额 ~*@^支付宝用户号~*@^合作网站用户号。其中,赏金分配流水号由第三方自行指定;打款金额以元为单位,精确到分。支付宝用户号是收款人的支付宝id;合作网站用户号是收款人的第三方用户号,如果绑定的话,需要跟绑定时传入的sns_user_id一致。

异步返回给notify_url的数据是分条的。及transfer_detail中若含有N条数据,则会异步返回N次。异步返回成功后,可以在创建者绑定的支付宝帐号的账单明细中看到支付记录。

注意:失败时返回的错误提示码并不是很友好,比如,当传入的收款人不存在时,返回的是ILLEGAL_SIGN。并且如果传入的第三方用户号与支付宝用户号不一致的话,也报ILLEGAL_SIGN。

注意2:传入的notify_url中,不可以有GET参数!比如http://abc.com/ri.php?action=notify,否则支付宝会解析为:“http://abc.com/ri.php?action=notify,charset=utf-8,params={name=sign,”

最后,感谢支付宝合作方的各种帮助,我在开发过程中,提了N多的傻问题,不好意思了 :(

在php代码里写sql句子,我喜欢用sprintf,这样看起来比较清晰,但是今天就遇到了一个隐藏很深的bug。

有这样一段代码:

        public function update_catogery_ad($id, array $info){/*{{{*/
                if (!$id || !$info){
                        return false;
                }

                $sql = null;
                foreach($info as $a){
                        list($type, $key, $val) = $a;
                        switch($type){
                                case 'int':
                                        $val = intval($val);
                                        break;
                                case 'string':
                                        $val = "'"._es($val)."'";
                                        break;
                        }
                        $sql .= sprintf(" `%s` = $val,", _es($key)); #1
                }
                $sql = substr($sql, 0, -1);

                $sql = sprintf("update %s set $sql where id = %d", $this->_get_catogery_ad(), intval($id)); #2
                return $this->db->update($sql);
        }/*}}}*/

它执行的是,根据传入的info数组中的字段,更新某表。在测试环境,运行的很好,但是上线之后,有同事就反映无法更新数据,也不报错。

出问题的info数组是:

$info = array(

‘a’ => ‘bbbb’,

‘c’ => ‘<area target=”_blank” alt=”” href=”http%3A%2F%2Fitry.try8.info%2Ftaobao%2Ftry%2F%3Fcatogery%3D%E4%BC%98%E8%B4%A8%E8%89%AF%E5%93%81″ coords=”0,100,180,170″ shape=”rect”>’,

);

这里字段a可以被正常更新,但是字段c就维持原样。

仔细观察这段数据,可以看到字段c的value中含有%!在sprintf里,它会被当作特殊字符,于是第一条sql句子会报warning:Too few arguments,由于页面上display_errors关闭了,所以没有看到任何提示,这段代码会默默的执行下去,完成整个功能,看起来就好像c字段被忽略掉一样。

于是动手修改第一条sql句子为:$sql .= sprintf(” `%s` = %s,”, _es($key), $val);

再次运行,直接报sql句子为空!这时发现第二条sql句子也有同样的问题。于是将第二条句子修改为:$sql = sprintf(“update %s set %s where id = %d”, $this->_get_catogery_ad(), $sql, intval($id));

这些人为的bug会隐藏的很深!得多加小心,所以,编程习惯很重要!在使用sprintf的时候,如果变量中可能带有特殊字符,则一定要放在arg里,不能直接写入format里。

tokyo cabinet与tyrant一起,组成了一个可用的NoSQL数据库,cabinet是lib,tyrant是server。其性能和应用性毋庸置疑,希望可以学习,同时也对其稳定性持怀疑态度,希望可以看看是我用的不对,还是其源码有可改进的地方。

从tyrant/ttserver.c的main函数入手:

main(){
    ……
    g_serv = ttservnew();
    int rv = proc(dbname, host, port, thnum, tout, dmn, pidpath, kl, logpath,
                  ulogpath, ulim, uas, sid, mhost, mport, rtspath, ropts,
                  skelpath, mulnum, extpath, extpcs, mask);
    ttservdel(g_serv);
    if(extpcs) tclistdel(extpcs);
    return rv;
  }
}

省略的部分是命令行参数处理,ttservnew函数对TTSERV进行初始化,其中初始化了4个锁:
ttservnew(){
    ……
    if(pthread_mutex_init(&serv->qmtx, NULL) != 0) tcmyfatal("pthread_mutex_init failed");
    if(pthread_cond_init(&serv->qcnd, NULL) != 0) tcmyfatal("pthread_cond_init failed");
    if(pthread_mutex_init(&serv->tmtx, NULL) != 0) tcmyfatal("pthread_mutex_init failed");
    if(pthread_cond_init(&serv->tcnd, NULL) != 0) tcmyfatal("pthread_cond_init failed");
    ……
}

typedef struct _TTSERV {
    ……
    pthread_mutex_t qmtx;                  /* mutex for the queue */
    pthread_cond_t qcnd;                   /* condition variable for the queue */
    pthread_mutex_t tmtx;                  /* mutex for the timer */
    pthread_cond_t tcnd;                   /* condition variable for the timer */
    ……
}

这里就牵涉到两个重要的概念,timer线程和worker线程,稍候会介绍。qmtx和qcnd主要是供主线程和worker线程使用,tmtx和tcnd主要供主线程和timer线程使用。

可以看出,真正的处理逻辑是在proc函数里。这个函数里进行了大量的稳定、log、容错等处理,先都忽略(但是这些才保证了一份好代码的存在!)。

proc函数中主要进行了以下工作:

  1. 如果以daemon模式运行,则调用ttdaemonize函数,建立可运行的子子进程,父进程和子进程都退出
  2. tcadbopen函数,根据数据库文件,为TCADB对象赋值,对于hash类型的db而言,会将数据库文件mmap到共享内存中
  3. 调用ttservaddtimedhandler函数,建立一个tserv的timer对象,其handler是do_slave
  4. 如果有外部脚本相关的,则再调用ttservaddtimedhandler函数,建立N个timer对象,其handler是do_extpc
  5. 调用ttservsettaskhandler函数,将tserv的do_task赋值为do_task函数
  6. 调用ttservsettermhandler函数,将tserv的do_term赋值为do_term函数,处理退出
  7. do{ ttservstart(); }while(g_restart); 之类的g_restart值可以有signal改变,从而使ttserver可以再次执行ttservstart,以达到重启、reload的效果
  8. 在ttservstart函数中,是真正的业务逻辑

以下简单看看ttservstart函数:

  1. 根据host和port,建立socket,可以是unix原始套接口,也可以是TCP套接口
  2. 新建timer个数个线程,等待执行timer->do_timer函数
  3. 新建thnum个worker线程,以空的reqs初始化,等待执行timer->do_task函数
  4. epoll_wait:
    1. 如果是请求建立连接,则accept连接,并将accept的socket端口加入到epoll的监听队列里
    2. 如果是其他请求,则请求qmtx锁,将该端口push到serv->queue里,释放qmtx锁,并发送qcnd信号,触发worker线程

worker线程的主体就是do_task函数,其读取socket中的数据,如果以magic word打头,则是二进制格式的命令;否则就是memcached兼容的命令格式。根据请求内容,再分别调用如do_put、do_get等函数进行处理。

以do_get函数为例,主要是调用tcadbget函数,读取到内容,再通过socket返回给客户端。这里的tcadbget函数,会根据数据库的类型,调用其get函数进行读取,比如hash database,就使用tchdbget方法。tchdbget中,首先请求HDBLOCKMETHOD 的read lock(允许其他读,但是不允许写),对整个db加锁,然后调用tchdbbidx函数计算出key对应在hash数组中的位置,和二次hash的key(为了解决hash碰撞,对于第一次hash key相同的元素,使用tree再进行存储)。调用tchdbgetimpl函数,进行真正的获取数据。

tchdbgetimpl函数中:由于hash database使用了on-memory hash database作为缓存,所以,如果cache不为空,则先会执行tcmdbget在cache中查找,如果找到则返回,否则继续在真正的hash database里查找。首先找到bucket的偏移位置,对于hash database来说,一部分数据会被map到共享内存,如果bucket属于这部分数据,则从map中读取;否则从数据文件读取(可能存在bucket的一部分数据在map,另一部分在数据文件的情况)。

bucket的header数据结构为:

magic word | hash | left child off | right child off | padding size | size of key | size of value |

buckey里的数据是以树存储的,用上面计算出的二次hash的key,与bucket key比较,若二次key较小,则向左子树找;若较大则向右子树找;否则相等时,比较关键字,若相等,则找到结果,返回;如果虽然二次hash的值相等,但是比较关键字不等,则需要解决二次 hash的碰撞。

二次hash碰撞的解决方法,是沿着当前节点的左子树或者右子树下行直至找到二次hash值等、关键字也等的节点,或者找不到匹配的节点,则返回失败。

———————————- 小分割 ——————————————

2012-1-19

今天想解决3个问题:

  1. 复制数据库文件,与tcrmgr copy的区别;后者对锁的使用情况
  2. B+tree数据文件格式,为什么异常退出会导致数据丢失?
  3. 尝试用c代码读取ulog和数据文件的内容
tcrmgr copy的奥秘

tcrmgr copy的响应函数是proccopy,首先tcrdbnew数据库,并init了一个mutex锁,继而调用tcrdbcopy函数,先请求这个mutex锁,再进行具体的copy操作。

这里请求mutex锁的tcrdblockmethod函数,在多处被调用,比如get、put等等。这里是简单的CLI模式,每次数据库都是new的,mutex锁也是新建的,但是可以猜想,在多线程模式下,线程间的读写操作都是互斥的。(这里存疑,为什么用互斥锁,而非条件变量呢?)

tcrdbcopyimpl组装socket的copy包,通过tcrdbsend发送给ttserver。并通过ttsockgetc获取返回值。

可以看出,copy的奥秘不在tcrmgr这里,而是在ttserver怎样处理copy请求里。

ttserver.c的do_copy函数具体处理copy请求,针对B+tree,其实际处理函数为tcbdbcopy。

首先对数据库加写锁,排除一切的读写操作,从cache里读取叶节点和非叶节点,分别存储到lids和nids里。解锁。由于cache里的数据还未写入到数据文件里,所以针对每个节点,调用对应的save函数,写入到数据文件中(写入每个节点的前后加写锁、解锁)。

调用tcbdbtranbegin启动事务,加读锁,调用tchdbcopy函数(据说B+tree的底层存储其实是hash格式)将数据写到目的文件中,解锁。

实际执行复制操作的是tchdbcopyimpl,这里封装了两种操作,一种是直接调用tccopyfile进行复制,另一种是当目的文件以@开头,则执行命令。仅关注前者,很简单,就是将数据库文件中的内容读取出来,写入到目标文件。

那么tcrmgr copy和直接copy有哪些区别呢?

  • tcrmgr copy会先将cache中的数据写回到文件中;而直接copy不会,所以cache中的数据就丢失了
  • tcrmgr copy写备份文件的时候,会加读锁,排斥写,保证备份文件的数据格式是正确的;而直接copy时没有锁,如果正好遇到cache中的数据同步到数据库文件,则备份文件的格式可能是损坏的
B+tree数据库异常情况分析

从上面的copy过程,猜想,异常退出导致文件格式损坏的原因可能有:

  • 数据写在cache里,没有存入文件,cache里的新数据丢失
  • 存入文件过程中,异常退出,导致文件格式损坏,可能很多数据甚至整个文件都会丢失

具体需要看tcbdbputimpl函数。在该函数中,首先从history里查找key对应的叶节点的pid,如果找不到则从根节点开始二分查找,直至找到或者新建了一个leaf节点的pid(细节没看)。然后将新内容作为rec写入pid对应的leaf位置。之后如果需要,会对整个树进行调整

如果该节点所在的recs数目超过了启动ttserver时的预设值lmemb,或者该节点的的size超过了lsmax,则调整。调整的具体过程忽略,可以参考算法中的B+ tree部分。

否则,如果该节点的recs数目<1,则说明put失败,需要调用tcbdbleafkill函数切断该leaf节点与树的联系(疑问,这个leaf里应该还有其他旧数据,岂不是也丢失了?)。

如果不在事务里,则将数据写入cache。如果页节点的cache数目大于设定值lcnum,则需要将叶节点的cache内容写入到数据文件(又调用了hash的put函数,不清楚那边是否也有cache缓存),并清除叶节点cache的内容。同样处理非叶节点。那么,如果非正常退出,cache中的数据就有可能会丢失。

在将数据写入数据文件的过程中,注意到其rbuf的大小是BDBPAGEBUFSIZ,即32768,这就意味着,一个leaf的最大字节数是32768(数据部分应该是比这个要小)。

留个问题,cache里的数据难道不存在于数据文件中吗?

在csdn上看到有人问:mysql_connect后是不是非要mysql_close,是不是自动释放?

大家伙们,PHP数据库连接是不是在页面执行完之后自己释放,即使不用mysql_close()也可以释放掉?
同理PDO连接到数据库后,是不是非要将PDO实例赋值为NULL,才能将PDO连接断掉?

在N久以前我查过资料,但己经模糊不清了,具说PHP处理每一个被请求的页面时,里面有MYSQL连接的,当页面处理完就立刻释放掉所有用到的资料。如:mysql_connect后的连接。这个说法对吗?PDO同理吗?

第一反应是CGI模式下,mysql_connect是当前请求结束自动释放,而mysql_pconnect是可以被同一web进程(线程)中的多个请求重复使用的。但是为什么呢?

查看php.net上的帮助:

mysql_pconnect() acts very much like mysql_connect() with two major differences.

First, when connecting, the function would first try to find a (persistent) link that’s already open with the same host, username and password. If one is found, an identifier for it will be returned instead of opening a new connection.

Second, the connection to the SQL server will not be closed when the execution of the script ends. Instead, the link will remain open for future use (mysql_close() will not close links established by mysql_pconnect()).

This type of link is therefore called ‘persistent’.

首先,mysql_pconnect建立的链接不会在script结束后释放,那么对于CLI模式呢?从道理和实践代码中检测,都是会释放的。因为进程退出时,所有的资源都会被自动回收。通过以下的代码可以简单的验证:

$db = mysql_pconnect(‘localhost’, ‘mysql’, ‘pwd’);
sleep(100);
var_dump($db);
exit;
同时在mysql中执行:
mysql> show processlist;  # sleep期间
+—-+——-+—————–+——–+———+——+——-+——————+
| Id | User  | Host            | db     | Command | Time | State | Info             |
+—-+——-+—————–+——–+———+——+——-+——————+
|  1 | mysql | localhost:63950 | tshare | Sleep   |    1 |       | NULL             |
| 68 | mysql | localhost       | NULL   | Query   |    0 | NULL  | show processlist |
| 73 | mysql | localhost       | NULL   | Sleep   |   20 |       | NULL |
+—-+——-+—————–+——–+———+——+——-+——————+
3 rows in set (0.01 sec)
mysql> show processlist;  # 进程结束
+—-+——-+—————–+——–+———+——+——-+——————+
| Id | User  | Host            | db     | Command | Time | State | Info             |
+—-+——-+—————–+——–+———+——+——-+——————+
|  1 | mysql | localhost:63950 | tshare | Sleep   |    0 |       | NULL             |
| 68 | mysql | localhost       | NULL   | Query   |    0 | NULL  | show processlist |
+—-+——-+—————–+——–+———+——+——-+——————+
2 rows in set (0.00 sec)

那么CLI模式呢?将上面这段代码cp至一个web页面中,通过浏览器访问结束后,show processlist仍然可以看到有连接存在!这就使得多个web请求可以复用同一mysql链接,从而节省了建立连接的过程。但是这样也可能造成问题,就是当允许的web进程(线程)数目超过mysql的max_connections时,会造成web进程连接mysql服务器的阻塞,因为每个web进程都不释放自己的mysql连接,从而可能造成too many connections的mysql错误。可以参考我之前的blog:http://flykobe.com/index.php/2011/01/20/mysql-too-many-connections%E8%A7%A3%E5%86%B3%E6%96%B9%E6%A1%88/

那么它会存在多久呢?与mysqld的wait_timeout相关。如果一直没有被复用,那么它会存在wait_timeout时长,单位是秒。

mysql> show variables like ‘wait_timeout';
+—————+——-+
| Variable_name | Value |
+—————+——-+
| wait_timeout  | 28800 |
+—————+——-+
1 row in set (0.00 sec)
当然,如果web server的进程(线程)结束,mysql_pconnect建立的连接也会结束。

今天很有趣,我需要把一张mysql600多万数据、读写频繁的表拆分,趁着导测试数据的时间,总结一下我的2011.

2011年,最大的收获是亲近了佛法,完成了北京佛教研究所与北京广化寺开办的佛教初级和中级研修班的学习,并在正信、正见的道路上迈下了坚实的一步。

性格方面有了一些改变,之前我是个自认为蛮聪明、暴躁、急性子的人。而现在,走在路上,会突然警醒自己正在埋着头、飞快的迈着步子,于是刻意的放缓脚步、抬起头看看远方,发现生活中、道路上的种种美好。

在与人相处中,之前认为非黑即白,不可相容。而其实,善和恶,在一个短时间内是很难看得清楚的,强行的把我对善恶的观点加诸他人,其实不是因为我有多美好,而是我有多专制。

对于事业成功的定义,也有所变化。以金钱来衡量成功,其实很虚无。今年的高薪到了明年,可能就变成中下水平,而工作的愉悦程度、自身能力是否 稳步提高、企业文化才是略微真实和可把握的。

2011年是创业的一年。还行,公司能够保持正的资金流和一个相对稳定的发展态势。

2011年,我的技术进步可能稍慢,这一点比较遗憾。涉猎的范围比较杂乱, 部署了多台服务器,从apache转移到lighttpd,又转移到nginx;用了用NoSQL的cabinet存储,提高了性能,但是也遇到一些问题;做了mysql的主从备份,但是备份机没有发挥大的用途。做的最多的,还是业务层面的开发,年初对设计模式比较感兴趣,年末发现有些是过度设计,于是重构;遇到过性能瓶颈,从服务器、数据库和代码的角度进行了一些调优,但是也暴露出我经验不足的劣势。

2011年,家里又添了一只猫,现在有3个小家伙:噜噜、团团和咪宝。噜噜、团团仍然向肥胖不懈努力,咪宝在和病魔抗争,希望2012他可以健健康康!搬到了顺义,继续喂养小区里的流浪猫。也跟随公司在帮助几个流浪动物基地。

2011年,我的生活还不错,从空虚和追求享受,到学会从奉献和佛法中获取安定和快乐。

2012年,希望在修行的道路上能有进展,希望生活平静幸福,也希望技术方面可以有所突破。

休息十分钟,写写自己的感悟。

早晨看到一个朋友在工作群里感慨印度的见闻,落后、欺骗、不准时、贫穷等等,她的出发点是好的,描述这些情况,印证因果不爽,然后教导别人向善。

我作为被教育者之一,稍稍有些诧异。因为在我的观念里,印度是伟大佛陀的故乡,有很多久负盛名的禅修中心;当代的印度计算机业十分发达,有很多优秀的工程师;当然,也少不了《3 idiots》和《贫民窟的百万富翁》之类电影的熏陶。所以,我对印度还是充满了向往。而朋友言语描述的印度,充满了反差,也稍稍感觉有一些绝对。

恰逢最近在修《弟子规》功过格,自然而然的自省,我是否也有这样的以说教的口吻对待别人呢,并认为自己的看法是绝对正确、希望他人接受的呢?答案是肯定的!

《坛经》里说,若真修道人,不闻他人过。而我经常看到他人的过错,并揪着不放。比如挤公交车,我一般是始发站上车,都有位子,车子开到中途很挤的时候,很多人上车了就挤门口也不往里面走,外面的人就挤不上来。我看到这种情况,就会很愤慨的谴责别人,不为他人着想,只顾自己。甚至还为此与别人争吵过几次。事后,也都觉得自己是有理的,是善的,是清高的。而昨天,坐了另外一辆车,短途的,没几站我就要下了,于是我也不大愿意往后面走。当这个念头闪现的时候,就发觉,其实自己和他们没有区别。

当我指摘别人的时候,如果换位思考一下,也许结论就是不同的了。当然,坐公交车,礼让他人是肯定需要的,但是我指责的态度和做法都是非常有错误的!!我并不是代表着善,只是出于自私、我慢等心理在做这些事情。

经论和师父都说过,言语是苍白的,行动才是有力的。仍然以公交车为例,同学也说过,如果人多的时候,我先给年长者让个座位,然后自己走到最后面去,其实不需要在意其他人怎样。这些其实都是世间法,都是虚幻的,站在哪里、是否挤上这趟车、早晚,在广袤的时间和空间里都是没有意义的,因缘法皆空,重要的是,能否从这生活的点滴中,发掘出佛法的智慧?!

第一义谛中,没有善恶,只需要以清净平等心行住坐卧即可。