PostgreSQL笔记30:锁机制深度解析——表级锁、行级锁、轻量级锁与自旋锁

【PostgreSQL】PostgreSQL查锁表详情 PostgreSQL查锁表详情 阅读详情

纲要

  • 表级锁(Table-Level Locks)
    • 锁模式与冲突矩阵
      • ACCESS SHARE
      • ROW SHARE
      • ROW EXCLUSIVE
      • SHARE UPDATE EXCLUSIVE
      • SHARE
      • SHARE ROW EXCLUSIVE
      • EXCLUSIVE
      • ACCESS EXCLUSIVE
    • 常见操作对应的锁模式
      • SELECT → ACCESS SHARE
      • INSERT / UPDATE / DELETE → ROW EXCLUSIVE
      • VACUUM(不带 FULL) → SHARE UPDATE EXCLUSIVE
      • CREATE INDEX(不带 CONCURRENTLY) → SHARE
      • CREATE INDEX CONCURRENTLY → SHARE UPDATE EXCLUSIVE
      • TRUNCATE / VACUUM FULL / CLUSTER / REINDEX → ACCESS EXCLUSIVE
    • 锁的共享内存限制:max_locks_per_transaction × (max_connections + max_prepared_transactions)
    • lock_timeout 与锁等待队列
    • Fast-Path 锁机制
      • 每个后端进程 16 个 fast-path 槽位
      • 索引数量与 fast-path 失效的关系
  • 行级锁(Row-Level Locks)
    • 锁模式
      • FOR UPDATE
      • FOR NO KEY UPDATE
      • FOR SHARE
      • FOR KEY SHARE
    • 存储位置:磁盘上的元组头(tuple header),非共享内存
    • pg_rowlocks 插件
  • 轻量级锁(Lightweight Locks / LWLock)
    • 保护内存结构(如哈希表、缓冲区)
    • BufMapping 与 BufferContent
    • LWLock 等待事件
  • 自旋锁(Spinlocks)
    • 特点:忙等待(busy-wait),无等待队列
    • 典型案例:old_snapshot 参数导致的性能问题
      • PostgreSQL 9.6 → 13 升级后 CPU 飙升
      • 根因:old_snapshot_threshold 开启后,页面修剪(page pruning)路径中频繁获取自旋锁
      • 修复版本:PostgreSQL 14 及以后

PostgreSQL 的锁机制是并发控制的基石。理解不同锁的层级、模式及其冲突关系,对于数据库性能调优和问题排查至关重要。本文将从表级锁、行级锁、轻量级锁和自旋锁四个维度,系统性地梳理 PostgreSQL 的锁体系。

表级锁

表级锁是 PostgreSQL 中最常接触的锁类型。PostgreSQL 官方文档将表级锁划分为八种模式。这些模式在源代码中分别对应一个整数值,因此社区中有时会以“几级锁”来指代某种锁模式。

锁模式与冲突矩阵

八种表级锁模式及其冲突关系如下:

锁模式强度递增

ACCESS SHARE

ROW SHARE

ROW EXCLUSIVE

SHARE UPDATE EXCLUSIVE

SHARE

SHARE ROW EXCLUSIVE

EXCLUSIVE

ACCESS EXCLUSIVE

各锁模式的典型获取场景与冲突范围:

锁模式自动获取场景冲突模式
ACCESS SHARESELECTACCESS EXCLUSIVE
ROW SHARESELECT FOR UPDATE / SELECT FOR SHAREEXCLUSIVE, ACCESS EXCLUSIVE
ROW EXCLUSIVEINSERT, UPDATE, DELETE, MERGESHARE, SHARE ROW EXCLUSIVE, EXCLUSIVE, ACCESS EXCLUSIVE
SHARE UPDATE EXCLUSIVEVACUUM(不带 FULL), ANALYZE, CREATE INDEX CONCURRENTLY, REINDEX CONCURRENTLYSHARE UPDATE EXCLUSIVE, SHARE, SHARE ROW EXCLUSIVE, EXCLUSIVE, ACCESS EXCLUSIVE
SHARECREATE INDEX(不带 CONCURRENTLY)ROW EXCLUSIVE, SHARE UPDATE EXCLUSIVE, SHARE ROW EXCLUSIVE, EXCLUSIVE, ACCESS EXCLUSIVE
SHARE ROW EXCLUSIVE部分 ALTER TABLE 变体ROW EXCLUSIVE, SHARE UPDATE EXCLUSIVE, SHARE, SHARE ROW EXCLUSIVE, EXCLUSIVE, ACCESS EXCLUSIVE
EXCLUSIVE部分 ALTER TABLE / REFRESH MATERIALIZED VIEW CONCURRENTLYROW SHARE, ROW EXCLUSIVE, SHARE UPDATE EXCLUSIVE, SHARE, SHARE ROW EXCLUSIVE, EXCLUSIVE, ACCESS EXCLUSIVE
ACCESS EXCLUSIVEDROP TABLE, TRUNCATE, REINDEX(不带 CONCURRENTLY), CLUSTER, VACUUM FULL所有锁模式(自冲突)

数据来源:PostgreSQL 官方文档

关键锁模式的实践解读

ACCESS SHARE:SELECT 查询在访问表时会获取此锁。它仅与 ACCESS EXCLUSIVE 冲突,因此普通的查询操作不会被大多数 DML 操作阻塞。

