月度归档:2013年08月

Erlang文件I/O小结

Erlang中用来进行文件操作的函数封装在4个库里,分别为file,filename,filelib和io模块:
1. file模块:包括文件打开/关闭,读取/写入,文件/目录操作等。
2. filename模块:提供了一组文件名相关操作的函数,这些函数平台独立,方便写跨平台的程序。
3. filelib模块:file模块的扩展,大部分函数基于file模块的导出函数实现。
4. io模块:提供一组对已打开文件进行操作的函数。

下面分场景总结下文件I/O函数的使用:

0. 准备
数据文件data.dat内容:

1
2
3
4
{person, "kaka",
  [{occupation, programmer},{favorite, erlang}]}.
 
{cat, {name, "tom"},{owner, "kaka"}}.

1. 按Erlang项式读写
按Erlang项式读写是指每次I/O的对象是Erlang项式。

从文件中一次读取所有项式:

1
2
3
4
1> file:consult('data.dat').
{ok,[{person,"kaka",
             [{occupation,programmer},{favorite,erlang}]},
     {cat,{name,"tom"},{owner,"kaka"}}]}

一次读取一个项式需要多个函数组合首先,先用file:open/2打开文件,再用io:read/2函数读取一项,直到文件结束,返回eof。

1
2
3
4
5
6
7
8
9
10
11
1> {ok, File} = file:open('data.dat', [read]). %File是一个用于访问文件的设备。
{ok,<0.37.0>}
2> io:read(File, '').
{ok,{person,"kaka",
            [{occupation,programmer},{favorite,erlang}]}}
3> io:read(File, '').
{ok,{cat,{name,"tom"},{owner,"kaka"}}}
4> io:read(File, '').
eof
5> file:close(File). %关闭设备。
ok

如果文件内容包含非法Erlang项式,file:consult/1和io:read/2都会抛出erl_parse异常。

向文件中写入项式可用io:format/3函数实现:

1
2
3
4
5
6
7
8
1> {ok, File} = file:open('data.dat', [write]).
{ok,<0.33.0>}
2> Person = {person, "kaka", [{occupation, programmer},{favorite, erlang}]}.
{person,"kaka",[{occupation,programmer},{favorite,erlang}]}
3> io:format(File, "~p.~n", [Person]).
ok
5> file:consult('data.dat').
{ok,[{person,"kaka", [{occupation,programmer},{favorite,erlang}]}]}

io:format/3可以对输出进行格式化,格式化参数较多,可参考io模块手册

2. 按行读写
io:get_line/2函数可以一次从文件中读取一行,文件结束时返回eof:

1
2
3
4
5
6
7
8
9
10
11
12
1> {ok, File} = file:open('data.dat', [read]).
{ok,<0.33.0>}
2> io:get_line(File, '').
"{person, \"kaka\",\n"
3> io:get_line(File, '').
"  [{occupation, programmer},{favorite, erlang}]}.\n"
4> io:get_line(File, '').
"\n"
5> io:get_line(File, '').
"{cat, {name, \"tom\"},{owner, \"kaka\"}}."
6> io:get_line(File, '').
eof

向文件中写入一行也是用到io:format/3。同前文。

3. 随机读写。
从文件随机位置读取数据用到file:pread/3函数,它的3个参数分别表示文件设备IoDevice,起始位置Start,读取长度Len。如下:

