我的触发器有一个奇怪的问题。共有2个表:Table A
和Table 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
示例:
用户X目前的总和为15
,然后(两者之间几乎没有延迟):
5
7
在此表中我们持有总和,此用户的总和为15
触发器以这种方式更新此表:
20
22
< ---错了,这应该是27
正如您所看到的,没有插入任何数字2
,由于某种原因,它会增加7-5 = 2。
这怎么可能?为什么从7中减去5并将2加到总和而不是正常加7?
警告:这不会起作用,请检查已接受的答案
其中一个答案建议选择更新方法。
这个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;
如果有人需要一个非常快速的 临时 修复此问题,然后再进行实际操作。逻辑建议修复:我所做的是在PHP中的INSERT查询之前放置一个随机usleep(rand(1,500000))
,以减少同时插入的机会。
答案 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
已经有update
和Table_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){}
});
的数据进行操作。