背景:
这是一个多租户应用程序,因此普通标识列将不起作用。所有表都有唯一的客户端标识符Clients.id。所以每个客户都可以有很多客户。为简单起见,此列未包含在下面。
我们希望从1000开始生成唯一的客户编号。
我们将当前(最后)生成的数字存储在名为Master
的表中。我们说Master.CustomerNumber
。所以数字将是1001,1002等,最后一个存储在那里。
因此,每次我们添加客户时,我们都会有一个查询当前值,增加它并将其插入Customer.Number
。
注意:我们正在使用SQL Server 2008.群集中有多个服务器。
确保如果同时添加两个客户,每个客户获得一个唯一的客户编号,最好的方法是什么?存储过程,锁定,CFLOCKING?
我如何确保此流程是“单线程”且相同的号码不会发出两次?
我在Customer.Number+Clients.id
上有一个唯一索引。我感兴趣的是如何在生成时保证唯一性。
答案 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
表然后他们可能会有差距。
它是如何工作的?
READ COMMITTED
)下,但在READ UNCOMMITTED
,REPETABLE READ
和SERIALIZABLE
下,UPDATE
语句需要X
锁定。如果我们有两个尝试生成 new CustomerNum的可靠SQL Server会话(和事务),那么第一个会话将成功获取租户行上的X
锁定,第二个会话将不得不等待直到第一个会话(和交易)将结束(COMMIT
或ROLLBACK
)。注意:我假设每个会话都有一个活动事务。X
锁定行为:这是可能的,因为两个[concurent] X
锁是不兼容的。参见下面的表格&#34;请求模式&#34;和[授权模式]:
dbo.[Master]
租户行中使用新客户编号进行更新。-- 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返回任何请求,并且新索引将用于插入记录。
如果您需要任何样品,请告诉我。