OceanBase 2.2 版本在存储过程功能上做了哪些改进和突破?

OB君:好消息!「 OceanBase 2.2 版本 」正式上线官网啦!(点击阅读原文即可直接下载)OceanBase 2.2版本是成功支撑2019年天猫双11大促的稳定版本,同时也是用于TPC-C测试且荣登TPC-C性能榜首的版本。本文为 “OceanBase 2.2 解析系列” 第二篇,该系列将从事务、SQL、兼容性等维度为大家全面解读2.2版本的功能和特性。欢迎持续关注!

OceanBase 从2.0版本开始支持了存储过程(点击文章:“OceanBase 2.0——第一款支持“存储过程”的原生分布式数据库 ”回顾),并提供了MySQL和Oracle两种兼容模式。其中,2.0版本提供了较为完备的MySQL模式的存储过程功能,但对Oracle模式的存储过程功能(PL/SQL)支持还不够完全。在OceanBase 2.2版本中,我们在存储过程方面做了许多重大的改进和突破,本文将从基本功能完善、协议层的扩展和性能提升等方面介绍OceanBase 2.2版本在存储过程方面取得的新进展。

基本功能完善

OceanBase 2.0版本提供了与Oracle兼容的存储过程功能,包括各种数据类型、声明和定义语句、控制语句、异常处理等基本功能,在这些基本功能的基础上,用户可以完成各种基础的编程功能。但是因为一些功能方面的限制,在使用上会不太方便,有时需要对业务代码做一些改造才可以使用。一方面这些使用限制影响了OceanBase拓展市场的进展,另一方面也为我们进一步的发展指明了方向。我们根据业务的紧迫程度,迅速补充了大量业务中常用的功能点,提高产品的成熟度,主要有以下几个方面的改进和增强:

  • 支持子过程和子类型
  • Collection类型的完善:在原来仅提供Nested Table一种Collection的基础上,增加了Varray和Associative Array两种类型;实现了first、last、prior、next、limit、exists方法,并完善了delete方法
  • 支持 %TYPE 与 %ROWTYPE
  • 游标的完善:游标变量、游标的属性、带参数游标等功能
  • 支持自治事务
  • 支持FORALL语句
  • 新增系统包:dmbs_random、dbms_lob、dbms_metadata、dbms_output、dbms_spm、utl_raw

通过以上功能的补充和完善,OceanBase 2.2版本已经完成了对Oracle存储过程 90%以上的功能覆盖。在对外输出中我们发现,用户原本运行在Oracle上的存储过程代码绝大部分都可以无修改的在2.2版本的Oracle模式正常执行。

协议层的扩展

Oracle的PL/SQL提供了非常丰富的数据类型,这些数据类型不仅可以在PL/SQL内部调用中作为参数传递,还可以通过JDBC、ODBC等驱动和应用程序之间进行数据传递。OceanBase和驱动之间的通信协议基于MySQL协议开发,为了实现兼容Oracle的协议,必须对原来的MySQL协议进行扩展。

复合数据类型传输协议

OceanBase 2.2版本为了支持和Oracle兼容的复合数据类型参数传输功能,对MySQL的二进制协议进行了拓展,并解决了存储过程在输入输出Oracle的复合数据类型( 其中包括 Record 、VARRAY 、NestedTable 、Associative Array )的时候,协议层和server进行数据交换时候的传输协议和序列化反序列化协议问题。

驱动扩展了MySQL Protocol Type,并增加了对复杂数据类型的set接口:setArray和setObject,根据复杂类型的序列化方法对数据进行序列化并发送给server。OceanBase对外提供兼容Oracle的视图ALL_TYPES、USER_COLL_TYPES、USER_TYPE_ATTRS、ALL_TYPE_ATTRS、ALL_COLL_TYPES等,驱动根据需要发送SQL去访问这些视图拉取需要的META信息,从而能够反序列化server发送的数据。

复合数据类型传输协议的扩展,使得从OceanBase 2.2版本开始,用户可以通过应用程序直接向server发送Array和Object数据对象,或者从server读取Array和Object数据对象,大大降低了应用开发的复杂度,简化了开发和维护的工作量,如 例1 为OceanBase 2.2版本的 TPC-C测试new order代码。否则,应用需要传输Array或者Object时,必须把结构拆成多个的基础数据类型,如 例2 为DBT2的 TPCC测试new order代码。

