跨越Oracle和MSSQL关系数据库开发 -- 05 流水号

来源:互联网 发布:数据漫游开了会扣费吗 编辑:程序博客网 时间:2024/04/16 22:03

1. 概述

  流水号的实现可以说是所有应用程序都要做的一件事情。但是,很多开发者并没有意识到某些实现方法会带来负面效应。而在不同的数据库中,流水号也应该使用不同的实现方式。

 

2. 使用流水号表

使用流水号表的优点之一是适用于不同数据库。但是其确定也是很明显的。

首先,让我们来看如何使用流水号表来实现自增列。本节所描述的例子也将被下面所用。

例如我们有一个证券交易流水表,在Oracle中表示为:

create table fsjour

(

  serial_no       NUMBER(12)   NOT NULL,    /*流水号  */

  branch_no       NUMBER(8)    NOT NULL,    /*营业部号*/

  fund_account    NUMBER(12)   NOT NULL,    /*资金帐号*/

  occur_balance   NUMBER(19,2) NOT NULL,    /*发生金额*/

  stock_code      char(6)      NOT NULL,    /*股票代码*/

  status_code     char(1)      NOT NULL,    /*流水状态*/

  constraint pk_fsjour primary key(serial_no)

);

为了产生该表中的serial_no列,需要下面的流水表:

create table fsjourno

(

  serial_no       NUMBER(12)   NOT NULL

);

insert into fsjourno values(0);

 

现在,有一段代码需要向交易流水表中查入一条记录:

declare

    v_serial_no     fsjour.serial_no%type;

    v_branch_no     fsjour.branch_no%type;

    v_fund_account  fsjour.fund_account%type;

    v_occur_balance fsjour.occur_balance%type;

    v_stock_code    fsjour.stock_code%type;

    v_status_code   fsjour.status_code%type;

begin

    --you can optimize it by using RETURNING

    update fsjourno set serial_no = serial_no + 1;

    select serial_no into v_serial_no from fsjourno;

   

    v_branch_no := 1001;

    v_fund_account := 1002100978;

    v_occur_balance := 12056.00;

    v_stock_code := '600571';

    v_status_code := '0';

   

    insert into fsjour

        (serial_no,       branch_no,      fund_account,

         occur_balance,   stock_code,     status_code)

      values

        (v_serial_no,     v_branch_no,    v_fund_account,

         v_occur_balance, v_stock_code,   v_status_code);

    commit;

exception

    when others then

        rollback;

end;

/

看起来该种方法实现起来不难,但是代码显得有些复杂。为了降低复杂性,聪明的开发人员想到了使用触发器,将取流水号放进触发器,这样就只要直接向流水表中插数据即可。

下面我们来看如何用触发器在Oracle中实现取流水号。

create or replace trigger trg_fsjour

  before insert on fsjour

  for each row

declare

  v_serial_no     number(12);

begin

  update fsjourno set serial_no = serial_no + 1;

  select serial_no into v_serial_no from fsjourno;

  :new.serial_no := v_serial_no;

end;

/

创建了触发器后,新增记录只要写成以下代码即可:

declare

    v_branch_no     fsjour.branch_no%type;

    v_fund_account  fsjour.fund_account%type;

    v_occur_balance fsjour.occur_balance%type;

    v_stock_code    fsjour.stock_code%type;

    v_status_code   fsjour.status_code%type;

begin

    v_branch_no := 1001;

    v_fund_account := 1002100978;

    v_occur_balance := 12056.00;

    v_stock_code := '600571';

    v_status_code := '0';

   

    insert into fsjour

        (branch_no,       fund_account,

         occur_balance,   stock_code,     status_code)

      values

        (v_branch_no,     v_fund_account,

         v_occur_balance, v_stock_code,   v_status_code);

 

    commit;

exception

    when others then

        rollback;

end;

/

现在一切看起来都很完美,这种方法实现了列的自动增长,称为“自增列”。但是上面的设计使用了表锁,因此代码无法并行执行。对于性能要求高的OLTP程序是行不通的。

开发人员马上想到不使用流水号表,而是使用max(serial_no)fsjour表中取下一个流水号。但实际上该方法还是无法增加并行性的。

