【Oracle】锁

发布于:2025-06-05 ⋅ 阅读:(21) ⋅ 点赞:(0)

在这里插入图片描述

个人主页:Guiat
归属专栏:Oracle

在这里插入图片描述

正文

1. 锁基础概述

锁是Oracle数据库中用于控制并发访问数据的重要机制,确保数据的一致性和完整性。Oracle使用多种类型的锁来管理不同级别的资源访问。

1.1 锁的概念与作用

Oracle锁机制
并发控制
数据一致性
事务隔离
资源保护
多用户同时访问
防止数据冲突
ACID特性保证
读写一致性
事务隔离级别
幻读防护
表级保护
行级保护
系统资源保护

1.2 锁的工作原理

无锁
有兼容锁
有冲突锁
事务请求资源
检查锁状态
获得锁
等待队列
执行操作
锁释放?
继续等待
事务结束
释放锁
通知等待事务

1.3 Oracle锁的分类

Oracle锁类型
按锁定对象分类
按锁定模式分类
按锁定范围分类
按锁定时间分类
行级锁 Row Lock
表级锁 Table Lock
页级锁 Page Lock
系统锁 System Lock
共享锁 Shared
排他锁 Exclusive
意图锁 Intent
更新锁 Update
数据库级
表空间级
表级
行级
事务锁
语句锁
临时锁

2. 行级锁 (Row-Level Locks)

行级锁是Oracle中最常用的锁类型,提供最高的并发性。

2.1 行级锁的基本概念

2.1.1 TX锁(事务锁)

-- 演示行级锁的基本操作
-- Session 1
BEGIN
  UPDATE employees 
  SET salary = salary + 1000 
  WHERE employee_id = 100;
  -- 此时employee_id=100的行被锁定
  -- 不要提交,保持锁定状态
END;
/

-- 查看当前锁定信息
SELECT 
    s.sid,
    s.serial#,
    s.username,
    s.program,
    l.type,
    l.mode_held,
    l.mode_requested,
    o.object_name,
    l.block
FROM v$lock l
JOIN v$session s ON l.sid = s.sid
JOIN dba_objects o ON l.id1 = o.object_id
WHERE s.username IS NOT NULL
ORDER BY s.sid;

2.1.2 行级锁的工作机制

-- 创建测试表来演示锁机制
CREATE TABLE lock_demo (
    id NUMBER PRIMARY KEY,
    name VARCHAR2(50),
    amount NUMBER,
    last_updated DATE DEFAULT SYSDATE
);

-- 插入测试数据
INSERT INTO lock_demo VALUES (1, 'Record 1', 1000, SYSDATE);
INSERT INTO lock_demo VALUES (2, 'Record 2', 2000, SYSDATE);
INSERT INTO lock_demo VALUES (3, 'Record 3', 3000, SYSDATE);
COMMIT;

-- Session 1: 锁定第一行
UPDATE lock_demo SET amount = 1500 WHERE id = 1;
-- 不提交,保持锁定

-- Session 2: 可以访问其他行
SELECT * FROM lock_demo WHERE id = 2; -- 成功
UPDATE lock_demo SET amount = 2500 WHERE id = 2; -- 成功

-- Session 2: 尝试访问被锁定的行
UPDATE lock_demo SET amount = 1200 WHERE id = 1; -- 等待

2.2 行级锁的类型

2.2.1 共享行级锁 (SELECT FOR UPDATE)

-- 使用SELECT FOR UPDATE获取共享行级锁
DECLARE
  CURSOR emp_cursor IS
    SELECT employee_id, first_name, last_name, salary
    FROM employees
    WHERE department_id = 20
    FOR UPDATE;
    
  emp_rec emp_cursor%ROWTYPE;
  
BEGIN
  DBMS_OUTPUT.PUT_LINE('=== 锁定部门20的所有员工 ===');
  
  OPEN emp_cursor;
  
  LOOP
    FETCH emp_cursor INTO emp_rec;
    EXIT WHEN emp_cursor%NOTFOUND;
    
    DBMS_OUTPUT.PUT_LINE('锁定员工: ' || emp_rec.employee_id || 
                        ' - ' || emp_rec.first_name || ' ' || emp_rec.last_name);
    
    -- 在这里可以进行其他业务逻辑
    -- 所有相关行都被锁定,防止其他会话修改
    
  END LOOP;
  
  CLOSE emp_cursor;
  
  -- 可以选择提交或回滚
  COMMIT; -- 释放所有锁
  
END;
/

2.2.2 排他行级锁 (UPDATE/DELETE)

-- 演示排他行级锁
BEGIN
  -- 更新操作自动获取排他锁
  UPDATE employees 
  SET salary = salary * 1.1 
  WHERE department_id = 10;
  
  DBMS_OUTPUT.PUT_LINE('更新了 ' || SQL%ROWCOUNT || ' 行');
  
  -- 删除操作也获取排他锁
  DELETE FROM lock_demo WHERE amount < 1000;
  
  DBMS_OUTPUT.PUT_LINE('删除了 ' || SQL%ROWCOUNT || ' 行');
  
  -- 提交释放锁
  COMMIT;
  
END;
/

2.3 行级锁的高级特性

2.3.1 SELECT FOR UPDATE的选项

-- NOWAIT选项:立即返回而不等待
BEGIN
  -- 尝试锁定,如果不能立即获得锁就返回错误
  SELECT employee_id, salary
  INTO :emp_id, :emp_salary
  FROM employees
  WHERE employee_id = 100
  FOR UPDATE NOWAIT;
  