CREATE OR REPLACE TYPE numarray IS TABLE OF NUMBER;
CREATE OR REPLACE TYPE distarray IS TABLE OF VARCHAR(24); 
CREATE OR REPLACE TYPE chararray IS TABLE OF VARCHAR(1); 

CREATE OR REPLACE PROCEDURE neworder (
  par_w_id INTEGER,
  par_d_id INTEGER,
  par_c_id INTEGER, 
  par_o_all_local INTEGER, 
  par_o_ol_cnt IN OUT BINARY_INTEGER, 
  par_w_tax OUT NUMBER, 
  par_d_tax OUT NUMBER, 
  par_o_id OUT INTEGER, 
  par_c_discount OUT NUMBER,
  par_c_credit OUT varchar2, 
  par_c_last OUT varchar2,
  par_retry IN OUT BINARY_INTEGER, 
  par_cr_date DATE,
  par_ol_i_id intarray, 
  par_ol_supply_w_id intarray, 
  par_i_price OUT numarray, 
  par_i_name OUT distarray, 
  par_s_quantity OUT intarray, 
  par_brand_generic OUT chararray, 
  par_ol_amount OUT numarray, 
  par_s_remote intarray, 
  par_ol_quantity intarray
) 
IS BEGIN
 ……
END;

例2:

                           d_id INT,           
                           c_id INT,           
                           o_all_local INT,
                           o_ol_cnt INT,           
                           ol_i_id1 INT,           
                           ol_supply_w_id1 INT,           
                           ol_quantity1 INT,           
                           ol_i_id2 INT,           
                           ol_supply_w_id2 INT,           
                           ol_quantity2 INT,           
                           ol_i_id3 INT,           
                           ol_supply_w_id3 INT,           
                           ol_quantity3 INT,           
                           ol_i_id4 INT,           
                           ol_supply_w_id4 INT,           
                           ol_quantity4 INT,           
                           ol_i_id5 INT,           
                           ol_supply_w_id5 INT,           
                           ol_quantity5 INT,           
                           ol_i_id6 INT,           
                           ol_supply_w_id6 INT,           
                           ol_quantity6 INT,           
                           ol_i_id7 INT,           
                           ol_supply_w_id7 INT,           
                           ol_quantity7 INT,           
                           ol_i_id8 INT,           
                           ol_supply_w_id8 INT,           
                           ol_quantity8 INT,           
                           ol_i_id9 INT,           
                           ol_supply_w_id9 INT,           
                           ol_quantity9 INT,           
                           ol_i_id10 INT,           
                           ol_supply_w_id10 INT,           
                           ol_quantity10 INT,           
                           ol_i_id11 INT,           
                           ol_supply_w_id11 INT,           
                           ol_quantity11 INT,           
                           ol_i_id12 INT,           
                           ol_supply_w_id12 INT,           
                           ol_quantity12 INT,           
                           ol_i_id13 INT,           
                           ol_supply_w_id13 INT,           
                           ol_quantity13 INT,           
                           ol_i_id14 INT,           
                           ol_supply_w_id14 INT,           
                           ol_quantity14 INT,           
                           ol_i_id15 INT,           
                           ol_supply_w_id15 INT,           
                           ol_quantity15 INT,
                           out rc int)
IS BEGIN
 ……
END;
/

服务端游标传输协议

Oracle可以使用REF CURSOR作为输入和输出参数,在procedure之间,以及在server和client之间进行数据传递。Oracle的服务端游标指向一个结果集的指针,在OPEN的时候生成该结果集,如有必要会同时锁行。返回给客户端CURSOR的唯一标识ID,客户端通过该ID进行服务端的FETCH和CLOSE,以及本地的缓存。而MySQL通过结果集在服务端的物化实现服务端游标,不支持基于游标的更新和滚动。
OceanBase 2.2的Oracle模式实现了服务端游标功能。服务端游标的主要作用是可以实现Client从server按需获取数据,减少数据通信量,并简化应用程序开发,如 例3 所示。而在支持这一协议前,应用只能通过Array获取SQL语句的所有结果集,当结果集非常大的时候,不仅对网络造成巨大压力,而且可能会造成驱动的内存耗尽。

例3:

CREATE OR REPLACE PROCEDURE emp_cursor(p_cursor OUT sys_refcursor)
IS
BEGIN
    open p_cursor for select * from test_emp;
END;
/

