引言:理解DB2死锁的本质

在DB2数据库管理系统中,死锁(Deadlock)是一种常见的并发控制问题,它发生在两个或多个事务相互等待对方释放资源时,形成一个无法解开的等待循环。死锁会导致相关事务被系统回滚,影响应用程序的正常运行,严重时甚至会导致整个系统的性能下降。理解死锁的成因并掌握高效的解决方法,对于数据库管理员和开发人员来说至关重要。

死锁就像两个人在一条狭窄的单行道上相遇,每个人都希望对方先退让,结果两人都无法前进。在数据库环境中,这种”僵局”会消耗系统资源,降低吞吐量,并可能导致数据不一致。DB2数据库提供了死锁检测机制,但预防胜于治疗,通过分析死锁原因并采取相应措施,可以显著减少死锁的发生频率。

死锁的四个必要条件

要形成死锁,必须同时满足以下四个条件:

  1. 互斥条件(Mutual Exclusion):资源不能被共享,只能独占使用。例如,一个事务对某行数据加了排他锁,其他事务无法同时访问该行。

  2. 请求与保持条件(Hold and Wait):事务已经持有了至少一个资源,同时又在请求其他被占用的资源。

  3. 不剥夺条件(No Preemption):事务已经获得的资源不能被强制剥夺,只能由持有者主动释放。

  4. 循环等待条件(Circular Wait):存在一个事务等待链,形成闭环。例如:事务A等待事务B释放资源,而事务B又在等待事务A释放资源。

DB2的锁管理器会监控这些条件,并在检测到死锁时选择一个或多个事务作为”牺牲品”进行回滚,以打破死锁循环。

DB2死锁的常见原因分析

1. 不合理的事务设计

问题描述:事务过长、包含过多操作或处理大量数据,会增加锁竞争的概率和持续时间。

具体表现:

  • 事务中包含复杂的业务逻辑计算
  • 在事务中执行用户交互或外部API调用
  • 事务处理成千上万条记录

示例场景:

-- 问题代码:事务过长,锁持有时间过久
BEGIN
    -- 1. 查询用户信息
    SELECT * FROM users WHERE user_id = 1001;
    
    -- 2. 复杂的业务逻辑计算(耗时操作)
    CALL calculate_user_credit_score(1001); -- 假设这个存储过程执行10秒
    
    -- 3. 更新用户状态
    UPDATE users SET status = 'active' WHERE user_id = 1001;
    
    -- 4. 插入日志记录
    INSERT INTO user_logs (user_id, action, timestamp) VALUES (1001, 'update', CURRENT_TIMESTAMP);
    
    -- 5. 提交事务
    COMMIT;
END

在这个例子中,事务从开始到提交可能持续10秒以上,期间持有的锁(特别是更新锁)会阻塞其他并发事务,大大增加死锁概率。

解决方案:

  • 将事务拆分为多个小事务
  • 将非数据库操作移到事务外部
  • 优化业务逻辑,减少事务持续时间
-- 改进后的代码:拆分事务,减少锁持有时间
-- 步骤1:在事务外执行复杂计算
CALL calculate_user_credit_score(1001);

-- 步骤2:执行简短的数据库事务
BEGIN
    UPDATE users SET status = 'active' WHERE user_id = 1001;
    INSERT INTO user_logs (user_id, action, timestamp) VALUES (1001, 'update', CURRENT_TIMESTAMP);
    COMMIT;
END

2. 不一致的锁获取顺序

问题描述:当多个事务以不同的顺序访问和锁定相同的资源集时,极易形成循环等待。

具体表现:

  • 事务A先锁表1再锁表2,事务B先锁表2再锁表1
  • 在循环中以随机顺序访问资源
  • 不同模块的代码以不同顺序访问相同表

示例场景:

-- 事务A的代码
BEGIN
    UPDATE accounts SET balance = balance - 100 WHERE account_id = 1;
    UPDATE orders SET status = 'paid' WHERE order_id = 100;
    COMMIT;
END

-- 事务B的代码(在另一个会话中同时执行)
BEGIN
    UPDATE orders SET status = 'processing' WHERE order_id = 100;
    UPDATE accounts SET balance = balance + 50 WHERE account_id = 1;
    COMMIT;
END

如果事务A先锁accounts表的第1行,然后尝试锁orders表的第100行;而事务B先锁orders表的第100行,然后尝试锁accounts表的第11行,就形成了循环等待。

解决方案:

  • 制定并遵守统一的锁获取顺序规则
  • 在所有代码中以相同顺序访问资源
  • 使用数据库设计规范强制顺序
-- 改进:两个事务都遵循相同的锁顺序(先accounts后orders)
-- 事务A
BEGIN
    UPDATE accounts SET balance = balance - 100 WHERE account_id = 1;
    UPDATE orders SET status = 'paid' WHERE order_id = 100;
    COMMIT;
END

-- 事务B(修改为相同顺序)
BEGIN
    UPDATE accounts SET balance = balance + 50 WHERE account_id = 1;
    UPDATE orders SET status = 'processing' WHERE order_id = 100;
    COMMIT;
END

3. 索引缺失或不当

问题描述:缺少合适的索引会导致全表扫描,增加锁的范围和持续时间。

具体表现:

  • WHERE子句中的条件列没有索引
  • 使用函数或表达式导致索引失效
  • 复合索引顺序不当

示例场景:

-- 问题:没有索引的查询
CREATE TABLE orders (
    order_id INT PRIMARY KEY,
    customer_id INT,
    order_date DATE,
    status VARCHAR(20),
    amount DECIMAL(10,2)
);

-- 没有在customer_id上创建索引
-- 以下查询会锁定整个表或大量行
BEGIN
    UPDATE orders SET status = 'shipped' WHERE customer_id = 12345;
    -- 如果customer_id=12345的订单很多,会锁定大量行
    -- 其他事务更新不同customer_id的订单也会被阻塞
    COMMIT;
END

解决方案:

  • 为常用查询条件创建合适的索引
  • 定期分析查询执行计划
  • 避免在WHERE子句中使用函数
-- 改进:创建合适的索引
CREATE INDEX idx_orders_customer_id ON orders(customer_id);

-- 验证索引是否被使用
-- 在DB2中可以使用:
EXPLAIN PLAN FOR UPDATE orders SET status = 'shipped' WHERE customer_id = 12345;

-- 查看执行计划,确保使用了索引扫描而非全表扫描

4. 锁升级(Lock Escalation)

问题描述:当事务持有的行锁数量超过DB2配置的阈值时,DB2会自动将行锁升级为表锁,导致并发性急剧下降。

具体表现:

  • 大批量UPDATE/DELETE操作
  • 缺少WHERE条件的全表更新
  • 锁升级阈值设置过低

示例场景:

-- 问题:大批量操作导致锁升级
BEGIN
    -- 假设orders表有100万行数据
    -- 此查询会锁定所有满足条件的行,可能触发锁升级
    UPDATE orders 
    SET status = 'expired' 
    WHERE order_date < '2023-01-01' 
      AND status = 'pending';
    
    COMMIT;
END

解决方案:

  • 分批处理大数据量操作
  • 调整锁升级阈值
  • 使用适当的隔离级别
-- 改进:分批处理避免锁升级
BEGIN
    DECLARE v_rows_updated INT DEFAULT 0;
    DECLARE v_batch_size INT DEFAULT 1000; -- 每批处理1000行
    
    REPEAT
        UPDATE orders 
        SET status = 'expired' 
        WHERE order_date < '2023-01-01' 
          AND status = 'pending'
          AND ROWNUMBER() <= v_batch_size;
        
        GET DIAGNOSTICS v_rows_updated = ROW_COUNT;
        
        -- 提交当前批次,释放锁
        COMMIT;
        
        -- 短暂延迟,让其他事务有机会执行
        -- DB2中可以使用:CALL SYSPROC.ADMIN_CMD('SLEEP 1');
        
    UNTIL v_rows_updated = 0 END REPEAT;