EXCEPTION
  WHEN OTHERS THEN
    IF SQLCODE = -54 THEN -- ORA-00054: resource busy
      DBMS_OUTPUT.PUT_LINE('资源正忙,无法获得锁');
    ELSE
      DBMS_OUTPUT.PUT_LINE('其他错误: ' || SQLERRM);
    END IF;
END;
/

-- WAIT选项:指定等待时间
DECLARE
  v_emp_id NUMBER;
  v_salary NUMBER;
BEGIN
  SELECT employee_id, salary
  INTO v_emp_id, v_salary
  FROM employees
  WHERE employee_id = 100
  FOR UPDATE WAIT 5; -- 等待5秒
  
  DBMS_OUTPUT.PUT_LINE('成功获得锁,员工ID: ' || v_emp_id);
  
EXCEPTION
  WHEN OTHERS THEN
    IF SQLCODE = -30006 THEN -- ORA-30006: resource busy; acquire with WAIT timeout expired
      DBMS_OUTPUT.PUT_LINE('等待超时,无法获得锁');
    ELSE
      DBMS_OUTPUT.PUT_LINE('错误: ' || SQLERRM);
    END IF;
END;
/

2.3.2 选择性列锁定

-- 只锁定特定列
SELECT employee_id, first_name, last_name, salary
FROM employees
WHERE department_id = 30
FOR UPDATE OF salary;

-- 跳过已锁定的行
SELECT employee_id, first_name, last_name, salary
FROM employees
WHERE department_id = 40
FOR UPDATE SKIP LOCKED;

3. 表级锁 (Table-Level Locks)

表级锁控制对整个表的访问,通常在DDL操作或需要表级控制时使用。

3.1 表级锁的类型

3.1.1 表锁模式

-- 手动获取表级锁
-- 共享锁模式 (Share Mode)
LOCK TABLE employees IN SHARE MODE;

-- 排他锁模式 (Exclusive Mode)
LOCK TABLE lock_demo IN EXCLUSIVE MODE;

-- 行共享锁模式 (Row Share Mode)
LOCK TABLE employees IN ROW SHARE MODE;

-- 行排他锁模式 (Row Exclusive Mode)
LOCK TABLE employees IN ROW EXCLUSIVE MODE;

-- 共享行排他锁模式 (Share Row Exclusive Mode)
LOCK TABLE employees IN SHARE ROW EXCLUSIVE MODE;

3.1.2 锁兼容性矩阵

表锁兼容性
RS - Row Share
RX - Row Exclusive
S - Share
SRX - Share Row Exclusive
X - Exclusive
与RS/RX/S兼容
仅与RS/RX兼容
与RS/S兼容
仅与RS兼容
与任何模式不兼容

3.2 DDL锁

DDL操作会自动获取表级排他锁。

3.2.1 DDL锁的影响

-- DDL操作示例
-- 创建表时的锁定
CREATE TABLE ddl_lock_demo (
    id NUMBER,
    description VARCHAR2(100),
    created_date DATE DEFAULT SYSDATE
);

-- 修改表结构时的锁定
ALTER TABLE ddl_lock_demo ADD (
    status VARCHAR2(20) DEFAULT 'ACTIVE'
);

-- 创建索引时的锁定(在线DDL)
CREATE INDEX CONCURRENTLY idx_ddl_demo_status 
ON ddl_lock_demo(status) ONLINE;

-- 删除表时的锁定
DROP TABLE ddl_lock_demo;

3.2.2 在线DDL操作

-- 在线添加列(不锁定表)
ALTER TABLE employees ADD (
    bonus_pct NUMBER(3,2) DEFAULT 0.00
) ONLINE;

-- 在线创建索引
CREATE INDEX idx_emp_bonus 
ON employees(bonus_pct) 
ONLINE;

-- 在线重建索引
ALTER INDEX idx_emp_bonus REBUILD ONLINE;

4. 死锁检测与处理

死锁是两个或多个事务相互等待对方释放锁的情况。

4.1 死锁的产生

4.1.1 典型死锁场景

-- 创建死锁演示表
CREATE TABLE deadlock_demo1 (id NUMBER PRIMARY KEY, value VARCHAR2(50));
CREATE TABLE deadlock_demo2 (id NUMBER PRIMARY KEY, value VARCHAR2(50));

INSERT INTO deadlock_demo1 VALUES (1, 'Value A');
INSERT INTO deadlock_demo2 VALUES (1, 'Value B');
COMMIT;

-- Session 1: 
BEGIN
  UPDATE deadlock_demo1 SET value = 'Updated A' WHERE id = 1;
  -- 等待几秒,然后执行下面的语句
  DBMS_LOCK.SLEEP(5);
  UPDATE deadlock_demo2 SET value = 'Updated B from Session 1' WHERE id = 1;
  COMMIT;
END;
/

-- Session 2: (同时在另一个会话中执行)
BEGIN
  UPDATE deadlock_demo2 SET value = 'Updated B' WHERE id = 1;
  -- 等待几秒,然后执行下面的语句
  DBMS_LOCK.SLEEP(5);
  UPDATE deadlock_demo1 SET value = 'Updated A from Session 2' WHERE id = 1;
  COMMIT;
END;
/

4.1.2 死锁检测机制

