返回 筑基・数据元府藏真

事务隔离级别与并发问题深度剖析

博主
大约 19 分钟

事务隔离级别与并发问题深度剖析

问题引入:诡异的数据不一致

去年双十一期间,我们的电商系统出现了一个诡异的问题:用户下单后,订单状态显示"已支付",但库存却没有扣减。更奇怪的是,客服系统查询到的订单金额和用户看到的不一致。经过三天三夜的排查,最终发现是事务隔离级别配置不当导致的并发问题。

// 事务A:订单支付
BEGIN;
UPDATE orders SET status = 'PAID', paid_amount = 999.00 WHERE id = 10001;
UPDATE inventory SET stock = stock - 1 WHERE product_id = 2001;
-- 此时事务A还未提交

// 事务B:客服查询
SELECT status, paid_amount FROM orders WHERE id = 10001; 
-- 读到了未提交的PAID状态和999.00金额!(脏读)

// 事务C:库存查询
SELECT stock FROM inventory WHERE product_id = 2001;
-- 读到了未扣减前的库存!(脏读)

// 事务A回滚(支付失败)
ROLLBACK;

这个案例让我深刻认识到:事务隔离级别不是简单的配置项,而是需要深入理解其原理和适用场景的核心技术点。

现象描述:并发问题的三种形态

案例1:脏读(Dirty Read)

场景:银行转账系统

-- 事务A:转账操作
BEGIN;
UPDATE account SET balance = balance - 1000 WHERE id = 1;  -- 余额:10000 -> 9000
-- 此时事务A还未提交

-- 事务B:查询余额(脏读)
SELECT balance FROM account WHERE id = 1;  -- 读到9000

-- 事务A回滚(转账失败)
ROLLBACK;  -- 余额恢复为10000

-- 事务B基于错误数据做决策
-- 用户认为余额只有9000,可能导致透支

影响:基于未提交数据做决策,可能导致严重的业务错误。

案例2:不可重复读(Non-Repeatable Read)

场景:电商库存校验

-- 事务A:下单流程
BEGIN;
SELECT stock FROM inventory WHERE product_id = 1001;  -- 读到stock=10
-- 业务逻辑校验:10 > 0,可以下单

-- 事务B:并发下单并提交
BEGIN;
UPDATE inventory SET stock = stock - 5 WHERE product_id = 1001;  -- stock=10->5
COMMIT;

-- 事务A:再次查询库存(不可重复读)
SELECT stock FROM inventory WHERE product_id = 1001;  -- 读到stock=5
-- 同一事务内两次读取结果不一致!

-- 事务A继续下单
UPDATE inventory SET stock = stock - 1 WHERE product_id = 1001;  -- stock=5->4
COMMIT;

影响:同一事务内多次读取同一数据,结果不一致,可能导致业务逻辑混乱。

案例3:幻读(Phantom Read)

场景:统计报表生成

-- 事务A:生成销售报表
BEGIN;
SELECT COUNT(*) FROM orders WHERE order_date = '2024-01-15';  -- 读到100条
SELECT SUM(amount) FROM orders WHERE order_date = '2024-01-15';  -- 读到50000元

-- 事务B:插入新订单并提交
BEGIN;
INSERT INTO orders (order_date, amount) VALUES ('2024-01-15', 999.00);
COMMIT;

-- 事务A:再次统计(幻读)
SELECT COUNT(*) FROM orders WHERE order_date = '2024-01-15';  -- 读到101条!
SELECT SUM(amount) FROM orders WHERE order_date = '2024-01-15';  -- 读到50999元!
-- 同一事务内统计结果不一致!
COMMIT;

影响:同一事务内多次执行范围查询,结果集不一致,可能导致统计报表错误。

原因分析:隔离级别实现原理

1. 四种隔离级别详解

隔离级别脏读不可重复读幻读实现机制性能影响
READ UNCOMMITTED无锁,直接读取最新数据最高
READ COMMITTEDMVCC,读取已提交版本
REPEATABLE READMVCC + 间隙锁
SERIALIZABLE所有操作加锁,串行执行最低

2. MVCC实现原理深度剖析

2.1 InnoDB行记录隐藏字段

