Skip to content

共享锁 (Shared Lock / S Lock) ​

📋 概述 ​

共享锁(Shared Lock),又称读锁(Read Lock)或 S 锁,是一种允许多个事务同时读取同一数据资源的锁机制。当一个事务获得某资源的共享锁后,其他事务也可以获得该资源的共享锁,但不能获得排他锁。

核心特点 ​

✅ 优点:

  • 高并发读: 多个事务可以同时读取同一资源
  • 读写分离: 支持读写分离架构
  • 数据一致性: 确保读取期间数据不被修改
  • 无死锁风险: 纯共享锁不会导致死锁

❌ 缺点:

  • 阻塞写操作: 持有共享锁时会阻止写操作
  • 可能升级为排他锁: 需要修改时需重新获取锁
  • 长事务影响: 长时间持有会阻塞写操作
  • 快照读vs当前读: 不同数据库实现有差异

适用场景 ​

  • 一致性读: 需要确保读取期间数据不变
  • 报表查询: 大量并发的只读操作
  • 数据验证: 读取后基于数据进行决策
  • 读写分离: 从库上的查询操作
  • 引用完整性检查: 外键约束检查

🔧 工作原理 ​

锁兼容性矩阵 ​

共享锁的核心特性体现在其兼容性上:

        | 无锁  | S锁   | X锁
--------|-------|-------|------
S锁    | ✅    | ✅    | ❌
X锁    | ✅    | ❌    | ❌

解读:

  • ✅ = 兼容,可以同时持有
  • ❌ = 冲突,必须等待

示例:

事务A: SELECT ... LOCK IN SHARE MODE;  -- 获得 S 锁
事务B: SELECT ... LOCK IN SHARE MODE;  -- ✅ 可以获得 S 锁
事务C: UPDATE ...                       -- ❌ 必须等待事务A释放 S 锁

锁的获取与释放 ​

显式获取 ​

sql
-- MySQL
SELECT * FROM users WHERE id = 1 LOCK IN SHARE MODE;

-- PostgreSQL
SELECT * FROM users WHERE id = 1 FOR SHARE;

-- SQL Server
SELECT * FROM users WITH (HOLDLOCK, ROWLOCK) WHERE id = 1;

-- Oracle
-- Oracle 没有直接的共享锁语法,使用 MVCC

自动获取 ​

某些操作会自动获取共享锁:

操作说明
SELECT (某些隔离级别)可串行化隔离级别
外键检查检查引用完整性时
CREATE INDEX创建索引时
某些 DDL 操作需要读取表结构时

锁的释放 ​

sql
-- 共享锁在事务结束时自动释放
BEGIN;
SELECT * FROM users WHERE id = 1 LOCK IN SHARE MODE;
-- ... 其他操作
COMMIT;  -- 或 ROLLBACK,自动释放 S 锁

💻 各数据库实现 ​

MySQL ​

MySQL InnoDB 引擎支持显式的共享锁。

基本用法 ​

sql
-- 显式加共享锁
BEGIN;
SELECT * FROM users WHERE id = 1 LOCK IN SHARE MODE;
-- 其他事务也可以加共享锁读取
SELECT * FROM users WHERE id = 1 LOCK IN SHARE MODE;  -- ✅ 允许
-- 但更新会被阻塞
UPDATE users SET name = 'test' WHERE id = 1;  -- ❌ 阻塞
COMMIT;

-- 共享锁也适用于多行
SELECT * FROM users WHERE age > 18 LOCK IN SHARE MODE;

快照读 vs 当前读 ​

MySQL InnoDB 有两种读取方式:

1. 快照读 (Snapshot Read)

  • 普通的 SELECT 语句
  • 使用 MVCC 读取历史版本
  • 不加锁
  • 看到的数据可能是旧版本
sql
-- 快照读,不加锁
SELECT * FROM users WHERE id = 1;

2. 当前读 (Current Read)

  • SELECT ... LOCK IN SHARE MODE
  • SELECT ... FOR UPDATE
  • UPDATE, DELETE, INSERT
  • 加锁
  • 看到最新提交的数据
sql
-- 当前读,加共享锁
SELECT * FROM users WHERE id = 1 LOCK IN SHARE MODE;

查看共享锁 ​

sql
-- 查看当前的共享锁
SELECT 
    lock_id,
    lock_trx_id,
    lock_mode,
    lock_type,
    lock_table,
    lock_index,
    lock_data
FROM performance_schema.data_locks
WHERE lock_mode = 'S';  -- S 表示共享锁

-- 查看锁等待
SELECT * FROM performance_schema.data_lock_waits;

PostgreSQL ​

PostgreSQL 提供更细粒度的共享锁控制。

锁模式 ​

PostgreSQL 有多种共享锁模式:

sql
-- FOR SHARE - 标准共享锁
SELECT * FROM users WHERE id = 1 FOR SHARE;

-- FOR KEY SHARE - 最弱的锁,不锁定非关键列
SELECT * FROM users WHERE id = 1 FOR KEY SHARE;

