【问题标题】:Why is my UPDATE stored procedure executed multiple times?为什么我的 UPDATE 存储过程执行了多次?
【发布时间】:2018-07-17 11:32:03
【问题描述】:

我使用存储过程来管理仓库。 PDA 扫描仪扫描添加的库存并将其批量(插入后)发送到 SQL 数据库 (SQL Server 2016)。

SQL 数据库相当偏远(其他国家/地区),因此某些查询有时会出现延迟,但这个特定的查询是有问题的:即使 stock 表没问题,我在 更新仓库地点。 PDA 将每个点中添加的项目作为 SMALLINT 进行跟踪,然后将此值发送回下面的存储过程。

PDA“send_spots”查询:

SELECT spot, added_items FROM spots WHERE change=1

存储过程:

CREATE PROCEDURE [dbo].[update_spots]
@spot VARCHAR(10),
@added_items SMALLINT
AS
BEGIN
    BEGIN TRAN
    UPDATE storage_spots
    SET remaining_capacity = remaining_capacity - @added_items
    WHERE storage_spot=@spot
    IF @@ROWCOUNT <> 1
    BEGIN
        ROLLBACK TRAN
        RETURN - 1
    END
    ELSE
    BEGIN
        COMMIT TRAN
        RETURN 0
    END
END
GO

如果remaining_capacity 的值为0,则PDA 无法在下一轮向其添加更多项目。但是在这个过程中,我得到了负值,因为据称查询运行了两次(因此减去了两次 @added_items)。

有没有办法做到这一点?我该如何解决?据我了解,如果受影响的行是!= 1,则事务应该被取消(回滚),但也许那是别的东西。

编辑:在@Zero 的帮助下的当前解决方案:

CREATE PROCEDURE [dbo].[update_spots]
    @spot VARCHAR(10),
    @added_racks SMALLINT
AS
BEGIN
    -- Recover current capacity of the spot
    DECLARE @orig_capacity SMALLINT
    SELECT TOP 1
        @orig_capacity = remaining_capacity
    FROM storage_spots
    WHERE storage_spot=@spot

    -- Test if double is present in logs by comparing dates (last 10 seconds)
    DECLARE @is_double BIT = 0
    SELECT @is_double = CASE WHEN EXISTS(SELECT *
    FROM spot_logs
    WHERE log_timestamp >= dateadd(second, -10, getdate()) AND storage_spot=@spot AND delta=@added_racks)
    THEN 1 ELSE 0 END

    BEGIN
        BEGIN TRAN
        UPDATE storage_spots
        SET remaining_capacity= @orig_capacity - @added_racks
        WHERE storage_spot=@spot

        IF @@ROWCOUNT <> 1 OR @is_double <> 0
            -- If double, rollback UPDATE
            ROLLBACK TRAN
        ELSE
            -- If no double, commit UPDATE
            COMMIT TRAN

        -- write log
        INSERT INTO spot_logs
            (storage_spot, former_capacity, new_capacity, delta, log_timestamp, double_detected)
        VALUES 
            (@spot, @orig_capacity, @orig_capacity-@added_racks, @added_racks, getdate(), @is_double)
    END
END
GO

【问题讨论】:

  • 你应该检查调用这个过程的任何代码,因为这个过程看起来很好。要对其进行测试,请尝试直接运行此过程并查看它是否执行了两次(它应该只执行一次)。
  • 您采取了哪些措施来确保传入的 added_items 不大于数据库中的剩余空间。 Remaining_space = 5,added_items 传入 = 10,这会将剩余空间更新为 -5,而 proc 不会运行两次。
  • 如果您要实施审计表 - 即记录每个 UPDATE 的详细信息(值、日期时间、用户等),这将更容易诊断,因为它可能是如何使用系统的(并发更改例如)而不是数据库逻辑本身的问题。
  • Remaining_space = 5, added_items pass in = 10, remaining_space to -5 这种情况是不可能的,因为 PDA 应用程序确保您不能扫描超过剩余空间.有一种情况,两个(或多个)PDA 可以各自添加项目以溢出 remaining_space 值,但之前物理空间会被填满。
  • 不能解决您当前的问题(您有两个单独的过程来读取和更新数据,并且没有事务来确保一致性),但我通常会将此建模为插槽具有固定的 容量,然后根据项目记录它们占用的插槽及其容量/容量使用情况。 remaining_capacity 是从这两者中派生的数据。存储派生数据只是提供了不一致的机会(与其他数据、与现实等)