JAVA代码:CallableStatement csmt = conn.prepareCall("{call emp_cursor(?)}");
csmt.registerOutParameter(1, OracleTypes.CURSOR);
csmt.execute();
ResultSet resultSet = (ResultSet) csmt.getObject(1);
while (resultSet.next()) {
  int columnCnt = resultSet.getMetaData().getColumnCount();
  for (int j = 1; j <= columnCnt; j++) {
    System.out.println(resultSet.getMetaData().getColumnName(j) + ":"
                                           + resultSet.getString(j));
  }
}

OceanBase 的server在打开游标时会生成result set,对于作为输出参数的REF CURSOR,该结果集存放在session上,通过ID访问游标的结果集,session断开时统一释放掉。结果集里面包含了游标对应语句的物理执行计划和执行上下文,在游标关闭之前所持有的资源不会释放,并通过SQL层的get_next()方法来实现FETCH功能。

驱动通过getCursor(int)方法,可以获取server端的游标信息,而在每次Fetch的时候驱动并不会每次都跟server进行交互——如果本地有数据缓存,从本地直接返回,没有缓存的时候才会去交互一次预取一批。

性能提升

在OceanBase 2.2版本中,存储过程进行了大量的性能方面的优化,以TPC-C测试为比较基准的话,同上一版本相比,存储过程所做的优化为TPC-C带来了约30%的性能提升。

Array Binding

Array Binding是对DML语句进行批量处理的能力,这一优化在TPC-C测试中发挥了重要的作用。一条SQL在数据库中的执行过程大致可以分为“计划生成”和“执行”两个阶段,尽管我们对SQL的执行计划做了高速缓存,但找到一个合适的执行计划在整个执行过程中仍然是比较耗时的一个部分。当一组SQL的执行计划完全一样而只有执行期参数不同是,在存储过程中我们可以通过特定的语法将他们的执行做成一个批量处理的过程,此时“计划生成”只需要做一次即可。在Array Binding中,数据库首先找到需要使用的计划,然后执行该计划,并在每次执行完毕后,重新执行参数绑定(Binding)的过程。

Array Binding这一性能优化是通过存储过程中使用FORALL语句进行触发的。使用FORALL不仅可以优化SQL语句的计划生成,还大幅减少了存储过程引擎和SQL引擎之间的交互次数。在Client和Server之间的交互通过存储过程被大幅减少之后,存储过程引擎和SQL引擎之间交互的减少带来的性能提升十分可观。如 例4 所示是TPC-C测试中delivery事务的实现代码,使用了FORALL后,原本几十次的存储过程引擎和SQL引擎之间交互、以及SQL语句的计划生成和查找过程被减少到了4次。

例4:

  ware_id IN INTEGER,
  dist_id OUT intarray,
  order_id OUT intarray,
  ordcnt OUT INTEGER,
  sums OUT numarray,
  del_date IN DATE,
  carrier_id IN INTEGER,
  order_c_id OUT intarray,
  retry IN OUT BINARY_INTEGER
)
IS
  TYPE int_array IS TABLE OF BINARY_INTEGER;
  var_dist int_array := int_array();

  not_serializable EXCEPTION;
  PRAGMA EXCEPTION_INIT(not_serializable,-6235); 

BEGIN

  var_dist.EXTEND(10);
  FOR var_x IN 1..10 LOOP
    var_dist(var_x) := var_x;
  END LOOP;

  LOOP
    BEGIN
      ordcnt := 0;
      IF dist_id.count != 0 THEN
        dist_id.delete;
        order_id.delete;
      END IF;

      FORALL IDX IN 1..10
        DELETE FROM nord
          WHERE no_w_id = ware_id AND no_d_id = var_dist(IDX) AND no_o_id =
            (SELECT no_o_id FROM nord
              WHERE no_d_id = var_dist(IDX) AND no_w_id = ware_id AND rownum <= 1)
          RETURNING no_d_id, no_o_id BULK COLLECT INTO dist_id, order_id;
  
      ordcnt := SQL%ROWCOUNT;

      FORALL o in 1.. ordcnt
        UPDATE ordr SET o_carrier_id = carrier_id WHERE o_w_id = ware_id 
            AND o_d_id = dist_id(o)
            AND o_id = order_id(o)
            RETURNING o_c_id BULK COLLECT INTO order_c_id;

      FORALL o in 1.. ordcnt
        UPDATE ordl SET ol_delivery_d = del_date WHERE ol_w_id = ware_id
            AND ol_d_id = dist_id(o)
            AND ol_o_id = order_id(o)
            RETURNING sum(ol_amount) BULK COLLECT INTO sums;

      FORALL c IN 1.. ordcnt
        UPDATE cust
            SET c_balance = c_balance + sums(c), c_delivery_cnt = c_delivery_cnt + 1
            WHERE c_w_id = ware_id AND c_d_id = dist_id(c) AND c_id = order_c_id(c);

      COMMIT;

      /* No exceptions, exit*/
      EXIT;

      EXCEPTION
        WHEN not_serializable THEN
          BEGIN
            ROLLBACK;
            retry := retry + 1;
          END;
    END;      
  END LOOP; 

