Upsert

Upsert是个合成词,指的是:在数据库表中首先Update一条可能记录,如果目标记录并不存在,则Insert一条新的记录,也就是所谓的merge操作。
听起来这是一个数据库操作中很常见的场景,写起来很简单,不是吗?最直接的做法:我先搜一下记录存在与否,如果存在,就Update;否则就Insert,不是很直观吗?是的,就是这么简单--如果您能保证您的Upsert操作对象永远不会出现并发更新的情况的话。而如果您使用的数据库本身就支持merge操作的话,这个问题甚至可以更简单:您只需一条SQL就能实现上面三条SQL的功能。
比如MySQL的内建INSERT…ON DUPLICATE KEY UPDATE语法,就提供了这样的便利——如果insert的数据会引起唯一索引(包括主键索引)的冲突,即这个唯一值重复了,则不会执行insert操作,而执行后面的update操作。

1
INSERT INTO test (a,b) values (1,2) ON DUPLICATE KEY UPDATE b = b + 1;

在这个例子里,假设表中只有a=1,b=1这一条记录,当试图插入a=1,b=2时,因为a=1记录已经存在,则执行update后面的b=b+1,记录被更新为a=1,b=2(这个2是被b=b+1更新的,而不是插入的)。

1
INSERT INTO test (a,b) values (2,2) ON DUPLICATE KEY UPDATE b = b + 1;

此条语句因为a=2的记录不存在,则执行Insert的操作。
但是,不是所有的数据库都是MySQL这样。另一个应用广泛的数据库PostgreSQL,本身就不提供ON DUPLICATE KEY UPDATE的语法支持。有意思的是,在PostgreSQL的官方文档中提供的例子中,这个实现是依赖错误处理来实现的。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
CREATE TABLE db (a INT PRIMARY KEY, b TEXT);
CREATE FUNCTION merge_db(key INT, data TEXT) RETURNS VOID AS
$$
BEGIN
LOOP
-- first try to update the key
UPDATE db SET b = data WHERE a = key;
IF found THEN
RETURN;
END IF;
-- not there, so try to insert the key
-- if someone else inserts the same key concurrently,
-- we could get a unique-key failure
BEGIN
INSERT INTO db(a,b) VALUES (key, data);
RETURN;
EXCEPTION WHEN unique_violation THEN
-- Do nothing, and loop to try the UPDATE again.
END;
END LOOP;
END;
$$
LANGUAGE plpgsql;
SELECT merge_db(1, 'david');
SELECT merge_db(1, 'dennis');

这段代码在一个LOOP中先做update,如果成功就返回;否则继续尝试Insert,在这个操作时要捕获唯一性冲突的异常,如果出现此异常,说明在update时拥有该键值的记录不存在,但是尝试Insert操作的时候却存在了,所以会有冲突的错误。就如注释指出的,这种情况是并发时常见的问题,所以要用LOOP保证此时还有机会返回重新尝试Update。
软件设计中如何实现一个功能,是脱离不开功能具体的应用场景和上下文的。之所以在PostgreSQL中用这样一个有趣的方式来实现Upsert,是和数据库应用的常用场景分不开的。而在数据库应用中,并发效率可以说是功能性和非功能性需求最具代表性的两个方面。
这篇文章从思考的循序渐进的方式结合实验很好地展示了为什么要有这样的设计,简单地串起来,从直观但是问题很大的方案,一步步优化,到最终的最优方案,是经历了这样的一个筛选和比较的过程:

  1. 简单到两条SQL顺序调用,先update,无效后执行Insert。如前所述,并发时在Insert时只要两个调用不是同时进行,后来到必然失败,这种对竞态条件不做任何处理对方式,对并发的支持可以说是0;
  2. 进一步,初学者会想到那就把两个操作捆绑起来,放在一个事务里执行不就好了?但是等等,仔细想一下,事务保证的只是事务内所有的操作作为一个整体表现为最终的结果,都成功或者都rollback回去;事务本身并不保证事务之间的操作是资源独占的,所以竞态条件依然存在。
  3. 默认的事务是Default Isolation Level,可是事务不是还有一个Serializable Level么?从这个level本身的描述看,似乎可以让serializable transaction按照一定顺序来执行。但是根据引文的作者的实际实验,这条路行不通。表现为在并发时,虽然不会出现唯一性破坏的错误,但是在事务Commit的时候,大量的读写依赖关系会导致数据库的access无法真正穿行化(这是直接来自于报错信息,深层的原因有待深入研究)。
  4. 那好吧,要保证资源的独占访问,我给数据库表加锁总可以了吧(因为要update的记录可能不存在,所以不能直接给记录加锁)。当然可以!功能没有任何问题,在持有操作的表的锁的情况下,上面的两部分操作不会引起任何冲突。但是,效率啊,可以想像地惨不忍睹。
  5. 放弃表加锁,使用advisory locks呢?即所谓的劝告性锁,不在任何真实的实体上加锁,而是使用一个生成的数字,这个数字和操作的对象没有一毛钱的关系,操作完成之后加于其上的锁释放。这样实现效率可以提高很多。问题在于:如果别人不通过我们的程序操作相同的资源,怎么让他们也保证使用相同的advisory locks?没人能保证,我们总不能让别人都不能从console简单地执行一条Insert或者Update的SQL语句吧?
  6. 好吧,看来官方文档里的例子是最佳方案了。可是等等,为什么在PL/pgSQL一定要使用LOOP?我在Insert失败抛出错误之后在异常处理里直接再做一次Update不就好了么?有道理,但是不全面,仍然忽略了一种可能性,尽管这种可能性出现的概率不高——在我们Insert之前Insert的数据,导致了我们Insert失败,但是当我们尝试在异常处理中Update之前,这条记录被Delete了…那么我们再次Update必然就又失败了,如果此处没有LOOP,显然我们就失去了再次尝试并可能成功的机会。
    所以,我们看到的官方文档中的例子,是目前考虑了所有情况,兼顾并发和效率的最优的通用解决方案。
    这是一个很好的例子,不仅仅是最终的方案,重要的是整个思维过程,关于问题是怎么得到最优解的,很有借鉴的意义。