ROW EXCLUSIVE:INSERT、UPDATE、DELETE 等数据修改操作获取此锁。多个 DML 操作可以并发执行,因为它们之间不冲突。

SHARE UPDATE EXCLUSIVE:VACUUM(不带 FULL)获取此锁。该锁是自冲突的——同一张表上同时只能有一个 VACUUM 进程运行。这正是大表维护的痛点所在:如果单表数据量巨大,死元组(dead tuples)累积过多,VACUUM 的运行时间会非常长,而在此期间其他 VACUUM 操作无法并发执行。因此,对于大表,通常建议采用分区表策略,将数据分散到多个子表中,以便并行执行 VACUUM。

SHARE:CREATE INDEX(不带 CONCURRENTLY)获取此锁。从冲突矩阵可以看出,SHARE 与 ROW EXCLUSIVE 冲突——这意味着在创建索引期间,表上的 INSERT、UPDATE、DELETE 操作会被阻塞。在生产环境中,这通常是不可接受的。解决方案是使用 CREATE INDEX CONCURRENTLY,它仅需要 SHARE UPDATE EXCLUSIVE 锁,不会阻塞 DML 操作。但需要注意,CREATE INDEX CONCURRENTLY 会带来额外的复杂度:它需要执行两次表扫描,并且如果构建过程中出现问题,可能会留下无效(invalid)的索引,需要手动清理。

ACCESS EXCLUSIVE:这是 PostgreSQL 中最高级别的锁,与所有锁模式冲突。TRUNCATE、VACUUM FULL、CLUSTER、REINDEX(不带 CONCURRENTLY)等重量级操作会获取此锁。在这些操作执行期间,目标表上不能有任何并发的读写操作。

锁的共享内存限制

PostgreSQL 的表级锁存储在共享内存中。共享锁表的大小由以下公式决定:

max_locks_per_transaction * (max_connections + max_prepared_transactions)

默认情况下,max_locks_per_transaction = 64,max_connections = 100,因此共享锁表大约可以跟踪 64 × 100 = 6400 个对象的锁。当 pg_locks 视图中的锁记录总数超过此上限时,会报错:

ERROR: out of shared memory
HINT: You might need to increase max_locks_per_transaction

需要注意的是,这里的限制是可锁对象的数量,而非行锁的数量——行锁存储在磁盘上,不受此限制。

锁等待与 lock_timeout

PostgreSQL 的锁等待队列采用先来先服务(FIFO)策略。当一个事务因锁冲突而等待时,后续到达的请求即使与当前持有者不冲突,也需要排队等待。这可能导致锁队列不断累积,进而引发更广泛的阻塞。

lock_timeout 参数用于设置锁获取的超时时间(单位为毫秒)。当设置为大于 0 的值时,任何锁获取操作如果等待超过指定时间,语句将被中止并返回错误:

SET lock_timeout = '5s';

-- 如果 5 秒内无法获取锁,将报错:
-- ERROR:  canceling statement due to lock timeout

在生产环境中,将 lock_timeout 设置为 60 秒或 120 秒是一种常见的最佳实践,可以有效防止锁队列无限增长导致的潜在雪崩效应。

查看锁阻塞情况

PostgreSQL 提供了多种方式查看锁阻塞信息。

方式一:使用 pg_blocking_pids() 函数

-- 查看指定 PID 被哪些进程阻塞
SELECT pg_blocking_pids(31591);

pg_blocking_pids() 返回一个整数数组,包含阻塞指定进程的所有进程 ID。如果返回空数组,表示该进程未被阻塞。

方式二:查询 pg_locks 视图

SELECT locktype, database, relation::regclass, pid, mode, granted
FROM pg_locks
WHERE pid IN (31591, 23835);

granted 字段为 false 表示该锁尚未获取,进程正在等待。

方式三:综合查询被阻塞的进程

SELECT
    pid,
    usename,
    application_name,
    state,
    wait_event_type,
    wait_event,
    pg_blocking_pids(pid) AS blocked_by,
    query
FROM pg_stat_activity
WHERE wait_event_type = 'Lock';

Fast-Path 锁机制

Fast-Path 是 PostgreSQL 中一项重要的锁优化机制。其核心思想是:对于弱锁(如 ACCESS SHARE、ROW SHARE、ROW EXCLUSIVE 等互不冲突的锁),如果某个事务在表上只申请了弱锁,且能够确认该表上不存在其他事务持有的强锁,那么就不必将该锁记录在共享内存的 pg_locks 表中,从而避免频繁访问共享锁表,提升性能。

Fast-Path 机制存在一个关键限制:每个后端进程(backend)只有 16 个 fast-path 槽位。这意味着:

  • 如果一个会话在同一个表上持有超过 16 个锁(例如,查询涉及超过 16 个索引),fast-path 将失效,后续锁操作将回退到常规锁管理器。
  • 表上索引数量过多时,一次索引扫描需要对所有索引加锁,很容易耗尽 fast-path 槽位。

因此,在生产环境中应避免创建过多冗余索引,以减少 fast-path 失效带来的性能损耗。

行级锁

行级锁用于控制对表中特定行的并发访问。与表级锁不同,行级锁的信息存储在磁盘上的元组头(tuple header)中,而非共享内存。因此,行级锁在理论上没有数量上限。

行级锁模式

