Mysql

关注公众号 jb51net

关闭
首页 > 数据库 > Mysql > MySQL事务锁故障排查

MySQL Docker容器中XA事务锁故障的终极排查指南

作者:码上有潜

本文记录了一次生产环境中MySQL XA事务锁故障的完整排查过程,从问题发现到最终解决,涵盖了分布式事务原理、Docker数据持久化、MySQL恢复机制等深度技术细节

一、问题现象:诡异的数据库锁死

1.1 故障表现

周二早上,业务团队报告订单系统出现异常:

1.2 初步锁分析

首先查询MySQL的锁状态:

-- 查看当前所有锁信息
SELECT * FROM `performance_schema`.data_locks;

查询结果显示了令人担忧的情况:

ENGINEOBJECT_NAMELOCK_TYPELOCK_MODELOCK_STATUSLOCK_DATA
INNODBcms_orderTABLEIXGRANTEDNULL
INNODBbus_supplierTABLEIXGRANTEDNULL
INNODBbus_supplierRECORDX,REC_NOT_GAPGRANTED367
INNODBcms_orderRECORDX,REC_NOT_GAPGRANTED3440

关键发现:

二、深入排查:发现僵尸XA事务

2.1 检查活动事务

-- 查看所有活动事务
SELECT 
    trx_id,
    trx_state,
    trx_started,
    TIMESTAMPDIFF(SECOND, trx_started, NOW()) as age_seconds
FROM information_schema.innodb_trx 
WHERE trx_state = 'RUNNING';

震惊的发现:

+----------+-----------+---------------------+--------------+
| trx_id   | trx_state | trx_started         | age_seconds  |
+----------+-----------+---------------------+--------------+
| 30330271 | RUNNING   | 2025-11-17 15:56:12 | 64362        |
| 28994418 | RUNNING   | 2025-11-17 15:56:12 | 64362        |
| 27956230 | RUNNING   | 2025-11-17 15:56:12 | 64362        |
+----------+-----------+---------------------+--------------+

这些事务已经运行了超过17小时!明显是僵尸事务。

2.2 XA事务的发现

检查MySQL错误日志发现了关键线索:

docker logs mysql

2025-11-18T01:54:18.024659Z 0 [Warning] [MY-010225] [Server] Found 5 prepared XA transactions

这5个prepared状态的XA事务就是问题的根源!

2.3 什么是XA事务

XA是一种分布式事务协议,采用两阶段提交(2PC):

当应用在prepare阶段后崩溃,就会留下这些"僵尸"XA事务。

三、解决尝试:从温和到激进

3.1 第一轮:正常清理(失败)

尝试正常提交XA事务:

-- 查看XA事务状态
XA RECOVER;

-- 结果
+----------+-------------+-------------+--------------------------------------+
| formatID | gtrid_length | bqual_length | data                                 |
+----------+-------------+-------------+--------------------------------------+
| 1        | 39          | 2           | 01c89f12-...-9a0d2d3176c5:3397      |
| 1        | 40          | 3           | 6a2e296b-...-0ef641639038:680149    |
+----------+-------------+-------------+--------------------------------------+

尝试提交:

XA COMMIT '01c89f12-98fa-49bd-b9a2-9a0d2d3176c5:3397';
-- 错误:XAER_NOTA: Unknown XID

XA COMMIT 1, '01c89f12-98fa-49bd-b9a2-9a0d2d3176c5', '3397';
-- 错误:语法错误

结论: XA事务ID格式异常,无法正常清理。

3.2 第二轮:配置修复(失败)

使用MySQL的强制恢复模式:

# docker-compose.yml
services:
  mysql:
    image: mysql:8.0.27
    command: 
      - --innodb-force-recovery=1
      # ... 其他参数

重启后检查:

SHOW VARIABLES LIKE 'innodb_force_recovery';
-- 输出: innodb_force_recovery = 1

XA RECOVER;
-- 仍然显示2个XA事务!

问题: 恢复模式虽然生效,但无法清除prepared状态的XA事务。

3.3 第三轮:数据重建(成功!)

步骤1:完整备份

# 备份所有数据,跳过锁表
docker exec mysql mysqldump -uroot -p123456 \
  --all-databases \
  --skip-lock-tables \
  --set-gtid-purged=OFF \
  > /tmp/all_dbs_backup.sql

步骤2:彻底清理

# 停止并清理容器
docker-compose down
docker rm -f mysql

