【问题标题】:Stored procedure structure and concurrency issues存储过程结构和并发问题
【发布时间】:2017-06-24 03:51:19
【问题描述】:

有人在 SQL Server 2008 中编写了以下存储过程。我认为这不是实现他需要实现的目标的最佳方法之一。

如您所见,我们需要在某些情况下更新MyTable,但在其他情况下则不需要。并且只要存在允许用户更新MyTable 的条件,我们不希望多个用户同时更新MyTable。问题是这个过程的编写方式,所有变量都是使用 select 语句设置的,然后Begin Transaction 开始。

但是鉴于此过程的结构,最好执行sp_getapplock 过程以仅允许一个用户进行更新(考虑到其结构,这可能适用于此过程,但我通读了sp_getapplock 可以导致死锁),其他选项是执行可序列化的READ_COMMITED

但是在可序列化的情况下,这个存储过程是否允许并发用户之一(比如说 2)仍然选择 col1、col2、col3 和 col4 的旧值并填充变量,如 @onGoingSession@timeDiff等其他人完成更新并释放行(通过可序列化)时使用这些旧值。由于事务完成后轮到第二个用户。而且我确实想阻止其他用户阅读正在修改的行。

我们不应该在声明变量之后立即将开始事务向上移动并在它之后执行可序列化。因此,当其中一位用户(显然已经获得行锁的用户)正在更新锁定的行但尚未提交时,任何并发用户都不应读取旧值并填充变量。

 USE [myDatabase]
 GO

 SET ANSI_NULLS ON
 GO
 SET QUOTED_IDENTIFIER ON
 GO

CREATE PROCEDURE [dbo].[MyTableProc]
    (@UserId char(20))   
AS
BEGIN
    DECLARE @error varchar(200)
    DECLARE @warning varchar(300)
    DECLARE @timeDiff smallint
    DECLARE @onGoingSession bit = 1
    DECLARE @userName char(20)
    DECLARE @counterOfResync tinyint

    SET @counterOfResync = (SELECT [Col1] FROM MyTable)
    SET @userName = (SELECT [Col2] FROM MyTable)
    SET @onGoingSession = (SELECT [Col3] FROM MyTable)

    SET @timeDiff = (SELECT DATEDIFF(MINUTE, Col4, CURRENT_TIMESTAMP) from MyTable)

    BEGIN TRANSACTION 
        IF(@onGoingSession = 1)
        BEGIN
            IF(@timeDiff >= 360)
            BEGIN
                UPDATE MyTable 
                SET UserId = @UserId, Col4 = CURRENT_TIMESTAMP

                IF(@@ERROR = 0)
                BEGIN
                    SET @warning = RTRIM('An unfinsihed session for ' + 
                                   LTRIM(RTRIM(@userName)) + ' is going on for the past ' +    
                                   LTRIM(RTRIM(@timeDiff)) + ' minutes but updates from ' + LTRIM(RTRIM(@UserId)) + ' are successful')
                    COMMIT

                    RAISERROR(@warning,7, 1) 

                    RETURN
                END
                ELSE
                BEGIN
                    ROLLBACK

                    RETURN @@ERROR
                END
           END  
           ELSE
           BEGIN
               SET @error = RTRIM('A session of updates for '+ LTRIM(RTRIM(@userName))+ ' is already in progress concurrent updates are not allowed')

               IF(@@ERROR = 0)
               BEGIN
                   COMMIT

                   RAISERROR(@error, 8, 1)
                   RETURN
               END
               ELSE
               BEGIN
                   ROLLBACK

                   RETURN @@ERROR
               END
           END
       END
       ELSE IF(@onGoingSession = 0 AND @counterOfResync = 0)
       BEGIN
           UPDATE MyTable 
           SET UserId = @UserId, Col3 = 1, Col4 = CURRENT_TIMESTAMP

           IF(@@ERROR =0)
           BEGIN
               COMMIT

               RETURN
           END
           ELSE
           BEGIN
               ROLLBACK

               RETURN @@ERROR
           END
       END
       ELSE IF(@onGoingSession = 0 AND @counterOfResync > 0 AND @timeDiff >= 5)
       BEGIN
           UPDATE MyTable 
           SET Col3 = 1, CountOfResync = 0, UserId = @UserId, Col4 = 
  CURRENT_TIMESTAMP

           IF(@@ERROR = 0)
           BEGIN
               COMMIT

               RETURN
           END
           ELSE
           BEGIN
               ROLLBACK
               RETURN @@ERROR
           END
       END
       ELSE 
       BEGIN
           SET @error  = RTRIM('A server resync session already in progress, updates can''t be made at the same time')

           IF(@@ERROR = 0)
           BEGIN
               COMMIT
               RAISERROR(@error, 9, 1)
               RETURN
           END
           ELSE
           BEGIN
               ROLLBACK
               RETURN @@ERROR
           END
      END

      RETURN @@ERROR