END

5. 隔离级别选择不当

问题描述:隔离级别决定了事务之间的可见性和锁行为,选择不当会增加锁竞争。

具体表现:

  • 使用可重复读(RR)隔离级别处理大量数据
  • 读操作使用排他锁
  • 不同事务使用不同隔离级别导致复杂交互

示例场景:

-- 问题:使用可重复读(RR)隔离级别
-- 在DB2中,默认隔离级别是游标稳定性(CS)

-- 设置为可重复读(RR)- 会锁定整个结果集
SET CURRENT ISOLATION = RR;

BEGIN
    -- 此查询会锁定所有读取的行,直到事务结束
    SELECT * FROM orders WHERE customer_id = 12345;
    
    -- 长时间运行的业务逻辑...
    
    COMMIT; -- 锁才释放
END

解决方案:

  • 根据业务需求选择合适的隔离级别
  • 了解不同隔离级别的锁行为
  • 在保证数据一致性的前提下使用最低隔离级别
-- 改进:使用游标稳定性(CS)或读稳定性(RS)
SET CURRENT ISOLATION = CS; -- 或 RS

BEGIN
    -- CS级别只锁定当前游标所在行,读完立即释放
    -- RS级别锁定所有读取的行,但比RR级别锁范围小
    SELECT * FROM orders WHERE customer_id = 12345;
    
    COMMIT;
END

-- 对于只读查询,可以使用未提交读(UR)
SET CURRENT ISOLATION = UR;
SELECT * FROM orders WHERE customer_id = 12345;

6. 应用程序连接管理不当

问题描述:长连接、连接池配置不当或事务未及时提交/回滚。

具体表现:

  • 应用程序持有连接但不执行操作
  • 事务未正确关闭
  • 连接池泄漏

示例示例:

// 问题代码:Java中事务未正确关闭
public void updateOrderStatus(int orderId, String status) {
    Connection conn = null;
    PreparedStatement stmt = null;
    try {
        conn = dataSource.getConnection();
        conn.setAutoCommit(false); // 开始事务
        
        stmt = conn.prepareStatement("UPDATE orders SET status = ? WHERE order_id = ?");
        stmt.setString(1, status);
        stmt.setInt(2, orderId);
        stmt.executeUpdate();
        
        // 问题:这里没有提交或回滚!
        // 事务会一直持有锁直到连接关闭或超时
        
    } catch (SQLException e) {
        // 问题:异常时没有回滚
        e.printStackTrace();
    } finally {
        // 关闭资源
        if (stmt != null) stmt.close();
        if (conn != null) conn.close(); // 此时才释放锁
    }
}

解决方案:

  • 确保每个事务都有明确的提交或回滚
  • 使用try-with-resources或finally块确保资源释放
  • 合理配置连接池参数
// 改进后的代码:正确管理事务和连接
public void updateOrderStatus(int orderId, String status) {
    // 使用try-with-resources自动关闭资源
    try (Connection conn = dataSource.getConnection();
         PreparedStatement stmt = conn.prepareStatement(
             "UPDATE orders SET status = ? WHERE order_id = ?")) {
        
        conn.setAutoCommit(false); // 开始事务
        
        stmt.setString(1, status);
        stmt.setInt(2, orderId);
        stmt.executeUpdate();
        
        // 明确提交事务,释放锁
        conn.commit();
        
    } catch (SQLException e) {
        // 异常时回滚事务
        try {
            if (conn != null) conn.rollback();
        } catch (SQLException ex) {
            ex.printStackTrace();
        }
        throw new RuntimeException("更新订单状态失败", e);
    }
}

高效解决DB2死锁的方法

1. 死锁检测与分析工具

DB2内置监控工具

DB2提供了多种内置工具来监控和分析死锁:

-- 1. 查看当前锁信息
SELECT 
    tabname,
    lock_mode,
    lock_object_type,
    status,
    member
FROM SYSIBMADM.LOCKS
WHERE lock_mode = 'X' OR lock_mode = 'U';

-- 2. 查看锁等待链
SELECT 
    requester_tabname,
    requester_lock_mode,
    holder_tabname,
    holder_lock_mode,
    wait_start
FROM SYSIBMADM.LOCKWAITS;

-- 3. 查看死锁历史(需要配置事件监视器)
-- 首先创建死锁事件监视器
CREATE EVENT MONITOR deadlock_mon FOR DEADLOCKS WRITE TO TABLE;

-- 激活监视器
SET EVENT MONITOR deadlock_mon STATE 1;

-- 查询死锁事件
SELECT 
    event_timestamp,
    deadlocked_tabname,
    deadlocked_lock_mode,
    member
FROM deadlock_mon.deadlocks;

使用db2pd命令

# 查看数据库的锁信息
db2pd -db your_database -locks showlocks

# 查看事务和锁等待
db2pd -db your_database -transactions -locks

# 查看死锁事件历史
dbdb2pd -db your_database -deadlocks

配置数据库管理器参数以捕获死锁信息

-- 启用死锁事件监视器
UPDATE DBM CFG USING DFT_EVENT_MONITORS ON;

-- 设置死锁检测时间间隔(默认为10秒)
UPDATE DB CFG USING DLCHKTIME 5000; -- 5秒

-- 设置锁等待超时(防止无限期等待)
UPDATE DB CFG USING LOCKTIMEOUT 30; -- 30秒

2. 诊断死锁的步骤

步骤1:识别死锁模式

-- 查询最近的死锁事件
SELECT 
    event_timestamp,
    member,
    deadlocked_tabname,
    deadlocked_lock_mode,
    deadlocked_tabname,
    deadlocked_tabname
FROM SYSIBMADM.LOCKS
WHERE lock_mode = 'X' AND status = 'WAIT';

-- 或者使用事件监视器数据
SELECT 
    e.event_timestamp,
    e.member,
    e.deadlocked_tabname,
    e.deadlocked_lock_mode,
    e.application_handle,
    a.app_name,
    a.client_hostname,
    a.client_username
FROM deadlock_mon.deadlocks e
LEFT JOIN SYSIBMADM.APPLICATIONS a ON e.application_handle = a.application_handle
ORDER BY e.event_timestamp DESC
FETCH FIRST 10 ROWS ONLY;

步骤2:分析锁等待链

-- 查看当前锁等待情况
SELECT 
    w.waiter_application_handle,
    w.holder_application_handle,
    w.waiter_tabname,
    w.holder_tabname,
    w.waiter_lock_mode,
    w.holder_lock_mode,
    w.wait_start,
    TIMESTAMPDIFF(2, CURRENT_TIMESTAMP - w.wait_start) AS wait_seconds
FROM SYSIBMADM.LOCKWAITS w
ORDER BY wait_seconds DESC;

步骤3:获取问题SQL语句

-- 查看导致死锁的SQL
SELECT 
    a.application_handle,
    a.app_name,
    s.stmt_text,
    s.stmt_pkglist_id,
    s.num_executions,
    s.total_exec_time
FROM SYSIBMADM.APPLICATIONS a
JOIN SYSIBMADM.SNAP_APPL_INFO s ON a.application_handle = s.application_handle
WHERE a.application_handle IN (
    SELECT DISTINCT holder_application_handle FROM SYSIBMADM.LOCKWAITS
)
ORDER BY s.total_exec_time DESC;