# 彻底删除数据目录
sudo rm -rf data/*

# 清理配置文件中的恢复参数
sed -i '/innodb_force_recovery/d' my.cnf

步骤3:重新初始化

# 使用干净的配置启动
docker-compose up -d

# 等待初始化完成(重要!)
docker logs mysql -f

等待看到以下日志输出:

[Note] [Entrypoint]: MySQL init process done. Ready for start up.

步骤4:恢复数据

# 导入备份数据
docker exec -i mysql mysql -uroot -p123456 < /tmp/all_dbs_backup.sql

四、根本原因分析

4.1 问题根源

通过代码审查发现,应用程序在使用C#的分布式事务时:

using System.Transactions;

public class OrderService
{
    private readonly IOrderRepository _orderRepository;
    private readonly IInventoryService _inventoryService;
    
    public OrderService(IOrderRepository orderRepository, IInventoryService inventoryService)
    {
        _orderRepository = orderRepository;
        _inventoryService = inventoryService;
    }
    
    public void CreateOrder(Order order)
    {
        using (var scope = new TransactionScope())
        {
            try
            {
                // 业务逻辑
                _orderRepository.Save(order);
                
                // 调用外部系统(可能超时或异常)
                _inventoryService.UpdateStock(order); // 这里可能抛出异常
                
                // 如果这里异常,事务可能停留在prepared状态
                scope.Complete();
            }
            catch (Exception ex)
            {
                // 事务会自动回滚,但分布式事务可能异常
                throw;
            }
        }
    }
}

或者在使用Entity Framework时:

public class OrderService
{
    private readonly ApplicationDbContext _context;
    
    public async Task CreateOrderAsync(Order order)
    {
        using (var transaction = await _context.Database.BeginTransactionAsync())
        {
            try
            {
                // 保存订单
                _context.Orders.Add(order);
                await _context.SaveChangesAsync();
                
                // 调用外部服务
                await UpdateInventoryAsync(order); // 可能在这里失败
                
                // 提交事务
                await transaction.CommitAsync();
            }
            catch (Exception)
            {
                // 回滚事务
                await transaction.RollbackAsync();
                throw;
            }
        }
    }
}

根本原因:

4.2 XA事务的生命周期问题

正常流程:

开始事务 → 业务操作 → prepare → commit → 完成

异常流程:

开始事务 → 业务操作 → prepare → [应用崩溃/超时] → 事务悬挂

五、预防措施

5.1 应用程序层面

public class TransactionMonitorService : IHostedService
{
    private readonly Timer _timer;
    private readonly ILogger<TransactionMonitorService> _logger;
    private readonly IServiceProvider _serviceProvider;
    
    public TransactionMonitorService(ILogger<TransactionMonitorService> logger, IServiceProvider serviceProvider)
    {
        _logger = logger;
        _serviceProvider = serviceProvider;
        _timer = new Timer(CheckXATransactions, null, Timeout.Infinite, Timeout.Infinite);
    }
    
    public Task StartAsync(CancellationToken cancellationToken)
    {
        _timer.Change(TimeSpan.Zero, TimeSpan.FromMinutes(30)); // 每30分钟检查一次
        return Task.CompletedTask;
    }
    
    private async void CheckXATransactions(object state)
    {
        try
        {
            using var scope = _serviceProvider.CreateScope();
            var dbContext = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
            
            // 检查长时间运行的事务
            var longRunningTransactions = await dbContext.Database.SqlQueryRaw<string>(
                "SELECT trx_id FROM information_schema.innodb_trx WHERE trx_state = 'RUNNING' AND TIMESTAMPDIFF(SECOND, trx_started, NOW()) > 300")
                .ToListAsync();
                
            if (longRunningTransactions.Any())
            {
                _logger.LogWarning("发现长时间运行的事务: {TransactionIds}", string.Join(",", longRunningTransactions));
            }
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "检查XA事务时发生错误");
        }
    }
    
    public Task StopAsync(CancellationToken cancellationToken)
    {
        _timer?.Dispose();
        return Task.CompletedTask;
    }
}

5.2 数据库监控

创建监控脚本 monitor_xa_transactions.sql

-- 检查XA事务
SELECT COUNT(*) as xa_count FROM performance_schema.xa_transactions 
WHERE STATE = 'PREPARED';

-- 检查长事务
SELECT 
    trx_id,
    TIMESTAMPDIFF(SECOND, trx_started, NOW()) as age_seconds
FROM information_schema.innodb_trx 
WHERE trx_state = 'RUNNING' 
AND TIMESTAMPDIFF(SECOND, trx_started, NOW()) > 300; -- 5分钟以上

5.3 C#代码最佳实践

public class ResilientOrderService
{
    private readonly IOrderRepository _orderRepository;
    private readonly IInventoryService _inventoryService;
    private readonly ILogger<ResilientOrderService> _logger;
    
    public async Task<bool> CreateOrderWithRetryAsync(Order order)
    {
        var retryCount = 0;
        const int maxRetries = 3;
        
        while (retryCount < maxRetries)
        {
            try
            {
                await using var transaction = await BeginTransactionWithTimeoutAsync(TimeSpan.FromSeconds(30));
                
                try
                {
                    // 保存订单
                    await _orderRepository.SaveAsync(order);
                    
                    // 更新库存(设置超时)
                    await _inventoryService.UpdateStockAsync(order)
                        .TimeoutAfter(TimeSpan.FromSeconds(10));
                    
                    await transaction.CommitAsync();
                    return true;
                }
                catch (Exception)
                {
                    await transaction.RollbackAsync();
                    throw;
                }
            }
            catch (MySqlException ex) when (ex.Number == 1397) // XAER_NOTA
            {
                retryCount++;
                _logger.LogWarning("XA事务异常,重试 {RetryCount}/{MaxRetries}", retryCount, maxRetries);
                
                if (retryCount >= maxRetries)
                {
                    _logger.LogError(ex, "XA事务重试次数耗尽");
                    throw;
                }
                
                await Task.Delay(TimeSpan.FromSeconds(1 * retryCount));
            }
        }
        
        return false;
    }
    
    private async Task<MySqlTransaction> BeginTransactionWithTimeoutAsync(TimeSpan timeout)
    {
        // 实现带超时的事务开始逻辑
        // ...
    }
}

// 超时扩展方法
public static class TaskExtensions
{
    public static async Task<T> TimeoutAfter<T>(this Task<T> task, TimeSpan timeout)
    {
        using var timeoutCancellationTokenSource = new CancellationTokenSource();
        var completedTask = await Task.WhenAny(task, Task.Delay(timeout, timeoutCancellationTokenSource.Token));
        
        if (completedTask == task)
        {
            timeoutCancellationTokenSource.Cancel();
            return await task;
        }
        else
        {
            throw new TimeoutException("操作超时");
        }
    }
}

5.4 Docker配置优化

version: "3.9"
services:
  mysql:
    image: mysql:8.0.27
    container_name: mysql
    restart: unless-stopped  # 修改重启策略
    ports:
      - "3306:3306"
    volumes:
      - ./data:/var/lib/mysql
      - ./conf.d:/etc/mysql/conf.d  # 统一配置目录
    environment:
      MYSQL_ROOT_PASSWORD: 123456
      TZ: Asia/Shanghai
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
      timeout: 5s
      retries: 3

六、经验总结

6.1 关键教训

6.2 排查流程图

应用异常报错
    ↓
检查performance_schema.data_locks
    ↓
发现长期运行事务
    ↓
检查MySQL错误日志
    ↓
发现XA事务警告
    ↓
尝试XA COMMIT/ROLLBACK
    ↓ → 失败
使用innodb_force_recovery
    ↓ → 失败
完整备份 + 数据重建
    ↓ → 成功
根本原因分析 + 预防措施

6.3 应急脚本

创建 emergency_recovery.sh

#!/bin/bash
# MySQL XA事务紧急恢复脚本

BACKUP_DIR="/tmp/mysql_backup_$(date +%Y%m%d_%H%M%S)"
mkdir -p $BACKUP_DIR

echo "1. 备份数据库..."
docker exec mysql mysqldump -uroot -p$MYSQL_PWD --all-databases > $BACKUP_DIR/full_backup.sql

echo "2. 停止服务..."
docker-compose down

echo "3. 清理数据..."
sudo rm -rf data/*

echo "4. 重新初始化..."
docker-compose up -d

echo "5. 等待启动完成..."
sleep 60

echo "6. 恢复数据..."
docker exec -i mysql mysql -uroot -p$MYSQL_PWD < $BACKUP_DIR/full_backup.sql

echo "恢复完成!备份保存在: $BACKUP_DIR"

结语

这次故障排查历时数小时,但收获颇丰。通过深入理解MySQL XA事务机制、Docker数据管理和分布式事务原理,我们不仅解决了眼前的问题,更重要的是建立了一套完整的预防和应急体系。

对于C#开发者来说,特别注意:

记住:好的系统不是从不出问题,而是出了问题能快速定位和解决。希望这篇详细的排查记录能够帮助遇到类似问题的同行少走弯路。

附录:常用命令速查

用途命令
查看锁SELECT * FROM performance_schema.data_locks;
查看事务SELECT * FROM information_schema.innodb_trx;
查看XA事务XA RECOVER;
强制恢复SET GLOBAL innodb_force_recovery=1;
备份数据mysqldump --all-databases --skip-lock-tables > backup.sql

到此这篇关于MySQL Docker容器中XA事务锁故障的终极排查指南的文章就介绍到这了,更多相关MySQL事务锁故障排查内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文