Skip to content

Oracle 乐观锁与悲观锁:并发控制的双剑客

你有没有遇到过这种纠结:

用乐观锁还是悲观锁?

乐观锁说:最后再检查冲突。 悲观锁说:先把锁拿了再说。

选错了,系统卡死;选对了,性能飞起。

今天,彻底搞懂这两种锁策略。


为什么需要锁策略?

并发环境下,数据可能被多个事务同时修改:

时间线:
─────────────────────────────────────────────────────►

事务A:  读取库存=100     计算-1=99       写入99
事务B:       读取库存=100     计算-1=99       写入99

结果:库存从100变成99,实际扣减了2次!

锁策略就是来解决这个问题的。


悲观锁(Pessimistic Locking)

核心思想

先锁定再操作:假设并发冲突一定会发生,所以在读取数据时就加锁。

sql
-- 悲观锁:SELECT FOR UPDATE
SELECT stock INTO v_stock 
FROM inventory 
WHERE product_id = 100 
FOR UPDATE;  -- 锁定这行

-- 检查库存
IF v_stock >= v_quantity THEN
    -- 更新库存
    UPDATE inventory SET stock = stock - v_quantity WHERE product_id = 100;
    COMMIT;
ELSE
    ROLLBACK;
    -- 库存不足
END IF;

悲观锁的 Java 实现

java
public class InventoryService {
    
    public boolean purchaseWithPessimisticLock(
            Connection conn, long productId, int quantity) throws SQLException {
        
        try {
            conn.setAutoCommit(false);
            
            // 1. 锁定并读取库存
            String lockSql = """
                SELECT stock FROM inventory 
                WHERE product_id = ? 
                FOR UPDATE
                """;
            
            try (PreparedStatement ps = conn.prepareStatement(lockSql)) {
                ps.setLong(1, productId);
                ResultSet rs = ps.executeQuery();
                
                if (!rs.next()) {
                    conn.rollback();
                    return false;  // 产品不存在
                }
                
                int stock = rs.getInt("stock");
                
                // 2. 检查库存
                if (stock < quantity) {
                    conn.rollback();
                    return false;  // 库存不足
                }
                
                // 3. 更新库存
                String updateSql = """
                    UPDATE inventory 
                    SET stock = stock - ? 
                    WHERE product_id = ?
                    """;
                
                try (PreparedStatement ups = conn.prepareStatement(updateSql)) {
                    ups.setInt(1, quantity);
                    ups.setLong(2, productId);
                    ups.executeUpdate();
                }
            }
            
            conn.commit();
            return true;
            
        } catch (SQLException e) {
            conn.rollback();
            throw e;
        }
    }
}

悲观锁的特点

特点说明
锁获取时机读取数据时立即加锁
冲突处理阻塞等待
适用场景冲突频繁、数据一致性要求高
性能影响锁持有时间长,并发能力下降
死锁风险存在(需要统一加锁顺序)

悲观锁的变体

sql
-- NOWAIT:不等待锁
SELECT stock FROM inventory WHERE product_id = 100 FOR UPDATE NOWAIT;
-- 锁被占用时立即报错 ORA-00054

-- WAIT n:等待 n 秒
SELECT stock FROM inventory WHERE product_id = 100 FOR UPDATE WAIT 10;
-- 等待超过 10 秒后报错

-- SKIP LOCKED:跳过已锁定的行(Oracle 12c+)
SELECT * FROM task_queue WHERE status = 'PENDING' 
FOR UPDATE SKIP LOCKED;
-- 已锁定的行被跳过,不等待

乐观锁(Optimistic Locking)

核心思想

最后再检查冲突:假设并发冲突很少发生,读取数据时不加锁,更新时检查数据是否被修改。

sql
-- 乐观锁:使用版本号
-- 读取数据(包含版本号)
SELECT stock, version INTO v_stock, v_version
FROM inventory 
WHERE product_id = 100;

-- 计算新库存
v_new_stock := v_stock - v_quantity;

