事务与并发控制
最后更新: 2026-04-15
作者: Oracle Team
页面目录
第八章:事务与并发控制
文档信息
- 适用版本:Oracle 12c / 18c / 19c / 21c
- 阅读时间:约 50 分钟
- 前置知识:第七章触发器
8.1 事务基础
8.1.1 ACID 特性
┌─────────────────────────────────────────────────────────────────┐
│ Oracle ACID 特性实现 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Atomicity (原子性) │
│ ──────────────────── │
│ • 事务作为最小工作单元 │
│ • COMMIT: 所有操作永久保存 │
│ • ROLLBACK: 所有操作撤销 │
│ • 由 Undo Segment 实现 │
│ │
│ Consistency (一致性) │
│ ──────────────────── │
│ • 事务前后数据保持一致状态 │
│ • 由约束、触发器、应用程序逻辑保证 │
│ • SCN (System Change Number) 保证 │
│ │
│ Isolation (隔离性) │
│ ─────────────────── │
│ • 并发事务相互隔离 │
│ • 由锁机制 + MVCC 实现 │
│ • 支持多种隔离级别 │
│ │
│ Durability (持久性) │
│ ───────────────────── │
│ • 提交的事务永不丢失 │
│ • 由 Redo Log + 控制文件保证 │
│ • 写入磁盘后持久化 │
│ │
└─────────────────────────────────────────────────────────────────┘
8.1.2 事务控制语句
-- 开始事务(隐式)
-- 第一个 DML 语句自动开始事务
-- 提交事务
COMMIT; -- 所有更改永久保存
COMMIT COMMENT 'Update employee salary'; -- 带注释提交
COMMIT WRITE NOWAIT; -- 不等待写盘(性能优先)
COMMIT WRITE WAIT; -- 等待写盘(默认)
-- 回滚事务
ROLLBACK; -- 回滚所有未提交更改
ROLLBACK TO SAVEPOINT sp1; -- 回滚到保存点
-- 创建保存点
SAVEPOINT sp1;
-- 事务示例
BEGIN
UPDATE accounts SET balance = balance - 1000 WHERE account_id = 1;
SAVEPOINT before_deposit;
UPDATE accounts SET balance = balance + 1000 WHERE account_id = 2;
-- 检查是否有错误
IF SQL%NOTFOUND THEN
ROLLBACK TO SAVEPOINT before_deposit;
DBMS_OUTPUT.PUT_LINE('Transfer failed');
ELSE
COMMIT;
DBMS_OUTPUT.PUT_LINE('Transfer successful');
END IF;
EXCEPTION
WHEN OTHERS THEN
ROLLBACK;
DBMS_OUTPUT.PUT_LINE('Error: ' || SQLERRM);
END;
/
8.1.3 隐式事务控制
-- 自动提交(SQL*Plus 环境)
SET AUTOCOMMIT ON; -- 每个 DDL/DCL 语句后自动提交
SET AUTOCOMMIT OFF; -- 手动提交(默认)
-- SQLPlus 退出时自动提交未提交事务
-- 但 DROP/TRUNCATE 立即提交
-- DDL 语句自动提交
-- CREATE, ALTER, DROP, GRANT, REVOKE, TRUNCATE
-- 这些语句执行前会隐式提交当前事务
8.2 并发问题
8.2.1 并发问题类型
| 问题 | 说明 | 示例 |
|---|---|---|
| 脏读 | 读取未提交数据 | 事务 A 修改,事务 B 读取,A 回滚 |
| 不可重复读 | 同一事务两次读取不同 | 事务 A 读取,事务 B 修改,A 再次读取 |
| 幻读 | 两次查询结果集不同 | 事务 A 查询,事务 B 插入,A 再次查询 |
| 更新丢失 | 并发更新覆盖 | 事务 A/B 读取,修改后依次写入 |
脏读:读取未提交的数据
┌─────────────────────────────────────────────────────────────────┐
│ 事务A: UPDATE emp SET salary=10000 WHERE empno=1001 │
│ (未提交) │
│ 事务B: SELECT salary FROM emp WHERE empno=1001 │
│ → 看到 10000(脏数据) │
│ 事务A: ROLLBACK │
│ → 工资回到原值,事务B读到了不存在的数据 │
└─────────────────────────────────────────────────────────────────┘
不可重复读:同一事务两次读取结果不同
┌─────────────────────────────────────────────────────────────────┐
│ 事务A: SELECT salary FROM emp WHERE empno=1001 → 5000 │
│ 事务B: UPDATE emp SET salary=6000 WHERE empno=1001 │
│ COMMIT │
│ 事务A: SELECT salary FROM emp WHERE empno=1001 → 6000 │
│ → 同一事务中数据不一致 │
└─────────────────────────────────────────────────────────────────┘
幻读:新行出现
┌─────────────────────────────────────────────────────────────────┐
│ 事务A: SELECT COUNT(*) FROM emp WHERE dept=10 → 10 │
│ 事务B: INSERT INTO emp VALUES (...) WHERE dept=10 │
│ COMMIT │
│ 事务A: SELECT COUNT(*) FROM emp WHERE dept=10 → 11 │
│ → 结果集行数不同 │
└─────────────────────────────────────────────────────────────────┘
8.3 隔离级别
8.3.1 Oracle 隔离级别
| 隔离级别 | 脏读 | 不可重复读 | 幻读 | 实现方式 |
|---|---|---|---|---|
| READ UNCOMMITTED | 可能 | 可能 | 可能 | 不支持(Oracle 不提供) |
| READ COMMITTED | 不可能 | 可能 | 可能 | 默认,读取已提交数据 |
| SERIALIZABLE | 不可能 | 不可能 | 不可能 | 悲观锁,类似 MVCC |
| READ ONLY | 不可能 | 可能 | 可能 | 只读事务 |
-- 设置事务隔离级别
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
SET TRANSACTION READ ONLY; -- 只读事务
-- READ COMMITTED(默认)
BEGIN
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
-- 每个查询看到的是查询开始时的已提交数据
END;
/
-- SERIALIZABLE
BEGIN
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
-- 整个事务看到的是事务开始时的数据快照
END;
/
-- READ ONLY
BEGIN
SET TRANSACTION READ ONLY;
-- 只读事务,不能执行 DML
END;
/
-- SCN 机制
SELECT SCN_TO_TIMESTAMP(12345678) FROM DUAL;
SELECT TIMESTAMP_TO_SCN(SYSTIMESTAMP) FROM DUAL;
8.4 锁机制
8.4.1 锁类型
| 锁类型 | 说明 | 持有时间 |
|---|---|---|
| TX (行级锁) | 锁定单行 | 事务期间 |
| TM (表级锁) | 锁定整表 | DML 期间 |
| LMD (分布式锁) | 分布式事务 | 事务期间 |
行级锁 (TX) - 多读少写场景
┌─────────────────────────────────────────────────────────────────┐
│ 事务A: SELECT * FROM emp WHERE empno=1001 FOR UPDATE; │
│ → 对第1001行加锁 │
│ 事务B: UPDATE emp SET sal=5000 WHERE empno=1001; │
│ → 等待获取锁... │
│ 事务A: COMMIT/ROLLBACK → 释放锁 │
│ → 事务B 继续执行 │
└─────────────────────────────────────────────────────────────────┘
表级锁 (TM) 模式:
• RS: Row Share - 已锁定行的表锁
• RX: Row Exclusive - DML 默认锁
• S: Share - 禁止 DML
• SRX: Share Row Exclusive - 允许查询,禁止其他锁
• X: Exclusive - 独占访问
8.4.2 锁查询
-- 查看当前锁
SELECT
session_id AS sid,
serial#,
lock_type,
mode_held,
mode_requested,
lock_id1 AS id1,
lock_id2 AS id2
FROM dba_lock
WHERE session_id IN (
SELECT sid FROM v$session WHERE username = 'HR'
);
-- 查看锁等待
SELECT
s1.username || '@' || s1.machine || ' (SID=' || s1.sid || ')' AS waiting_session,
s2.username || '@' || s2.machine || ' (SID=' || s2.sid || ')' AS blocking_session,
o1.object_name || ' (ID1=' || l1.id1 || ', ID2=' || l1.id2 || ')' AS locked_object,
l1.type AS lock_type,
DECODE(l1.lmode,
1, 'NULL', 2, 'Row-S', 3, 'Row-X',
4, 'Share', 5, 'S/Row-X', 6, 'Exclusive') AS lock_mode
FROM v$lock l1
JOIN v$session s1 ON l1.sid = s1.sid
JOIN v$lock l2 ON l1.id1 = l2.id1 AND l1.id2 = l2.id2 AND l2.request > 0
JOIN v$session s2 ON l2.sid = s2.sid
LEFT JOIN dba_objects o1 ON l1.id1 = o1.object_id;
-- 简化查看
SELECT
sid,
serial#,
username,
object_name,
object_type,
DECODE(lmode,
0, 'None', 1, 'Null', 2, 'Row Share',
3, 'Row Exclusive', 4, 'Share',
5, 'Share Row Exclusive', 6, 'Exclusive') AS lock_mode,
CTIME,
BLOCK
FROM v$session s
JOIN v$locked_object l ON s.sid = l.session_id
JOIN dba_objects o ON l.object_id = o.object_id;
8.4.3 锁冲突处理
-- 查看阻塞者
SELECT
blocker.sid AS blocker_sid,
blocker.serial# AS blocker_serial,
waiter.sid AS waiter_sid,
waiter.serial# AS waiter_serial,
o.object_name
FROM v$session blocker
JOIN v$session waiter ON blocker.sid = waiter.blocking_session
JOIN v$session s ON waiter.sid = s.sid
JOIN dba_objects o ON s.row_wait_obj# = o.object_id;
-- 杀会话(谨慎使用)
ALTER SYSTEM KILL SESSION 'sid,serial#' IMMEDIATE;
ALTER SYSTEM KILL SESSION '145,2037' IMMEDIATE;
-- 或者使用 DBMS_SESSION
BEGIN
DBMS_SESSION.MODIFY_PACKAGE_STATE(DBMS_SESSION.KILL_SESSION);
END;
/
8.4.4 FOR UPDATE 锁
-- 加锁查询
SELECT * FROM employees WHERE department_id = 50 FOR UPDATE;
-- 加锁并等待(默认)
SELECT * FROM employees WHERE department_id = 50 FOR UPDATE;
-- 加锁不等待,立即返回
SELECT * FROM employees WHERE department_id = 50 FOR UPDATE NOWAIT;
-- 等待 N 秒
SELECT * FROM employees WHERE department_id = 50 FOR UPDATE WAIT 10;
-- 锁定特定列
SELECT employee_id, first_name, salary
FROM employees
WHERE department_id = 50
FOR UPDATE OF salary; -- 只锁定 salary 列
-- FOR UPDATE SKIP LOCKED(并发友好)
SELECT * FROM employees
WHERE department_id = 50
FOR UPDATE SKIP LOCKED;
-- 用法场景:任务分配
SELECT * FROM tasks
WHERE status = 'PENDING'
ORDER BY priority
FOR UPDATE SKIP LOCKED;
-- 应用锁
BEGIN
UPDATE employees SET salary = 5000 WHERE employee_id = 100;
-- 不提交,等待应用确认
END;
/
8.5 多版本并发控制 (MVCC)
8.5.1 MVCC 原理
┌─────────────────────────────────────────────────────────────────┐
│ Oracle MVCC 工作原理 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 读取流程: │
│ ───────── │
│ 1. 事务开始获取 SCN │
│ 2. 查询数据时,根据 SCN 查找一致版本 │
│ 3. 通过 Undo 段构造历史数据 │
│ 4. 返回查询结果 │
│ │
│ 数据结构: │
│ ───────── │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Data Block │ ←── │ Undo Block │ ←── │ Undo Block │ │
│ │ (当前数据) │ │ (前镜像1) │ │ (前镜像2) │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ SCN=100 SCN=90 SCN=80 │
│ │
│ 一致性读取: │
│ 查询 SCN=95 → 读取 Data Block │
│ 查询 SCN=85 → 读取 Undo Block 1 │
│ 查询 SCN=75 → 读取 Undo Block 2 │
│ │
└─────────────────────────────────────────────────────────────────┘
8.5.2 SCN 与时间戳
-- 获取当前 SCN
SELECT DBMS_FLASHBACK.GET_SYSTEM_CHANGE_NUMBER AS scn FROM DUAL;
SELECT TIMESTAMP_TO_SCN(SYSTIMESTAMP) FROM DUAL;
-- SCN 转换为时间戳
SELECT SCN_TO_TIMESTAMP(12345678) FROM DUAL;
-- 查看事务的 SCN
SELECT
xid,
start_scn,
commit_scn,
status
FROM v$transaction;
-- 一致性读取演示
SELECT * FROM employees AS OF SCN 12345678;
SELECT * FROM employees AS OF TIMESTAMP TO_TIMESTAMP('2024-01-01 10:00:00', 'YYYY-MM-DD HH24:MI:SS');
8.6 自治事务
8.6.1 自治事务原理
┌─────────────────────────────────────────────────────────────────┐
│ 自治事务 vs 普通事务 │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 普通事务: │
│ ───────── │
│ 主事务 ──────────────────────────────── │
│ │ │
│ ├── 触发器/存储过程 (参与主事务) │
│ │ │ │
│ └── COMMIT/ROLLBACK → 全部提交/回滚 │
│ │
│ 自治事务: │
│ ───────── │
│ 主事务 ──────────────────────────────────── │
│ │ │
│ ├── 自治过程 (独立事务) │
│ │ │ │
│ │ └── COMMIT → 仅自治事务提交 │
│ │ │
│ └── COMMIT → 主事务提交 │
│ │
└─────────────────────────────────────────────────────────────────┘
8.6.2 自治事务应用
-- 创建自治事务存储过程
CREATE OR REPLACE PROCEDURE p_log_error(
p_message IN VARCHAR2
)
IS
PRAGMA AUTONOMOUS_TRANSACTION;
BEGIN
INSERT INTO error_log (
error_message, log_date, username
) VALUES (
p_message, SYSDATE, USER
);
COMMIT; -- 自治事务必须提交
END p_log_error;
/
-- 在主事务中调用
BEGIN
UPDATE employees SET salary = 5000 WHERE employee_id = 100;
-- 无论主事务是否提交,日志都会保存
p_log_error('Updated employee 100 salary');
ROLLBACK; -- 员工更新回滚,但日志已保存
END;
/
-- 自治事务触发器
CREATE OR REPLACE TRIGGER trg_emp_audit
AFTER INSERT OR UPDATE OR DELETE ON employees
FOR EACH ROW
DECLARE
PRAGMA AUTONOMOUS_TRANSACTION;
BEGIN
IF INSERTING THEN
INSERT INTO audit_trail VALUES ('INSERT', :NEW.employee_id, USER, SYSDATE);
ELSIF UPDATING THEN
INSERT INTO audit_trail VALUES ('UPDATE', :OLD.employee_id, USER, SYSDATE);
ELSIF DELETING THEN
INSERT INTO audit_trail VALUES ('DELETE', :OLD.employee_id, USER, SYSDATE);
END IF;
COMMIT;
END trg_emp_audit;
/
-- 函数中使用自治事务
CREATE OR REPLACE FUNCTION f_commit_log(p_msg VARCHAR2)
RETURN NUMBER
IS
PRAGMA AUTONOMOUS_TRANSACTION;
BEGIN
INSERT INTO app_log VALUES (p_msg, SYSDATE);
COMMIT;
RETURN 1;
EXCEPTION
WHEN OTHERS THEN
RETURN 0;
END f_commit_log;
/
8.7 锁优化建议
8.7.1 减少锁竞争
-- 1. 减小事务范围
BEGIN
-- 锁定后立即处理,避免长事务
FOR rec IN (SELECT employee_id FROM employees WHERE status = 'NEW' FOR UPDATE)
LOOP
UPDATE employees SET status = 'PROCESSED' WHERE employee_id = rec.employee_id;
END LOOP;
COMMIT;
END;
/
-- 2. 使用批量操作
BEGIN
FORALL i IN 1..1000
UPDATE employees SET salary = salary * 1.1 WHERE employee_id = i;
COMMIT;
END;
/
-- 3. 顺序访问
-- 多个事务按相同顺序访问资源,避免死锁
-- 4. 使用序列而非应用生成ID
CREATE SEQUENCE emp_seq;
INSERT INTO employees VALUES (emp_seq.NEXTVAL, ...);
-- 避免应用生成ID导致的主键冲突
-- 5. 分区减少锁竞争
-- 将大表按时间分区,删除旧数据时锁定特定分区
ALTER TABLE sales TRUNCATE PARTITION p_2023_q1;
8.7.2 死锁检测与处理
-- Oracle 自动检测并处理死锁
-- 会选择一个事务回滚(代价最小)
-- 查看死锁信息
SELECT * FROM v$session WHERE event = 'enq: TX - contention';
-- 查看最近死锁图
SELECT * FROM v$lock WHERE type = 'TX' AND request != 0;
-- 死锁追踪参数
SHOW PARAMETER deadlock
-- alert.log 中的死锁信息
-- ORA-00060: deadlock detected while waiting for resource
8.8 本章小结
✅ ACID 特性:原子性、一致性、隔离性、持久性
✅ 并发问题:脏读、不可重复读、幻读、更新丢失
✅ 隔离级别:READ COMMITTED(默认)、SERIALIZABLE、READ ONLY
✅ 锁类型:TX(行级)、TM(表级)
✅ MVCC:通过 Undo 段实现一致性读取
✅ 自治事务:独立于主事务的子事务
✅ 锁优化:减小事务范围、批量操作、顺序访问
📖 下章预告:性能优化实战
下一步:学习 第九章:性能优化实战