PostgreSQL笔记31:咨询锁(Advisory Lock)在高并发场景下的原理与实践

AI系统中的人工干预机制设计:从停止按钮到人机协同闭环 在AI工程化落地过程中,‘人工干预’已不再是UI层的可选功能,而是保障系统安全、可信与合规的核心能力。其本质涉及AI治理框架下的实时拦截逻辑、操作员否决权的技术实现、以及人机协同阈值的动态设定。关键技术包括前端防误触交互设计、API网关级熔断控制、数据库事务锚定与审计日志可追溯性,支撑起工业级AI系统的‘前置拦截—状态留痕—流程恢复’三位一体能力。该能力广泛应用于智能制造质检、医疗影像辅助诊断、政务智能审批等高可靠性场景,尤其在AI置信度处于0.85–0.94的灰色区间时,人类经验的价值最为凸显。本文聚焦真 阅读详情

纲要

  • 行级锁(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]。

最佳实践与注意事项

  1. 优先使用事务级咨询锁:事务级锁(pg_advisory_xact_*)在事务提交或回滚时自动释放,避免锁泄漏(Lock Leakage)[reference:26]。

  2. 锁键值设计:确保锁键值在应用程序范围内具有唯一且明确的语义。例如,使用 product_id 作为锁键值,或使用 (table_oid, row_id) 的组合键。

  3. 非阻塞式优先:在秒杀等场景中,优先使用 pg_try_advisory_* 系列函数,获取锁失败时立即返回,避免客户端长时间等待[reference:27]。

  4. 注意锁的数据库范围:咨询锁是数据库级(Database-Level)的,不同数据库之间的锁键值空间相互独立。

  5. 监控与诊断:通过 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;

运行说明

  1. 确保本地 PostgreSQL 服务已启动,并创建数据库 testdb。
  2. 执行 init.sql 初始化表结构和测试数据。
  3. 安装依赖:npm install
  4. 启动 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(&currentStock)
    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/pqasyncpgorg.postgresql.Driver
异步模型Promise.all / async/awaitGoroutine + sync.WaitGroupasyncio / async/awaitCompletableFuture + 线程池
连接管理pg.Poolsql.Open + 手动管理asyncpg.create_poolDriverManager(示例)/ HikariCP(推荐)
事务 APIclient.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 + 回滚
并发控制实现循环创建 Promisego func() + channelasyncio.gatherCompletableFuture.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 行级锁在高并发更新场景下的性能瓶颈,以及 NOWAIT 和 SKIP LOCKED 两种优化手段的适用场景与语法。在此基础上,深入介绍了咨询锁(Advisory Lock)的设计哲学、会话级与事务级的区别、核心 API 函数及其使用方法。

通过性能对比数据可以看出,咨询锁在秒杀等高并发场景中可将吞吐量从约 8.6 万/秒提升至 39 万/秒,同时避免了 MVCC 带来的表膨胀问题。最后,通过一个基于 Node.js 的完整 Demo 演示了事务级咨询锁在库存扣减场景中的实际应用。

PostgreSQL 咨询锁advisory lock使用详解 PostgreSQL从8.2版本就开始支持advisory lock这一功能。那么什么是advisory lock呢?官方文档的解释是: PostgreSQL提供了一种方法创建由应用定义其含义的锁。这种锁被称为咨询锁,因为系统并不强迫其使用——而是由应用来保证其正确的使用。 advisory lock分为会话级别和事务级别两种(9.1版本加入事务级别)。事务级别的advisory lock不能显性... 阅读详情

相关推荐

csp-j/s

CSP-J/S 是中国计算机学会(CCF)主办的 ‌‌(Certified Software Professional Junior/Senior),是信息学奥赛(NOI)体系的入门级选拔考试,也是国内最具权威性的青少年编程能力认证之一。

gxyxxrYYDS的博客 2033

PostgreSQL 锁解密

2019独角兽企业重金招聘Python工程师标准>>> ...

weixin_34417183的博客 314

Nginx下ModSecurity模块安装详细教程

本文提供了在Nginx 1.28.0上安装ModSecurity v3的完整教程。首先需安装编译依赖,然后从源码编译安装ModSecurity库并设置优化参数。接着编译Nginx动态模块并配置加载。核心配置包括设置规则引擎、日志路径及性能优化参数。安装OWASP核心规则集后,在Nginx站点配置中启用ModSecurity。最后测试规则有效性并给出性能优化建议,如规则排除、精简规则集等。整个过程包含详细的命令和配置示例,确保安全防护功能正常运作。

深山技术宅的博客 1785

GBase 8c V3.0.0数据类型——咨询锁函数

GBase 8c V3.0.0数据类型——咨询锁函数

ricemaster的博客 269

MVSNet (pytorch版) 搭建环境 运行dtu数据集重建 实操教程(图文并茂、超详细)