-- 更新(带版本检查)
UPDATE inventory 
SET stock = v_new_stock, 
    version = version + 1
WHERE product_id = 100 
  AND version = v_version;  -- 版本匹配才更新

-- 检查影响行数
IF SQL%ROWCOUNT = 0 THEN
    -- 版本不匹配,说明数据被修改过,重试
    ROLLBACK;
ELSE
    COMMIT;
END IF;

乐观锁的 Java 实现

java
public class InventoryService {
    
    public boolean purchaseWithOptimisticLock(
            Connection conn, long productId, int quantity, int expectedVersion) 
            throws SQLException {
        
        String sql = """
            UPDATE inventory 
            SET stock = stock - ?,
                version = version + 1
            WHERE product_id = ?
              AND version = ?
              AND stock >= ?
            """;
        
        try (PreparedStatement ps = conn.prepareStatement(sql)) {
            ps.setInt(1, quantity);
            ps.setLong(2, productId);
            ps.setInt(3, expectedVersion);
            ps.setInt(4, quantity);
            
            int rows = ps.executeUpdate();
            return rows > 0;
        }
    }
    
    // 带重试的乐观锁
    public boolean purchaseWithRetry(
            Connection conn, long productId, int quantity, int maxRetries) 
            throws SQLException {
        
        int retries = 0;
        while (retries < maxRetries) {
            // 获取当前版本
            int currentVersion = getCurrentVersion(conn, productId);
            
            // 尝试更新
            if (purchaseWithOptimisticLock(conn, productId, quantity, currentVersion)) {
                return true;  // 成功
            }
            
            retries++;
            if (retries < maxRetries) {
                try {
                    Thread.sleep(50 * retries);  // 指数退避
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }
        }
        
        return false;  // 重试次数用尽
    }
    
    private int getCurrentVersion(Connection conn, long productId) throws SQLException {
        String sql = "SELECT version FROM inventory WHERE product_id = ?";
        try (PreparedStatement ps = conn.prepareStatement(sql)) {
            ps.setLong(1, productId);
            ResultSet rs = ps.executeQuery();
            if (rs.next()) {
                return rs.getInt("version");
            }
            throw new SQLException("Product not found: " + productId);
        }
    }
}

乐观锁的特点

特点说明
锁获取时机更新时才检查
冲突处理失败后重试
适用场景冲突较少、数据一致性要求高
性能影响无阻塞,并发能力强
死锁风险

性能对比

并发性能对比

并发度

   │    ╭─── 乐观锁
   │   ╱
   │  ╱
   │ ╱
   │╱
   └───────────────────────────► 数据冲突率
场景推荐策略原因
冲突率 < 5%乐观锁几乎无阻塞,并发能力最强
冲突率 5-20%乐观锁(加重试)偶尔重试,性能可接受
冲突率 > 20%悲观锁乐观锁重试开销大
冲突率 > 50%悲观锁乐观锁几乎不可用

锁等待时间对比

java
// 悲观锁:锁等待时间 = 锁持有时间
// 悲观锁场景:
// 1. 读取数据:立即获取锁
// 2. 业务处理:锁持有中
// 3. 写入数据:释放锁
// 总等待时间 = 业务处理时间

// 乐观锁:等待时间 = 重试次数 × 每次操作时间
// 乐观锁场景:
// 1. 读取数据:无锁
// 2. 业务处理:无锁
// 3. 写入数据:检查版本
//    - 成功:无等待
//    - 失败:重试(可能多次)

实际场景选择

场景一:库存扣减

java
// 推荐:悲观锁(冲突频繁)
public class InventoryService {
    
    @Transactional
    public void purchase(long productId, int quantity) {
        // 悲观锁:确保数据一致性
        Inventory inv = inventoryRepository.findByIdForUpdate(productId);
        if (inv.getStock() &lt; quantity) {
            throw new InsufficientStockException();
        }
        inv.setStock(inv.getStock() - quantity);
        inventoryRepository.save(inv);
    }
}

场景二:用户资料更新

java
// 推荐:乐观锁(冲突极少)
public class UserService {
    
