一、锁的基本概念
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. 创建测试数据库和表
sql
创建测试表
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;

2. 创建监控视图
vbnet
创建更完整的锁监控视图
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;


四、测试场景1:SHARE锁的基本特性
测试场景1:SHARE锁允许多个读事务共享
会话1:获取SHARE锁
sql
BEGIN;
LOCK TABLE shanjiatest_lock IN SHARE MODE;
SELECT '会话1:已获取SHARE锁,当前时间:' || now();
SELECT pg_sleep(10);
会话2:同时获取SHARE锁
sql
BEGIN;
LOCK TABLE shanjiatest_lock IN SHARE MODE NOWAIT;
SELECT '会话2:成功获取SHARE锁,SHARE锁允许多个事务共享';
COMMIT;
会话3:普通SELECT
sql
SELECT '会话3:普通SELECT可以执行,返回数据:', COUNT(*) FROM shanjiatest_lock;
会话4:尝试UPDATE
sql
BEGIN;
UPDATE shanjiatest_lock SET balance = balance + 100 WHERE id = 1;
SELECT '会话4:UPDATE被SHARE锁阻塞,等待中...';

监控锁状态
sql
SELECT * FROM lock_monitor WHERE 对象名 = 'shanjiatest_lock';

进行会话锁清理
会话1回滚释放锁:
ROLLBACK;
会话4的UPDATE现在应该成功
COMMIT;

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

如上验证结果:
会话1和会话2可以同时持有SHARE锁
会话3的普通SELECT可以执行
会话4的UPDATE被阻塞,直到会话1释放锁。
总结:(SHARE锁的基本特性(允许多个读事务共享))
| 会话 | 操作 | 结果 | 验证结论 |
|---|---|---|---|
| 会话1 | LOCK TABLE IN SHARE MODE | 成功获取 | 多个事务可同时持有SHARE锁 |
| 会话2 | LOCK TABLE IN SHARE MODE | 成功获取 | SHARE锁自兼容 |
| 会话3 | 普通SELECT | 成功执行 | 允许ACCESS SHARE |
| 会话4 | UPDATE | 被阻塞 | 阻止修改操作 |
五、测试场景2:EXCLUSIVE锁的独占性
会话1:获取EXCLUSIVE锁
sql
BEGIN;
LOCK TABLE shanjiatest_lock IN EXCLUSIVE MODE;
SELECT '会话1:已获取EXCLUSIVE锁,当前时间:' || now();
SELECT pg_sleep(10);
会话2:尝试获取SHARE锁
sql
BEGIN;
LOCK TABLE shanjiatest_lock IN SHARE MODE;
SELECT '会话2:SHARE锁被EXCLUSIVE锁阻塞';
会话3:尝试获取另一个EXCLUSIVE锁
sql
BEGIN;
LOCK TABLE shanjiatest_lock IN EXCLUSIVE MODE NOWAIT;
SELECT '会话3:EXCLUSIVE锁是自冲突的,无法同时获取';
会话4:普通SELECT
sql
SELECT '会话4:普通SELECT可以执行,EXCLUSIVE锁允许ACCESS SHARE';
SELECT COUNT(*) FROM shanjiatest_lock;

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

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

使用sys_blocking_pids查看阻塞链
vbnet
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();

进行锁清理
会话1提交释放锁
COMMIT;

会话2、3、5现在应该可以继续
COMMIT; -- 会话2
ROLLBACK; -- 会话3(NOWAIT失败)

COMMIT; -- 会话5

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