实际上,对于两个并发的事务,使用max方法可能会导致其中的一个失败。由于Oracle在使用select max(serial_no) from fsjour;时并不加锁,因此两个事务可能会得到同样的流水号,在插入时后提交的事务必然会失败。

在上面的代码中有时会存在一个问题。用户可能既想使用自增列,又想获得该列的值以便其他的操作。上面的代码中完全没有出现序号列,在这种情况下怎样才能获得当前记录的序列号呢?有些人的直觉会引导他们使用select max(serial_no)的方法。这种方法是可行的,但是缺点很大,主要就是增加了事务长度,降低了性能。这里推荐的方法是使用RETURNING关键字,在不增加额外SQL的基础上就能获得。例如下面的代码所示:

SET SERVEROUTPUT ON;

declare

    v_serial_no     fsjour.serial_no%type;

    v_branch_no     fsjour.branch_no%type;

    v_fund_account  fsjour.fund_account%type;

    v_occur_balance fsjour.occur_balance%type;

    v_stock_code    fsjour.stock_code%type;

    v_status_code   fsjour.status_code%type;

begin

    v_branch_no := 1001;

    v_fund_account := 1002100978;

    v_occur_balance := 12056.00;

    v_stock_code := '600571';

    v_status_code := '0';

   

    insert into fsjour

        (branch_no,       fund_account,

         occur_balance,   stock_code,     status_code)

      values

        (v_branch_no,     v_fund_account,

         v_occur_balance, v_stock_code,   v_status_code)

    RETURNING serial_no INTO v_serial_no;

    DBMS_OUTPUT.PUT_LINE('v_serial_no=' || v_serial_no);

    commit;

exception

    when others then

        rollback;

end;

/

这种方法的应用是很广泛的,读者应该很好的掌握之。

2. MSSQL的自增列

  MSSQL推荐使用IDENTITY列来定义自增列。IDENTITY列是专门用来快速生成流水号的。

例如,前面的表结构在MSSQL可以表示为:

create table fsjour

(

  serial_no       NUMERIC(12,0) IDENTITY(1,1) primary key,    /*流水号  */

  branch_no       NUMERIC(8,0)  NOT NULL,    /*营业部号*/

  fund_account    NUMERIC(12,0) NOT NULL,    /*资金帐号*/

  occur_balance   NUMERIC(19,2) NOT NULL,    /*发生金额*/

  stock_code      CHAR(6)       NOT NULL,    /*股票代码*/

  status_code     CHAR(1)       NOT NULL     /*流水状态*/

)

go

 

此时,流水号使用的起始值是1,每次递增值也是1

当需要向该表插入数据时,不必关注serial_no列,MSSQL会自动为其生成下一个值。由于是直到COMMIT时才生成该值,因此避免了锁定问题,从而提高了并发性。

例如,在表建立后,执行下面两个语句:

insert into fsjour

  (branch_no, fund_account, occur_balance, stock_code,  status_code)

values

  (1001,      1008001,      12300.09,      '00638',     '0')

go

insert into fsjour

  (branch_no, fund_account, occur_balance, stock_code,  status_code)

values

  (1001,      1007202,      45700.18,      '00676',     '0')

go

然后查看该表内容:

select * from fsjour

结果为:

serial_no      branch_no  fund_account   occur_balance         stock_code status_code

-------------- ---------- -------------- --------------------- ---------- -----------

1              1001       1008001        12300.09              00638      0

2              1001       1007202        45700.18              00676      0

 

(2 row(s) affected)

 

很多时候,在插入一条记录后,马上需要获得它进一步进行处理。然后使用IDENTITY列后,MSSQL自动生成该值,如何在应用程序中获取它呢?

很简单。MSSQL提供了一个全局变量@@IDENTITY。该变量存放了当前SESSION中最后一个INSERT或者SELECT INTO语句所涉及的IDENTITY列的值。

因此,当执行完上面的两句插入语句后,运行:

select @@IDENTITY

可以得到值2

注意,当INSERT语句或者SELECT INTO语句影响多行记录时,该变量返回的值是其中的最后一行。

3. Oracle的自增列实现

Oracle本身没有如同MSSQL那样的自增列。Oracle通过所谓的流水号对象sequence来提供类似的功能。

流水号对象存储了一系列值,多个事务可以并发地获取,不存在任何锁定。通过流水号和触发器,我们可以模拟出与MSSQLIDENTITY列相似的功能。

