实战案例概览

案例核心问题解决方案难点
转账业务数据一致性、死锁固定加锁顺序、悲观锁多账户并发转账
秒杀抢购超卖、高并发乐观锁 + 限流10000人抢100件商品
订单支付重复支付、幂等性悲观锁 + 唯一约束防止重复扣款
红包发放余额不足、公平性悲观锁 + 事务隔离1个红包被多人抢
积分扣减负数积分乐观锁 + 余额检查并发扣减积分

案例1:转账业务

需求

用户A向用户B转账100元,要求:

  • 余额不能为负数
  • 转账过程中不能被打断
  • 防止死锁

方案1:基础实现(有死锁风险)

-- ❌ 可能死锁
-- 事务A:A向B转100
START TRANSACTION;
UPDATE account SET balance = balance - 100 WHERE user_id = 'A'; -- 锁A
UPDATE account SET balance = balance + 100 WHERE user_id = 'B'; -- 等待锁B
COMMIT;

-- 事务B:B向A转50(并发执行)
START TRANSACTION;
UPDATE account SET balance = balance - 50 WHERE user_id = 'B'; -- 锁B
UPDATE account SET balance = balance + 50 WHERE user_id = 'A'; -- 等待锁A(死锁!)
COMMIT;

方案2:固定加锁顺序(推荐)

-- ✅ 避免死锁:按user_id升序加锁

-- 转账函数(伪代码)
FUNCTION transfer(from_user, to_user, amount):
    -- 1. 固定加锁顺序(按user_id升序)
    first_user = MIN(from_user, to_user)
    second_user = MAX(from_user, to_user)

    START TRANSACTION;

    -- 2. 按顺序锁定账户
    SELECT balance FROM account WHERE user_id = first_user FOR UPDATE;
    SELECT balance FROM account WHERE user_id = second_user FOR UPDATE;

    -- 3. 检查余额
    IF from_user.balance < amount THEN
        ROLLBACK;
        RETURN "余额不足";
    END IF;

    -- 4. 扣款和到账
    UPDATE account SET balance = balance - amount WHERE user_id = from_user;
    UPDATE account SET balance = balance + amount WHERE user_id = to_user;

    COMMIT;
    RETURN "转账成功";
END FUNCTION;

方案3:Java实现(完整代码)

@Service
public class TransferService {

    @Autowired
    private AccountMapper accountMapper;

    @Transactional(rollbackFor = Exception.class)
    public void transfer(String fromUser, String toUser, BigDecimal amount) {
        // 1. 固定加锁顺序(避免死锁)
        String firstUser = fromUser.compareTo(toUser) < 0 ? fromUser : toUser;
        String secondUser = fromUser.compareTo(toUser) < 0 ? toUser : fromUser;

        // 2. 按顺序锁定账户(悲观锁)
        Account first = accountMapper.selectForUpdate(firstUser);
        Account second = accountMapper.selectForUpdate(secondUser);

        // 3. 检查余额
        Account fromAccount = fromUser.equals(firstUser) ? first : second;
        if (fromAccount.getBalance().compareTo(amount) < 0) {
            throw new BusinessException("余额不足");
        }

        // 4. 扣款和到账
        accountMapper.updateBalance(fromUser, amount.negate()); // 扣款
        accountMapper.updateBalance(toUser, amount);             // 到账

        // 5. 记录流水(可选)
        recordTransferLog(fromUser, toUser, amount);
    }
}
<!-- MyBatis Mapper -->
<select id="selectForUpdate" resultType="Account">
    SELECT * FROM account WHERE user_id = #{userId} FOR UPDATE
</select>

<update id="updateBalance">
    UPDATE account
    SET balance = balance + #{amount}
    WHERE user_id = #{userId}
</update>

案例2:秒杀抢购

需求

10000个用户抢购100件商品,要求:

  • 不能超卖(库存>=0)
  • 高并发(TPS>1000)
  • 防止刷单

