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

时间:2015-12-23 03:13:18

标签: python database rest concurrency etag

也许我在这里忽略了一些简单而明显的东西,但这里有:

因此,HTTP请求/响应中的Etag标头的一个特性是强制并发,即多个客户端不能覆盖彼此对资源的编辑(通常在执行PUT请求时)。我认为那部分是众所周知的。

我不太确定的是后端/ API实现如何在没有竞争条件的情况下实际实现这一点;例如:

设定:

  • RESTful API位于标准关系数据库之上,使用ORM进行所有交互(例如SQL Alchemy或Postgres)。
  • Etag基于上次更新时间'资源
  • Web框架(Flask)位于多线程/流程网络服务器(nginx + gunicorn)的后面,因此可以同时处理多个请求。

问题:

  • 客户端1和2都请求资源(获取请求),两者现在都具有相同的Etag。
  • 客户端1和2都发送PUT请求以同时更新资源。 API接收请求,继续使用ORM从数据库中获取所需信息,然后将请求Etag与最后更新时间'进行比较。从数据库...他们匹配,所以每个都是一个有效的请求。每个请求都会继续,并将更新提交到数据库。
  • 每次提交都是同步/阻止事务,因此一个请求将在另一个请求之前进入,因此会覆盖其他请求。
  • 这是否打破了Etag的目的?

我能想到的唯一一个万无一失的解决方案就是让数据库在更新查询中执行检查。我错过了什么吗?

P.S由于使用了框架而被标记为Python,但这应该是语言/框架无关的问题。

3 个答案:

答案 0 :(得分:1)

这实际上是关于如何使用ORM进行更新的问题,而不是关于ETag的问题。

想象一下,两个流程同时将资金转入银行账户 - 他们都读取旧余额,添加一些,然后写入新余额。其中一次转移失败了。

当您使用关系数据库进行编写时,这些问题的解决方案是将读取+写入同一事务,然后使用SELECT FOR UPDATE读取数据和/或确保您具有适当的隔离级别设置。

各种ORM实现都支持事务,因此获取读取,检查和写入同一事务将很容易。如果设置SERIALIZABLE隔离级别,那么这足以修复竞争条件,但您可能必须处理死锁。

ORM通常也以某种方式支持SELECT FOR UPDATE。这将允许您使用默认的READ COMMITTED隔离级别编写安全代码。如果你谷歌SELECT FOR UPDATE和你的ORM,它可能会告诉你如何做。

两个情况下(可序列化隔离级别或select for update),数据库将通过在读取实体时锁定该实体的行来解决问题。如果另一个请求进入并尝试在事务提交之前读取实体,则会强制它等待。

答案 1 :(得分:0)

Etag可以通过多种方式实施,而不仅仅是last updated time。如果您选择纯粹基于Etag实施last updated time,那么为什么不使用Last-Modified标题?

如果您要将更多信息编码到Etag有关底层资源的信息中,您将不会受到上述概述的竞争条件的影响。

  

我能想到的唯一可靠的解决方案是让数据库在更新查询中执行检查。我错过了什么吗?

这是你的答案。

另一种选择是为每个资源添加一个版本,每次成功更新时都会增加一个版本。更新资源时,请在WHERE中指定ID和版本。另外,设置version = version + 1。如果资源自上次请求后已更新,则更新将失败,因为不会找到任何记录。这消除了锁定的需要。

答案 2 :(得分:0)

很正确,如果“检查最后一个标签”和“进行更改”不在一个原子操作中,那么您仍然可以获得比赛条件。

本质上,如果您的服务器本身具有竞争条件,则将etag发送给客户端将无济于事。

您已经提到了实现这种原子性的好方法:

我能想到的唯一简单的解决方案是也使数据库执行检查,例如在更新查询中。

您可以执行其他操作,例如使用互斥锁。或者使用两个线程无法处理相同数据的体系结构。

但是数据库检查对我来说似乎很好。您描述的关于ORM检查的内容可能是为了更好地显示错误消息,但它本身并不像您发现的那样足够。