继续使用前面例子中用过的fsjour表。现在,我们需要定义一个流水号对象。

create sequence seq_fsjour

  start with     1

  increment by   1

  nocache

  nocycle;

(关于sequence的详细语法,请参考Oracle文档)

然后,我们重写上面的触发器:

create or replace trigger trg_fsjour

  before insert on fsjour

  for each row

declare

  v_serial_no     number(12);

begin

  select seq_fsjour.nextval into v_serial_no from dual;

  :new.serial_no := v_serial_no;

end;

/

之后,你在插入时就不必关心serial_no列了,并且不存在并发限制问题。

同样,Oracle提供了返回当前插入语句的自动生成的列的功能,和MSSQL不同的是,Oracle使用了增强SQLreturning 语法。

例如,可以使用以下语句:

insert into fsjour

  (branch_no, fund_account, occur_balance, stock_code,  status_code)

values

  (1001,      1007202,      45700.18,      '00676',     '0')

returning serial_no into v_serial_no;

上面的与语句会在插入成功后将serial_no列的值存放进变量v_serial_no

 

必须指出,Oracle的流水号对象并不保证号码连续,即使你每次递增为1

这种不连续表现在两个方面。第一,当你获取了一个流水号,但是并没有使用该流水号向表中插入数据,那么表中的流水号将不是连续的。第二,因为数据库关闭等原因,sequence对象会丢失已经缓存的号码。下次启动后,新的号码将比当前的号码跳过几个。有时为了提高生成效率,可能将缓存设置的比较大,那么这中不连续将很明显。

4. 使用流水号的误区

这里要谈的是使用自动生成的流水号时,如何取得该流水号,以便对刚插入的记录进一步操作。如果使用不当,会出现一些潜在的问题。

现在我们首先来看一个例子,该例子是一个Java类,该类实现的功能是对含有BLOB字段的一个表进行操作。由于将代码全部写出来的话太长,因此我只列出部分关键的代码,该代码适用于ORACLE

代码中涉及的表EIA_QUEUE,其中的列EIA_XML_MESSAGE是一个BLOB字段,而列EIA_SEQ,是用序列号和触发器实现。

public class CDOLoader {

    private final String INSERT_EIA_QUEUE_SQL =

                            "INSERT INTO EIA_QUEUE(EIA_XML_MESSAGE, SOURCE)

                            VALUES(EMPTY_BLOB(),'CDOLoader')";

    private final String GET_EIA_SEQ_SQL =

                            "SELECT MAX(EIA_SEQ) FROM EIA_QUEUE";

    private final String UPDATE_SELECT_EIA_SQL =

                            "SELECT EIA_XML_MESSAGE FROM EIA_QUEUE WHERE EIA_SEQ=? FOR UPDATE";

 

    final public EIAMessage processRequest {

        //......

        conn.setAutoCommit(false);

                ps1 = conn.prepareStatement(INSERT_EIA_QUEUE_SQL);

                ps1.executeUpdate();

                stmt = conn.createStatement();

                rs = stmt.executeQuery(GET_EIA_SEQ_SQL);

                if (rs.next()) {

                            sequence = rs.getString(1);

                } else {

                            throw new SQLException("get sequence failed!");

                }

                ps2 = conn.prepareStatement(UPDATE_SELECT_EIA_SQL);

                ps2.setString(1, sequence);

                rsblob = (oracle.jdbc.OracleResultSet) ps2.executeQuery();

                if (!rsblob.next()) {

                            throw new SQLException(

                                                    "No record found for keyValue: '" + sequence + "'");

                }

                blob = (BLOB) rsblob.getBLOB(1);

                OutputStream outstream = blob.getBinaryOutputStream();

                byte[] buffer = strRequest.getBytes("UTF-8");

                outstream.write(buffer);

                outstream.close();

                conn.commit();

        //.......

    }

    //......

}

我们来看这段代码的意思:首先插入一条记录,其中的BLOB字段使用函数EMPTY_BLOB()生成一个空的字段。然后使用MAX(EIA_SEQ)取出刚才插入的流水号,然后用这个流水号查询记录并锁住这条记录,使用流对象向BLOB写入内容。