PostgreSQL 支持四种行级锁模式:

锁模式对应 SQL 语法说明
FOR UPDATESELECT ... FOR UPDATE最严格的模式,阻止其他事务对行进行任何修改或加锁
FOR NO KEY UPDATESELECT ... FOR NO KEY UPDATE与 FOR UPDATE 类似,但不阻塞 FOR KEY SHARE 锁
FOR SHARESELECT ... FOR SHARE共享锁,阻止其他事务执行 UPDATE、DELETE 或 FOR UPDATE,但允许 FOR SHARE
FOR KEY SHARESELECT ... FOR KEY SHARE最弱的模式,仅阻止其他事务修改行的键值

查看行级锁

PostgreSQL 提供了一个扩展插件 pg_rowlocks,用于查看表上的行级锁信息:

-- 启用扩展(如未安装)
CREATE EXTENSION IF NOT EXISTS pg_rowlocks;

-- 查看表上的行锁
SELECT * FROM pg_rowlocks('t1');

该插件提供的 pg_rowlocks() 函数可以显示每个行锁的类型(如 NoKeyUpdate、KeyShare 等),帮助 DBA 理解当前的并发冲突状况。

轻量级锁

轻量级锁(LWLock)是 PostgreSQL 中用于保护共享内存结构的锁机制。与表级锁和行级锁不同,轻量级锁不直接面向 SQL 层面,而是服务于内核层面的数据结构保护。

常见的轻量级锁

BufMapping :当进程需要访问某个数据块时,首先需要通过哈希表定位该数据块在共享缓冲区中的位置。访问这个哈希表时需要获取 BufMapping 锁。在高并发场景下,如果 BufMapping 成为瓶颈,通常意味着需要增大 shared_buffers,或通过 fillfactor 减少热块冲突。

BufferContent :在访问共享缓冲区中具体的数据页内容时,需要获取 BufferContent 锁。读取时获取共享锁,写入时获取独占锁。

WALWrite :在写入 WAL 日志时获取,用于保护 WAL 缓冲区的并发写入。

监控轻量级锁等待

通过 pg_stat_activity 视图可以监控轻量级锁等待事件:

SELECT pid, wait_event_type, wait_event, query
FROM pg_stat_activity
WHERE wait_event_type = 'LWLock';

自旋锁

自旋锁(Spinlock)是 PostgreSQL 中最底层的锁机制,用于保护极短临界区的并发访问。自旋锁的特点包括:

  • 忙等待(busy-wait) :获取不到锁时不会进入睡眠,而是持续循环检测,消耗 CPU 时间。
  • 无等待队列:没有 FIFO 队列,所有等待进程平等竞争。
  • 事务结束时自动释放:无需显式解锁。

由于自旋锁的忙等待特性,在 CPU 资源紧张时,自旋锁竞争会显著加剧 CPU 负载。

经典案例:old_snapshot 参数引发的自旋锁风暴

这是一个在 PostgreSQL 社区中广为流传的性能问题案例。

背景:某团队将 PostgreSQL 从 9.6 升级到 13 后,发现 CPU 使用率从 16% 飙升至 100%,且业务负载和数据库参数均无明显变化。

排查过程:通过 perf 工具抓取 CPU 热点,发现大量 CPU 时间消耗在 s_lock 相关函数(自旋锁实现)上。进一步分析堆栈,定位到 TransactionIdLimitedForOldSnapshots 函数。

根因:在 PostgreSQL 13 及更早版本中,当 old_snapshot_threshold 参数被启用(设置为大于 0 的值)时,每个后端进程在执行页面修剪(page pruning)时都会调用 TransactionIdLimitedForOldSnapshots,该函数内部会频繁获取自旋锁。在高并发场景下,这导致大量 CPU 时间消耗在自旋锁竞争上。

验证与解决:在相同并发(如 10 个或 30 个会话)和相同 SQL 负载下,关闭 old_snapshot_threshold 参数后,CPU 使用率降低约 50%。该问题在 PostgreSQL 14 中得到了显著优化。

经验教训:在 PostgreSQL 14 之前的版本中,除非有明确需求,否则不建议启用 old_snapshot_threshold 参数。

API 速览

pg_blocking_pids()

属性说明
所属库PostgreSQL 系统函数
方法签名pg_blocking_pids(pid integer) → integer[]
参数pid:目标服务器进程的进程 ID
返回值阻塞目标进程的所有进程 ID 数组;若未被阻塞或进程不存在则返回空数组
-- 示例:查询 PID 12345 被哪些进程阻塞
SELECT pg_blocking_pids(12345);

-- 示例:查询所有被阻塞的进程及其阻塞者
SELECT
    pid,
    pg_blocking_pids(pid) AS blocked_by,
    query
FROM pg_stat_activity
WHERE pg_blocking_pids(pid) != '{}';

pg_locks 系统视图

属性说明
所属库PostgreSQL 系统目录
关键字段locktype(锁类型)、database、relation、pid、mode(锁模式)、granted(是否已获取)
说明行级锁通常不在此视图中显示,因其存储在磁盘上
-- 查询当前所有未获取的锁等待
SELECT
    pid,
    mode,
    granted,
    relation::regclass AS relation_name,
    locktype
FROM pg_locks
WHERE granted = false;

pg_rowlocks() 函数