3. 优化策略与最佳实践

策略1:事务设计优化

原则:保持事务简短,尽快提交或回滚。

-- 最佳实践:最小化事务范围
-- 错误示例:在事务中包含用户交互
BEGIN
    UPDATE inventory SET quantity = quantity - 1 WHERE product_id = 100;
    -- 等待用户输入(绝对禁止!)
    -- 用户输入期间锁一直持有
    COMMIT;
END

-- 正确做法:在事务外处理用户输入
-- 1. 收集用户输入
-- 2. 开始事务
BEGIN
    UPDATE inventory SET quantity = quantity - 1 WHERE product_id = 100;
    COMMIT;
END

策略2:批量操作优化

-- 大批量更新时使用分批提交
CREATE PROCEDURE batch_update_orders(
    IN batch_size INT DEFAULT 1000
)
BEGIN
    DECLARE v_done INT DEFAULT 0;
    DECLARE v_rows INT DEFAULT 0;
    DECLARE v_total_rows INT DEFAULT 0;
    
    -- 创建临时表存储需要更新的订单ID
    DECLARE GLOBAL TEMPORARY TABLE temp_orders_to_update (
        order_id INT
    ) ON COMMIT PRESERVE ROWS;
    
    -- 插入需要更新的订单ID
    INSERT INTO session.temp_orders_to_update
    SELECT order_id FROM orders 
    WHERE status = 'pending' AND order_date < CURRENT_DATE - 30 DAYS;
    
    -- 分批更新
    WHILE NOT v_done DO
        UPDATE orders 
        SET status = 'expired' 
        WHERE order_id IN (
            SELECT order_id FROM session.temp_orders_to_update 
            LIMIT batch_size
        );
        
        GET DIAGNOSTICS v_rows = ROW_COUNT;
        
        IF v_rows = 0 THEN
            SET v_done = 1;
        ELSE
            SET v_total_rows = v_total_rows + v_rows;
            COMMIT; -- 提交当前批次
            
            -- 删除已处理的记录
            DELETE FROM session.temp_orders_to_update 
            WHERE order_id IN (
                SELECT order_id FROM orders 
                WHERE status = 'expired' 
                LIMIT batch_size
            );
        END IF;
    END WHILE;
    
    -- 清理临时表
    DROP TABLE session.temp_orders_to_update;
END

策略3:索引优化

-- 1. 分析现有索引使用情况
SELECT 
    tabname,
    colnames,
    uniqrule,
    nleaf,
    nlevels,
    firstkeycard,
    fullkeycard
FROM SYSIBM.SYSINDEXES
WHERE tabname = 'ORDERS'
ORDER BY nleaf DESC;

-- 2. 创建覆盖索引(Covering Index)
-- 如果查询经常使用customer_id和status作为条件
CREATE INDEX idx_orders_customer_status ON orders(customer_id, status);

-- 3. 创建复合索引优化排序
CREATE INDEX idx_orders_date_customer ON orders(order_date DESC, customer_id);

-- 4. 查看索引是否被有效使用
-- 使用DB2 Design Advisor
db2advis -d your_database -i input.sql

-- input.sql 内容:
-- SELECT * FROM orders WHERE customer_id = ? AND status = ?;

策略4:隔离级别优化

-- 根据业务场景选择合适的隔离级别
-- 场景1:只读查询 - 使用UR(未提交读)
SET CURRENT ISOLATION = UR;
SELECT COUNT(*) FROM orders WHERE status = 'pending';

-- 场景2:需要读稳定性 - 使用RS(读稳定性)
SET CURRENT ISOLATION = RS;
SELECT * FROM orders WHERE customer_id = 12345 FOR UPDATE;

-- 场景3:需要可重复读 - 仅在必要时使用RR
SET CURRENT ISOLATION = RR;
-- 用于需要严格一致性的报表查询
SELECT SUM(amount) FROM orders WHERE order_date BETWEEN ? AND ?;

-- 场景4:默认使用CS(游标稳定性)- 平衡并发性和一致性
SET CURRENT ISOLATION = CS;
-- 适用于大多数OLTP场景
UPDATE orders SET status = 'processed' WHERE order_id = ?;

策略5:应用程序连接管理

// 使用连接池的最佳实践
public class DatabaseManager {
    private static DataSource dataSource;
    
    static {
        // 配置连接池参数
        HikariConfig config = new HikariConfig();
        config.setJdbcUrl("jdbc:db2://localhost:50000/YOURDB");
        config.setUsername("username");
        config.setPassword("password");
        
        // 关键参数:控制连接生命周期
        config.setMaximumPoolSize(20); // 最大连接数
        config.setMinimumIdle(5); // 最小空闲连接
        config.setIdleTimeout(300000); // 空闲超时5分钟
        config.setMaxLifetime(1800000); // 连接最大生命周期30分钟
        config.setConnectionTimeout(30000); // 获取连接超时30秒
        
        // 事务相关参数
        config.setAutoCommit(false); // 默认不自动提交
        config.setLeakDetectionThreshold(60000); // 检测连接泄漏60秒
        
        dataSource = new HikariDataSource(config);
    }
    
    // 使用try-with-resources确保自动关闭
    public void executeTransaction(Consumer<Connection> operation) {
        try (Connection conn = dataSource.getConnection()) {
            conn.setAutoCommit(false);
            try {
                operation.accept(conn);
                conn.commit();
            } catch (SQLException e) {
                conn.rollback();
                throw e;
            }
        } catch (SQLException e) {
            throw new RuntimeException("Database error", e);
        }
    }
}

4. 死锁预防策略

应用程序层面的预防

-- 1. 使用SELECT FOR UPDATE锁定记录
BEGIN
    -- 先锁定记录
    SELECT * FROM accounts WHERE account_id = 123 FOR UPDATE;
    
    -- 执行业务逻辑
    -- ...
    
    -- 更新记录
    UPDATE accounts SET balance = balance - 100 WHERE account_id = 123;
    
    COMMIT;
END

-- 2. 使用原子操作减少锁持有时间
-- 避免:先查询再更新
BEGIN
    SELECT balance INTO v_balance FROM accounts WHERE account_id = 123;
    -- 此处有时间差,可能被其他事务修改
    UPDATE accounts SET balance = v_balance - 100 WHERE account_id = 123;
    COMMIT;
END

-- 改进:直接更新
BEGIN
    UPDATE accounts SET balance = balance - 100 WHERE account_id = 123;
    COMMIT;
END

-- 3. 使用乐观锁(版本号机制)
ALTER TABLE orders ADD COLUMN version INT DEFAULT 0;

BEGIN
    DECLARE v_version INT;
    DECLARE v_rows INT;
    
    -- 读取当前版本
    SELECT version INTO v_version FROM orders WHERE order_id = 100;
    
    -- 尝试更新,检查版本是否变化
    UPDATE orders 
    SET status = 'processed', version = version + 1 
    WHERE order_id = 100 AND version = v_version;
    
    GET DIAGNOSTICS v_rows = ROW_COUNT;
    
    IF v_rows = 0 THEN
        -- 版本已变化,说明数据被其他事务修改
        ROLLBACK;
        -- 重试逻辑或抛出异常
    ELSE
        COMMIT;
    END IF;
END

数据库配置优化

-- 1. 调整锁升级阈值
-- 查看当前设置
GET DB CFG FOR your_database | grep LOCKSIZE

-- 如果频繁发生锁升级,可以适当提高阈值
-- 注意:这会增加内存使用
UPDATE DB CFG USING LOCKSIZE AUTO; -- 自动管理
-- 或者手动设置
UPDATE DB CFG USING LOCKSIZE 4096; -- 4096行