方案1:乐观锁(推荐)

-- 1. 建表
CREATE TABLE product (
    id INT PRIMARY KEY,
    name VARCHAR(50),
    stock INT,
    version INT DEFAULT 0,
    INDEX idx_stock (stock)
);

-- 2. 秒杀逻辑(乐观锁)
-- 伪代码
FUNCTION buy_product(product_id, quantity):
    max_retries = 3

    FOR i IN 1..max_retries:
        -- 查询库存和版本号(不加锁)
        product = SELECT stock, version FROM product WHERE id = product_id;

        -- 检查库存
        IF product.stock < quantity THEN
            RETURN "库存不足";
        END IF;

        -- 尝试扣减库存(乐观锁)
        affected_rows = UPDATE product
                        SET stock = stock - quantity,
                            version = version + 1
                        WHERE id = product_id
                          AND version = product.version
                          AND stock >= quantity; -- 双重检查

        -- 更新成功
        IF affected_rows > 0 THEN
            -- 创建订单
            INSERT INTO orders (user_id, product_id, quantity) VALUES (...);
            RETURN "购买成功";
        END IF;

        -- 版本冲突,重试
        SLEEP(10ms);
    END FOR;

    RETURN "购买失败,请重试";
END FUNCTION;

方案2:悲观锁(库存少时)

-- 当库存剩余<10时,切换到悲观锁(防止超卖)

START TRANSACTION;

-- 1. 锁定商品(悲观锁)
SELECT stock FROM product WHERE id = 1001 FOR UPDATE;

-- 2. 检查库存
IF stock < 1 THEN
    ROLLBACK;
    RETURN "库存不足";
END IF;

-- 3. 扣减库存
UPDATE product SET stock = stock - 1 WHERE id = 1001;

-- 4. 创建订单
INSERT INTO orders (user_id, product_id, quantity) VALUES (...);

COMMIT;
RETURN "购买成功";

方案3:Redis + MySQL(高并发)

# Python示例(Redis预减库存 + MySQL兜底)

def buy_product_with_redis(user_id, product_id):
    # 1. Redis预减库存(快速失败)
    stock = redis.decr(f"product:{product_id}:stock")

    if stock < 0:
        # 库存不足,恢复Redis
        redis.incr(f"product:{product_id}:stock")
        return {"success": False, "message": "库存不足"}

    # 2. 异步写入MySQL(消息队列)
    mq.send({
        "user_id": user_id,
        "product_id": product_id,
        "quantity": 1
    })

    return {"success": True, "message": "购买成功"}

# 消费者(异步写入MySQL)
def consume_order_message(message):
    user_id = message['user_id']
    product_id = message['product_id']

    # MySQL扣减库存(乐观锁兜底)
    affected_rows = db.execute("""
        UPDATE product
        SET stock = stock - 1,
            version = version + 1
        WHERE id = %s AND stock > 0
    """, product_id)

    if affected_rows > 0:
        # 创建订单
        db.execute("""
            INSERT INTO orders (user_id, product_id)
            VALUES (%s, %s)
        """, user_id, product_id)
    else:
        # MySQL库存不足,恢复Redis
        redis.incr(f"product:{product_id}:stock")

案例3:订单支付

需求

用户支付订单,要求:

  • 防止重复支付
  • 幂等性(多次请求结果一致)
  • 扣款和订单状态更新原子性

方案1:悲观锁 + 状态检查

-- 1. 建表
CREATE TABLE orders (
    id INT PRIMARY KEY,
    user_id INT,
    amount DECIMAL(10,2),
    status ENUM('UNPAID', 'PAID', 'CANCELLED') DEFAULT 'UNPAID',
    INDEX idx_user_status (user_id, status)
);

-- 2. 支付逻辑
START TRANSACTION;

-- 1. 锁定订单(悲观锁)
SELECT status FROM orders WHERE id = 12345 FOR UPDATE;