    @Transactional
    public void updateUser(Long userId, UserUpdateRequest request) {
        // 乐观锁:读取当前版本
        User user = userRepository.findById(userId);
        
        // 修改数据
        user.setName(request.getName());
        user.setEmail(request.getEmail());
        
        // 乐观锁更新(版本检查)
        int rows = userRepository.updateWithOptimisticLock(user);
        if (rows == 0) {
            throw new OptimisticLockException("用户数据已被修改");
        }
    }
}

场景三:订单状态流转

java
// 推荐:悲观锁(状态流转必须串行)
public class OrderService {
    
    @Transactional
    public void processOrder(long orderId) {
        // 悲观锁:锁定订单
        Order order = orderRepository.findByIdForUpdate(orderId);
        
        // 状态检查
        if (order.getStatus() != OrderStatus.PENDING) {
            throw new InvalidOrderStatusException();
        }
        
        // 状态流转
        order.setStatus(OrderStatus.PROCESSING);
        orderRepository.save(order);
    }
}

场景四:计数器更新

java
// 推荐:乐观锁(计数器冲突概率低)
public class CounterService {
    
    public void increment(String counterName, int delta) {
        int maxRetries = 3;
        int retries = 0;
        
        while (retries &lt; maxRetries) {
            Counter counter = counterRepository.findByName(counterName);
            int newValue = counter.getValue() + delta;
            
            int rows = counterRepository.updateValue(
                counterName, counter.getVersion(), newValue);
            
            if (rows > 0) {
                return;
            }
            retries++;
        }
        
        throw new ConcurrentModificationException("计数器更新失败");
    }
}

混合策略

有时候可以结合两种策略:

java
public class SmartInventoryService {
    
    // 热点商品使用悲观锁(冲突多)
    @Transactional
    public void purchaseHotItem(long productId, int quantity) {
        Inventory inv = inventoryRepository.findByIdForUpdate(productId);
        // ... 悲观锁处理
    }
    
    // 普通商品使用乐观锁(冲突少)
    public void purchaseNormalItem(long productId, int quantity) {
        int maxRetries = 3;
        int retries = 0;
        
        while (retries &lt; maxRetries) {
            Inventory inv = inventoryRepository.findById(productId);
            
            if (inv.getStock() &lt; quantity) {
                throw new InsufficientStockException();
            }
            
            int rows = inventoryRepository.updateWithOptimisticLock(
                productId, inv.getVersion(), inv.getStock() - quantity);
            
            if (rows > 0) {
                return;
            }
            retries++;
        }
        
        throw new ConcurrentModificationException();
    }
}

面试高频问题

Q1: 乐观锁和悲观锁的区别?

悲观锁在读取数据时就加锁,假设并发冲突一定发生;乐观锁在更新时才检查冲突,假设并发冲突很少发生。悲观锁适合冲突频繁的场景,乐观锁适合冲突较少的场景。

Q2: 乐观锁怎么实现?

常用方法:版本号(version)、时间戳(timestamp)、或计算更新前后的值是否变化。更新时 WHERE 条件包含版本号,如果影响行数为 0 说明版本已变化。

Q3: 乐观锁重试次数用完了怎么办?

重试次数用完后应该抛出明确的异常(如 OptimisticLockException),让调用方决定如何处理:显示错误给用户、放入重试队列、或人工干预。

Q4: 什么时候选择悲观锁?

高并发写场景(如库存扣减)、状态流转必须串行的场景、长时间计算后更新的场景。


总结

特性乐观锁悲观锁
锁时机更新时检查读取时加锁
冲突处理重试等待
并发能力
死锁风险存在
适用场景冲突少冲突多
实现复杂度稍高简单

没有最好的锁策略,只有最适合场景的策略。


下一步

基于 VitePress 构建