如何在多租户数据库中最好地生成唯一的用户编号而不重复

时间:2016-05-24 16:43:50

标签: sql-server sql-server-2008

背景:

这是一个多租户应用程序,因此普通标识列将不起作用。所有表都有唯一的客户端标识符Clients.id。所以每个客户都可以有很多客户。为简单起见,此列未包含在下面。

我们希望从1000开始生成唯一的客户编号。

我们将当前(最后)生成的数字存储在名为Master的表中。我们说Master.CustomerNumber。所以数字将是1001,1002等,最后一个存储在那里。

因此,每次我们添加客户时,我们都会有一个查询当前值,增加它并将其插入Customer.Number

注意:我们正在使用SQL Server 2008.群集中有多个服务器。

确保如果同时添加两个客户,每个客户获得一个唯一的客户编号,最好的方法是什么?存储过程,锁定,CFLOCKING?

我如何确保此流程是“单线程”且相同的号码不会发出两次?

我在Customer.Number+Clients.id上有一个唯一索引。我感兴趣的是如何在生成时保证唯一性。

4 个答案:

答案 0 :(得分:3)

我没有回顾过现有的解决方案,因为它们很长很精细。您不需要以下所有内容吗?

CREATE TABLE MasterDB.dbo.Sequences (ClientId INT NOT NULL PRIMARY KEY, LastGeneratedNumber INT NOT NULL)

DECLARE @nextId INT; //Holds the newly allocated ID

UPDATE MasterDB.dbo.Sequences
SET LastGeneratedNumber = LastGeneratedNumber + 1, @nextId = LastGeneratedNumber + 1
WHERE ClientId = 1234

在任何隔离级别和任何索引结构下都是正确的。保存ID信息的行将由引擎锁定U或X.

如果从未生成过ID,则此更新语句将不执行任何操作。您可以使用MERGE或使用控制流来解决这个问题。我推荐MERGE

或者,每当您创建新客户端时都插入一行。设置LastGeneratedNumber = 1000 - 1

没有必要使用存储过程,但你当然可以。从应用程序批量执行此T-SQL几乎没有性能差异。做什么对你来说更方便。

如果您将此T-SQL作为主要事务的一部分,则ID分配将是事务性的。它可能会回滚,它将序列化客户创建。如果您不喜欢,请使用单独的交易。但是,ID可能会丢失。这在任何解决方案中都是不可避免的。

上面给出的UPDATE变体是:

UPDATE MasterDB.dbo.Sequences
SET LastGeneratedNumber = LastGeneratedNumber + 1
OUTPUT INSERTED.LastGeneratedNumber
WHERE ClientId = 1234

您可以为每个客户端使用一个序列。这要求您的应用程序执行DDL。这可能很尴尬。此外,您无法进行ID生成事务。控制力较弱。我不建议这样做,但它是可能的。

答案 1 :(得分:2)

您可以使用以下解决方案:

CREATE TABLE dbo.[Master] (
    -- Foreign key to dbo.Tenant table ?
    -- Only one row for every tenant is allowed => PK on tenant identifier
    TenantNum   INT NOT NULL
        CONSTRAINT PK_Master PRIMARY KEY CLUSTERED (TenantNum),
    -- LastCustomerNum = last generated value for CustomerNum
    -- NULL means no value was generated
    LastCustomerNum INT NULL, 
    -- It will create one clustered unique index on these two columns
    InitialValue INT NOT NULL
        CONSTRAINT DF_Master_InitialValue DEFAULT (1),
    Step        INT NOT NULL
        CONSTRAINT DF_Master_Step DEFAULT (1)
);
GO

CREATE PROCEDURE dbo.p_GetNewCustomerNum
@TenantNum          INT,
@NewCustomerNum     INT OUTPUT,
@HowManyCustomerNum INT = 1 -- Ussualy, we want to generate only one CustomerNum 
AS
BEGIN
    BEGIN TRY
        IF @TenantNum IS NULL
            RAISERROR('Invalid value for @TenantNum: %d', 16, 1, @TenantNum);
        IF @HowManyCustomerNum IS NULL OR @HowManyCustomerNum < 1
            RAISERROR('Invalid value for @HowManyCustomerNum: %d', 16, 1, @HowManyCustomerNum)

        -- It updated the LastCustomerNum column and it assign the new value to @NewCustomerNum output parameter
        UPDATE  m
        SET     @NewCustomerNum 
                    = LastCustomerNum 
                    = CASE WHEN LastCustomerNum IS NULL THEN InitialValue - Step ELSE LastCustomerNum END 
                        + Step * @HowManyCustomerNum
        FROM    dbo.[Master] AS m
        WHERE   m.TenantNum = @TenantNum

        IF @@ROWCOUNT = 0
            RAISERROR('@TenantNum: %d doesn''t exist', 16, 1, @TenantNum);
    END TRY
    BEGIN CATCH
        -- ReThrow intercepted exception/error
        DECLARE @ExMessage NVARCHAR(2048) = ERROR_MESSAGE()
        RAISERROR(@ExMessage, 16, 1)
        -- Use THROW for SQL2012+
    END CATCH
END
GO

用法(无间隙):

BEGIN TRAN
...
DECLARE @cn INT
EXEC dbo.p_GetNewCustomerNum
    @TenantNum          = ...,
    @NewCustomerNum     = @cn OUTPUT,
    [@HowManyCustomerNum = ...]