属性说明
所属库pg_rowlocks 扩展
方法签名pg_rowlocks(relation regclass) → setof record
参数relation:目标表的名称或 OID
返回值表中每行的锁信息,包括锁类型、持有者 PID 等
-- 安装扩展
CREATE EXTENSION IF NOT EXISTS pg_rowlocks;

-- 查看表 t1 上的行级锁
SELECT * FROM pg_rowlocks('t1');

lock_timeout 配置参数

属性说明
所属库PostgreSQL 运行时配置
类型integer(毫秒)
默认值0(禁用)
作用域会话级别(可动态设置)
-- 会话级设置
SET lock_timeout = '5s';

-- 在事务中临时设置
BEGIN;
SET LOCAL lock_timeout = '3s';
-- 执行可能加锁的操作
COMMIT;

old_snapshot_threshold 配置参数

属性说明
所属库PostgreSQL 运行时配置
类型integer(分钟)
默认值-1(禁用)
适用版本PostgreSQL 9.6+(14 版本后性能问题已优化)
-- 查看当前设置
SHOW old_snapshot_threshold;

-- 禁用(推荐,尤其适用于 PG 14 以下版本)
SET old_snapshot_threshold = -1;

Demo 简单示例

以下 Demo 演示了 PostgreSQL 表级锁的阻塞与排查流程。

环境准备

-- 创建测试表
DROP TABLE IF EXISTS t1;
CREATE TABLE t1 (id INT PRIMARY KEY, name TEXT);
INSERT INTO t1 VALUES (1, 'Alice'), (2, 'Bob');

场景一:CREATE INDEX 被 INSERT 阻塞

会话 A(执行 INSERT)

BEGIN;
INSERT INTO t1 VALUES (3, 'Charlie');
-- 不提交,持有 ROW EXCLUSIVE 锁

会话 B(执行 CREATE INDEX)

-- 将被阻塞,因为 SHARE 锁与 ROW EXCLUSIVE 冲突
CREATE INDEX idx_t1_name ON t1 (name);

会话 C(排查阻塞)

-- 获取会话 A 和 B 的 PID
SELECT pid, backend_start, query FROM pg_stat_activity WHERE query LIKE '%t1%';

-- 假设会话 B 的 PID 为 31591
SELECT pg_blocking_pids(31591);

-- 查看锁详情
SELECT
    locktype,
    relation::regclass,
    pid,
    mode,
    granted
FROM pg_locks
WHERE pid IN (31591, 23835)  -- 替换为实际 PID
ORDER BY pid;

场景二:使用 CREATE INDEX CONCURRENTLY 避免阻塞

-- 不会阻塞 DML 操作(仅需 SHARE UPDATE EXCLUSIVE 锁)
CREATE INDEX CONCURRENTLY idx_t1_name ON t1 (name);

场景三:设置 lock_timeout 防止无限等待

-- 设置 5 秒超时
SET lock_timeout = '5s';

-- 如果 5 秒内无法获取锁,将报错退出
CREATE INDEX idx_t1_name ON t1 (name);
-- ERROR:  canceling statement due to lock timeout

运行说明

  1. 在 PostgreSQL 实例中创建测试数据库。
  2. 按顺序在多个会话(psql 连接)中执行上述 SQL。
  3. 使用排查语句观察锁阻塞情况。

技术点总结

  • INSERT 获取 ROW EXCLUSIVE 表级锁。
  • CREATE INDEX(不带 CONCURRENTLY)获取 SHARE 表级锁,与 ROW EXCLUSIVE 冲突。
  • CREATE INDEX CONCURRENTLY 仅获取 SHARE UPDATE EXCLUSIVE,不与 DML 冲突。
  • pg_blocking_pids() 和 pg_locks 是锁排查的核心工具。
  • lock_timeout 可防止锁等待无限期持续。

多语言示例

以下分别使用 Node.js、Go、Python 和 Java 演示 PostgreSQL 锁机制相关的操作,包括:

  • 设置 lock_timeout
  • 执行 SELECT ... FOR UPDATE 行级锁
  • 查询锁等待情况(pg_locks 和 pg_blocking_pids)

所有示例均基于 PostgreSQL 14+,使用各语言主流驱动或 ORM。

Node.js(使用 pg 驱动)

// 依赖: npm install pg
const { Client } = require('pg');

const config = {
  host: 'localhost',
  port: 5432,
  database: 'testdb',
  user: 'testuser',
  password: 'testpass',
};

async function run() {
  const client = new Client(config);
  await client.connect();

  try {
    // 1. 设置锁超时
    await client.query('SET lock_timeout = \'5s\'');

    // 2. 开启事务并获取行锁
    await client.query('BEGIN');
    const res = await client.query(
      'SELECT * FROM t1 WHERE id = 1 FOR UPDATE'
    );
    console.log('锁定行:', res.rows[0]);

    // 3. 模拟业务处理(保持事务未提交)
    await new Promise(resolve => setTimeout(resolve, 10000)); // 10秒

    await client.query('COMMIT');
  } catch (err) {
    console.error('错误:', err.message);
    await client.query('ROLLBACK');
  } finally {
    await client.end();
  }
}

run();

// 配套查询锁等待(可另开连接执行)
async function checkLocks() {
  const client = new Client(config);
  await client.connect();
  const res = await client.query(`
    SELECT pid, mode, granted, relation::regclass, query
    FROM pg_locks
    LEFT JOIN pg_stat_activity USING (pid)
    WHERE locktype = 'relation'
  `);
  console.table(res.rows);
  await client.end();
}

