Sage CRM 自增ID的方案和遇到的问题

  Sage CRM的每个表的ID都是用自己包装的过程生成的,这个ID会在每个页面之间作为标识来传递。很有意思的是,系统本身并没有默认给这些表的ID设置为主键或者增加索引。我尝试了存在相同的ID,结果页面列表显示和页面都被系统随机选了一条记录显示,列表显示的可能是李四,打开之后是张三。

  这个生成自增ID的存储过程大体类似 这个园友的方法《弃用数据库自增ID,曝光一下我自己用到的解决方法》。思路:

  1:定义一张表,专门用来存放存所有需要唯一ID的表名称以及该表当前所使用到的ID值。
  2: 写一个存储过程,专门用来在上一步的表中取ID值。【引用上文的表述】

  但Sage CRM 在这个思路下增加了两个很有意思的东西。第一个就是自定义的锁。第二个是ID的区间范围增加限定。

  一、 先上存储过程和表结构。

  1、 表:SQL_Identity,保存表ID和该表当前所使用到的ID值

CREATE TABLE [dbo].[SQL_Identity](
[Id_TableId] [int] NOT NULL,
[Id_NextId] [int] NOT NULL
) ON [PRIMARY]

  2、 存储过程:crm_next_id,根据表ID查询表SQL_Identity返回记录ID值。

ALTER procedure [dbo].[crm_next_id]
@table_id int
AS

declare @id_next int
declare @id_table int
select @id_next = 0

declare @errmsg nvarchar(250)
select @errmsg =''

declare @id_inc int
declare @sql nvarchar(200)

BEGIN TRANSACTION T1
exec('DECLARE my_cursor CURSOR FOR SELECT Id_TableId, Id_NextId FROM ' +
'SQL_Identity WITH (UPDLOCK) WHERE Id_TableId ='+@table_id)

--open cursor and fetch max back
open my_cursor
fetch next from my_cursor into @id_table, @id_next
if (@@fetch_status=0) begin
select @id_inc = @id_next+1
select @sql = 'update SQL_Identity set Id_NextId='+STR(@id_inc)+' where Id_TableId='+STR(@id_table)
exec(@sql)
end
else begin
select @errmsg = 'NOROW';
end

close my_cursor
deallocate my_cursor
COMMIT TRANSACTION T1

-- now check got back decent result
if (@errmsg<>'') begin
raiserror(@errmsg,16,-1)
return 0
end
return @id_next

3、 表: Rep_Ranges。 这个表就是用来做NextID的范围控制的。

CREATE TABLE [dbo].[Rep_Ranges](
[Range_TableId] [int] NOT NULL,
[Range_RangeStart] [int] NULL,
[Range_RangeEnd] [int] NULL,
[Range_NextRangeStart] [int] NULL,
[Range_NextRangeEnd] [int] NULL,
[Range_Control_NextRange] [int] NULL
) ON [PRIMARY]

 

4、 表:Locks,用于自定义锁

CREATE TABLE [dbo].[Locks](
[Lock_SessionId] [int] NOT NULL,
[Lock_TableId] [int] NOT NULL,
[Lock_RecordId] [nchar](50) NOT NULL,
[Lock_CreatedBy] [int] NOT NULL,
[Lock_CreatedDate] [datetime] NOT NULL,
[Lock_IsWapUser] [nchar](1) NULL,
[Lock_DeviceID] [int] NULL
) ON [PRIMARY]

 

5、 存储过程: eware_get_identity_id。 这个过程才是最终被程序调用的过程。下面才做具体的分析。

ALTER PROCEDURE [dbo].[eware_get_identity_id]
@table_name NVARCHAR(80)
AS


DECLARE @table_id INT

DECLARE @errmsg NVARCHAR(250)
SELECT @errmsg =''

DECLARE @timeout INTeger
DECLARE @start_lock datetime

DECLARE @id_inc INT
DECLARE @sql NVARCHAR(200)

DECLARE @r_start INT
DECLARE @r_end INT
DECLARE @r_nextstart INT
DECLARE @r_nextend INT

