2009年1月18日星期日

用 PhantomReference 避免OutOfMemory

PhantomReference比Weak/Soft的引用强度都要低,PhantomReference.get()总是返回null,为什么呢?

其实PhantomReference而要是不可缺少的重要引用类型, 我们知道WeakReference在finalize方法调用或被gc清理之前会进入ReferenceQueue, 这时候没有任何strong reference引用对象,然后可以通过ReferenceQueue去做收尾工作。但是在finalize()方法,或者重新给这个对象一个引用使它reachable(从某个活动线程调用栈,或者静态变量可达), 使这个对象延长生命周期暂时不会被gc清理。而PhantomReference只会在对象被从内存中清除后才会进入队列,get()总是返回null (ReferenceQueue通知一个PhantomReference的时候,既然内存都已经物理释放了,当然也无法给你一个对象,所以get总是返回null也有这个原因), 所以你没有办法重新使这个对象再次reachable。PhantomReference只是提供了一种方式让你跟踪一个曾经产生过的对象,由此让你知道这个对象到底有没有被物理的清除。

比如当你的applet程序需要处理一个非常大的图片的时候,你可能希望图片处理结束并且内存被释放之后再处理下一个图片。如果用PhantomReference来引用上一个图片对象,当ReferenceQueue通知你的时候,你就可以知道上一个内存对象已经被物理清除,你可以继续下一个大内存对象的处理了。这样就可以避免由于GC线程优先级低导致上一个大内存对象还没有释放下一个大内存对象又被创建,让OutOfMemoryError出现的概率低一些。

还有一个好处,就是PhantomReference比用finalize方法好的多,因为VM对finalize的处理不如PhantomReference简单可靠,只不过是你要写的代码稍微多一点点而已。


Ethan有一个更完整的对四种reference的解释 http://weblogs.java.net/blog/enicholas/archive/2006/05/understanding_w.html

2009年1月4日星期日

Oracle 的锁基础原理

Oracle有以下类别的锁,本文主要只讨论DML的锁

•DML locks (data locks)

•DDL locks (dictionary locks)

•Oracle Internal Locks/Latches

•Oracle Distributed Locks

•Oracle Parallell Cache Management Locks

DML的锁是针对并发数据访问的,因此分行级别TX和表级别TM

两种经典锁

共享锁S表示其他事务可以读取这个资源但是任何事务都不可以更改这个资源,任何事物都不可以再加X锁。

排他锁X表示其他事务可以读取资源,但只有当前事务可以更改这个资源,其他任何事物都不可以再加S或者X锁。

行级锁

也叫事务锁,简称TX锁。两种经典锁,行级别上面只支持X锁(排它锁),Oracle没有行级别S锁(共享锁)。所有DML操作一般自动生成行级锁,行级别锁理论上比表级别锁效率高,因为他的锁定范围更小更精确。

Oracle当中一个记录被insert/update/delete/select for update之后,当前事务T1自动获得这行的X排他锁,另外一个事务T2如果要更改这条记录,另外一个事务T2会被阻塞,直到T1提交或者回滚。

比如

步骤 T1 T2 备注
1 update row1
2 update row1 T2被阻塞
3 commit T1提交,T2可以继续
4 commit

如果在第一步之后,你执行select * from v$lock where type in ('TX','TM'),你就会看到有一个行级别锁TX

image

其中LMODE=3和REQUEST=0,其中LMODE是已经获得的锁模式,取值从0到6, 对于行级别锁其实唯一的值是6,其他值都是对表级别锁的。REQUEST表示要求的锁,如果一个事务所要求的锁需要等待另一个事务释放,那么REQUEST可以看到这个事务需要请求什么类型的锁。

0 NO LOCK
1 NULL
2 RS (Row Share)
3 RX (Row Exclusive)
4 S (Share)
5 SRX (Share + Row Exclusive)
6 X (Exclusive)

这里这个行级别锁TX已经获得了在行row1上的排他锁。

如果在第二步执行之后,T2会被阻塞,你打开一个新的连接再一次执行select * from v$lock where type in ('TX','TM'),你就会看到多出来一个行级别锁TX

image

这个新多出来的行级别锁在row1上获得锁是0(无),要求锁是6(X),同时,刚才那个行级别锁的block标志位变成了1,表示有别的事务等待我释放这个锁。

然后如果你提交T1,T2获得锁,可以继续下去,相应的只剩下了T2的row1上的锁,而且LMODE从0变成了6, REQUEST从6变成了0

image

另外,这时候T1会自动在表级别也加一个锁,目的是防止事务过程中发生DDL操作。其中SID是session ID,可以通过V$session查看,ID1和ID2是rollback segment和transaction table entry.

