RESTful API中使用的Etags仍然容易受到竞态条件的影响。

6
也许我在这里忽略了一些简单而明显的东西,但是还是让我们来看看吧:
HTTP请求/响应中Etag标头的一个特性是实现并发性,即使多个客户端不能覆盖资源的编辑(通常在进行PUT请求时)。我认为这部分是非常知名的。
我不确定的部分是后端/API实现如何在没有竞争条件的情况下实际实现这一点;例如:
设置:
- 基于ORM的RESTful API位于标准关系数据库之上(SQLAlchemy或Postgres)。 - Etag基于资源的“最后更新时间”。 - Web框架(Flask)位于多线程/进程Web服务器之后(nginx+gunicorn),因此可以同时处理多个请求。
问题:
- 客户端1和2都请求资源(get请求),现在它们有相同的Etag。 - 客户端1和2都同时发送PUT请求以更新资源。API接收到请求,然后使用ORM从数据库获取所需信息,然后将请求Etag与数据库中的“最后更新时间”进行比较...它们匹配,因此每个请求都是有效的。每个请求都继续并提交更新到数据库。 - 每个提交都是同步/阻塞事务,因此一个请求会在另一个请求之前完成,因此一个请求将覆盖其他更改。这不破坏Etag的目的吗?
我能想到的唯一可靠的解决方案是使数据库也执行检查,在更新查询中进行检查。我有什么遗漏的吗?
P.S由于使用的框架,这应该是一个与语言/框架无关的问题。
3个回答

2
这实际上是关于如何使用ORM进行更新的问题,而不是关于ETags的问题。
想象一下,有两个进程同时向银行账户转账 - 它们都读取旧余额,加上一些金额,然后写入新余额。其中一个转账被丢失了。
当您使用关系型数据库编写时,解决这些问题的方法是将读取和写入放在同一个事务中,然后使用SELECT FOR UPDATE来读取数据和/或确保设置适当的隔离级别。
各种ORM实现都支持事务,因此将读取、检查和写入放入同一事务中将很容易。如果设置SERIALIZABLE隔离级别,则足以解决竞争条件,但您可能需要处理死锁。
ORM通常也以某种方式支持SELECT FOR UPDATE。这将让您使用默认的READ COMMITTED隔离级别编写安全代码。如果您在Google中搜索SELECT FOR UPDATE和您的ORM,它可能会告诉您如何做到这一点。
在这两种情况下(串行化隔离级别或选择更新),数据库将通过在读取实体时对该行获取锁来解决问题。如果另一个请求在您的事务提交之前尝试读取实体,它将被迫等待。

1
Etag 可以有很多实现方式,不仅仅是基于 last updated time。如果你选择纯粹基于 last updated time 实现 Etag,那么为什么不直接使用 Last-Modified 标头呢?
如果将更多信息编码到 Etag 中,关于底层资源,您就不会受到上面概述的竞争条件的影响。
“我能想到的唯一的绝对可靠的解决方案是让数据库执行检查,例如在更新查询中。我错过了什么吗?” 这就是你的答案。
另一种选择是为每个资源添加一个版本号,在每次成功更新时递增。在更新资源时,在WHERE中同时指定ID和版本。此外,设置version = version + 1。如果自上次请求以来已更新资源,则更新将失败,因为找不到记录。这样可以避免锁定的需要。

关于你的第二段,那样怎么解决问题?你能举个例子吗? - Metalstorm
当然。Etag实际上只是资源的哈希,稍后用于确定资源是否已更改。如果您要对记录进行完整哈希,则可以在事务/阻塞部分中通过使用相同的算法对其进行哈希并将用户提供的哈希与DB中的当前记录进行比较。 - Evan
另一个选择是对数据进行版本控制。每当您更新底层资源时,您可以添加and version = xxx的约束条件。如果没有记录被更新,那么您可以假设该记录在客户端获取数据后已经被更新。在更新的上下文中,您将包括set version = version + 1 - Evan
版本号基本上是一个序列号,但除了最后更新时间和更新次数(这不是问题),它并没有提供更多的信息。 - Metalstorm
让我们在聊天中继续这个讨论 - Evan
显示剩余3条评论

1
你说得对,如果“检查上一个etag”和“进行更改”不在同一个原子操作中,则仍可能出现竞态条件。
实质上,如果服务器本身存在竞态条件,则向客户端发送etag也无济于事。
你已经提到了实现这种原子性的好方法:
唯一我能想到的绝对可靠的解决方案是让数据库执行检查,例如在更新查询中。
你可以做其他事情,比如使用互斥锁。或者使用两个线程无法处理相同数据的架构。
但是数据库检查对我来说很好。你描述的ORM检查可能是为了更好的错误消息而添加的,但是本身并不足够,正如你发现的那样。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接