DECLARE @r_nextcontrol INT

DECLARE @range_limit INT

DECLARE @new_id INT
SELECT @new_id = 0

DECLARE @no_lock INT

SELECT @range_limit = 10000

SELECT @timeout = 5 -- try for 5 seconds to get the lock

SET NOCOUNT ON

-- Get the Table Id from the table name
SELECT @table_id = Bord_TableId FROM Custom_Tables NOLOCK WHERE Bord_Name = @table_name
IF (@@ERROR <> 0) OR @table_id = 0
BEGIN
RAISERROR('crm_new_id: No Table Found %s',16,-1,@table_name) WITH LOG
RETURN 0
END

BEGIN TRANSACTION T1
-- keep trying until we get the lock
-- must put a timeout here and RETURN ERROR IF cannot get lock
SELECT @start_lock = getdate();
SELECT @no_lock = 0
WHILE (@no_lock = 0) AND (Getdate() < DateAdd(second,@timeout,@start_lock) )
BEGIN
INSERT INTO Locks (Lock_SessionId, Lock_TableId,Lock_RecordId,Lock_CreatedBy,Lock_CreatedDate)
VALUES(0,@table_id,-99,0,getDate())

IF (@@ERROR = 0) BEGIN
SELECT @no_lock = 1
END
END

IF @no_lock = 0 BEGIN
ROLLBACK TRANSACTION T1
RAISERROR('crm_new_id: Timeout trying to get lock (%s)',16,-1,@table_name) WITH LOG
RETURN 0
END
ELSE BEGIN
COMMIT TRANSACTION T1
END


BEGIN TRANSACTION T2
-- check out the range values for this table
SELECT @r_start = Range_RangeStart, @r_end = Range_RangeEnd,
@r_nextstart = Range_NextRangeStart, @r_nextend = Range_NextRangeEnd,
@r_nextcontrol = Range_Control_NextRange FROM Rep_Ranges NOLOCK WHERE Range_TableId = @table_id

-- get the next id for this table
EXEC @new_id = crm_next_id @table_id = @table_id

-- is the id within the range?
IF (@new_id < @r_start OR @new_id > @r_end) AND (@new_id <> 0) BEGIN

-- have used up all the range so create a new range
UPDATE Rep_Ranges SET
Range_RangeStart = @r_nextstart,
Range_RangeEnd = @r_nextend,
Range_NextRangeStart = @r_nextcontrol,
Range_NextRangeEnd = @r_nextcontrol + @range_limit - 1,
Range_Control_NextRange = @r_nextcontrol + @range_limit
WHERE Range_TableId = @table_id

IF @@ERROR <> 0 BEGIN
SELECT @new_id = 0
SELECT @errmsg = 'crm_new_id: Update New Range Failed (%s)'
GOTO END_TRANS
END

-- result is first id FROM new range
SELECT @new_id = @r_nextstart

-- apply the new range so eware_get_next_id will RETURN next id FROM within new range
UPDATE Sql_Identity SET Id_NextId = @r_nextstart+1 WHERE Id_TableId = @table_id
IF @@ERROR <> 0 BEGIN
SELECT @new_id = 0
SELECT @errmsg = 'crm_new_id: Apply New Range Failed (%s)'
GOTO END_TRANS
END

END

END_TRANS: -- commit or rollback the transaction

IF @new_id = 0 BEGIN
ROLLBACK TRANSACTION T2
IF @errmsg = ''
SELECT @errmsg = 'crm_new_id: Get Next Id Failed (%s)'

END
ELSE BEGIN
COMMIT TRANSACTION T2
END


--Finished so remove the lock
BEGIN TRANSACTION T3
SELECT @start_lock = getdate();
SELECT @no_lock = 0
WHILE (@no_lock = 0) AND (Getdate() < DateAdd(second,@timeout,@start_lock) ) BEGIN