-- 查看死锁信息的存储过程
CREATE OR REPLACE PROCEDURE check_deadlocks
AS
BEGIN
  -- 检查当前的锁等待情况
  FOR rec IN (
    SELECT 
      waiting.sid AS waiting_session,
      waiting.username AS waiting_user,
      waiting.machine AS waiting_machine,
      holding.sid AS holding_session,
      holding.username AS holding_user,
      holding.machine AS holding_machine,
      l.type AS lock_type,
      o.object_name
    FROM v$lock l1
    JOIN v$lock l2 ON l1.id1 = l2.id1 AND l1.id2 = l2.id2
    JOIN v$session waiting ON l1.sid = waiting.sid
    JOIN v$session holding ON l2.sid = holding.sid
    JOIN dba_objects o ON l1.id1 = o.object_id
    WHERE l1.block = 0 AND l2.block = 1
      AND l1.type = l2.type
  ) LOOP
    DBMS_OUTPUT.PUT_LINE('等待会话: ' || rec.waiting_session || 
                        ' 用户: ' || rec.waiting_user ||
                        ' 被会话: ' || rec.holding_session ||
                        ' 用户: ' || rec.holding_user || ' 阻塞');
    DBMS_OUTPUT.PUT_LINE('锁类型: ' || rec.lock_type || 
                        ' 对象: ' || rec.object_name);
    DBMS_OUTPUT.PUT_LINE('---');
  END LOOP;
END;
/

-- 执行死锁检查
EXEC check_deadlocks;

4.2 死锁的解决

4.2.1 Oracle自动死锁检测

Oracle会自动检测死锁并回滚其中一个事务。

-- 监控死锁事件
SELECT 
    name,
    value
FROM v$sysstat
WHERE name LIKE '%deadlock%';

-- 查看死锁的详细信息
SELECT 
    to_char(timestamp, 'YYYY-MM-DD HH24:MI:SS') AS deadlock_time,
    substr(message_text, 1, 100) AS message
FROM dba_hist_active_sess_history
WHERE event = 'enq: TX - row lock contention'
ORDER BY timestamp DESC;

4.2.2 预防死锁的策略

-- 策略1: 按固定顺序访问资源
CREATE OR REPLACE PROCEDURE safe_update_procedure(
  p_id1 NUMBER,
  p_id2 NUMBER,
  p_value1 VARCHAR2,
  p_value2 VARCHAR2
) AS
  v_first_id NUMBER;
  v_second_id NUMBER;
  v_first_value VARCHAR2(50);
  v_second_value VARCHAR2(50);
BEGIN
  -- 确定访问顺序(按ID升序)
  IF p_id1 < p_id2 THEN
    v_first_id := p_id1;
    v_second_id := p_id2;
    v_first_value := p_value1;
    v_second_value := p_value2;
  ELSE
    v_first_id := p_id2;
    v_second_id := p_id1;
    v_first_value := p_value2;
    v_second_value := p_value1;
  END IF;
  
  -- 按固定顺序更新
  UPDATE deadlock_demo1 SET value = v_first_value WHERE id = v_first_id;
  UPDATE deadlock_demo2 SET value = v_second_value WHERE id = v_second_id;
  
  COMMIT;
END;
/

-- 策略2: 使用超时机制
CREATE OR REPLACE PROCEDURE timeout_update_procedure AS
BEGIN
  -- 设置语句超时
  EXECUTE IMMEDIATE 'ALTER SESSION SET ddl_lock_timeout = 10';
  
  UPDATE deadlock_demo1 SET value = 'Safe Update' WHERE id = 1;
  UPDATE deadlock_demo2 SET value = 'Safe Update' WHERE id = 1;
  
  COMMIT;
  
EXCEPTION
  WHEN OTHERS THEN
    ROLLBACK;
    IF SQLCODE = -30006 THEN
      DBMS_OUTPUT.PUT_LINE('操作超时,避免了潜在的死锁');
    ELSE
      RAISE;
    END IF;
END;
/

5. 锁监控与管理

5.1 锁信息查询

5.1.1 当前锁状态查询

-- 查看当前所有锁信息
SELECT 
    s.sid,
    s.serial#,
    s.username,
    s.status,
    s.machine,
    s.program,
    l.type AS lock_type,
    DECODE(l.type,
           'TM', 'Table Lock',
           'TX', 'Transaction Lock',
           'UL', 'User Lock',
           l.type) AS lock_description,
    DECODE(l.lmode,
           0, 'None',
           1, 'Null',
           2, 'Row Share',
           3, 'Row Exclusive',
           4, 'Share',
           5, 'Share Row Exclusive',
           6, 'Exclusive',
           l.lmode) AS lock_mode,
    o.owner,
    o.object_name,
    o.object_type
FROM v$lock l
JOIN v$session s ON l.sid = s.sid
LEFT JOIN dba_objects o ON l.id1 = o.object_id
WHERE s.username IS NOT NULL
ORDER BY s.sid, l.type;

5.1.2 锁等待情况分析