如上验证结果:
会话1持有EXCLUSIVE锁
会话2的SHARE锁被阻塞
会话3的EXCLUSIVE锁立即失败(NOWAIT)
会话4的普通SELECT成功执行
会话5的UPDATE被阻塞
总结:(EXCLUSIVE锁的独占性)
| 会话 | 操作 | 结果 | 验证结论 |
|---|---|---|---|
| 会话1 | LOCK TABLE IN EXCLUSIVE MODE | 成功获取 | 独占访问 |
| 会话2 | LOCK TABLE IN SHARE MODE | 被阻塞 | 与SHARE锁冲突 |
| 会话3 | LOCK TABLE IN EXCLUSIVE MODE NOWAIT | 立即失败 | 自冲突 |
| 会话4 | 普通SELECT | 成功执行 | 允许ACCESS SHARE |
| 会话5 | UPDATE | 被阻塞 | 阻止修改 |
六、测试场景3:自动加锁行为
1. CREATE INDEX自动加SHARE锁测试
会话1:创建索引(非并发)
sql
BEGIN;
SELECT '会话1:开始创建索引,自动获取SHARE锁';
CREATE INDEX idx_shanjiatest_lock_balance ON shanjiatest_lock(balance);
SELECT '会话1:索引创建中,持有SHARE锁...';
SELECT pg_sleep(5);
会话2:尝试UPDATE(应该被SHARE锁阻塞)
sql
BEGIN;
SELECT '会话2:尝试UPDATE,被CREATE INDEX的SHARE锁阻塞';
UPDATE shanjiatest_lock SET name = 'Eve' WHERE id = 3;

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

清理
COMMIT; 会话1提交,索引创建完成
COMMIT; 会话2的UPDATE现在完成

2. REFRESH MATERIALIZED VIEW CONCURRENTLY自动加EXCLUSIVE锁
创建物化视图
sql
CREATE MATERIALIZED VIEW test_mv AS
SELECT id, name, balance FROM shanjiatest_lock;

会话1:刷新物化视图(并发方式)
sql
BEGIN;
SELECT '会话1:开始刷新物化视图,自动获取EXCLUSIVE锁';
REFRESH MATERIALIZED VIEW CONCURRENTLY test_mv;
SELECT '会话1:刷新中,持有EXCLUSIVE锁...';
SELECT pg_sleep(5);
会话2:尝试获取SHARE锁(应该被阻塞)
sql
BEGIN;
SELECT '会话2:尝试获取SHARE锁,被EXCLUSIVE锁阻塞';
LOCK TABLE test_mv IN SHARE MODE;
会话3:普通SELECT物化视图(应该成功)
sql
SELECT '会话3:普通SELECT物化视图可以执行';
SELECT COUNT(*) FROM test_mv;
监控SQL:
vbnet
SELECT
锁类型,
锁模式,
是否授予,
对象名,
当前查询
FROM lock_monitor
WHERE 对象名 LIKE 'test_mv%'
ORDER BY 进程ID;

清理锁
COMMIT; -- 会话1

COMMIT; -- 会话2


总结:(自动加锁行为)
| 操作 | 自动获取的锁 | 阻塞情况 |
|---|---|---|
| CREATE INDEX(非并发) | SHARE锁 | 阻塞UPDATE/DELETE |
| REFRESH MATERIALIZED VIEW CONCURRENTLY | EXCLUSIVE锁 | 阻塞其他SHARE锁,允许SELECT |
七、测试场景4:死锁与锁升级
准备两个测试表
sql
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);

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


场景2:NOWAIT选项
会话1保持EXCLUSIVE锁
sql
BEGIN;
LOCK TABLE account_b IN EXCLUSIVE MODE;
SELECT '会话1:持有EXCLUSIVE锁';
会话2:使用NOWAIT
sql
BEGIN;
SELECT '会话2:使用NOWAIT尝试获取SHARE锁';
LOCK TABLE account_b IN SHARE MODE NOWAIT; -- 立即失败,不等待
SELECT '会话2:NOWAIT立即返回错误';

清理
COMMIT; -- 会话1
ROLLBACK; -- 会话2

场景3:锁兼容性验证脚本
创建测试函数
sql
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;

-- 测试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');

总结:(死锁与锁升级控制)
| 机制 | 示例 | 效果 |
|---|---|---|
| lock_timeout | SET lock_timeout = '5s' | 等待5秒后超时报错 |
| NOWAIT | LOCK TABLE ... NOWAIT | 无法立即获取锁时直接报错 |
通过以上测试,完整验证了KingbaseES中共享锁与排他锁的特性、冲突规则以及实际应用场景,为数据库锁机制的理解和问题诊断提供了实践基础。