DELETE FROM Locks WHERE Lock_TableId=@table_id and Lock_RecordId = -99
IF (@@ERROR = 0) BEGIN
SELECT @no_lock = 1
END
END
IF @no_lock = 0 BEGIN
ROLLBACK TRANSACTION T3
RAISERROR('crm_new_id: Error Deleting Lock (%s)',16,-1,@table_name) WITH LOG
RETURN 0
END
ELSE BEGIN
COMMIT TRANSACTION T3
END

-- Make sure RaisError is the last call, so that @@ERROR is set correctly
IF (@errmsg <> '') BEGIN
RAISERROR(@errmsg,16,-1,@table_name) WITH LOG
RETURN 0
END

SET NOCOUNT OFF
RETURN @new_id

 


  二、 说明和分析。

  1、 SageCRM创建表需要通过系统界面的方式。所以创建的时候,系统会自动保存表名称和表的ID,接着给表[SQL_Identity]增加记录,给表[Rep_Ranges]增加默认的范围控制。如图,Lead表是被我更改过的。第一个区间是 20050001 ~ 20100000.第一个区间用完之后,会使用第二个区间,第三个区间的开始值就是最后一个字段。

  2、 分析存储过程[eware_get_identity_id]

  第一部分:参数和验证提供的参数表名是否存在,返回表的ID。

    DECLARE @no_lock INT

SELECT @range_limit = 10000

SELECT @timeout = 5 -- try for 5 seconds to get the lock

SET NOCOUNT ON

-- Get the Table Id from the table name
SELECT @table_id = Bord_TableId FROM Custom_Tables NOLOCK WHERE Bord_Name = @table_name
IF (@@ERROR <> 0) OR @table_id = 0
BEGIN
RAISERROR('crm_new_id: No Table Found %s',16,-1,@table_name) WITH LOG
RETURN 0
END

参数主要有两个比较重要的:@range_limit 就是区间增加的幅度。 @timeout 用来防止超时的。

  第二部分:增加自定义锁。

BEGIN TRANSACTION T1  
-- keep trying until we get the lock
-- must put a timeout here and RETURN ERROR IF cannot get lock
SELECT @start_lock = getdate();
SELECT @no_lock = 0
WHILE (@no_lock = 0) AND (Getdate() < DateAdd(second,@timeout,@start_lock) )
BEGIN
INSERT INTO Locks (Lock_SessionId, Lock_TableId,Lock_RecordId,Lock_CreatedBy,Lock_CreatedDate)
VALUES(0,@table_id,-99,0,getDate())

IF (@@ERROR = 0) BEGIN
SELECT @no_lock = 1
END
END

IF @no_lock = 0 BEGIN
ROLLBACK TRANSACTION T1
RAISERROR('crm_new_id: Timeout trying to get lock (%s)',16,-1,@table_name) WITH LOG
RETURN 0
END
ELSE BEGIN
COMMIT TRANSACTION T1
END

原理: Locks表中,系统默认给Lock_SessionId增加一个 唯一非聚集索引。代码中,使用while在5秒内不停的向Locks插入一条Lock_SessionId为0数据,如果报错则继续执行。如果没有出错或者5秒之后,则跳出循环。

第三部分:ID的范围控制和生成有效的ID

BEGIN TRANSACTION T2
-- check out the range values for this table
SELECT @r_start = Range_RangeStart, @r_end = Range_RangeEnd,
@r_nextstart = Range_NextRangeStart, @r_nextend = Range_NextRangeEnd,
@r_nextcontrol = Range_Control_NextRange FROM Rep_Ranges NOLOCK WHERE Range_TableId = @table_id

-- get the next id for this table
EXEC @new_id = crm_next_id @table_id = @table_id

-- is the id within the range?
IF (@new_id < @r_start OR @new_id > @r_end) AND (@new_id <> 0) BEGIN

-- have used up all the range so create a new range
UPDATE Rep_Ranges SET
Range_RangeStart = @r_nextstart,
Range_RangeEnd = @r_nextend,
Range_NextRangeStart = @r_nextcontrol,
Range_NextRangeEnd = @r_nextcontrol + @range_limit - 1,
Range_Control_NextRange = @r_nextcontrol + @range_limit
WHERE Range_TableId = @table_id