InnoDB每行记录包含三个隐藏字段:
┌─────────────────────────────────────────────────────────────┐
│  实际列数据  │  DB_TRX_ID  │  DB_ROLL_PTR  │  DB_ROW_ID    │
├─────────────────────────────────────────────────────────────┤
│  用户定义的  │  6字节      │   7字节       │   6字节       │
│  列数据      │  最后修改的  │  回滚指针     │  行ID(无主键 │
│             │  事务ID     │  指向undo log │  时自动生成) │
└─────────────────────────────────────────────────────────────┘

2.2 Undo Log版本链

事务ID分配:
- 只读事务:不分配事务ID
- 读写事务:分配递增的事务ID(trx_id)

Undo Log版本链结构:
┌──────────┐    ┌──────────┐    ┌──────────┐
│  行记录R  │───→│  Undo    │───→│  Undo    │───→ ...
│  trx_id=50│    │  trx_id=40│    │  trx_id=30│
│  roll_ptr │    │  指向trx=30│    │  指向更早 │
└──────────┘    └──────────┘    └──────────┘
     ↑
     │
当前事务看到的可能是历史版本

2.3 Read View可见性判断

// Read View结构
typedef struct read_view_struct {
    trx_id_t creator_trx_id;    // 创建Read View的事务ID
    trx_id_t up_limit_id;       // 活跃事务列表中最小的事务ID
    trx_id_t low_limit_id;      // 下一个将被分配的事务ID
    trx_id_t* trx_ids;          // 创建Read View时活跃的事务ID列表
    ulint n_trx_ids;            // 活跃事务数量
} read_view_t;

// 可见性判断算法
bool row_visible(read_view_t* view, trx_id_t row_trx_id) {
    // 1. 如果row_trx_id < up_limit_id:事务已提交,可见
    if (row_trx_id < view->up_limit_id) {
        return true;
    }
    
    // 2. 如果row_trx_id >= low_limit_id:事务在Read View创建后启动,不可见
    if (row_trx_id >= view->low_limit_id) {
        return false;
    }
    
    // 3. 如果在trx_ids列表中:事务活跃,不可见
    for (ulint i = 0; i < view->n_trx_ids; i++) {
        if (view->trx_ids[i] == row_trx_id) {
            return false;
        }
    }
    
    // 4. 事务已提交,可见
    return true;
}

2.4 不同隔离级别的Read View创建时机

隔离级别Read View创建时机效果
READ COMMITTED每条SELECT语句执行时创建看到其他事务已提交的最新数据
REPEATABLE READ事务第一条SELECT时创建,整个事务复用同一事务内看到的数据一致

3. 锁机制详解

3.1 InnoDB锁类型

InnoDB锁分类:
┌─────────────────────────────────────────────────────────────┐
│                      InnoDB锁体系                           │
├─────────────────────────────────────────────────────────────┤
│  按锁粒度:                                                 │
│  ├── 行锁(Record Lock):锁定单个行记录                     │
│  ├── 间隙锁(Gap Lock):锁定索引记录间隙                    │
│  └── 临键锁(Next-Key Lock):行锁+间隙锁                  │
│                                                             │
│  按锁模式:                                                 │
│  ├── 共享锁(S Lock):SELECT ... LOCK IN SHARE MODE        │
│  └── 排他锁(X Lock):SELECT ... FOR UPDATE / DML          │
│                                                             │
│  按使用方式:                                               │
│  ├── 显式锁:用户主动加锁                                   │
│  └── 隐式锁:数据库自动加锁                                 │
└─────────────────────────────────────────────────────────────┘

3.2 行锁(Record Lock)

-- 锁定主键索引记录
SELECT * FROM orders WHERE id = 1001 FOR UPDATE;
-- 锁定唯一索引记录
SELECT * FROM users WHERE email = 'user@example.com' FOR UPDATE;

3.3 间隙锁(Gap Lock)

-- 锁定id在(10, 20)之间的间隙,防止其他事务插入id=15的记录
SELECT * FROM orders WHERE id BETWEEN 10 AND 20 FOR UPDATE;

-- 间隙锁范围示意(假设表中已有id=10和id=20的记录)
-- (-∞, 10]  [10, 20]  [20, +∞)
--           ↑锁定这个间隙

3.4 临键锁(Next-Key Lock)

-- 临键锁 = 行锁 + 间隙锁
-- 锁定id=10的行记录,以及(10, 20)的间隙
SELECT * FROM orders WHERE id = 10 FOR UPDATE;

