openGauss 中的锁机制及死锁诊断与处理

2024年 7月 22日 77.5k 0

引言

openGauss 是华为开源的一款关系型数据库管理系统,其锁机制与 PostgreSQL 类似。以下是一些openGauss中的锁类型定义、冲突关系以及测试用例。这些测试用例可以帮助您在openGauss中重现各种锁的情况。

1.锁类型定义

在 openGauss 中,pg_locks 表中的锁类型包括表级锁和行级锁:

  • 表级锁:包括 relation、page、transactionid、object、userlock、advisory,这些锁用于锁定整个表、特定页、事务、数据库对象或应用程序定义的资源。
  • 行级锁:包括 tuple 和 virtualxid,这些锁用于锁定表中的特定行或当前活跃的虚拟事务。

要获得当前数据库中的所有锁类型及其相关信息,可以通过查询 pg_locks 表:

SELECT locktype, relation::regclass, tuple, virtualxid, transactionid, mode, granted
FROM pg_locks;

通过检查 locktype 字段来区分哪些是表级锁(如 relation、page、transactionid、object、userlock、advisory)、行级锁(如 tuple 和 virtualxid)。

2.8种主要锁

下面将讲解八种主要的锁类型,这些锁在不同数据库操作中扮演着关键角色:

  • AccessShareLock 允许事务执行 SELECT 查询,这种锁允许其他事务同时读取相同的数据,但不允许修改数据结构。它的主要用途是在进行读取操作时确保数据的稳定性,防止数据在读取过程中被修改。与 AccessExclusiveLock 冲突。

  • RowShareLock 允许多个事务读取并共享锁,但不允许对表结构进行修改。它通常用于 SELECT FOR UPDATESELECT FOR SHARE 操作,以确保读取的数据在事务完成之前不会被其他事务修改。与 ExclusiveLockAccessExclusiveLock 冲突。

  • RowExclusiveLock 允许多个事务读取表,但不允许同时修改相同的数据。它在 INSERTUPDATEDELETE 操作中被使用,以确保数据的一致性和防止数据竞争。与 ShareLockShareRowExclusiveLockExclusiveLockAccessExclusiveLock 冲突。

  • ShareUpdateExclusiveLock 用于防止表在读取时被其他事务修改,常用于 VACUUM(非 FULL)、ANALYZE 和并发创建索引操作。此锁确保在维护操作期间数据不会被修改,从而保持数据的完整性。与自身、ShareLockShareRowExclusiveLockExclusiveLockAccessExclusiveLock 冲突。

  • ShareLock 允许多个事务读取表,但不允许对表进行修改。它在非并发创建索引操作中被使用,以确保在索引创建过程中数据的稳定性。与 RowExclusiveLockShareUpdateExclusiveLockShareRowExclusiveLockExclusiveLockAccessExclusiveLock 冲突。

  • ShareRowExclusiveLock 类似于排他锁,但允许行共享。这种锁用于防止其他事务读取和修改表,同时允许行级别的共享锁。它通常用于复杂的操作,以确保数据的完整性。与 RowExclusiveLockShareUpdateExclusiveLockShareLockExclusiveLockAccessExclusiveLock 冲突。

  • ExclusiveLock 防止其他事务读取和修改表。它阻止行共享和 SELECT...FOR UPDATE 操作,以确保在关键操作期间数据的排他性访问。与 RowShareLockRowExclusiveLockShareUpdateExclusiveLockShareLockShareRowExclusiveLockAccessExclusiveLock 冲突。

  • AccessExclusiveLock 阻止所有其他类型的锁,包括 SELECT。这种锁用于更改表结构的操作,例如 ALTER TABLEDROP TABLEVACUUM FULL,确保在结构性变更期间没有其他事务访问该表。与所有其他锁类型冲突。

3. 冲突关系

表级锁用于锁定整个表、特定页、事务、数据库对象或应用程序定义的资源;行级锁用于锁定特定行或当前活跃的虚拟事务。理解这些锁类型及其冲突关系,有助于优化数据库性能和确保数据一致性,如下为整理好各个锁冲突关系表可以作为参考:

