Oracle存储过程并发读取Blob字段并生成文件

这几天上线的项目涉及从数据库中读Blob字段,落成文件,但是发现写文件太慢了,几百个文件要落好几个小时。很心疼运维兄弟加班初始化历史数据,自己就在网上搜索了下,对于读取Blob字段写入文件本身的代码,并未发现太有用的帮助文档。但是之前看过一篇并发执行存过的帖子,借此改造了下,实现并发读取Blob字段,并生成文件。写到博客中,以备以后用到,同时为程序员朋友们共享下思路(我也简单写了段 java代码测试,java的多线程也能实现相关功能)。将代码抽象后示例如下:

先建两个表,一个存储Blob字段,一个用作日志和监控并发使用

-- 存储blob字段的表
create table T_STORE_BLOG
(
  sid       VARCHAR2(32) not null,
  blogdata  BLOB,
  create_tm DATE,
  file_name VARCHAR2(200)
);
-- 日志和控制并发的表
create table T_DOWNBLOG_LOG
(
  batchid   VARCHAR2(32) not null,
  c_result  VARCHAR2(20),
  create_tm DATE,
  end_tm    DATE,
  n_count   NUMBER,
  err_msg   VARCHAR2(200)
);

存过中涉及到了directory 和创建job,因此,执行存过的用户要有directory的读、写、执行权限和创建job的权限,对于程序中注释掉的“dbms_lock.sleep”,如果希望使用,则还需要有执行dbms_lock的权限。相关脚本参考如下:

--DBA创建directory
create or replace directory DOWN_BLOB_DIR as 'G:\test';
--授权给 username
grant all on directory DOWN_BLOB_DIR to username;
--查看下是否授权
select * from user_tab_privs t
where t.table_name = 'DOWN_BLOB_DIR' and t.grantee = 'username'; 

--DBA赋予user执行  create job 的权限
grant create job to caojyster;
--DBA赋予user执行 dbms_lock 的权限
grant execute on dbms_lock to username;

主存过如下,每个并发处理的文件个数是通过修改变量 V_STEPLEN 的值控制的,可以修改此值调整并发数量,也可以修改为入参。我这里写死了,每个并发执行50个文件,如果数据库里有500个文件需要处理,主控程序会自动发起10个并发。

CREATE OR REPLACE PROCEDURE P_BATCHWRITEBLOB_MAIN(
    IDATE IN VARCHAR2, --数据日期:YYYYMMDD格式
    R_MSG OUT VARCHAR2  --返回信息
)
IS
    /*
    多线程下载 BLOB文件主控程序
    */
    V_STEPLEN  NUMBER := 50 ;  --每个并发执行的记录数,可以修改此值调整并发数量,也可以修改为入参
    V_RECCOUNT NUMBER :=0;  --满足条件的记录条数
    V_BATCHNUM NUMBER :=0;  --并发次数
    V_ORDER    NUMBER :=0;  --并发的序号
    V_BATCHSEQ VARCHAR2(30);--主控标识串
    V_ERR_MSG VARCHAR2(200);
    
    V_START_TM DATE;  --执行开始时间
    V_END_TM DATE;    --执行结束时间或到了超时的时间
    
    V_SUCC_COUNT NUMBER:=0; --成功执行完的子过程数
    V_MONI_TIME NUMBER:=0;  --持续时间,避免超时
    
