我一直在寻找一种方法来设计我的 API,使其具有幂等性,这意味着其中一些方法是为了使我的 POST 请求路由幂等,我偶然发现了 this 文章。
(如果我理解的不对,请纠正我!)
其中对总体思路有很好的解释。但缺少的是他自己实现的一些例子。
有人问文章作者,他怎么保证原子性?所以作者添加了一个代码示例。
本质上,在他的代码示例中有两种情况,
如果一切顺利的话流程:
Idempotency-key
键和值,这是对客户端的响应代码内部出现问题时的流程:
注意,打开的事务是针对某个DB的,我们称他为A。 但是,这与他也使用的redis存储无关,这意味着事务的回滚只会影响DB A。
因此它涵盖了代码内部发生某些事情导致无法完成交易的情况。
但是,如果运行代码的机器崩溃了,而它已经执行了
Set expire time to that key
并且现在即将运行事务的提交,会发生什么?
在这种情况下,密钥将在 Redis 存储中可用,但事务尚未提交。 这将导致服务确定所需的更改已经发生,但他们没有发生,机器在完成它之前就失败了。
我需要以这样的方式设计API:如果redis中数据的更改或键和值的设置失败,它们都会回滚。
这个问题有什么解决办法吗?
如何保证在一个数据库中更改所需数据的原子性,同时在redis中设置密钥和所需响应,如果其中任何一个失败,则将它们都回滚? (包括机器在动作中途崩溃的情况)
回答时请添加代码示例!我使用与文章中相同的技术(nodejs、redis、mongo - 用于数据本身)
谢谢:)
根据您在问题中共享的代码示例,您想要的行为是确保在将幂等键设置到 Redis 中表示此事务已经发生的时刻和事务发生的时刻之间,服务器上没有崩溃,事实上,保存在您的数据库中。
但是,当 Redis 和另一个数据库一起使用时,您会遇到两个独立的故障点,并且两个操作在不同时刻顺序执行(即使它们同时异步执行,也不能保证服务器不会崩溃)其中任何一个已完成)。
您可以做的是在事务中包含一条插入语句,该语句指向保存此请求的相关信息(包括幂等键)的表。由于 ACID 属性确保原子性,它保证事务上的所有语句都成功执行,或者都不执行,这意味着如果事务成功,您的幂等性密钥将在数据库中可用。
您仍然可以使用 Redis,因为它会提供比数据库更快的结果。
下面提供了一个代码示例,但最好考虑一下插入 Redis 和数据库之间的失败与您的业务之间的相关性(是否可以用其他策略来处理?)以避免过度设计。
async function execute(idempotentKey) {
try {
// append to the query statement an insert into executions table.
// this will be persisted with the transaction
query = ```
UPDATE firsttable SET ...;
UPDATE secondtable SET ...;
INSERT INTO executions (idempotent_key, success) VALUES (:idempotent_key, true);
```;
const db = await dbConnection();
await db.beginTransaction();
await db.execute(query);
// we're setting a key on redis with a value: "false".
await redisClient.setAsync(idempotentKey, false, 'EX', process.env.KEY_EXPIRE_TIME);
/*
if server crashes exactly here, idempotent key will be on redis with false as value.
in this case, there are two possibilities: commit to database suceeded or not.
if on next request redis provides a false value, query database to verify if transaction was executed.
*/
await db.commit();
// you can now set key value to true, meaning commit suceeded and you won't need to query database to verify that.
await redis.setAsync(idempotentKey, true);
} catch (err) {
await db.rollback();
throw err;
}
}