悲观锁与乐观锁


悲观锁与乐观锁

在数据库并发控制中,乐观锁和悲观锁是两种核心机制,用于解决多事务同时操作同一数据时的冲突问题(如脏写、不可重复读等)。两者的核心区别在于对 “并发冲突概率” 的假设不同,进而导致实现方式和适用场景的差异。

1. 悲观锁(Pessimistic Lock)

1. 核心思想

假设并发冲突会频繁发生,因此在操作数据前,会先 “锁定” 数据,阻止其他事务对该数据的修改,直到当前事务完成。

2. 实现方式

依赖数据库底层的锁机制(如行锁、表锁),通过显式或隐式的方式锁定数据:

  • 显式锁定:通过 SQL 语句主动加锁,例如 SELECT ... FOR UPDATE(排他锁),事务执行时会锁定查询到的行,其他事务尝试修改或加锁时会阻塞等待,直到当前事务提交 / 回滚释放锁。
  • 隐式锁定:数据库引擎自动加锁,例如 InnoDB 在执行UPDATE/DELETE时,会对匹配的行自动加行锁,事务结束后释放。

3. 优缺点

  • 优点:能有效防止并发冲突,安全性高,无需额外的重试逻辑。
  • 缺点:
  • 锁定期间会阻塞其他事务,降低并发性能(尤其是长事务);
  • 可能导致死锁(例如两个事务互相等待对方释放锁);
  • 若锁定范围过大(如无索引导致行锁升级为表锁),会严重影响性能。

4. 适用场景

  • 写操作频繁,并发冲突概率高的场景(如库存扣减、金融交易);
  • 事务执行时间长,需要确保数据一致性的场景。

2. 乐观锁(Optimistic Lock)

1. 核心思想

假设并发冲突很少发生,因此操作数据时不会预先锁定,而是在 “更新数据时” 检查数据是否被其他事务修改过,若未被修改则成功,否则失败(需重试)。

2. 实现方式

由应用层实现,不依赖数据库锁机制,常见方式:

  • 版本号机制:在表中新增一个version字段(整数),逻辑如下:
  • 查询数据时,同时获取当前版本号:SELECT id, name, version FROM table WHERE id = 1;(假设版本号为 2);
  • 更新数据时,检查版本号是否与查询时一致:UPDATE table SET name = 'new', version = version + 1 WHERE id = 1 AND version = 2;
  • 若更新行数为 1,说明成功(无冲突);若为 0,说明数据已被其他事务修改(版本号变化),需重试。
  • 时间戳机制:原理同版本号,用update_time字段(时间戳)代替版本号,更新时检查时间戳是否一致。

3. 优缺点

  • 优点:
  • 无锁竞争,并发性能高(尤其读操作多的场景);
  • 不会导致死锁,实现简单(应用层逻辑)。
  • 缺点:
  • 存在 “重试成本”:冲突发生时需重新执行事务(可能多次重试);
  • 无法解决 “幻读”(因为不锁定数据,其他事务可能插入新数据)。

4. 适用场景

  • 读操作频繁,写操作少,并发冲突概率低的场景(如商品详情页、用户信息查询);
  • 短事务场景(减少重试成本)。

3. 对比总结

维度 悲观锁 乐观锁
冲突假设 冲突频繁发生 冲突很少发生
实现层面 数据库底层锁机制 应用层(版本号 / 时间戳)
并发性能 低(阻塞等待) 高(无锁竞争)
死锁风险 有(锁竞争时)
适用场景 写多读少,冲突频繁 读多写少,冲突稀少

选择原则:根据并发冲突的概率决定 —— 冲突多则用悲观锁保证安全性,冲突少则用乐观锁提升性能。

4. 具体实现

MySQL + InnoDB为例,结合库存扣减场景(经典的并发控制场景),分别展示悲观锁和乐观锁的具体实现。

场景说明:多个用户同时购买同一商品,需要扣减商品库存,确保库存不会出现负数(避免超卖)。