Go(使用 pgx 驱动)

// 依赖: go get github.com/jackc/pgx/v5
package main

import (
	"context"
	"fmt"
	"log"
	"time"

	"github.com/jackc/pgx/v5"
)

func main() {
	ctx := context.Background()
	conn, err := pgx.Connect(ctx, "postgres://testuser:testpass@localhost:5432/testdb")
	if err != nil {
		log.Fatal(err)
	}
	defer conn.Close(ctx)

	// 1. 设置锁超时
	_, err = conn.Exec(ctx, "SET lock_timeout = '5s'")
	if err != nil {
		log.Fatal(err)
	}

	// 2. 开启事务并获取行锁
	tx, err := conn.Begin(ctx)
	if err != nil {
		log.Fatal(err)
	}
	defer tx.Rollback(ctx)

	var id int
	var name string
	err = tx.QueryRow(ctx, "SELECT id, name FROM t1 WHERE id = 1 FOR UPDATE").Scan(&id, &name)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("锁定行: id=%d, name=%s\n", id, name)

	// 模拟业务处理
	time.Sleep(10 * time.Second)

	err = tx.Commit(ctx)
	if err != nil {
		log.Fatal(err)
	}
}

// 查询锁等待(可单独执行)
func checkLocks() {
	ctx := context.Background()
	conn, _ := pgx.Connect(ctx, "postgres://testuser:testpass@localhost:5432/testdb")
	defer conn.Close(ctx)

	rows, _ := conn.Query(ctx, `
		SELECT pid, mode, granted, relation::regclass
		FROM pg_locks
		WHERE locktype = 'relation'
	`)
	defer rows.Close()
	for rows.Next() {
		var pid int, mode string, granted bool, rel string
		rows.Scan(&pid, &mode, &granted, &rel)
		fmt.Printf("PID=%d mode=%s granted=%v rel=%s\n", pid, mode, granted, rel)
	}
}

Python(使用 psycopg2)

# 依赖: pip install psycopg2-binary
import psycopg2
import time

config = {
    'host': 'localhost',
    'port': 5432,
    'dbname': 'testdb',
    'user': 'testuser',
    'password': 'testpass'
}

def run():
    conn = psycopg2.connect(**config)
    conn.autocommit = False
    cur = conn.cursor()

    try:
        # 1. 设置锁超时
        cur.execute("SET lock_timeout = '5s'")

        # 2. 开启事务并获取行锁
        cur.execute("SELECT id, name FROM t1 WHERE id = 1 FOR UPDATE")
        row = cur.fetchone()
        print(f"锁定行: id={row[0]}, name={row[1]}")

        # 模拟业务处理
        time.sleep(10)

        conn.commit()
    except Exception as e:
        print("错误:", e)
        conn.rollback()
    finally:
        cur.close()
        conn.close()

if __name__ == "__main__":
    run()

# 查询锁等待(独立函数)
def check_locks():
    conn = psycopg2.connect(**config)
    cur = conn.cursor()
    cur.execute("""
        SELECT pid, mode, granted, relation::regclass
        FROM pg_locks
        WHERE locktype = 'relation'
    """)
    for row in cur.fetchall():
        print(f"PID={row[0]} mode={row[1]} granted={row[2]} rel={row[3]}")
    cur.close()
    conn.close()

Java(使用 JDBC)

// 依赖: org.postgresql:postgresql:42.7.3
import java.sql.*;
import java.util.Properties;

public class PgLockDemo {
    public static void main(String[] args) throws Exception {
        String url = "jdbc:postgresql://localhost:5432/testdb";
        Properties props = new Properties();
        props.setProperty("user", "testuser");
        props.setProperty("password", "testpass");

        try (Connection conn = DriverManager.getConnection(url, props)) {
            conn.setAutoCommit(false);

            // 1. 设置锁超时
            try (Statement stmt = conn.createStatement()) {
                stmt.execute("SET lock_timeout = '5s'");
            }

            // 2. 获取行锁
            String sql = "SELECT id, name FROM t1 WHERE id = 1 FOR UPDATE";
            try (PreparedStatement pstmt = conn.prepareStatement(sql);
                 ResultSet rs = pstmt.executeQuery()) {
                if (rs.next()) {
                    System.out.printf("锁定行: id=%d, name=%s%n",
                            rs.getInt("id"), rs.getString("name"));
                }
            }

            // 模拟业务处理
            Thread.sleep(10000);

            conn.commit();
        } catch (SQLException e) {
            System.err.println("错误: " + e.getMessage());
            // 回滚由 try-with-resources 自动处理,但需注意
        }
    }

    // 查询锁等待(独立方法)
    public static void checkLocks() throws Exception {
        String url = "jdbc:postgresql://localhost:5432/testdb";
        Properties props = new Properties();
        props.setProperty("user", "testuser");
        props.setProperty("password", "testpass");
        try (Connection conn = DriverManager.getConnection(url, props);
             Statement stmt = conn.createStatement();
             ResultSet rs = stmt.executeQuery(
                     "SELECT pid, mode, granted, relation::regclass FROM pg_locks WHERE locktype = 'relation'")) {
            while (rs.next()) {
                System.out.printf("PID=%d mode=%s granted=%b rel=%s%n",
                        rs.getInt("pid"), rs.getString("mode"),
                        rs.getBoolean("granted"), rs.getString("relation"));
            }
        }
    }
}

