← 中文资源

简体中文版本

悲观行锁

悲观锁在读时就取真实锁——SELECT FOR UPDATE——让并发事务排队等待而不是事后冲突;高争用、重试昂贵时是正确选择。

阅读英文原文

概念定义

两个事务更新同一行不能都赢。悲观策略早解决:先读者持锁,后读者等待。乐观策略晚解决:都执行,提交时一个失败。冲突频繁且重试昂贵时悲观占优;冲突稀少时反之。

工作机制

SELECT FOR UPDATE 取排他行锁,FOR SHARE 让并发读者持共享锁。PostgreSQL 还有 FOR NO KEY UPDATE、NOWAIT 与队列领取专用的 SKIP LOCKED;InnoDB 在高隔离级别下加间隙锁与临键锁,锁的是范围而不只是行。

使用场景

进行中的状态机、带不变量的计数器、先领取再处理的队列。锁定区间要短、全代码路径按规范顺序加锁、设置 lock_timeout 让等待变成可度量的错误。

常见误区

行锁只锁行,不串行化整表。把锁跨外部调用持有,会把争用变成系统性停顿;不同路径以不同顺序加锁,生产流量里就会撞上死锁检测。MVCC 下普通读不阻塞写——很多团队在“读后写”校验从未被保护并出事故后才补 FOR UPDATE。工作队列领取必须配 SKIP LOCKED,否则 worker 在一行上排队。

代码示例

from __future__ import annotations

import sqlite3

SCHEMA = """
CREATE TABLE inventory (
    sku TEXT PRIMARY KEY,
    quantity INTEGER NOT NULL CHECK (quantity >= 0),
    price_cents INTEGER NOT NULL CHECK (price_cents >= 0)
)
"""

UPSERT = """
INSERT INTO inventory (sku, quantity, price_cents)
VALUES (?, ?, ?)
ON CONFLICT(sku) DO UPDATE SET
    quantity = excluded.quantity,
    price_cents = excluded.price_cents
"""


def apply_stock_updates(
    connection: sqlite3.Connection,
    rows: list[tuple[str, int, int]],
) -> int:
    for sku, quantity, price_cents in rows:
        if not sku or quantity < 0 or price_cents < 0:
            raise ValueError(f"invalid inventory row: {sku!r}")

    connection.execute("BEGIN IMMEDIATE")
    try:
        cursor = connection.executemany(UPSERT, rows)
        connection.commit()
    except Exception:
        connection.rollback()
        raise
    return cursor.rowcount


def main() -> None:
    connection = sqlite3.connect(":memory:")
    connection.executescript(SCHEMA)

    first = apply_stock_updates(connection, [("A-1", 5, 1200), ("B-2", 3, 800)])
    second = apply_stock_updates(connection, [("A-1", 9, 1150), ("C-3", 1, 450)])
    print("affected rows:", first, second)
    print(connection.execute(
        "SELECT sku, quantity, price_cents FROM inventory ORDER BY sku"
    ).fetchall())

    try:
        apply_stock_updates(connection, [("D-4", -1, 100)])
    except ValueError as exc:
        print(f"rejected: {exc}")
    assert connection.execute("SELECT COUNT(*) FROM inventory").fetchone()[0] == 3


if __name__ == "__main__":
    main()
package main

import (
	"context"
	"fmt"
	"time"

	"github.com/redis/go-redis/v9"
)

func seedAndMeasure(ctx context.Context, client *redis.Client) error {
	keys := []string{"metric:hits", "metric:misses", "metric:errors"}

	// 独立命令走管道:一次网络往返
	pipe := client.Pipeline()
	incrCmds := make([]*redis.IntCmd, len(keys))
	for i, key := range keys {
		incrCmds[i] = pipe.Incr(ctx, key)
	}
	if _, err := pipe.Exec(ctx); err != nil {
		return fmt.Errorf("pipeline exec: %w", err)
	}

	// 事务管道:命令打包进 MULTI/EXEC 原子执行
	tx := client.TxPipeline()
	expireCmds := make([]*redis.BoolCmd, len(keys))
	for i, key := range keys {
		expireCmds[i] = tx.Expire(ctx, key, time.Hour)
	}
	if _, err := tx.Exec(ctx); err != nil {
		return fmt.Errorf("tx exec: %w", err)
	}

	for i, key := range keys {
		fmt.Printf("%s=%d expires=%v\n",
			key, incrCmds[i].Val(), expireCmds[i].Val())
	}
	return nil
}

func main() {
	client := redis.NewClient(&redis.Options{
		Addr:        "127.0.0.1:6379",
		PoolSize:    10,
		ReadTimeout: 500 * time.Millisecond,
	})
	defer client.Close()

	ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
	defer cancel()

	if err := seedAndMeasure(ctx, client); err != nil {
		fmt.Println("failed:", err)
	}
}