标签: sql-server stored-procedures


【解决方案1】:

我正在考虑可能的原因(以及追踪它们的方法),然后它击中了我 - 你没有价值验证!

这里有一个简单的例子来说明问题:

 Spot | capacity 
 ---------------
  x1  |     1

Update spots set capacity = capacity - 2 where spot = 'X1'

您的扫描仪很可能给您提供的数量超出了您的承受能力。 我不确定您的业务逻辑是如何进行的,但是您需要按照

行执行某些操作
Update spots set capacity = capacity - @added_items where spot = 'X1' and capacity  >= @added_items
if @@rowcount <> 1;
   rollback;

编辑:在不实施验证的情况下跟踪问题的几种方法: 创建一个日志表(带有timestampuser id(连接到数据库的用户)、session idold valuenew valuedelta value(添加的项目)。


选项 1: 记录所有将值从正变为负的更新(至少在您找出问题之前)。 此选项的缺点是它不会注册不会导致负容量的双重调用。


选项 2:(记录相同的更新): 创建一个脚本,该脚本创建一个全局临时表并从该表中删除记录,时间戳早于...假设每分钟 10 分钟左右一次(玩弄数字)。

这个临时表应该保存传递给你的更新语句的数据,所以'spot',' added_items' + 'timestamp'(用于跟踪)。 现在是关键部分:当您调用更新语句时,检查临时表中是否存在类似的记录(相同的位置和 added_items 以及当前时间戳为between timestamp and [timestamp + 1 second] - 再次使用数字)。如果存在这样的记录,则记录该更新,否则将其添加到临时表中。

这将注册在 1 秒内(或您选择的任何时间范围)进行的相同更新。

【讨论】:

  • 我故意没有价值验证,因为我希望仓库人员能够添加比可能更多的项目,然后在他们意识到物理容量超载时纠正它。但是我没有考虑到我当前的多次执行问题,所以这可能是解决方案(即使客户不喜欢它)。事实是,偶尔连接的设备没有完美的解决方案,但我会试试你的方法。
  • 在这种情况下,做一些不同的事情:为那个 1 函数创建一个日志表并记录旧值、新值和增量值(added_items)(和user id + session id + timestamp)每次从正面更新为负面。这样,您将能够检查您的函数是否以及何时被调用两次并产生否定结果。至少要做到这一点,直到您确定该问题是一个问题还是只是缺乏验证的问题。
  • 谢谢!我会选择你的第二个选项,看起来很有希望。我必须等待它在生产中部署,所以明天我会看到它是否记录了相同的更新(为了安全起见,我添加了“userid”和“pda_id”)。 UPDATE 问题发生在晚上(今天早上 1 点左右),所以也许这个时间有服务器可用性的链接。
  • "option 2" 成功了,谢谢!我们能够记录所有双重记录并使用这些日志回滚更新,所有这些都无需更改 PDA 的代码。我将使用最终的存储过程编辑我的原始帖子。
【解决方案2】:

我找到了here 一个替代 SQL 查询,它按照我需要的方式进行更新,但使用 DECLARE 提供了一个临时值。在我的情况下会更好吗,还是我的初始查询正确?

初始查询:

UPDATE storage_spots
    SET remaining_capacity = remaining_capacity - @added_items
    WHERE storage_spot=@spot

替代查询:

DECLARE @orig_capacity SMALLINT
SELECT TOP 1 @orig_capacity = remaining_capacity 
    FROM storage_spots 
    WHERE spot=@spot
UPDATE Products 
    SET remaining_capacity = @orig_capacity - @added_items 
    WHERE spot=@spot

另外,我应该去掉 ROLLBACK/COMMIT 指令吗?

【讨论】:

  • 据我所知,它做完全相同的事情和事务语句的一般规则:如果您执行超过 1 个数据库操作(在您的情况下选择和更新) - 使用事务。例如,如果选择失败,您应该回滚(如果它没有选择任何东西,@orig_capacity 将为空并导致异常。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2010-12-28
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-06-18
  • 1970-01-01
相关资源
最近更新 更多