编号 锁名称 锁用途 冲突关系
1 AccessShareLock SELECT 8
2 RowShareLock SELECT FOR UPDATE/FOR SHARE 7|8
3 RowExclusiveLock INSERT, UPDATE, DELETE 5|6|7|8
4 ShareUpdateExclusiveLock VACUUM (non-FULL), ANALYZE, CREATE INDEX CONCURRENTLY 4|5|6|7|8
5 ShareLock CREATE INDEX (WITHOUT CONCURRENTLY) 3|4|6|7|8
6 ShareRowExclusiveLock Like EXCLUSIVE MODE, but allows ROW SHARE 3|4|5|7|8
7 ExclusiveLock Blocks ROW SHARE/SELECT…FOR UPDATE 2|3|4|5|6|8
8 AccessExclusiveLock ALTER TABLE, DROP TABLE, VACUUM FULL, and unqualified 1|2|3|4|5|6|7|8

4.测试用例

首先,创建测试表并插入一些数据:

CREATE TABLE user_test (
id SERIAL PRIMARY KEY,
user_name varchar(16)
);

INSERT INTO user_test (user_name) VALUES ('jimfad'), ('farcde'), ('jack');

4.1. AccessShareLock

测试用例:使用 SELECT 语句。

-- Session 1
BEGIN;
SELECT * FROM user_test WHERE id = 1;
-- Hold the lock

-- Session 2
BEGIN;
SELECT * FROM user_test WHERE id = 1;
-- 不应该阻塞,因为AccessShareLock允许并发读取

4.2. RowShareLock

测试用例:使用 SELECT FOR UPDATESELECT FOR SHARE

-- Session 1
BEGIN;
SELECT * FROM user_test WHERE id = 1 FOR UPDATE;
-- Hold the lock

-- Session 2
BEGIN;
SELECT * FROM user_test WHERE id = 1 FOR UPDATE;
-- Should block since RowShareLock conflicts with another RowShareLock on the same row

-- Session 2 alternative
BEGIN;
SELECT * FROM user_test WHERE id = 1 FOR SHARE;
-- Should block since RowShareLock conflicts with another RowShareLock on the same row

4.3. RowExclusiveLock

测试用例:使用 INSERTUPDATEDELETE

-- Session 1
BEGIN;
UPDATE user_test SET user_name = 'jack wang' WHERE id = 1;
-- Hold the lock

-- Session 2
BEGIN;
INSERT INTO user_test (id, user_name) VALUES (2, 100);
-- Should block since RowExclusiveLock conflicts with another RowExclusiveLock

-- Session 2 alternative
BEGIN;
DELETE FROM user_test WHERE id = 2;
-- Should block since RowExclusiveLock conflicts with another RowExclusiveLock

4.4. ShareUpdateExclusiveLock

测试用例:使用 VACUUM(非 FULL)、ANALYZE 或并发创建索引。

-- Session 1
BEGIN;
VACUUM (ANALYZE) user_test;
-- Hold the lock

-- Session 2
BEGIN;
CREATE INDEX CONCURRENTLY idx_test_table_value ON test_table (user_name);
-- Should block since ShareUpdateExclusiveLock conflicts with another ShareUpdateExclusiveLock

4.5. ShareLock

测试用例:使用非并发创建索引。

-- Session 1
BEGIN;
CREATE INDEX idx_test_table_value ON user_test (user_name);
-- Hold the lock

-- Session 2
BEGIN;
SELECT * FROM user_test WHERE user_name = 'jack wang';
-- Should block since ShareLock conflicts with RowExclusiveLock

4.6. ShareRowExclusiveLock

测试用例:类似于排他锁,但允许行共享。

-- Session 1
BEGIN;
LOCK TABLE user_test IN SHARE ROW EXCLUSIVE MODE;
-- Hold the lock

-- Session 2
BEGIN;
SELECT * FROM user_test WHERE id = 1 FOR UPDATE;
-- Should block since ShareRowExclusiveLock conflicts with another ShareRowExclusiveLock

4.7. ExclusiveLock

测试用例:阻止行共享和 SELECT...FOR UPDATE

-- Session 1
BEGIN;
LOCK TABLE user_test IN EXCLUSIVE MODE;
-- Hold the lock

-- Session 2
BEGIN;
SELECT * FROM user_test WHERE id = 1 FOR UPDATE;
-- Should block since ExclusiveLock conflicts with RowShareLock and others

4.8. AccessExclusiveLock

测试用例:用于更改表结构。

-- Session 1
BEGIN;
ALTER TABLE user_test ADD COLUMN new_column INTEGER;
-- Hold the lock

-- Session 2
BEGIN;
SELECT * FROM user_test;
-- Should block since AccessExclusiveLock conflicts with all other locks

