侧边栏壁纸
博主头像
colo

欲买桂花同载酒

  • 累计撰写 1823 篇文章
  • 累计收到 0 条评论

高并发转账场景下的事务与锁设计

2025-12-12 / 0 评论 / 2 阅读

题目

高并发转账场景下的事务与锁设计

信息

  • 类型:问答
  • 难度:⭐⭐

考点

事务ACID特性,悲观锁与乐观锁应用,死锁预防,隔离级别选择

快速回答

在高并发转账场景中,需通过事务和锁机制保证数据一致性:

  • 使用数据库事务确保操作的原子性和一致性
  • 采用SELECT FOR UPDATE实现悲观锁,或版本号机制实现乐观锁
  • 设置合理的隔离级别(推荐Read Committed或Repeatable Read)
  • 添加死锁检测和重试机制
  • 遵循固定顺序的锁获取原则预防死锁
## 解析

场景说明

设计一个银行转账系统,账户表结构如下:

CREATE TABLE accounts (
  id INT PRIMARY KEY,
  name VARCHAR(50),
  balance DECIMAL(10,2),
  version INT DEFAULT 0  -- 乐观锁版本号字段
);

在高并发场景下,从账户A向账户B转账100元,需保证:
1. 余额不能为负
2. 并发操作不出现超扣
3. 避免死锁

解决方案对比

方案1:悲观锁实现(SELECT FOR UPDATE)

BEGIN TRANSACTION;

-- 锁定账户A和B(按固定ID顺序避免死锁)
SELECT * FROM accounts WHERE id IN (A,B) ORDER BY id ASC FOR UPDATE;

-- 检查A账户余额
SELECT balance FROM accounts WHERE id = A;

-- 转账操作
UPDATE accounts SET balance = balance - 100 WHERE id = A;
UPDATE accounts SET balance = balance + 100 WHERE id = B;

COMMIT;

原理说明:

  • 通过FOR UPDATE在事务开始时锁定相关行
  • 按固定ID顺序获取锁可预防死锁(如总是先锁小ID)
  • 事务提交时自动释放锁

方案2:乐观锁实现(版本号控制)

-- 伪代码流程
DO $$
DECLARE 
  retry_count INT := 0;
  max_retries INT := 3;
BEGIN
  WHILE retry_count < max_retries LOOP
    -- 获取当前版本号和余额
    SELECT balance, version INTO old_balance_A, old_version_A 
    FROM accounts WHERE id = A;

    SELECT balance, version INTO old_balance_B, old_version_B 
    FROM accounts WHERE id = B;

    -- 业务校验
    IF old_balance_A < 100 THEN
      RAISE EXCEPTION 'Insufficient balance';
    END IF;

    -- 尝试更新(带版本校验)
    UPDATE accounts 
    SET balance = old_balance_A - 100, version = version + 1
    WHERE id = A AND version = old_version_A;

    IF row_count = 0 THEN
      retry_count := retry_count + 1;
      CONTINUE;
    END IF;

    UPDATE accounts 
    SET balance = old_balance_B + 100, version = version + 1
    WHERE id = B AND version = old_version_B;

    IF row_count = 0 THEN
      ROLLBACK;
      retry_count := retry_count + 1;
      CONTINUE;
    END IF;

    COMMIT;
    RETURN;
  END LOOP;
  RAISE EXCEPTION 'Transfer failed after retries';
END$$;

原理说明:

  • 通过version字段检测数据是否被修改
  • 更新失败时自动重试(需设置重试上限)
  • 不长期占用锁,适合低冲突场景

最佳实践

  1. 隔离级别选择:推荐Read Committed(平衡性能与一致性)
  2. 锁策略选择
    • 高冲突场景 → 悲观锁
    • 低冲突场景 → 乐观锁(减少锁开销)
  3. 死锁预防
    • 统一锁获取顺序(如按ID排序)
    • 设置锁等待超时(SET lock_timeout = '500ms'
  4. 索引优化:确保WHERE条件使用索引,减少锁范围

常见错误

  • 锁缺失:未加锁直接查询后更新,导致超扣
  • 锁顺序不一致:不同事务按不同顺序加锁引发死锁
  • 长事务:事务中包含耗时操作,增大锁竞争概率
  • 过度锁定:使用表锁导致性能下降

扩展知识

  • MVCC机制:PostgreSQL/MySQL通过多版本控制实现非阻塞读
  • 锁升级:SQL Server在锁过多时自动升级为表锁
  • 分布式事务:跨数据库转账需考虑XA协议或Saga模式
  • 监控工具
    • MySQL:SHOW ENGINE INNODB STATUS查看死锁
    • PostgreSQL:pg_stat_activity监控锁等待