-- 2. 调整死锁检测间隔
-- 默认10秒,可以适当缩短以更快检测死锁
UPDATE DB CFG USING DLCHKTIME 3000; -- 3秒

-- 3. 设置锁等待超时,防止无限等待
UPDATE DB CFG USING LOCKTIMEOUT 30; -- 30秒

-- 4. 启用锁链表(Lock Chain)记录
UPDATE DBM CFG USING HEALTH_MON ON;
UPDATE DBM CFG USING SYSADM_GROUP sysadm;

-- 5. 配置事件监视器自动捕获死锁
CREATE EVENT MONITOR deadlock_event FOR DEADLOCKS 
WRITE TO TABLE 
TABLE deadlocks IN your_tablespace,
TABLE deadlock_details IN your_tablespace;

-- 激活监视器
SET EVENT MONITOR deadlock_event STATE 1;

-- 将其设置为自动启动
UPDATE DB CFG USING DFT_EVENT_MONITORS ON;

5. 监控与告警

创建持续监控脚本

#!/bin/bash
# monitor_deadlocks.sh

DB_NAME="YOURDB"
DB_USER="db2inst1"
LOG_FILE="/var/log/db2/deadlock_monitor.log"

# 检查最近5分钟的死锁数量
DEADLOCK_COUNT=$(db2 -x "SELECT COUNT(*) FROM SYSIBMADM.LOCKS WHERE lock_mode = 'X' AND status = 'WAIT' AND timestamp > CURRENT_TIMESTAMP - 5 MINUTES")

if [ "$DEADLOCK_COUNT" -gt 5 ]; then
    echo "$(date): 警告!检测到 $DEADLOCK_COUNT 个死锁相关事件" >> $LOG_FILE
    
    # 发送告警邮件
    echo "DB2死锁告警:$DB_NAME 在 $(date) 检测到 $DEADLOCK_COUNT 个死锁" | \
    mail -s "DB2死锁告警" dba@company.com
    
    # 收集详细信息
    db2 -x "SELECT * FROM SYSIBMADM.LOCKWAITS" >> $LOG_FILE
fi

使用DB2 Health Monitor

-- 启用健康监控
UPDATE DBM CFG USING HEALTH_MON ON;

-- 配置死锁告警阈值
-- 在数据库管理器配置中设置
UPDATE DBM CFG USING NOTIFYPEVENT DEADLOCK;

-- 创建健康监控配置文件
CREATE HEALTH MONITOR CONFIGURATION deadlock_threshold AS
    DEADLOCK_COUNT > 10 PER MINUTE;

实际案例分析

案例1:电商订单处理系统

问题描述:在订单提交高峰期,系统频繁出现死锁,导致订单处理失败。

死锁分析:

-- 通过事件监视器捕获的死锁信息
-- 事务A:扣减库存 + 创建订单
BEGIN
    UPDATE inventory SET quantity = quantity - 1 WHERE product_id = 100;
    INSERT INTO orders (product_id, quantity, status) VALUES (100, 1, 'pending');
    COMMIT;
END

-- 事务B:查询库存 + 扣减库存
BEGIN
    SELECT quantity FROM inventory WHERE product_id = 100;
    UPDATE inventory SET quantity = quantity - 1 WHERE product_id = 100;
    COMMIT;
END

解决方案:

-- 1. 为inventory表添加索引
CREATE INDEX idx_inventory_product ON inventory(product_id);

-- 2. 统一锁顺序:所有事务都先更新inventory,再操作orders
-- 事务B修改为:
BEGIN
    UPDATE inventory SET quantity = quantity - 1 WHERE product_id = 100;
    SELECT quantity FROM inventory WHERE product_id = 100; -- 在更新后查询
    COMMIT;
END

-- 3. 使用SELECT FOR UPDATE确保锁顺序
BEGIN
    SELECT * FROM inventory WHERE product_id = 100 FOR UPDATE;
    UPDATE inventory SET quantity = quantity - 1 WHERE product_id = 100;
    INSERT INTO orders (product_id, quantity, status) VALUES (100, 1, 'pending');
    COMMIT;
END

案例2:银行转账系统

问题描述:两个账户之间的转账操作导致死锁。

死锁分析:

-- 事务A:账户1向账户2转账
BEGIN
    UPDATE accounts SET balance = balance - 100 WHERE account_id = 1;
    UPDATE accounts SET balance = balance + 100 WHERE account_id = 2;
    COMMIT;
END

-- 事务B:账户2向账户1转账(同时执行)
BEGIN
    UPDATE accounts SET balance = balance - 50 WHERE account_id = 2;
    UPDATE accounts SET balance = balance + 50 WHERE account_id = 1;
    COMMIT;
END

解决方案:

-- 1. 统一锁顺序:按account_id排序
-- 事务A和B都按account_id升序处理
BEGIN
    -- 事务A:先处理1,再处理2
    UPDATE accounts SET balance = balance - 100 WHERE account_id = 1;
    UPDATE accounts SET balance = balance + 100 WHERE account_id = 2;
    COMMIT;
END

BEGIN
    -- 事务B:先处理1,再处理2(与事务A相同顺序)
    UPDATE accounts SET balance = balance + 50 WHERE account_id = 1;
    UPDATE accounts SET balance = balance - 50 WHERE account_id = 2;
    COMMIT;
END

-- 2. 使用存储过程封装转账逻辑,确保一致性
CREATE PROCEDURE transfer_funds(
    IN from_account INT,
    IN to_account INT,
    IN amount DECIMAL(10,2)
)
BEGIN
    DECLARE from_balance DECIMAL(10,2);
    DECLARE to_balance DECIMAL(10,2);
    
    -- 统一锁定顺序:按account_id排序
    IF from_account < to_account THEN
        -- 先锁from_account,再锁to_account
        SELECT balance INTO from_balance FROM accounts WHERE account_id = from_account FOR UPDATE;
        SELECT balance INTO to_balance FROM accounts WHERE account_id = to_account FOR UPDATE;
        
        IF from_balance >= amount THEN
            UPDATE accounts SET balance = balance - amount WHERE account_id = from_account;
            UPDATE accounts SET balance = balance + amount WHERE account_id = to_account;
        ELSE
            SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Insufficient funds';
        END IF;
    ELSE
        -- 先锁to_account,再锁from_account
        SELECT balance INTO to_balance FROM accounts WHERE account_id = to_account FOR UPDATE;
        SELECT balance INTO from_balance FROM accounts WHERE account_id = from_account FOR UPDATE;
        
        IF from_balance >= amount THEN
            UPDATE accounts SET balance = balance + amount WHERE account_id = to_account;
            UPDATE accounts SET balance = balance - amount WHERE account_id = from_account;
        ELSE
            SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Insufficient funds';
        END IF;
    END IF;
    
    COMMIT;
END

总结

DB2死锁问题虽然无法完全避免,但通过合理的分析和优化可以显著减少其发生频率和影响。关键要点包括:

  1. 理解死锁成因:掌握四个必要条件,识别常见原因
  2. 优化事务设计:保持事务简短,统一锁顺序
  3. 合理使用索引:减少锁的范围和持续时间
  4. 选择合适的隔离级别:平衡一致性和并发性
  5. 分批处理大数据量操作:避免锁升级
  6. 加强监控和告警:及时发现和解决问题
  7. 应用程序最佳实践:正确管理连接和事务

通过系统性地应用这些策略,可以构建更健壮、更高并发的DB2应用程序,将死锁影响降到最低。记住,预防胜于治疗,在设计阶段就考虑并发控制问题,比事后解决要有效得多。# DB2死锁的常见原因分析与高效解决方法详解

