我的问题有点复杂,我会尝试用一个例子来简化它。
假设我想实现一个支持SQL数据库的聊天系统。该 系统由一个房间组成,任何人都可以在其上发布消息。
要使用聊天,您需要安装应用(想想一个移动应用)。这个程序保持 设备上的历史记录并定期连接到服务器以获取 新消息。
我想解决的问题是如何只获取这些新消息而不会丢失 其中任何一个。
一个天真的实现将使用带有两列(id
AUTO_INCREMENT, message)
的SQL表。客户端连接,第一次获取历史记录
然后只询问id大于他们拥有的最后一个id的消息。
在实践中,这似乎不起作用,并且您有失踪的风险
消息。我制作了两个使用PostgreSQL的程序。一个程序只有
插入表中,另一个程序定期SELECT *
进行检查
在每次迭代时,没有新项目出现,其id小于
上一次迭代的最大ID。
换句话说,让我们在一次迭代中说你SELECT *
,得到ids 3,5和
9.然后在下一次迭代中,你得到3,5,8和9.一条新消息出现了
比上一次迭代的最大id小的id。这意味着
如果客户要求所有大于9的ID,则会错过8。
如果我不清楚,我可以提供程序的来源。
现在,如果我运行一个checker程序实例和5个实例 插入器,未命中每分钟发生2至3次。
我需要一个数据库架构才能实现应用程序可以实现的系统 仅获取自上次连接以来的新消息。我以SQL为例, 但我对任何其他奇特的NoSQL替代品持开放态度。如果有人知道多么真实 聊天系统,如Signal,Whatsapp或其他实施,我也会 感兴趣。
编辑:这些例子的表格如下,与上述情况略有不同:
create table test (id serial primary key, value int);
这是检查程序的代码:
#include <iostream>
#include <libpq-fe.h>
#include <stdio.h>
#include <stdlib.h>
#include <unordered_set>
int main(int argc, char** argv)
{
auto const conninfo = "dbname = test password = postgres";
PGconn* conn = PQconnectdb(conninfo);
if (PQstatus(conn) != CONNECTION_OK)
{
std::cerr << "error: " << PQerrorMessage(conn) << std::endl;
return 1;
}
std::unordered_set<uint64_t> vals;
uint64_t lastId = 0;
while (true)
{
PGresult* res = PQexec(conn, "SELECT id FROM test ORDER BY id");
if (PQresultStatus(res) != PGRES_TUPLES_OK)
{
std::cerr << "error: " << PQerrorMessage(conn) << std::endl;
return 1;
}
for (int i = 0; i < PQntuples(res); i++)
{
auto const id = std::atoll(PQgetvalue(res, i, 0));
if (id <= lastId)
{
if (!vals.count(id))
{
std::cout << id << " was missed" << std::endl;
vals.insert(id);
}
}
else
{
vals.insert(id);
lastId = id;
}
}
PQclear(res);
}
PQfinish(conn);
return 0;
}
以下是插件程序的代码:
#include <iostream>
#include <libpq-fe.h>
#include <stdio.h>
#include <stdlib.h>
#include <unordered_set>
int main(int argc, char** argv)
{
auto const conninfo = "dbname = test password = postgres";
PGconn* conn = PQconnectdb(conninfo);
if (PQstatus(conn) != CONNECTION_OK)
{
std::cerr << "error: " << PQerrorMessage(conn) << std::endl;
return 1;
}
while (true)
{
PGresult* res = PQexec(conn, "INSERT INTO test (value) VALUES (1)");
if (PQresultStatus(res) != PGRES_COMMAND_OK)
{
std::cerr << "error: " << PQerrorMessage(conn) << std::endl;
return 1;
}
PQclear(res);
}
PQfinish(conn);
return 0;
}
运行一个检查程序进程和一些5个插入程序(所有编译器至少编译为-O2),您将看到偶尔出现的“错过xxx”消息,这些消息证明在上一次迭代的最后一个id之前生成了id。 / p>
证明这一点的另一种方法如下。启动两个psql shell,1和2.我将命令在每个shell中按顺序输入:
1> create table test (id serial primary key, value int);
1> begin;
1> insert into test (value) values (1);
2> insert into test (value) values (2);
2> select * from test;
-- you see the row (id:2, value:2) in the table
1> commit;
2> select * from test;
-- now you see (id:1, value:1) and (id:2, value:2)
-- an id smaller than 2 has just appeared
答案 0 :(得分:1)
好吧,根据您自己的解释,您的问题很可能是您的实施不尊重您的“理论”方法。因为你没有提供MCVE,我会根据这个假设回答你。
如果您确实使用id
定义AUTO_INCREMENT
,那么您将永远不会遇到id
低于客户端的新消息的情况。
因此,当客户端写入新消息时,它没有id
并且在队列中推送到服务器。在客户端,您可以使用颜色编码(如灰色背景)将队列中的所有消息显示为“暂定”。
当服务器收到消息时,它会执行INSERT
并为消息分配ID。
下次发生来自服务器的更新时,客户端会更新自上一个最高id
值以来的消息列表。用户自己的消息将按顺序显示在聊天缓冲区中。然后可以同时丢弃队列中的所有消息。
然后,您可以保证数据始终是最新的,因为您的不变“新消息始终具有更高的ID”始终为真。
话虽这么说,最好混合使用ID和时间戳,并使用id
字段表示要显示数据的顺序,使用timestamp
查询所有更改自上次刷新以来然后,您可以支持消息可变性(修改或删除)。
虽然,这是一个免费的好建议:不要重新发明轮子,使用已经证明多年来运作良好的现有协议。您可以使用XMPP+MUC,SILC,IRC或Axolotl(multiuser上有一篇论文与之聊天。)
基本上,在互联网上聊天是一个问题已经解决了很多次,如果你认为你可以做得更好,不要重新发明轮子,只需添加你对现有堆栈的贡献。您可以与现有的工具,库,框架进行互操作。
关于你的编辑:
你遇到的问题有两个方面:
INSERT
语句的事务的原子性规则,特别是对于id的AUTO_INCREMENT
规则,以帮助实现高效的并发插入。因此,您对AUTO_INCREMENT
ed id将始终更高的考虑是对您的算法的错误假设。
作为解决方案,您可以:
gapless
increments,代价是每次在事务中插入时锁定数据库,AFTER INSERT
触发器来处理插入后自动递增非自动增量id
值,