KingbaseES 共享锁(SHARE)与排他锁(EXCLUSIVE)详解及测试复现

25 阅读8分钟

一、锁的基本概念

1. 什么是共享锁(SHARE)?

定义:共享锁允许多个事务同时读取同一资源,但阻止任何事务修改该资源。

特性:

读共享:多个事务可以同时持有SHARE锁

写互斥:持有SHARE锁时,其他事务不能获取修改类锁(ROW EXCLUSIVE及以上)

自兼容:同一事务可以多次获取SHARE锁

2. 什么是排他锁(EXCLUSIVE)?

定义:排他锁确保只有一个事务可以访问资源,其他事务既不能读也不能写(除了普通的SELECT)。
特性:

独占访问:一次只能有一个事务持有EXCLUSIVE锁

读有限允许:只允许并发的ACCESS SHARE锁(普通SELECT)

自冲突:同一时刻只能有一个EXCLUSIVE锁

二、冲突规则矩阵

完整冲突矩阵

请求的锁模式当前持有的锁模式 → SHARE当前持有的锁模式 → EXCLUSIVE
ACCESS SHARE兼容兼容
ROW SHARE兼容冲突
ROW EXCLUSIVE冲突冲突
SHARE UPDATE EXCLUSIVE冲突冲突
SHARE兼容冲突
SHARE ROW EXCLUSIVE冲突冲突
EXCLUSIVE冲突冲突
ACCESS EXCLUSIVE冲突冲突

关键结论:

SHARE与EXCLUSIVE相互冲突

SHARE锁允许其他SHARE锁和读锁

EXCLUSIVE锁只允许ACCESS SHARE(普通SELECT)

三、测试环境准备脚本

1. 创建测试数据库和表

创建测试表
CREATE TABLE shanjiatest_lock (
    id INT PRIMARY KEY,
    name VARCHAR(50),
    balance DECIMAL(10,2)
);

插入测试数据
INSERT INTO shanjiatest_lock VALUES 
(1, 'Alice', 1000.00),
(2, 'Bob', 2000.00),
(3, 'Charlie', 3000.00);

创建索引(用于测试索引锁)
CREATE INDEX idx_shanjiatest_lock_name ON shanjiatest_lock(name);

验证数据
SELECT * FROM shanjiatest_lock ORDER BY id;

image.png

2. 创建监控视图

创建更完整的锁监控视图
CREATE OR REPLACE VIEW lock_monitor AS
SELECT
    l.pid AS 进程ID,
    a.usename AS 用户名,
    a.application_name AS 应用名,
    a.state AS 状态,
    a.wait_event_type AS 等待类型,
    a.wait_event AS 等待事件,
    l.locktype AS 锁类型,
    l.mode AS 锁模式,
    l.granted AS 是否授予,
    CASE l.locktype
        WHEN 'relation' THEN l.relation::regclass::text
        WHEN 'virtualxid' THEN 'Virtual Transaction ID'
        WHEN 'transactionid' THEN 'Transaction ID: ' || l.transactionid::text
        WHEN 'tuple' THEN 'Tuple in ' || l.relation::regclass::text
        WHEN 'page' THEN 'Page in ' || l.relation::regclass::text
        WHEN 'object' THEN 'Object: ' || l.objid::text
        WHEN 'advisory' THEN 'Advisory Lock'
        ELSE 'N/A'
    END AS 对象名,
    a.query AS 当前查询,
    a.xact_start AS 事务开始时间,
    age(now(), a.xact_start) AS 事务时长,
    a.client_addr AS 客户端地址,
    a.client_port AS 客户端端口
FROM sys_locks l
LEFT JOIN sys_stat_activity a ON l.pid = a.pid
WHERE l.pid <> sys_backend_pid()
ORDER BY 
    CASE WHEN l.granted THEN 0 ELSE 1 END, 
    l.pid, 
    l.locktype;


创建阻塞关系视图
CREATE OR REPLACE VIEW blocking_chains AS
SELECT 
    blocked.pid AS 被阻塞进程ID,
    blocked.query AS 被阻塞查询,
    blocked.state AS 被阻塞状态,
    blocking.pid AS 阻塞进程ID,
    blocking.query AS 阻塞查询,
    blocking.state AS 阻塞状态,
    age(now(), blocked.xact_start) AS 阻塞时长
