触发器正在计算错误的总和,从而产生意外结果

时间:2016-07-09 19:31:19

标签: mysql triggers

我的触发器有一个奇怪的问题。共有2个表:Table ATable B

每当插入一行Table A时,此表中列的总和就会插入Table B

一开始工作正常,但最近我发现当用户在确切时间插入> 1行时,触发器会以奇怪的方式返回总和。

CREATE TRIGGER `update_something` AFTER INSERT ON `Table_A` 

FOR EACH ROW BEGIN

    DECLARE sum BIGINT(20);

    SELECT IFNULL(SUM(number), 0) INTO sum FROM Table_A WHERE `user` = NEW.user;

    UPDATE Table_B SET sum_number = sum WHERE id = NEW.id;

END

示例:

表A

用户X目前的总和为15,然后(两者之间几乎没有延迟):

  1. 为他插入了号码5
  2. 为他插入了号码7
  3. 表B

    在此表中我们持有总和,此用户的总和为15

    触发器以这种方式更新此表:

    1. 20
    2. 22< ---错了,这应该是27
    3. 正如您所看到的,没有插入任何数字2,由于某种原因,它会增加7-5 = 2。

      这怎么可能?为什么从7中减去5并将2加到总和而不是正常加7?

      编辑1:

        

      警告:这不会起作用,请检查已接受的答案

      其中一个答案建议选择更新方法。

      这个SELECT ... FOR UPDATE是否会以极大的方式对性能产生负面影响?

      CREATE TRIGGER `update_something` AFTER INSERT ON `Table_A` 
      
      FOR EACH ROW BEGIN
      
          DECLARE sum BIGINT(20);
      
          SELECT IFNULL(SUM(number), 0) INTO sum FROM Table_A WHERE `user` = NEW.user FOR UPDATE; 
      
          UPDATE Table_B SET sum_number = sum WHERE id = NEW.id;
      
      END
      

      基本上我们只将FOR UPDATE添加到SELECT行的末尾,它会在InnoDB中执行Row Lock来解决问题吗?

      SELECT IFNULL(SUM(number), 0) INTO sum FROM Table_A WHERE user = NEW.user FOR UPDATE;

      编辑2(临时修复):

      如果有人需要一个非常快速的 临时 修复此问题,然后再进行实际操作。逻辑建议修复:我所做的是在PHP中的INSERT查询之前放置一个随机usleep(rand(1,500000)),以减少同时插入的机会。

2 个答案:

答案 0 :(得分:3)

此行为的原因是插入的数据仅在触发器完成执行时提交到数据库。因此,当两个插入操作(5和7)并行执行触发器时,它们会读取事务中的数据,即提交的数据及其在自己的事务中所做的更改,而不是在任何其他正在进行的事务中所做的更改

表A中的已提交数据对于两个事务总计最多为20,并且添加了在其自己的事务中插入的记录。对于一个这是5,对于另一个它是7,但由于这些记录尚未提交,另一个事务没有看到这个值。

这就是为什么一个是20 + 5,另一个是20 + 7。然后,交易一个接一个地更新表B(因为表B将在更新期间被锁定,直到交易结束),而最新的那个"会赢得"。

要解决此问题,请不要从表A中读取总和,但请在表B中保留一笔运行金额:

CREATE TRIGGER `update_something` AFTER INSERT ON `Table_A` 
FOR EACH ROW BEGIN
    UPDATE Table_B SET sum_number = sum_number + NEW.number WHERE id = NEW.id;
END;
/

我认为您delete已经有updateTable_B的触发器,否则您会有另一个不一致的来源。

所以这些也需要(重新)写出来:

CREATE TRIGGER `delete_something` AFTER DELETE ON `Table_A` 
FOR EACH ROW BEGIN
    UPDATE Table_B SET sum_number = sum_number - OLD.number WHERE id = OLD.id;
END;
/
CREATE TRIGGER `update_something` AFTER UPDATE ON `Table_A` 
FOR EACH ROW BEGIN
    UPDATE Table_B SET sum_number = sum_number - OLD.number WHERE id = OLD.id;
    UPDATE Table_B SET sum_number = sum_number + NEW.number WHERE id = NEW.id;
END;
/

这样可以防止在触发器中锁定可能的很多行。

然后,在完成上述操作后,您可以解决过去的问题,并进行一次性更新:

update Table_B
join   (select   id, user, ifnull(sum(number),0) sum_number 
        from     Table_A 
        group by id, user) A
     on Table_B.id = A.id
    and Table_B.sum_number <> A.sum_number
set     Table_B.sum_number = A.sum_number;

答案 1 :(得分:1)

你得到这个是因为触发器中的竞争条件。两个触发器同时被触发,因此SELECT ... FOR UPDATE为它们返回相同的值 - 15.然后首先触发更新值,添加5并产生20,然后第二次更新以15 + 7运行= 22。

您应该使用FOR UPDATE代替。这样,如果第一个触发器发出select,那么第二个触发器必须等到第一个触发器完成。

编辑:

你的问题让我想到了,也许使用Table A并不是最好的解决方案。根据{{​​3}}:

  

对于搜索遇到的索引记录,SELECT ... FOR UPDATE会锁定行和任何关联的索引条目,就像为这些行发出UPDATE语句一样。

并且因为您正在选择来自Table B的条目总和,它将锁定这些条目,但仍然允许插入新条目,因此问题将无法解决。

here所示,最好只根据触发器内bakedGoods.set({ data: [{key: "key1", value: "value1"}, {key: "key2", value: "value2"}), storageTypes: ["indexedDB"], complete: function(byStorageTypeResultDataObj, byStorageTypeErrorObj){} }); 的数据进行操作。