资讯动态

PL/SQL例外处理:构建Oracle数据库的程序免疫系统

发布时间:2026/10/16 22:50:32 来源:尧图企业网站定制
1. 这不是“报错处理”而是PL/SQL程序的免疫系统你写完一个存储过程测试数据跑通了心里刚松一口气用户那边就打来电话“一查张三的订单就卡死”“导出2023年全量数据直接报错退出”。你翻日志看到一行ORA-01422: exact fetch returns more than requested number of rows再看代码里那句SELECT ... INTO ...——没错它只准备接1条结果数据库塞给你3条。这不是bug是程序没长“免疫细胞”。PL/SQL里的例外Exception机制从来就不是教你怎么把错误信息打印出来而是让你在数据异常、资源不足、逻辑冲突、权限缺失这些真实生产现场中依然能守住业务底线该回滚的事务不残留脏数据该返回友好提示的绝不抛裸错该降级执行的自动切换备用路径。我带过的几个项目里90%以上的线上故障追溯下来问题不在SQL写得对不对而在于例外分支压根没写或者写了但只写了WHEN OTHERS THEN NULL;——这等于给程序装了个假安全气囊撞上墙时它根本不会弹。关键词“Oracle数据库”“PL/SQL程序设计”“例外”三个词连在一起指向的是一套成熟、严谨、不容妥协的错误应对范式。它不依赖外部监控告警来事后补救而是在每一行PL/SQL代码执行前就预设好所有可能的“失序点”。比如你调用一个银行转账过程UPDATE account SET balance balance - 1000 WHERE id 1001执行成功但紧接着UPDATE account SET balance balance 1000 WHERE id 1002因为账户被锁住失败了——这时候不是让第一笔扣款悬在那里而是整个事务必须原子性回滚。这个“必须”就是由例外处理块强制保障的。它适合谁不是只适合DBA或资深开发。如果你正在写一个每天要处理5万条物流单据的入库过程如果你负责维护一个对接三方支付接口的结算模块甚至如果你只是个刚学完基础语法、正准备写第一个真实业务存储过程的新人——例外处理都是你绕不开的第一道实战门槛。跳过它你的代码就像没装刹车的自行车平路能骑下坡必翻。而掌握它意味着你能把“程序可能出错”这个模糊担忧转化成可枚举、可捕获、可响应、可记录的确定性动作。接下来的内容我会带你从最常踩的坑开始一层层拆开PL/SQL例外机制的肌肉与神经告诉你每种写法背后的真实代价和收益。2. 例外机制的设计逻辑为什么不能只靠WHEN OTHERS2.1 Oracle的例外分类预定义、非预定义、用户自定义三者不可混用PL/SQL的例外不是笼统的“错误集合”而是分层明确的三类实体每类解决不同维度的问题强行混用会直接导致逻辑失控。预定义例外Predefined Exceptions是Oracle内核为你打包好的高频故障场景共21个全部有固定名称和唯一错误号。比如NO_DATA_FOUNDORA-01403、TOO_MANY_ROWSORA-01422、ZERO_DIVIDEORA-01476。它们的特点是只要触发对应条件Oracle自动抛出无需你声明你捕获时也必须用它的标准名不能自己起名。我见过最典型的错误是有人写WHEN NO_ROW_FOUND THEN ...——少了个S编译直接报错因为Oracle根本不认识这个标识符。非预定义例外Non-predefined Exceptions解决的是另外约2000个标准Oracle错误号如ORA-00001唯一约束冲突、ORA-01400插入空值到非空列它们没有内置名称但你必须给它们“注册”一个名字才能在代码里捕获。注册方式是通过PRAGMA EXCEPTION_INIT编译指令将一个自定义的例外变量绑定到具体错误号。这步绝不能省否则你只能靠WHEN OTHERS捕获后再用SQLCODE去判断——这等于放弃编译期检查把问题拖到运行时。用户自定义例外User-defined Exceptions则完全由你掌控用于表达业务规则层面的异常比如“客户信用额度不足”“订单状态不允许取消”。它不对应任何Oracle错误号需要你先DECLARE一个例外变量再在逻辑中用RAISE主动抛出。它的价值在于把“业务语义”注入到错误流中让调用方能根据WHEN insufficient_credit THEN ...这样的分支做出精准的业务响应而不是面对一个模糊的ORA-20001干瞪眼。这三类例外的并存本质是Oracle对“错误来源”的分层治理内核级故障预定义、数据库约束级故障非预定义、业务规则级故障自定义。混淆它们就像把交通违章、车辆故障、司机醉驾全归为“开车出事”后续处理必然粗放失效。2.2 WHEN OTHERS的陷阱它不是兜底而是最后防线WHEN OTHERS THEN看似万能实则是PL/SQL里最危险的语法糖。它的危险性不在于功能而在于它诱使开发者放弃思考“具体会出什么错”。我参与过一个电商库存扣减过程的重构。原代码是这样的CREATE OR REPLACE PROCEDURE reduce_stock(p_sku VARCHAR2, p_qty NUMBER) IS BEGIN UPDATE inventory SET qty qty - p_qty WHERE sku p_sku; IF SQL%ROWCOUNT 0 THEN RAISE_APPLICATION_ERROR(-20001, SKU not found: || p_sku); END IF; COMMIT; EXCEPTION WHEN OTHERS THEN ROLLBACK; DBMS_OUTPUT.PUT_LINE(Error: || SQLERRM); END;表面看有回滚、有输出很“完整”。但上线后发现两个致命问题第一当并发高时UPDATE可能因行锁等待超时ORA-00054WHEN OTHERS捕获后只打印日志用户看到的是“操作失败”却不知道是系统忙还是数据错第二DBMS_OUTPUT.PUT_LINE在生产环境通常被禁用日志实际没留下任何痕迹排查时只能靠猜。正确的做法是把WHEN OTHERS当作真正的“最后防线”只做最基础的兜底动作并确保前面已覆盖所有可预见的主干路径EXCEPTION -- 业务主干错误SKU不存在、库存不足 WHEN no_sku_found THEN log_error(SKU_NOT_FOUND, p_sku); RAISE_APPLICATION_ERROR(-20001, 商品不存在); WHEN insufficient_stock THEN log_error(INSUFF_STOCK, p_sku || : || p_qty); RAISE_APPLICATION_ERROR(-20002, 库存不足); -- 数据库约束错误唯一索引冲突、外键违规 WHEN dup_val_on_index THEN log_error(DUP_INDEX, p_sku); RAISE_APPLICATION_ERROR(-20003, 数据重复请勿重试); -- 系统级错误锁超时、空间不足需人工介入 WHEN others THEN log_error(UNEXPECTED_ERROR, SQLCODE || SQLCODE || , SQLERRM || SQLERRM); -- 发送告警邮件或写入专用错误表 send_alert_to_dba(SQLCODE, SQLERRM); RAISE; -- 重新抛出让上层知道这里崩了 END;关键变化有三点第一WHEN OTHERS里不再NULL或简单DBMS_OUTPUT而是强制记录结构化错误日志并触发告警第二RAISE;无参数表示原样重抛保留原始错误堆栈避免掩盖真相第三它被放在所有具体分支之后真正成为“兜不住时才启动的应急协议”。2.3 例外传播链子程序出错父程序如何知情PL/SQL的例外不是孤立事件而是一条贯穿调用栈的传播链。理解这条链才能避免“子过程挂了主过程还傻乎乎往下走”的灾难。假设你有一个主过程process_order它内部调用validate_payment和update_inventory两个子过程。如果update_inventory因为ORA-01400插入空值抛出例外而它自己没处理这个例外会立即向上“冒泡”传给process_order。此时process_order的执行点会停在update_inventory调用的那一行其后的所有代码比如发通知、更新订单状态都不会执行。但问题来了如果process_order自己也没写例外处理块这个例外会继续向上传播最终到达匿名块或客户端。这时整个事务默认回滚——这是Oracle的保障。可如果process_order写了WHEN OTHERS THEN NULL;那就糟了例外被无声吞掉process_order后续代码照常执行订单状态被标记为“已处理”但库存根本没扣减。用户付了钱货却没发出去这就是典型的“静默失败”。因此例外传播链的设计原则是每个过程都应明确自己的责任边界。底层工具过程如update_inventory应专注数据操作抛出清晰的例外中间业务过程如process_order应捕获并翻译这些例外决定是重试、降级还是终止顶层入口过程如API触发的匿名块则负责最终的事务控制和用户反馈。一个经过实战检验的模式是“三层例外策略”数据层包内私有过程只抛出预定义或自定义例外不处理让错误暴露服务层公有过程捕获数据层例外转换为业务语义例外如RAISE insufficient_stock;并记录操作上下文应用层调用方捕获服务层例外执行事务回滚/提交并返回用户友好的HTTP状态码或消息。这种分层让每个环节只关心自己该管的事既避免了例外处理的重复劳动又保证了错误信息在传递中不失真、不衰减。3. 核心实操从声明到捕获手把手构建健壮例外流3.1 预定义例外21个高频场景必须烂熟于心Oracle预定义的21个例外覆盖了95%以上的常见执行失败。它们不是可选项而是你写PL/SQL时的“肌肉记忆”。下面按使用频率排序给出每个的触发条件、典型场景、避坑要点和实操示例。例外名称触发条件典型场景避坑要点实操示例NO_DATA_FOUNDSELECT ... INTO未返回任何行查询用户信息、获取配置参数✅ 必须捕获否则程序中断❌ 不可用于UPDATE/DELETE的SQL%NOTFOUND判断BEGIN SELECT email INTO v_email FROM users WHERE id p_user_id; EXCEPTION WHEN NO_DATA_FOUND THEN v_email : defaultcompany.com; END;TOO_MANY_ROWSSELECT ... INTO返回多于1行用ID查单条记录但ID非唯一✅ 检查WHERE条件是否足够精确❌ 不能用SQL%ROWCOUNT 1替代因为INTO本身已报错BEGIN SELECT name INTO v_name FROM products WHERE category p_cat; -- 若category非唯一必报此错 EXCEPTION WHEN TOO_MANY_ROWS THEN log_warning(Multiple products in category ZERO_DIVIDE数值除以零计算折扣率、平均单价✅ 除法前加IF divisor ! 0 THEN防御❌ 不要等报错再处理性能损耗大IF p_total_qty 0 THEN v_avg_price : p_total_amt / p_total_qty; ELSE v_avg_price : 0; END IF;DUP_VAL_ON_INDEX插入/更新违反唯一索引约束用户注册填重邮箱、订单号重复✅ 捕获后引导用户修改❌ 不要WHEN OTHERS吞掉丢失具体原因INSERT INTO users(email, name) VALUES(p_email, p_name); EXCEPTION WHEN DUP_VAL_ON_INDEX THEN RAISE_APPLICATION_ERROR(-20001, 邮箱已被注册);INVALID_NUMBER字符串转数字失败TO_NUMBER解析前端传来的价格字符串✅ 用REGEXP_LIKE(p_str, ^[0-9]\.?[0-9]*$)预校验❌ 避免在循环内频繁调用TO_NUMBERBEGIN v_num : TO_NUMBER(p_input); EXCEPTION WHEN INVALID_NUMBER THEN log_error(Invalid number format: 其余16个如CURSOR_ALREADY_OPEN,PROGRAM_ERROR,STORAGE_ERROR虽出现频率低但一旦触发往往指向严重问题。例如STORAGE_ERRORORA-06500表示PL/SQL引擎内存耗尽这通常意味着你的游标没关闭、集合过大或递归过深必须立刻检查资源管理而不是简单捕获。实操心得别死记硬背21个名字。我的方法是在开发环境建一个测试包把每个预定义例外都写一个最小复现案例亲自触发一次。比如专门写一段代码去制造LOGIN_DENIEDORA-01017虽然生产环境不会用错密码但这个过程会让你深刻理解“连接级错误”和“执行级错误”的区别。只有亲手“捅过娄子”才能在真正遇到时秒级定位。3.2 非预定义例外用PRAGMA绑定错误号让隐性错误显性化Oracle有2000多个错误号但只有21个有预定义名字。剩下那些比如ORA-00001唯一约束、ORA-01400非空约束、ORA-02291外键约束你必须手动“认领”它们否则只能靠WHEN OTHERS这个笨办法。PRAGMA EXCEPTION_INIT就是这个“认领”工具。它的语法是PRAGMA EXCEPTION_INIT(例外变量名, 错误号)。注意错误号是负数Oracle约定且必须在声明段完成。来看一个真实案例某物流系统要求“同一运单号在同一时间只能有一条调度记录”。数据库加了唯一索引(waybill_no, schedule_time)。当并发插入相同运单号时会报ORA-00001。如果不用PRAGMA代码会这样写-- ❌ 错误示范用WHEN OTHERS硬扛 INSERT INTO dispatch_records(waybill_no, schedule_time, driver_id) VALUES(p_waybill, p_time, p_driver); EXCEPTION WHEN OTHERS THEN IF SQLCODE -1 THEN -- 手动判断错误号 RAISE_APPLICATION_ERROR(-20010, 运单号 || p_waybill || 在该时间已存在调度); ELSE RAISE; -- 其他错误原样抛出 END IF;问题在于SQLCODE -1这个判断编译器无法检查写错了比如写成-01或1要到运行时才发现而且所有非预定义错误都挤在OTHERS里逻辑混乱。正确做法是提前声明并绑定CREATE OR REPLACE PROCEDURE create_dispatch(p_waybill VARCHAR2, p_time DATE, p_driver NUMBER) IS -- 声明一个例外变量 e_dup_waybill EXCEPTION; -- 用PRAGMA将其绑定到ORA-00001 PRAGMA EXCEPTION_INIT(e_dup_waybill, -1); BEGIN INSERT INTO dispatch_records(waybill_no, schedule_time, driver_id) VALUES(p_waybill, p_time, p_driver); EXCEPTION -- 现在可以像预定义例外一样用名字捕获 WHEN e_dup_waybill THEN log_error(DUP_WAYBILL, p_waybill || | || TO_CHAR(p_time, YYYYMMDDHH24MISS)); RAISE_APPLICATION_ERROR(-20010, 运单号 || p_waybill || 在该时间已存在调度); WHEN OTHERS THEN log_error(UNEXPECTED, SQLCODE||SQLCODE||, SQLERRM||SQLERRM); RAISE; END;参数计算与选择逻辑为什么选-1因为ORA-00001的错误号就是-1Oracle文档明确列出。PRAGMA的作用就是在编译期建立“变量名 ↔ 错误号”的映射这样运行时Oracle就知道当抛出ORA-00001时应该激活e_dup_waybill这个分支。这个映射是静态的、确定的比运行时IF SQLCODE -1判断更安全、更高效。实操技巧把常用非预定义错误号做成一个“速查表”贴在IDE边栏。我常用的有-1(唯一约束),-1400(非空约束),-2291(外键不存在),-2292(子表有记录父表不能删)。每次建新表加约束顺手就把对应的PRAGMA声明写好养成习惯。3.3 用户自定义例外用RAISE_APPLICATION_ERROR注入业务灵魂预定义和非预定义例外解决的是“数据库能不能干”而用户自定义例外解决的是“业务该不该干”。它是把业务规则编码进错误流的核心手段。RAISE_APPLICATION_ERROR是创建它的唯一官方途径语法为RAISE_APPLICATION_ERROR(错误号, 错误消息[, 是否回滚])。错误号必须在-20000到-20999之间这是Oracle留给用户的专属区间。来看一个库存管理的典型场景扣减库存时不仅要检查数据库里有没有这条记录还要检查业务规则——比如“预售商品库存不能低于100件”。这个规则数据库约束无法表达必须由PL/SQL逻辑判断。CREATE OR REPLACE PROCEDURE safe_reduce_stock(p_sku VARCHAR2, p_qty NUMBER) IS v_current_qty NUMBER; -- 声明业务例外 e_low_stock EXCEPTION; e_invalid_qty EXCEPTION; BEGIN -- 1. 查询当前库存 SELECT qty INTO v_current_qty FROM inventory WHERE sku p_sku; -- 2. 业务规则检查扣减量不能为负 IF p_qty 0 THEN RAISE e_invalid_qty; -- 主动抛出自定义例外 END IF; -- 3. 业务规则检查扣减后库存不能低于安全线 IF v_current_qty - p_qty 100 THEN RAISE e_low_stock; -- 主动抛出自定义例外 END IF; -- 4. 执行扣减 UPDATE inventory SET qty qty - p_qty WHERE sku p_sku; EXCEPTION WHEN NO_DATA_FOUND THEN RAISE_APPLICATION_ERROR(-20001, 商品SKU不存在: || p_sku); WHEN e_invalid_qty THEN RAISE_APPLICATION_ERROR(-20002, 扣减数量必须大于0); WHEN e_low_stock THEN RAISE_APPLICATION_ERROR(-20003, 扣减后库存将低于安全线100当前库存: || v_current_qty); WHEN OTHERS THEN RAISE_APPLICATION_ERROR(-20004, 库存扣减失败: || SQLERRM); END;为什么必须用RAISE_APPLICATION_ERROR而不是RAISERAISE只能抛出已声明的例外变量如e_low_stock但它抛出的错误号是随机的Oracle内部分配调用方无法根据错误号做区分处理。而RAISE_APPLICATION_ERROR强制你指定一个确定的、范围内的错误号如-20003这样上层Java程序就能用if (sqlEx.getErrorCode() -20003)精准识别“库存不足”并触发相应的业务流程如自动创建补货单。实操心得自定义例外号要遵循“业务域错误类型”编码规则。比如库存域用-201xx-20101库存不足-20102SKU不存在订单域用-202xx-20201订单状态非法-20202支付超时。这样当DBA在日志里看到-20101不用看代码就知道是库存问题极大提升排障效率。4. 完整实操一个电商下单过程的例外处理全流程4.1 需求与架构下单不是一条SQL而是一条风险链我们来实现一个真实的电商下单核心过程place_order。它不是简单的INSERT INTO orders而是一条包含4个关键环节的风险链库存预占检查并锁定商品库存防止超卖价格校验确认下单时价格与当前价一致防价格篡改用户余额扣减从用户钱包扣款涉及资金安全订单创建生成订单主表及明细每个环节都可能失败且失败的影响范围不同库存检查失败可以提示用户“缺货”价格校验失败应拒绝下单并记录欺诈嫌疑余额扣减失败必须回滚所有前置操作否则钱扣了但单没下成。因此这个过程的例外处理必须是分层捕获、分级响应、统一回滚。4.2 代码实现嵌套例外块与事务控制CREATE OR REPLACE PROCEDURE place_order( p_user_id NUMBER, p_sku_list sku_array, -- 自定义数组类型存(SKU, qty)对 p_order_id OUT NUMBER ) IS -- 1. 声明所有可能用到的例外 e_sku_not_found EXCEPTION; e_insufficient_stock EXCEPTION; e_price_mismatch EXCEPTION; e_insufficient_fund EXCEPTION; e_system_error EXCEPTION; -- 2. PRAGMA绑定非预定义错误 PRAGMA EXCEPTION_INIT(e_sku_not_found, -1403); -- NO_DATA_FOUND的别名更语义化 PRAGMA EXCEPTION_INIT(e_insufficient_fund, -1); -- ORA-00001这里借用来表示余额唯一约束冲突实际中可能用其他方式 -- 3. 局部变量 v_total_amount NUMBER : 0; v_current_price NUMBER; v_user_balance NUMBER; -- 4. 内部过程校验单个SKU PROCEDURE validate_sku(p_sku VARCHAR2, p_req_qty NUMBER) IS v_stock_qty NUMBER; BEGIN -- 查询库存 SELECT qty INTO v_stock_qty FROM inventory WHERE sku p_sku FOR UPDATE NOWAIT; IF v_stock_qty p_req_qty THEN RAISE e_insufficient_stock; END IF; EXCEPTION WHEN NO_DATA_FOUND THEN RAISE e_sku_not_found; WHEN TIMEOUT_ON_RESOURCE THEN -- 行锁等待超时 RAISE_APPLICATION_ERROR(-20050, 库存系统繁忙请稍后重试); END validate_sku; BEGIN -- 开启事务隐式PL/SQL块即事务边界 -- 步骤1遍历SKU列表逐个校验库存 FOR i IN 1..p_sku_list.COUNT LOOP validate_sku(p_sku_list(i).sku, p_sku_list(i).qty); -- 同时查询当前价格累加总价 SELECT price INTO v_current_price FROM products WHERE sku p_sku_list(i).sku; v_total_amount : v_total_amount (v_current_price * p_sku_list(i).qty); END LOOP; -- 步骤2校验用户余额 SELECT balance INTO v_user_balance FROM user_wallet WHERE user_id p_user_id FOR UPDATE; IF v_user_balance v_total_amount THEN RAISE e_insufficient_fund; END IF; -- 步骤3扣减余额注意这里用UPDATE不是INSERT UPDATE user_wallet SET balance balance - v_total_amount WHERE user_id p_user_id; -- 步骤4创建订单主表 INSERT INTO orders(order_id, user_id, total_amount, status, created_at) VALUES(seq_orders.NEXTVAL, p_user_id, v_total_amount, PAID, SYSDATE) RETURNING order_id INTO p_order_id; -- 步骤5创建订单明细循环插入 FOR i IN 1..p_sku_list.COUNT LOOP INSERT INTO order_items(order_id, sku, qty, price) VALUES(p_order_id, p_sku_list(i).sku, p_sku_list(i).qty, v_current_price); END LOOP; -- 所有步骤成功提交事务 COMMIT; EXCEPTION -- 分级捕获业务规则错误返回用户友好提示 WHEN e_sku_not_found THEN ROLLBACK; RAISE_APPLICATION_ERROR(-20101, 商品不存在请检查SKU); WHEN e_insufficient_stock THEN ROLLBACK; RAISE_APPLICATION_ERROR(-20102, 库存不足无法下单); WHEN e_price_mismatch THEN ROLLBACK; RAISE_APPLICATION_ERROR(-20103, 商品价格已变动请刷新页面重试); WHEN e_insufficient_fund THEN ROLLBACK; RAISE_APPLICATION_ERROR(-20104, 账户余额不足); -- 系统级错误记录详细日志通知运维 WHEN OTHERS THEN ROLLBACK; -- 记录结构化错误日志写入error_log表 INSERT INTO error_log(error_code, error_msg, module, params, created_at) VALUES(SQLCODE, SQLERRM, PLACE_ORDER, user_id||p_user_id||, sku_list||p_sku_list.COUNT, SYSDATE); COMMIT; -- 确保日志写入 RAISE_APPLICATION_ERROR(-20999, 系统繁忙请稍后重试。错误代码 || SQLCODE); END place_order;关键设计解析FOR UPDATE NOWAIT在库存查询时立即加锁避免长时间等待。如果锁被占用直接抛TIMEOUT_ON_RESOURCE而不是让用户等几秒再失败。嵌套过程validate_sku将库存校验封装成独立单元其内部的例外如NO_DATA_FOUND被转换为更高层的e_sku_not_found实现了错误语义的升级。RETURNING INTO在INSERT时直接获取生成的order_id避免额外查询减少竞态风险。ROLLBACK的位置每个业务例外分支都显式ROLLBACK确保事务干净结束OTHERS分支也ROLLBACK但额外做了日志落盘COMMIT日志表这是生产环境的黄金准则——错误日志必须持久化不能随主事务回滚而丢失。4.3 测试用例用真实错误场景验证例外流光写代码不够必须用真实错误场景“锤炼”它。以下是我在测试环境中必跑的5个用例SKU不存在传入一个数据库里根本没有的p_sku。预期捕获e_sku_not_found抛出-20101日志表无记录因为ROLLBACK前没到日志写入点。库存不足传入一个库存为50的SKU但请求扣减100。预期捕获e_insufficient_stock抛出-20102。余额不足用户余额100但订单总价200。预期捕获e_insufficient_fund抛出-20104。并发超卖用两个会话同时对同一SKU下单且库存刚好等于总需求如库存10两个会话各要5。预期第二个会话在SELECT ... FOR UPDATE NOWAIT时立即报ORA-00054进入OTHERS分支写入error_log表抛出-20999。价格篡改在validate_sku查询价格后手动UPDATE products SET price price * 2 WHERE sku xxx再执行扣减。预期UPDATE user_wallet成功但INSERT orders后的INSERT order_items会因价格不一致失败此处简化实际应在扣款前二次校验触发OTHERS。实操心得测试时一定要打开SQL*Plus的SET SERVEROUTPUT ON并在关键点加DBMS_OUTPUT.PUT_LINE。但记住这只是调试手段生产代码里必须删除所有DBMS_OUTPUT因为它会严重拖慢性能。真正的日志只应写入error_log这样的专用表并配好分区和清理策略。5. 常见问题与独家避坑指南5.1 “例外被吞了”——WHEN OTHERS THEN NULL的七宗罪这是PL/SQL新手最常犯、也是最致命的错误。WHEN OTHERS THEN NULL;看似“安静”实则埋下无数雷。罪状一掩盖真实错误让问题雪球越滚越大某次线上故障订单状态卡在“支付中”查日志一片空白。最后发现一个关键的UPDATE语句后面跟着WHEN OTHERS THEN NULL;。而那个UPDATE失败的真实原因是ORA-01555快照过旧因为事务太长。NULL吞掉后程序继续执行把订单状态设为“已支付”但库存根本没扣。用户付了钱货没发客服电话被打爆。罪状二破坏事务一致性BEGIN INSERT INTO t1 VALUES(1); INSERT INTO t2 VALUES(2); -- 这里报ORA-00001 EXCEPTION WHEN OTHERS THEN NULL; -- 吞掉错误 END;执行后t1里多了一条记录t2没插入事务没回滚。数据处于不一致状态。罪状三让调试变成玄学当你在WHEN OTHERS里只写NULL等于主动放弃了所有线索。下次出问题你只能靠SELECT * FROM v$session_longops猜哪个会话在干啥效率极低。解决方案永远用WHEN OTHERS THEN做三件事记录INSERT INTO error_log ...写入结构化日志告警调用UTL_MAIL.SEND或写入消息队列触发运维告警重抛RAISE;无参数把错误原样抛给上层让调用方决定如何收场。提示如果业务逻辑确实要求“忽略某个特定错误”请务必用PRAGMA EXCEPTION_INIT绑定它然后写WHEN specific_exception THEN NULL;。例如WHEN e_no_such_file THEN NULL;文件不存在就跳过这比WHEN OTHERS安全一万倍。5.2 “回滚没生效”——SAVEPOINT与部分回滚的精确控制有时你不想整个事务回滚只想回滚到某个中间点。比如下单过程库存预占失败了但之前的价格校验和用户查询已经成功这些查询操作其实可以保留只需回滚预占部分。这时SAVEPOINT就是你的手术刀。BEGIN -- 步骤1查询用户信息无副作用可保留 SELECT name INTO v_user_name FROM users WHERE id p_user_id; -- 步骤2设置保存点标记“从此处开始的操作可撤销” SAVEPOINT before_stock_check; -- 步骤3库存预占可能失败 SELECT qty INTO v_stock FROM inventory WHERE sku p_sku FOR UPDATE NOWAIT; IF v_stock p_qty THEN RAISE e_insufficient_stock; END IF; -- 步骤4如果走到这里说明库存OK继续后续... ... EXCEPTION WHEN e_insufficient_stock THEN -- 只回滚到保存点用户查询结果v_user_name依然有效 ROLLBACK TO before_stock_check; -- 然后可以尝试其他逻辑比如推荐替代商品 recommend_alternative(p_sku); END;关键点SAVEPOINT名字是局部的只在当前PL/SQL块内有效ROLLBACK TO savepoint_name不会结束事务后续仍可COMMIT或ROLLBACK整个事务一个块内可设多个保存点形成嵌套回滚能力。注意SAVEPOINT不能跨过程调用。如果你在子过程中设了保存点父过程无法ROLLBACK TO它。所以保存点的粒度要和过程职责对齐。5.3 “日志写不进去”——错误日志表的事务陷阱很多团队会建一个error_log表来记录错误但常犯一个致命错误在主事务里INSERT INTO error_log然后ROLLBACK。结果是错误日志和主数据一起被回滚了什么都没留下。正确姿势自治事务Autonomous Transaction自治事务是一个

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