FROM sys_stat_activity blocked
JOIN sys_locks blocked_locks ON blocked.pid = blocked_locks.pid
JOIN sys_locks blocking_locks ON (
    blocked_locks.locktype = blocking_locks.locktype
    AND blocked_locks.database IS NOT DISTINCT FROM blocking_locks.database
    AND blocked_locks.relation IS NOT DISTINCT FROM blocking_locks.relation
    AND blocked_locks.page IS NOT DISTINCT FROM blocking_locks.page
    AND blocked_locks.tuple IS NOT DISTINCT FROM blocking_locks.tuple
    AND blocked_locks.virtualxid IS NOT DISTINCT FROM blocking_locks.virtualxid
    AND blocked_locks.transactionid IS NOT DISTINCT FROM blocking_locks.transactionid
    AND blocked_locks.classid IS NOT DISTINCT FROM blocking_locks.classid
    AND blocked_locks.objid IS NOT DISTINCT FROM blocking_locks.objid
    AND blocked_locks.objsubid IS NOT DISTINCT FROM blocking_locks.objsubid
    AND blocked_locks.pid <> blocking_locks.pid
)
JOIN sys_stat_activity blocking ON blocking_locks.pid = blocking.pid
WHERE NOT blocked_locks.granted
  AND blocking_locks.granted
  AND blocked.pid <> blocking.pid;

image.png
image.png

四、测试场景1:SHARE锁的基本特性

测试场景1:SHARE锁允许多个读事务共享

会话1:获取SHARE锁

BEGIN;
LOCK TABLE shanjiatest_lock IN SHARE MODE;
SELECT '会话1:已获取SHARE锁,当前时间:' || now();
SELECT pg_sleep(10); 

会话2:同时获取SHARE锁

BEGIN;
LOCK TABLE shanjiatest_lock IN SHARE MODE NOWAIT;
SELECT '会话2:成功获取SHARE锁,SHARE锁允许多个事务共享';
COMMIT;

会话3:普通SELECT

SELECT '会话3:普通SELECT可以执行,返回数据:', COUNT(*) FROM shanjiatest_lock;

会话4:尝试UPDATE

BEGIN;
UPDATE shanjiatest_lock SET balance = balance + 100 WHERE id = 1;
SELECT '会话4:UPDATE被SHARE锁阻塞,等待中...';

image.png

监控锁状态

SELECT * FROM lock_monitor WHERE 对象名 = 'shanjiatest_lock';

image.png

进行会话锁清理

会话1回滚释放锁:
ROLLBACK;

会话4的UPDATE现在应该成功
COMMIT;

image.png

验证数据

SELECT '验证数据:', * FROM shanjiatest_lock ORDER BY id;

image.png

如上验证结果:

会话1和会话2可以同时持有SHARE锁
会话3的普通SELECT可以执行
会话4的UPDATE被阻塞,直到会话1释放锁。

总结:(SHARE锁的基本特性(允许多个读事务共享))

会话操作结果验证结论
会话1LOCK TABLE IN SHARE MODE成功获取多个事务可同时持有SHARE锁
会话2LOCK TABLE IN SHARE MODE成功获取SHARE锁自兼容
会话3普通SELECT成功执行允许ACCESS SHARE
会话4UPDATE被阻塞阻止修改操作

五、测试场景2:EXCLUSIVE锁的独占性

会话1:获取EXCLUSIVE锁

BEGIN;
LOCK TABLE shanjiatest_lock IN EXCLUSIVE MODE;
SELECT '会话1:已获取EXCLUSIVE锁,当前时间:' || now();
SELECT pg_sleep(10); 

会话2:尝试获取SHARE锁

BEGIN;
LOCK TABLE shanjiatest_lock IN SHARE MODE;
SELECT '会话2:SHARE锁被EXCLUSIVE锁阻塞';

会话3:尝试获取另一个EXCLUSIVE锁

BEGIN;
LOCK TABLE shanjiatest_lock IN EXCLUSIVE MODE NOWAIT;
SELECT '会话3:EXCLUSIVE锁是自冲突的,无法同时获取';

会话4:普通SELECT

SELECT '会话4:普通SELECT可以执行,EXCLUSIVE锁允许ACCESS SHARE';
SELECT COUNT(*) FROM shanjiatest_lock;

image.png

会话5:尝试UPDATE

BEGIN;
UPDATE shanjiatest_lock SET name = 'David' WHERE id = 2;
SELECT '会话5:UPDATE被EXCLUSIVE锁阻塞';