引言:理解DB2死锁的本质

在DB2数据库管理系统中,死锁(Deadlock)是一种常见的并发控制问题,它发生在两个或多个事务相互等待对方释放资源时,形成一个无法解开的等待循环。死锁会导致相关事务被系统回滚,影响应用程序的正常运行,严重时甚至会导致整个系统的性能下降。理解死锁的成因并掌握高效的解决方法,对于数据库管理员和开发人员来说至关重要。

死锁就像两个人在一条狭窄的单行道上相遇,每个人都希望对方先退让,结果两人都无法前进。在数据库环境中,这种”僵局”会消耗系统资源,降低吞吐量,并可能导致数据不一致。DB2数据库提供了死锁检测机制,但预防胜于治疗,通过分析死锁原因并采取相应措施,可以显著减少死锁的发生频率。

死锁的四个必要条件

要形成死锁,必须同时满足以下四个条件:

  1. 互斥条件(Mutual Exclusion):资源不能被共享,只能独占使用。例如,一个事务对某行数据加了排他锁,其他事务无法同时访问该行。

  2. 请求与保持条件(Hold and Wait):事务已经持有了至少一个资源,同时又在请求其他被占用的资源。

  3. 不剥夺条件(No Preemption):事务已经获得的资源不能被强制剥夺,只能由持有者主动释放。

  4. 循环等待条件(Circular Wait):存在一个事务等待链,形成闭环。例如:事务A等待事务B释放资源,而事务B又在等待事务A释放资源。

DB2的锁管理器会监控这些条件,并在检测到死锁时选择一个或多个事务作为”牺牲品”进行回滚,以打破死锁循环。

DB2死锁的常见原因分析

1. 不合理的事务设计

问题描述:事务过长、包含过多操作或处理大量数据,会增加锁竞争的概率和持续时间。

具体表现:

  • 事务中包含复杂的业务逻辑计算
  • 在事务中执行用户交互或外部API调用
  • 事务处理成千上万条记录

示例场景:

-- 问题代码:事务过长,锁持有时间过久
BEGIN
    -- 1. 查询用户信息
    SELECT * FROM users WHERE user_id = 1001;
    
    -- 2. 复杂的业务逻辑计算(耗时操作)
    CALL calculate_user_credit_score(1001); -- 假设这个存储过程执行10秒
    
    -- 3. 更新用户状态
    UPDATE users SET status = 'active' WHERE user_id = 1001;
    
    -- 4. 插入日志记录
    INSERT INTO user_logs (user_id, action, timestamp) VALUES (1001, 'update', CURRENT_TIMESTAMP);
    
    -- 5. 提交事务
    COMMIT;
END

在这个例子中,事务从开始到提交可能持续10秒以上,期间持有的锁(特别是更新锁)会阻塞其他并发事务,大大增加死锁概率。

解决方案:

  • 将事务拆分为多个小事务
  • 将非数据库操作移到事务外部
  • 优化业务逻辑,减少事务持续时间
-- 改进后的代码:拆分事务,减少锁持有时间
-- 步骤1:在事务外执行复杂计算
CALL calculate_user_credit_score(1001);

-- 步骤2:执行简短的数据库事务
BEGIN
    UPDATE users SET status = 'active' WHERE user_id = 1001;
    INSERT INTO user_logs (user_id, action, timestamp) VALUES (1001, 'update', CURRENT_TIMESTAMP);
    COMMIT;
END

2. 不一致的锁获取顺序

问题描述:当多个事务以不同的顺序访问和锁定相同的资源集时,极易形成循环等待。

具体表现:

  • 事务A先锁表1再锁表2,事务B先锁表2再锁表1
  • 在循环中以随机顺序访问资源
  • 不同模块的代码以不同顺序访问相同表

示例场景:

-- 事务A的代码
BEGIN
    UPDATE accounts SET balance = balance - 100 WHERE account_id = 1;
    UPDATE orders SET status = 'paid' WHERE order_id = 100;
    COMMIT;
END

-- 事务B的代码(在另一个会话中同时执行)
BEGIN
    UPDATE orders SET status = 'processing' WHERE order_id = 100;
    UPDATE accounts SET balance = balance + 50 WHERE account_id = 1;
    COMMIT;
END

如果事务A先锁accounts表的第1行,然后尝试锁orders表的第100行;而事务B先锁orders表的第100行,然后尝试锁accounts表的第11行,就形成了循环等待。

解决方案:

  • 制定并遵守统一的锁获取顺序规则
  • 在所有代码中以相同顺序访问资源
  • 使用数据库设计规范强制顺序
-- 改进:两个事务都遵循相同的锁顺序(先accounts后orders)
-- 事务A
BEGIN
    UPDATE accounts SET balance = balance - 100 WHERE account_id = 1;
    UPDATE orders SET status = 'paid' WHERE order_id = 100;
    COMMIT;
END

-- 事务B(修改为相同顺序)
BEGIN
    UPDATE accounts SET balance = balance + 50 WHERE account_id = 1;
    UPDATE orders SET status = 'processing' WHERE order_id = 100;
    COMMIT;
END

3. 索引缺失或不当

问题描述:缺少合适的索引会导致全表扫描,增加锁的范围和持续时间。

具体表现:

  • WHERE子句中的条件列没有索引
  • 使用函数或表达式导致索引失效
  • 复合索引顺序不当

示例场景:

-- 问题:没有索引的查询
CREATE TABLE orders (
    order_id INT PRIMARY KEY,
    customer_id INT,
    order_date DATE,
    status VARCHAR(20),
    amount DECIMAL(10,2)
);

-- 没有在customer_id上创建索引
-- 以下查询会锁定整个表或大量行
BEGIN
    UPDATE orders SET status = 'shipped' WHERE customer_id = 12345;
    -- 如果customer_id=12345的订单很多,会锁定大量行
    -- 其他事务更新不同customer_id的订单也会被阻塞
    COMMIT;
END

解决方案:

  • 为常用查询条件创建合适的索引
  • 定期分析查询执行计划
  • 避免在WHERE子句中使用函数
-- 改进:创建合适的索引
CREATE INDEX idx_orders_customer_id ON orders(customer_id);

-- 验证索引是否被使用
-- 在DB2中可以使用:
EXPLAIN PLAN FOR UPDATE orders SET status = 'shipped' WHERE customer_id = 12345;

-- 查看执行计划,确保使用了索引扫描而非全表扫描

4. 锁升级(Lock Escalation)

问题描述:当事务持有的行锁数量超过DB2配置的阈值时,DB2会自动将行锁升级为表锁,导致并发性急剧下降。

具体表现:

  • 大批量UPDATE/DELETE操作
  • 缺少WHERE条件的全表更新
  • 锁升级阈值设置过低

示例场景:

-- 问题:大批量操作导致锁升级
BEGIN
    -- 假设orders表有100万行数据
    -- 此查询会锁定所有满足条件的行,可能触发锁升级
    UPDATE orders 
    SET status = 'expired' 
    WHERE order_date < '2023-01-01' 
      AND status = 'pending';
    
    COMMIT;
END

解决方案:

  • 分批处理大数据量操作
  • 调整锁升级阈值
  • 使用适当的隔离级别
-- 改进:分批处理避免锁升级
BEGIN
    DECLARE v_rows_updated INT DEFAULT 0;
    DECLARE v_batch_size INT DEFAULT 1000; -- 每批处理1000行
    
    REPEAT
        UPDATE orders 
        SET status = 'expired' 
        WHERE order_date < '2023-01-01' 
          AND status = 'pending'
          AND ROWNUMBER() <= v_batch_size;
        
        GET DIAGNOSTICS v_rows_updated = ROW_COUNT;
        
        -- 提交当前批次,释放锁
        COMMIT;
        
        -- 短暂延迟,让其他事务有机会执行
        -- DB2中可以使用:CALL SYSPROC.ADMIN_CMD('SLEEP 1');
        
    UNTIL v_rows_updated = 0 END REPEAT;
