引言:理解DB2死锁的本质
在DB2数据库管理系统中,死锁(Deadlock)是一种常见的并发控制问题,它发生在两个或多个事务相互等待对方释放资源时,形成一个无法解开的等待循环。死锁会导致相关事务被系统回滚,影响应用程序的正常运行,严重时甚至会导致整个系统的性能下降。理解死锁的成因并掌握高效的解决方法,对于数据库管理员和开发人员来说至关重要。
死锁就像两个人在一条狭窄的单行道上相遇,每个人都希望对方先退让,结果两人都无法前进。在数据库环境中,这种”僵局”会消耗系统资源,降低吞吐量,并可能导致数据不一致。DB2数据库提供了死锁检测机制,但预防胜于治疗,通过分析死锁原因并采取相应措施,可以显著减少死锁的发生频率。
死锁的四个必要条件
要形成死锁,必须同时满足以下四个条件:
互斥条件(Mutual Exclusion):资源不能被共享,只能独占使用。例如,一个事务对某行数据加了排他锁,其他事务无法同时访问该行。
请求与保持条件(Hold and Wait):事务已经持有了至少一个资源,同时又在请求其他被占用的资源。
不剥夺条件(No Preemption):事务已经获得的资源不能被强制剥夺,只能由持有者主动释放。
循环等待条件(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死锁问题虽然无法完全避免,但通过合理的分析和优化可以显著减少其发生频率和影响。关键要点包括:
- 理解死锁成因:掌握四个必要条件,识别常见原因
- 优化事务设计:保持事务简短,统一锁顺序
- 合理使用索引:减少锁的范围和持续时间
- 选择合适的隔离级别:平衡一致性和并发性
- 分批处理大数据量操作:避免锁升级
- 加强监控和告警:及时发现和解决问题
- 应用程序最佳实践:正确管理连接和事务
通过系统性地应用这些策略,可以构建更健壮、更高并发的DB2应用程序,将死锁影响降到最低。记住,预防胜于治疗,在设计阶段就考虑并发控制问题,比事后解决要有效得多。# DB2死锁的常见原因分析与高效解决方法详解
引言:理解DB2死锁的本质
在DB2数据库管理系统中,死锁(Deadlock)是一种常见的并发控制问题,它发生在两个或多个事务相互等待对方释放资源时,形成一个无法解开的等待循环。死锁会导致相关事务被系统回滚,影响应用程序的正常运行,严重时甚至会导致整个系统的性能下降。理解死锁的成因并掌握高效的解决方法,对于数据库管理员和开发人员来说至关重要。
死锁就像两个人在一条狭窄的单行道上相遇,每个人都希望对方先退让,结果两人都无法前进。在数据库环境中,这种”僵局”会消耗系统资源,降低吞吐量,并可能导致数据不一致。DB2数据库提供了死锁检测机制,但预防胜于治疗,通过分析死锁原因并采取相应措施,可以显著减少死锁的发生频率。
死锁的四个必要条件
要形成死锁,必须同时满足以下四个条件:
互斥条件(Mutual Exclusion):资源不能被共享,只能独占使用。例如,一个事务对某行数据加了排他锁,其他事务无法同时访问该行。
请求与保持条件(Hold and Wait):事务已经持有了至少一个资源,同时又在请求其他被占用的资源。
不剥夺条件(No Preemption):事务已经获得的资源不能被强制剥夺,只能由持有者主动释放。
循环等待条件(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死锁问题虽然无法完全避免,但通过合理的分析和优化可以显著减少其发生频率和影响。关键要点包括:
- 理解死锁成因:掌握四个必要条件,识别常见原因
- 优化事务设计:保持事务简短,统一锁顺序
- 合理使用索引:减少锁的范围和持续时间
- 选择合适的隔离级别:平衡一致性和并发性
- 分批处理大数据量操作:避免锁升级
- 加强监控和告警:及时发现和解决问题
- 应用程序最佳实践:正确管理连接和事务
通过系统性地应用这些策略,可以构建更健壮、更高并发的DB2应用程序,将死锁影响降到最低。记住,预防胜于治疗,在设计阶段就考虑并发控制问题,比事后解决要有效得多。