-- 创建锁等待分析视图
CREATE OR REPLACE VIEW lock_wait_analysis AS
SELECT 
    waiter.sid AS waiting_session,
    waiter.username AS waiting_user,
    waiter.status AS waiting_status,
    waiter.machine AS waiting_machine,
    waiter.program AS waiting_program,
    holder.sid AS holding_session,
    holder.username AS holding_user,
    holder.status AS holding_status,
    holder.machine AS holding_machine,
    holder.program AS holding_program,
    l1.type AS lock_type,
    DECODE(l1.request,
           0, 'None',
           1, 'Null',
           2, 'Row Share',
           3, 'Row Exclusive', 
           4, 'Share',
           5, 'Share Row Exclusive',
           6, 'Exclusive',
           l1.request) AS requested_mode,
    DECODE(l2.lmode,
           0, 'None',
           1, 'Null',
           2, 'Row Share',
           3, 'Row Exclusive',
           4, 'Share', 
           5, 'Share Row Exclusive',
           6, 'Exclusive',
           l2.lmode) AS held_mode,
    o.owner,
    o.object_name,
    o.object_type
FROM v$lock l1
JOIN v$lock l2 ON l1.id1 = l2.id1 AND l1.id2 = l2.id2
JOIN v$session waiter ON l1.sid = waiter.sid
JOIN v$session holder ON l2.sid = holder.sid
LEFT JOIN dba_objects o ON l1.id1 = o.object_id
WHERE l1.block = 0 
  AND l2.block = 1
  AND l1.type = l2.type;

-- 查询当前锁等待情况
SELECT * FROM lock_wait_analysis;

5.2 锁等待诊断

5.2.1 创建锁监控工具

-- 创建锁监控存储过程
CREATE OR REPLACE PROCEDURE monitor_locks(
  p_show_details BOOLEAN DEFAULT TRUE
) AS
  v_count NUMBER := 0;
BEGIN
  DBMS_OUTPUT.PUT_LINE('=== Oracle锁监控报告 ===');
  DBMS_OUTPUT.PUT_LINE('时间: ' || TO_CHAR(SYSDATE, 'YYYY-MM-DD HH24:MI:SS'));
  DBMS_OUTPUT.PUT_LINE('');
  
  -- 统计锁数量
  SELECT COUNT(*) INTO v_count
  FROM v$lock l
  JOIN v$session s ON l.sid = s.sid
  WHERE s.username IS NOT NULL;
  
  DBMS_OUTPUT.PUT_LINE('当前活跃锁数量: ' || v_count);
  
  -- 检查锁等待
  SELECT COUNT(*) INTO v_count FROM lock_wait_analysis;
  
  IF v_count > 0 THEN
    DBMS_OUTPUT.PUT_LINE('*** 警告: 发现 ' || v_count || ' 个锁等待情况 ***');
    
    IF p_show_details THEN
      DBMS_OUTPUT.PUT_LINE('');
      DBMS_OUTPUT.PUT_LINE('锁等待详情:');
      DBMS_OUTPUT.PUT_LINE('----------------------------------------');
      
      FOR rec IN (SELECT * FROM lock_wait_analysis) LOOP
        DBMS_OUTPUT.PUT_LINE('等待者: SID=' || rec.waiting_session || 
                            ', 用户=' || rec.waiting_user ||
                            ', 机器=' || rec.waiting_machine);
        DBMS_OUTPUT.PUT_LINE('持有者: SID=' || rec.holding_session ||
                            ', 用户=' || rec.holding_user ||
                            ', 机器=' || rec.holding_machine);
        DBMS_OUTPUT.PUT_LINE('锁类型: ' || rec.lock_type ||
                            ', 对象: ' || rec.owner || '.' || rec.object_name);
        DBMS_OUTPUT.PUT_LINE('请求模式: ' || rec.requested_mode ||
                            ', 持有模式: ' || rec.held_mode);
        DBMS_OUTPUT.PUT_LINE('----------------------------------------');
      END LOOP;
    END IF;
  ELSE
    DBMS_OUTPUT.PUT_LINE('没有发现锁等待情况');
  END IF;
  
  -- 显示长时间运行的事务
  SELECT COUNT(*) INTO v_count
  FROM v$session s
  JOIN v$transaction t ON s.saddr = t.ses_addr
  WHERE (SYSDATE - t.start_date) * 24 * 60 > 30; -- 超过30分钟
  
  IF v_count > 0 THEN
    DBMS_OUTPUT.PUT_LINE('');
    DBMS_OUTPUT.PUT_LINE('*** 警告: 发现 ' || v_count || ' 个长时间运行的事务 ***');
  END IF;
  
END;
/

-- 执行锁监控
EXEC monitor_locks(TRUE);

5.2.2 自动化锁监控

-- 创建锁监控表
CREATE TABLE lock_monitor_log (
    log_id NUMBER PRIMARY KEY,
    log_time DATE DEFAULT SYSDATE,
    lock_count NUMBER,
    wait_count NUMBER,
    long_tx_count NUMBER,
    details CLOB
);

CREATE SEQUENCE lock_monitor_seq START WITH 1 INCREMENT BY 1;

-- 创建自动监控过程
CREATE OR REPLACE PROCEDURE auto_lock_monitor AS
  v_lock_count NUMBER;
  v_wait_count NUMBER;
  v_long_tx_count NUMBER;
  v_details CLOB;