Hexo环境迁移到Mac

作为时下比较流行的Blog搭建环境,Hexo适合程序员的口味,结合Github,有利于提升逼格。唯一让人感到不方便的地方就是如果想换个机器,需要把本地的一些文件(最重要的就是source目录)同步到新机器的环境里。
Windows的Hexo环境搭建比较简单,我个人的Blog之前一直是在单位的Windows机器上整的,感觉还好。最近想在家里的MacBook上移植一把,遇到了一些坑儿,在本篇文章里试着整理一下。

Hexo安装

  1. Git安装,这个没什么可说的。建议安装最新版的,Mac自带的版本稍微老一些。完成安装后确保新安装的git的路径在$PATH里在旧版本的路径之前;
  2. 为本机Mac生成SSH Key并加入Github;
  3. Node.js安装。我是从官网直接下的Node.js for Mac OS X的pkg安装包。直接安装即可;
  4. 安装好Node.js自动就带了npm,执行
1
sudo npm install -g hexo

如果不使用sudo,会报错。我遇到了此步hexo安装时抛出Warning后hang住的情况,Google了若干解决方案都不适用,直到尝试按照某个贴子修改了/usr/local的权限后

1
sudo chown -R $USER /usr/local

安装成功.

迁移Hexo环境

  1. 执行
1
hexo init

生成hexo环境,copy备份的source文件覆盖本地source目录。

  1. 安装theme并修改当前hexo工作目录下的_config.yml配置文件。我的Windows和Mac的Hexo版本不同,所以不能直接使用备份好的_config.yml文件直接覆盖。
  2. 修改下载的theme的配置。至此,可以在我的Mac上继续使用Hexo了。
    使用Markdown编写Blog的时候,Mac上推荐一款免费的编辑软件Mou,非常好用。

多台机器共享

而为了在不同机器的Hexo环境中共享写好的内容(因为部署上去的hexo生成的blog都是静态的,每次有新的文章都需要重新生成静态页面),基于我使用的light theme,我把以下内容备份到了Github上:

  1. source目录
  2. 根目录_config.yml文件
  3. themes/light/_config.yml
  4. themes/light/layout/_widget/blogroll.ejs

用到的git命令主要有:

1
git add <dir or file path>
1
git commit -m "<comment>"
1
remote add origin git@github.com:<my github name>/<my hexo git repo>.git
1
git push -u origin master
1
git rm -r -f --cached <dir of file path to remove from stage area>

Perl的进程控制(二)

