Administrator
Administrator
发布于 2026-01-23 / 7 阅读
0
0

数据库读写不加锁会出现的问题

如果不加锁,在并发环境下会出现以下严重问题

1. 脏读(Dirty Read)

-- 事务A更新但未提交
UPDATE accounts SET balance = balance - 100 WHERE id = 1;

-- 事务B此时读取到了未提交的数据
SELECT balance FROM accounts WHERE id = 1; -- 读到未提交的余额
-- 事务A回滚
ROLLBACK;

2. 不可重复读(Non-repeatable Read)

// 同一个事务内,两次读取结果不一致
@Transactional
public void checkAccount() {
    // 第一次读取
    BigDecimal balance1 = accountDao.getBalance(userId); // 1000元
    
    // 在此期间,另一个事务更新了数据
    // UPDATE accounts SET balance = 500 WHERE id = userId;
    
    // 第二次读取(在同一个事务内)
    BigDecimal balance2 = accountDao.getBalance(userId); // 变成500元!
    
    // balance1 != balance2 → 不可重复读
}

3. 幻读(Phantom Read)

@Transactional
public void statisticalAnalysis() {
    // 第一次查询
    int count1 = orderDao.countTodayOrders(); // 100条
    
    // 在此期间,另一个事务插入了新订单
    // INSERT INTO orders ... (新增1条)
    
    // 第二次查询
    int count2 = orderDao.countTodayOrders(); // 变成101条!
    
    // 同一事务内相同查询,结果集数量不同 → 幻读
}

4. 丢失更新(Lost Update)

// 两个用户同时修改同一数据
public void concurrentUpdate() {
    // 用户A和用户B同时读取
    // 都读取到 stock = 10
    
    // 用户A:stock = 10 - 1 = 9
    // 用户B:stock = 10 - 2 = 8
    
    // 后提交的覆盖先提交的 → 用户A的更新丢失!
    // 最终结果可能是8(应该是7)
}

5. 写倾斜(Write Skew)

-- 经典会议室预订场景
-- 规则:同一时间段只能有一个会议室被预订

-- 事务A检查:9:00-10:00时间段是否空闲
SELECT * FROM bookings 
WHERE room_id = 1 
AND time = '09:00-10:00';

-- 事务B检查:同一时间段是否空闲
SELECT * FROM bookings 
WHERE room_id = 2 
AND time = '09:00-10:00';

-- 两个事务都认为时间段空闲,同时插入 → 违反业务规则
INSERT INTO bookings (room_id, time) VALUES (1, '09:00-10:00');
INSERT INTO bookings (room_id, time) VALUES (2, '09:00-10:00');

6. 具体场景示例

场景1:库存超卖

// 不加锁,100个并发请求购买最后10个库存
public boolean purchase(Long productId) {
    Product product = productDao.selectById(productId);
    if (product.getStock() > 0) {
        // 多个线程同时通过检查
        product.setStock(product.getStock() - 1);
        productDao.updateById(product);
        return true; // 结果:卖出100个,库存变-90!
    }
    return false;
}

场景2:余额并发扣款

// 用户同时发起多次支付
public boolean pay(Long userId, BigDecimal amount) {
    Account account = accountDao.selectById(userId);
    if (account.getBalance().compareTo(amount) >= 0) {
        // 并发时多个线程同时通过余额检查
        account.setBalance(account.getBalance().subtract(amount));
        accountDao.updateById(account);
        return true; // 结果:100元余额被扣了3次50元
    }
    return false;
}

7. 隔离级别与对应问题

隔离级别

脏读

不可重复读

幻读

丢失更新

读未提交

❌ 可能

❌ 可能

❌ 可能

❌ 可能

读已提交

✅ 解决

❌ 可能

❌ 可能

❌ 可能

可重复读

✅ 解决

✅ 解决

❌ 可能

❌ 可能

串行化

✅ 解决

✅ 解决

✅ 解决

✅ 解决

8. 现实世界的后果

  1. 资金损失:用户重复提现、商户多发货

  2. 数据错乱:库存负数、余额错误

  3. 业务违规:超卖、超额预订

  4. 审计问题:账目对不上

  5. 客户投诉:订单状态混乱

关键结论:数据库操作不加锁就像在红灯时过马路——平时可能没事,但一旦出事就是灾难性的。在高并发系统中,要么加锁,要么用乐观锁/版本控制,没有第三条路。


评论