初一看,这没有什么问题,因为每次插入时,生成的流水号必然是表中最大的流水号。但是,问题就出在这里。如果只运行一个进程,那么没有大问题,但是一旦不能保证单进程,产生并发,那么问题就来了。由于序列号对象并不受事务限制,因此有可能用MAX(EIA_SEQ)取得的流水号根本不是刚才在本事务内插入的那条记录的流水号。后果是后面的流对象写错了记录。

那么,如何正确的取得刚插入的记录的流水号呢?ORACLE提供了RETURNING方法。

看下面修改过的代码:

public class CDOLoader {

    private final String INSERT_EIA_QUEUE_SQL =

    "begin INSERT INTO EIA_QUEUE(EIA_XML_MESSAGE, SOURCE) VALUES (EMPTY_BLOB(),'Loader')

                           RETURNING EIA_SEQ INTO ?;end;";

    private final String UPDATE_SELECT_EIA_SQL =

                            "SELECT EIA_XML_MESSAGE FROM EIA_QUEUE WHERE EIA_SEQ=? FOR UPDATE";

 

    final public EIAMessage processRequest {

        //......

        conn.setAutoCommit(false);

        CallableStatement cst = null;

        cst = conn.prepareCall(INSERT_EIA_QUEUE_SQL);

        cst.registerOutParameter(1, Types.INTEGER);

        cst.executeUpdate();

        sequence = cst.getString(1);

        ps2 = conn.prepareStatement(UPDATE_SELECT_EIA_SQL);

        ps2.setString(1, sequence);

        rsblob = (oracle.jdbc.OracleResultSet) ps2.executeQuery();

        if (!rsblob.next()) {

                throw new SQLException(

                                        "No record found for keyValue: '" + sequence + "'");

        }

        blob = (BLOB) rsblob.getBLOB(1);

        OutputStream outstream = blob.getBinaryOutputStream();

        byte[] buffer = strRequest.getBytes("UTF-8");

        outstream.write(buffer);

        outstream.close();

        conn.commit();

        //.......

    }

    //......

}

现在,不再把插入语句和取流水号分开来,而是合并为一句,其中用RETURNING将流水号返回给一个变量。

上面的例子是从实际的业务系统中截取出来的,由于篇幅原因未能完整列出代码,无法进行运行以便验证。而且,使用多线程来验证这个错位问题不那么简单。

下面,我以纯粹的PL/SQL代码为例,使用一个简单但是完整的例子再次进行说明。

首先我们创建一个最简单的表。其中的eia_seq是一个自增列,使用触发器和一个序列号对象实现。

create table eia_queue

(

  eia_seq         number(8)  primary key,

  eia_message     varchar2(200)

);

create sequence EIA_QUEUE_SEQUENCE

  minvalue 1

  maxvalue 999999999999999999999999999

  start with 1

  increment by 1

  nocache

  order;

CREATE OR REPLACE TRIGGER eia_queue_insert_trigger

  BEFORE INSERT ON eia_queue

  FOR EACH ROW

DECLARE

  current_id  NUMBER;

BEGIN

  SELECT eia_queue_sequence.NEXTVAL

    INTO   current_id

    FROM   DUAL;

  :NEW.eia_seq := current_id;

END;

/

接下来,我们创建一个简单的存储过程,该过程首先插入一条语句,然后使用max(eia_seq)获取其流水号,然后对此进行修改,目标结果是向数据库中新增一条记录。

create or replace procedure sp_test_eia_queue

as

  v_max_id   number(8);

begin

  insert into eia_queue(eia_message) values('This should be updated!');

  --stop and run insert independent insert SQL in anothere session, then resume

  select max(eia_seq) into v_max_id from eia_queue;

  update eia_queue set eia_message = 'believe me I can fly'

   where eia_seq = v_max_id;

  commit;

end sp_test_eia_queue;

/

这里还有另外一个独立的SQL语句,其目的也是向数据库中插入一条记录,其内容为上面存储过程中歌词的下一句:

insert into eia_queue(eia_message) values('singing in the sky');

commit;

假设读者使用PL/SQL Develpoer,如果顺序运行存储过程和插入语句,我们最后表eia_queue中的结果集是:

  EIA_SEQ EIA_MESSAGE

--------- ---------------------

        1 believe me I can fly

        2 singing in the sky

 