在实际的编程中,启动一个子进程,然后等待其结束,然后父进程接着干别的,这是很理想的情况。经常地,我们会需要一个Timeout的机制,在子进程运行的时间超出了我们的期望,明显情况不对的时候能够及时地终止它。
好消息是Perl提供了类似于Unix的信号机制,我们可以在某个信号上注册一个对应该信号的处理逻辑,比如收到ALARM信号的时候就强制退出(杀死)子进程。而父进程通过调用alarm函数使得在指定的时间会有一个SIGALRM信号发送过来,这样就能够通过(控制发送ALARM信号+ALARM信号处理函数)实现一个定时的机制。
当然,父进程还是通过waitpid以等待子进程的正常结束,ALARM信号只是用于处理Timeout的情况。所以waitpid之后,要调用“alarm 0”来取消之前的Timeout定时器。
一个典型的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
#!/usr/bin/perl
use strict;
use POSIX ":sys_wait_h";
my $timeOut = 5;
my $cmd = 'ping 10.10.10.10';
my $rv = 0;
my $child_pid = open ( my $CMD, "-|", $cmd ) // return -1;
if ( $child_pid ) {
# parent process
eval {
# Handle the SIGALRM and kill the child process(es)
local $SIG{ALRM} = sub {
kill 'SIGTERM', $child_pid;
sleep 1; # Give the process time to shut down nicely, then murder it.
kill 'SIGKILL', $child_pid;
die "timeout\n";
};
#-- Start Timer --------------------------------------------------------------#
alarm $timeOut if $timeOut;
# Wait for child processes. SIGALRM should take care of any hangs.
waitpid($child_pid, 0); # $? is set by waitpid when the process exits.
$rv = $?;
alarm 0 if $timeOut;
# -- Time's up! --------------------------------------------------------------#
};
if ( $@ ) {
#Exception handle
print "Error: $@\n";
$rv = -1;
}
if ($rv != 0) {
print "Failed to issue the command:" . "$rv" . "\n";
} else {
print "Succeed to issue the command:" . "$rv" . "\n";
}
}
close $CMD;

上面的代码通过一个ping命令来模拟一个运行超时的命令。第10行的open函数用于异步启动一个子进程执行该命令。16~21行的代码实现了针对SIGALRM信号的处理,24行启动了Alarm机制的timer,28行在非timeout的情况(子进程正常退出的情况)下取消timer。
注意eval经常被用于这种情况去捕获一个代码block中可能的异常。

Perl的进程控制(一)

先简单总结一下,Perl里调用shell命令的时候,会用到进程控制。经常用到的有以下几个方式:

  1. 通过system() 调用shell命令,Perl是父进程,Shell本身是子进程,实际执行的命令本身是孙子进程。
    例子:
1
system "ls -l \$HOME";

注意双引号内变量内插需要转义符。

  1. 通过exec()调用shell命令。1.与2.的区别在于system会等待shell返回,而exec直接结束Perl进程,要执行的命令在Shell中执行完毕后退出。
    例子:
1
exec "date";
  1. 反引号··用于捕获命令输出,不能乱用。通常返回的内容带回车符,依照需要可能要使用chomp。Perl解释反引号里的值的方式类似于system的单参数形式,在解释器中会以双引号字符串形式展开,这意味着反斜线转义与变量内插都会正常处理。
    例子:
1
2
my $now = `date`;
print "The time is now $now";
  1. 以上都是同步处理子进程。 使用open连接至文件句柄的方式可以启动并发运行的子进程。注意管道符号的位置,它表示文件句柄是被管道到命令的标准输出还是标准输入。
    例子:
1
my $child_pid = open ( my $CMD, "-|", @cmd_array ) or die "Failed to create a child process";

在上面的例子里, 一个文件句柄$CMD被打开并通过open被连接,@cmd_array表示的一个命令(包括其options)被异步地执行,其输出则管道至$CMD。

  1. 再高级一点儿的话,Perl也可以使用类似系统调用的fork()。和Unix下的fork()系统调用是一样一样的。
    例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
my $cld_pid = fork();# fork出一个子进程
if (!defined $cld_pid) {
die "Failed to fork a process.\n";
}
unless ($cld_pid) {
# 子进程
print "child!\n";
sleep 3;
} else {
# 父进程
print "parent!\n";
waitpid($cld_pid, 0);
}
# 父进程子进程都会执行到的地方
print "common.\n";

此例子的输出是:

1
2
3
4
parent
child!
common.
common.

注意父进程在打印第11行后会等待子进程结束,然后才执行15行。
实际上,system 做的事情,就是fork一个子进程处理命令,在fork出的子进程内调用exec执行命令(对应替换我们上面例子里的第8行),然后父进程wait在子进程的pid上。因为是exec调用,所以子进程就结束了, 等候在其上的父进程也就结束退出。—— 这是Unix系统标准的系统调用方式。

PermGen Memory Leak