IF @@ERROR <> 0 BEGIN
SELECT @new_id = 0
SELECT @errmsg = 'crm_new_id: Update New Range Failed (%s)'
GOTO END_TRANS
END

-- result is first id FROM new range
SELECT @new_id = @r_nextstart

-- apply the new range so eware_get_next_id will RETURN next id FROM within new range
UPDATE Sql_Identity SET Id_NextId = @r_nextstart+1 WHERE Id_TableId = @table_id
IF @@ERROR <> 0 BEGIN
SELECT @new_id = 0
SELECT @errmsg = 'crm_new_id: Apply New Range Failed (%s)'
GOTO END_TRANS
END

END

END_TRANS: -- commit or rollback the transaction

IF @new_id = 0 BEGIN
ROLLBACK TRANSACTION T2
IF @errmsg = ''
SELECT @errmsg = 'crm_new_id: Get Next Id Failed (%s)'

END
ELSE BEGIN
COMMIT TRANSACTION T2
END

从代码中,可以看出,如果生成的ID不在表所在的第一范围内的话,会把下一个范围的开始值赋值给@new_id【SELECT @new_id = @r_nextstart】,最后更新范围表。

 

第四部分:删除自定义锁。

    --Finished so remove the lock     
BEGIN TRANSACTION T3
SELECT @start_lock = getdate();
SELECT @no_lock = 0
WHILE (@no_lock = 0) AND (Getdate() < DateAdd(second,@timeout,@start_lock) ) BEGIN

DELETE FROM Locks WHERE Lock_TableId=@table_id and Lock_RecordId = -99
IF (@@ERROR = 0) BEGIN
SELECT @no_lock = 1
END
END
IF @no_lock = 0 BEGIN
ROLLBACK TRANSACTION T3
RAISERROR('crm_new_id: Error Deleting Lock (%s)',16,-1,@table_name) WITH LOG
RETURN 0
END
ELSE BEGIN
COMMIT TRANSACTION T3
END

-- Make sure RaisError is the last call, so that @@ERROR is set correctly
IF (@errmsg <> '') BEGIN
RAISERROR(@errmsg,16,-1,@table_name) WITH LOG
RETURN 0
END

SET NOCOUNT OFF
RETURN @new_id

自增ID的方案就是这样的。

 

 二、 问题。

先来执行下面这个过程: 去一个有67318条记录的表中,抓数据,然后游标往communication表插入记录。

alter PROC [dbo].p_test
AS
BEGIN
--------------沟通生成 开始----------------
declare @comm_leadid int

DECLARE CreateComm SCROLL CURSOR
FOR
SELECT lead_leadid FROM lead --67318
FOR UPDATE

OPEN CreateComm
FETCH FROM CreateComm INTO @comm_leadid
WHILE @@FETCH_STATUS = 0
BEGIN
DECLARE @commid INT
EXEC @commid = eware_get_identity_id 'Communication'

INSERT INTO Communication (comm_communicationid,comm_leadid,comm_action,comm_status,comm_note)
SELECT @commid,@comm_leadid,'Meeting','Complete','test'

FETCH FROM CreateComm INTO @comm_leadid
END
CLOSE CreateComm
DEALLOCATE CreateComm
----------------------沟通生成结束----------------
END

这个过程单个执行是没问题的。但如果打开两个页面同时执行的话就有下面的错了。而且两边都一样无法执行下去。


最杯具的是,客户使用LoadRunner测试我们自己开发页面和接口。都是超时和死锁。但是使用Loadrunner测试SageCRM自己自带的新建页面,也是会同时新建多个表,而且Loadrunner是50个用户并发都通过了测试。非常郁闷。

  按照先前分析的代码,5秒之后应该会自动解锁的。


  搞不明白,为什么?    希望文章对大家有用。也请大家有空分析一下。谢谢!



 




posted @ 2011-12-07 00:03  nick_lsf  阅读(1528)  评论(2编辑  收藏  举报