作者简介:靖顺,OcenaBase 开发工程师,专注于数据库诊断与调优   

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_IPVARCHAR2(46)NO持锁或请求锁的 OBServer 节点的 IP 地址
SVR_PORTNUMBER(38)NO持锁或请求锁的 OBServer 节点的端口号
TENANT_IDNUMBER(38)NO持锁或请求锁的租户的 ID
TRANS_IDNUMBER(38)NO持锁或请求锁的事务 ID
TYPEVARCHAR2(9)NO锁类型:TM:表锁TX:事务锁TR:行锁
ID1NUMBERNO锁标识符 1:表锁:可能是 TABLE_ID,也可能是 TABLET_ID事务锁:TX_ID行锁:TABLET_ID
ID2VARCHAR2(553)NO锁标识符 2:表锁:NULL事务锁:NULL行锁:TX_ID + 行 rowkey
LMODEVARCHAR2(4)NO当前持有锁的模式:NONE:表示未持有任何锁SS:ROW SHARESX:ROW EXCLUSIVES:SHARESSX:SHARE ROW EXCLUSIVEX:EXCLUSIVE
REQUESTVARCHAR2(8)NO当前请求锁的模式:NONE:表示未请求任何锁SS:ROW SHARESX:ROW EXCLUSIVES:SHARESSX:SHARE ROW EXCLUSIVEX:EXCLUSIVE
CTIMENUMBER(38)NO持有或等待锁的时间,单位为秒
BLOCKNUMBERNO表示当前事务请求的锁是否被其他事务持有: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 中过滤相关日志。常见的不解锁原因有以下几个:

  1. 长事务,即持锁事务本身很复杂,导致其长时间执行不结束。可通过持锁事务的 trans_id 过滤__all_virtual_processlist表,观察事务是否悬挂在某条 sql 请求上;或通过日志观察事务的对应 trace 中 slow query 相关的日志,定位哪条 sql 执行慢;
  2. 锁冲突,即该事务也在等其他事务的锁。对于该问题可先通过(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.

1709171343

如果不是通过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. 锁冲突排查思维导图

1709137633

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. 附录

第一篇如何修炼成“神医”——《OceanBase诊断系列》之一
第二篇走进SQL审计视图——《OceanBase诊断系列》之二
第三篇一键操作敏捷诊断工具obdiag收集诊断信息实践——《OceanBase诊断系列》之三
第四篇一键操作敏捷诊断工具obdiag分析OB集群日志设计与实践——《OceanBase诊断系列》之四
第五篇专为OceanBase打造的巡检工具已推出!给OceanBase进行一次体检吧——《OceanBase诊断系列》之五
第六篇obdiag帮你读懂全链路诊断日志——《OceanBase诊断系列》之六
第七篇如何排查合并问题——《OceanBase诊断系列》之七
第八篇一文教你轻松掌握锁冲突问题的排查方法——《OceanBase诊断系列》之八

Logo

了解最新的技术洞察和前沿趋势,参与 OceanBase 定期举办的线下活动,与行业开发者互动交流

更多推荐