问题在于实际情况中有可能发生两个事务并发的情况,正如存储过程中的注释显示的那样,如果第一个插入语句正好执行完毕,第二个语句未执行之前,独立的SQL语句被执行了。那么,等存储过程运行结束后,我们发现eia_queue表中的结果集为:

  EIA_SEQ EIA_MESSAGE

--------- -----------------------

        1 This should be updated!

        2 believe me I can fly

这个结果集中的第一行恰恰是我们不愿意看到的。

 

对于初用PL/SQL Develpoer的用户,我可以提示一下,可以使用存储过程的单步调试功能来完成上述操作:运行PL/SQL Developer两个进行,一个使用单步调试,执行完第一个语句后,在另一个中运行独立的SQL语句,然后继续运行存储过程至结束即可。

 

5. 取最小未使用号码

尽管使用IDENTITY列或者流水号对象很方便,并且效率也很高,但是很多时候我们不得不使用一些其他的方法来满足业务的需求。取最大未使用号码就是其中的一个例子。

在我们不能保证递增列是连续的时候,有时候应用程序需要取最小未使用号码来作为下一个号码。例如,有时候使用流水号作为帐户号码的种子,此时号码是一种宝贵的资源,不允许浪费。

我们还是使用前面的例子。假设此时应用某种原因,我们的流水号不是连续的,在插入下一条记录时,我们必须找出最小未使用的号码。

下面是在Oracle中的语句,MSSQL的也一样:

select min(serial_no + 1)

  from fsjour

 where serial_no + 1 not in

       (select serial_no from fsjour where serial_no >= 0);

该语句需要注意的是性能问题。如果能保证语句使用索引进行检索,那么效率是可以得到保障的,否则当数据量很大时,性能极其低下。所以,建议使用索引提示保障效率。

 

6. 流水号空洞压缩

有时候,需要周期性的剔除记录中的空洞。

有趣的是,该项任务也常出现在一些高级语言中,尤其是作为面试的题目。其要求是提高效率,同时少用空间。

6.1 少量数据的操作

剔除空洞的方法很多,先介绍如何使用一个SQL语句来实现之。

假设fsjour中有以下数据:

 

    SERIAL_NO BRANCH_NO  FUND_ACCOUNT         OCCUR_BALANCE STOCK_CODE STATUS_CODE

------------- --------- ------------- --------------------- ---------- -----------

            1      1001       1083212              15401.00 600787     0

            3      1001       1000203              23402.23 600343     0

            4      1001       1000312              34573.54 000834     0

            8      1001       1001202               4937.38 000345     0

            9      1001       1003002               3247.76 600001     0

           10      1001       1008102              45455.66 600900     0

           11      1001       1007202              45700.18 00676      0

           22      1001       1007202              45700.18 00676      0

           32      1001       1007202              45700.18 00676      0

           37      1001       1007202              45700.18 00676      0

           52      1001       1007202              45700.18 00676      0

           76      1001       1007202              45700.18 00676      0

          100      1001       1020003              34355.46 000343     0

压缩的目的实际上是使流水号依次递增,那么,可以用以下语句来实现:

update fsjour f

   set serial_no = 1 + (select count(*) from fsjour t

                         where t.serial_no < f.serial_no)

 where serial_no > 1;

该语句的出发点是每一个流水号的大小应该等于比其小的记录的总数。该方法简洁明了,可惜缺点在于运行效率不高,尤其是当表中数据很大时,由于事务太大,实际上不能使用。

 

6.2 大量数据的操作

 

下面讨论一下数据很多时,如何避免事务过大。采用的方法还是上面的一个语句的方式,但是需要将表中的数据进行分段。

假设fsjour表中有多达数百万的记录数。而流水号分布在11千万之间。为了缩减事务大小,我将一万个区间中的数据作为一个事务。下面是一段PL/SQL代码,请注意其中的一些技巧。

 

 

 

declare

  v_section_count      integer;

  v_current_max        integer;

  v_current_max_temp   integer;