-- 2. 检查订单状态
IF status != 'UNPAID' THEN
    ROLLBACK;
    RETURN "订单状态异常"; -- 已支付或已取消
END IF;

-- 3. 锁定账户
SELECT balance FROM account WHERE user_id = 1 FOR UPDATE;

-- 4. 检查余额
IF balance < order.amount THEN
    ROLLBACK;
    RETURN "余额不足";
END IF;

-- 5. 扣款
UPDATE account SET balance = balance - order.amount WHERE user_id = 1;

-- 6. 更新订单状态
UPDATE orders SET status = 'PAID' WHERE id = 12345;

-- 7. 记录支付流水
INSERT INTO payment_log (order_id, amount, pay_time) VALUES (12345, 100, NOW());

COMMIT;
RETURN "支付成功";

方案2:唯一约束 + 幂等性

-- 1. 建表(支付流水表)
CREATE TABLE payment_log (
    id INT PRIMARY KEY AUTO_INCREMENT,
    order_id INT NOT NULL,
    pay_id VARCHAR(64) NOT NULL, -- 支付请求唯一ID(客户端生成)
    amount DECIMAL(10,2),
    status ENUM('SUCCESS', 'FAILED'),
    create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    UNIQUE KEY uk_pay_id (pay_id) -- 唯一约束,防止重复支付
);

-- 2. 支付逻辑(幂等性)
START TRANSACTION;

-- 1. 插入支付流水(幂等性检查)
INSERT INTO payment_log (order_id, pay_id, amount, status)
VALUES (12345, 'PAY_1234567890', 100, 'SUCCESS')
ON DUPLICATE KEY UPDATE id = id; -- 如果pay_id已存在,不做任何操作

-- 2. 检查是否插入成功
IF affected_rows == 1 THEN
    -- 首次支付,执行扣款和订单更新
    UPDATE account SET balance = balance - 100 WHERE user_id = 1;
    UPDATE orders SET status = 'PAID' WHERE id = 12345;
    COMMIT;
    RETURN "支付成功";
ELSE
    -- 重复支付,直接返回成功
    ROLLBACK;
    RETURN "支付成功(已支付)";
END IF;

案例4:红包发放

需求

用户A发1个100元红包,10个人抢,要求:

  • 总金额不能超100元
  • 每人只能抢1次
  • 公平性(随机金额)

方案:悲观锁 + 原子性

-- 1. 建表
CREATE TABLE red_packet (
    id INT PRIMARY KEY,
    user_id INT,          -- 发红包的人
    total_amount DECIMAL(10,2),
    remain_amount DECIMAL(10,2), -- 剩余金额
    total_count INT,      -- 红包总数
    remain_count INT,     -- 剩余个数
    status ENUM('ACTIVE', 'FINISHED') DEFAULT 'ACTIVE'
);

CREATE TABLE red_packet_record (
    id INT PRIMARY KEY AUTO_INCREMENT,
    red_packet_id INT,
    user_id INT,          -- 抢红包的人
    amount DECIMAL(10,2),
    create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    UNIQUE KEY uk_packet_user (red_packet_id, user_id) -- 防止重复抢
);

-- 2. 抢红包逻辑
START TRANSACTION;

-- 1. 锁定红包(悲观锁)
SELECT remain_amount, remain_count, status
FROM red_packet
WHERE id = 1001 FOR UPDATE;

-- 2. 检查红包状态
IF status != 'ACTIVE' OR remain_count <= 0 THEN
    ROLLBACK;
    RETURN "红包已抢完";
END IF;

-- 3. 计算抢到的金额(随机)
IF remain_count == 1 THEN
    amount = remain_amount; -- 最后1个红包,拿走剩余所有金额