image.png

监控锁状态

SELECT 
    进程ID,
    用户名,
    锁模式,
    是否授予,
    当前查询,
    age(now(), 事务开始时间) AS 事务时长
FROM lock_monitor 
WHERE 对象名 = 'shanjiatest_lock'
ORDER BY 是否授予 DESC, 进程ID;

image.png

使用sys_blocking_pids查看阻塞链

SELECT 
    pid AS 被阻塞进程,
    sys_blocking_pids(pid) AS 阻塞进程列表,
    query AS 被阻塞查询,
    state AS 状态
FROM sys_stat_activity 
WHERE wait_event_type = 'Lock'
  AND pid <> sys_backend_pid();

image.png

进行锁清理

会话1提交释放锁
COMMIT;
image.png
会话2、3、5现在应该可以继续
COMMIT; – 会话2
ROLLBACK; – 会话3(NOWAIT失败)
image.png
COMMIT; – 会话5
image.png

– 验证最终数据
SELECT ‘最终数据:’, * FROM shanjiatest_lock ORDER BY id;
image.png

如上验证结果:

会话1持有EXCLUSIVE锁
会话2的SHARE锁被阻塞
会话3的EXCLUSIVE锁立即失败(NOWAIT)
会话4的普通SELECT成功执行
会话5的UPDATE被阻塞

总结:(EXCLUSIVE锁的独占性)

会话操作结果验证结论
会话1LOCK TABLE IN EXCLUSIVE MODE成功获取独占访问
会话2LOCK TABLE IN SHARE MODE被阻塞与SHARE锁冲突
会话3LOCK TABLE IN EXCLUSIVE MODE NOWAIT立即失败自冲突
会话4普通SELECT成功执行允许ACCESS SHARE
会话5UPDATE被阻塞阻止修改

六、测试场景3:自动加锁行为

1. CREATE INDEX自动加SHARE锁测试

会话1:创建索引(非并发)

BEGIN;
SELECT '会话1:开始创建索引,自动获取SHARE锁';
CREATE INDEX idx_shanjiatest_lock_balance ON shanjiatest_lock(balance);
SELECT '会话1:索引创建中,持有SHARE锁...';
SELECT pg_sleep(5);  

会话2:尝试UPDATE(应该被SHARE锁阻塞)

BEGIN;
SELECT '会话2:尝试UPDATE,被CREATE INDEX的SHARE锁阻塞';
UPDATE shanjiatest_lock SET name = 'Eve' WHERE id = 3;

image.png

监控

SELECT * FROM lock_monitor WHERE 锁模式 LIKE '%Share%';

image.png

清理

COMMIT; 会话1提交,索引创建完成
COMMIT; 会话2的UPDATE现在完成
image.png

2. REFRESH MATERIALIZED VIEW CONCURRENTLY自动加EXCLUSIVE锁

创建物化视图

CREATE MATERIALIZED VIEW test_mv AS 
SELECT id, name, balance FROM shanjiatest_lock;

image.png

会话1:刷新物化视图(并发方式)

BEGIN;
SELECT '会话1:开始刷新物化视图,自动获取EXCLUSIVE锁';
REFRESH MATERIALIZED VIEW CONCURRENTLY test_mv;
SELECT '会话1:刷新中,持有EXCLUSIVE锁...';
SELECT pg_sleep(5);

会话2:尝试获取SHARE锁(应该被阻塞)

BEGIN;
SELECT '会话2:尝试获取SHARE锁,被EXCLUSIVE锁阻塞';
LOCK TABLE test_mv IN SHARE MODE;

会话3:普通SELECT物化视图(应该成功)

SELECT '会话3:普通SELECT物化视图可以执行';
SELECT COUNT(*) FROM test_mv;

监控SQL:

SELECT 
    锁类型,
    锁模式,
    是否授予,
    对象名,
    当前查询
FROM lock_monitor 
WHERE 对象名 LIKE 'test_mv%'
ORDER BY 进程ID;

image.png

清理锁

COMMIT; – 会话1
image.png
COMMIT; – 会话2
image.png

image.png

总结:(自动加锁行为)

操作自动获取的锁阻塞情况
CREATE INDEX(非并发)SHARE锁阻塞UPDATE/DELETE
REFRESH MATERIALIZED VIEW CONCURRENTLYEXCLUSIVE锁阻塞其他SHARE锁,允许SELECT

