使用 MySQL 数据库在 Node.js 中编写 API,我正在实现一个相当标准的模式:
If exists then update
else insert
这当然可以正常工作,直到同时向 SPI 发出多个请求,此时请求 2 上的
If exists
可以在插入请求 1 之前执行,从而导致两条记录而不是一条记录。
我知道处理这个问题的一种方法是确保数据库具有防止重复记录的约束或键,但在这种情况下,确定是否应该插入或更新的规则更加复杂,因此检查需要用代码完成。
这听起来像是使用互斥锁/锁的好例子。我需要将其分发,因为 API 可能有多个实例作为池/场的一部分运行。
我想出了以下实现:
try {
await this.databaseConnection.knexRaw().raw(`SELECT GET_LOCK('lock1',10);`);
await this.databaseConnection.knexRaw().transaction(async (trx) => {
const existing = await this.findExisting(id);
if (existing) {
await this.update(myThing);
} else {
await this.insert(myThing);
}
});
} finally {
await this.databaseConnection.knexRaw().raw(`SELECT RELEASE_LOCK('lock1');`);
}
这一切似乎工作正常,我的测试现在只产生一个插入。虽然看起来有点暴力/手动。作为 MySQL 和 Node 的新手(我有 C# 和 SQL Server 背景),这种方法是否合理?有更好的方法吗?
理智吗?主观。
技术上安全吗? 可能是 -
GET_LOCK()
是可靠的 - 但不像你写的那样。
您忽略了
GET_LOCK()
的返回值,如果获得了锁,则返回值为1;如果超时且您没有获得锁,则返回值为0;在某些失败情况下,返回值为NULL。
如所写,您将等待 10 秒钟,然后无论如何都可以完成工作,所以,不安全。
这假设您只有一个 MySQL master。 如果您有多个主节点或 Galera,则它将不起作用,因为 Galera 不会在所有节点上复制
GET_LOCK()
。 (Galera 集群是一个由可写主节点组成的高可用性 MySQL/MariaDB/Percona 集群,可同步复制,并且能够在最多 (ceil(n/2) - 1) 节点总数中的 (ceil(n/2) - 1) 的故障/隔离中幸存下来) .
SELECT ... FOR UPDATE
查找并锁定相关行,它会锁定找到的行,或者在某些情况下锁定它们存在的间隙,从而阻止试图捕获相同行的其他事务锁定直到您回滚或提交...但如果这不切实际,使用 GET_LOCK()
是有效的,但要遵守上面关于返回值的观点。