多语言对比

特性Node.js (pg)Go (pgx)Python (psycopg2)Java (JDBC)
驱动/库pgpgx/v5psycopg2postgresql JDBC 驱动
连接方式Client 对象pgx.Connectpsycopg2.connectDriverManager.getConnection
事务控制client.query('BEGIN/COMMIT')conn.Begin() / tx.Commit()conn.commit() / conn.rollback()conn.setAutoCommit(false) / conn.commit()
设置超时client.query("SET lock_timeout")conn.Exec(ctx, "SET lock_timeout")cur.execute("SET lock_timeout")stmt.execute("SET lock_timeout")
行锁语法SELECT ... FOR UPDATE同同同
异步支持原生 async/await原生 context不支持(同步)同步(可配合 CompletableFuture)
连接池pg.Poolpgxpoolpsycopg2.poolHikariCP 等
错误处理try-catch返回 errortry-excepttry-catch
常见用途Web 后端、Serverless高性能微服务数据科学、脚本企业级应用

运行说明

  1. 确保本地 PostgreSQL 运行,创建测试表:
    CREATE TABLE t1 (id INT PRIMARY KEY, name TEXT);
    INSERT INTO t1 VALUES (1, 'Alice'), (2, 'Bob');
    
  2. 各语言示例均需安装对应依赖(见注释)。
  3. 分别运行,观察事务中 FOR UPDATE 加锁,并在另一个会话中尝试更新同一行(将被阻塞直至超时)。
  4. 使用提供的 checkLocks 函数查看锁信息。

技术点总结

  • 所有语言均通过标准 SQL 实现锁控制,无特殊方言。
  • 事务隔离级别默认 READ COMMITTED,FOR UPDATE 保证行锁。
  • lock_timeout 可在会话或事务级别设置,统一生效。
  • 锁查询依赖 pg_locks 和 pg_stat_activity,跨语言一致。
  • 各语言驱动均支持连接参数设置(如 connect_timeout 等)。

项目难点与解决方案

核心难点

  • PostgreSQL 锁机制层级复杂(表级、行级、轻量级、自旋锁),不同层级的锁相互独立又相互影响,故障排查时难以快速定位根因。
  • 锁的共享内存有上限(max_locks_per_transaction),在高并发或复杂事务场景下可能耗尽。
  • Fast-Path 机制在索引过多的表上容易失效,导致性能回退。
  • 自旋锁问题隐蔽性强,通常需要借助 perf 等工具才能定位。

解决方案

  • 建立锁监控体系,定期查询 pg_locks 和 pg_stat_activity,及时发现锁等待和阻塞链。
  • 对大表实施分区策略,分散 VACUUM 压力。
  • 使用 CREATE INDEX CONCURRENTLY 替代普通 CREATE INDEX,避免阻塞生产流量。
  • 合理设置 lock_timeout,防止锁队列无限堆积。
  • 定期审查索引使用情况,移除冗余索引,减少 fast-path 失效风险。
  • 升级到 PostgreSQL 14 及以上版本,规避 old_snapshot_threshold 的自旋锁性能问题。

广度

本文涵盖了 PostgreSQL 锁机制的完整金字塔——从面向 SQL 层的表级锁和行级锁,到内核层面的轻量级锁和自旋锁,并涉及了 Fast-Path 优化、共享内存限制、锁超时配置等横向知识点。

深度

深入分析了每种锁模式的冲突矩阵、典型应用场景和性能影响,并通过真实案例(old_snapshot_threshold 自旋锁风暴)展示了锁问题排查的完整思路——从现象观察到工具定位再到根因分析。

复杂度

锁机制本身是数据库内核中最复杂的子系统之一。本文在保持技术准确性的前提下,将复杂度分层拆解,使读者能够循序渐进地理解从最上层(表级锁)到最底层(自旋锁)的完整锁体系。

官方文档

参考链接

总结

PostgreSQL 的锁机制是一个自底向上的完整体系:自旋锁保护最底层的临界变量,采用忙等待策略;轻量级锁(LWLock)保护共享内存结构(如缓冲区、哈希表);表级锁控制对表级对象的并发访问,共有八种模式,通过冲突矩阵定义互斥关系;行级锁存储在磁盘元组头中,用于控制行级别的并发操作。

Fast-Path 机制通过将弱锁排除在共享锁表之外来优化性能,但每个后端仅限 16 个槽位。锁的共享内存受 max_locks_per_transaction 限制,锁等待队列采用 FIFO 策略。在实践中,应善用 CREATE INDEX CONCURRENTLY 避免阻塞 DML,合理设置 lock_timeout 防止锁队列堆积,并在 PostgreSQL 14 之前慎用 old_snapshot_threshold 以避免自旋锁性能问题。

避开ANSYS SOLID65钢筋模拟的坑:从材料TB到实常数R的完整配置清单 本文详细解析了ANSYS SOLID65单元在钢筋混凝土模拟中的关键配置与优化技巧,涵盖材料TB设置、实常数R配置及收敛性优化。特别针对钢筋建模的分布式钢筋模型进行深度解析,提供实战验证的参数设置手册,帮助工程师避免常见错误,提升模拟精度与效率。 阅读详情

