问题描述
我的MariaDB数据库中有一个InnoDB表,该表获得大量写入和读取。这是一个类似“仅追加”的表格:
+-------------------+--------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+-------------------+--------------+------+-----+---------+----------------+
| id | int(11) | NO | PRI | NULL | auto_increment |
| data_id | int(11) | NO | | NULL | |
| data | varchar(225) | YES | | NULL | |
| creation_date | datetime | NO | | NULL | |
| deactivation_date | datetime | YES | | NULL | |
+-------------------+--------------+------+-----+---------+----------------+
当然,添加数据后,它会被插入。当数据“更改”时,我将旧记录标记为“已停用”(通过在此处设置字段值),然后插入新数据。
我的代码如下:
SELECT ... WHERE id IN (...) FOR UPDATE
foreach row {
// Do we have a value for this data_id
if(saves.contains(row.data_id)) {
// Is it actually NEW?
if(row.data != saves[row.data_id]) {
// disable the record (later)
disables.add(row.id)
} else {
// Preserve the data in the db
saves.remove(row.data_id);
}
}
foreach save {
INSERT...
}
foreach disables {
UPDATE ... SET deactivation_date=Now() WHERE id=?
}
那个伪代码很糟糕,但是我想你明白了。
过去,我首先对所有内容进行UPDATE
,然后INSERT
处理剩余的内容。这让我陷入僵局,因为(我认为)UPDATE正在获取表索引上的锁,然后 then 获得INSERT
的表级锁。这样一来,两个独立的事务就可以相互锁定,而一个事务将被回滚以使另一个继续。因此,我在代码中添加了一个简单的“尝试3次”逻辑。 :(
我还重新编写了代码以决定要做什么,然后然后采取行动,因此我总是先执行INSERT
步,然后执行UPDATE
步。但是有时我仍然陷入僵局。两个同时进行的事务重叠在它们正在修改的记录中是非常罕见的,因此SELECT ... FOR UPDATE
通常应该是当前未锁定的锁定行。
执行此UPDATE
+ INSERT
(或INSERT
+ UPDATE
)的最佳方法是什么,以尽量减少必须解决(重试)的死锁,自己的代码?是否有更好的技术,还是我应该继续检测死锁并简单地重试?
我计划很快移至galera Cluster,这将使事情变得更加复杂,并且我可能必须检测到由于集群中的另一个节点抱怨提交顺序而导致提交失败的情况,因此我可能不得不无论如何,都要执行交易重试。迁移到galera是否会完全改变上述问题的答案?
更新
我应该提到,这当然是在具有适当回滚之类的所有事务中发生的。
此外,此示例表并未完全捕获我的用例的复杂性,其中“ data_id”实际上是……很多事情。不仅如此,data_id不能完全匹配和替换,因此INSERT ... ON DUPLICATE KEY UPDATE
有两个原因:
- PK永远不会重复
- data_id对于“活动”记录不是唯一的,因此可以有多个具有相同data_id的记录。唯一需要禁用记录的时间是来自用户的数据与数据库中的数据不完全匹配时(即,我正在做多对多的区别以避免不必要的写操作)
解决方法
此外,戴夫(Dave)在评论中说:
- 将事务隔离级别设置为READ-COMMITTED。这样可以防止相邻的行被锁定。
- 将INSERT合并到一个多行INSERT中。注意-整个语句必须适合您的
max_allowed_packet
大小,因此对于大行数或大有效负载,您可能需要将其分解为几个INSERT语句。 - 如果您不愿意在auto_increment值中留空,则将innodb_autoinc_lock_mode设置为2。