CREATE TABLE product_stock (
  id INT PRIMARY KEY AUTO_INCREMENT,
  product_id BIGINT NOT NULL COMMENT '商品ID',
  stock INT NOT NULL COMMENT '库存数量',
  version INT NOT NULL DEFAULT 0 COMMENT '版本号(乐观锁用)',
  update_time TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  UNIQUE KEY uk_product_id (product_id)
) ENGINE=InnoDB COMMENT '商品库存表';

-- 初始化数据:商品ID=1001,初始库存100
INSERT INTO product_stock (product_id, stock) VALUES (1001, 100);

1. 悲观锁实现

核心逻辑:在查询库存时显式加排他锁(FOR UPDATE),阻止其他事务同时修改,直到当前事务提交 / 回滚释放锁。

def deduct_stock_pessimistic(product_id, quantity=1):
    conn = None
    try:
        conn = get_conn()
        cursor = conn.cursor(DictCursor)  # 用字典游标,方便获取字段

        # 1. 查询库存并加排他锁(FOR UPDATE)
        # 注意:WHERE条件必须命中索引(这里product_id是唯一索引),否则会锁表
        cursor.execute(
            "SELECT stock FROM product_stock WHERE product_id = %s FOR UPDATE",
            (product_id,)
        )
        result = cursor.fetchone()
        if not result:
            print(f"商品{product_id}不存在")
            return False

        current_stock = result["stock"]

        # 2. 检查库存是否充足
        if current_stock < quantity:
            print(f"库存不足,当前库存:{current_stock},需扣减:{quantity}")
            conn.rollback()  # 无操作,回滚事务
            return False

        # 3. 扣减库存
        cursor.execute(
            "UPDATE product_stock SET stock = stock - %s WHERE product_id = %s",
            (quantity, product_id)
        )
        print(f"扣减成功,剩余库存:{current_stock - quantity}")
        conn.commit()  # 提交事务,释放锁
        return True

    except Exception as e:
        if conn:
            conn.rollback()  # 异常时回滚
        print(f"悲观锁扣减失败:{str(e)}")
        return False

    finally:
        if conn:
            conn.close()  # 关闭连接


# 测试调用
deduct_stock_pessimistic(product_id=1001, quantity=1)

2. 乐观锁实现

核心逻辑:查询时获取版本号,更新时检查版本号是否匹配(未被修改),若匹配则更新并递增版本号;否则重试。

def deduct_stock_optimistic(product_id, quantity=1, max_retry=3):
    conn = None
    retry_count = 0

    while retry_count < max_retry:
        try:
            conn = get_conn()
            cursor = conn.cursor(DictCursor)

            # 1. 查询库存和当前版本号
            cursor.execute(
                "SELECT stock, version FROM product_stock WHERE product_id = %s",
                (product_id,)
            )
            result = cursor.fetchone()
            if not result:
                print(f"商品{product_id}不存在")
                return False

            current_stock = result["stock"]
            current_version = result["version"]

            # 2. 检查库存是否充足
            if current_stock < quantity:
                print(f"库存不足,当前库存:{current_stock},需扣减:{quantity}")
                conn.rollback()
                return False

            # 3. 尝试更新:WHERE条件同时检查版本号(确保期间未被修改)
            affect_rows = cursor.execute(
                """UPDATE product_stock 
                   SET stock = stock - %s, version = version + 1 
                   WHERE product_id = %s AND version = %s""",
                (quantity, product_id, current_version)
            )

            if affect_rows == 1:
                # 更新成功(版本号匹配,无冲突)
                print(f"扣减成功,剩余库存:{current_stock - quantity},新版本号:{current_version + 1}")
                conn.commit()
                return True
            else:
                # 更新失败(版本号不匹配,被其他事务修改),重试
                print(f"检测到并发冲突,重试第{retry_count + 1}次...")
                conn.rollback()  # 回滚本次尝试
                retry_count += 1

        except Exception as e:
            if conn:
                conn.rollback()
            print(f"乐观锁扣减失败:{str(e)}")
            return False

        finally:
            if conn:
                conn.close()

    # 超过最大重试次数
    print(f"超过最大重试次数({max_retry}次),扣减失败")
    return False


# 测试调用
deduct_stock_optimistic(product_id=1001, quantity=1)

0 条评论

发表评论

暂无评论,欢迎发表您的观点!