每个测试用例应在不同的会话(Session)中执行。可以使用 openGauss 的客户端gsql工具或其他数据库管理工具打开多个连接会话来测试上述用例。

5.死锁

死锁是指在数据库系统中,两个或多个事务在互相等待对方释放锁资源,从而导致这几个事务都无法继续执行的现象。简单来说,死锁是一种事务之间的循环等待状态,导致所有相关事务都被永久阻塞,无法继续执行,下图详细描述了死锁产生的原因:
openGauss 中的锁机制及死锁诊断与处理-1

5.1 如何诊断死锁?

在openGauss当中,提供了如下两个视图用户诊断数据库是否产生了死锁:

  • pg_locks: 显示锁统计信息,每条数据记录代表一个持有锁或正在等待锁的进程。
  • pg_stat_activity: 显示会话统计信息,每个数据记录代表一个会话。

执行以下语句创建v_locks_monitor视图,并查询持有锁或正在等待锁的进程:

create view v_locks_monitor as
with
t_wait as
(
select a.mode,a.locktype,a.database,a.relation,a.page,a.tuple,a.classid,a.granted,
a.objid,a.objsubid,a.pid,a.transactionid,
b.xact_start,b.query_start,b.usename,b.datname,b.client_addr,b.client_port,b.application_name
from pg_locks a,pg_stat_activity b where a.pid=b.pid and not a.granted
),
t_run as
(
select a.mode,a.locktype,a.database,a.relation,a.page,a.tuple,a.classid,a.granted,
a.objid,a.objsubid,a.pid,a.transactionid,
b.xact_start,b.query_start,b.usename,b.datname,b.client_addr,b.client_port,b.application_name
from pg_locks a,pg_stat_activity b where a.pid=b.pid and a.granted
),
t_overlap as
(
select r.* from t_wait w join t_run r on
(
r.locktype is not distinct from w.locktype and
r.database is not distinct from w.database and
r.relation is not distinct from w.relation and
r.page is not distinct from w.page and
r.tuple is not distinct from w.tuple and

r.transactionid is not distinct from w.transactionid and
r.classid is not distinct from w.classid and
r.objid is not distinct from w.objid and
r.objsubid is not distinct from w.objsubid and
r.pid w.pid
)
),
t_unionall as
(
select r.* from t_overlap r
union all
select w.* from t_wait w
)
select locktype,datname,relation::regclass,page,tuple,transactionid::text,classid::regclass,objid,objsubid,
string_agg(
'Pid: '||case when pid is null then 'NULL' else pid::text end||chr(10)||
'Lock_Granted: '||case when granted is null then 'NULL' else granted::text end||
' , Mode: '||case when mode is null then 'NULL' else mode::text end||
' , Username: '||case when usename is null then 'NULL' else usename::text end||
' , Database: '||case when datname is null then 'NULL' else datname::text end||
' , Client_Addr: '||case when client_addr is null then 'NULL' else client_addr::text end||
' , Client_Port: '||case when client_port is null then 'NULL' else client_port::text end||
' , Application_Name: '||case when application_name is null then 'NULL' else application_name::text end||chr(10)||
' , Xact_Start: '||case when xact_start is null then 'NULL' else xact_start::text end||
' , Query_Start: '||case when query_start is null then 'NULL' else query_start::text end||
' , Xact_Elapse: '||case when (now()-xact_start) is null then 'NULL' else (now()-xact_start)::text end||
chr(10)||'--------'||chr(10), (case when granted then '0' else '1' end)
order by
( case mode
when 'INVALID' then 0
when 'AccessShareLock' then 1
when 'RowShareLock' then 2
when 'RowExclusiveLock' then 3
when 'ShareUpdateExclusiveLock' then 4
when 'ShareLock' then 5
when 'ShareRowExclusiveLock' then 6
when 'ExclusiveLock' then 7
when 'AccessExclusiveLock' then 8
else 0
end ) desc )
as lock_conflict
from t_unionall
group by
locktype,datname,relation,page,tuple,transactionid::text,classid,objid,objsubid;

然后通过查询v_locks_monitor视图诊断出当前数据库是否存在死锁:

postgres=# select * from v_locks_monitor;

5.2 如何处理死锁?

从v_locks_monitor视图中,您可以获得数据库系统中的锁。执行以下语句终止用于运行触发锁的事务的进程:

postgres=# select pg_terminate_backend(PID);