begin

  v_current_max := 0;

  v_section_count := 10000;

  for i in 0..1000 loop

    begin

      update fsjour f

         set serial_no = i * v_section_count + 1 +

                         (select count(*) from fsjour t

                           where t.serial_no between i * v_section_count + 1

                                                 and (i + 1) * v_section_count

                             and t.serial_no < f.serial_no)

       where serial_no between i * v_section_count + 1

                           and (i + 1) * v_section_count;

      

      update fsjour

         set serial_no = serial_no - i * v_section_count + v_current_max

       where serial_no between i * v_section_count + 1

                           and (i + 1) * v_section_count;

      

      select max(serial_no) into v_current_max_temp

        from fsjour;

       where serial_no <= (i + 1) * v_section_count;

      

      commit;

      v_current_max := nvl(v_current_max_temp, v_current_max);

    exception

      when others then

      begin

        rollback;

        exit;

      end;

    end;

  end loop;

end;

/

上述代码开起来有点复杂,我们来解读一下。

主要的思路是将流水号首先按照区间划分,在每一区间内单独压缩,然后将当前区间内的记录与前一区间的最大值进行拼接。因此,上面的第一个SQL语句实现的是在某一个区间内压缩,第二个SQL实现与前一个区间进行拼接,第三个SQL则取出当前已经压缩的所有记录的最大值,以便后一次拼接。

其中,变量v_section_count表示区间的大小,例子中取10000。变量v_current_max表示已经压缩的记录的最大值。在获取v_current_max时,使用了一个临时变量v_current_max_temp,主要是因为可能取到NULL

读者可以使用上面的数据,将v_section_count设置成10,并且将循环次数设置成10,然后执行该段代码,以验证其正确性。

上面的代码避免了大事务,不过一定要保证在执行其间没有其他人在插入数据,否则结果有误。

6.3 大量数据的其他处理方法

  前面所述代码可以完成任务,但是相信读者还有更多的好办法。例如使用一个和源表结构一样的中间表,用游标打开结果集,然后取一条插一条,插入时就可以指定没有空洞的流水号了。当然,这时也是要避免使用大事务的。

 

必须指出,压缩流水号其实是一种DBA行为。当表上有外键联接时,这种行为事实上是不可取的,必须考虑该行为是否有意义。

 

7. Java中用单例模式获取流水号

在使用Java来进行数据库操作时,有时我们采用不同于前面的流水号生成机制。我们可以不使用数据库提供的流水号生成方式,而是自己编码来实现之。这样做的好处自然是可以做到各个数据库通用。

在实现时,必须注意两个问题。一是不能使用表锁定以免影响效率,二是流水号生成必须可靠,不能出现插入冲突。而单例模式最长被用来实现以上目标。

本节中实现一个简单的流水号类,使用数据库是Oracle,涉及的表格是本文中描述的fsjour表。我们不再需要序列号对象。

以下是一个流水号生成类,为了描述方便,我没有使用分离的类来进行测试,而是将测试代码放在main函数中。我也没有将该类放在任何一个自命名包中。另外,为了简单,我也没有使用任何数据库连接池,而是每当需要使用数据库时,就生成一个连接对象,用完了马上关闭。通过这些简化,我将所有的代码集中在一个类中描述。希望读者能够理解。

读者如果要运行下面的类,必须替换其中的数据库连接信息。下面的代码中不厌其烦地在三个地方重复书写连接代码。

 

import java.sql.Connection;

import java.sql.DriverManager;

import java.sql.ResultSet;

import java.sql.SQLException;

import java.sql.Statement;

import java.sql.PreparedStatement;

 

public class SequenceGenerator {

    private int key = -1;

    private SequenceGenerator() {

       

    }

    private void readCurrentKeyFromDB() {

        Connection conn = null;

        String GET_MAX_NUMBER = "select max(serial_no) from fsjour";

        try {

            Class.forName("oracle.jdbc.driver.OracleDriver").newInstance();

            conn = DriverManager.getConnection

                    ("jdbc:oracle:thin:@127.0.0.1:1521:TESTDB",

                     "TEST_USER",

                     "TEST_PWD");

            Statement st = conn.createStatement();

            ResultSet rs = st.executeQuery(GET_MAX_NUMBER);

            if (rs.next()) {

                key = rs.getInt(1);

            } else {

                key = 0;

            }

        } catch (Exception ex) {

            ex.printStackTrace();

        } finally {

            if (conn != null) {

                try {

                    conn.close();

                } catch (SQLException se) {

                    se.printStackTrace();

                }

            }

        }

    }

    private static SequenceGenerator seq = new SequenceGenerator();

    static {

        seq.readCurrentKeyFromDB();

    }