...
INSERT INTO dbo.Customer(..., CustomerNum, ...)
VALUES (..., @cs, ...)
COMMIT

注意:如果您不使用交易生成新客户编号 ,请将此值插入Customer然后他们可能会有差距。

它是如何工作的?

  1. {主键|在TenantNum 和CustomerNum 上定义的唯一索引}将防止任何重复
  2. 在默认隔离级别(READ COMMITTED)下,但在READ UNCOMMITTEDREPETABLE READSERIALIZABLE下,UPDATE语句需要X锁定。如果我们有两个尝试生成 new CustomerNum的可靠SQL Server会话(和事务),那么第一个会话将成功获取租户行上的X锁定,第二个会话将不得不等待直到第一个会话(和交易)将结束(COMMITROLLBACK)。注意:我假设每个会话都有一个活动事务。
  3. 关于X锁定行为:这是可能的,因为两个[concurent] X锁是不兼容的。参见下面的表格&#34;请求模式&#34;和[授权模式]: enter image description here
  4. 由于上述原因,只有一个连接/ TX 可以在dbo.[Master]租户行中使用新客户编号进行更新。
  5. enter image description here

    -- Tests #1
    
    -- It insert few new and "old" tenants
    INSERT  dbo.[Master] (TenantNum) VALUES (101)
    INSERT  dbo.[Master] (TenantNum, LastCustomerNum) VALUES (102, 1111)
    
    SELECT * FROM dbo.[Master]
    /*
    TenantNum   LastCustomerNum InitialValue Step
    ----------- --------------- ------------ -----------
    101         NULL            1            1
    102         1111            1            1
    */
    GO
    
    
    -- It generate one CustomerNum for tenant 101
    DECLARE @cn INT
    EXEC p_GetNewCustomerNum 101, @cn OUTPUT 
    SELECT @cn AS [cn]
    /*
    cn
    -----------
    1
    */
    GO
    -- It generate second CustomerNums for tenant 101
    DECLARE @cn INT
    EXEC p_GetNewCustomerNum 101, @cn OUTPUT 
    SELECT @cn AS [cn]
    /*
    cn
    -----------
    2
    */
    GO
    -- It generate three CustomerNums for tenant 101
    DECLARE @cn INT
    EXEC p_GetNewCustomerNum 101, @cn OUTPUT, 3
    SELECT @cn AS [cn]
    /*
    cn
    -----------
    5           <-- This ID means that following range was reserved [(5-3)+1, 5] = [3, 5] = {3, 4, 5}; Note: 1 = Step
    */
    GO
    -- It generate one CustomerNums for tenant 102
    DECLARE @cn INT
    EXEC p_GetNewCustomerNum 102, @cn OUTPUT 
    SELECT @cn AS [cn]
    /*
    cn
    -----------
    1112
    */
    GO
    -- End status of Master table
    SELECT * FROM dbo.Master
    /*
    TenantNum   LastCustomerNum InitialValue Step
    ----------- --------------- ------------ -----------
    101         5               1            1
    102         1112            1            1
    */
    GO
    

    -- Tests #2: To test concurent sesssions / TX you could use bellow script
    -- Step 1: Session 1
    BEGIN TRAN
        -- It generate three CustomerNums for tenant 101
        DECLARE @cn INT
        EXEC p_GetNewCustomerNum 101, @cn OUTPUT
        SELECT @cn AS [cn] -- > It generates @cn 6
    
    
    
    -- Step 2: Session 2
    BEGIN TRAN
        -- It generate three CustomerNums for tenant 101
        DECLARE @cn INT
        EXEC p_GetNewCustomerNum 101, @cn OUTPUT -- Update waits for Session 1 to finish
        SELECT @cn AS [cn]
    COMMIT
    
    
    -- Step 3: Session 1
    COMMIT -- End of first TX. Check Session 2: it'll output 7.
    

    首先注意:要管理事务和异常,我会使用SET XACT_ABORT ON和/或BEGIN TRAN ... END CATCH。关于这个主题的讨论超出了这个答案的目的。

    第二个结尾注释:请参阅更新的部分&#34;它是如何工作的?&#34; (第3和第4章)。

答案 2 :(得分:0)

您想使用Sequence,例如:

CREATE SEQUENCE Customer_Number_Seq
 AS INTEGER
 START WITH 1
 INCREMENT BY 1
 MINVALUE 1000
 MAXVALUE 100
 CYCLE;

然后可能是这样的:

 CREATE TABLE Customers
(customer_nbr INTEGER DEFAULT NEXT VALUE FOR Customer_Number_Seq,
 .... other columns ....

documentation有更多详情。

答案 3 :(得分:0)

我知道这有点晚了,但仍然希望它可以帮助你:)。

我们在这里也有同样的情况...... 我们通过在一个单独的数据库中使用一个公共表来解决它,该数据库只包含三列(即tablename,columnname和LastIndex)。 现在,我们使用一个单独的SP来始终从该表中获取具有单独事务的新数字(因为它应该始终提交,无论您的主要插入函数是true / false)。 因此,这将始终以新ID返回任何请求,并且新索引将用于插入记录。

如果您需要任何样品,请告诉我。