-- 在REPEATABLE READ下,范围查询使用临键锁防止幻读
SELECT * FROM orders WHERE id > 10 AND id < 20 FOR UPDATE;
-- 锁定:(10, 20] 的所有记录和间隙

4. 死锁产生与处理

4.1 死锁产生条件

死锁产生的四个必要条件:
1. 互斥条件:资源一次只能被一个事务占用
2. 请求与保持:事务持有资源同时请求新资源
3. 不剥夺条件:资源只能由持有者主动释放
4. 循环等待:事务之间形成循环等待链

死锁示例:
事务A:持有锁1,请求锁2
事务B:持有锁2,请求锁1
→ 死锁!

4.2 死锁检测与处理

-- 查看死锁日志
SHOW ENGINE INNODB STATUS;

-- 关键信息
------------------------
LATEST DETECTED DEADLOCK
------------------------
*** (1) TRANSACTION:
TRANSACTION 12345, ACTIVE 3 sec starting index read
mysql tables in use 1, locked 1
LOCK WAIT 2 lock struct(s), heap size 1136, 1 row lock(s)
MySQL thread id 100, OS thread handle 123456789, query id 1000 localhost 127.0.0.1 user updating
UPDATE account SET balance = balance - 100 WHERE id = 1
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 58 page no 3 n bits 72 index PRIMARY of table `test`.`account` trx id 12345 lock_mode X locks rec but not gap waiting

*** (2) TRANSACTION:
TRANSACTION 12346, ACTIVE 5 sec starting index read
mysql tables in use 1, locked 1
3 lock struct(s), heap size 1136, 2 row lock(s)
MySQL thread id 101, OS thread handle 123456790, query id 1001 localhost 127.0.0.1 user updating
UPDATE account SET balance = balance - 200 WHERE id = 2
*** (2) HOLDS THE LOCK(S):
RECORD LOCKS space id 58 page no 3 n bits 72 index PRIMARY of table `test`.`account` trx id 12346 lock_mode X locks rec but not gap

*** (2) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 58 page no 3 n bits 72 index PRIMARY of table `test`.`account` trx id 12346 lock_mode X locks rec but not gap waiting

*** WE ROLL BACK TRANSACTION (1)

解决方案:隔离级别选择与优化

1. 隔离级别选择决策树

隔离级别选择决策树:
                    ┌─────────────────┐
                    │  业务场景分析    │
                    └────────┬────────┘
                             │
            ┌────────────────┼────────────────┐
            ↓                ↓                ↓
    ┌───────────────┐ ┌───────────────┐ ┌───────────────┐
    │  强一致性要求  │ │  一般业务场景  │ │  允许短暂不一致│
    │  金融/支付    │ │  电商/社交    │ │  日志/统计    │
    └───────┬───────┘ └───────┬───────┘ └───────┬───────┘
            │                │                │
            ↓                ↓                ↓
    ┌───────────────┐ ┌───────────────┐ ┌───────────────┐
    │ SERIALIZABLE  │ │ REPEATABLE READ│ │ READ COMMITTED│
    │ 或外部协调    │ │  + 乐观锁     │ │  + 应用补偿   │
    └───────────────┘ └───────────────┘ └───────────────┘

2. 不同业务场景的隔离级别推荐

业务场景推荐隔离级别原因额外措施
银行转账SERIALIZABLE绝对一致性要求配合分布式事务
电商订单REPEATABLE READ防止不可重复读和幻读乐观锁版本控制
库存扣减REPEATABLE READ防止超卖数据库行锁 + 应用限流
用户查询READ COMMITTED性能优先,允许短暂不一致缓存补偿
日志记录READ UNCOMMITTED最高性能,可接受脏读
报表统计REPEATABLE READ数据一致性要求快照读或副本

3. 乐观锁实现方案

-- 方案1:版本号机制
CREATE TABLE inventory (
    id BIGINT PRIMARY KEY,
    product_id BIGINT NOT NULL,
    stock INT NOT NULL DEFAULT 0,
    version INT NOT NULL DEFAULT 0,
    update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    INDEX idx_product_id (product_id)
) ENGINE=InnoDB;