七、测试场景4:死锁与锁升级

准备两个测试表

DROP TABLE IF EXISTS account_a, account_b;
CREATE TABLE account_a (id INT PRIMARY KEY, balance DECIMAL);
CREATE TABLE account_b (id INT PRIMARY KEY, balance DECIMAL);
INSERT INTO account_a VALUES (1, 1000);
INSERT INTO account_b VALUES (1, 1000);

image.png

场景1:锁超时设置

会话1:获取EXCLUSIVE锁并保持

BEGIN;
LOCK TABLE account_a IN EXCLUSIVE MODE;
SELECT '会话1:获取EXCLUSIVE锁,将保持30秒';
SELECT pg_sleep(30);

会话2:设置锁超时

BEGIN;
SET lock_timeout = '5s';  -- 5秒超时
SELECT '会话2:设置lock_timeout=5s,尝试获取SHARE锁';
LOCK TABLE account_a IN SHARE MODE;  -- 5秒后超时报错
SELECT '会话2:如果看到此消息,说明锁获取成功(不应该出现)';

image.png

image.png

场景2:NOWAIT选项

会话1保持EXCLUSIVE锁

BEGIN;
LOCK TABLE account_b IN EXCLUSIVE MODE;
SELECT '会话1:持有EXCLUSIVE锁';

会话2:使用NOWAIT

BEGIN;
SELECT '会话2:使用NOWAIT尝试获取SHARE锁';
LOCK TABLE account_b IN SHARE MODE NOWAIT;  -- 立即失败,不等待
SELECT '会话2:NOWAIT立即返回错误';

image.png

清理

COMMIT; – 会话1
ROLLBACK; – 会话2

image.png

场景3:锁兼容性验证脚本

创建测试函数

CREATE OR REPLACE FUNCTION shanjiatest_lock_compatibility(
    lock1_mode TEXT, 
    lock2_mode TEXT
) RETURNS TABLE (
    test_result TEXT,
    lock1_status TEXT,
    lock2_status TEXT,
    is_compatible BOOLEAN
) AS $$
DECLARE
    lock_id BIGINT := 1;  -- 使用一个固定的 advisory lock ID
BEGIN
    -- 会话1获取锁
    PERFORM pg_advisory_lock(lock_id);
    lock1_status := '已授予';
    
    -- 会话2尝试获取锁(使用try锁)
    BEGIN
        IF pg_try_advisory_lock(lock_id) THEN
            -- 成功获取
            test_result := '兼容';
            lock2_status := '已授予';
            is_compatible := true;
            PERFORM pg_advisory_unlock(lock_id);  -- 会话2释放锁
        ELSE
            -- 失败
            test_result := '冲突';
            lock2_status := '未授予';
            is_compatible := false;
        END IF;
    EXCEPTION WHEN OTHERS THEN
        test_result := '冲突';
        lock2_status := '错误';
        is_compatible := false;
    END;
    
    -- 会话1释放锁
    PERFORM pg_advisory_unlock(lock_id);
    
    RETURN QUERY SELECT 
        test_result,
        lock1_status,
        lock2_status,
        is_compatible;
END;
$$ LANGUAGE plpgsql;

image.png

– 测试SHARE和EXCLUSIVE的兼容性
SELECT ‘SHARE vs EXCLUSIVE:’, * FROM shanjiatest_lock_compatibility(‘SHARE’, ‘EXCLUSIVE’);
SELECT ‘EXCLUSIVE vs SHARE:’, * FROM shanjiatest_lock_compatibility(‘EXCLUSIVE’, ‘SHARE’);
SELECT ‘SHARE vs SHARE:’, * FROM shanjiatest_lock_compatibility(‘SHARE’, ‘SHARE’);
SELECT ‘EXCLUSIVE vs EXCLUSIVE:’, * FROM shanjiatest_lock_compatibility(‘EXCLUSIVE’, ‘EXCLUSIVE’);

image.png

总结:(死锁与锁升级控制)

机制示例效果
lock_timeoutSET lock_timeout = ‘5s’等待5秒后超时报错
NOWAITLOCK TABLE … NOWAIT无法立即获取锁时直接报错

通过以上测试,完整验证了KingbaseES中共享锁与排他锁的特性、冲突规则以及实际应用场景,为数据库锁机制的理解和问题诊断提供了实践基础。