ELSE
    -- 随机金额(0.01 ~ 剩余平均值*2)
    max_amount = remain_amount / remain_count * 2;
    amount = RAND() * max_amount;
    amount = MAX(0.01, amount); -- 至少0.01元
END IF;

-- 4. 插入抢红包记录(防止重复抢)
INSERT INTO red_packet_record (red_packet_id, user_id, amount)
VALUES (1001, 123, amount);
-- 如果重复抢,唯一约束报错,事务回滚

-- 5. 更新红包余额
UPDATE red_packet
SET remain_amount = remain_amount - amount,
    remain_count = remain_count - 1,
    status = IF(remain_count - 1 = 0, 'FINISHED', 'ACTIVE')
WHERE id = 1001;

-- 6. 用户余额增加
UPDATE account SET balance = balance + amount WHERE user_id = 123;

COMMIT;
RETURN "抢到红包:" + amount + "元";

案例5:积分扣减

需求

用户兑换商品,扣减100积分,要求:

  • 积分不能为负数
  • 高并发(1000 TPS)
  • 记录扣减日志

方案:乐观锁 + 版本号

-- 1. 建表
CREATE TABLE user_points (
    user_id INT PRIMARY KEY,
    points INT DEFAULT 0,
    version INT DEFAULT 0
);

CREATE TABLE points_log (
    id INT PRIMARY KEY AUTO_INCREMENT,
    user_id INT,
    change_amount INT, -- 正数表示增加,负数表示扣减
    reason VARCHAR(100),
    create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- 2. 扣减积分逻辑(乐观锁)
FUNCTION deduct_points(user_id, amount, reason):
    max_retries = 3

    FOR i IN 1..max_retries:
        -- 1. 查询积分和版本号(不加锁)
        user = SELECT points, version FROM user_points WHERE user_id = user_id;

        -- 2. 检查积分
        IF user.points < amount THEN
            RETURN "积分不足";
        END IF;

        -- 3. 尝试扣减积分(乐观锁)
        affected_rows = UPDATE user_points
                        SET points = points - amount,
                            version = version + 1
                        WHERE user_id = user_id
                          AND version = user.version
                          AND points >= amount; -- 双重检查

        -- 4. 更新成功
        IF affected_rows > 0 THEN
            -- 记录扣减日志
            INSERT INTO points_log (user_id, change_amount, reason)
            VALUES (user_id, -amount, reason);

            RETURN "扣减成功";
        END IF;

        -- 版本冲突,重试
        SLEEP(10ms);
    END FOR;

    RETURN "扣减失败,请重试";
END FUNCTION;

实战总结

1. 选择合适的锁

场景锁类型原因
转账、支付悲观锁高一致性要求,防止余额为负
秒杀、抢购乐观锁高并发,冲突少
红包、库存悲观锁防止超发、超卖
积分、点赞乐观锁高并发,允许重试

2. 避免死锁

  • ✅ 固定加锁顺序(按主键升序)
  • ✅ 一次性获取所有锁
  • ✅ 缩小事务范围

3. 幂等性设计

  • ✅ 唯一约束(pay_id、order_id)
  • ✅ 状态检查(已支付不再扣款)
  • ✅ 插入前查询(防止重复操作)

4. 性能优化

  • ✅ 乐观锁 + 重试(高并发)
  • ✅ Redis预减库存(秒杀)
  • ✅ 异步消息队列(削峰)

小结

转账:固定加锁顺序,避免死锁 ✅ 秒杀:乐观锁 + Redis,高并发 ✅ 支付:悲观锁 + 幂等性,防重复 ✅ 红包:悲观锁 + 唯一约束,防超发 ✅ 积分:乐观锁 + 版本号,高性能

掌握这些实战案例,就能应对大部分并发控制场景。


📚 相关阅读

  • 推荐:《乐观锁与悲观锁:应用场景对比》
  • 推荐:《死锁:产生原因与解决方案》

🎉 恭喜!你已完成MySQL事务与锁篇的全部内容!