BEGIN
  -- 统计锁信息
  SELECT COUNT(*) INTO v_lock_count
  FROM v$lock l JOIN v$session s ON l.sid = s.sid
  WHERE s.username IS NOT NULL;
  
  SELECT COUNT(*) INTO v_wait_count FROM lock_wait_analysis;
  
  SELECT COUNT(*) INTO v_long_tx_count
  FROM v$session s JOIN v$transaction t ON s.saddr = t.ses_addr
  WHERE (SYSDATE - t.start_date) * 24 * 60 > 30;
  
  -- 如果有异常情况,记录详细信息
  IF v_wait_count > 0 OR v_long_tx_count > 0 THEN
    v_details := 'Lock waits detected: ' || v_wait_count || CHR(10) ||
                 'Long transactions: ' || v_long_tx_count;
  END IF;
  
  -- 记录监控日志
  INSERT INTO lock_monitor_log (
    log_id, lock_count, wait_count, long_tx_count, details
  ) VALUES (
    lock_monitor_seq.NEXTVAL, v_lock_count, v_wait_count, v_long_tx_count, v_details
  );
  
  COMMIT;
  
  -- 如果有严重问题,发送告警
  IF v_wait_count > 5 THEN
    DBMS_OUTPUT.PUT_LINE('ALERT: High number of lock waits detected!');
  END IF;
  
END;
/

-- 设置定时任务(需要DBMS_SCHEDULER权限)
BEGIN
  DBMS_SCHEDULER.CREATE_JOB(
    job_name        => 'LOCK_MONITOR_JOB',
    job_type        => 'PLSQL_BLOCK',
    job_action      => 'BEGIN auto_lock_monitor; END;',
    start_date      => SYSDATE,
    repeat_interval => 'FREQ=MINUTELY; INTERVAL=5', -- 每5分钟执行一次
    enabled         => TRUE
  );
END;
/

6. 锁优化策略

6.1 减少锁争用

6.1.1 优化事务设计

-- 不好的做法:长事务
CREATE OR REPLACE PROCEDURE bad_batch_update AS
BEGIN
  -- 开始一个长事务
  FOR rec IN (SELECT * FROM large_table) LOOP
    -- 复杂的业务逻辑
    UPDATE another_table SET status = 'PROCESSED' 
    WHERE id = rec.id;
    
    -- 更多的处理...
    DBMS_LOCK.SLEEP(1); -- 模拟耗时操作
  END LOOP;
  
  COMMIT; -- 长时间持有锁
END;
/

-- 好的做法:短事务批处理
CREATE OR REPLACE PROCEDURE good_batch_update AS
  CURSOR data_cursor IS SELECT * FROM large_table;
  TYPE data_array IS TABLE OF data_cursor%ROWTYPE;
  v_data data_array;
  v_batch_size CONSTANT NUMBER := 1000;
BEGIN
  OPEN data_cursor;
  
  LOOP
    FETCH data_cursor BULK COLLECT INTO v_data LIMIT v_batch_size;
    
    -- 处理一批数据
    FORALL i IN 1..v_data.COUNT
      UPDATE another_table 
      SET status = 'PROCESSED' 
      WHERE id = v_data(i).id;
    
    COMMIT; -- 及时提交,释放锁
    
    EXIT WHEN data_cursor%NOTFOUND;
  END LOOP;
  
  CLOSE data_cursor;
END;
/

6.1.2 使用适当的隔离级别

-- 设置事务隔离级别
-- READ COMMITTED(默认)
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;

-- SERIALIZABLE(更高的一致性,但可能增加锁争用)
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;

-- READ ONLY(只读事务,不获取写锁)
SET TRANSACTION READ ONLY;

-- 使用只读事务进行报表查询
BEGIN
  SET TRANSACTION READ ONLY;
  
  -- 执行复杂的报表查询
  SELECT d.department_name,
         COUNT(e.employee_id) AS emp_count,
         AVG(e.salary) AS avg_salary
  FROM departments d
  LEFT JOIN employees e ON d.department_id = e.department_id
  GROUP BY d.department_name
  ORDER BY avg_salary DESC;
  
  COMMIT;
END;
/

6.2 锁性能调优

6.2.1 索引优化减少锁争用

-- 创建适当的索引减少锁扫描范围
CREATE INDEX idx_emp_dept_status ON employees(department_id, status);

-- 使用索引友好的查询
UPDATE employees 
SET status = 'INACTIVE' 
WHERE department_id = 10 AND status = 'ACTIVE';

-- 避免全表扫描的锁定
-- 不好的做法
UPDATE employees SET last_login = SYSDATE; -- 锁定所有行

-- 好的做法
UPDATE employees 
SET last_login = SYSDATE 
WHERE employee_id IN (
  SELECT employee_id FROM recent_logins
); -- 只锁定必要的行

6.2.2 分区表减少锁争用

-- 创建分区表减少锁争用
CREATE TABLE partitioned_transactions (
    transaction_id NUMBER,
    transaction_date DATE,
    amount NUMBER,
    status VARCHAR2(20)
)
PARTITION BY RANGE (transaction_date) (
    PARTITION p_2023 VALUES LESS THAN (DATE '2024-01-01'),
    PARTITION p_2024 VALUES LESS THAN (DATE '2025-01-01'),
    PARTITION p_2025 VALUES LESS THAN (DATE '2026-01-01')
);

-- 并行处理不同分区,减少锁争用
-- Session 1: 处理2023年数据
UPDATE partitioned_transactions 
SET status = 'PROCESSED' 
WHERE transaction_date >= DATE '2023-01-01' 
  AND transaction_date < DATE '2024-01-01'
  AND status = 'PENDING';

-- Session 2: 同时处理2024年数据(不同分区,无锁争用)
UPDATE partitioned_transactions 
SET status = 'PROCESSED' 
WHERE transaction_date >= DATE '2024-01-01' 
  AND transaction_date < DATE '2025-01-01'
  AND status = 'PENDING';

