zoukankan      html  css  js  c++  java
  • 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秒之后应该会自动解锁的。


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






    作者:Novus
    出处:http://www.cnblogs.com/novus/
    本文版权归作者和博客园共有,欢迎任何形式的转载,但请务必注明出处。

  • 相关阅读:
    随堂笔记 17day
    随堂笔记16day
    随堂笔记day15 python
    随堂笔记14day python
    随堂笔记12day python
    随堂笔记python 11day 补
    java
    微信小程序入门
    #JS# 如何判断一个字符串是否为日期格式
    JS如何按时间粒度获取date的时间差
  • 原文地址:https://www.cnblogs.com/novus/p/2266956.html
Copyright © 2011-2022 走看看