返回 筑基・数据元府藏真
事务隔离级别与并发问题深度剖析
博主
大约 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 COMMITTED | ✗ | ✓ | ✓ | MVCC,读取已提交版本 | 高 |
| REPEATABLE READ | ✗ | ✗ | ✓ | MVCC + 间隙锁 | 中 |
| 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 UNCOMMITTED | 12,500 | 8ms | 65% | 0ms |
| READ COMMITTED | 10,800 | 12ms | 72% | 2ms |
| REPEATABLE READ | 9,200 | 18ms | 78% | 8ms |
| SERIALIZABLE | 3,500 | 65ms | 85% | 45ms |
测试环境:MySQL 8.0.32,16核32G,并发连接100
2. 乐观锁 vs 悲观锁性能对比
| 并发度 | 乐观锁TPS | 悲观锁TPS | 乐观锁冲突率 | 悲观锁等待时间 |
|---|---|---|---|---|
| 10 | 8,500 | 8,200 | 0.5% | 5ms |
| 50 | 7,800 | 6,500 | 3.2% | 25ms |
| 100 | 6,200 | 4,800 | 8.5% | 60ms |
| 200 | 4,100 | 2,900 | 18.3% | 150ms |
测试场景:库存扣减操作,初始库存10000
3. 死锁重试策略效果
| 重试策略 | 成功率 | 平均延迟 | 最大延迟 |
|---|---|---|---|
| 不重试 | 92.5% | 25ms | 50ms |
| 重试1次 | 97.8% | 28ms | 80ms |
| 重试3次 | 99.5% | 35ms | 200ms |
| 指数退避3次 | 99.7% | 38ms | 350ms |
经验总结
✅ 最佳实践
-
默认使用REPEATABLE READ
- MySQL默认隔离级别,平衡一致性和性能
- 配合MVCC实现高效的并发控制
-
事务尽量短小
- 减少锁持有时间,降低死锁概率
- 避免在事务中调用外部服务
-
按相同顺序加锁
- 所有事务按固定顺序访问资源
- 从根本上避免循环等待
-
捕获死锁异常重试
- 业务代码必须处理死锁异常
- 实现指数退避重试机制
-
读写分离时注意一致性
- 读操作根据业务需求选择主库或从库
- 关键业务查询走主库,避免延迟问题
-
合理使用乐观锁
- 读多写少场景优先使用乐观锁
- 写多读少场景考虑悲观锁
❌ 常见错误
-
事务范围过大
// 错误:整个方法都在事务中 @Transactional public void processOrder() { validate(); // 不需要事务 callExternalAPI(); // 不应该在事务中 saveToDB(); // 需要事务 sendMessage(); // 不应该在事务中 } -
在事务中调用外部服务
// 错误:事务中调用HTTP接口 @Transactional public void createOrder() { orderMapper.insert(order); paymentService.charge(order); // HTTP调用,可能导致事务长时间挂起 } -
忽略锁等待超时
// 错误:不处理锁等待超时 @Transactional public void updateInventory() { inventoryMapper.updateStock(); // 可能因锁等待超时失败 } // 正确:设置合理的超时时间 @Transactional(timeout = 5) public void updateInventory() { // ... } -
不处理死锁异常
// 错误:不捕获死锁异常 @Transactional public void transfer() { accountMapper.debit(from); accountMapper.credit(to); } // 正确:捕获并处理死锁异常 @Retryable(DeadlockLoserDataAccessException.class) @Transactional public void transfer() { // ... } -
滥用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 功能