7. 特殊锁类型

7.1 用户定义锁

7.1.1 DBMS_LOCK包的使用

-- 使用DBMS_LOCK创建用户定义锁
CREATE OR REPLACE PROCEDURE demo_user_lock AS
  v_lockhandle VARCHAR2(128);
  v_lock_result NUMBER;
BEGIN
  -- 分配锁句柄
  DBMS_LOCK.ALLOCATE_UNIQUE('MY_CUSTOM_LOCK', v_lockhandle);
  
  DBMS_OUTPUT.PUT_LINE('尝试获取用户锁...');
  
  -- 请求锁(排他模式,等待10秒)
  v_lock_result := DBMS_LOCK.REQUEST(
    lockhandle => v_lockhandle,
    lockmode   => DBMS_LOCK.X_MODE, -- 排他模式
    timeout    => 10,               -- 10秒超时
    release_on_commit => TRUE       -- 提交时释放
  );
  
  CASE v_lock_result
    WHEN 0 THEN 
      DBMS_OUTPUT.PUT_LINE('成功获得锁');
      
      -- 执行需要同步的操作
      DBMS_OUTPUT.PUT_LINE('执行关键业务逻辑...');
      DBMS_LOCK.SLEEP(5); -- 模拟处理时间
      
      -- 手动释放锁
      v_lock_result := DBMS_LOCK.RELEASE(v_lockhandle);
      DBMS_OUTPUT.PUT_LINE('锁已释放');
      
    WHEN 1 THEN
      DBMS_OUTPUT.PUT_LINE('获取锁超时');
    WHEN 2 THEN
      DBMS_OUTPUT.PUT_LINE('死锁检测到');
    WHEN 3 THEN
      DBMS_OUTPUT.PUT_LINE('参数错误');
    WHEN 4 THEN
      DBMS_OUTPUT.PUT_LINE('锁已被持有');
    ELSE
      DBMS_OUTPUT.PUT_LINE('未知错误: ' || v_lock_result);
  END CASE;
  
END;
/

-- 测试用户锁
EXEC demo_user_lock;

7.1.2 应用级锁控制

-- 创建应用级锁管理表
CREATE TABLE app_locks (
    lock_name VARCHAR2(100) PRIMARY KEY,
    session_id NUMBER,
    username VARCHAR2(30),
    acquired_time DATE,
    purpose VARCHAR2(200)
);

-- 创建应用锁管理包
CREATE OR REPLACE PACKAGE app_lock_mgr AS
  FUNCTION acquire_lock(
    p_lock_name VARCHAR2,
    p_purpose VARCHAR2 DEFAULT NULL,
    p_timeout NUMBER DEFAULT 30
  ) RETURN BOOLEAN;
  
  FUNCTION release_lock(p_lock_name VARCHAR2) RETURN BOOLEAN;
  
  PROCEDURE show_locks;
END app_lock_mgr;
/

CREATE OR REPLACE PACKAGE BODY app_lock_mgr AS
  
  FUNCTION acquire_lock(
    p_lock_name VARCHAR2,
    p_purpose VARCHAR2 DEFAULT NULL,
    p_timeout NUMBER DEFAULT 30
  ) RETURN BOOLEAN IS
    v_count NUMBER;
    v_session_id NUMBER;
    v_start_time DATE := SYSDATE;
  BEGIN
    SELECT SYS_CONTEXT('USERENV', 'SID') INTO v_session_id FROM DUAL;
    
    LOOP
      -- 尝试获取锁
      BEGIN
        INSERT INTO app_locks (
          lock_name, session_id, username, acquired_time, purpose
        ) VALUES (
          p_lock_name, v_session_id, USER, SYSDATE, p_purpose
        );
        
        COMMIT;
        RETURN TRUE; -- 成功获取锁
        
      EXCEPTION
        WHEN DUP_VAL_ON_INDEX THEN
          -- 锁已被其他会话持有
          IF (SYSDATE - v_start_time) * 24 * 60 * 60 > p_timeout THEN
            RETURN FALSE; -- 超时
          END IF;
          
          DBMS_LOCK.SLEEP(1); -- 等待1秒后重试
      END;
    END LOOP;
    
  END acquire_lock;
  
  FUNCTION release_lock(p_lock_name VARCHAR2) RETURN BOOLEAN IS
    v_session_id NUMBER;
    v_count NUMBER;
  BEGIN
    SELECT SYS_CONTEXT('USERENV', 'SID') INTO v_session_id FROM DUAL;
    
    DELETE FROM app_locks 
    WHERE lock_name = p_lock_name 
      AND session_id = v_session_id;
    
    v_count := SQL%ROWCOUNT;
    COMMIT;
    
    RETURN v_count > 0;
  END release_lock;
  
  PROCEDURE show_locks IS
  BEGIN
    DBMS_OUTPUT.PUT_LINE('=== 应用级锁状态 ===');
    FOR rec IN (
      SELECT lock_name, session_id, username, 
             TO_CHAR(acquired_time, 'YYYY-MM-DD HH24:MI:SS') AS acquired,
             purpose
      FROM app_locks
      ORDER BY acquired_time
    ) LOOP
      DBMS_OUTPUT.PUT_LINE('锁名: ' || rec.lock_name ||
                          ', 会话: ' || rec.session_id ||
                          ', 用户: ' || rec.username ||
                          ', 获取时间: ' || rec.acquired);
      IF rec.purpose IS NOT NULL THEN
        DBMS_OUTPUT.PUT_LINE('  目的: ' || rec.purpose);
      END IF;
    END LOOP;
  END show_locks;
  