    public static SequenceGenerator getInstance() {

        return seq;

    }

    public synchronized int getNextValue() throws Exception{

        if (key < 0) {

            throw new Exception("seed from database not available!");

        }

        return ++key;

    }

 

    public static void main(String[] args) {

        final String INSERT_FSJOUR = "insert into fsjour values(?,?,?,?,?,?)";

        Thread th1 = new Thread() {

            public void run(){

                SequenceGenerator seq = SequenceGenerator.getInstance();

                Connection conn = null;

 

                try {

                    Class.forName("oracle.jdbc.driver.OracleDriver").newInstance();

                    conn = DriverManager.getConnection

                            ("jdbc:oracle:thin:@127.0.0.1:1521:TESTDB",

                             "TEST_USER",

                             "TEST_PWD");

                    conn.setAutoCommit(false);

                    PreparedStatement st = conn.prepareStatement(INSERT_FSJOUR);

                    st.setInt(1, seq.getNextValue());

                    st.setInt(2, 1);

                    st.setInt(3, 1003);

                    st.setDouble(4, 2300.09);

                    st.setString(5, "600900");

                    st.setString(6, "0");

                    st.execute();

                    conn.commit();

                } catch (Exception ex) {

                    ex.printStackTrace();

                } finally {

                    if (conn != null) {

                        try {

                            conn.close();

                        } catch (SQLException se) {

                            se.printStackTrace();

                        }

                    }

                }

            }

        };

        Thread th2 = new Thread() {

            public void run(){

                SequenceGenerator seq = SequenceGenerator.getInstance();

                Connection conn = null;

 

                try {

                    Class.forName("oracle.jdbc.driver.OracleDriver").newInstance();

                    conn = DriverManager.getConnection

                            ("jdbc:oracle:thin:@127.0.0.1:1521:TESTDB",

                             "TEST_USER",

                             "TEST_PWD");

                    conn.setAutoCommit(false);

                    PreparedStatement st = conn.prepareStatement(INSERT_FSJOUR);

                    st.setInt(1, seq.getNextValue());

                    st.setInt(2, 2);

                    st.setInt(3, 1013);

                    st.setDouble(4, 12409.70);

                    st.setString(5, "000839");

                    st.setString(6, "0");

                    st.execute();

                    conn.commit();

                } catch (Exception ex) {

                    ex.printStackTrace();

                } finally {

                    if (conn != null) {

                        try {

                            conn.close();

                        } catch (SQLException se) {

                            se.printStackTrace();

                        }

                    }

                }

            }

        };

        th1.start();

        th2.start();

    }

}

让我们一起来阅读这个类。

这个类分成两部分。第一部分实现了单例模式。第二部分进行测试,测试时启动了两个线程。

在实现单例模式时,我采用了饿汉式生成法,一开始就从数据库中获取当前的流水号,这是通过调用函数readCurrentKeyFromDB()来实现的。

如果从数据库无法读取的记录为空,这表示fsjour表中还没有数据,这时我认为下一条记录将使用1作为流水号,否则下一条记录使用当前值加一作为流水号。

如果从数据库读取数据错误,那么我在这里简单的抛出一个异常,表明数据库的流水号不可获得,此时key的值为初始化值-1。调用不能初始化的单例对象会得到一个异常。读者可以尝试修改连接参数来得到一个连接异常,从而可以看到被抛出的异常。

main函数中,我使用同样的格式创建了两个线程。这两个线程访问单例类以获取流水号,然后分别使用一些不同的业务数据向fsjour表中插入记录。最后,启动这两个线程。

读者可能担心使用上面这种办法无法保证不产生流水号冲突。实际上这种情况很少发生,除非用户不通过该单例类生成了流水号。否则,即使该类被释放,一旦被重建,它又会重新获取数据库中最新的号码,并往后延伸。

Oraclesequence对象一样,使用单例模式方法也会导致流水号不连续问题。例如某个线程获取了一个号码,但是因为某种原因,并没有成功地向数据库中插入数据。

如果你的应用程序需要使用到多个数据库,那么,这种方法值得一试。

另外,如果你的应用程序中需要不止一个流水号生成器,那么,你完全可以只用一个类来实现之,只不过每种流水号的初始化有点不同而已,这里就不在举例了。

原创粉丝点击