在不使用索引的情况下防止插入重复项

Posted

技术标签:

【中文标题】在不使用索引的情况下防止插入重复项【英文标题】:Preventing insertion of duplicates without using indices 【发布时间】:2020-02-03 03:28:56 【问题描述】:

我有一个 MariaDB 表 users,大致如下:

id INT PRIMARY KEY AUTOINCREMENT,
email_hash INT, -- indexed
encrypted_email TEXT,
other_stuff JSON

出于隐私原因,我无法在数据库中存储实际的电子邮件。

用于电子邮件的加密不是一对一的,即一封电子邮件可以被加密为许多不同的加密表示。这使得在 encrypted_email 列上添加索引毫无意义,因为它永远不会捕获重复项。

数据库中已有数据,更改加密方式或散列方式是不可能的。

email_hash 列也不能有唯一索引,因为它应该是一个短散列来加速重复检查。它不能太独特,因为它会使所有隐私保证无效。

如何防止两个具有相同电子邮件的条目出现在数据库中?

另一个限制:根据文档https://mariadb.com/kb/en/library/lock-tables/,我可能无法使用LOCK TABLE

LOCK TABLES 在使用 Galera 集群时不起作用。与 Galera 一起使用时,您可能会遇到崩溃或锁定。

LOCK TABLES implicitly 提交活动事务,如果有的话。此外,启动事务总是会释放使用LOCK TABLES 获取的所有表锁。

(我确实使用 Galera 并且我确实需要事务,因为插入新用户会伴随着其他几个插入和更新)


由于后端应用程序服务器(单体)只要不存储个人信息(例如用于发送电子邮件、验证登录等),就可以处理个人信息,因此我在应用程序中进行了重复检查。

目前,我正在做这样的事情(伪代码):

perform "START TRANSACTION"
h := hash(new_user.email)
conflicts := perform "SELECT encrypted_email FROM users WHERE email_hash = ?", h
for conflict in conflicts :
    if decrypt(conflict) == new_user.email :
        perform "ROLLBACK"
        return DUPLICATE
e := encrypt(new_user.email)
s := new_user.other_stuff
perform "INSERT INTO users (email_hash, encrypted_email, other_stuff) VALUES (?,?,?)", h, e, s
perform some other inserts as part of the transaction
perform "COMMIT"
return OK

如果两次尝试及时分开,效果很好。但是,当两个线程尝试同时添加同一个用户时,两个事务并行运行,执行选择,查看没有冲突的重复项,然后都继续添加用户。如何防止这种情况发生,或者至少优雅地立即恢复?


这是比赛的样子,简化了:

两个线程开始他们的事务

两个线程都执行选择,并且选择在两种情况下都返回零行。

两个线程都假设不会有重复。

两个线程都添加了用户。

两个线程都提交事务。

现在有两个用户使用相同的电子邮件。

【问题讨论】:

other stuff 中是否有任何内容可能有助于唯一地识别个人(即使他们只能将其拉到少数匹配项而不能完全唯一地识别他们)?我的想法是,如果这可行,您可以检查所有匹配项...... @user2366842 没有。至于“把它拉到少数匹配”,这就是哈希的用途。 “一封电子邮件可以被加密为许多不同的加密表示”——也许你的意思正好相反? @RickJames 不。我的意思是每封电子邮件都可以使用不同的初始化向量甚至不同的密钥进行加密,因此我无法检测到仅比较加密数据的重复电子邮件。 听起来您已经无法解密。正如其他人所说,单向哈希可能是一种更好的去重方法。 【参考方案1】:

评论太长了。

你不能。您有一个字段,其中一封电子邮件获得多个值。这对于识别重复值没有用。

您有另一个字段,其中多封电子邮件具有相同的值。这只会引发重复的错误错误。

如果您想防止重复,那么我会建议一种更强大的散列机制,可以大大减少冲突,以便您可以使用它。否则,您需要在 PII 墙后面进行验证。

【讨论】:

"否则,您需要在 PII 墙后面进行验证。"是的,这就是我目前正在做的事情,但效果不佳。我不期望一个纯 SQL 解决方案。任何提示如何使它工作? @KarolS 。 . .使用不太容易发生冲突的不同散列函数。一个四字节的哈希结果是不够的。【参考方案2】:

评论也太长了:

为防止表中出现重复条目​​,您应该使用唯一索引,以便 MariaDB 能够检测到重复项。

4 字节散列/校验和 (INT) 不够独特,可能有太多冲突。您应该在表中存储加密密码(例如,使用 AES-256-CTR 或任何其他分组密码对其进行加密),而不是校验和,密钥和 iv(初始化向量)应存储在客户端上。现在每个加密值都是唯一的,为了安全起见,加密值和密钥/iv 存储在不同的位置。

/* Don't send plain password, e.g. by using MariaDB's aes_encryot function
   we encrypt it already on client*/
