唯一且连续的编码的实现


    在信息系统的开发过程中,我们通常要处理各种各样的编码问题,有的教科书甚至将编码设计提升为系统设计阶段的一个重要步骤。此处所谓的编码,是“编号”的近义词,而非有时我们所说的“编写代码”,它通常作为对象的标识存储在数据库中。

    既然是标识,那么编码应当是唯一的,事实上,唯一性是比较容易实现的:自动编号类型的字段(如果支持的话)、一定精度的当前时间等等。此时,我们不考虑这种编码是否具备一定的含义(通常这是编码设计的一个原则),我们在唯一性基础之上考虑另外一种常见的需求:编码应当是连续的,即不重号且不断号。

    这种需求下的编码中无疑都存在一个由简单整数数字组成的部分,如下图所示。由于其它部分可以通过某种方式补齐,为了简化问题,我们可以假设要实现的编码只有该整数数字部分,即我们要实现的编码序列是{1, 2, 3, ..., 10, 11, 12, ...}。

    图1 唯一且连续的编码示例

    下面的篇幅将讨论在后台数据库为SQL Server 2000的情况下的解决方案和可能遇到的一些问题。

    假设SQL Server 2000中已经存在一个名为tEsTdB的数据库,执行以下脚本——    

ContractedBlock.gif ExpandedBlockStart.gif 代码1 创建用户表Invoice的SQL语句
 1None.gifif exists (select * from dbo.sysobjects where id = object_id(N'[dbo].[Invoice]'and 
 2None.gif
 3None.gifOBJECTPROPERTY(id, N'IsUserTable'= 1)
 4None.gifdrop table [dbo].[Invoice]
 5None.gifGO
 6None.gif
 7None.gifCREATE TABLE [dbo].[Invoice] (
 8None.gif    [InvoiceID] [bigint] IDENTITY (11NOT NULL ,
 9None.gif    [InvoiceNo] [bigint] NOT NULL ,
10None.gif    [Type] [varchar] (50) COLLATE Chinese_PRC_CI_AS NULL ,
11None.gif    [Status] [varchar] (50) COLLATE Chinese_PRC_CI_AS NULL 
12None.gifON [PRIMARY]
13None.gifGO

    通过以上脚本,我们在tEsTdB数据库中获得一个名为Invoice的用户表,其中InvoiceID是一个自动编号的bigint型字段,而InvoiceNo是一个普通的bigint型字段。准备工作之后,我们要做的就是向Invoice表中新增记录,要求是对于InvoiceNo字段必须是唯一且连续的。

    对于有过开发经验的朋友来说,这是一个很稀松平常的任务,看看他是如何实现的:

ContractedBlock.gif ExpandedBlockStart.gif 代码2 向Invoice表中增加新记录
 1None.gif        private void btnInsert_Click(object sender, EventArgs e)
 2ExpandedBlockStart.gifContractedBlock.gif        dot.gif{
 3InBlock.gif            string strConn = @"Data Source=WAXDOLL\SQL2K;Initial Catalog=tEsTdB;Integrated 
 4InBlock.gif
 5InBlock.gifSecurity=True";
 6InBlock.gif            string strSQL = @"
 7InBlock.gif                    SELECT 
 8InBlock.gif                        MAX(InvoiceNo) AS MaxNO
 9InBlock.gif                    FROM Invoice
10InBlock.gif                ";
11InBlock.gif            System.Data.SqlClient.SqlConnection sconn =
12InBlock.gif                new System.Data.SqlClient.SqlConnection(
13InBlock.gif                strConn
14InBlock.gif                );
15InBlock.gif            sconn.Open();
16InBlock.gif            System.Data.SqlClient.SqlCommand scomm =
17InBlock.gif                new System.Data.SqlClient.SqlCommand(
18InBlock.gif                strSQL, sconn
19InBlock.gif                );
20InBlock.gif            object objMaxNo = scomm.ExecuteScalar();
21InBlock.gif            int intMaxNo = 0;
22InBlock.gif            if (objMaxNo != null && objMaxNo != System.DBNull.Value)
23ExpandedSubBlockStart.gifContractedSubBlock.gif            dot.gif{
24InBlock.gif                intMaxNo = System.Convert.ToInt32(objMaxNo);
25ExpandedSubBlockEnd.gif            }

26InBlock.gif            intMaxNo++;
27InBlock.gif            this.lblCurrentNO.Text = intMaxNo.ToString();
28InBlock.gif            scomm.CommandText = "INSERT INTO Invoice (InvoiceNo, [Type], Status) VALUES (" + 
29InBlock.gif
30InBlock.gifintMaxNo.ToString() + ", 'a', 'b')";
31InBlock.gif            scomm.ExecuteNonQuery();
32InBlock.gif            scomm.Dispose();
33InBlock.gif            sconn.Close();
34InBlock.gif            sconn.Dispose();
35ExpandedBlockEnd.gif        }

    OK,这段简单代码看起来运行不错,尤其是在一些基础数据维护时由于基础数据不需要经常变动(也许整个生命周期都不会变动一次,因为在数据准备阶段已经固定下来了,提供修改的接口只是为了软件的完整性),不会出现什么大问题。现在,来看看下面的情况——

    修改代码2让按钮单击事件中的代码执行10000次,然后将生成的exe文件复制10份并分别执行它们,如下图所示。

    图2 将SeqNo_IncorrectWay.exe复制10份并运行

    [下载代码2]

    分别单击这10个窗体上的按钮向Invoice表中插入数据,不用等到所有的操作都完成,大概差不多就可以了,在查询分析器中执行下面的语句:

ContractedBlock.gif ExpandedBlockStart.gif 代码3 检查InvoiceID和InvoiceNo是否相等
1None.gifSELECT * FROM Invoice WHERE InvoiceID <> InvoiceNo

    由于InvoiceID是自动编号类型的字段,在只有Insert操作的情况下,如果InvoiceNo是唯一且连续的,每一条记录的InvoiceID应该和InvoiceNo相同,当然,在执行Insert操作之前应当保证InvoiceID是从1开始的,最好在这之前使用语句TRUNCATE TABLE Invoice重新创建Invoice表。所以,如果代码3返回任何记录,则证明InvoiceNo不是唯一的,即出现了重号的情况。(如果是SQL Server 2005的话,我们甚至根本不需要InvoiceID这个字段,因为在SQL Server 2005中可以使用ROW_NUMBER()返回记录的行号,我的随笔RDL(C) Report Design Step by Step 3: Mail Label中有一个关于ROW_NUMBER()的应用)

    事实上,上面同时运行的10个窗体模拟了多个客户端的并发情况,但由于InvoiceNo字段并非不能重复,所以这又和常说的并发冲突有所区别,程序的执行没有问题,但是在逻辑上是错误的。也就是说,在多用户并发的情况下,这种实现无法满足需求。

    需要指出的是,上面的简单代码中,由于线程被阻塞,所以标签lblCurrentNO并不会显示当前插入的InvoiceNo,在稍后会给出一个多线程的可以显示当前插入的InvoiceNo的值的例子。

    接下来,我们看一下,是否可以通过调用下面的存储过程来达到我们的目的:

ContractedBlock.gif ExpandedBlockStart.gif 代码4 存储过程GetInvoiceNo
 1None.gifCREATE  PROCEDURE dbo.GetInvoiceNo
 2None.gif(
 3None.gif    @SeqNo BIGINT OUTPUT
 4None.gif)
 5None.gifAS
 6None.gif    BEGIN TRANSACTION
 7None.gif    UPDATE dbo.CurrentNo SET @SeqNo = CurrentNo = CurrentNo + 1
 8None.gif    INSERT INTO dbo.Invoice (InvoiceNo, Type, Status) VALUES (@SeqNo'a''b')
 9None.gif    COMMIT
10None.gifGO

    其中,表CurrentNo专门用于存储当前插入的InvoiceNo的值,这样可以带来的一个显而易见的好处是避免扫描Invoice表获取InvoiceNo的最大值,在记录数较大时提高效率。该表只有一个bigint型的字段CurrentNo,可以使用下面的语句创建该表:

ContractedBlock.gif ExpandedBlockStart.gif 代码5 创建用户表CurrentNo的SQL语句
 1None.gifif exists (select * from dbo.sysobjects where id = object_id(N'[dbo].[CurrentNo]'and 
 2None.gif
 3None.gifOBJECTPROPERTY(id, N'IsUserTable'= 1)
 4None.gifdrop table [dbo].[CurrentNo]
 5None.gifGO
 6None.gif
 7None.gifCREATE TABLE [dbo].[CurrentNo] (
 8None.gif    [CurrentNo] [bigint] NOT NULL 
 9None.gifON [PRIMARY]
10None.gifGO

    测试用窗体的界面如下图所示:

    图3 多线程测试用窗体

    窗体上的每个按钮单击后都创建一个一个新的线程向数据库中循环插入记录,同时避免由于线程阻塞无法显示当前插入的InvoiceNo的值,窗体代码如下:

ContractedBlock.gif ExpandedBlockStart.gif 代码6 窗体SeqNo_MultiThread.frmTestSeqNo
  1None.gifusing System;
  2None.gifusing System.Threading;
  3None.gifusing System.Collections.Generic;
  4None.gifusing System.ComponentModel;
  5None.gifusing System.Data;
  6None.gifusing System.Data.SqlClient;
  7None.gifusing System.Drawing;
  8None.gifusing System.Text;
  9None.gifusing System.Windows.Forms;
 10None.gif
 11None.gifnamespace SeqNo_MultiThread
 12ExpandedBlockStart.gifContractedBlock.gifdot.gif{
 13InBlock.gif    public partial class frmTestSeqNo : Form
 14ExpandedSubBlockStart.gifContractedSubBlock.gif    dot.gif{
 15InBlock.gif        public frmTestSeqNo()
 16ExpandedSubBlockStart.gifContractedSubBlock.gif        dot.gif{
 17InBlock.gif            InitializeComponent();
 18ExpandedSubBlockEnd.gif        }

 19InBlock.gif
 20InBlock.gif        private delegate void SetCaptionCallBack(string text, int no);
 21InBlock.gif
 22InBlock.gif        private Thread[] thInsert = new Thread[10];
 23InBlock.gif
 24InBlock.gif        private void ThreadSafeInsert(object no)
 25ExpandedSubBlockStart.gifContractedSubBlock.gif        dot.gif{
 26InBlock.gif            for (int i = 0; i < 10000; i++)
 27ExpandedSubBlockStart.gifContractedSubBlock.gif            dot.gif{
 28InBlock.gif                string strConn = @"Data Source=WAXDOLL\SQL2K;Initial 
 29InBlock.gif
 30InBlock.gifCatalog=tEsTdB;Integrated Security=True";
 31InBlock.gif                SqlConnection sconn = new SqlConnection(strConn);
 32InBlock.gif                sconn.Open();
 33InBlock.gif                SqlCommand scomm = new SqlCommand("GetInvoiceNo", sconn);
 34InBlock.gif                scomm.CommandType = CommandType.StoredProcedure;
 35InBlock.gif                SqlParameter sp = new SqlParameter("@SeqNo", SqlDbType.BigInt);
 36InBlock.gif                sp.Direction = ParameterDirection.Output;
 37InBlock.gif                sp.Value = null;
 38InBlock.gif                scomm.Parameters.Add(sp);
 39InBlock.gif                scomm.ExecuteNonQuery();
 40InBlock.gif                scomm.Dispose();
 41InBlock.gif                sconn.Close();
 42InBlock.gif                sconn.Dispose();
 43InBlock.gif                this.SetCaption(sp.Value.ToString(), Convert.ToInt32(no));
 44InBlock.gif                System.Threading.Thread.Sleep(100);
 45ExpandedSubBlockEnd.gif            }

 46ExpandedSubBlockEnd.gif        }

 47InBlock.gif
 48InBlock.gif        private void SetCaption(string text, int no)
 49ExpandedSubBlockStart.gifContractedSubBlock.gif        dot.gif{
 50InBlock.gif            Label lblCurrent = null;
 51InBlock.gif            foreach( Control ctl in this.Controls )
 52ExpandedSubBlockStart.gifContractedSubBlock.gif            dot.gif{
 53InBlock.gif                if (ctl.Name == "lblCurrent" + no.ToString())
 54ExpandedSubBlockStart.gifContractedSubBlock.gif                dot.gif{
 55InBlock.gif                    lblCurrent = ctl as Label;
 56InBlock.gif                    break;
 57ExpandedSubBlockEnd.gif                }

 58ExpandedSubBlockEnd.gif            }

 59InBlock.gif            if (lblCurrent != null)
 60ExpandedSubBlockStart.gifContractedSubBlock.gif            dot.gif{
 61InBlock.gif                if (lblCurrent.InvokeRequired)
 62ExpandedSubBlockStart.gifContractedSubBlock.gif                dot.gif{
 63InBlock.gif                    SetCaptionCallBack d = new SetCaptionCallBack(SetCaption);
 64ExpandedSubBlockStart.gifContractedSubBlock.gif                    this.Invoke(d, new object[] dot.gif{ text, no });
 65ExpandedSubBlockEnd.gif                }

 66InBlock.gif                else
 67ExpandedSubBlockStart.gifContractedSubBlock.gif                dot.gif{
 68InBlock.gif                    lblCurrent.Text = text;
 69ExpandedSubBlockEnd.gif                }

 70ExpandedSubBlockEnd.gif            }

 71ExpandedSubBlockEnd.gif        }

 72InBlock.gif
 73InBlock.gif        private void btnThread_Click(object sender, EventArgs e)
 74ExpandedSubBlockStart.gifContractedSubBlock.gif        dot.gif{
 75InBlock.gif            string strName = (sender as Button).Name;
 76InBlock.gif            int intOrder = 0;
 77InBlock.gif            if (strName == "btnThread10")
 78ExpandedSubBlockStart.gifContractedSubBlock.gif            dot.gif{
 79InBlock.gif                intOrder = 10;
 80ExpandedSubBlockEnd.gif            }

 81InBlock.gif            else
 82ExpandedSubBlockStart.gifContractedSubBlock.gif            dot.gif{
 83InBlock.gif                intOrder = Convert.ToInt32(strName.Substring(strName.Length - 1));
 84ExpandedSubBlockEnd.gif            }

 85InBlock.gif            this.thInsert[intOrder - 1= new Thread(new ParameterizedThreadStart
 86InBlock.gif
 87InBlock.gif(ThreadSafeInsert));
 88InBlock.gif            this.thInsert[intOrder - 1].Start(intOrder);
 89InBlock.gif            (sender as Button).Enabled = false;
 90ExpandedSubBlockEnd.gif        }

 91InBlock.gif
 92InBlock.gif        private void frmTestSeqNo_Load(object sender, EventArgs e)
 93ExpandedSubBlockStart.gifContractedSubBlock.gif        dot.gif{
 94InBlock.gif            for (int i = 1; i <= 11; i++)
 95ExpandedSubBlockStart.gifContractedSubBlock.gif            dot.gif{
 96InBlock.gif                foreach (Control ctl in this.Controls)
 97ExpandedSubBlockStart.gifContractedSubBlock.gif                dot.gif{
 98InBlock.gif                    if (ctl.Name == "btnThread" + i.ToString())
 99ExpandedSubBlockStart.gifContractedSubBlock.gif                    dot.gif{
100InBlock.gif                        (ctl as Button).Click += new EventHandler(btnThread_Click);
101InBlock.gif                        break;
102ExpandedSubBlockEnd.gif                    }

103ExpandedSubBlockEnd.gif                }

104ExpandedSubBlockEnd.gif            }

105ExpandedSubBlockEnd.gif        }

106InBlock.gif
107InBlock.gif        private void frmTestSeqNo_FormClosing(object sender, FormClosingEventArgs e)
108ExpandedSubBlockStart.gifContractedSubBlock.gif        dot.gif{
109InBlock.gif            if (MessageBox.Show(
110InBlock.gif                      "R U Sure to Exit?"
111InBlock.gif                    , "Threads may still B Alive"
112InBlock.gif                    , MessageBoxButtons.YesNo
113InBlock.gif                    , MessageBoxIcon.Question
114InBlock.gif                    ) == DialogResult.Yes
115InBlock.gif                )
116ExpandedSubBlockStart.gifContractedSubBlock.gif            dot.gif{
117InBlock.gif                for (int i = 0; i < 10; i++)
118ExpandedSubBlockStart.gifContractedSubBlock.gif                dot.gif{
119InBlock.gif                    if (this.thInsert[i] != null && this.thInsert[i].IsAlive)
120ExpandedSubBlockStart.gifContractedSubBlock.gif                    dot.gif{
121InBlock.gif                        this.thInsert[i].Abort();
122ExpandedSubBlockEnd.gif                    }

123ExpandedSubBlockEnd.gif                }

124ExpandedSubBlockEnd.gif            }

125ExpandedSubBlockEnd.gif        }

126ExpandedSubBlockEnd.gif    }

127ExpandedBlockEnd.gif}

     [下载代码6]

    运行上面的代码,窗体激活时,不断按下Space键触发按钮事件向Invoice表中插入数据。

    程序运行一段时间后,使用代码3检验Invoice表中是否存在InvoiceID <> InvoiceNo的记录。我们会发现,如果初始CurrentNo为0且使用过语句TRUNCATE TABLE Invoice重新创建Invoice表,那么这种方法是可以满足我们的需求的。

    接下来,我们把代码6中调用的存储过程换为以下代码所示的存储过程:

ContractedBlock.gif ExpandedBlockStart.gif 代码7 存储过程GetInvoiceNo_Deadlock
 1None.gifCREATE  PROCEDURE dbo.GetInvoiceNo_Deadlock
 2None.gif(
 3None.gif    @SeqNo BIGINT OUTPUT
 4None.gif)
 5None.gifAS
 6None.gif    BEGIN TRANSACTION
 7None.gif    UPDATE dbo.CurrentNo SET CurrentNo = CurrentNo + 1
 8None.gif    SET @SeqNo = ( SELECT CurrentNo FROM dbo.CurrentNo )
 9None.gif    INSERT INTO dbo.Invoice (InvoiceNo, Type, Status) VALUES (@SeqNo'a''b')
10None.gif    COMMIT
11None.gifGO

    运行程序,我们可以发现,一段时间后,出现下图所示的错误:

    图4 死锁

    为什么代码7会出现死锁?由于代码7中SET @SeqNo = ( SELECT CurrentNo FROM dbo.CurrentNo )的本质是一个SELECT语句,因而SQL Server 2000以共享(S)锁锁定资源,而其它线程很可能正好执行到语句UPDATE dbo.CurrentNo SET CurrentNo = CurrentNo + 1,而执行UPDATE语句时SQL Server 2000需要使用排它(X)锁锁定资源,即对于CurrentNo需要从共享(S)锁提升到排它(X)锁,而锁的提升需要时间,线程之间相互等待,因而发生了死锁。而对于代码4来说,一开始即使用排它(X)锁(UPDATE语句),所以不会发生死锁。

    基本上,代码4和代码6的结合可以实现我们的需求,“唯一”肯定没有问题,对于“连续”来说,需要把所有的一次操作包含在事务中。如果你的系统中有多个实体需要使用这种唯一且连续的编号,在CurrentNo中添加一个用于标明实体的字段并简单修改存储过程就可以了。

    14.gif

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值