BEGIN
    SELECT SYSDATE INTO V_START_TM
    FROM DUAL; 

    --查询记录条数
    SELECT COUNT(*) INTO V_RECCOUNT
    FROM T_STORE_BLOG T
    WHERE T.CREATE_TM = TO_DATE(IDATE,'YYYYMMDD');
    
    --生成一个标识串
    V_BATCHSEQ := IDATE||'-'||DBMS_RANDOM.STRING('U',10);
    DBMS_OUTPUT.put_line('V_BATCHSEQ is:'||V_BATCHSEQ);
    
    --清空此标识对应的日志
    DELETE FROM T_DOWNBLOG_LOG T
    WHERE T.BATCHID LIKE V_BATCHSEQ||'%';
    COMMIT;

    --判断需要多少并发数,如果为0则不会进入后续的循环处理
    V_BATCHNUM := CEIL(V_RECCOUNT/V_STEPLEN);
    DBMS_OUTPUT.put_line('V_BATCHNUM is:'||V_BATCHNUM);

    --循环调用子存过发起并发
    FOR V_ORDER IN 1..V_BATCHNUM LOOP
        dbms_scheduler.create_job(job_name   => dbms_scheduler.generate_job_name,
                            job_type   => 'PLSQL_BLOCK',
                            job_action => 'begin P_BATCHWRITEBLOB_SON('''||V_BATCHSEQ||''','||V_ORDER||','||V_STEPLEN||','''||IDATE||'''); end;',
                            start_date => sysdate,
                            enabled    => true,
                            comments   => 'temporary job,will be delete soon after complete!',
                            auto_drop  => true
                            );
        --DBMS_OUTPUT.put_line('action is:'||'begin P_BATCHWRITEBLOB_SON('''||V_BATCHSEQ||''','||V_ORDER||','||V_STEPLEN||','''||IDATE||'''); end;');
    END LOOP;
    
    --监控并发执行状态,超过下面设定的时间不完成,则主控程序判定超时退出,但是注意,子存过其实还在执行,所以要关注下,不一定要重跑
    WHILE( V_SUCC_COUNT < V_BATCHNUM AND V_MONI_TIME < 10 ) LOOP
        --成功的个数
        SELECT COUNT(*) INTO V_SUCC_COUNT
        FROM T_DOWNBLOG_LOG T
        WHERE T.BATCHID LIKE V_BATCHSEQ||'%'
          AND T.C_RESULT = '0';
        --持续时间
        SELECT (SYSDATE - V_START_TM)*100  --如果不设定超时时间就取消对于超时时间的限制
        INTO V_MONI_TIME
        FROM DUAL; 

        --dbms_lock.sleep(10); --sleep 10秒,需要DBA赋予本用户执行 dbms_lock 的权限
        
    END LOOP;
    
    IF V_MONI_TIME >= 10 THEN  --这个时间变量值的判断和上面的要保持一致
        SELECT SYSDATE INTO V_END_TM
        FROM DUAL; 
    
        INSERT INTO T_DOWNBLOG_LOG( BATCHID,  C_RESULT, CREATE_TM, END_TM, ERR_MSG)
        VALUES(V_BATCHSEQ, '9',V_START_TM, V_END_TM, '超时强制退出,但是子存过依旧执行中,需要关注子存过执行状态');
        COMMIT;
        RETURN; --退出
    END IF;

    SELECT SYSDATE INTO V_END_TM
    FROM DUAL; 
    INSERT INTO T_DOWNBLOG_LOG( BATCHID,  C_RESULT, CREATE_TM, END_TM, N_COUNT)VALUES(V_BATCHSEQ, '0',V_START_TM, V_END_TM, V_RECCOUNT);
    COMMIT;
EXCEPTION
  WHEN OTHERS THEN
    V_ERR_MSG := SUBSTR(SQLERRM,1,200);
    SELECT SYSDATE INTO V_END_TM
    FROM DUAL;
    INSERT INTO T_DOWNBLOG_LOG( BATCHID,  C_RESULT, CREATE_TM, END_TM, ERR_MSG)VALUES(V_BATCHSEQ, '9',V_START_TM, V_END_TM, V_ERR_MSG);
    COMMIT;
END;

子存过如下:

CREATE OR REPLACE PROCEDURE P_BATCHWRITEBLOB_SON(
    V_BATCHSEQ IN VARCHAR2, --并发标识
    V_ORDER    IN NUMBER,   --并发序号
    V_STEPLEN  IN NUMBER,   --每个批次处理的文件数量
    IDATE IN VARCHAR2      --数据日期:YYYYMMDD格式
                                     
)
IS
    /*
    多线程下载 BLOB文件子程序,实现文件下载
    */
    V_ROWNUM_BEGIN  NUMBER :=0;  --开始的rownum
    V_ROWNUM_END NUMBER :=0;     --结束的rownum
    V_BATCH_SONID  VARCHAR2(30); --子流程标识号
    V_START_TM DATE;
    V_END_TM DATE;
    
    V_LOCALDIR VARCHAR2(150);  --DIRECTORY 名称,间接映射本地路径,需要DBA创建,赋权限给当前用户
    V_FILENAME VARCHAR2(200);  --文件名
    V_UFILE UTL_FILE.FILE_TYPE;  --文件标识
    V_FBLOB  BLOB;  --BLOB 变量,承载游标中的每一个blob
    V_FILE_LEN NUMBER; --文件长度
    V_POS NUMBER;  --每次的起读位移
    V_AMOUNT BINARY_INTEGER := 32767;
    V_BUFFER RAW(32767); --缓冲区
    V_REC_COUNT NUMBER := 0; --处理的记录条数
    V_ERR_MSG VARCHAR2(200);
    
BEGIN
    SELECT SYSDATE INTO V_START_TM
    FROM DUAL; 
    
    V_LOCALDIR := 'DOWN_BLOB_DIR';    --DIRECTORY 名称,间接映射本地路径,需要DBA创建,赋权限给当前用户

    V_BATCH_SONID := V_BATCHSEQ||'-'||V_ORDER; --子流程标识号

    V_ROWNUM_BEGIN := (V_ORDER - 1 )*V_STEPLEN + 1;  --开始记录的rownum
    V_ROWNUM_END := V_ORDER * V_STEPLEN;  --结束记录的rownum
    
    --清空此标识对应的日志
    DELETE FROM T_DOWNBLOG_LOG T
    WHERE T.BATCHID = V_BATCH_SONID 
        AND T.C_RESULT = '0';
    COMMIT;
    
    --循环处理文件
    FOR REC IN (
        SELECT * FROM (
            SELECT ROWNUM AS ROWN, T.*
            FROM T_STORE_BLOG T
            WHERE T.CREATE_TM = TO_DATE(IDATE,'YYYY-MM-DD')
            ORDER BY T.SID ASC)
        WHERE ROWN >= V_ROWNUM_BEGIN AND ROWN <=V_ROWNUM_END
    )
    LOOP
        V_POS:= 1; --每次循环从第一位开始
        V_REC_COUNT := V_REC_COUNT +1;
        V_FILENAME := REC.FILE_NAME; --文件名
        V_FBLOB := REC.BLOGDATA;  --BLOG 字段
        V_FILE_LEN := DBMS_LOB.getlength(V_FBLOB);  --长度
        
        --创建文件
        V_UFILE := UTL_FILE.fopen(V_LOCALDIR, V_FILENAME,'WB',32767);
        --写文件
        WHILE V_POS < V_FILE_LEN LOOP
            DBMS_LOB.READ(V_FBLOB, V_AMOUNT, V_POS, V_BUFFER );
            UTL_FILE.PUT_RAW(V_UFILE, V_BUFFER, TRUE);
            V_POS := V_POS + V_AMOUNT;
        END LOOP;
        
        UTL_FILE.FCLOSE(V_UFILE); --关闭文件
    END LOOP;
    
    SELECT SYSDATE INTO V_END_TM
    FROM DUAL;
    INSERT INTO T_DOWNBLOG_LOG( BATCHID,  C_RESULT, CREATE_TM, END_TM, N_COUNT)VALUES(V_BATCH_SONID, '0',V_START_TM, V_END_TM, V_REC_COUNT);
    COMMIT;

EXCEPTION
  WHEN OTHERS THEN
    V_ERR_MSG := SUBSTR(SQLERRM,1,198);
    SELECT SYSDATE INTO V_END_TM
    FROM DUAL;
    IF UTL_FILE.is_open(V_UFILE) THEN
        UTL_FILE.FCLOSE(V_UFILE); --关闭文件
    END IF;
    
    INSERT INTO T_DOWNBLOG_LOG( BATCHID,  C_RESULT, CREATE_TM, END_TM,  ERR_MSG)VALUES(V_BATCH_SONID, '9', V_START_TM, V_END_TM, V_ERR_MSG);
    COMMIT;
END;

并发执行存储过程参考了如下的文档,非常感谢:

(26条消息) Oracle11gR2 并行执行存储过程_SeanData的博客-CSDN博客_oracle并行执行存储过程

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值