相关推荐

2021年秋招面经分享·字节跳动【芯片工程师-系统架构】

前言 如果想参考本人背景情况的可见这篇概述 ============================ 正文开始 ============================== 【时间线】 7.29 字节一面 8.4 字节二面 8.11 字节三面 PS:红字表示我当时没答出来的问题,蓝字表示面试官的解答。 面经分享 一面·技术面【1小时5分钟】   面试官京腔重,聊high了所以没时间撕代码了,但是华为应该不会这样。 自我介绍; 介绍一下APB; APB中3个状态的跳转是怎么跳转的; AXI中va

qq_43132308的博客 5616

PostgreSQL如何查看事务所占有的锁?

PostgreSQL如何查看事务所占有的锁?

贾欣晓的博客 9950

国家大地2000坐标系投影坐标转高德地图经纬度坐标-【高斯投影坐标】

国家大地2000坐标系投影坐标转换成高德经纬度坐标显示画图。

devil_zhuzhu的博客 1万+

postgresql 如何进行表有锁查询?

pgsql进行有锁进程查询

weixin_43492708的博客 2610

rfid标签技术原理

奇怪的是,RFID标签技术原理的深入剖析不仅揭示了其背后的奥秘,也让我们更加全面地认识到这一技术在数字化转型中的重要性和潜力。随着技术的不断进步和应用场景的不断拓展,RFID标签将在更多领域发挥重要作用,为物联网、智能制造等领域的发展注入新的活力。这意味着在一个读写器的覆盖范围内,可以同时读取多个RFID标签的数据,大大提高了数据收集的速度和效率。:RFID标签的“大脑”,负责存储和处理数据。芯片内部集成了存储器、控制逻辑和射频接口等电路,能够接收来自读写器的射频信号,执行相应的数据处理和通信任务。

x3088的博客 2159

VScode中配置 C/C++ 环境,超级详细,问题分析全面,绝对好用

VScode中配置 C/C++ 环境,超级详细,问题分析全面,绝对好用 VScode中配置 C/C++ 环境 你好! 这是你第一次使用 Markdown编辑器 所展示的欢迎页。如果你想学习如何使用Markdown编辑器, 可以仔细阅读这篇文章,了解一下Markdown的基本语法知识。 1. 下载编辑器VScode 1.官网:https://code.visualstudio.com/ 2. 安装VScode(建议附加任务全部勾选; 2. 下载编译器MinGW并解压 官网页面:https://www.mi

m0_46147064的博客 8万+

Postgres 数据库查询表锁,释放表锁

Postgres 数据库查询表锁,释放表锁

ispringmw的博客 4814

AES攻击方法 :差分密码分析 boomerang attack飞去来器攻击

AES具有123比特的分组长度,三种可选的密钥长度(128,192,256) AES是一个迭代型密码,轮数依赖于密钥长度。如果密钥长度为128比特,则轮数为10;如果密钥长度为192比特,则轮数为12;如果密钥长度为256比特,则轮数为14; 混淆(Confusion) 混淆是为了让密钥与密文的统计信息之间的关系变得复杂,从而增加通过统计方法进行攻击的难度。混淆可以通过各种代换算法实现,如S盒、乘法(加)与异或等运算组合,为非线性部件。 扩散(Diffusion) 扩散是为了让明文的单个比特影像到

m0_67105022的博客 3492

PostgreSQL-锁机制详解

PostgreSQL表级锁模式与行级锁模式

贾欣晓的博客 4101

GEE:关于遥感生态指数(RSEI)的若干疑问

本文记录了若干关于在 Google Earth Engine (GEE)平台上,计算基于主成分分析(PCA)的遥感生态指数(RSEI)的问题。并记录了解决这些问题的一些答案。问题包括,主成分分析的特征值的正负,主成分的占比,去云后空值像素的处理方法,异常值处理方法,如何看载荷等。

养乐多的博客 3419

Open3D——RANSAC 三维点云空间直线拟合【2025最新版】

使用RANSAC算法在点云数据中拟合三维空间直线的python代码实现。博客长期更新,本文最新更新时间为:2025年3月23日。

点云侠的博客 7354

【Postgres】Postgres查看死锁及解决方法

【代码】【Postgres】Postgres查看死锁及解决方法。

daohangtaiqian的博客 4304

零信任实战:Tetragon阻断容器运行时0day攻击

零信任容器安全防御:Tetragon实战探析 摘要:针对容器0day攻击防护难题,本文提出基于Tetragon的eBPF零信任防御方案。通过内核级系统调用监控(如execve、mount),结合动态行为分析,实现3ms级攻击阻断。实战案例显示:成功拦截Log4j2漏洞利用(JNDI注入前阻断)和未知容器逃逸(/proc敏感路径防护)。某银行部署后实现100%攻击拦截率,性能损耗<3%,安全响应时间从30分钟降至5秒。方案突破传统防御瓶颈,为容器运行时安全提供新范式。

python,运维,测试,数据分析,人工智能 766

postgresql查询锁表以及解除锁表

postgresql查询锁表以及解除锁表

林胖的专栏 6224

假人有限元模型

假人模型,车辆碰撞假人有限元模型,适合做乘员安全的研究者利用

Statistics and Machine Learning Toolbox

