H2是一个用Java开发的嵌入式数据库,这里指的嵌入式不是手持设备之类的,而是H2数据库作为一个类库,直接嵌入到上层的应用程序中,与应用运行在同一个进程中。

最大的优势在于可以同应用程序打包在一起发布,对于客户端应用来说,非常方便。比如说腾讯QQ或者Mozilla Firefox,用户不可能为了用个软件还得在自己机器上装个MySQL?SQL Server?上述软件就使用嵌入式数据库SQLite来进行客户端本地存储。H2的定位和SQLite一样,属于嵌入式数据库。(H2也可以用在Android上哦)

另一个优势是,写代码时需要写单元测试,与数据库操作相关的功能单元测试都比较不好做,因为有一个环境的问题,而采用H2来进行就要比MySQL要方便的多,首先是启动速度快,而且可以关闭持久化功能,表只存在内存中,每一个用例执行完自动还原到纯净环境。

现在很多开源产品的发布版中所附的测试用例,都是用的H2,目前大家都是把它用作测试。我感觉它的另一个用处是作为内存缓存,NoSQL的一个补充,当某些场景下数据模型必须为关系型,可以拿它当Memcached使,作为后端MySQL/Oracle的一个缓冲层,缓存一些不经常变化但需要频繁访问的数据,比如字典表、权限表。

H2使用非常简单,使用URL: jdbc:h2:~/test 来建立JDBC连接,就会自动创建一个test.h2.db文件和一个test.lock.db文件,前者就是用来存储数据的。只要这个Connection不断开,H2就始终处于运行状态。

H2支持3种运行模式:

1.嵌入式模式。H2运行在应用程序的进程中,执行效率会比较高,但由于不允许其他进程访问,管理起来麻烦点。


2.服务器模式。类似于MySQL那种C/S模型,H2运行在一个独立的进程中,应用程序通过TCP协议与其远程通信。优势就是管理方便,而且可以部署在不同的机器上,使用包括集群等特性。


3.混合模式。综合以上两种情况,由应用程序首先启动H2,这时对于应用来说H2工作在嵌入式模式,同时H2监听TCP某个端口,等待远程连接,这就是服务器模式,便于管理维护。


通常来说,混合模式比较实用。使用jdbc:h2:~/test;AUTO_SERVER=TRUE来建立JDBC连接,就可以开启混合模式。

还可以关闭持久化功能,所有数据都保存在内存中,效率很高。URL:jdbc:h2:mem:test ,同样可以开启混合模式允许远程访问。

对于多版本并发,默认的实现是基于表锁的,读操作加共享锁,写操作加互斥锁,(也就是写会阻塞读)等待锁超时会抛出异常。可以开启MVCC (Multi-Version Concurrent Control) 多版本并发控制模式,URL为jdbc:h2:~/test;MVCC=TRUE,开启MVCC模式后,所有操作都基于行锁,只能看到已提交的数据,写不阻塞读。

举个例子来说明下这个问题:


// 事务1
Connection conn1 = DriverManager.getConnection(jdbcUrl, user, passwd);
conn1.setAutoCommit(false); // 开启事务

ResultSet rs1 = conn1.createStatement().executeQuery(
"select name from students where id = 1 limit 1"); // 事务中读
if (rs1.next())
System.out.println("1. " + rs1.getString("name")); // 修改前

conn1.createStatement().execute(
"update students set name = 'lisi' where id = 1");
// 写操作 注意!完成后故意不提交

ResultSet rs2 = conn1.createStatement().executeQuery(
"select name from students where id = 1 limit 1"); // 事务中读
if (rs2.next())
System.out.println("2. " + rs2.getString("name")); // 修改后

// 事务2
Connection conn2 = DriverManager.getConnection(jdbcUrl, user, passwd);
conn2.setAutoCommit(false); // 开启事务

ResultSet rs3 = conn2.createStatement().executeQuery(
"select name from students where id = 1"); // 事务外读
if (rs3.next())
System.out.println("3. " + rs3.getString("name")); // ?
这个实验的流程是这样的:首先开启一个事务1,读取初始值,修改它,再重新读取这个值。保持事务1没有提交的情况下,在事务2中读取这个值。

如果使用默认模式,则是基于表锁,写阻塞读,事务1的3个操作顺利完成,但事务1没有提交,写锁没有释放。这个时候事务2中会读取这个值,发现数据被锁了,等待一小会后发现仍然无法获取锁,于是第24行抛出异常,控制台打印信息如下:

1. zhangsan
2. lisi
Exception in thread "main" org.h2.jdbc.JdbcSQLException:
Timeout trying to lock table "STUDENTS"; SQL statement:
select name from students where id = 1 [50200-157]
如果开启了MVCC模式,写操作就不会阻塞读操作了,任何时间点只能读到已提交数据。第24行代码正常执行,因为事务1没有提交,所以只能读取到事务1修改前的值,控制台打印信息如下:

1. zhangsan
2. lisi
3. zhangsan
如果应用有并发请求的话,建议开启MVCC模式。