首批通过分布式安全可靠测评,为关键业务系统打造
锁机制
更新时间:2026-06-28 10:22:11
OceanBase 数据库使用了多版本两阶段锁来维护其并发控制模型的正确性,锁机制是保证正确的数据并发性和一致性很重要的一点。
OceanBase 数据库的锁机制使用了以数据行为级别的锁粒度。同一行不同列之间的修改会导致同一把锁上的互斥;而不同行的修改是不同的两把锁,因此是无关的。类似于其余的多版本两阶段锁的数据库,OceanBase 数据库的读取是不上锁的,因此可以做到读写不互斥从而提高用户读写事务的并发能力。对于锁的存储模式,选择将锁存储在行上(可能存储在内存与磁盘上)从而避免在内存中维护大量锁的数据结构。其次,会在内存中维护锁之间的等待关系,从而在锁释放的时候唤醒等待在锁上面的其余事务。
注意
SELECT ... FOR UPDATE无法做到读写不互斥。- 在事务提交过程中,为了维护事务的一致性快照,会有短暂的读写互斥,我们称之为 lock for read。
OceanBase 数据库锁机制的使用
在深入之前,我们先来看一下如何使用 OceanBase 数据库的行锁能力。如下所示是一个很常见的业务 SQL 来更新货物的信息。
UPDATE GOODS
SET PRICE = ?, AMOUNT = ?
WHERE GOOD_ID = ?
AND LOCATION = ?;
在上述 SQL 中,根据用户填入的货物 ID 和 地址,去更新对应的价格和存量。对应事务中的一个 SQL,在事务结束前,对应货物 ID 和 地址的数据的一行会被加上行锁,所有并发的更新都会被阻塞并等待。从而预防并发的修改导致的脏写(Dirty Write)。由此可见用户在更新数据的同时,隐式地为修改的数据行上加上了对应的锁,用户无需显示地指示锁的范围等的情况下, 就可以依赖 OceanBase 数据库内部的机制做到并发控制的效果。
当然用户可以显式地指定使用锁机制。如下所示是一个很常见的业务 SQL 来互斥地获取货物的信息。
SELECT PRICE = ?, AMOUNT = ?
FROM GOODS
WHERE GOOD_ID = ?
AND LOCATION = ?
FOR UPDATE;
在上述 SQL 中,根据用户填入的货物 ID 和 地址,去获取对应的价格和存量。对应事务中的一个 SQL,在事务结束前,对应货物 ID 和地址的数据的一行会被加上行锁,所有并发的更新都会被阻塞并等待。从而做到用户指定的显示加锁。在不同的业务需求下,是极其重要的一点。
锁机制的粒度
OceanBase 数据库现在不支持表锁,只支持行锁,且只存在互斥行锁。传统数据库中的表锁主要是用来实现一些较为复杂的 DDL 操作,在 OceanBase 数据库中,还未支持一些极度依赖表锁的复杂 DDL,而其余 DDL 通过在线 DDL 变更来实现。
在更新同一行的不同列时,事务依旧会互相阻塞,如此选择的原因是为了减小锁数据结构在行上的存储开销。而更新不同行时,事务之间不会有任何影响。
锁机制的互斥
OceanBase 数据库使用了多版本两阶段锁,事务的修改每次并不是原地修改,而是产生新的版本。因此读取可以通过一致性快照获取旧版本的数据,因而不需要行锁依旧可以维护对应的并发控制能力,因此能做到执行中的读写不互斥,这极大提升了 OceanBase 数据库的并发能力。比较特殊的是 SELECT ... FOR UPDATE,此类执行依旧会加上行锁,并与修改或 SELECT ... FOR UPDATE 产生互斥与等待。而修改操作则会与所以需要获取行锁的操作产生互斥。
锁机制的存储
OceanBase 数据库的锁存储在行上,从而减少内存中所需要维护的锁数据结构带来的开销。在内存中,当事务获取到行锁时,会在对应的行上设置对应的事务标记,即行锁持有者。当事务尝试获取行锁时,会通过对应的事务标记发现自己不是行锁持有者而放弃并等待或发现自己是行锁持有者后获得行的使用能力。当事务释放行锁后,就会在所有事务涉及的行上解除对应的事务标记,从而允许之后的事务继续尝试获取。
当数据被转储当 SSTable 后,在宏块内部的数据上,记录着对应的事务标记。其余事务依旧需要通过事务标识来辨识是否可以允许访问对应的数据。与内存中的锁机制不同的是,由于 SSTable 不可变的特性,无法在事务释放行锁后,立即清除宏块内部的数据上的事务标记。当然依旧可以通过事务标识来确认找到对应的事务信息来确认事务是否已经解锁。
锁机制的释放
类似于大部分的两阶段锁实现,OceanBase 数据库的锁在事务结束(提交或回滚)的时候释放的,从而避免数据不一致性的影响。OceanBase 数据库还存在其余的释放时机,即 SAVEPOINT,当用户选择回滚至 SAVEPOINT 后,事务内部会将 SAVEPOINT 及之后所有涉及数据的行锁,全部根据本文 锁机制的互斥 中介绍的机制进行释放。
锁机制的唤醒
为了唤醒事务,当产生互斥后,会在内存中维护行与事务的等待关系,如图所示,行 A 被事务 B 持有,被事务 C 与事务 D 等待。此等待关系的维护,是为了行锁释放的时候可以唤醒对应的事务 C 与 D。当事务 B 释放行 A 后,会根据顺序唤醒事务 C,并依赖事务 C 唤醒事务 D。

