纲要
- 行级锁(Row-Level Lock)在高并发更新场景下的挑战
UPDATE与行级锁的等待机制- MVCC 机制下的表膨胀问题
SELECT ... FOR UPDATE的优化选项NOWAIT:立即报错而非等待SKIP LOCKED:跳过已被锁定的行- 适用版本说明
- 咨询锁(Advisory Lock)
- 概念与设计哲学
- 会话级(Session-Level)与事务级(Transaction-Level)咨询锁
- 核心 API 函数
- 与行级锁的对比
- 性能对比与适用场景
- 最佳实践与注意事项
行级锁在高并发场景下的瓶颈
在秒杀、抢购等典型高并发场景中,大量并发请求同时更新同一条记录(如库存扣减)。以商品库存表为例:
CREATE TABLE inventory (
product_id INT PRIMARY KEY,
stock INT NOT NULL
);
INSERT INTO inventory VALUES (1, 100);
当大量并发事务执行以下更新操作时:
BEGIN;
UPDATE inventory SET stock = stock - 1 WHERE product_id = 1 AND stock > 0;
COMMIT;
PostgreSQL 的行级锁(Row-Level Lock)机制会导致所有试图更新同一行的事务依次排队等待[reference:0]。由于 UPDATE 操作本身是排他的(Exclusive),后到的事务必须等待前一个事务提交或回滚后才能获取锁并执行更新[reference:1]。这种机制在并发量较大时会产生严重的锁等待(Lock Waiting),进而导致事务响应时间急剧上升、连接池耗尽等问题。
此外,PostgreSQL 的 MVCC 机制使得每次 UPDATE 都会产生一条新的元组版本(Tuple Version),旧版本在 VACUUM 之前不会被回收[reference:2]。高频更新同一行会导致表膨胀(Table Bloat),进一步影响查询性能。
SELECT … FOR UPDATE 的优化选项
PostgreSQL 为行级锁提供了两个优化选项,用于避免不必要的等待[reference:3]。
NOWAIT
NOWAIT 选项指定当请求的行无法立即被锁定时,语句直接报错而非等待[reference:4]。
-- 会话 1:锁定行
BEGIN;
SELECT * FROM inventory WHERE product_id = 1 FOR UPDATE;
-- 会话 2:尝试锁定同一行,立即报错
BEGIN;
SELECT * FROM inventory WHERE product_id = 1 FOR UPDATE NOWAIT;
-- ERROR: could not obtain lock on row in relation "inventory"
NOWAIT 适用于“抢不到就放弃”的场景,避免客户端长时间阻塞等待。
SKIP LOCKED
SKIP LOCKED 选项在 PostgreSQL 9.5 中引入[reference:5][reference:6]。指定该选项后,SELECT ... FOR UPDATE 会跳过所有无法立即锁定的行,仅返回可以被成功锁定的行[reference:7]。
-- 会话 1:锁定 product_id = 1 的行
BEGIN;
SELECT * FROM inventory WHERE product_id = 1 FOR UPDATE;
-- 会话 2:跳过已被锁定的行,查询其他可锁定的行
BEGIN;
SELECT * FROM inventory WHERE product_id IN (1, 2, 3) FOR UPDATE SKIP LOCKED;
-- 仅返回 product_id 为 2 和 3 的行
SKIP LOCKED 适用于队列消费(Queue-like Table)等场景,多个消费者可以并行处理不同的任务,互不阻塞[reference:8]。
版本说明:
SKIP LOCKED自 PostgreSQL 9.5 起可用[reference:9]。NOWAIT的支持更早,在 9.5 之前已存在。
咨询锁(Advisory Lock)
概念与设计哲学
PostgreSQL 提供了一种由应用程序自定义含义的锁机制,称为咨询锁(Advisory Lock)[reference:10]。与行级锁不同,咨询锁不与数据库表中的任何行绑定,其锁定的资源是一个由应用程序定义的 64 位整型键值(或两个 32 位整型键值组合)[reference:11]。系统并不强制使用咨询锁——它完全由应用程序负责正确使用[reference:12]。
咨询锁的核心优势在于:
- 轻量级:不需要检索和锁定表中的行,开销远小于行级锁[reference:13]。
- 无表膨胀:不产生 MVCC 元组版本,不会导致表膨胀[reference:14]。
- 自动清理:会话结束或事务结束时,持有的咨询锁会被自动释放[reference:15]。
会话级与事务级咨询锁
咨询锁分为两种级别[reference:16][reference:17]:
| 级别 | 特点 | 释放时机 |
|---|---|---|
| 会话级(Session-Level) | 不与事务绑定,跨事务持有 | 显式调用解锁函数,或会话结束 |
| 事务级(Transaction-Level) | 与当前事务绑定 | 事务提交或回滚时自动释放 |
会话级锁适用于需要跨事务持有锁的场景,但需注意会话级锁不会随事务回滚而释放[reference:18][reference:19]。事务级锁则更加安全,推荐在大多数场景下优先使用。
核心 API 函数
PostgreSQL 提供了一系列咨询锁管理函数[reference:20][reference:21]:
-- 会话级排他锁(阻塞式)
pg_advisory_lock(key bigint) RETURNS void
pg_advisory_lock(key1 int, key2 int) RETURNS void
-- 会话级共享锁(阻塞式)
pg_advisory_lock_shared(key bigint) RETURNS void
pg_advisory_lock_shared(key1 int, key2 int) RETURNS void
-- 会话级排他锁(非阻塞式,尝试获取)
pg_try_advisory_lock(key bigint) RETURNS boolean
pg_try_advisory_lock(key1 int, key2 int) RETURNS boolean
-- 会话级共享锁(非阻塞式,尝试获取)
pg_try_advisory_lock_shared(key bigint) RETURNS boolean
pg_try_advisory_lock_shared(key1 int, key2 int) RETURNS boolean
-- 会话级解锁
pg_advisory_unlock(key bigint) RETURNS boolean
pg_advisory_unlock(key1 int, key2 int) RETURNS boolean
pg_advisory_unlock_all() RETURNS void
-- 事务级排他锁(阻塞式)
pg_advisory_xact_lock(key bigint) RETURNS void
pg_advisory_xact_lock(key1 int, key2 int) RETURNS void
-- 事务级共享锁(阻塞式)
pg_advisory_xact_lock_shared(key bigint) RETURNS void
pg_advisory_xact_lock_shared(key1 int, key2 int) RETURNS void
-- 事务级排他锁(非阻塞式,尝试获取)
pg_try_advisory_xact_lock(key bigint) RETURNS boolean
pg_try_advisory_xact_lock(key1 int, key2 int) RETURNS boolean
参数说明:
- 单参数版本:
key为BIGINT类型,取值范围-9223372036854775808到9223372036854775807。 - 双参数版本:
key1和key2均为INT类型,两个 32 位键值组合成一个 64 位锁空间(两个键空间不重叠)[reference:22]。
返回值说明:
void返回值的函数为阻塞式:若锁已被占用,调用会一直等待直到获取锁。boolean返回值的函数为非阻塞式:获取成功返回true,否则立即返回false。
咨询锁的使用示例
以下示例演示了事务级咨询锁的基本用法:
-- 会话 1:获取事务级排他咨询锁,锁键值为 1
BEGIN;
SELECT pg_advisory_xact_lock(1);
-- 执行业务逻辑...
COMMIT; -- 事务结束,锁自动释放
-- 会话 2:尝试获取同一个锁键值
BEGIN;
SELECT pg_try_advisory_xact_lock(1);
-- 返回 false,表示锁已被占用
COMMIT;
咨询锁的键值与数据库对象无关[reference:23]。在实际应用中,可以将业务实体的 ID(如订单号、商品 ID)作为锁键值,实现对业务资源的精细化并发控制。
咨询锁与行级锁的对比
| 对比维度 | 行级锁(SELECT FOR UPDATE) | 咨询锁(Advisory Lock) |
|---|---|---|
| 锁目标 | 表中的行 | 应用程序定义的整型键值 |
| 是否需要检索行 | 是 | 否 |
| 是否产生 MVCC 版本 | 是(UPDATE 时) | 否 |
| 表膨胀风险 | 高 | 无 |
| 锁开销 | 较重 | 轻量 |
| 自动释放 | 事务结束 | 会话/事务结束 |
| 适用版本 | 所有版本 | PostgreSQL 8.2+ |
性能对比
根据实际测试数据,在高并发秒杀场景下,咨询锁的性能优势非常显著[reference:24]。
测试场景:单表单行高频更新,模拟 1 万并发请求。
| 方案 | 吞吐量(TPS) | CPU 开销 | 说明 |
|---|---|---|---|
传统 UPDATE + 行级锁 | ~8.6 万/秒 | 高 | 大量事务排队等待 |
| 咨询锁方案 | ~39 万/秒 | 低 | 无需检索行,锁冲突极小 |
咨询锁之所以能达到如此高的吞吐量,根本原因在于它不需要访问表中的行——锁操作完全在共享内存中完成,不涉及缓冲区扫描、元组可见性判断或 MVCC 版本生成[reference:25]。
最佳实践与注意事项
-
优先使用事务级咨询锁:事务级锁(
pg_advisory_xact_*)在事务提交或回滚时自动释放,避免锁泄漏(Lock Leakage)[reference:26]。 -
锁键值设计:确保锁键值在应用程序范围内具有唯一且明确的语义。例如,使用
product_id作为锁键值,或使用(table_oid, row_id)的组合键。 -
非阻塞式优先:在秒杀等场景中,优先使用
pg_try_advisory_*系列函数,获取锁失败时立即返回,避免客户端长时间等待[reference:27]。 -
注意锁的数据库范围:咨询锁是数据库级(Database-Level)的,不同数据库之间的锁键值空间相互独立。
-
监控与诊断:通过
pg_locks视图可以查看当前持有的咨询锁:
SELECT locktype, database, objid, mode, granted
FROM pg_locks
WHERE locktype = 'advisory';
API 速览
本节汇总博客中涉及的所有核心 API。
行级锁相关
SELECT ... FOR UPDATE [NOWAIT | SKIP LOCKED]
- 所属模块:PostgreSQL SQL 语法(
SELECT语句的锁定子句) - 语法:
SELECT ... FROM table_name ... FOR UPDATE [NOWAIT | SKIP LOCKED]
- 参数说明:
NOWAIT:无法立即获取行锁时立即报错,不等待[reference:28]。SKIP LOCKED:跳过已被锁定的行,仅返回可锁定的行[reference:29]。
- 返回值:返回被锁定的行集。
- 适用版本:
NOWAIT早期版本即支持;SKIP LOCKED自 PostgreSQL 9.5 引入[reference:30]。
示例:
-- NOWAIT:立即报错
BEGIN;
SELECT * FROM inventory WHERE product_id = 1 FOR UPDATE NOWAIT;
COMMIT;
-- SKIP LOCKED:跳过被锁定的行
BEGIN;
SELECT * FROM inventory WHERE stock > 0 FOR UPDATE SKIP LOCKED;
COMMIT;
咨询锁核心函数
pg_advisory_xact_lock(key bigint)
- 所属模块:PostgreSQL 咨询锁函数
- 方法签名:
pg_advisory_xact_lock(key bigint) RETURNS void - 参数:
key- 64 位整型锁键值 - 返回值:无
- 说明:获取事务级排他咨询锁。若锁已被占用,阻塞等待[reference:31]。
pg_try_advisory_xact_lock(key bigint)
- 所属模块:PostgreSQL 咨询锁函数
- 方法签名:
pg_try_advisory_xact_lock(key bigint) RETURNS boolean - 参数:
key- 64 位整型锁键值 - 返回值:
true表示获取成功,false表示锁已被占用 - 说明:非阻塞式尝试获取事务级排他咨询锁[reference:32]。
pg_advisory_lock(key bigint)
- 所属模块:PostgreSQL 咨询锁函数
- 方法签名:
pg_advisory_lock(key bigint) RETURNS void - 参数:
key- 64 位整型锁键值 - 返回值:无
- 说明:获取会话级排他咨询锁。若锁已被占用,阻塞等待[reference:33]。
pg_try_advisory_lock(key bigint)
- 所属模块:PostgreSQL 咨询锁函数
- 方法签名:
pg_try_advisory_lock(key bigint) RETURNS boolean - 参数:
key- 64 位整型锁键值 - 返回值:
true表示获取成功,false表示锁已被占用 - 说明:非阻塞式尝试获取会话级排他咨询锁[reference:34]。
pg_advisory_unlock(key bigint)
- 所属模块:PostgreSQL 咨询锁函数
- 方法签名:
pg_advisory_unlock(key bigint) RETURNS boolean - 参数:
key- 64 位整型锁键值 - 返回值:
true表示释放成功,false表示该会话未持有该锁 - 说明:释放会话级排他咨询锁[reference:35]。
pg_advisory_unlock_all()
- 所属模块:PostgreSQL 咨询锁函数
- 方法签名:
pg_advisory_unlock_all() RETURNS void - 参数:无
- 返回值:无
- 说明:释放当前会话持有的所有咨询锁[reference:36]。
Demo 简单示例
以下是一个基于 Node.js 和 pg 驱动实现的秒杀扣库存完整示例,演示了咨询锁在高并发场景下的应用。
项目结构
advisory-lock-demo/
├── package.json
├── init.sql
└── index.js
初始化脚本(init.sql)
-- 创建商品库存表
CREATE TABLE IF NOT EXISTS products (
id INT PRIMARY KEY,
name VARCHAR(100) NOT NULL,
stock INT NOT NULL CHECK (stock >= 0)
);
-- 插入测试数据:商品 ID 为 1,库存 10 件
INSERT INTO products (id, name, stock) VALUES (1, 'iPhone 15 Pro', 10)
ON CONFLICT (id) DO UPDATE SET stock = EXCLUDED.stock;
package.json
{
"name": "advisory-lock-demo",
"version": "1.0.0",
"description": "PostgreSQL Advisory Lock Demo for Seckill",
"main": "index.js",
"scripts": {
"start": "node index.js"
},
"dependencies": {
"pg": "^8.11.0"
}
}
完整示例代码(index.js)
const { Pool } = require('pg');
const pool = new Pool({
host: 'localhost',
port: 5432,
database: 'testdb',
user: 'postgres',
password: 'postgres',
max: 20, // 连接池最大连接数
});
// 使用事务级咨询锁进行秒杀扣库存
async function seckillWithAdvisoryLock(productId, quantity) {
const client = await pool.connect();
try {
await client.query('BEGIN');
// 1. 尝试获取事务级排他咨询锁,锁键值为商品 ID
// 使用 pg_try_advisory_xact_lock 非阻塞式获取
const lockResult = await client.query(
'SELECT pg_try_advisory_xact_lock($1) AS locked',
[productId]
);
if (!lockResult.rows[0].locked) {
// 获取锁失败,说明有其他事务正在处理该商品
await client.query('ROLLBACK');
return { success: false, reason: '商品正在处理中,请稍后重试' };
}
// 2. 查询当前库存
const stockResult = await client.query(
'SELECT stock FROM products WHERE id = $1 FOR UPDATE',
[productId]
);
if (stockResult.rows.length === 0) {
await client.query('ROLLBACK');
return { success: false, reason: '商品不存在' };
}
const currentStock = stockResult.rows[0].stock;
// 3. 检查库存是否充足
if (currentStock < quantity) {
await client.query('ROLLBACK');
return { success: false, reason: '库存不足' };
}
// 4. 扣减库存
await client.query(
'UPDATE products SET stock = stock - $1 WHERE id = $2',
[quantity, productId]
);
await client.query('COMMIT');
return { success: true, remainingStock: currentStock - quantity };
} catch (error) {
await client.query('ROLLBACK');
console.error('秒杀失败:', error.message);
return { success: false, reason: error.message };
} finally {
client.release();
}
}
// 模拟并发秒杀请求
async function runDemo() {
console.log('=== PostgreSQL 咨询锁秒杀 Demo ===\n');
const productId = 1;
const quantity = 1;
const concurrency = 20; // 并发请求数
console.log(`商品 ID: ${productId}, 初始库存: 10 件`);
console.log(`并发请求数: ${concurrency}\n`);
const promises = [];
for (let i = 0; i < concurrency; i++) {
promises.push(seckillWithAdvisoryLock(productId, quantity));
}
const results = await Promise.all(promises);
const successCount = results.filter(r => r.success).length;
const failCount = results.filter(r => !r.success).length;
console.log('=== 执行结果 ===');
console.log(`成功: ${successCount} 笔`);
console.log(`失败: ${failCount} 笔`);
// 验证最终库存
const finalStock = await pool.query(
'SELECT stock FROM products WHERE id = $1',
[productId]
);
console.log(`最终库存: ${finalStock.rows[0].stock} 件`);
await pool.end();
}
runDemo().catch(console.error);
对应的 PostgreSQL 原生指令注释
上述 Node.js 代码中执行的核心 SQL 操作如下:
-- 1. 非阻塞式获取事务级咨询锁
SELECT pg_try_advisory_xact_lock(1) AS locked;
-- 2. 查询库存(同时加行锁)
SELECT stock FROM products WHERE id = 1 FOR UPDATE;
-- 3. 扣减库存
UPDATE products SET stock = stock - 1 WHERE id = 1;
运行说明
- 确保本地 PostgreSQL 服务已启动,并创建数据库
testdb。 - 执行
init.sql初始化表结构和测试数据。 - 安装依赖:
npm install - 启动 Demo:
npm start
代码说明
- 锁获取:使用
pg_try_advisory_xact_lock非阻塞式获取事务级咨询锁,获取失败立即返回,避免客户端长时间等待[reference:37]。 - 事务边界:锁在事务提交或回滚时自动释放,无需显式调用解锁函数。
- 双重锁定:咨询锁控制业务并发,
FOR UPDATE确保库存查询的读一致性。 - 连接池:使用
pg.Pool管理数据库连接,支持高并发请求。
技术点总结
- 事务级咨询锁(
pg_try_advisory_xact_lock)的非阻塞式获取 - 咨询锁与行级锁(
FOR UPDATE)的协同使用 - Node.js +
pg驱动操作 PostgreSQL 的完整流程 - 高并发场景下的库存扣减原子性保证
多语言示例
以下基于上文 Node.js 示例的业务逻辑,分别给出 Go、Python 和 Java 语言的完整实现。所有示例均使用 PostgreSQL 事务级咨询锁(pg_try_advisory_xact_lock)实现非阻塞式秒杀扣库存。
Go 示例
使用 lib/pq 驱动和原生 database/sql 接口。
package main
import (
"database/sql"
"fmt"
"log"
"sync"
_ "github.com/lib/pq"
)
type SeckillResult struct {
Success bool
Reason string
RemainingStock int
}
func seckillWithAdvisoryLock(db *sql.DB, productID int, quantity int) SeckillResult {
tx, err := db.Begin()
if err != nil {
return SeckillResult{Success: false, Reason: err.Error()}
}
defer tx.Rollback()
// 1. 非阻塞式获取事务级咨询锁
var locked bool
err = tx.QueryRow(
"SELECT pg_try_advisory_xact_lock($1)", productID,
).Scan(&locked)
if err != nil {
return SeckillResult{Success: false, Reason: err.Error()}
}
if !locked {
return SeckillResult{Success: false, Reason: "商品正在处理中,请稍后重试"}
}
// 2. 查询库存(加行锁保证一致性)
var currentStock int
err = tx.QueryRow(
"SELECT stock FROM products WHERE id = $1 FOR UPDATE", productID,
).Scan(¤tStock)
if err != nil {
if err == sql.ErrNoRows {
return SeckillResult{Success: false, Reason: "商品不存在"}
}
return SeckillResult{Success: false, Reason: err.Error()}
}
if currentStock < quantity {
return SeckillResult{Success: false, Reason: "库存不足"}
}
// 3. 扣减库存
_, err = tx.Exec(
"UPDATE products SET stock = stock - $1 WHERE id = $2",
quantity, productID,
)
if err != nil {
return SeckillResult{Success: false, Reason: err.Error()}
}
if err := tx.Commit(); err != nil {
return SeckillResult{Success: false, Reason: err.Error()}
}
return SeckillResult{Success: true, RemainingStock: currentStock - quantity}
}
func main() {
connStr := "host=localhost port=5432 dbname=testdb user=postgres password=postgres sslmode=disable"
db, err := sql.Open("postgres", connStr)
if err != nil {
log.Fatal(err)
}
defer db.Close()
const productID = 1
const quantity = 1
const concurrency = 20
fmt.Printf("商品 ID: %d, 初始库存: 10 件\n", productID)
fmt.Printf("并发请求数: %d\n\n", concurrency)
var wg sync.WaitGroup
results := make(chan SeckillResult, concurrency)
for i := 0; i < concurrency; i++ {
wg.Add(1)
go func() {
defer wg.Done()
results <- seckillWithAdvisoryLock(db, productID, quantity)
}()
}
wg.Wait()
close(results)
successCount, failCount := 0, 0
for r := range results {
if r.Success {
successCount++
} else {
failCount++
}
}
fmt.Println("=== 执行结果 ===")
fmt.Printf("成功: %d 笔\n", successCount)
fmt.Printf("失败: %d 笔\n", failCount)
var finalStock int
db.QueryRow("SELECT stock FROM products WHERE id = $1", productID).Scan(&finalStock)
fmt.Printf("最终库存: %d 件\n", finalStock)
}
依赖:go get github.com/lib/pq
运行:go run main.go
Python 示例
使用 psycopg2 驱动(支持 PostgreSQL 原生协议)。
import asyncio
import asyncpg
from typing import Dict, Any
async def seckill_with_advisory_lock(pool: asyncpg.Pool, product_id: int, quantity: int) -> Dict[str, Any]:
async with pool.acquire() as conn:
async with conn.transaction():
# 1. 非阻塞式获取事务级咨询锁
locked = await conn.fetchval(
"SELECT pg_try_advisory_xact_lock($1)", product_id
)
if not locked:
return {"success": False, "reason": "商品正在处理中,请稍后重试"}
# 2. 查询库存(加行锁)
row = await conn.fetchrow(
"SELECT stock FROM products WHERE id = $1 FOR UPDATE", product_id
)
if row is None:
return {"success": False, "reason": "商品不存在"}
current_stock = row["stock"]
if current_stock < quantity:
return {"success": False, "reason": "库存不足"}
# 3. 扣减库存
await conn.execute(
"UPDATE products SET stock = stock - $1 WHERE id = $2",
quantity, product_id
)
return {"success": True, "remaining_stock": current_stock - quantity}
async def main():
pool = await asyncpg.create_pool(
host="localhost",
port=5432,
database="testdb",
user="postgres",
password="postgres",
min_size=1,
max_size=20,
)
product_id = 1
quantity = 1
concurrency = 20
print(f"商品 ID: {product_id}, 初始库存: 10 件")
print(f"并发请求数: {concurrency}\n")
tasks = [seckill_with_advisory_lock(pool, product_id, quantity) for _ in range(concurrency)]
results = await asyncio.gather(*tasks)
success_count = sum(1 for r in results if r["success"])
fail_count = len(results) - success_count
print("=== 执行结果 ===")
print(f"成功: {success_count} 笔")
print(f"失败: {fail_count} 笔")
async with pool.acquire() as conn:
final_stock = await conn.fetchval("SELECT stock FROM products WHERE id = $1", product_id)
print(f"最终库存: {final_stock} 件")
await pool.close()
if __name__ == "__main__":
asyncio.run(main())
依赖:pip install asyncpg
运行:python main.py
Java 示例
使用 JDBC 和 org.postgresql.Driver,配合 HikariCP 连接池。
import java.sql.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;
public class SeckillDemo {
static class Result {
boolean success;
String reason;
int remainingStock;
Result(boolean success, String reason, int remainingStock) {
this.success = success;
this.reason = reason;
this.remainingStock = remainingStock;
}
}
private static Result seckillWithAdvisoryLock(Connection conn, int productId, int quantity) {
try {
conn.setAutoCommit(false);
// 1. 非阻塞式获取事务级咨询锁
try (PreparedStatement stmt = conn.prepareStatement(
"SELECT pg_try_advisory_xact_lock(?)")) {
stmt.setInt(1, productId);
ResultSet rs = stmt.executeQuery();
if (rs.next() && !rs.getBoolean(1)) {
conn.rollback();
return new Result(false, "商品正在处理中,请稍后重试", 0);
}
}
// 2. 查询库存(加行锁)
int currentStock;
try (PreparedStatement stmt = conn.prepareStatement(
"SELECT stock FROM products WHERE id = ? FOR UPDATE")) {
stmt.setInt(1, productId);
ResultSet rs = stmt.executeQuery();
if (!rs.next()) {
conn.rollback();
return new Result(false, "商品不存在", 0);
}
currentStock = rs.getInt("stock");
}
if (currentStock < quantity) {
conn.rollback();
return new Result(false, "库存不足", 0);
}
// 3. 扣减库存
try (PreparedStatement stmt = conn.prepareStatement(
"UPDATE products SET stock = stock - ? WHERE id = ?")) {
stmt.setInt(1, quantity);
stmt.setInt(2, productId);
int affected = stmt.executeUpdate();
if (affected == 0) {
conn.rollback();
return new Result(false, "更新失败", 0);
}
}
conn.commit();
return new Result(true, "成功", currentStock - quantity);
} catch (SQLException e) {
try { conn.rollback(); } catch (SQLException ignored) {}
return new Result(false, e.getMessage(), 0);
} finally {
try { conn.setAutoCommit(true); } catch (SQLException ignored) {}
}
}
public static void main(String[] args) throws Exception {
String jdbcUrl = "jdbc:postgresql://localhost:5432/testdb";
String user = "postgres";
String password = "postgres";
Class.forName("org.postgresql.Driver");
// 使用 HikariCP 连接池,此处为简化直接创建 DriverManager 连接(生产环境请用连接池)
// 本示例为演示多线程并发,采用每个线程独立连接(实际建议用连接池)
int productId = 1;
int quantity = 1;
int concurrency = 20;
System.out.println("商品 ID: " + productId + ", 初始库存: 10 件");
System.out.println("并发请求数: " + concurrency + "\n");
ExecutorService executor = Executors.newFixedThreadPool(concurrency);
AtomicInteger successCount = new AtomicInteger(0);
AtomicInteger failCount = new AtomicInteger(0);
CompletableFuture<?>[] futures = new CompletableFuture[concurrency];
for (int i = 0; i < concurrency; i++) {
futures[i] = CompletableFuture.runAsync(() -> {
try (Connection conn = DriverManager.getConnection(jdbcUrl, user, password)) {
Result r = seckillWithAdvisoryLock(conn, productId, quantity);
if (r.success) {
successCount.incrementAndGet();
} else {
failCount.incrementAndGet();
}
} catch (SQLException e) {
failCount.incrementAndGet();
}
}, executor);
}
CompletableFuture.allOf(futures).join();
executor.shutdown();
System.out.println("=== 执行结果 ===");
System.out.println("成功: " + successCount.get() + " 笔");
System.out.println("失败: " + failCount.get() + " 笔");
// 查询最终库存
try (Connection conn = DriverManager.getConnection(jdbcUrl, user, password);
PreparedStatement stmt = conn.prepareStatement("SELECT stock FROM products WHERE id = ?")) {
stmt.setInt(1, productId);
ResultSet rs = stmt.executeQuery();
if (rs.next()) {
System.out.println("最终库存: " + rs.getInt("stock") + " 件");
}
}
}
}
依赖:postgresql-42.x.x.jar + HikariCP(可选,本示例使用 DriverManager 简化)
运行:编译后执行 java SeckillDemo
多语言对比表格
| 维度 | Node.js (pg) | Go (lib/pq) | Python (asyncpg) | Java (JDBC) |
|---|---|---|---|---|
| 驱动/库 | pg (纯 JS) | lib/pq | asyncpg | org.postgresql.Driver |
| 异步模型 | Promise.all / async/await | Goroutine + sync.WaitGroup | asyncio / async/await | CompletableFuture + 线程池 |
| 连接管理 | pg.Pool | sql.Open + 手动管理 | asyncpg.create_pool | DriverManager(示例)/ HikariCP(推荐) |
| 事务 API | client.query('BEGIN/COMMIT') | tx := db.Begin() | conn.transaction() | conn.setAutoCommit(false) |
| 咨询锁函数 | pg_try_advisory_xact_lock($1) | 同 SQL | 同 SQL | 同 SQL |
| 行锁语法 | FOR UPDATE | 同 SQL | 同 SQL | 同 SQL |
| 错误处理 | try/catch 回滚 | 返回结构体 | 异常捕获 + 回滚 | SQLException + 回滚 |
| 并发控制实现 | 循环创建 Promise | go func() + channel | asyncio.gather | CompletableFuture.runAsync |
| 代码行数(约) | 70 行 | 95 行 | 60 行 | 110 行 |
| 适用场景 | 全栈开发、快速原型 | 高性能服务、微服务 | 高并发 I/O、数据管道 | 企业级应用、Spring 生态 |
说明:
- 所有示例均使用 PostgreSQL 原生
pg_try_advisory_xact_lock实现非阻塞事务级咨询锁。 - 各语言在连接池、异步模型上有所不同,但核心 SQL 逻辑完全一致。
- 实际生产环境中,建议所有语言均使用连接池(如 Node.js 的
pg.Pool,Go 的database/sql内置池,Python 的asyncpg.Pool,Java 的 HikariCP)。 - 性能方面,各语言在相同硬件和数据库配置下差距不大,主要瓶颈在于数据库锁竞争和网络延迟。
技术点总结(多语言视角)
- 咨询锁的调用方式在所有语言中完全一致,均为 SQL 函数调用。
- 事务管理是保证锁正确释放的关键,不同语言的事务 API 虽有差异,但均支持
BEGIN/COMMIT/ROLLBACK。 - 非阻塞式获取锁(
pg_try_*)可避免应用层线程/协程阻塞,提升整体吞吐量。 - 行级锁(
FOR UPDATE)在咨询锁之后使用,确保库存查询的读一致性,两者协同工作。
项目难点与解决方案
核心难点
在高并发秒杀场景中,大量请求同时更新同一条库存记录,行级锁导致请求串行排队,吞吐量急剧下降。同时,高频 UPDATE 操作引发 MVCC 表膨胀,进一步劣化性能。
解决方案
采用 PostgreSQL 咨询锁替代传统行级锁进行并发控制。咨询锁不依赖表中的行,锁操作在共享内存中完成,开销极低。配合事务级锁的自动释放机制,确保锁不会泄漏。
广度
该方案适用于所有需要高并发资源控制的场景:
- 电商秒杀、限量抢购
- 任务队列消费(多个 Worker 并行取任务)
- 分布式调度(多节点协调)
- 微服务中的分布式锁(基于数据库)
深度
咨询锁的实现原理涉及 PostgreSQL 的锁管理器(Lock Manager)和共享内存结构。锁键值通过哈希映射到共享内存中的锁槽(Lock Slot),整个过程不涉及缓冲区管理或 WAL 日志写入,因此性能远高于行级锁。
复杂度
- 实现复杂度:低——仅需调用标准 API 函数。
- 运维复杂度:低——无需额外组件,依托 PostgreSQL 自身能力。
- 调优复杂度:中——需根据业务特征设计合理的锁键值空间,避免锁热点。
官方文档
- PostgreSQL Documentation: Advisory Lock Functions
- PostgreSQL Documentation: SELECT - The Locking Clause
参考链接
- digoal/blog: PostgreSQL 使用 advisory lock 实现行级读写堵塞
- PostgreSQL 实力解剖锁系列(三) advisory lock
- PostgreSQL: Advisory Locks - A Deep Dive
总结
本文系统梳理了 PostgreSQL 行级锁在高并发更新场景下的性能瓶颈,以及 NOWAIT 和 SKIP LOCKED 两种优化手段的适用场景与语法。在此基础上,深入介绍了咨询锁(Advisory Lock)的设计哲学、会话级与事务级的区别、核心 API 函数及其使用方法。
通过性能对比数据可以看出,咨询锁在秒杀等高并发场景中可将吞吐量从约 8.6 万/秒提升至 39 万/秒,同时避免了 MVCC 带来的表膨胀问题。最后,通过一个基于 Node.js 的完整 Demo 演示了事务级咨询锁在库存扣减场景中的实际应用。
2033



被折叠的 条评论
为什么被折叠?