-- FOR NO KEY UPDATE - 比 FOR UPDATE 弱
SELECT * FROM users WHERE id = 1 FOR NO KEY UPDATE;

锁兼容性 ​

请求模式          | FOR KEY SHARE | FOR SHARE | FOR NO KEY UPDATE | FOR UPDATE
------------------|---------------|-----------|-------------------|----------
FOR KEY SHARE     |      ✓        |     ✓     |         ✓         |    ✓
FOR SHARE         |      ✓        |     ✓     |         ✓         |    ✗
FOR NO KEY UPDATE |      ✓        |     ✓     |         ✗         |    ✗
FOR UPDATE        |      ✓        |     ✗     |         ✗         |    ✗

查看共享锁 ​

sql
-- 查看所有锁
SELECT 
    l.locktype,
    l.relation::regclass AS table_name,
    l.mode,
    l.granted,
    a.pid,
    a.usename,
    a.query
FROM pg_locks l
JOIN pg_stat_activity a ON l.pid = a.pid
WHERE l.mode LIKE '%SHARE%'
ORDER BY l.relation;

SQL Server ​

SQL Server 通过提示(Hints)来控制共享锁。

基本用法 ​

sql
-- 使用共享锁提示
SELECT * FROM users WITH (ROWLOCK, SHARELOCK) WHERE id = 1;

-- 保持共享锁到事务结束
BEGIN TRANSACTION;
    SELECT * FROM users WITH (HOLDLOCK, ROWLOCK) WHERE id = 1;
    -- HOLDLOCK 等同于 SERIALIZABLE 隔离级别
    -- 共享锁会保持到事务结束
COMMIT TRANSACTION;

-- 表级共享锁
SELECT * FROM users WITH (TABLOCK) WHERE id = 1;

隔离级别的影响 ​

sql
-- READ COMMITTED (默认)
-- SELECT 不加锁,使用行版本控制
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;

-- REPEATABLE READ
-- SELECT 加共享锁,保持到事务结束
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;

-- SERIALIZABLE
-- SELECT 加范围共享锁
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;

查看共享锁 ​

sql
-- 查看当前的共享锁
SELECT 
    request_session_id,
    resource_type,
    resource_database_id,
    resource_associated_entity_id,
    request_mode,
    request_status
FROM sys.dm_tran_locks
WHERE request_mode IN ('S', 'IS');  -- S=共享锁, IS=意向共享锁

Oracle ​

Oracle 主要通过 MVCC 实现读一致性,较少使用传统共享锁。

MVCC 机制 ​

sql
-- Oracle 的 SELECT 不加锁
SELECT * FROM users WHERE id = 1;
-- 通过 undo 日志读取一致性版本

-- 如果需要显式锁定,通常使用排他锁
SELECT * FROM users WHERE id = 1 FOR UPDATE;

表级共享锁 ​

sql
-- 显式锁定表
LOCK TABLE users IN SHARE MODE;
-- 阻止其他事务获取排他锁
-- 但仍允许其他事务获取共享锁

查看锁 ​

sql
-- 查看共享锁
SELECT 
    s.sid,
    s.serial#,
    s.username,
    l.type,
    l.lmode,
    l.request,
    o.object_name
FROM v$session s
JOIN v$lock l ON s.sid = l.sid
JOIN dba_objects o ON l.id1 = o.object_id
WHERE l.type = 'TM'  -- TM = DML/DDL 锁
  AND l.lmode = 4;   -- 4 = Share mode

📊 性能影响分析 ​

并发度评估 ​

场景并发度说明
多个 S 锁⭐⭐⭐⭐⭐完全兼容
S 锁 + X 锁⭐互相阻塞
长事务 S 锁⭐⭐阻塞写操作
短事务 S 锁⭐⭐⭐⭐⭐几乎无影响

性能测试示例 ​

sql
-- 测试场景:100个并发事务读取同一行

-- 使用共享锁
-- 100个事务可以并行读取,总耗时:~0.1秒
BEGIN;
SELECT * FROM users WHERE id = 1 LOCK IN SHARE MODE;
COMMIT;

-- 如果有一个事务持有排他锁
-- 100个事务需要串行等待,总耗时:~50秒

🎯 最佳实践 ​

✅ 推荐做法 ​

1. 短时间持有共享锁 ​

sql
-- ✅ 推荐:快速读取并提交
BEGIN;
SELECT * FROM users WHERE id = 1 LOCK IN SHARE MODE;
-- 立即处理数据
COMMIT;

-- ❌ 不推荐:长时间持有
BEGIN;
SELECT * FROM users WHERE id = 1 LOCK IN SHARE MODE;
-- 执行大量无关操作
SLEEP(60);
COMMIT;

2. 使用合适的隔离级别 ​

sql
-- 读已提交(大多数场景)
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;

-- 可重复读(需要更强一致性)
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;

-- 可串行化(最强,但性能最差)
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;

3. 避免不必要的共享锁 ​

sql
-- ❌ 不推荐:普通读取不需要共享锁
SELECT * FROM users WHERE id = 1 LOCK IN SHARE MODE;

