1. 前言
OceanBase数据库通过两阶段封锁机制确保读写事务并发控制的正确性。在高冲突场景下,事务处理中经常会遇到行锁冲突的问题。然而,许多OceanBase用户对于何时发生锁冲突,锁冲突的表现如何,以及如何排查锁冲突的原因,甚至于定位具体的行与锁冲突的会话。本文为读者提供一个系统的方法,帮助大家有效排查和解决锁冲突问题。
2. OceanBase 3.x & 4.1 锁冲突问题排查指北
以行锁为单位,可以抽象出与行锁有密切关系的两个对象:行锁的持有者,行锁的等待者。如果能够将这两个信息进行监控,那么对排查行锁的相关问题,将带来很大的帮助。本质来讲,行锁冲突都是由事务引起的。上述两个对象,都是隶属于事务,因此对活跃事务的监控也是必不可少的。基于以上分析,我们对不同的角色,分别实现了不同的虚拟表,用于展示上述信息,具体为:行锁持有者(__all_virtual_trans_lock_stat
),行锁等待者(__all_virtual_lock_wait_stat
),活跃事务(__all_virtual_trans_stat
)
2.1 活跃事务(__all_virtual_trans_stat)
OceanBase (root@oceanbase)> desc __all_virtual_trans_stat;
+---------------------------+---------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+---------------------------+---------------+------+-----+---------+-------+
| tenant_id | bigint(20) | NO | PRI | NULL | |
| svr_ip | varchar(32) | NO | PRI | NULL | |
| svr_port | bigint(20) | NO | PRI | NULL | |
| inc_num | bigint(20) | NO | PRI | NULL | |
| session_id | bigint(20) | NO | | NULL | |
| proxy_id | varchar(512) | NO | | NULL | |
| trans_type | bigint(20) | NO | | NULL | |
| trans_id | varchar(512) | NO | | NULL | |
| is_exiting | bigint(20) | NO | | NULL | |
| is_readonly | bigint(20) | NO | | NULL | |
| is_decided | bigint(20) | NO | | NULL | |
| active_memstore_version | varchar(64) | NO | | NULL | |
| partition | varchar(64) | NO | | NULL | |
| participants | varchar(1024) | NO | | NULL | |
| autocommit | bigint(20) | NO | | NULL | |
| trans_consistency | bigint(20) | NO | | NULL | |
| ctx_create_time | timestamp(6) | YES | | NULL | |
| expired_time | timestamp(6) | YES | | NULL | |
| refer | bigint(20) | NO | | NULL | |
| sql_no | bigint(20) | NO | | NULL | |
| state | bigint(20) | NO | | NULL | |
| part_trans_action | bigint(20) | NO | | NULL | |
| lock_for_read_retry_count | bigint(20) | NO | | NULL | |
+---------------------------+---------------+------+-----+---------+-------+
23 rows in set (0.01 sec)
该表中展示了该集群中所有参与者上下文的当前状态,关键参数说明如下:
svr_ip 表示该上下文创建的server地址;
session_id 表示该事务所对应session的唯一标识;
proxy_id, 表示客户端(proxy/java client)所对应的”ip:port“
trans_id, 表示事务的唯一标识
is_exiting, 当前的事务上下文是否正在退出
partition, 当前的事务上下文在哪个分区上创建
participants, 当前事务的参与者列表
ctx_create_time, 事务上下文创建的时间;
ref, 表示context当前的引用计数
sql_no, 表示当前context上最后一次执行sql的sql_no
state, 表示context当前的状态:INIT/PREPARE/COMMIT/ABORT/CLEAR,值域为0/1/2/3/4
part_trans_action, 表示当前context最后一次操作的动作:START_TASK/END_TASK/COMMIT
lock_for_read_retry_count 表示当前context在table scan过程中,是否遇到过锁冲突重试,值越大,表示冲突越严重。
2.2 行锁持有者(__all_virtual_trans_lock_stat)
OceanBase (root@oceanbase)> desc __all_virtual_trans_lock_stat;
+-----------------+--------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+-----------------+--------------+------+-----+---------+-------+
| tenant_id | bigint(20) | NO | PRI | NULL | |
| trans_id | varchar(512) | NO | PRI | NULL | |
| svr_ip | varchar(32) | NO | PRI | NULL | |
| svr_port | bigint(20) | NO | PRI | NULL | |
| partition | varchar(64) | NO | PRI | NULL | |
| rowkey | varchar(512) | NO | PRI | NULL | |
| session_id | bigint(20) | NO | | NULL | |
| proxy_id | varchar(512) | NO | | NULL | |
| ctx_create_time | timestamp(6) | YES | | NULL | |
| expired_time | timestamp(6) | YES | | NULL | |
+-----------------+--------------+------+-----+---------+-------+
10 rows in set (0.00 sec)
该表记录以行为单位,记录了当前集群所有活跃事务持有行的相关信息;关键参数说明如下:
rowkey,表示内部表示的行消息
session_id,proxy_id,trans_id等其他参数都是为了跟活跃事务对应起来。
2.3 行锁等待者(写锁)__all_virtual_lock_wait_stat
OceanBase (root@oceanbase)> desc __all_virtual_lock_wait_stat;
+-----------------+---------------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+-----------------+---------------------+------+-----+---------+-------+
| svr_ip | varchar(32) | NO | PRI | NULL | |
| svr_port | bigint(20) | NO | PRI | NULL | |
| table_id | bigint(20) | NO | PRI | NULL | |
| rowkey | varchar(512) | NO | PRI | NULL | |
| addr | bigint(20) unsigned | NO | PRI | NULL | |
| need_wait | tinyint(4) | NO | | NULL | |
| recv_ts | bigint(20) | NO | | NULL | |
| lock_ts | bigint(20) | NO | | NULL | |
| abs_timeout | bigint(20) | NO | | NULL | |
| try_lock_times | bigint(20) | NO | | NULL | |
| time_after_recv | bigint(20) | NO | | NULL | |
| session_id | bigint(20) | NO | | NULL | |
| block_session_id | bigint(20) | NO | | NULL | |
| type | bigint(20) | NO | | NULL | |
| lock_mode | bigint(20) | NO | | NULL | |
+-----------------+---------------------+------+-----+---------+-------+
12 rows in set (0.00 sec)
该表统计了当前集群中,所有正在等待行锁的请求/语句的相关信息,关键参数说明如下:
session_id,proxy_id,trans_id等其他参数都是为了跟活跃事务对应起来,不再一一赘述。
lock_ts,表示该请求开始等锁的时间点;(us)
abs_timeout,该语句的绝对超时时间(us)
try_lock_times,表示该语句曾经尝试过加锁的次数,值越大,表示锁冲突越严重
block_session_id, 表示第一个等在这个行上的事务的session
3. OB >= 4.2 锁冲突问题排查指北
随着 4.2 版本(G)V$OB_LOCKS视图的引入,锁冲突问题排查有了更加简单、直接的方法,我们基本实现了在一张视图内排查锁冲突问题。
3.1 (G)V$OB_LOCKS 介绍
字段 | 类型 | 是否为 NULL | 描述 |
SVR_IP | VARCHAR2(46) | NO | 持锁或请求锁的 OBServer 节点的 IP 地址 |
SVR_PORT | NUMBER(38) | NO | 持锁或请求锁的 OBServer 节点的端口号 |
TENANT_ID | NUMBER(38) | NO | 持锁或请求锁的租户的 ID |
TRANS_ID | NUMBER(38) | NO | 持锁或请求锁的事务 ID |
TYPE | VARCHAR2(9) | NO | 锁类型:TM:表锁TX:事务锁TR:行锁 |
ID1 | NUMBER | NO | 锁标识符 1:表锁:可能是 TABLE_ID,也可能是 TABLET_ID事务锁:TX_ID行锁:TABLET_ID |
ID2 | VARCHAR2(553) | NO | 锁标识符 2:表锁:NULL事务锁:NULL行锁:TX_ID + 行 rowkey |
LMODE | VARCHAR2(4) | NO | 当前持有锁的模式:NONE:表示未持有任何锁SS:ROW SHARESX:ROW EXCLUSIVES:SHARESSX:SHARE ROW EXCLUSIVEX:EXCLUSIVE |
REQUEST | VARCHAR2(8) | NO | 当前请求锁的模式:NONE:表示未请求任何锁SS:ROW SHARESX:ROW EXCLUSIVES:SHARESSX:SHARE ROW EXCLUSIVEX:EXCLUSIVE |
CTIME | NUMBER(38) | NO | 持有或等待锁的时间,单位为秒 |
BLOCK | NUMBER | NO | 表示当前事务请求的锁是否被其他事务持有:0:表示请求的锁未被其他事务持有,即该事务持有锁1:表示请求的锁被其他事务持有,即该事务被阻塞 |
锁视图中TYPE
列表示了锁的类型,目前 Oceanbase 主要展示以下 3 种类型的锁:
- 表锁(TM):即表 / 分区(tablet)上的锁。通常 insert / update / delete 等 dml 操作会产生分区级表锁锁,而 ddl 操作会产生表级表锁,这是由表锁的设计决定的。
- 行锁(TR):即写行时上的锁。在 Oceanbase 的实现中,并不存在通常意义上理解的狭义行锁(即支持读 / 写等多种模式的实际存在的锁结构),而是通过行上是否有活跃事务来表现。因此,行锁可以理解为事务锁的一种特例。
- 事务锁(TX):最常见的锁,通常可以理解为在事务内做过写操作即会产生事务锁。事务锁可以理解为行锁的退化形式,当无法维护事务到行的映射关系时,行锁即会转换成事务锁。
行锁和事务锁有着密切的关联,为了避免歧义和混乱,在(G)V$OB_LOCKS
的实际实现中,我们不区分行锁和事务锁的退化 / 特例关系,而是采用了全量维护的方式。这意味着,如果一个事务持有行锁,那么其一定也持有自己的事务锁。
3.2 锁冲突排查流程
由于(G)V$OB_LOCKS
目前不展示实际持锁的 session_id,因此以下的排查手段都只获取持锁的 trans_id。
- 应急场景
在获取到持锁的 trans_id 后,通过(G)V$OB_TRANSACTION_PARTICIPANTS
关联 trans_id 对应的 session_id,并通过kill session_id
回滚对应事务。
- 非应急场景
在获取到持锁的 trans_id 后,确认该事务为什么一直不解锁。不解锁的原因非常多样,可以通过 trans_id 在 observer.log 中过滤相关日志。常见的不解锁原因有以下几个:
- 长事务,即持锁事务本身很复杂,导致其长时间执行不结束。可通过持锁事务的 trans_id 过滤
__all_virtual_processlist
表,观察事务是否悬挂在某条 sql 请求上;或通过日志观察事务的对应 trace 中 slow query 相关的日志,定位哪条 sql 执行慢; - 锁冲突,即该事务也在等其他事务的锁。对于该问题可先通过
(G)V$OB_LOCKS
过滤该 trans_id,检查是否有BLOCK=1
的行,若有则进一步确认该持锁事务的不解锁原因,若无则通过 trans_id 过滤 observer.log 日志,确认悬挂过程中是否有其请求锁时报 -6005 报错(需要确认非该 trans_id 持锁报 -6005,例如该 trans_id 为 conflict_tx_id)。
说明:日志中-6x的错误码是事务相关的,其-6003/-6004/-6005和锁相关。
-6001:ERROR 6001 (25000): OB-6001:Transaction set changed during the execution:事务在执行过程中SET改变。事务回滚。
-6002: ERROR 6002 (40000): OB-6002:transaction is rolled back:事务被回滚。
-6003:ERROR 1205 (HY000): OB-1205:Lock wait timeout exceeded; try restarting transaction:锁等待时间已经超过超时时间,尝试重新启动事务。当前SQL失败,不改变事务状态,需要应用重试或做其他处理。
-6004: ERROR 6004 (HY000): OB-6004:Shared lock conflict:执行INSERT/UPDATE/DELETE时,行锁冲突。
-6005:ERROR 6005 (HY000): OB-6005: Trylock row conflict:获取行锁冲突。
-6210:ERROR (25000): OB-4012:Transaction is timeout:事务超时。需要明确发起回滚才可以继续复用当前连接。
-6211:ERROR 6211 (25000): OB-6002:Transaction is killed:事务被杀。数据库端事务会回滚。
-6213:ERROR 6002 (HY000): OB-6002:Transaction context does not exist:事务内容不存在。数据库端事务会回滚。
-6224: ERROR 6002 (25000): OB-6002:transaction need rollback:事务需要回滚。不需要客户端发起回滚,数据库端事务会回滚。
-6225:ERROR 4012 (25000): OB-4012:Transaction result is unknown:事务结果未知。数据库端事务可能已提交或者回滚。需要业务重试。
-6226: ERROR 1792 (25006): OB-1792:Cannot execute statement in a READ ONLY transaction:不能在只读事务中执行语句。
同时如果你的环境是通过ocp部署的,还可以通过里面的SQL诊断的TOP SQL页面过滤重试次数大于0的SQL,也可以初步筛查可能的SQL.
如果不是通过ocp部署的集群,也可以通过查询gv$sql_audit(4.x版本以后是gv$ob_sql_audit) 其中的retry_cnt字段数值大于0,是有可能发生了锁冲突的。是否真的发生了锁冲突,需要进一步通过日志来确认。在日志中出现-6003/-6004-6005的错误码则发生了锁冲突。日志分析推荐一个非常轻量的黑屏小工具obdiag。链接: OceanBase分布式数据库-海量数据 笔笔算数 , 安装配置好后可执行obdiag analyze log分析observer的日志,看是否有-6003/-6004-6005的错误日志。
4. 锁冲突排查思维导图
5. 王炸:obdiag 一键诊断锁冲突
obdiag官网文档参见: OceanBase分布式数据库-海量数据 笔笔算数
使用 obdiag rca 命令可帮助 OceanBase 数据库相关的诊断信息分析,目前支持对 OceanBase 的异常场景进行分析,找出可能导致问题的原因。
obdiag rca list # 列出所有的根因分析场景
obdiag rca run --scene=<scene_name> #执行具体场景的根因分析
scene_name
包含如下:
- disconnection:一键断连诊断,基于obproxy的诊断日志。
- major_hold: 一键卡合并诊断。
- lock_conflict: 一键锁冲突诊断。
示例:分析卡合并场景
obdiag rca run --scene=lock_conflict
6. 附录
- obdiag 官方文档: OceanBase分布式数据库-海量数据 笔笔算数
- obdiag github地址: GitHub - oceanbase/oceanbase-diagnostic-tool: OceanBase Diagnostic Tool is designed to help OceanBase users quickly gather necessary information and analyze the cause of the problem.
第一篇 | 如何修炼成“神医”——《OceanBase诊断系列》之一 |
第二篇 | 走进SQL审计视图——《OceanBase诊断系列》之二 |
第三篇 | 一键操作敏捷诊断工具obdiag收集诊断信息实践——《OceanBase诊断系列》之三 |
第四篇 | 一键操作敏捷诊断工具obdiag分析OB集群日志设计与实践——《OceanBase诊断系列》之四 |
第五篇 | 专为OceanBase打造的巡检工具已推出!给OceanBase进行一次体检吧——《OceanBase诊断系列》之五 |
第六篇 | obdiag帮你读懂全链路诊断日志——《OceanBase诊断系列》之六 |
第七篇 | 如何排查合并问题——《OceanBase诊断系列》之七 |
第八篇 | 一文教你轻松掌握锁冲突问题的排查方法——《OceanBase诊断系列》之八 |