MVSNet (pytorch版) 代码注释版 下载 (注释非常详细,代码结构有所调整,使用起来更方便) 深度学习三维重建 MVSNet-ECCV-2018 (源码、pytorch版、原文+注释、译文+批注)Meshlab查看三维点云时 ,使用技巧总结。

程序员,他们想的是什么?他们想的永远都是技术,他们崇尚的也永远都是技术。 2938

lammps案例:聚乙烯和石墨烯界面pull out模拟

大家好,我是小马老师。 本文介绍lammps模拟计算界面结合力的一个案例。 本案例参考论文《The interfacial mechanical properties of functionalized graphene- polymer nanocomposites》,对论文作者表示感谢。 模型共分2层,上层为聚乙烯模型,下次为单层石墨烯,模拟过程共分3部分:建模、弛豫、拉伸。 (1)建模 使用MS建立聚乙烯和石墨烯模型,具体方法可参考之前的推文,使用build layer方法建立聚乙烯和石墨烯组合模型,

lammps_jiayou的博客 3497

【转载】PostgreSQL 的 pg_advisory_lock 函数

pg_try_advisory_xact_lock(key) 尝试获取事务级咨询锁(非阻塞)SELECT pg_try_advisory_lock(555555);pg_try_advisory_lock(key) 尝试获取会话级咨询锁(非阻塞)pg_advisory_xact_lock(key) 获取事务级咨询锁(阻塞)pg_advisory_unlock_all() 释放当前会话持有的所有咨询锁。pg_advisory_lock(key) 获取会话级咨询锁(阻塞)

yang1276的博客 107

黑猫带你学eMMC协议第29篇:eMMC设备锁详解(CMD42)

通过cmd42命令,可以给emmc设置密码,并且锁定emmc。这时候就不能对emmc的user分区进行读写数据访问了。设备锁在V4.3版本后的emmc里面,就只能保护user area分区,因此,被锁定的emmc,依旧可以正常访问BOOT\RPMB\GPP分区。emmc内部有128 bit PWD寄存器,用来存放密码,以及有8bit PWD_LEN寄存器,用来密码长度。这两个寄存器非易失性,掉电也不会丢失。

黑猫奥利奥的博客 3240

PostgreSQL 的 pg_advisory_lock 函数

pg_advisory_lock 是 PostgreSQL 提供的一种应用级锁机制,它不锁定具体的数据库对象(如表或行),而是通过数字键值来协调应用间的并发控制。

文牧之的博客 1658

3D结构光 vs TOF——手机厂商为何选择TOF方案?

3D视觉感知技术路线分析:结构光与TOF对比 3D视觉感知领域存在结构光和TOF两大主流技术。结构光基于三角定位法,精度高(毫米级)但工作距离短(<1.5米),适合人脸识别等近距离场景;TOF通过测量光脉冲飞行时间计算深度,精度中等(厘米级)但工作距离长(0.5-5米+),更适合AR导航等中远距离应用。手机厂商选型时考虑:前置认证选择结构光(如苹果FaceID),后置场景倾向TOF(安卓阵营),因后者更适配全面屏设计、环境适应性强且成本优势明显。两种技术互补共存,共同推动3D感知应用发展。

致力于lidar应用开发 115

块存储:AIO的直接写流程注释

提交io流程中aio_write之前函数的注释,可参考“块存储:AIO的直接读流程注释”。

yiyeguzhou100的专栏 1336

postgresql Advisory Locks

新加入的Advisory Locks似乎是个不错的特性 http://www.postgresql.org/docs/8.2/interactive/explicit-locking.html#ADVISORY-LOCKS http://www.postgresql.o

朝着梦想 渐行前进 3349

PyTorch深度学习实战(3)——使用PyTorch构建神经网络

PyTorch 是一个用于构建深度神经网络的库,具有灵活性和可扩展性,可以轻松自定义模型。在本节中,我们将使用 PyTorch 库构建神经网络,利用张量对象操作和梯度值计算更新网络权重,并利用 Sequential 类简化网络构建过程,最后还介绍了如何使用 save、load 方法保存和加载模型,以节省模型训练时间。

盼小辉丶的博客 8万+

【PostgreSQL从零到精通】第30篇:序列与咨询锁——并发控制的实用工具

PostgreSQL提供序列(Sequence)和咨询锁(Advisory Lock)解决并发问题。序列是独立对象,用于生成唯一递增数字,支持nextval()获取值、currval()查看当前值、setval()设置值等操作。序列允许间隙以保证高性能,SERIAL类型实际是序列+整数的组合。咨询锁是与数据库对象无关的应用层锁,支持会话级和事务级两种生命周期,以及独占锁和共享锁模式,适用于分布式锁场景。这两种机制分别解决了唯一ID生成和资源互斥控制问题,是PostgreSQL处理并发场景的重要工具。