下面我们讲模拟死锁以及如何解决死锁的过程。
模拟死锁和解锁过程可以帮助理解事务之间如何产生冲突,以及如何解决这些冲突。以下是一个详细的示例,包括如何在 openGauss 中模拟死锁,以及如何检测和解决死锁。

5.3模拟死锁

5.3.1. 创建测试表并插入数据

CREATE TABLE user_test (
id SERIAL PRIMARY KEY,
user_name varchar(16)
);
INSERT INTO user_test (user_name) VALUES ('jimfad'), ('farcde'), ('jack');

5.3.2. 模拟死锁

会话 1(Session 1)

-- 开启第一个事务
BEGIN;
-- 锁定 id 为 1 的行
SELECT * FROM user_test WHERE id = 1 FOR UPDATE;

会话 2(Session 2)

-- 开启第二个事务
BEGIN;
-- 锁定 id 为 2 的行
SELECT * FROM user_test WHERE id = 2 FOR UPDATE;

会话 1(Session 1)

-- 尝试锁定 id 为 2 的行,此时会等待,因为会话 2 已经锁定了该行
SELECT * FROM user_test WHERE id = 2 FOR UPDATE;

会话 2(Session 2)

-- 尝试锁定 id 为 1 的行,此时会产生死锁,因为会话 1 已经锁定了该行
SELECT * FROM user_test WHERE id = 1 FOR UPDATE;

5.3.3. 查询死锁

通过查询v_locks_monitor视图诊断死锁:

SELECT * FROM v_locks_monitor;

得到如下信息:

openGauss=# SELECT * FROM v_locks_monitor;
locktype | datname | relation | page | tuple | transactionid | classid | objid | objsubid | lock_conflict

---------------+----------+----------+------+-------+---------------+---------+-------+----------+-------------------------------------------------------------------------------------------------------------
---------------------------------
transactionid | postgres | | | | 26010 | | | | Pid: 139928107087616
+
| | | | | | | | | Lock_Granted: true , Mode: ExclusiveLock , Username: omm , Database: postgres , Client_Addr: NULL , Client_P
ort: -1 , Application_Name: gsql+
| | | | | | | | | , Xact_Start: 2024-07-12 10:25:44.188946+08 , Query_Start: 2024-07-12 10:26:20.846636+08 , Xact_Elapse: 00:
01:02.347009 +
| | | | | | | | | --------
+
| | | | | | | | | 1Pid: 139928273024768
+
| | | | | | | | | Lock_Granted: false , Mode: ShareLock , Username: omm , Database: postgres , Client_Addr: NULL , Client_Port
: -1 , Application_Name: gsql +
| | | | | | | | | , Xact_Start: 2024-07-12 10:25:59.710104+08 , Query_Start: 2024-07-12 10:26:29.295124+08 , Xact_Elapse: 00:
00:46.825851 +
| | | | | | | | | --------
+
| | | | | | | | |
(1 row)

5.3.4. 解除死锁

要解除死锁,可以终止一个或多个事务。下面是一个示例,显示如何终止用于运行触发锁的事务的进程:

-- 终止会话 2 的进程
SELECT pg_terminate_backend(139928107087616);

v_locks_monitor 视图中获取到 PID 后,通过 pg_terminate_backend 函数结束当前死锁进程。
通过这些步骤,您可以模拟死锁情况并学习如何检测和解决死锁问题。

总结

在 openGauss 数据库中,锁机制是确保数据一致性和完整性的重要手段。本文详细介绍了八种主要的锁类型及其用途和冲突关系,帮助读者理解不同锁在数据库操作中的关键作用。通过示例代码,展示了如何在 pg_locks 表中查询锁信息,以及如何模拟各种锁的使用场景。此外,还提供了模拟死锁的具体步骤,并介绍了如何检测和解除死锁。理解和合理应用这些锁机制,对于优化数据库性能、避免潜在的性能瓶颈和数据一致性问题至关重要。通过对锁机制的深入了解,数据库管理员可以更好地管理数据库事务,确保系统的高效稳定运行。

参考链接

  1. How do I diagnose and handle locks?
  2. Deadlocks in PostgreSQL

相关文章

Oracle如何使用授予和撤销权限的语法和示例
Awesome Project: 探索 MatrixOrigin 云原生分布式数据库
下载丨66页PDF,云和恩墨技术通讯(2024年7月刊)
社区版oceanbase安装
Oracle 导出CSV工具-sqluldr2
ETL数据集成丨快速将MySQL数据迁移至Doris数据库

发布评论