END

【问题讨论】:

  • 如果你们不要投票给你们宝贵的建议,我将不胜感激。
  • 没有缩进的双倍行距代码不会让我们更容易为您提供帮助。也许你可以让它更整洁一点。然后有四个select 语句来获取MyTable 中允许的唯一行
  • @marc_s 非常感谢您编辑了我的代码。谢谢一堆。谁能回答我的问题,将不胜感激。
  • 在事务中移动初始查询并使用带有updlock 提示的单个select 获取值应该在不阻塞其他读取器的情况下序列化更新。如果您想在事务处于活动状态但未提交时防止读取陈旧值,那么您将需要类似排他锁的东西。
  • 是的,这就是我现在计划在事务中移动初始查询,在此之前Set ISOLATION LEVEL SERIALIZABLE 这样我将防止并发更新以及万一 2 个用户获得共享锁并且轮流更新行。然后,当一个更新另一个正在等待轮到它时,一旦第一个用户的事务完成,第二个用户就会点击任何IF 语句,因此它将绕过整个事务并返回错误/警告消息到调用系统或可能会更新。

标签: sql sql-server tsql


【解决方案1】:

类似以下内容会将事务简化为单个 update 语句,其中包含更新由 case 表达式处理的各个列的逻辑。后处理处理任何错误并生成预期的各种奇怪的结果。

create procedure dbo.MyTableProc( @UserId as Char(20) )
  as
  begin

  declare @CounterOfResync as TinyInt;
  declare @Error as Int;
  declare @ErrorMessage as VarChar(200);
  declare @OngoingSession as Bit = 1;
  declare @PriorUsername as Char(20);
  declare @TimeDiff as SmallInt;
  declare @WarningMessage as VarChar(300);

  update MyTable
    set
      @CounterOfResync = Col1,
      @PriorUsername = UserId,
      @OngoingSession = Col3,
      @TimeDiff = DateDiff( minute, Col4, Current_Timestamp ),
      UserId = @UserId,
      Col2 = case when Col3 = 0 and Col1 > 0 and DateDiff( minute, Col4, Current_Timestamp ) >= 5 then 1 else Col2 end,
      Col3 = case when Col1 = 0 and Col3 = 0 then 1 else Col3 end,
      Col4 = Current_Timestamp
    set @Error = @@Error;

    if ( @CounterOfResync = 1 )
      if ( @TimeDiff >= 360 )
        begin
        if ( @Error = 0 )
          begin
          set @WarningMessage = 'An unfinished session for user ' + @PriorUsername + ' is going on for the past ' +
            Cast( @TimeDiff as VarChar(10) ) + ' minutes but updates from ' + @UserId + ' are successful';
          RaIsError( @WarningMessage, 7, 1 );
          end
        else
          return @Error;
        end
      else
        begin
        if ( @Error = 0 )
          begin
          set @ErrorMessage = 'A session of updates for '+ @PriorUsername + ' is already in progress concurrent updates are not allowed';
          RaIsError( @ErrorMessage, 8, 1 );
          end
        else
          return @Error;
        end
    else
      if ( @OngoingSession = 0 and @CounterOfResync = 0 )
        return @Error
      else
        -- ...

对于在尝试浏览现有代码并将其转换为陌生代码时出现的任何错误,我们深表歉意。我的目的是提供一个你可以选择遵循的(错误)方向,而不是完整的代码。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-03-19
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多