你是否有通过 JDBC 访问 OceanBase(Oracle 模式)的 LOB 类型数据?那大概率会报错。
在某客户现场做 OceanBase 测试时,遇到一则报错如下。
(conn=499974) ORA-06502: PL/SQL: numeric or value error: no data found at oceanbase.DBMS_LOB.READ , line : 162, col : 1
该测试任务在报错的情况下能正常跑完(不阻塞任务流程),但经过业务侧反馈确认,若任务存在报错,则该笔交易会标识为失败。
我们通过 SQL_AUDIT 可以找到相关的查询语句,再通过报错的时间点去过滤下 gv$ob_sql_audit
视图。
select
/*+ parallel(16) */
usec_to_time(request_time),svr_ip,sid,ret_code,retry_cnt,trace_id,plan_id,plan_type,elapsed_time,queue_time,execute_time,affected_rows,partition_cnt,query_sql
from gv$ob_sql_audit
where
request_time > '2024-xx-xx 09:00:00'
and request_time < '2024-xx-xx 09:10:00'
and ret_code <> 0;
这里获取到报错 SQL 信息:
通过 trace_id 去 OBServer 日志查到报错信息:
传入的第 3 个参数,超出了 OB 中 lob 数据值得限定范围。
通过日志可知,反馈给业务端的报错确实为 CALL DBMS_LOB.READ()
语句导致,但是开发侧表示业务代码并不会执行该 SQL???
所以我们继续寻找报错原因。
继续通过报错的 SQL,在 Audit 中通过 SID(Session ID),查找这个会话上下文相关 SQL,分析是否因业务 SQL 执行导致。
select
/*+ parallel(16) */
usec_to_time(request_time),svr_ip,sid,ret_code,retry_cnt,trace_id,plan_id,plan_type,elapsed_time,queue_time,execute_time,affected_rows,partition_cnt,query_sql
from gv$ob_sql_audit
where
request_time > '2024-xx-xx 09:00:00'
and request_time < '2024-xx-xx 09:10:00'
and sid = xxxxx;
这里找到了关键 SQL!
select * from ACT_xxxxx WHERE xxxxxx_ID = ? ORDER BY NAME_ ASC;
这条语句需要获取 BLOB 对象所在字段 BYTES_
,存储 BYTES_
字段的值大小是 15007,这就跟 OBServer 日志中的报错能对应上。
call DBMS_LOB.READ('666666F777',32767,15008,NULL);
这里的 offset(15008) 大于 LOBMAXSIZE(实际 LOB 数据的存储的最大长度,这里业务表查出来是 15007),所以触发了异常。具体参考[1]
因为业务侧否认代码/框架中直接调用了 CALL DBMS_LOG.READ
语句,我们先推测该 SQL 是 OceanBase 的 JDBC 驱动触发的。
根据业务报错“no data found”搜索 JDBC 2.4.12 源码 (com/oceanbase/jdbc/Blob.java
)。
通过源码可知,业务侧执行的 SQL 是查询业务表获取 BLOB 字段数据 select * from ACT_xxxxx WHERE xxxxx_ID = ? ORDER BY NAME_ ASC;
,但需要通过调用 JDBC 驱动中的 readFromServer
的方法去获取 BLOB 数据,正是该方法下发了 call DBMS_LOB.READ()
语句。
该方法会从数据库服务器读取 BLOB 数据,并将其存储在 BLOB 对象的 data
字段中。通过一个无限循环重复调用存储过程取读取数据,直到读取完毕或遇到异常。
在处理数据时,它会检查数据长度是否超过了 maxLength,并处理可能的字符编码问题。如果遇到 "no data found" 异常,它会认为读取已经完成。
第一次 offset 是1,最后一次是 15008 (超出了业务实际字段长度值 15007 一个字节),这里的越界是符合预期的。该驱动版本中,代码逻辑上会多读一次,最后一次报错后(no data found),就不继续读取。
通过 JDBC 获取 BLOB 数据有两种方式(oceanbase-client 2.4.7 版本引入了 useLobLocatorV2
参数):
/* 1066 */ USE_LOB_LOCATOR_V2("useLobLocatorV2", Boolean.TRUE, "2.4.7", "Set to true to use lob v2, false to use the original lob", false),
当 JDBC 的参数 useLobLocatorV2=true
(默认为 TRUE)时,要查询 LOB 类型业务数据时,驱动先给你返回一个 lob locator(一般不携带实际数据),然后要读实际数据时,驱动再调用 readFromServer
方法发一个 dbms_lob.read
去读实际数据。
当 JDBC 的参数 useLobLocatorV2=false
时,不走上述逻辑,直接通过 StringReader 返回业务数据。
实际客户场景,设置 useLobLocatorV2=false
后,报错问题解决。
报错问题实际可以忽略,目前是符合驱动预期的行为,不影响业务正常读写使用。
如果业务侧不想返回报错,可以将 JDBC 的 options 加上 useLobLocatorV2=false
。
[1]
DBMS_LOB READ 的异常情况: https://www.oceanbase.com/docs/enterprise-oceanbase-database-cn-10000000000886801