PermGen memory leak是Java开发者时不时会遇到的一种比较恼人的内存问题,伴随的典型的症状就是java.lang.OutOfMemoryError: PermGen space这个错误。
PermGen就是Permanent Generation这么个JVM里的内存区,里面存放的主要内容是一堆堆的Java Class定义。当您的Java应用跑起来的时候,相关的所有Class信息就会装载到这个内存区里。这个内存区的大小可以通过启动JVM的时候通过参数配置,通常不会很大,默认的是82M。当遇到刚才的那条OOME的时候,有两种可能:

  1. 您的application确实比较牛,用到了很多类,PermGen装不下了。这种情况的解决方法很简单,就是提高这个内存区的大小。
  2. 另外一种可能就是一刀入魂的内存泄露了。为啥会泄露,下面这张图对于理解这个问题很关键!(很赞的一张图,来自这里
    Class和ClassLoader引用关系
    简单地说,每一个对象都会持有一个它的Class的引用,这个Class又会引用到装载这个Class的ClassLoader,最关键的是每个ClassLoader又持有其所有装载的Class的引用。那么问题就在于,当某一个Class因为某种原因没有被垃圾收集的话,它的ClassLoader装载的所有的Class也就不会被收集 —— 这么看ClassLoader真是负责任啊!

Tomcat因为其ClassLoader的机制和Webapp装载卸载的典型性就成为这个问题的温床。一个Webapp卸载后,按理说其对应的Class都应该没有用了从而被收集掉。可是一旦有某个Class仍然不幸保持引用,那么恭喜你,还有一堆从同一个ClassLoader装载的类也还僵尸般存在着,仍然霸占着有限的PermGen空间。

一个很好的例子可以参看Leaking Drivers,里面详细描述了JDBC规范下DriverManager持有一个具体Driver的引用,使得该引用在应用卸载时不能释放,从而导致的PermGen memory leak的细节。

如果搜索PermGen Memory Leak的解决方法,网上的方案五花八门,包括Overflow Stack上被狂点赞的那些帖子。但是真心都不是真正解决问题的思路。

相比之下,这篇帖子很坦诚也很中肯。对于这样的问题,我觉得负责任的回答应该是“分析你的code,找到你的code中ClassLoader和Class的不恰当的关系。只有真正找到root cause,才能真正解决它“。听起来像是废话,但是跟从网上那些仅从表面入手的建议,我的尝试证明了这句话的正确。

好消息是Tomcat一直在努力从它的角度避免这些可能的不恰当关系的发生,他们令人尊敬的结果可以在这里找到。

如果您想查看您JVM中的PermGen的实时数据,JDK中的这个命令可以帮助您

1
<jdk path>/bin/jstat -gcpermcapacity <pid>

Shell中的多command combine执行

在Shell中使用“&&”和“||”连接多个command来用简单的语法表示多个command执行序列的关系是一种常见的做法。在Makefile的动作部分更是可以简单的通过这种combine的方式达到当一组命令序列中的某条命令出错则退出整个执行的逻辑并避免使用大量if-else的控制语句。
一个有趣的问题是:当command不是一条简单的命令,而是一组命令甚至命令本身中包括if-else这样的控制结构的话,怎么来写这样的combined commands?考虑到不同的Shell,这个问题足够我这样的菜鸟喝一壶了……
Why not googling? 不幸的是,网上没有这样的问题的比较清楚的解决方案,基本focus在简单命令的combine使用上,例子大同小异(当然,也许是我google的关键词的原因?)。
不管怎样,经过一些摸索,我通过初步跑通的两个例子,试图大致总结一下如何实现。
第一个例子是Bash的实现,如下:

1
2
3
4
5
6
7
#!/bin/bash
flag="YES"
echo "1" && \
(if [[ $flag == "YES" ]]; then echo "2"; fi;) && \
echo "3" && \
(if [[ $flag == "YES" ]]; then echo "4"; fi;) && \
echo "5"

其执行结果为:

1
2
3
4
5
1
2
3
4
5

另一个例子是Csh的实现,如下:

1
2
3
4
5
6
7
#!/usr/bin/csh
set flag="YES"
echo "1" && \
(if (( $flag == "YES" )) then; echo "2"; endif) && \
echo "3" && \
(if (( $flag == "YES" )) then; echo "4"; endif) && \
echo "5"

同样实现了相同的执行结果:

1
2
3
4
5
1
2
3
4
5

注意不同shell中if-else控制结构中“;”的位置,这里很容易成一个坑。另外,csh不同于bash,如果变量没有定义的话,在判断时引用变量会报错退出。所以针对不同shell的时候,一定要小心它们的语法区别。
通过这样的实现,我们可以使用不同的flag来控制一个command序列,使这个序列作为一个整体序列来工作,这不仅意味着只有前面的命令成功时,后续的命令才会执行。而且当我们知道怎么在其中加入条件控制之后,我们可以根据特定条件(flag或其他条件)来“短路”掉或“插入”某些命令以适应不同的应用场景。这在Makefile中非常有用。因为我们可能会针对不同的软硬件版本有不同的编译方式。用这样的combined commands, 我们可以自由插拔一些操作而又能方便地保证某个task编译时某一步出错时整个task的编译会报错退出。