-- ✅ 推荐:普通 SELECT 使用快照读
SELECT * FROM users WHERE id = 1;

4. 读写分离架构 ​

sql
-- 主库:写操作
UPDATE users SET name = 'test' WHERE id = 1;

-- 从库:读操作(使用共享锁或不加锁)
SELECT * FROM users WHERE id = 1;

❌ 避免陷阱 ​

1. 避免共享锁导致的写阻塞 ​

sql
-- ❌ 危险:长时间持有共享锁阻塞写
BEGIN;
SELECT * FROM users WHERE id = 1 LOCK IN SHARE MODE;
-- 执行大量操作(30秒)
COMMIT;
-- 这期间所有 UPDATE/DELETE 都被阻塞

-- ✅ 推荐:快速完成
BEGIN;
SELECT * FROM users WHERE id = 1 LOCK IN SHARE MODE;
-- 快速处理(< 1秒)
COMMIT;

2. 避免忘记提交事务 ​

python
# ❌ 危险:忘记提交
cursor.execute("SELECT * FROM users WHERE id = 1 LOCK IN SHARE MODE")
# 忘记 commit(),锁一直持有

# ✅ 正确:使用上下文管理器
with connection.begin():
    cursor.execute("SELECT * FROM users WHERE id = 1 LOCK IN SHARE MODE")
    # 自动提交或回滚

3. 避免在循环中使用共享锁 ​

sql
-- ❌ 性能差
FOR i IN 1..1000 LOOP
    SELECT * FROM users WHERE id = i LOCK IN SHARE MODE;
END LOOP;

-- ✅ 批量读取
SELECT * FROM users WHERE id BETWEEN 1 AND 1000 LOCK IN SHARE MODE;

🔍 监控与诊断 ​

MySQL 监控 ​

sql
-- 查看共享锁
SELECT 
    lock_id,
    lock_trx_id,
    lock_mode,
    lock_type,
    lock_table,
    lock_data
FROM performance_schema.data_locks
WHERE lock_mode = 'S';

-- 查看锁等待
SELECT 
    requesting_thread_id,
    blocking_thread_id,
    wait_age
FROM performance_schema.data_lock_waits;

-- 查看长时间运行的事务
SELECT * FROM information_schema.innodb_trx 
WHERE trx_state = 'RUNNING'
  AND TIME_TO_SEC(TIMEDIFF(NOW(), trx_started)) > 10;

PostgreSQL 监控 ​

sql
-- 查看共享锁
SELECT 
    l.relation::regclass AS table_name,
    l.mode,
    l.granted,
    a.pid,
    a.query,
    now() - a.query_start AS duration
FROM pg_locks l
JOIN pg_stat_activity a ON l.pid = a.pid
WHERE l.mode LIKE '%SHARE%'
ORDER BY duration DESC;

SQL Server 监控 ​

sql
-- 查看共享锁
SELECT 
    request_session_id,
    OBJECT_NAME(resource_associated_entity_id) AS table_name,
    request_mode,
    request_status
FROM sys.dm_tran_locks
WHERE request_mode IN ('S', 'IS');

🚨 常见问题 ​

Q1: 共享锁和快照读有什么区别? ​

答:

特性共享锁 (当前读)快照读
语法SELECT ... LOCK IN SHARE MODESELECT
加锁✅ 是❌ 否
数据版本最新历史快照
阻塞写✅ 是❌ 否
并发度较低较高
一致性强弱

Q2: 共享锁会导致死锁吗? ​

答:

纯共享锁不会死锁,因为多个 S 锁互相兼容。

但如果涉及锁升级(S → X),则可能死锁:

sql
-- 事务 A
SELECT * FROM users WHERE id = 1 LOCK IN SHARE MODE;  -- S 锁
UPDATE users SET name = 'A' WHERE id = 1;  -- 尝试升级为 X 锁

-- 事务 B
SELECT * FROM users WHERE id = 1 LOCK IN SHARE MODE;  -- S 锁
UPDATE users SET name = 'B' WHERE id = 1;  -- 尝试升级为 X 锁

-- 结果:死锁!两个事务都在等待对方释放 S 锁

解决: 直接使用 FOR UPDATE 获取 X 锁。


Q3: 何时应该使用共享锁? ​

答:

推荐使用场景:

  1. 需要确保读取期间数据不被修改
  2. 读取后要基于数据做决策
  3. 外键约束检查
  4. 可串行化隔离级别要求

不推荐使用场景:

  1. 普通查询(使用快照读)
  2. 高并发写场景
  3. 长事务

Q4: 共享锁的性能开销大吗? ​

答:

开销较小,但需要注意:

  1. 内存开销: 每个锁对象占用少量内存
  2. 管理开销: 锁表的维护成本
  3. 阻塞开销: 可能阻塞写操作

建议:

  • 短时间持有
  • 避免不必要的共享锁
  • 优先使用快照读

📚 相关资源 ​

内部链接 ​

外部资源 ​


最后更新: 2026-04-12
维护状态: ✅ 完整

Released under MIT License.