END delivery;
/                                               

Prepared Statement协议优化

Prepared Statement是一种二进制的请求交换协议,可以大大降低系统的交互成本。OceanBase 2.2版本不仅支持用户程序与数据库之间使用Prepared Statement协议执行SQL和存储过程,同时在存储过程引擎和SQL引擎之间的调用中使用了这一交互方式。存储过程引擎在编译的时候会对每一条SQL进行Prepare操作并获取唯一的ID,后续每次在执行的时候通过传入该ID和对应的参数,系统就可以通过高速缓存找到对应的该SQL的执行计划开始执行。相对于文本协议,这一过程省去了大部分的文本解析开销,性能获得大幅提升。

表达式计算优化

因为采用了基于LLVM的编译执行实现存储过程基础框架,存储过程中的性能消耗主要发生在表达式计算和SQL执行上,真正存储过程控制逻辑的占比非常少。所以表达式计算是性能优化的重要方向。

为了实现更好的代码可维护性,存储过程的表达式计算采用的是同SQL一样的表达式计算引擎,也就是说所有存储过程需要计算表达式的时候,会通过调用SQL引擎完成。这里除了本身大量的调用开销之外,SQL引擎的表达式计算框架采用的是基于递归的中缀计算框架。存储过程里存在大量的整数比较、自加自减、数值初始化等操作,这些表达式的计算非常简单,走一遍执行一遍SQL引擎的表达式计算流程非常耗时,严重影响了存储过程控制逻辑的运行速度。为此我们对这种简单表达式实现了一套简单的基于LLVM的计算框架,并内嵌在存储过程的执行引擎当中,一方面避免了对SQL引擎的额外调用,另一方面大幅提升了表达式计算效率。根据测试,仅这一优化就使得循环控制逻辑的性能提升百倍以上。

TCL处理优化

TCL是指事务控制语句,在存储过程里主要是COMMIT、ROLLBACK。为了实现更高的性能,2.2改掉了原本通过SQL引擎执行TCL语句的方式,而是直接通过存储过程引擎调用事务层接口实现,省掉了中间十几层的调用栈消耗。
另外,在一些特殊场景,存储过程里的TCL语句可以进行异步提交,工作线程在执行到TCL语句时,不必等待事务提交完成,而是把事务提交任务交给异步线程完成,自己去接受新的请求。

总结

OceanBase 2.2版本是全面支持Oracle PL/SQL的版本,其中的存储过程功能除了在TPC-C测试中发挥重大作用之外,在内部业务中也已经上线,经历了双11极致场景的考验,而且已经输出到了多家外部客户业务。

随着OceanBase的产品越来越成熟,存储过程将在OceanBase商业化进程中发挥越来越大的作用。尽管2.2版本的存储过程功能相比之前已经有了长足进步,但相比Oracle丰富的PL/SQL还存在很多不完善的地方,后续版本正在迅速弥补这些短板,尤其是下一阶段,我们将支持更多兼容Oracle的内建系统包,配合存储过程的功能支持,为用户业务的平滑迁移提供有力的支持。

立即申请免费体验OceanBase 2.2版本

「 OceanBase 2.2 版本 」正式上线官网啦!OceanBase 2.2版本是成功支撑2019年天猫双11大促的稳定版本,同时也是用于TPC-C测试且荣登TPC-C性能榜首的版本。OceanBase 2.2版本除了在蚂蚁金服和网商银行广泛使用外,目前也在部分金融机构中使用。

想要立即体验「OceanBase 2.2版本」?

免费获取链接:
https://oceanbase.alipay.com/download/resource

如果你在安装和使用的过程中遇到问题且希望跟OceanBase一线专家进行技术交流,您加入OceanBase技术交流钉钉群,打开钉钉搜索群号:21949783(备注:OB 2.2)

我们非常重视来自每一位开发者用户的体验和心得,希望能够获得你们的宝贵反馈。

上一篇:C#中使用Windows消息队列服务(MSMQ)简单示例


下一篇:阿里云服务器选择及使用体验!