1
2
3
4
1> {ok, File} = file:open('data.dat', [read, binary, raw]).
{ok,{file_descriptor,prim_file,{#Port<0.587>,11}}}
2> file:pread(File, 5, 10).
{ok,<<"on, \"kaka\"">>}

它表示从第5个字节开始,读取10个字节长度的二进制数据。如果当前位置离文件末尾不足Len个字节,则返回能读取到的字节数的内容。

与随机读对应的随机写函数为file:pwrite/3,它的3个参数分别表示文件设备IoDevice,起始位置Start,待写二进制内容Bin。如下:

1
2
3
4
1> {ok, File} = file:open('data.dat', [write, binary, raw]).
{ok,{file_descriptor,prim_file,{#Port<0.587>,11}}}
2> file:pwrite(File, 5, <<"test">>).
ok

此外,file:position/2可以任意指定当前文件的指针位置,它的2个参数分别表示文件设备IoDevice,目标位置Location。如下:

1
2
3
4
5
6
1> {ok, File} = file:open('data.dat', [write, binary, read,raw]). 
{ok,{file_descriptor,prim_file,{#Port<0.587>,11}}}
2> file:position(File, 0). %指针移到文首。
{ok,0}
3> file:position(File, eof). %指针移到文末
{ok,105}

4. 以二进制方式读取整个文件内容

1
2
3
1> file:read_file('data.dat').
{ok,<<"{person, \"kaka\",\n  [{occupation, programmer},{favorite, erlang}]}.\n\n
{cat, {name, \"tom\"},{owner, \"kaka\"}}.">>}

摘自Joe Armstrong - 『Programming Erlang』。

--EOF--

DBCP预检测连接有效性

今天碰到一个SQL查询操作,后端MySQL驱动抛出一个CommunicationsException的异常:

1
2
3
4
5
6
7
8
9
10
11
org.springframework.dao.RecoverableDataAccessException: 
### Error querying database.  
Cause: com.mysql.jdbc.exceptions.jdbc4.CommunicationsException: 
The last packet successfully received from the server 
was 57,695,440 milliseconds ago.  
The last packet sent successfully to the server was 57,695,441 milliseconds ago. 
is longer than the server configured value of 'wait_timeout'. 
You should consider either expiring and/or testing connection validity 
before use in your application, 
increasing the server configured values for client timeouts, or using 
the Connector/J connection property 'autoReconnect=true' to avoid this problem.

看这个提示很眼熟,以前也碰到过,起因是DBCP连接池在执行SQL语句时未验证连接有效性。本文就此问题的相关知识和解决方法做个总结吧。

数据库为提升利用率一般会回收长时间空闲的连接。MySQL中默认这个时间为28800s,也就是8小时,由wait_timeout参数指定:

1
show global variables like 'wait_timeout';

DBCP连接池的基本思路一次性创建多个数据库连接,当上层应用需要执行SQL时,从连接池中取出一条空闲连接,执行完之后将连接归还连接池,这样避免了数据库连接的重复创建和销毁。当连接池中的连接长时间处于空闲状态时,连接另一头的MySQL会根据设置的wait_timeout值大小回收这些连接,但是DBCP对连接被回收却不知情,因此,只要应用有请求,连接池就会把空闲连接分配出去,尽管这已是一条半闭合的连接,这就造成了通过这条连接执行的SQL语句运行失败,也就触发了数据库驱动层抛出CommunicationsException。

DBCP提供多种方式解决这一问题,有文章已经总结过了,其实最简单、最有效的方法就是利用DBCP提供的validationQuery参数进行连接的预检测,它会在与连接池交互的过程中加入一些钩子,定点执行validationQuery指定的SQL语句,如果SQL语句执行成功,表示此连接有效,分配给应用,如果执行失败,则丢弃此连接,这种方法还能应对网络故障等问题造成的MySQL连接失效问题。其他的一些方法,比如空闲连接检测,乐观获取连接等方式,都无法保证完全对应用透明,应用还是能感知到数据库操作失败。

DBCP配置项中,与validationQuery相关的有validationQuery,testOnBorrow,testOnReturn,testWhileIdle等几个,详见官方文档。validationQuery要求必需是个SELECT类型的SQL语句,至少返回一行,由于它会在所有应用的SQL语句执行之前运行一次,所以原则上应该对数据库服务器带来的压力越小越好,在不同类型的数据库中有不同的推荐值,这里有篇文章对此进行了总结,MySQL推荐使用“SELECT 1”。testXXX系列的配置参数用来指定运行validationQuery的时机,比如testOnBorrow设为true表示从连接池中获取连接前运行validationQuery,testOnReturn设为true表示将连接归还连接池前运行validationQuery,testWhileIdle要与timeBetweenEvictionRunsMillis、numTestsPerEvictionRun等参数配合使用,只要设置得当,DBCP会定期对连接池中的空闲连接进行有效性验证。默认情况下,testOnBorrow设置为true,testOnReturn和testWhileIdle设置为false。因此,支持数据库连接预检测的Spring DBCP连接池参数可以简化为以下配置:

1
2
3
4
5
6
7
8
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource"
    destroy-method="close">
    <property name="driverClassName" value="com.mysql.jdbc.Driver" />
    <property name="url" value="jdbc:mysql://10.10.120.16:3306/db" />
    <property name="username" value="username" />
    <property name="password" value="password" />
    <property name="validationQuery" value="SELECT 1" />
</bean>

关于使用validationQuery参数来预检测连接有效性的缺陷,前文已有提及,它肯定会带来数据库使用效率的降低,特别是大量短连接的场景中。一般来说,局域网中执行一条“SELECT 1”的耗时约为1~5ms,如果能接受这种程度的损耗,那么推荐使用预检测连接的方式来解决连接池中连接失效的问题,它很粗暴但是最有效。

--EOF--

『打造Facebook』

『打造Facebook』尽管时常有关其市值、用户数量屡创新高的消息见诸报端,尽管它被媒体吹捧着如何重新定义了社交网络,尽管它已经拥有了顶级的IT技术人才储备,可是Facebook始终无法给我留下一个“豪门”的印象,原因包括但不限于以下几点:1.我非常讨厌人人网,2.电影『社交网络』中阴暗的扎克伯格。尽管人人网只是山寨到了Facebook的皮囊,尽管王淮说『社交网络』中的扎克伯格与现实中的扎克伯格只有2%相似的地方,但是喜好本来就是非常主观的东西,跟理性无关。

作为一个Facebook黑,我本是带着批判成功学的心态到书中挑刺的,还是那句话,花时间花精力去了解是为了更好的批判它。但没想到作者把Facebook的招聘、新人培训、薪酬福利、项目管理、开发流程、公司文化等等讲得面面俱到,必须承认王淮很诚恳,谁不承认谁就是不客观了。

作为一个刚进入职场不久的新人,那些关于跳槽、升职、招聘和团队建设的东西离我似乎还有些遥远。“不在其位,不谋其政”。我目前只在乎工作是否开心,压力和动力并存,每天能比前一天的自己有进步,就像现在。

作为一个程序开发人员,对书中提到的Facebook产品开发流程倒是有些共鸣,特别是项目管理的重要性。一年以来,我一直在一个大项目里做事,平时需要与多个团队协作完成开发任务。跨部门的合作难就难在双方对需求理解不一致,对任务的优先级定义不统一,以及由于座位、人员调整等原因造成的沟通不方便,单纯由开发人员去沟通效率很低,特别是任务优先级的问题,基本没有老大出马搞不定。有了专门的项目管理人员来组织碰头会议、讨论需求、指定任务优先级、知会相关人员、定期跟进的方式来解决这些琐事,收到的成效非常明显,基本都能保证按期交付,也能把开发从低效的工作方式中解放出来。

--EOF--

Erlang顺序编程 [2]

1. 函数
函数参数的个数称为函数的目,例如sum/2表示函数名为sum,参数个数为2个。Erlang中判断两个函数是否相同的标准是其函数名和函数目是否严格一致,跟参数类型没半毛钱关系。因此,sum/1和sum/2除了恰好名称相同之外,再无其他联系。如下代码:

1
2
3
4
sum(L) -> sum(L, 0).
 
sum([], N) -> N;
sum([H|T], N) -> sum(T, N+H).

虽然看似有三个函数声明,但实际上只有两个函数sum/1和sum/2。sum/2由两个函数子句组成,子句之间用分号;隔开,如果子句sum([], N) -> N后面用了句号,那么编译器会抛function sum/2 already defined异常。

多个函数子句之间,Erlang根据子句出现的顺序对参数进行模式匹配,一旦匹配到,就会执行该子句后面表达式,所以子句的顺序非常重要。如果所有子句都匹配失败,则会抛出一个no function clause matching的运行时异常。

2. 匿名函数
匿名函数是没有名字的函数,用fun定义,形式为fun(参数) -> Expr end.。例如:

1
2
3
4
1> Double = fun(X) -> 2*X end.
#Fun<erl_eval.6.82930912>
2> Double(1).
2

当存在多个匿名函数子句时,fun函数形式为fun(参数1) -> Expr1; (参数2) -> Expr2 end.。例如:

1
2
3
4
5
6
1> Fun = fun({plus, X, Y}) -> X + Y; ({minus, X, Y}) -> X - Y end.
#Fun<erl_eval.6.82930912>
2> Fun({plus, 2, 1}).
3
3> Fun({minus, 2, 1}).
1

fun函数还可作为函数的参数,也可作为函数的结果。fun函数作为函数的参数的例子在lists模块的导出函数中就有用到:

1
2
3
4
5
6
7
 
1> Double = fun(X) -> 2*X end.
#Fun<erl_eval.6.82930912>
2> L = [1, 2, 3, 4].
[1,2,3,4]
3> lists:map(Double, L).
[2,4,6,8]

当fun函数作为某个函数的返回结果时,那个函数就相当于一个用来生成函数的函数。例如:

1
2
3
4
5
6
7
8
9
10
1> Fruit = [apple, pear, orange].
[apple,pear,orange]
2> MakeTest = fun(L) -> (fun(X) -> lists:member(X, L) end) end.
#Fun<erl_eval.6.82930912>
3> IsFruit = MakeTest(Fruit).
#Fun<erl_eval.6.82930912>
4> IsFruit(apple).
true
5> IsFruit(dog).
false

MakeTest本身是一个匿名函数,它接受列表L作为参数,并且生成一个匿名函数IsFruit,IsFruit匿名函数用来判断参数是否是列表L中的成员。

3. 列表解析
列表解析是生成列表的一种方式,记号为[ F(X) || X <- L ],它表示由F(X)组成一个列表,其中X取值于列表L。例如:

1
2
1> [ 2 * X || X <-  [1, 2, 3, 4] ].
[2,4,6,8]

显然这种方式比用list:map方式实现相同的功能更加简单:

1
2
1> lists:map(fun(X) -> 2 * X end,  [1, 2, 3, 4]).
[2,4,6,8]

列表解析还有以下更为常健的形式:

1
[ X || Qualifier1, Qualifier2,]

其中,X是任意一个表达式,每个限定词Qualifier可以是一个生成器或者过滤器,用来限定或者过滤出符合条件的列表值,其中生成器部分必须,并且生成器要在过滤器之前,例如:

1
2
1> [X || X <- [-1, 0, 1], X > 0].
[1]

列表解析可以用来解决一些经典问题,见如下两个例子:
1) 毕达哥拉斯三元组:给定整数N,求出符合条件的A2 + B2 = C2,A + B + C < N的所有A,B,C集合。

1
2
3
4
5
6
7
8
pythag(N) ->
     [{A,B,C} || 
          A<-lists:seq(1, N), 
          B<-lists:seq(1, N), 
          C<-lists:seq(1, N), 
          A*A + B*B =:= C*C, 
          A + B + C < N
     ].

2) 变位词:求出一个字符串的全排列。

1
2
3
perms([]) -> [[]];
perms(L) ->
     [[H|T] || H<-L, T<-perms(L -- [H])].

4. 异常
遇到系统内部错误时,可以有三种方式显示抛出异常:exit(Reason), throw(Reason), erlang:error(Reason)。用try…catch...语句捕捉异常的语法为:

1
2
3
4
5
6
7
8
9
10
11
try FuncOrExpressionSequence of 
     Pattern1 [ when Guard1 ] -> Expression1;
     Pattern2 [ when Guard2 ] -> Expression2;catch
     ExceptionType: ExPattern1 [ when ExGuard1 ] -> ExExpression1;
     ExceptionType: ExPattern2 [ when ExGuard2 ] -> ExExpression2;after
     AfterExpressions
end

try…catch…流程:对FuncOrExpressionSequence求值,如果不产生异常,则进入正常的PatternX分支,返回ExpressionX。如果产生异常,则进入ExceptionType: ExPatternX分支,其中ExceptionType为以下三个值之一:throw, exit和error,若为标明,默认为throw。after区块可选,如果有after区块,则无论FuncOrExpressionSequence求值过程中有没有异常抛出,AfterExpressions都会执行,但是表达式的值会被舍去。

以下为try…catch...的示例程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
-module(test).
-compile(export_all).
 
generate_exception(1) -> reason;
generate_exception(2) -> throw(reason);
generate_exception(3) -> exit(reason);
generate_exception(4) -> exit(other);
generate_exception(5) -> {'EXIT', reason};
generate_exception(6) -> erlang:error(reason).
 
demo() ->
     [catcher(X) || X <- lists:seq(1, 6)].
 
 
catcher(N) ->
     try generate_exception(N) of
          Val -> {N, normal, Val}
     catch
          throw: X -> {N, caught, thrown, X};
          exit: reason -> {N, caught, exited, reason};
          exit: _ -> {N, caught, exited, other};
          error: X-> {N, caught, error, X}
     end.

运行结果如下:

1
2
3
4
5
6
7
1> test:demo().
[{1,normal,reason},
 {2,caught,thrown,reason},
 {3,caught,exited,reason},
 {4,caught,exited,other},
 {5,normal,{'EXIT',reason}},
 {6,caught,error,reason}]

5. 模式中使用匹配操作符
例如:

1
2
3
4
5
6
7
8
1> Func1 = fun({tag, X} = Z) -> {tag, X, Z} end.
#Fun<erl_eval.6.82930912>
2> Func1({tag, 1}).
{tag,1,{tag,1}}
3> Func2 = fun(Z = {tag, X}) -> {tag, X, Z} end.
#Fun<erl_eval.6.82930912>
4> Func2({tag, 2}).
{tag,2,{tag,2}}

为了重复进行模式匹配,可以将某个模式第一次出现时赋给一个临时变量,之后就可以用该临时变量替换那个模式。示例中将{tag, X}与临时变量Z绑定,更加高效(以该元组作为参数调用其他函数时避免创建新元组)同时更少出错,Z与{tag, X}绑定时,两者出现的左右顺序无关。

6. 基本类型
Erlang的基本类型之间可以进行比较操作,它们之间的优先级为:
number < atom < reference < fun < port < pid < tuple < list < binary 意思是所有数值类型都比原子类型小,表达式1 < a返回true;所有元组比列表小,表达式{1} < [1]返回true。 Erlang数值类型分为整形和浮点型。整形变量可代表的数据长度仅受限于可用的内存,它有两种表示方法:传统语法和K进制语法,后者的表示方法为K#Digits,K为进制,2<=K<=36,大于十进制时,Digits可用[a-zA-Z]表示数值10-35。例如2#10表示2,11#A表示10,36#Z表示35,36#10表示36。浮点数内部以IEEE 754的64bit格式表示,因此其可表示的范围为-10323~10308

整数和浮点数的比较有以下一些原则:
1) 如果比较双方一个是整数,一个是浮点数,则整数在比较之前会转换为浮点数。
2) 如果比较双方都是整数或是浮点数,则以原来类型进行比较。

7. 布尔表达式
Erlang的布尔表达式有两套运算符:orelse/andalso和or/and。前者称为短路布尔表达式,当表达式的左边能决定最终运算结果时,右边表达式不进行运算。例如Expr1 orelse Expr2,如果Expr1为true,那么Expr2不会进行求值。而or/and表达式中,无论表达式的值是否可以从左边参数推断出来,右边表达式必须进行求值。

摘自Joe Armstrong - 『Programming Erlang』。

--EOF--