-- 乐观锁扣减库存
UPDATE inventory 
SET stock = stock - 1, version = version + 1 
WHERE product_id = 1001 AND version = 5 AND stock > 0;
-- 如果影响行数为0,说明并发冲突,需要重试

-- 方案2:CAS(Compare And Swap)
UPDATE account 
SET balance = balance - 100 
WHERE id = 1 AND balance >= 100;
// Java乐观锁实现
@Service
public class InventoryService {
    
    @Autowired
    private InventoryMapper inventoryMapper;
    
    public boolean deductStock(Long productId, int quantity) {
        int retryCount = 0;
        int maxRetries = 3;
        
        while (retryCount < maxRetries) {
            // 1. 查询当前版本号
            Inventory inventory = inventoryMapper.selectByProductId(productId);
            if (inventory == null || inventory.getStock() < quantity) {
                return false; // 库存不足
            }
            
            // 2. 尝试更新(乐观锁)
            int affectedRows = inventoryMapper.updateStock(
                productId, 
                inventory.getStock() - quantity,
                inventory.getVersion(),
                inventory.getVersion() + 1
            );
            
            // 3. 更新成功
            if (affectedRows > 0) {
                return true;
            }
            
            // 4. 更新失败,重试
            retryCount++;
            // 可选:随机退避
            try {
                Thread.sleep((long)(Math.random() * 50));
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
        
        return false; // 超过重试次数
    }
}

4. 悲观锁实现方案

-- 悲观锁扣减库存
BEGIN;
-- 1. 查询并加排他锁
SELECT stock FROM inventory WHERE product_id = 1001 FOR UPDATE;
-- 2. 业务逻辑判断
-- 3. 更新库存
UPDATE inventory SET stock = stock - 1 WHERE product_id = 1001;
COMMIT;
// Java悲观锁实现
@Service
public class InventoryService {
    
    @Transactional
    public boolean deductStockWithPessimisticLock(Long productId, int quantity) {
        // 1. 查询并加排他锁(FOR UPDATE)
        Inventory inventory = inventoryMapper.selectByProductIdForUpdate(productId);
        
        if (inventory == null || inventory.getStock() < quantity) {
            return false; // 库存不足
        }
        
        // 2. 更新库存
        inventoryMapper.updateStock(productId, inventory.getStock() - quantity);
        
        return true;
    }
}

5. 死锁预防策略

// 策略1:按固定顺序访问资源
@Service
public class TransferService {
    
    @Transactional
    public void transfer(Long fromAccountId, Long toAccountId, BigDecimal amount) {
        // 按ID升序加锁,避免循环等待
        Long firstId = Math.min(fromAccountId, toAccountId);
        Long secondId = Math.max(fromAccountId, toAccountId);
        
        // 先锁定ID小的账户
        Account firstAccount = accountMapper.selectByIdForUpdate(firstId);
        // 再锁定ID大的账户
        Account secondAccount = accountMapper.selectByIdForUpdate(secondId);
        
        // 执行转账逻辑
        if (fromAccountId.equals(firstId)) {
            // fromAccount是firstAccount
            firstAccount.debit(amount);
            secondAccount.credit(amount);
        } else {
            // fromAccount是secondAccount
            secondAccount.debit(amount);
            firstAccount.credit(amount);
        }
        
        accountMapper.update(firstAccount);
        accountMapper.update(secondAccount);
    }
}

// 策略2:减少事务范围
@Service
public class OrderService {
    
    public void createOrder(OrderDTO orderDTO) {
        // 1. 参数校验(事务外)
        validateOrder(orderDTO);
        
        // 2. 查询商品信息(事务外,使用缓存)
        Product product = productService.getProductFromCache(orderDTO.getProductId());
        
        // 3. 核心业务逻辑(事务内)
        transactionTemplate.execute(status -> {
            // 3.1 扣减库存
            boolean success = inventoryService.deductStock(
                orderDTO.getProductId(), 
                orderDTO.getQuantity()
            );
            if (!success) {
                throw new BusinessException("库存不足");
            }
            
            // 3.2 创建订单
            Order order = createOrderEntity(orderDTO);
            orderMapper.insert(order);
            
            return order;
        });
        
        // 4. 发送消息通知(事务外)
        messageService.sendOrderCreatedMessage(order);
    }
}

// 策略3:死锁检测与重试
@Service
public class RetryableService {
    
    private static final Logger logger = LoggerFactory.getLogger(RetryableService.class);
    
    @Retryable(
        value = {DeadlockLoserDataAccessException.class},
        maxAttempts = 3,
        backoff = @Backoff(delay = 100, multiplier = 2)
    )
    @Transactional
    public void executeWithRetry(BusinessOperation operation) {
        try {
            operation.execute();
        } catch (DeadlockLoserDataAccessException e) {
            logger.warn("检测到死锁,准备重试", e);
            throw e; // 抛出异常触发重试
        }
    }
}

实战案例:电商系统事务设计

案例1:订单创建流程

@Service
public class OrderCreationService {
    
    @Autowired
    private IdGenerator idGenerator;
    @Autowired
    private InventoryService inventoryService;
    @Autowired
    private OrderMapper orderMapper;
    @Autowired
    private OrderItemMapper orderItemMapper;
    
    /**
     * 创建订单 - 使用REPEATABLE READ + 悲观锁
     */
    @Transactional(isolation = Isolation.REPEATABLE_READ, timeout = 10)
    public Order createOrder(CreateOrderRequest request) {
        // 1. 生成订单号
        String orderNo = idGenerator.generateOrderNo();
        
        // 2. 锁定库存(悲观锁)
        List<InventoryLock> locks = new ArrayList<>();
        try {
            for (OrderItemRequest item : request.getItems()) {
                // 按商品ID排序加锁,防止死锁
                locks.add(inventoryService.lockStock(item.getProductId(), item.getQuantity()));
            }
            
            // 3. 计算订单金额
            BigDecimal totalAmount = calculateTotalAmount(request.getItems());
            
            // 4. 创建订单
            Order order = new Order();
            order.setOrderNo(orderNo);
            order.setUserId(request.getUserId());
            order.setTotalAmount(totalAmount);
            order.setStatus(OrderStatus.CREATED);
            order.setCreateTime(LocalDateTime.now());
            orderMapper.insert(order);
            
            // 5. 创建订单项
            for (OrderItemRequest item : request.getItems()) {
                OrderItem orderItem = new OrderItem();
                orderItem.setOrderId(order.getId());
                orderItem.setProductId(item.getProductId());
                orderItem.setQuantity(item.getQuantity());
                orderItem.setUnitPrice(item.getUnitPrice());
                orderItemMapper.insert(orderItem);
            }
            
            // 6. 确认库存扣减
            for (InventoryLock lock : locks) {
                inventoryService.confirmDeduction(lock);
            }
            
            return order;
            
        } catch (Exception e) {
            // 回滚库存锁定
            for (InventoryLock lock : locks) {
                inventoryService.releaseLock(lock);
            }
            throw e;
        }
    }
}

案例2:库存扣减优化

-- 方案1:数据库层面原子操作(推荐)
UPDATE inventory 
SET stock = stock - #{quantity}, 
    version = version + 1,
    update_time = NOW()
WHERE product_id = #{productId} 
  AND stock >= #{quantity};

-- 方案2:使用存储过程
DELIMITER //
CREATE PROCEDURE DeductStock(
    IN p_product_id BIGINT,
    IN p_quantity INT,
    OUT p_result INT
)
BEGIN
    DECLARE v_stock INT;
    DECLARE EXIT HANDLER FOR SQLEXCEPTION
    BEGIN
        ROLLBACK;
        SET p_result = -1;
    END;
    
    START TRANSACTION;
    
    -- 查询库存并加锁
    SELECT stock INTO v_stock 
    FROM inventory 
    WHERE product_id = p_product_id 
    FOR UPDATE;
    
    IF v_stock IS NULL THEN
        SET p_result = -2; -- 商品不存在
    ELSEIF v_stock < p_quantity THEN
        SET p_result = -3; -- 库存不足
    ELSE
        UPDATE inventory 
        SET stock = stock - p_quantity 
        WHERE product_id = p_product_id;
        SET p_result = 0; -- 成功
    END IF;
    
    COMMIT;
END //
DELIMITER ;

案例3:分布式场景下的库存扣减

@Service
public class DistributedInventoryService {
    
    @Autowired
    private StringRedisTemplate redisTemplate;
    @Autowired
    private RedissonClient redissonClient;
    @Autowired
    private InventoryMapper inventoryMapper;
    
    private static final String STOCK_KEY_PREFIX = "stock:";
    private static final String LOCK_KEY_PREFIX = "lock:stock:";
    
    /**
     * 预扣库存(Redis + 数据库双写)
     */
    public boolean preDeductStock(Long productId, Integer quantity) {
        String stockKey = STOCK_KEY_PREFIX + productId;
        String lockKey = LOCK_KEY_PREFIX + productId;
        
        RLock lock = redissonClient.getLock(lockKey);
        
        try {
            // 1. 获取分布式锁
            boolean locked = lock.tryLock(3, 10, TimeUnit.SECONDS);
            if (!locked) {
                return false;
            }
            
            // 2. 查询Redis库存
            String stockStr = redisTemplate.opsForValue().get(stockKey);
            if (stockStr == null) {
                // 3. 缓存未命中,从数据库加载
                loadStockFromDB(productId);
                stockStr = redisTemplate.opsForValue().get(stockKey);
            }
            
            // 4. 检查库存
            int stock = Integer.parseInt(stockStr);
            if (stock < quantity) {
                return false; // 库存不足
            }
            
            // 5. 扣减Redis库存
            Long newStock = redisTemplate.opsForValue().decrement(stockKey, quantity);
            if (newStock < 0) {
                // 6. 扣减失败,回滚
                redisTemplate.opsForValue().increment(stockKey, quantity);
                return false;
            }
            
            // 7. 发送异步消息,同步到数据库
            stockChangeProducer.sendStockChangeMessage(productId, -quantity);
            
            return true;
            
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return false;
        } finally {
            if (lock.isHeldByCurrentThread()) {
                lock.unlock();
            }
        }
    }
    
    /**
     * 异步同步库存到数据库
     */
    @KafkaListener(topics = "stock-change")
    @Transactional
    public void syncStockToDB(StockChangeMessage message) {
        // 使用乐观锁更新数据库
        int retryCount = 0;
        while (retryCount < 3) {
            Inventory inventory = inventoryMapper.selectByProductId(message.getProductId());
            if (inventory == null) {
                log.error("商品不存在: {}", message.getProductId());
                return;
            }
            
            int affected = inventoryMapper.updateStock(
                message.getProductId(),
                inventory.getStock() + message.getChangeQuantity(),
                inventory.getVersion(),
                inventory.getVersion() + 1
            );
            
            if (affected > 0) {
                return; // 更新成功
            }
            
            retryCount++;
        }
        
        log.error("库存同步失败,需要人工介入: {}", message);
        // 发送告警,进入人工处理流程
    }
}

性能测试数据

1. 不同隔离级别性能对比

隔离级别TPS平均响应时间CPU使用率锁等待时间
READ UNCOMMITTED12,5008ms65%0ms
READ COMMITTED10,80012ms72%2ms
REPEATABLE READ9,20018ms78%8ms
SERIALIZABLE3,50065ms85%45ms

测试环境:MySQL 8.0.32,16核32G,并发连接100

2. 乐观锁 vs 悲观锁性能对比

并发度乐观锁TPS悲观锁TPS乐观锁冲突率悲观锁等待时间
108,5008,2000.5%5ms
507,8006,5003.2%25ms
1006,2004,8008.5%60ms
2004,1002,90018.3%150ms

测试场景:库存扣减操作,初始库存10000

3. 死锁重试策略效果

重试策略成功率平均延迟最大延迟
不重试92.5%25ms50ms
重试1次97.8%28ms80ms
重试3次99.5%35ms200ms
指数退避3次99.7%38ms350ms

经验总结

✅ 最佳实践

  1. 默认使用REPEATABLE READ

    • MySQL默认隔离级别,平衡一致性和性能
    • 配合MVCC实现高效的并发控制
  2. 事务尽量短小

    • 减少锁持有时间,降低死锁概率
    • 避免在事务中调用外部服务
  3. 按相同顺序加锁

    • 所有事务按固定顺序访问资源
    • 从根本上避免循环等待
  4. 捕获死锁异常重试

    • 业务代码必须处理死锁异常
    • 实现指数退避重试机制
  5. 读写分离时注意一致性

    • 读操作根据业务需求选择主库或从库
    • 关键业务查询走主库,避免延迟问题
  6. 合理使用乐观锁

    • 读多写少场景优先使用乐观锁
    • 写多读少场景考虑悲观锁

❌ 常见错误

  1. 事务范围过大

    // 错误:整个方法都在事务中
    @Transactional
    public void processOrder() {
        validate(); // 不需要事务
        callExternalAPI(); // 不应该在事务中
        saveToDB(); // 需要事务
        sendMessage(); // 不应该在事务中
    }
    
  2. 在事务中调用外部服务

    // 错误:事务中调用HTTP接口
    @Transactional
    public void createOrder() {
        orderMapper.insert(order);
        paymentService.charge(order); // HTTP调用,可能导致事务长时间挂起
    }
    
  3. 忽略锁等待超时

    // 错误:不处理锁等待超时
    @Transactional
    public void updateInventory() {
        inventoryMapper.updateStock(); // 可能因锁等待超时失败
    }
    
    // 正确:设置合理的超时时间
    @Transactional(timeout = 5)
    public void updateInventory() {
        // ...
    }
    
  4. 不处理死锁异常

    // 错误:不捕获死锁异常
    @Transactional
    public void transfer() {
        accountMapper.debit(from);
        accountMapper.credit(to);
    }
    
    // 正确:捕获并处理死锁异常
    @Retryable(DeadlockLoserDataAccessException.class)
    @Transactional
    public void transfer() {
        // ...
    }
    
  5. 滥用SERIALIZABLE

    -- 错误:所有操作都用最高隔离级别
    SET SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE;
    
    -- 正确:根据业务场景选择
    -- 普通查询:READ COMMITTED
    -- 订单操作:REPEATABLE READ
    -- 金融转账:SERIALIZABLE
    

决策树:如何选择隔离级别

                    ┌─────────────────────────────────────┐
                    │         业务场景分析                 │
                    └─────────────────┬───────────────────┘
                                      │
        ┌─────────────────────────────┼─────────────────────────────┐
        ↓                             ↓                             ↓
┌───────────────┐            ┌───────────────┐            ┌───────────────┐
│  金融/支付    │            │  电商/库存    │            │  日志/统计    │
│  强一致性     │            │  一般一致性   │            │  最终一致性   │
└───────┬───────┘            └───────┬───────┘            └───────┬───────┘
        │                            │                            │
        ↓                            ↓                            ↓
┌───────────────┐            ┌───────────────┐            ┌───────────────┐
│ SERIALIZABLE  │            │ REPEATABLE READ│            │ READ COMMITTED│
│ + 分布式事务  │            │ + 乐观锁/悲观锁│            │ 或更低        │
└───────────────┘            └───────────────┘            └───────────────┘
        │                            │                            │
        ↓                            ↓                            ↓
┌───────────────┐            ┌───────────────┐            ┌───────────────┐
│ 两阶段提交    │            │ 版本号控制    │            │ 无锁查询      │
│ 外部协调器    │            │ 行锁/间隙锁   │            │ 异步同步      │
└───────────────┘            └───────────────┘            └───────────────┘

检查清单

事务设计检查清单

  • 是否选择了合适的隔离级别?
  • 事务范围是否最小化?
  • 是否在事务中调用了外部服务?
  • 是否处理了锁等待超时?
  • 是否捕获并处理了死锁异常?
  • 是否按固定顺序访问资源?
  • 是否使用了合适的锁策略(乐观/悲观)?
  • 是否设置了合理的事务超时时间?
  • 是否避免了长事务?
  • 是否在事务中进行了大量计算?

并发问题排查清单

  • 是否出现了脏读现象?
  • 是否出现了不可重复读?
  • 是否出现了幻读?
  • 是否出现了死锁?
  • 是否出现了锁等待超时?
  • 是否出现了更新丢失?
  • 是否出现了读倾斜(Read Skew)?
  • 是否出现了写倾斜(Write Skew)?

系列上一篇高级查询技巧:窗口函数与递归查询

系列下一篇分布式事务实战:从本地到分布式的一致性

知识点测试

读完文章了?来测试一下你对知识点的掌握程度吧!

评论区

使用 GitHub 账号登录后即可发表评论,支持 Markdown 格式。

如果评论系统无法加载,请确保:

  • 您的网络可以访问 GitHub
  • giscus GitHub App 已安装到仓库
  • 仓库已启用 Discussions 功能