除了行与事务的等待关系,OceanBase 数据库可能会维护事务与事务的等待关系。为了减小对于内存的占用,OceanBase 数据库内部可能会将行与事务的等待关系转换为事务与事务的等待关系。如图所示,行 A 被事务 B 持有,被事务 C 与事务 D 等待,并被转换为事务 B 被事务 C 与事务 D 等待。当事务 B 结束后,由于不明确知道行之间的锁等待关系,会同时唤醒事务 C 与事务 D。

锁机制的死锁
锁机制的实现会导致死锁,死锁是指对于资源的循环依赖,举例来说,当事务 A 与事务 B 同时获取资源 C 与 D 的情况下,若事务 A 优先获取到资源 C 并去获取资源 D;而事务 B 优先获取到资源 D 并去获取资源 C。此时若没有任何人愿意放弃自己已经获取到的资源,就没有事务可以正常结束。
基于超时的死锁解决
OceanBase 数据库当前主要依赖超时回滚机制来解决业务逻辑上的死锁。
存在三种超时机制用来解决对应的问题:
锁超时机制:配置项名称为
ob_trx_lock_timeout,默认为语句超时时间,若加锁等待超过锁超时时间,则会回滚对应的语句,并返回锁超时对应的错误码。此时,由于某一个循环依赖中的资源依赖已经消失,因此就不再存在死锁。以事务 B 获取资源 C 超时为例,只要事务 B 结束,则事务 A 就可以获取到对应的资源 D。语句超时机制:配置项名称为
ob_query_timeout,默认为10s,若加锁等待超过语句超时时间,则会回滚对应的语句,并返回语句超时对应的错误码。此时,由于某一个循环依赖中的资源依赖已经消失,因此就不再存在死锁。以事务 B 获取资源 C 超时为例,只要事务 B 结束,则事务 A 就可以获取到对应的资源 D。事务超时机制:配置项名称为
ob_trx_timeout,默认为100s,若加锁等待超过事务超时时间,则会回滚对应的事务,并返回语句事务对应的错误码。由于某一个循环依赖中的资源依赖已经消失,因此就不再存在死锁。以事务 B 超时为例,由于事务 B 结束,事务 A 就可以获取到对应的资源 D。