Statistics and Machine Learning Toolbox(带书签),MATLAB统计学习工具

PostgreSQL插件

以上是一些常用的PostgreSQL插件,它们可以帮助开发者扩展PostgreSQL的功能,提高数据库的性能和灵活性。pgAudit是一个用于审计PostgreSQL数据库的插件,可以记录每个SQL语句的执行情况,包括执行时间、执行用户、执行结果等信息。是一个 PostgreSQL pgaudit 插件的设置命令,它的作用是启用 pgaudit 插件并将所有数据库操作记录到日志中。以上就是pgAudit插件的使用方法,通过使用pgAudit插件,可以方便地对PostgreSQL数据库进行审计和监控。

喝醉酒的小白 9074

锂电池充电器电路设计 里面有电路

锂电池充电器电路设计 里面有电路 3.7V升压到5V电路设计。

PostgreSQL中的行锁

FOR UPDATE:该模式允许修改任何元组字段,甚至删除整个元组FOR NO KEY UPDATE:对除主(唯一)键外的字段更新,此更改也不影响外键FOR SHARE:当我们需要读取一行,但不允许其他事务更改它时,使用该模式FOR KEY SHARE:读该行的键值,但只允许对除键外的其他字段更新。在检查外键约束时会自动使用该锁。

魂醉的一亩二分地 3773

Outlook Express 6 (OE6) for Win7/Win8/Win10

英文版 Outlook Express6.0, 兼容Win7/Win10/WinXP,解压就可用。

pgsql查看锁表(pg查询锁表并删除)

与” ROW SHARE, ROW EXCLUSIVE, SHARE UPDATE EXCLUSIVE, SHARE, SHARE ROW EXCLUSIVE, EXCLUSIVE, ACCESS EXCLUSIVE”模式冲突,这种索模式仅能与Access Share 模式并发,换句话说,只有读操作可以和持有”EXCLUSIVE”锁的事务并行;查询命令(Select command)将会在它查询的表上获取”Access Shared” 锁,一般地,任何一个对表上的只读查询操作都将获取这种类型的锁。

u013817721的博客 5434

PostgreSQL中的锁--spinLock、LWLock、Lock

一、PostgreSQL中的锁 PostgreSQL中根据不同对象,不同使用场景,使用到了三种锁,即spinLock,LWLock,Lock 1.spinLock SpinLock也就是所谓的自旋锁,是并发场景下(多进程/线程),保护共享资源的一种机制。实现的成本最低,一般是使用基于硬件的TAS操作(test-and-set来实现的)。显著的特点是审请锁的进程一直在尝试能否加锁成功,只有等到持有锁的线程释放锁之后才可以获取锁。在等待锁的过程中进程并不是切入内核态进行sleep,而是忙等待,即忙循环–旋转–等

qq_43687755的博客 4712

【项目实战】Postgresql数据库中出现锁表如何解决

本章介绍生产环境中Postgresql数据库出现锁表情况,应该如何解决?

Java 技术专栏,从入门到精通,熟悉企业级开发 6514

PostgreSQL锁机制——行级锁

PostgreSQL除了表级锁之外,还有行级锁。需要注意的是一个事务可能会在相同的行上保持冲突的 锁,甚至是在不同的子事务中。但是除此之外,两个事务永远不可能在相同的行上持有冲突的锁。 行级锁不影响数据查询,它们只阻塞对同一行的写入者和加锁者。 FOR UPDATE FOR UPDATE会导致由SELECT语句检索到的行被锁定,就好像它们要被更新。这可以 阻止它们被其他事务锁定、修改或者删除,一直...

Focus on PostgreSQL 4884

在 PostgreSQL 中,查看表是否被锁住以及解锁语句

在 PostgreSQL 中,查看表是否被锁住以及解锁语句

qq_43804077的博客 7120

pg死锁查询

pg查询死锁的方法

qq_42051115的博客 2183

postgresql数据库查询锁表信息及解锁

一、查询PG_STAT_ACTIVITY的信息 select T.PID, T.STATE, T.QUERY, T.WAIT_EVENT_TYPE, T.WAIT_EVENT, T.QUERY_START from PG_STAT_ACTIVITY T where T.DATNAME = '数据库名'; 二、查询PG_STAT_ACTIVITY中的死锁信息 select T.PID, T.STATE, T.QUERY, T.WAIT_EVENT_TYPE, T.WAIT_EVEN

甜到你了么的博客 1万+

PostgreSQL查看是否锁表

pgsql是否锁表查询

IT_Hierarch的博客 1720

【pg数据库】锁表问题(转载)

PG 数据库锁表问题

weixin_55282974的博客 2370

【PostgreSQL & OpenGauss 】数据库查询表锁,释放表锁

在数据库中,表锁是用于控制对表的并发访问的一种机制。不同的数据库系统(如 PostgreSQL 和 OpenGauss)可能具有不同的表锁管理机制。下面分别介绍如何在 PostgreSQL 和 OpenGauss 中查询和释放表锁。

沁禹的博客 3071
上一篇: PostgreSQL笔记29:事务隔离级别深度解析——从MVCC快照到SSI可串行化
下一篇: PostgreSQL笔记31:咨询锁(Advisory Lock)在高并发场景下的原理与实践
Wang's Blog
博客等级 码龄12年 7438粉丝 2421原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Wang's Blog

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值