END

5. 隔离级别选择不当

问题描述:隔离级别决定了事务之间的可见性和锁行为,选择不当会增加锁竞争。

具体表现:

  • 使用可重复读(RR)隔离级别处理大量数据
  • 读操作使用排他锁
  • 不同事务使用不同隔离级别导致复杂交互

示例场景:

-- 问题:使用可重复读(RR)隔离级别
-- 在DB2中,默认隔离级别是游标稳定性(CS)

-- 设置为可重复读(RR)- 会锁定整个结果集
SET CURRENT ISOLATION = RR;

BEGIN
    -- 此查询会锁定所有读取的行,直到事务结束
    SELECT * FROM orders WHERE customer_id = 12345;
    
    -- 长时间运行的业务逻辑...
    
    COMMIT; -- 锁才释放
END

解决方案:

  • 根据业务需求选择合适的隔离级别
  • 了解不同隔离级别的锁行为
  • 在保证数据一致性的前提下使用最低隔离级别
-- 改进:使用游标稳定性(CS)或读稳定性(RS)
SET CURRENT ISOLATION = CS; -- 或 RS

BEGIN
    -- CS级别只锁定当前游标所在行,读完立即释放
    -- RS级别锁定所有读取的行,但比RR级别锁范围小
    SELECT * FROM orders WHERE customer_id = 12345;
    
    COMMIT;
END

-- 对于只读查询,可以使用未提交读(UR)
SET CURRENT ISOLATION = UR;
SELECT * FROM orders WHERE customer_id = 12345;

6. 应用程序连接管理不当

问题描述:长连接、连接池配置不当或事务未及时提交/回滚。

具体表现:

  • 应用程序持有连接但不执行操作
  • 事务未正确关闭
  • 连接池泄漏

示例示例:

// 问题代码:Java中事务未正确关闭
public void updateOrderStatus(int orderId, String status) {
    Connection conn = null;
    PreparedStatement stmt = null;
    try {
        conn = dataSource.getConnection();
        conn.setAutoCommit(false); // 开始事务
        
        stmt = conn.prepareStatement("UPDATE orders SET status = ? WHERE order_id = ?");
        stmt.setString(1, status);
        stmt.setInt(2, orderId);
        stmt.executeUpdate();
        
        // 问题:这里没有提交或回滚!
        // 事务会一直持有锁直到连接关闭或超时
        
    } catch (SQLException e) {
        // 问题:异常时没有回滚
        e.printStackTrace();
    } finally {
        // 关闭资源
        if (stmt != null) stmt.close();
        if (conn != null) conn.close(); // 此时才释放锁
    }
}

解决方案:

  • 确保每个事务都有明确的提交或回滚
  • 使用try-with-resources或finally块确保资源释放
  • 合理配置连接池参数
// 改进后的代码:正确管理事务和连接
public void updateOrderStatus(int orderId, String status) {
    // 使用try-with-resources自动关闭资源
    try (Connection conn = dataSource.getConnection();
         PreparedStatement stmt = conn.prepareStatement(
             "UPDATE orders SET status = ? WHERE order_id = ?")) {
        
        conn.setAutoCommit(false); // 开始事务
        
        stmt.setString(1, status);
        stmt.setInt(2, orderId);
        stmt.executeUpdate();
        
        // 明确提交事务,释放锁
        conn.commit();
        
    } catch (SQLException e) {
        // 异常时回滚事务
        try {
            if (conn != null) conn.rollback();
        } catch (SQLException ex) {
            ex.printStackTrace();
        }
        throw new RuntimeException("更新订单状态失败", e);
    }
}

高效解决DB2死锁的方法

1. 死锁检测与分析工具

DB2内置监控工具

DB2提供了多种内置工具来监控和分析死锁:

-- 1. 查看当前锁信息
SELECT 
    tabname,
    lock_mode,
    lock_object_type,
    status,
    member
FROM SYSIBMADM.LOCKS
WHERE lock_mode = 'X' OR lock_mode = 'U';

-- 2. 查看锁等待链
SELECT 
    requester_tabname,
    requester_lock_mode,
    holder_tabname,
    holder_lock_mode,
    wait_start
FROM SYSIBMADM.LOCKWAITS;

-- 3. 查看死锁历史(需要配置事件监视器)
-- 首先创建死锁事件监视器
CREATE EVENT MONITOR deadlock_mon FOR DEADLOCKS WRITE TO TABLE;

-- 激活监视器
SET EVENT MONITOR deadlock_mon STATE 1;

-- 查询死锁事件
SELECT 
    event_timestamp,
    deadlocked_tabname,
    deadlocked_lock_mode,
    member
FROM deadlock_mon.deadlocks;

使用db2pd命令

# 查看数据库的锁信息
db2pd -db your_database -locks showlocks

# 查看事务和锁等待
db2pd -db your_database -transactions -locks

# 查看死锁事件历史
db2pd -db your_database -deadlocks

配置数据库管理器参数以捕获死锁信息

-- 启用死锁事件监视器
UPDATE DBM CFG USING DFT_EVENT_MONITORS ON;

-- 设置死锁检测时间间隔(默认为10秒)
UPDATE DB CFG USING DLCHKTIME 5000; -- 5秒

-- 设置锁等待超时(防止无限期等待)
UPDATE DB CFG USING LOCKTIMEOUT 30; -- 30秒

2. 诊断死锁的步骤

步骤1:识别死锁模式

-- 查询最近的死锁事件
SELECT 
    event_timestamp,
    member,
    deadlocked_tabname,
    deadlocked_lock_mode,
    deadlocked_tabname,
    deadlocked_tabname
FROM SYSIBMADM.LOCKS
WHERE lock_mode = 'X' AND status = 'WAIT';

-- 或者使用事件监视器数据
SELECT 
    e.event_timestamp,
    e.member,
    e.deadlocked_tabname,
    e.deadlocked_lock_mode,
    e.application_handle,
    a.app_name,
    a.client_hostname,
    a.client_username
FROM deadlock_mon.deadlocks e
LEFT JOIN SYSIBMADM.APPLICATIONS a ON e.application_handle = a.application_handle
ORDER BY e.event_timestamp DESC
FETCH FIRST 10 ROWS ONLY;

步骤2:分析锁等待链

-- 查看当前锁等待情况
SELECT 
    w.waiter_application_handle,
    w.holder_application_handle,
    w.waiter_tabname,
    w.holder_tabname,
    w.waiter_lock_mode,
    w.holder_lock_mode,
    w.wait_start,
    TIMESTAMPDIFF(2, CURRENT_TIMESTAMP - w.wait_start) AS wait_seconds
FROM SYSIBMADM.LOCKWAITS w
ORDER BY wait_seconds DESC;

步骤3:获取问题SQL语句

-- 查看导致死锁的SQL
SELECT 
    a.application_handle,
    a.app_name,
    s.stmt_text,
    s.stmt_pkglist_id,
    s.num_executions,
    s.total_exec_time
FROM SYSIBMADM.APPLICATIONS a
JOIN SYSIBMADM.SNAP_APPL_INFO s ON a.application_handle = s.application_handle
WHERE a.application_handle IN (
    SELECT DISTINCT holder_application_handle FROM SYSIBMADM.LOCKWAITS
)
ORDER BY s.total_exec_time DESC;