表级锁

简称TM锁,可分为以下5种

RS: row share,意向锁,表示表内部分行有S锁了,部分行不许更改了

RX: row exclusive ,意向锁,表示表内部分行有变更,有X锁了

S: share ,整个表有S锁,这个表不可以更改

SRX: share + row exclusive,只有一个事务可以获得SRX锁,其他事务可以查询但是不可以更改

X: exclusive, 只有一个事务可以获得X表锁, 其他事务只能查询

RS,RX比较特殊,他们是DML操作产生的常见表级锁,他们都只是意向锁,真正的加锁粒度还是在行级,相当于某些行被锁之后,用这两个意向锁锁一下所属的表,数据库会比较容易判断行级别的锁情况。

从刚才的例子中可以看到,TX总是伴随着一个LMODE=3的TM锁,即RX锁,只要表中有行有X锁,表就会有RX锁。RX表示事务已经更改了某些行,比如insert/update/delete,获得了某些行X锁。RX不会阻塞RX锁,除非行锁上有阻塞。刚才的例子,如果两个事务更改的是两个记录,那么就不会互相阻塞。

如果执行了lock table test in row share mode你会看到LMODE=2(Row Share)的一个表级别锁。RS级别的锁只阻止X级别表锁,它不阻止RS, S等。

image

详细的锁之间的关系如下(相信大多数人就算是理解了也要要想半天的)image

SQL Server, Oracle 和 MySQL 事务隔离等级实现差别

基本概念

脏读:包含未提交数据的读。例如,事务1 更改了某行。事务2 在事务1 提交更改之前读取已更改的行。如果事务1 回滚更改,则事务2 便读取了逻辑上从未存在过的行。

不可重复读取:当某个事务不止一次读取同一行,并且一个单独的事务在两次(或多次)读取之间修改该行时。因为在同一个事务内的多次读取之间修改了该行,所以每次读取都生成不同值,从而引发不一致问题。

幻像:通过一个任务,在以前由另一个尚未提交其事务的任务读取的行的范围中插入新行或删除现有行。带有未提交事务的任务由于该范围中行数的更改而无法重复其原始读取。如果某个连接设置其事务隔离级别为可串行,则 SQL Server 使用键范围锁定以防止幻像。

SQL-92 定义了下列四种隔离级别

隔离级别

脏读

不可重复读取

幻像

未提交读

提交读

可重复读

可串行读

我们使用三个主流数据库测试,分别是Oracle 10g, SQL Server 2005和MySQL 5.1 (InnoDB only)

测试幻读

首先创建一个表test,并插入三条测试数据

CREATE TABLE test
(
c1 INTEGER,
c2 INTEGER
);

insert into test values(1,1);
insert into test values(2,2);
insert into test values(3,3);
commit;

SQL Server消除幻读测试
Steps T1 T2 备注
1

set transaction isolation level serializable;

begin transaction;

select * from test;


T1事务启动,设置隔离等级到Serializable, 然后查询一下test,会看到3条记录
2

begin transaction;

select * from test;
insert into test values(100, 100);
T2启动,插入一行并提交,这时候SQL Server会阻塞这个插入, 因为T1加了范围锁
3 select * from test;
commit;

T1第二次查询应该看到和第一次一样的三行,然后提交


commit; 只有T1提交之后,T2才可以插入




从sp_lock输出来看,在第一步之后,T1对test表加了S锁,所以T2可以选test的数据但是无法更改

Oracle消除幻读测试

Steps T1 T2 备注
1

set transaction isolation level serializable;

begin transaction;

select * from test;


T1事务启动,设置隔离等级到Serializable, 然后查询一下test,会看到3条记录
2

begin transaction;

select * from test;
insert into test values(100, 100);
commit;
T2启动,插入一行并提交,这时候Oracle不会阻塞这个插入,然后T2成功提交
3 select * from test;
commit;

T1第二次查询仍然看到和第一次一样的三行,然后提交
4 select * from test;
新事物开始,这时候T1才会看到刚才T2插入的记录




从V$lock来看,整个过程Oracle没有加锁,Oracle使用了SCN从回滚段重建出来当初时刻的数据提供一个snapshot 来保证T1不受T2的影响。

MySQL消除幻读测试

Steps T1 T2 备注
1

set transaction isolation level serializable;

start transaction;

select * from test;


T1事务启动,设置隔离等级到Serializable, 然后查询一下test,会看到3条记录
2

start transaction;

select * from test;
insert into test values(100, 100);
T2启动,插入一行并提交,这时候MySQL会阻塞这个插入, 因为T1加了范围锁
3 select * from test;
commit;