END app_lock_mgr;
/

-- 使用应用级锁
DECLARE
  v_result BOOLEAN;
BEGIN
  -- 尝试获取锁
  v_result := app_lock_mgr.acquire_lock('BATCH_PROCESS', '每日批处理作业');
  
  IF v_result THEN
    DBMS_OUTPUT.PUT_LINE('成功获取应用锁');
    
    -- 执行批处理逻辑
    DBMS_OUTPUT.PUT_LINE('执行批处理...');
    DBMS_LOCK.SLEEP(3);
    
    -- 释放锁
    v_result := app_lock_mgr.release_lock('BATCH_PROCESS');
    DBMS_OUTPUT.PUT_LINE('锁已释放');
  ELSE
    DBMS_OUTPUT.PUT_LINE('无法获取应用锁');
  END IF;
END;
/

-- 查看当前锁状态
EXEC app_lock_mgr.show_locks;

8. 锁的最佳实践

8.1 设计原则

8.1.1 最小化锁持有时间

-- 原则1: 尽快提交事务
CREATE OR REPLACE PROCEDURE process_orders AS
BEGIN
  FOR order_rec IN (
    SELECT order_id, customer_id, total_amount 
    FROM orders 
    WHERE status = 'PENDING'
  ) LOOP
    -- 处理单个订单
    UPDATE orders 
    SET status = 'PROCESSING', 
        last_updated = SYSDATE 
    WHERE order_id = order_rec.order_id;
    
    -- 立即提交,不要等待整个循环结束
    COMMIT;
    
    -- 非数据库操作(如调用外部服务)放在事务外
    -- call_external_service(order_rec.order_id);
  END LOOP;
END;
/

-- 原则2: 避免用户交互期间持有锁
CREATE OR REPLACE PROCEDURE safe_interactive_update(
  p_emp_id NUMBER,
  p_new_salary NUMBER
) AS
  v_current_salary NUMBER;
  v_emp_name VARCHAR2(100);
BEGIN
  -- 首先获取数据(不锁定)
  SELECT salary, first_name || ' ' || last_name
  INTO v_current_salary, v_emp_name
  FROM employees
  WHERE employee_id = p_emp_id;
  
  -- 显示信息给用户确认(这里不持有锁)
  DBMS_OUTPUT.PUT_LINE('员工: ' || v_emp_name);
  DBMS_OUTPUT.PUT_LINE('当前工资: ' || v_current_salary);
  DBMS_OUTPUT.PUT_LINE('新工资: ' || p_new_salary);
  
  -- 只在实际更新时才获取锁
  UPDATE employees 
  SET salary = p_new_salary,
      last_updated = SYSDATE
  WHERE employee_id = p_emp_id;
  
  COMMIT; -- 立即提交
END;
/

8.1.2 合理的锁粒度选择

-- 选择合适的锁粒度
-- 场景1: 批量更新少量记录 - 使用行级锁
UPDATE employees 
SET salary = salary * 1.1 
WHERE department_id = 10; -- 只影响几行

-- 场景2: 大量更新操作 - 考虑表级锁
LOCK TABLE temp_import_data IN EXCLUSIVE MODE;
-- 执行大量数据导入
INSERT /*+ APPEND */ INTO main_table SELECT * FROM temp_import_data;
COMMIT;

-- 场景3: 复杂报表查询 - 使用读锁或快照
SET TRANSACTION READ ONLY;
-- 执行复杂查询,确保数据一致性
SELECT /* 复杂报表查询 */ * FROM multiple_tables;
COMMIT;

8.2 性能优化技巧

8.2.1 减少锁升级

-- 避免锁升级的技巧
-- 1. 使用绑定变量
CREATE OR REPLACE PROCEDURE update_salaries_efficient(
  p_dept_id NUMBER,
  p_increase_pct NUMBER
) AS
BEGIN
  -- 使用绑定变量,减少SQL解析开销
  UPDATE employees 
  SET salary = salary * (1 + p_increase_pct / 100)
  WHERE department_id = p_dept_id;
  
  COMMIT;
END;
/

-- 2. 批量操作
CREATE OR REPLACE PROCEDURE bulk_salary_update AS
  TYPE emp_id_array IS TABLE OF employees.employee_id%TYPE;
  TYPE salary_array IS TABLE OF employees.salary%TYPE;
  
  v_emp_ids emp_id_array;
  v_new_salaries salary_array;
  
BEGIN
  -- 批量获取数据
  SELECT employee_id, salary * 1.1
  BULK COLLECT INTO v_emp_ids, v_new_salaries
  FROM employees
  WHERE department_id = 20;
  
  -- 批量更新
  FORALL i IN 1..v_emp_ids.COUNT
    UPDATE employees 
    SET salary = v_new_salaries(i)
    WHERE employee_id = v_emp_ids(i);
    
  COMMIT;
END;
/

8.2.2 并发控制优化

-- 乐观锁实现
ALTER TABLE employees ADD version_number NUMBER DEFAULT 1;

CREATE OR REPLACE PROCEDURE optimistic_update_salary(
  p_emp_id NUMBER,
  p_new_salary NUMBER,
  p_version NUMBER
) AS
  v_rows_updated NUMBER;