3. 优化策略与最佳实践

策略1:事务设计优化

原则:保持事务简短,尽快提交或回滚。

-- 最佳实践:最小化事务范围
-- 错误示例:在事务中包含用户交互
BEGIN
    UPDATE inventory SET quantity = quantity - 1 WHERE product_id = 100;
    -- 等待用户输入(绝对禁止!)
    -- 用户输入期间锁一直持有
    COMMIT;
END

-- 正确做法:在事务外处理用户输入
-- 1. 收集用户输入
-- 2. 开始事务
BEGIN
    UPDATE inventory SET quantity = quantity - 1 WHERE product_id = 100;
    COMMIT;
END

策略2:批量操作优化

-- 大批量更新时使用分批提交
CREATE PROCEDURE batch_update_orders(
    IN batch_size INT DEFAULT 1000
)
BEGIN
    DECLARE v_done INT DEFAULT 0;
    DECLARE v_rows INT DEFAULT 0;
    DECLARE v_total_rows INT DEFAULT 0;
    
    -- 创建临时表存储需要更新的订单ID
    DECLARE GLOBAL TEMPORARY TABLE temp_orders_to_update (
        order_id INT
    ) ON COMMIT PRESERVE ROWS;
    
    -- 插入需要更新的订单ID
    INSERT INTO session.temp_orders_to_update
    SELECT order_id FROM orders 
    WHERE status = 'pending' AND order_date < CURRENT_DATE - 30 DAYS;
    
    -- 分批更新
    WHILE NOT v_done DO
        UPDATE orders 
        SET status = 'expired' 
        WHERE order_id IN (
            SELECT order_id FROM session.temp_orders_to_update 
            LIMIT batch_size
        );
        
        GET DIAGNOSTICS v_rows = ROW_COUNT;
        
        IF v_rows = 0 THEN
            SET v_done = 1;
        ELSE
            SET v_total_rows = v_total_rows + v_rows;
            COMMIT; -- 提交当前批次
            
            -- 删除已处理的记录
            DELETE FROM session.temp_orders_to_update 
            WHERE order_id IN (
                SELECT order_id FROM orders 
                WHERE status = 'expired' 
                LIMIT batch_size
            );
        END IF;
    END WHILE;
    
    -- 清理临时表
    DROP TABLE session.temp_orders_to_update;
END

策略3:索引优化

-- 1. 分析现有索引使用情况
SELECT 
    tabname,
    colnames,
    uniqrule,
    nleaf,
    nlevels,
    firstkeycard,
    fullkeycard
FROM SYSIBM.SYSINDEXES
WHERE tabname = 'ORDERS'
ORDER BY nleaf DESC;

-- 2. 创建覆盖索引(Covering Index)
-- 如果查询经常使用customer_id和status作为条件
CREATE INDEX idx_orders_customer_status ON orders(customer_id, status);

-- 3. 创建复合索引优化排序
CREATE INDEX idx_orders_date_customer ON orders(order_date DESC, customer_id);

-- 4. 查看索引是否被有效使用
-- 使用DB2 Design Advisor
db2advis -d your_database -i input.sql

-- input.sql 内容:
-- SELECT * FROM orders WHERE customer_id = ? AND status = ?;

策略4:隔离级别优化

-- 根据业务场景选择合适的隔离级别
-- 场景1:只读查询 - 使用UR(未提交读)
SET CURRENT ISOLATION = UR;
SELECT COUNT(*) FROM orders WHERE status = 'pending';

-- 场景2:需要读稳定性 - 使用RS(读稳定性)
SET CURRENT ISOLATION = RS;
SELECT * FROM orders WHERE customer_id = 12345 FOR UPDATE;

-- 场景3:需要可重复读 - 仅在必要时使用RR
SET CURRENT ISOLATION = RR;
-- 用于需要严格一致性的报表查询
SELECT SUM(amount) FROM orders WHERE order_date BETWEEN ? AND ?;

-- 场景4:默认使用CS(游标稳定性)- 平衡并发性和一致性
SET CURRENT ISOLATION = CS;
-- 适用于大多数OLTP场景
UPDATE orders SET status = 'processed' WHERE order_id = ?;

策略5:应用程序连接管理

// 使用连接池的最佳实践
public class DatabaseManager {
    private static DataSource dataSource;
    
    static {
        // 配置连接池参数
        HikariConfig config = new HikariConfig();
        config.setJdbcUrl("jdbc:db2://localhost:50000/YOURDB");
        config.setUsername("username");
        config.setPassword("password");
        
        // 关键参数:控制连接生命周期
        config.setMaximumPoolSize(20); // 最大连接数
        config.setMinimumIdle(5); // 最小空闲连接
        config.setIdleTimeout(300000); // 空闲超时5分钟
        config.setMaxLifetime(1800000); // 连接最大生命周期30分钟
        config.setConnectionTimeout(30000); // 获取连接超时30秒
        
        // 事务相关参数
        config.setAutoCommit(false); // 默认不自动提交
        config.setLeakDetectionThreshold(60000); // 检测连接泄漏60秒
        
        dataSource = new HikariDataSource(config);
    }
    
    // 使用try-with-resources确保自动关闭
    public void executeTransaction(Consumer<Connection> operation) {
        try (Connection conn = dataSource.getConnection()) {
            conn.setAutoCommit(false);
            try {
                operation.accept(conn);
                conn.commit();
            } catch (SQLException e) {
                conn.rollback();
                throw e;
            }
        } catch (SQLException e) {
            throw new RuntimeException("Database error", e);
        }
    }
}

4. 死锁预防策略

应用程序层面的预防

-- 1. 使用SELECT FOR UPDATE锁定记录
BEGIN
    -- 先锁定记录
    SELECT * FROM accounts WHERE account_id = 123 FOR UPDATE;
    
    -- 执行业务逻辑
    -- ...
    
    -- 更新记录
    UPDATE accounts SET balance = balance - 100 WHERE account_id = 123;
    
    COMMIT;
END

-- 2. 使用原子操作减少锁持有时间
-- 避免:先查询再更新
BEGIN
    SELECT balance INTO v_balance FROM accounts WHERE account_id = 123;
    -- 此处有时间差,可能被其他事务修改
    UPDATE accounts SET balance = v_balance - 100 WHERE account_id = 123;
    COMMIT;
END

-- 改进:直接更新
BEGIN
    UPDATE accounts SET balance = balance - 100 WHERE account_id = 123;
    COMMIT;
END

-- 3. 使用乐观锁(版本号机制)
ALTER TABLE orders ADD COLUMN version INT DEFAULT 0;

BEGIN
    DECLARE v_version INT;
    DECLARE v_rows INT;
    
    -- 读取当前版本
    SELECT version INTO v_version FROM orders WHERE order_id = 100;
    
    -- 尝试更新,检查版本是否变化
    UPDATE orders 
    SET status = 'processed', version = version + 1 
    WHERE order_id = 100 AND version = v_version;
    
    GET DIAGNOSTICS v_rows = ROW_COUNT;
    
    IF v_rows = 0 THEN
        -- 版本已变化,说明数据被其他事务修改
        ROLLBACK;
        -- 重试逻辑或抛出异常
    ELSE
        COMMIT;
    END IF;
END

数据库配置优化

-- 1. 调整锁升级阈值
-- 查看当前设置
GET DB CFG FOR your_database | grep LOCKSIZE

-- 如果频繁发生锁升级,可以适当提高阈值
-- 注意:这会增加内存使用
UPDATE DB CFG USING LOCKSIZE AUTO; -- 自动管理
-- 或者手动设置
UPDATE DB CFG USING LOCKSIZE 4096; -- 4096行