xyghehehehe的专栏 39

汉书HE8,好用的汇编代码编写工具

汉书HE8,好用的汇编代码编写工具早期编写汇编代码所用功能强大

霍尼韦尔SoftMaster使用说明

霍尼韦尔SoftMaster使用说明

Medusa 模块 SDK 深度解析:从 MedusaApp 到模块化架构的演进与实践

`@medusajs/modules-sdk` 是 Medusa 平台模块化体系的"发动机",它负责模块定义、动态加载、生命周期编排、模块间链接(Link)、远程查询以及数据库迁移。本文以 [packages/core/modules-sdk/CHANGELOG.md](https://link.gitcode.com/i/305be6b1826fd6ce805b682673f9d534) 记录的

gitblog_00949的博客 250

贝叶斯统计 [韦来生编]

贝叶斯(Bayes)统计是近几十年来迅速发展起来的数理统计的一个重要分支。贝叶斯方法与经典统计方法的主要不同之处是进行统计推断时除了利用样本信息外,还要利用参数的先验信息,因此可以提高统计推断或统计决策的效果。它在经济、金融、生物、医学、自然科学和社会科学等许多领域具有广泛的应用。贝叶斯方法的研究已渗透到了统计学的几乎所有领域。作者在给中国科学技术大学概率论与数理统计专业本科生开设的“数理统计”课程中有一章专门讲授“贝叶斯方法和统计决策理论”,近十年来也给中国科学技术大学概率统计专业研究生开设过几次“贝叶斯分析”课。本书是在对过去讲稿的内容作了适当的增减和调整的基础上完成的。  本书共分六章。第1章是绪论,介绍了贝叶斯统计的若干基本概念,同时对必要的数理统计的基础知识有重点地作了回顾。第二章介绍了确定先验分布的若干可供选择的方法。第三章介绍了几类常见统计模型参数的后验分布的主要结果和计算方法。第四章和第五章分别介绍了“贝叶斯统计推断”和“贝叶斯统计决策”的内容。第六章介绍了贝叶斯统计计算的若干方法,包括蒙特卡洛方法和马尔可夫链蒙特卡洛(MCMC)方法以及相关统计软件的简介。本书的每一章附右大量的例题和习题,书末附有常用的几个表格和部分习题解答供读者查用。  大约可在54学时内讲授本书的第1章至第六章的主要内容,教师可根据学时需要选讲本书第1章至第六章的部分内容。书中标“*”号的章节可略去不讲,留给读者作为阅读材料。第六章例题中的R代码和部分例题的数据文件等可从作者个人主页下载。  作者在编写本书的过程中参考了J.O.Berger教授的《统计决策论及贝叶斯分析》和茆诗松教授编写的《贝叶斯统计》等书中的一些内容,以及《贝叶斯分析》一书中由张伟平博士编写的有关贝叶斯统计计算的一些内容。在此表示衷心感谢。  本书编写准备过程中,中国科学技术大学统计与金融系的几个研究生帮助完成了本书部分中文的录入和排版,作者对他们的辛勤工作表示真诚的感谢。高等教育出版社为本书的出版给予了大力支持。在此一并致谢。

Orca 的 Agent Skill Sharing 实现全解:不可变技能包、跨平台安装事务与云端共享链路

> 本文基于仓库中 [docs/agent-skill-sharing-implementation-checklist.md](https://link.gitcode.com/i/a7d9f2ccfffa260b9a01b9bd1a917226) 这一实施检查清单展开。Orca 的 Skill Sharing 让用户能把一个或一组本地 Agent 技能打包成一个**不可变的 Skill Bun

gitblog_01164的博客 848

真理约束型人工智能商业计划书—— 基于贾子-USDS-2.0 认知宪法体系与贾子 KWMM/KSFT 六层分层判定范式的全球首创“去-认知-殖民“AI 基础设施商业化方案(Pre-A)

本商业计划书提出真理约束型AI基础设施TD-AI,以贾子-USDS-2.0认知宪法为底层架构,以KWMM世界模型矩阵与KSFT成败定理为评测引擎,使AI从叙事型概率拟合机器转向可程序化审查、可独立验证的求真型认知基础设施。本轮Pre-A融资1.5亿元,投前估值10亿元,资金用于TruthBench六层分层评测平台与AlignRepair对齐改造服务商业化。文件强调理论主张、技术指标与财务预测均属待尽调假设,需经独立验证。

首席技术执行官CTO(Chief Technology Officer)|技术总监--邓斌博客 433

PostgreSQL 事务与并发控制

而MVCC是一种后验性的机制,读不阻塞写,写也不阻塞读,等到提交的时候才检验是否有冲突,由于没有锁,所以读写不会相互阻塞,避免了大粒度和长时间的锁定,能更好地适应对读的响应速度和并发性要求高的场景,大大提升了并发性能。在该隔离级别,所有事务都可以看到其他未提交事务的执行结果,在多用户数据库中,脏读是非常危险的,在并发情况下,查询结果非常不可控,即使不考虑结果的严谨性只追求性能,它的性能也并不比其他事务隔离级别好多少,可以说脏读没有任何好处。简言之,它是在每个读的数据行上加上共享锁。

这个人很懒什么都没有~ 2002

进阶数据库系列(八):PostgreSQL 锁机制

点击下方名片,设为星标!回复“1024”获取2TB学习资源!前面介绍了 PostgreSQL 常用管理命令、访问控制与认证、语法、数据类型和运算符、常用函数相关的知识点,今天我将详细的为大家介绍 PostgreSQL 锁操作相关知识,希望大家能够从中收获多多!如有帮助,请点在看、转发支持一波!!!锁存在的意义在了解 PostgreSQL 锁之前,我们需要了解锁存在的意义是啥?当多个会话同时访问数据...

民工哥的博客 1443

PostgreSQL 锁机制

我会告诉你精通 Postgres 的锁机制需要知道的一切,要知道对锁了解的越多,解决与锁相关的问题就会越快。不同于标准的锁请求,会话层级的劝告锁请求并不遵守事务语义:事务被回滚后锁也会随着回滚保持着,同样地即使调用锁的事务之后失败了,解锁请求仍然是有效的。例如,如果事务 1 在表 A 上获得一个排他锁,同时试图获取一个在表 B 上的排他锁, 而事务 2 已经持有表 B 的排他锁,同时却正在请求表 A 上的一个排他锁,那么两个事务就都不能进行下去。因此,事务一被事务二阻塞,而事务二也被事务一阻塞:一个死锁。

weixin_44917075的博客 1837

PostgreSQL 的 pg_advisory_lock_shared 函数

pg_advisory_lock_shared 是 PostgreSQL 提供的共享咨询锁函数,允许多个会话同时获取相同键值的共享锁,但排斥排他锁。

文牧之的博客 1176

postgresql 索引状态_PostgreSQL中的锁:3.其他锁

我们已经讨论了一些对象级锁(特别是关系级锁),以及行级锁及其与对象级锁的连接,还探讨了等待队列。这次我们要来个大杂烩。我们将从死锁开始(实际上,我计划上次讨论死锁,但是这篇文章本身篇幅太长了),然后简要回顾一下对象级锁,最后讨论谓词锁。死锁使用锁时,我们可能会遇到死锁。当事务一试图获取事务二已经使用的资源,而事务二试图获取事务一使用的资源时,就会发生这种情况。左下图说明了这一点:实线箭头...

weixin_39732534的博客 572

postgresql集群报错解决:SELECT pg_catalog.pg_try_advisory_xact_lock_shared(65535, 65535)

10月25号之后只能对evap数据库进行select操作,且运行报错如下: org.postgresql.util.PSQLException: ERROR: pooler: failed to create connections in parallel mode for thread 140705041217280 在位置:SQL statement "EXECUTE DIRECT ...

SunWuKong_Hadoop的博客 3478

pg_advisory_lock 正确使用方法

pg_advisory_lock和pg_advisory_unlock是配对使用的,但是如果之间的部分出现异常时,可能会导致不能正确的调用pg_advisory_unlock.例如: create or replace function test_insert_advisory(integer) returns integer as $$ declare begin perform p...

kmblack1的专栏 2854

PostgreSQL advisory lock

在PostgreSQL的锁定机制中,读和写操作是互相不冲突的,这大大增加了数据库的并发能力,但是如果我想要实现对于某些关键数据修改时,别的用户禁止读取的功能(即读写冲突)的功能,该怎么办呢? PostgreSQL提供了一种锁advisory lock,可以实现读写堵塞的功能。 1. 创建表,注意使用一个唯一列(用于advisory lock) bill=# create table t1(id...

Focus on PostgreSQL 2342

PostgreSQL 使用advisory lock实现行级读写堵塞

标签 PostgreSQL , select for update , 读写冲突 , 读写堵塞 , advisory lock 背景 PostgreSQL的读写是不冲突的,这听起来是件好事对吧,读和写相互不干扰,可以数据库提高读写并发能力。 但是有些时候,用户也许想让读写冲突(需求:数据正在被更新或者删除时,不允许被读取)。 那么有方法能实现读写冲突...

weixin_34266504的博客 410
上一篇: PostgreSQL笔记30:锁机制深度解析——表级锁、行级锁、轻量级锁与自旋锁
下一篇: PostgreSQL笔记32:死锁的诊断、处理与加锁原则
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、付费专栏及课程。

余额充值