文章基于 RegularTable 来分析和拆解更新操作。
PageStore 存储引擎默认不开启 MVCC,锁模型比较简单,方便了解更新的整个流程。
主要涉及读写锁(事务隔离),数据更新的实现、事务的提交和回滚。
相关概念
讨论更新操作,就需要涉及到事务隔离级别以及事务的概念。
也就是讨论如何控制并发读写的问题、以及undolog 的问题。
①MVCC
multi version concurrency。在 h2database 实现中,默认 MVStore 存储引擎支持该特性。
为了简化事务实现模型,只关注非 MVCC 模式。 MVCC 实现原理参考《Insight h2database MVCC 实现原理》。
/**
* Check if multi version concurrency is enabled for this database.
* 使用 PageStore 存储引擎时,使用 MVCC=true 开启。
* @see org.h2.engine.Database#isMultiVersion
*/
public boolean isMultiVersion() {
// this.multiVersion = ci.getProperty("MVCC", dbSettings.mvStore);
return multiVersion;
}
/**
* 通过设置或者版本确定是否启用 MVStore 存储引擎
* @see org.h2.engine.DbSettings#mvStore
*/
public boolean mvStore = get("MV_STORE", Constants.VERSION_MINOR >= 4);
②事务隔离级别
the isolation level. 在 h2database 中,通过 LOCK_MODE 体现。不同的锁定模式决定了事务级别。参考命令 SET LOCK_MODE int。
-
SET LOCK_MODE 命令是数据库级别的,影响全局(affects all connections)。
-
默认的事务隔离级别为 READ_COMMITTED。MVStore 存储引擎默认支持。
-
对于 RegularTable 只存在三种级别:READ_UNCOMMITTED, READ_COMMITTED, SERIALIZABLE(默认)。
-
READ_UNCOMMITTED,即无锁定模式(仅用于测试)
-
READ_COMMITTED, 避免了脏读,相比于 SERIALIZABLE,并发性能更好,事务的读写操作不阻塞。开启 MVCC 模式即可。
-
SERIALIZABLE,不同事务(session)读写互斥。可以防止脏读、不可重复读和幻读,但是效率较低,因为它会锁定所涉及的全部表,直到整个事务完成。
RegularTable 表级独占锁
更新流程中,首先会调用 table.lock(session, exclusive = true, false);
在 RegularTable 中,表会按照 session 粒度控制并发度。这个方法只能当前 session 可重入,其他 session 想 lock 成功,需要等待当前会话释放锁。
①独占锁示例
-- session 1 更新数据并持有锁
SET AUTOCOMMIT OFF;
update city set code = 'bjx' where id = 9;
-- session 2 获取锁超时,异常
select * from city where id = 5;
Timeout trying to lock table "CITY"; SQL statement:
select * from city where id = 5 [50200-184] HYT00/50200
②独占锁实现
独占锁是个 Java 经典的多线程同步案例。同时包含了死锁检测的解决方案。
/**
* 通过会话,给表加锁。
* 如果要加写锁,会存在等待锁的情况。
* 如果发生锁超时,将抛出DbException异常。如上示例。
* @param session 当前会话
* @param exclusive 如果为true,表示需要写锁;如果为false,表示需要读锁。写锁是排他的,即在同一时间只能有一个线程持有写锁。读锁是共享的,即在同一时间可以有多个线程持有读锁。
* @see org.h2.table.RegularTable#lock
*/