T1第二次查询应该看到和第一次一样的三行,然后提交


commit; 只有T1提交之后,T2才可以插入




和SQL Server一样的处理方式,但是MySQL也有snapshot, 效果和Oracle一样,语法是START TRANSACTION WITH CONSISTENT SNAPSHOT;从这个角度来说MySQL实现还是不错的,比SQL Server好一些。

测试不可重复读

SQL Server消除不可重复测试

Steps T1 T2 备注
1

set transaction isolation level repeatable read;

begin transaction;

select * from test;


T1事务启动,设置隔离等级到Serializable, 然后查询一下test,会看到3条记录
2

begin transaction;

select * from test;
update test set c2=200 where c1=2;
T2启动,更改一行,这时候SQL Server会阻塞这个更改, 因为T1在记录行上加了X和U锁 (排他更新锁)
3 select * from test;
commit;

T1第二次查询应该看到和第一次一样记录,然后提交


commit; 只有T1提交之后,T2才可以更新




Oracle消除不可重复测试

Oracle不支持REPEATABLE READ这个隔离等级,我们需要用更高的Serializable级别来测试

Steps T1 T2 备注
1

set transaction isolation level serializable;

begin transaction;

select * from test;


T1事务启动,设置隔离等级到Serializable, 然后查询一下test,会看到3条记录
2

begin transaction;

select * from test;
update test set c2=200 where c1=2;
commit;
T2启动,更改一行并提交,这时候Oracle不会阻塞这个更新,然后T2成功提交
3 select * from test;
commit;

T1第二次查询仍然看到和第一次一样的记录,然后提交
4 select * from test;
新事物开始,这时候T1才会看到刚才T2更改的记录




从V$lock来看,整个过程Oracle没有加锁,Oracle使用了SCN从回滚段重建出来当初时刻的数据提供一个snapshot 来保证T1不受T2的影响。

MySQL消除不可重复测试

Steps T1 T2 备注
1

set transaction isolation level serializable;

start transaction;

select * from test;


T1事务启动,设置隔离等级到Serializable, 然后查询一下test,会看到3条记录
2

start transaction;

select * from test;
update test set c2=200 where c1=2;
commit;
T2启动,更改一行并提交,这时候MySQL不会阻塞这个更新.
3 select * from test;
commit;

T1第二次查询应该看到和第一次一样的记录




对于不可重复读,MySQL和Oracle一样采取了类似快照的方式,有点出乎意料,这点上MySQL实现比SQL Server要好一些。

测试脏读

Read Committed可以消除脏读,由于比较简单,各个数据库实现差别不大,不会加特殊的锁,不再详细测试。这也是我们大多数情况下的数据库默认事务隔离等级。

总结

事务隔离等级不同的数据库实现有差别,有时候必须要清楚的了解之间的差别才能避免应用程序在多个数据库上兼容性和稳定性。

对于REPEATABLE READS,SQL Server设定隔离等级到REPEATABLE READ,对所有select过的数据加锁,阻止其他事务更新数据。但是其他事务可以插入幻读记录。而Oracle不直接支持这个等级,必须设定更高的SERIALIZABLE,Oracle使用SCN和Rollback segment重构snapshot,MySQL也是使用类似的snapshot,因次理论上讲SQL Server的这个隔离级别上并发性能比较差。

对于PHANTOM幻读,三种数据库都是设定到SERIALIZABLE等级,但是SQL Server是通过加范围锁阻塞其他事务的插入和更新,Oracle使用snapshot, MySQL两种方式都支持。

另外,由于SERIALIZABLE级别上,SQL Server使用的是范围锁,所以其他数据无法插入或者更新,而Oracle不会阻塞其他事务更新数据,Oracle假设大多数情况下多个事务不会更新同一条记录,但是如果其他更新的记录和当前事务碰巧修改了同一条记录,Oracel会通过乐观锁发现这种情况,并报错臭名昭著的ORA-8177 Cannot serialize access for this transaction. 所以Oracle的Serializable虽然性能高,但是不可以用于长时间的事务或者频繁的OLTP系统。如果有这样的需要,必须通过lock table实现。

尽管这几个数据库都实现了ACID但是各有千秋,实现跨数据库的应用的时候需要小心。还有就是,不管数据库本身事务隔离等级和锁的实现效率差别如何,关键还是良好的架构,不好的架构往往在单节点低压力测试的时候速度很快但是在高并发的多处理器或者集群上横向伸缩的时候性能下降很快,Facebook或者LiveJournal用一大堆MySQL照样跑的很好, 不比昂贵的Oracle RAC差,架构决定伸缩性,而不应该迷信数据库本身。