ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

KingbaseES PLSQL异常处理实战:从Oracle迁移到生产级设计

KingbaseES PLSQL异常处理实战:从Oracle迁移到生产级设计 凌晨两点一套从 Oracle 迁移到 KingbaseES 的批处理又在第二天早上准时死在了日志里。前端同事把报错截图发过来屏幕上只有一行干巴巴的ORA-06502: 数值转换错误跑了十几万行的存储过程愣是看不出来是哪个步骤出了岔子。后来我花了整整一天把整套 PLSQL 异常处理逻辑翻出来重写加错误日志表、定义业务异常码、用保存点切分事务边界才真正把这类问题压住。那晚之后我得了一个很深的结论——PLSQL 异常处理在 KingbaseES 里绝不是会写 WHEN OTHERS 就行它同时是语法机制、业务设计、性能因素和故障定位工具。这篇文章就把我在这几年 KingbaseES 项目里攒下的异常处理经验完整拆开讲清楚。1. 先拆机制异常在 KingbaseES 里要经历哪几层才被捕获1.1 三类异常预定义、非预定义和用户自定义KingbaseES 在 Oracle 兼容模式下保留了三类异常的定义方式很多从 Oracle 迁移过来的老存储过程基本不用改语法但能分清三类异常的触发来源对后面写处理逻辑非常关键。预定义异常是 PLSQL 引擎内置的那一批比如NO_DATA_FOUND、TOO_MANY_ROWS、DUP_VAL_ON_INDEX、VALUE_ERROR这类最常见的基础错误。它们的特征是不需要你先声明直接就能写在WHEN子句里。SELECT INTO 查不到数据会触发的就是NO_DATA_FOUND插入主键重复触发的就是DUP_VAL_ON_INDEX字符串截断、数字溢出这类则归到VALUE_ERROR。非预定义异常是数据库返回了错误码但 PLSQL 引擎不认的外部错误。比如你在存储过程里操作一个序列或触发器抛出的底层错误直接写WHEN OTHERS能接住但想精确捕获就得在DECLARE部分先声明一个异常变量再用PRAGMA EXCEPTION_INIT把它和指定错误码绑在一起。这个方法在需要精确区分多种数据库层异常时非常有用。用户自定义异常则完全由开发者自己声明、自己抛出。比如账户余额不足合同编号不存在这类业务状态本质上不是数据库错误而是业务规则被破坏手动RAISE e_business_exception抛出来再在WHEN子句里接住能让业务逻辑的表达清晰很多。1.2 异常传播链路从 SQL 引擎到调用方的完整路径理解机制的核心是搞清楚异常从产生到被捕获中间到底经过哪几步。以最常见的SELECT ... INTO单行查询为例当执行结果为空时KingbaseES 的 SQL 引擎并不会直接返回一个数据库错误给客户端而是先在 PLSQL 内部触发NO_DATA_FOUND异常接着运行时开始在当前块查找有没有对应的WHEN NO_DATA_FOUND处理器。找到了跳到处理器执行没找到就把异常往外层块传。一层层往外传直到最外层块也没接住才作为数据库错误返回给调用方。这个逐层外抛的传播链路决定了异常边界就是块边界。很多人以为在存储过程里随便包一层BEGIN...EXCEPTION...END就能把异常限死在当前块这个理解不完整——内层块没接住的异常照样会向上冒泡。真正能隔离的只有两种情况要么当前块完整捕获并消化掉要么当前块重新抛出一个新异常把原来的传播链路打断。在异常处理块内部可以通过SQLCODE和SQLERRM拿到当前错误的编码和文本信息。这两个函数在 KingbaseES 兼容模式下和 Oracle 的用法基本一致但强烈建议不要在业务逻辑里反解析SQLERRM文本来做条件分支因为错误信息文本可能因为数据库版本或字符集产生差异最好是基于预定义异常、EXCEPTION_INIT绑定或自定义异常码来判断业务分支。1.3 用户自定义异常与 RAISE_APPLICATION_ERROR 的边界Oracle 兼容模式里最常用的业务异常抛出方式是RAISE_APPLICATION_ERROR它的特点是带着一个明确的错误号和自定义文本能让调用方在日志里一眼看出业务语义。KingbaseES 支持的错误号范围同样是-20000到-20999这一千个号码本质上就是你手里的业务错误码池。但这里有个容易犯的错把RAISE_APPLICATION_ERROR当成万能的RAISE。如果你的异常只在本块内部处理并不需要抛给上层或外部应用那直接用RAISE一个自定义异常就够了完全没必要给它编个全球唯一的错误号。反过来如果这个错误要让外层存储过程、Java 服务或者 PLSQL Developer 客户端看到RAISE_APPLICATION_ERROR更合适因为外面拿到的错误信息是清晰的业务描述而不是一个没头没尾的 PLSQL 异常对象。下面这段代码演示了一个典型组合先声明非预定义异常并绑定-20001后续再通过RAISE_APPLICATION_ERROR让错误文本直接透出到客户端。DECLARE v_cnt INT; e_table_missing EXCEPTION; PRAGMA EXCEPTION_INIT(e_table_missing, -20001); BEGIN SELECT COUNT(*) INTO v_cnt FROM user_tables WHERE table_name T_XX; IF v_cnt 0 THEN RAISE_APPLICATION_ERROR(-20001, T_XX 表不存在禁止继续执行); END IF; EXCEPTION WHEN e_table_missing THEN DBMS_OUTPUT.PUT_LINE(发现表缺失先建表再执行业务); RAISE_APPLICATION_ERROR(-20001, T_XX 表不存在禁止继续执行); WHEN OTHERS THEN DBMS_OUTPUT.PUT_LINE(SQLERRM); RAISE; END;1.4 和 Oracle 不一样的地方迁移后必须先做异常注入测试KingbaseES 底层源自 PostgreSQL所以在非兼容模式下很多行为跟 Oracle 并不一样。即便你启用了 Oracle 兼容模式一些边界情况仍然跟原生 Oracle 有微妙差异比如字符集和隐式转换规则不同VALUE_ERROR的触发时机可能和预期不一致。事务默认行为、自动提交的开关策略不同异常后是否需要显式回滚要单独验证。嵌套 SQL 里某个子查询抛出的异常在某些版本下错误码映射到SQLCODE的值可能跟 Oracle 不完全一致。所以我给团队立的规矩是Oracle 迁移到 KingbaseES 的项目每个存储过程上线前必须做一轮异常注入测试——人为构造NO_DATA_FOUND、DUP_VAL_ON_INDEX、TOO_MANY_ROWS、类型转换失败这四类典型异常再走一遍捕获逻辑确认异常传播行为和错误码都符合预期。不懂这一条很容易在迁移后遇到测试环境一切正常生产跑批偶尔异常且查不到原因的尴尬局面。2. WHEN 子句怎么组织才不坑捕获顺序、嵌套块和重抛2.1 处理器顺序与最外层 DEFAULTS陷阱异常处理块里的多个WHEN子句是按代码书写顺序从上到下匹配的匹配成功后就直接进入对应处理器后面的不再执行。所以一个隐藏得很深的坑是有人把WHEN OTHERS写在前面等于给所有异常建立了一个截胡通道后面辛辛苦苦写的精确处理器全成了摆设。正确做法永远是WHEN OTHERS放最后并且必须保证它只做兜底不吞细节。我见过最离谱的代码是把WHEN OTHERS THEN NULL;放在中间位置然后整个存储过程无论出什么错都静默成功。这种代码在单测里可能跑得很顺但丢到生产环境中要么数据对不上要么上游系统永远收不到失败信号最后排查成本成倍增长。2.2 嵌套块是异常隔离的最小单元嵌套块是异常处理里最值得用心设计的地方。它的价值在于能把可能出错的局部操作和核心业务主流程分离开让局部失败不影响整体流程。举个例子一个跑批任务要对一批订单做处理每条订单可能有单独的脏数据导致插入失败如果整个跑批只包一个大异常块一条脏数据就中断全部任务如果对单条处理逻辑用嵌套块包住异常就能被限在局部外层继续处理下一条。DECLARE v_order_id NUMBER : 10086; BEGIN BEGIN INSERT INTO t_order_detail(order_id, status) VALUES (v_order_id, NEW); EXCEPTION WHEN DUP_VAL_ON_INDEX THEN -- 幂等处理该订单明细已存在记录后继续 DBMS_OUTPUT.PUT_LINE(订单明细重复 || v_order_id); END; -- 继续后续业务 DBMS_OUTPUT.PUT_LINE(主流程继续执行); END;嵌套块使用的核心法则是内层块只捕获你知道怎么处理的异常未知异常继续往外抛让真正能决策的外层去处理。不要为了隔离就把所有异常都吞在内层那样反而丢掉了异常向上传播的价值。2.3 重抛异常把上下文留全异常处理里最容易被低估的操作是RAISE重抛。说白了就是把当前异常按原样扔回上层让外层块继续处理或记录。很多团队的业务日志只记录了错误码和错误文本真正定位问题时发现根本不知道是在哪个函数、哪个步骤抛出来的这就是因为少了重抛时机之前的上下文拼装。我的习惯是内层块在捕获异常后先把当前模块名、操作名、关键参数拼进 SQLERRM再RAISE出去。比如这样EXCEPTION WHEN OTHERS THEN RAISE_APPLICATION_ERROR(-20099, 在生成对账单模块处理订单[ || v_order_id || ]时异常: || SQLERRM);这样上层拿到错误信息就能直接定位到业务环节而不用靠猜。记得RAISE_APPLICATION_ERROR的文本有长度限制别把没完没了的调试信息往里塞保留关键参数就够了。2.4 保存点批量场景下的事务切分利器异常处理和事务控制经常是绑在一起的。一个循环里做多条 DML只要有一条失败整个事务应该怎么处理全回滚数据一致但效率低全提交前边成功的脏数据可能悬空。实际项目中我常用SAVEPOINT切分每个循环开始前建一个保存点单条处理失败时回滚到这个保存点把当前这条恢复原样但不影响前面已经提交或已处理的记录。BEGIN FOR rec IN (SELECT id FROM t_temp_job WHERE status PENDING) LOOP SAVEPOINT sp_item; BEGIN -- 处理单条业务 UPDATE t_main SET status DONE WHERE id rec.id; INSERT INTO t_detail(id, result) VALUES (rec.id, OK); EXCEPTION WHEN OTHERS THEN ROLLBACK TO sp_item; -- 记录错误并继续下一条 proc_log_error(BATCH, SQLCODE, SQLERRM, rec.id); END; END LOOP; END;这里要注意ROLLBACK TO sp_item只是回滚到保存点事务本身还没结束最后仍然需要显式COMMIT或最外层统一提交。保存点不是用来代替事务边界的它只是帮你把一条失败和整批失败恰当地解耦。2.5 空异常块可能让问题彻底隐形WHEN ... THEN NULL;这种写法如果不是刻意为之几乎都是坑。特别是在数据跑批和接口同步场景里一个空异常块等于主动放弃了错误信号。我见过有同事为了让流程别断给所有可能出错的查询都套了空捕获结果月底对账差了几万块完全不知道哪一步丢了。如果确实想忽略某个已知的、不影响结果的异常至少要在NULL之前写一条日志哪怕是DBMS_OUTPUT或调试表让这个异常发生过的事实留下痕迹。 否则这个系统长期运行后会出现数据莫名其妙少了但没人知道原因的情况。3. 别把异常当 if 用性能开销到底在哪儿3.1 抛出异常不是一个廉价分支很多人习惯用异常来做业务分支判断比如查不到记录就RAISE然后在外面WHEN NO_DATA_FOUND里处理默认值。这种写法从语法上没毛病但性能上是典型的反面教材。异常抛出在运行时内部不是简单的跳转它涉及异常对象的构造、错误栈的展开、向调用链上层逐级传递最后还要靠异常处理器接住。这一整套动作的开销比正常的一个IF判断高几个数量级。在单次调用、低频操作上你感觉不出来但在循环次数过万的批处理里用异常当分支控制整体耗时能明显拉开差距。如果只是处理没有数据这种情况完全可以直接用SELECT COUNT(*)或IF判断或者用游标%NOTFOUND状态来区分。毕竟NO_DATA_FOUND的语义就是帮你把空结果这个状态从查询语句里传出来的你把它原本要表达的东西丢掉了反而去抛异常属于本末倒置。3.2 批量处理中逐行异常的成本与应对批量 INSERT 或 UPDATE 一旦设计成每行包一个BEGIN...EXCEPTION...END性能就被绑死了。因为每一行都是一次独立的异常块切换每条脏数据都要走一遍异常产生、捕获、记录、回滚的完整链条。数据量到十万级以上差距会从秒级拉到分钟级。我的做法是分两步走第一步能提前校验的尽量提前做。比如跑批前先跑一段预检 SQL把会主键冲突、外键违例、类型非法的数据筛出来直接写到错误表里干净的干净数据用普通的批量语句插入不触任何异常块。第二步确实避免不了运行时异常的数据也别逐行做保存点。用临时表先收集脏数据批量结束后统一回滚、统一记录。真要做到逐行可控也要先用BULK COLLECT限批比如 500 行一批再在批量执行外层套异常处理尽量避免每行都走完整套异常机制。3.3 动态 SQL 中的异常与绑定变量动态 SQL 是 PLSQL 项目里异常处理的高发区。为什么因为拼出来的 SQL 如果字段类型、字符串引号、日期格式处理得不严谨运行时生成的真实语句很可能跟你写测试时用的语句不一样触发VALUE_ERROR或INVALID_NUMBER这类异常的概率非常高。在 KingbaseES 里写动态 SQL能用绑定变量就别把值直接拼进语句。绑定变量一方面减少重复解析开销另一方面也让异常现场更容易定位——因为错误信息里带着的是占位符你能直接知道是哪个参数出了问题。BEGIN EXECUTE IMMEDIATE UPDATE t_user SET age :1 WHERE id :2 USING 30, 1001; EXCEPTION WHEN OTHERS THEN proc_log_error(DYNAMIC_SQL, SQLCODE, SQLERRM, UPDATE t_user); END;在异常块里记录动态 SQL 的语句文本和分析参数比只记SQLERRM有用得多。因为很多动态 SQL 的错误只有拿到实际执行时的语句才能复现。3.4 怎么判断你的异常处理拖慢了系统如果怀疑异常处理拖了性能可以打开数据库的慢 SQL 日志或参数追踪观察大量消耗时间的语句是不是集中在某个频繁调用的存储过程上。还有一个更直接的手段在存储过程出入口用CLOCK_TIMESTAMP()记录自己的耗时按模块拆开对比。比如我发现某个批处理里WHEN OTHERS分支被大量触发但业务日志根本看不到明细先打开记录分析被吞掉的异常类型再对症下药——通常是数据源有脏数据或者类型转换逻辑不严谨。4. 三个线上事故的完整排查链路从客户端卡到发布失败4.1 场景一PLSQL Developer 查询卡死是数据库异常还是事务没结束这是社区里很常见的现象在 PLSQL Developer 里跑一个存储过程或长时间查询结果界面一直转圈表现就是卡死。很多人第一反应是数据库死锁了或者PLSQL 工具有问题但我接触的实际案例里很大一部分是会话持有了一个未提交的事务。正常情况下存储过程做的 DML 操作在过程结束时如果没有显式COMMIT这个事务会一直保持打开。你在 PLSQL Developer 里跑完过程不提交就接着发下一条更新语句这条语句会被前一条事务留下的行锁挡住表现为查询没有返回。真正卡住的往往不是单条 SQL 的执行而是锁等待链。排查链路我建议按这个顺序第一步先在数据库侧看会话状态。用系统视图看一下当前连接都在干什么SELECT pid, state, wait_event_type, wait_event, query FROM sys_stat_activity WHERE state idle;这里能看到类似transactionid、tuple等锁等待信息。第二步查锁表关系定位谁锁了谁。找到持有锁的会话后看它事务的起始时间通常就是某个异常块里只做了ROLLBACK或者干脆没提交。第三步检查客户端连接配置。PLSQL Developer 的 Tools 菜单里有个 Preferences里面可以设置每次连接是否自动提交DML 操作是否需要在结束后手动 commit。很多之前卡死其实只是没提交导致的锁等待。这个场景的核心教训是编写存储过程时要在异常处理块里对事务状态做明确决策——这条存储过程是完整提交还是部分回滚还是把回滚权力交给外层。决不能留下异常后啥都没做事务悬空的状态。4.2 场景二数据迁移时不断报类型转换异常但单条 SQL 测试没有问题另一个高频问题是迁移 Oracle 数据到 KingbaseES 时写好的 INSERT 语句在工具里单条执行正常但放到存储过程里跑批就报VALUE_ERROR或ORA-06502类的数值转换错误。原因通常不是语句本身的问题而是源数据的某一行存在异常值比如日期字段里混进了空字符串或者数字字段里带空格又或者某个字段实际上超长。单条测试时你用的测试数据没有问题但真实源数据里总有一两条漏网之鱼。排这种问题最笨也最有效的办法是把异常捕获范围细化到能定位到具体记录。我的做法是临时在异常处理块里把当前游标对应的主键值记录下来再结合SQLERRM定位是哪一行、哪一列出的错。BEGIN FOR rec IN (SELECT * FROM v_import_src) LOOP BEGIN INSERT INTO t_target(..., dt) VALUES (..., TO_DATE(rec.str_date, YYYY-MM-DD)); EXCEPTION WHEN OTHERS THEN proc_log_error(IMPORT, SQLCODE, SQLERRM, 业务键: || rec.biz_key || 日期: || rec.str_date); END; END LOOP; END;配一个错误日志表把业务键和具体异常值记录下来基本一轮就能找全脏数据。这类问题的根治手段是导入前先做数据质量清洗和类型预检而不是在异常处理里层层打补丁。4.3 场景三KingbaseES 发布到 SuperMap iServer 时连接反复失败GIS 项目里很常见的场景是把 KingbaseES 作为空间数据库发布到 SuperMap iServer 上。现象通常是发布地图或服务的时候iServer 日志里出现数据库连接失败、连接池耗尽或事务超时之类的异常过一会儿又自动恢复。如果你在 iServer 侧能看到连接被拒绝连接池已满这类信号问题往往不在发布本身而在于数据源连接池里的会话被长时间占用了。这类占用经常来自 PLSQL 里的一个问题触发器或存储过程在更新空间表时抛了异常但异常块没有把事务回滚导致持有大量锁和事务信息的会话一直不释放。iServer 侧反复重试但连接池里的连接都被卡住表现就是发布失败或时好时坏。排查链路建议这样走先在 KingbaseES 侧看连接数是否满然后查活动事务持续时长。如果发现某个会话的state是idle in transaction且事务开始时间很早基本就是它锁住了数据源连接。接着查是不是触发器或表函数里抛了异常。因为 iServer 发布时的很多读写操作是长事务一旦内部 PLSQL 逻辑抛异常而没回滚整个连接就悬住了。解决办法给相关触发器、存储过程补上完备的异常处理在所有 DML 事务路径上明确COMMIT或ROLLBACK同时把 iServer 数据源连接池参数调小测试帮助快速暴露问题。这个案例说明一件事PLSQL 异常处理不能只站在我这个存储过程能不能正确返回的角度设计还要站在我的异常会不会把上层连接池拖死的角度设计。不能被捕获的异常、悬空事务比异常本身更致命。4.4 从这些事故里提炼的定位模板综合上面三个场景远程排查 PLSQL 异常时我几乎固定按这个顺序走一、先看现象发生在客户端、数据库层还是连接池层二、从数据库活动视图抓当前会话快照三、看锁等待和事务状态四、查错误日志表里有没有留下调用上下文五、最小化复现——用同一份脏数据或同一个参数在测试环境跑一遍。这套模板的价值不在于每一步多高深而在于它强制你从代码对不对转向数据对不对、事务悬不悬、连接池会不会被耗干三个更常见的根因。5. 可落地的生产级异常处理设计5.1 错误日志表排查事故的第一手证据生产环境里任何没有被持久化的异常信息都是没有价值的。因为你不可能总是即时坐在 PLSQL Developer 前盯着输出窗口。所以我在项目里都会先建一张错误日志表让所有异常处理块统一往这里写。CREATE TABLE t_err_log ( log_id NUMBER, module_name VARCHAR2(200), err_code NUMBER, err_msg VARCHAR2(4000), exec_sql CLOB, biz_key VARCHAR2(200), occur_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, CONSTRAINT pk_err_log PRIMARY KEY (log_id) ); CREATE SEQUENCE seq_err_log;配套一个统一的日志插入存储过程CREATE OR REPLACE PROCEDURE proc_log_error( p_module IN VARCHAR2, p_code IN NUMBER, p_msg IN VARCHAR2, p_sql IN VARCHAR2 DEFAULT NULL, p_bizkey IN VARCHAR2 DEFAULT NULL ) AS PRAGMA AUTONOMOUS_TRANSACTION; BEGIN INSERT INTO t_err_log(log_id, module_name, err_code, err_msg, exec_sql, biz_key) VALUES (seq_err_log.NEXTVAL, p_module, p_code, p_msg, p_sql, p_bizkey); COMMIT; END;这里必须用自治事务否则如果主事务要回滚错误日志也会被一起回滚掉这等于什么都没记。自治事务让日志插入独立提交不会因为主流程失败而丢失。5.2 错误码规范比错误信息文本更可靠错误信息文本在不同版本、不同字符集下可能被改写但错误码是稳定的。所以业务异常码值得像接口协议一样去规范管理。我的习惯是把RAISE_APPLICATION_ERROR支持的-20000到-20999这一千个号码按模块和错误类型分段错误码段业务模块说明-20001 ~ -20050客户数据模块客户不存在、身份证重复、黑名单等-20051 ~ -20100订单模块订单状态不合法、订单已关闭、库存不足等-20101 ~ -20150对账模块源数据缺失、对账不平、重复对账-20151 ~ -20200接口同步模块第三方响应超时、报文解析失败、签名校验失败-29900 ~ -29999全局兜底异常链上下文、未知错误这样做的好处是上层应用收到一个-20023不用查文档就能猜到大概是对账模块出了问题。团队内部沟通、监控告警都方便很多。5.3 分层异常设计数据层、业务层、接口层各管各的事情异常处理最忌讳的就是所有异常处理逻辑堆在一个大块里。我推荐按三层去设计数据层异常主要面对主键冲突、非空约束、外键违例、类型转换失败。这一层通常直接捕获后转成明确的数据问题比如DUP_VAL_ON_INDEX就转成XX 表主键冲突请检查重复数据再向上抛或记录。业务层异常主要面对业务规则被破坏比如订单状态不允许更新、客户不在白名单里。这类问题不是数据库错误应当用自定义异常或RAISE_APPLICATION_ERROR抛出带了明确的业务错误码。接口层异常这个层面面向外部调用方包括 Java 服务、iServer、报表工具。它要处理的不只是异常捕获还有事务回调、日志记录、给调用方的结构化错误码。所有未知异常在这里统一兜底绝不能把内部异常堆栈直接丢给前端但也不能在不留任何解释的情况下返回成功。三者之间通过RAISE或RAISE_APPLICATION_ERROR传递上下文让错误信号沿着数据层→业务层→接口层的方向一层层变得更接近人话也更便于追踪。5.4 不同业务场景的差异化处理策略批处理、接口服务、报表查询对异常处理的诉求完全不一样不能在项目里一套模板套到底。批处理场景跑批、日终的核心诉求是尽量别中断。单条数据出错不应当让整个批次失败但也不能无条件吞掉要设置阈值比如同一批次错误达到 50 条或者错误比例超过 1%就中止跑批并发告警避免跑了一晚上结果全错完了还在继续。接口同步场景的核心诉求是状态明确。上游调用一次同步接口如果出现中间异常不能稀里糊涂地部分成功。要保证事务边界清晰要么全成功要么全失败并返回明确的错误码让调用方决定是否需要重发。报表查询场景的核心诉求是别因为一条脏数据让整个报表出不来。上游数据质量问题很常见这种情况我一般建议在查询层捕获可预期的脏数据异常转到一个默认值或错误行但保留原始数据明细方便后续数据治理。5.5 进阶让异常处理服务于可观测性最后分享一个我现在做项目的习惯异常处理不只服务于捕获和报错还服务于监控。错误日志表里每个模块的错误量、错误类型分布、高频错误语句都是运维监控的重要指标。我会给关键模块加一个按小时统计错误数的汇总视图配合告警规则一旦某个模块错误率突然升高不用等用户反馈系统自己就暴露出来。这个思路的本质是把异常处理从写代码时保证不出错升级为就算错了也让它以最短路径暴露出来。从我个人的实际体验看一个 PLSQL 项目最怕的不是异常多而是异常发生后没有留下足够多的上下文信息让排查变成大海捞针。所以写每个异常块之前先问自己一个问题如果这个异常发生在半夜第二天我来看日志我能从这里顺藤摸瓜找到根因吗如果答案是不能那这个处理方式就要再改改。
返回列表