encrypted_unique_email= aes_256_ctr_encrypt(plain_pw);
encrypted_email=encrypt(user.email);
execute("INSERT INTO users VALUES (NULL, encrypted_unique_email, encrypted_email, other_stuff) ...

但是,此解决方案仅适用于空表,因为您可能无法解密现有记录。

在这种情况下,您的提议可能是最好的解决方案,但是您需要通过LOCK TABLE users WRITE 锁定用户表并使用UNLOCK TABLES 解锁以防止不一致。

【讨论】:

根据我的阅读,LOCK TABLE 不适用于事务 LOCK TABLE 适用于事务。你甚至可以在解锁后回滚。 "LOCK TABLES 隐式提交活动事务,如果有的话。此外,启动事务总是释放所有使用 LOCK TABLES 获取的表锁。" mariadb.com/kb/en/library/lock-tables 另外,我使用 Galera 和 acc。到文档 LOCK TABLES 不适用于它。【参考方案3】:

除非您可以强制代码连续运行,否则您的伪代码可能会出现竞争条件。也就是说,一次只有一个请求可以尝试插入电子邮件。您在伪代码中显示的整个代码块必须在 critical section 中。

如果你不能使用 LOCK TABLES,你可以试试MariaDB's GET_LOCK() function。我不确定这是否与 Galera 兼容,这有待您研究。

如果这不可能,您将不得不找到其他方法来强制该代码块串行运行。您尚未描述您的编程语言或应用程序部署架构。也许你可以使用某种distributed lock server in Redis 或类似的东西。

但即使您可以做到这一点,让代码串行运行,这也可能会在您的应用程序中造成瓶颈。一次只有一个线程能够插入一封新电子邮件,您可能会发现它们排队等待全局锁定。

抱歉,这是该系统限制的结果,因为您无法使用唯一键来实现它,这将是正确的方法。

祝你好运。

【讨论】:

我同意这个答案。鉴于 OP 概述的约束,mysql/MariaDB 无法强制执行唯一性。 +10【参考方案4】:

您需要添加另一列并使用它来存储一些从电子邮件到一些可比较输出的一对一、无冲突且不可恢复的投影。采用任意一种非对称密码算法,生成公私密钥对,然后销毁私钥并存储公钥以加密电子邮件。非对称加密的工作方式,即使攻击者获得了您用于加密电子邮件的公钥,也无法恢复私钥。

但是,请注意,这种方法与存储未加盐的哈希具有相同的漏洞:如果攻击者掌握了您的整个数据库、公钥和算法,他们可以使用一些已知的 e-邮件字典,并成功找到加密形式的匹配电子邮件,从而将系统中的帐户与实际电子邮件匹配。决定这种情况是否是实际的安全风险取决于您和您的 ITSec 部门;但我认为不应该,因为你似乎有decrypt 功能可用,所以如果攻击者已经可以访问数据库和系统内部,他们可以解密存储的电子邮件。

您可以更进一步,将这些加密的电子邮件存储在一个单独的表中与用户没有任何关系。当向users 插入新行时,请确保在该表中也插入一行。结合唯一索引和事务,这将确保没有重复;但是,管理更改和删除将变得更加麻烦。除了知道是的,他的一些已知电子邮件已在系统中注册之外,潜在的攻击者实际上将一无所获。

否则,您只需确保对users 表的写入始终在 DB 之前的软件层上序列化。编写一个微服务,对用户存储请求进行排队,并禁止以任何其他方式修改users

【讨论】:

【参考方案5】:

SELECT 末尾添加FOR UPDATE

另外,由于您使用的是 Galera,您必须检查COMMIT 之后的错误。 (即报告与其他节点的冲突。)

【讨论】:

FOR UPDATE 应该如何提供帮助?两个并发事务不会锁定任何行,因为另一个事务还没有添加它的新条目。 @KarolS - FOR UPDATE,在这种情况下,宣布可以在该区域插入一行。 @KarolS - select email_hash from users where email_hash = 123 limit 1 for update; 锁定第二个连接,直到第一个连接提交。即使桌子是空的。在 MariaDB 10.3.13 上测试。 (大概在 InnoDB 中一直都是这种情况。) 好的,所以我刚刚对此进行了测试,它似乎可以正常工作。谢谢。我不知道 FOR UPDATE 是这样工作的。

以上是关于在不使用索引的情况下防止插入重复项的主要内容,如果未能解决你的问题,请参考以下文章

mysql防止重复插入记录方法总结

数据库表中不建索引,在插入数据时,通过sql语句防止重复添加

MySQL可重复读防止幻读

仅在不存在的情况下在 Python 中注册 Prometheus 指标

防止从 ZipOutPutStream 中删除重复项

如何防止将重复数据插入到值为多个的 SQL Server 表中