BEGIN
  UPDATE employees 
  SET salary = p_new_salary,
      version_number = version_number + 1,
      last_updated = SYSDATE
  WHERE employee_id = p_emp_id 
    AND version_number = p_version;
    
  v_rows_updated := SQL%ROWCOUNT;
  
  IF v_rows_updated = 0 THEN
    RAISE_APPLICATION_ERROR(-20001, 
      '数据已被其他用户修改,请刷新后重试');
  END IF;
  
  COMMIT;
END;
/

-- 使用乐观锁
DECLARE
  v_emp_id NUMBER := 100;
  v_current_salary NUMBER;
  v_current_version NUMBER;
  v_new_salary NUMBER := 8000;
BEGIN
  -- 获取当前数据和版本号
  SELECT salary, version_number
  INTO v_current_salary, v_current_version
  FROM employees
  WHERE employee_id = v_emp_id;
  
  -- 执行业务逻辑...
  
  -- 尝试更新
  optimistic_update_salary(v_emp_id, v_new_salary, v_current_version);
  
  DBMS_OUTPUT.PUT_LINE('工资更新成功');
  
EXCEPTION
  WHEN OTHERS THEN
    DBMS_OUTPUT.PUT_LINE('更新失败: ' || SQLERRM);
END;
/

8.3 监控和维护

8.3.1 定期锁健康检查

-- 创建锁健康检查报告
CREATE OR REPLACE PROCEDURE lock_health_report AS
  v_total_locks NUMBER;
  v_waiting_locks NUMBER;
  v_long_tx NUMBER;
  v_deadlocks NUMBER;
BEGIN
  DBMS_OUTPUT.PUT_LINE('=== Oracle锁健康报告 ===');
  DBMS_OUTPUT.PUT_LINE('报告时间: ' || TO_CHAR(SYSDATE, 'YYYY-MM-DD HH24:MI:SS'));
  DBMS_OUTPUT.PUT_LINE('');
  
  -- 总锁数量
  SELECT COUNT(*) INTO v_total_locks
  FROM v$lock l JOIN v$session s ON l.sid = s.sid
  WHERE s.username IS NOT NULL;
  
  DBMS_OUTPUT.PUT_LINE('1. 当前活跃锁数量: ' || v_total_locks);
  
  -- 等待锁数量
  SELECT COUNT(*) INTO v_waiting_locks FROM lock_wait_analysis;
  DBMS_OUTPUT.PUT_LINE('2. 锁等待情况: ' || v_waiting_locks);
  
  -- 长事务数量
  SELECT COUNT(*) INTO v_long_tx
  FROM v$session s JOIN v$transaction t ON s.saddr = t.ses_addr
  WHERE (SYSDATE - t.start_date) * 24 * 60 > 15; -- 超过15分钟
  
  DBMS_OUTPUT.PUT_LINE('3. 长事务数量: ' || v_long_tx || ' (>15分钟)');
  
  -- 死锁统计
  SELECT value INTO v_deadlocks
  FROM v$sysstat
  WHERE name = 'enqueue deadlocks';
  
  DBMS_OUTPUT.PUT_LINE('4. 总死锁次数: ' || v_deadlocks);
  
  -- 健康评估
  DBMS_OUTPUT.PUT_LINE('');
  DBMS_OUTPUT.PUT_LINE('=== 健康评估 ===');
  
  IF v_waiting_locks = 0 AND v_long_tx = 0 THEN
    DBMS_OUTPUT.PUT_LINE('状态: 良好 ✓');
  ELSIF v_waiting_locks < 5 AND v_long_tx < 3 THEN
    DBMS_OUTPUT.PUT_LINE('状态: 正常 ⚠');
  ELSE
    DBMS_OUTPUT.PUT_LINE('状态: 需要注意 ✗');
    DBMS_OUTPUT.PUT_LINE('建议: 检查长事务和锁等待情况');
  END IF;
  
END;
/

-- 执行健康检查
EXEC lock_health_report;

8.3.2 清理和维护脚本

-- 清理孤立锁的脚本
CREATE OR REPLACE PROCEDURE cleanup_orphaned_locks AS
  v_count NUMBER := 0;
BEGIN
  -- 查找并清理应用级锁表中的孤立锁
  FOR rec IN (
    SELECT al.lock_name, al.session_id
    FROM app_locks al
    LEFT JOIN v$session s ON al.session_id = s.sid
    WHERE s.sid IS NULL  -- 会话已不存在
  ) LOOP
    DELETE FROM app_locks 
    WHERE lock_name = rec.lock_name 
      AND session_id = rec.session_id;
    
    v_count := v_count + 1;
    DBMS_OUTPUT.PUT_LINE('清理孤立锁: ' || rec.lock_name);
  END LOOP;
  
  COMMIT;
  
  DBMS_OUTPUT.PUT_LINE('清理完成,共清理 ' || v_count || ' 个孤立锁');
END;
/

-- 定期执行清理
EXEC cleanup_orphaned_locks;

通过这个全面的Oracle锁教程,我们深入探讨了Oracle数据库中锁的各个方面,从基础概念到高级应用,从监控诊断到性能优化。理解和正确使用锁机制对于构建高性能、高并发的数据库应用至关重要。在实际应用中,应该根据具体的业务需求和并发模式来选择合适的锁策略,并建立完善的监控和维护机制。

结语
感谢您的阅读!期待您的一键三连!欢迎指正!

在这里插入图片描述


网站公告

今日签到

点亮在社区的每一天
去签到