-- 2. 调整死锁检测间隔
-- 默认10秒,可以适当缩短以更快检测死锁
UPDATE DB CFG USING DLCHKTIME 3000; -- 3秒

-- 3. 设置锁等待超时,防止无限等待
UPDATE DB CFG USING LOCKTIMEOUT 30; -- 30秒

-- 4. 启用锁链表(Lock Chain)记录
UPDATE DBM CFG USING HEALTH_MON ON;
UPDATE DBM CFG USING SYSADM_GROUP sysadm;

-- 5. 配置事件监视器自动捕获死锁
CREATE EVENT MONITOR deadlock_event FOR DEADLOCKS 
WRITE TO TABLE 
TABLE deadlocks IN your_tablespace,
TABLE deadlock_details IN your_tablespace;

-- 激活监视器
SET EVENT MONITOR deadlock_event STATE 1;

-- 将其设置为自动启动
UPDATE DB CFG USING DFT_EVENT_MONITORS ON;

5. 监控与告警

创建持续监控脚本

#!/bin/bash
# monitor_deadlocks.sh

DB_NAME="YOURDB"
DB_USER="db2inst1"
LOG_FILE="/var/log/db2/deadlock_monitor.log"

# 检查最近5分钟的死锁数量
DEADLOCK_COUNT=$(db2 -x "SELECT COUNT(*) FROM SYSIBMADM.LOCKS WHERE lock_mode = 'X' AND status = 'WAIT' AND timestamp > CURRENT_TIMESTAMP - 5 MINUTES")

if [ "$DEADLOCK_COUNT" -gt 5 ]; then
    echo "$(date): 警告!检测到 $DEADLOCK_COUNT 个死锁相关事件" >> $LOG_FILE
    
    # 发送告警邮件
    echo "DB2死锁告警:$DB_NAME 在 $(date) 检测到 $DEADLOCK_COUNT 个死锁" | \
    mail -s "DB2死锁告警" dba@company.com
    
    # 收集详细信息
    db2 -x "SELECT * FROM SYSIBMADM.LOCKWAITS" >> $LOG_FILE
fi

使用DB2 Health Monitor

-- 启用健康监控
UPDATE DBM CFG USING HEALTH_MON ON;

-- 配置死锁告警阈值
-- 在数据库管理器配置中设置
UPDATE DBM CFG USING NOTIFYPEVENT DEADLOCK;

-- 创建健康监控配置文件
CREATE HEALTH MONITOR CONFIGURATION deadlock_threshold AS
    DEADLOCK_COUNT > 10 PER MINUTE;

实际案例分析

案例1:电商订单处理系统

问题描述:在订单提交高峰期,系统频繁出现死锁,导致订单处理失败。

死锁分析:

-- 通过事件监视器捕获的死锁信息
-- 事务A:扣减库存 + 创建订单
BEGIN
    UPDATE inventory SET quantity = quantity - 1 WHERE product_id = 100;
    INSERT INTO orders (product_id, quantity, status) VALUES (100, 1, 'pending');
    COMMIT;
END

-- 事务B:查询库存 + 扣减库存
BEGIN
    SELECT quantity FROM inventory WHERE product_id = 100;
    UPDATE inventory SET quantity = quantity - 1 WHERE product_id = 100;
    COMMIT;
END

解决方案:

-- 1. 为inventory表添加索引
CREATE INDEX idx_inventory_product ON inventory(product_id);

-- 2. 统一锁顺序:所有事务都先更新inventory,再操作orders
-- 事务B修改为:
BEGIN
    UPDATE inventory SET quantity = quantity - 1 WHERE product_id = 100;
    SELECT quantity FROM inventory WHERE product_id = 100; -- 在更新后查询
    COMMIT;
END

-- 3. 使用SELECT FOR UPDATE确保锁顺序
BEGIN
    SELECT * FROM inventory WHERE product_id = 100 FOR UPDATE;
    UPDATE inventory SET quantity = quantity - 1 WHERE product_id = 100;
    INSERT INTO orders (product_id, quantity, status) VALUES (100, 1, 'pending');
    COMMIT;
END

案例2:银行转账系统

问题描述:两个账户之间的转账操作导致死锁。

死锁分析:

-- 事务A:账户1向账户2转账
BEGIN
    UPDATE accounts SET balance = balance - 100 WHERE account_id = 1;
    UPDATE accounts SET balance = balance + 100 WHERE account_id = 2;
    COMMIT;
END

-- 事务B:账户2向账户1转账(同时执行)
BEGIN
    UPDATE accounts SET balance = balance - 50 WHERE account_id = 2;
    UPDATE accounts SET balance = balance + 50 WHERE account_id = 1;
    COMMIT;
END

解决方案:

-- 1. 统一锁顺序:按account_id排序
-- 事务A和B都按account_id升序处理
BEGIN
    -- 事务A:先处理1,再处理2
    UPDATE accounts SET balance = balance - 100 WHERE account_id = 1;
    UPDATE accounts SET balance = balance + 100 WHERE account_id = 2;
    COMMIT;
END

BEGIN
    -- 事务B:先处理1,再处理2(与事务A相同顺序)
    UPDATE accounts SET balance = balance + 50 WHERE account_id = 1;
    UPDATE accounts SET balance = balance - 50 WHERE account_id = 2;
    COMMIT;
END

-- 2. 使用存储过程封装转账逻辑,确保一致性
CREATE PROCEDURE transfer_funds(
    IN from_account INT,
    IN to_account INT,
    IN amount DECIMAL(10,2)
)
BEGIN
    DECLARE from_balance DECIMAL(10,2);
    DECLARE to_balance DECIMAL(10,2);
    
    -- 统一锁定顺序:按account_id排序
    IF from_account < to_account THEN
        -- 先锁from_account,再锁to_account
        SELECT balance INTO from_balance FROM accounts WHERE account_id = from_account FOR UPDATE;
        SELECT balance INTO to_balance FROM accounts WHERE account_id = to_account FOR UPDATE;
        
        IF from_balance >= amount THEN
            UPDATE accounts SET balance = balance - amount WHERE account_id = from_account;
            UPDATE accounts SET balance = balance + amount WHERE account_id = to_account;
        ELSE
            SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Insufficient funds';
        END IF;
    ELSE
        -- 先锁to_account,再锁from_account
        SELECT balance INTO to_balance FROM accounts WHERE account_id = to_account FOR UPDATE;
        SELECT balance INTO from_balance FROM accounts WHERE account_id = from_account FOR UPDATE;
        
        IF from_balance >= amount THEN
            UPDATE accounts SET balance = balance + amount WHERE account_id = to_account;
            UPDATE accounts SET balance = balance - amount WHERE account_id = from_account;
        ELSE
            SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Insufficient funds';
        END IF;
    END IF;
    
    COMMIT;
END

总结

DB2死锁问题虽然无法完全避免,但通过合理的分析和优化可以显著减少其发生频率和影响。关键要点包括:

  1. 理解死锁成因:掌握四个必要条件,识别常见原因
  2. 优化事务设计:保持事务简短,统一锁顺序
  3. 合理使用索引:减少锁的范围和持续时间
  4. 选择合适的隔离级别:平衡一致性和并发性
  5. 分批处理大数据量操作:避免锁升级
  6. 加强监控和告警:及时发现和解决问题
  7. 应用程序最佳实践:正确管理连接和事务

通过系统性地应用这些策略,可以构建更健壮、更高并发的DB2应用程序,将死锁影响降到最低。记住,预防胜于治疗,在设计阶段就考虑并发控制问题,比事后解决要有效得多。