在 REST Web 服务中处理批处理操作的模式?
Posted
技术标签:
【中文标题】在 REST Web 服务中处理批处理操作的模式?【英文标题】:Patterns for handling batch operations in REST web services? 【发布时间】:2010-10-05 09:45:41 【问题描述】:对于 REST 样式 Web 服务中的资源批处理操作存在哪些经过验证的设计模式?
我试图在性能和稳定性方面在理想和现实之间取得平衡。我们现在有一个 API,所有操作都可以从列表资源(即:GET /user)或单个实例(PUT /user/1、DELETE /user/22 等)中检索。
在某些情况下,您希望更新整组对象的单个字段。来回发送每个对象的整个表示来更新一个字段似乎非常浪费。
在 RPC 风格的 API 中,您可以有一个方法:
/mail.do?method=markAsRead&messageIds=1,2,3,4... etc.
这里的 REST 等价物是什么?还是可以偶尔妥协。添加一些真正提高性能的特定操作是否会破坏设计?现在所有情况下的客户端都是 Web 浏览器(客户端的 javascript 应用程序)。
【问题讨论】:
【参考方案1】:一点也不——我认为 REST 等价物(或至少一种解决方案)几乎完全一样——一种专门设计的接口可适应客户端所需的操作。
我想起了 Crane 和 Pascarello 的书 Ajax in Action(顺便说一句,是一本极好的书——强烈推荐)中提到的一种模式,他们在其中说明了实现 CommandQueue 类型的对象,其工作是将请求排队成批,然后定期将它们发布到服务器。
如果我没记错的话,该对象本质上只是包含一个“命令”数组——例如,为了扩展您的示例,每个记录都包含一个“markAsRead”命令、一个“messageId”,可能还有一个对回调/处理函数——然后根据一些时间表,或者根据一些用户操作,命令对象将被序列化并发布到服务器,客户端将处理随后的后处理。
我碰巧没有手头的详细信息,但听起来这种命令队列将是处理您的问题的一种方法;它会大大减少整体的闲聊,并且会以一种你可能会发现更灵活的方式抽象服务器端接口。
更新:啊哈!我在网上从那本书中找到了一个片段,并附有代码示例(尽管我仍然建议拿起真正的书!)。 Have a look here,从第 5.5.3 节开始:
这很容易编码,但可能会导致 很多非常小的流量 服务器,效率低下, 可能令人困惑。如果我们想 控制我们的流量,我们可以捕获 这些更新并在本地排队 然后将它们发送到服务器 在我们闲暇时分批。一个简单的 用 JavaScript 实现的更新队列 如清单 5.13 所示。 [...]
队列维护两个数组。
queued
是一个数字索引数组,到 附加了哪些新更新。sent
是一个关联数组,包含 那些已发送到的更新 服务器,但正在等待 回复。
这里有两个相关的函数——一个负责将命令添加到队列(addCommand
),一个负责序列化然后将它们发送到服务器(fireRequest
):
CommandQueue.prototype.addCommand = function(command)
if (this.isCommand(command))
this.queue.append(command,true);
CommandQueue.prototype.fireRequest = function()
if (this.queued.length == 0)
return;
var data="data=";
for (var i = 0; i < this.queued.length; i++)
var cmd = this.queued[i];
if (this.isCommand(cmd))
data += cmd.toRequestString();
this.sent[cmd.id] = cmd;
// ... and then send the contents of data in a POST request
这应该会让你继续前进。祝你好运!
【讨论】:
谢谢。这与我的想法非常相似,如果我们将批处理操作保留在客户端上,我将如何前进。问题是对大量对象执行操作的往返时间。 嗯,好的——我以为你想通过轻量级请求对大量对象(在服务器上)执行操作。我误会了吗? 是的,但我看不出该代码示例将如何更有效地执行操作。它对请求进行批处理,但仍一次将它们发送到服务器。我误解了吗? 实际上它将它们批量化,然后一次全部发送:fireRequest() 中的 for 循环本质上是收集所有未完成的命令,将它们序列化为字符串(使用 .toRequestString(),例如,“方法=markAsRead&messageIds=1,2,3,4"),将该字符串分配给“data”,并将数据 POST 到服务器。【参考方案2】:我很想在您的示例中这样的操作中编写一个范围解析器。
制作一个可以读取“messageIds=1-3,7-9,11,12-15”的解析器并不麻烦。它肯定会提高覆盖所有消息的一揽子操作的效率,并且更具可扩展性。
【讨论】:
良好的观察和良好的优化,但问题是这种请求风格是否可以与 REST 概念“兼容”。 嗨,是的,我明白了。优化确实使这个概念更加 RESTful,我不想仅仅因为它偏离主题而放弃我的建议。【参考方案3】:批处理的简单 RESTful 模式是利用集合资源。例如,一次删除多条消息。
DELETE /mail?&id=0&id=1&id=2
批量更新部分资源或资源属性稍微复杂一些。也就是说,更新每个markedAsRead 属性。基本上,不是将属性视为每个资源的一部分,而是将其视为放置资源的存储桶。已经发布了一个示例。我稍微调整了一下。
POST /mail?markAsRead=true
POSTDATA: ids=[0,1,2]
基本上,您正在更新标记为已读的邮件列表。
您也可以使用它来将多个项目分配给同一类别。
POST /mail?category=junk
POSTDATA: ids=[0,1,2]
执行 iTunes 风格的批量部分更新显然要复杂得多(例如,艺术家 + 专辑标题但不是 trackTitle)。桶类比开始失效。
POST /mail?markAsRead=true&category=junk
POSTDATA: ids=[0,1,2]
从长远来看,更新单个部分资源或资源属性要容易得多。只需使用子资源即可。
POST /mail/0/markAsRead
POSTDATA: true
或者,您可以使用参数化资源。这在 REST 模式中不太常见,但在 URI 和 HTTP 规范中是允许的。分号将资源中的水平相关参数分开。
更新几个属性,几个资源:
POST /mail/0;1;2/markAsRead;category
POSTDATA: markAsRead=true,category=junk
更新几个资源,只有一个属性:
POST /mail/0;1;2/markAsRead
POSTDATA: true
更新几个属性,只需要一个资源:
POST /mail/0/markAsRead;category
POSTDATA: markAsRead=true,category=junk
RESTful 创造力比比皆是。
【讨论】:
有人可能会说您的删除实际上应该是一个帖子,因为它实际上并没有破坏该资源。 没必要。 POST 是一种工厂模式方法,它不如 PUT/DELETE/GET 明确和明显。唯一的期望是服务器将根据 POST 决定做什么。 POST 与往常一样,我提交表单数据,服务器会做一些事情(希望是预期的)并给我一些关于结果的指示。我们不需要使用 POST 创建资源,我们只是经常选择这样做。我可以使用 PUT 轻松创建资源,只需将资源 URL 定义为发送者(通常不理想)。 @nishant,在这种情况下,您可能不需要在 URI 中引用多个资源,而只需在请求正文中传递带有引用/值的元组。例如,POST /mail/markAsRead,正文:i_0_id=0&i_0_value=true&i_1_id=1&i_1_value=false&i_2_id=2&i_2_value=true 为此目的保留分号。 很惊讶没有人指出PATCH
很好地涵盖了更新单个资源上的多个属性 - 在这种情况下不需要创造力。【参考方案4】:
虽然我认为 @Alex 走的是正确的道路,但从概念上讲,我认为它应该与建议的相反。
URL 实际上是“我们的目标资源”,因此:
[GET] mail/1
表示从 id 为 1 的邮件中获取记录
[PATCH] mail/1 data: mail[markAsRead]=true
表示为id为1的邮件记录打补丁。查询字符串是一个“过滤器”,过滤从URL返回的数据。
[GET] mail?markAsRead=true
因此,我们在这里请求所有已标记为已读的邮件。因此,[PATCH] 到这条路径的意思是“修补已经标记为 true 的记录”......这不是我们想要实现的目标。
所以一个批处理方法,按照这个思路应该是:
[PATCH] mail/?id=1,2,3 <the records we are targeting> data: mail[markAsRead]=true
当然,我并不是说这是真正的 REST(不允许批处理记录操作),而是它遵循 REST 已经存在并正在使用的逻辑。
【讨论】:
有趣的答案!对于你的最后一个例子,做[PATCH] mail?markAsRead=true data: ["id": 1, "id": 2, "id": 3]
(甚至只是data: "ids": [1,2,3]
)不是更符合[GET]
格式吗?这种替代方法的另一个好处是,如果您要更新集合中的数百/数千个资源,您将不会遇到“414 Request URI too long”错误。
@rinogo - 实际上没有。这就是我要提出的观点。查询字符串是我们想要操作的记录的过滤器(例如,[GET] mail/1 获取 id 为 1 的邮件记录,而 [GET] mail?markasRead=true 返回 markAsRead 已经为 true 的邮件)。修补到同一个 URL(即“修补 markAsRead=true 的记录”)是没有意义的,而事实上我们想要修补 ID 为 1、2、3 的特定记录,而不管字段 markAsRead 的当前状态。因此,我描述的方法。同意更新许多记录存在问题。我会构建一个耦合度较低的端点。【参考方案5】:
很棒的帖子。几天来我一直在寻找解决方案。我想出了一个解决方案,使用以逗号分隔的一堆 ID 传递查询字符串,例如:
DELETE /my/uri/to/delete?id=1,2,3,4,5
...然后将其传递给我的 SQL 中的 WHERE IN
子句。效果很好,但不知道其他人如何看待这种方法。
【讨论】:
我不太喜欢它,因为它引入了一种新类型,即您在 where in 中用作列表的字符串。我宁愿将其解析为特定于语言的类型,然后我可以在系统的多个不同部分以相同的方式使用相同的方法。 提醒您注意 SQL 注入攻击,并在采取这种方法时始终清理您的数据并使用绑定参数。 取决于DELETE /books/delete?id=1,2,3
在第 3 本书不存在时的期望行为——WHERE IN
将默默忽略记录,而我通常希望 DELETE /books/delete?id=3
到 404 如果 3 不存在'不存在。
使用此解决方案可能遇到的另一个问题是 URL 字符串中允许的字符数限制。如果有人决定批量删除 5,000 条记录,浏览器可能会拒绝该 URL,或者 HTTP 服务器(例如 Apache)可能会拒绝它。一般规则(希望随着更好的服务器和软件而改变)是最大大小为 2KB。在 POST 的正文中,您最多可以达到 10MB。 ***.com/questions/2364840/…【参考方案6】:
您的语言“它似乎非常浪费......”对我来说表明过早优化的尝试。除非可以证明发送对象的整个表示会对性能造成重大影响(我们说用户不能接受大于 150 毫秒),否则尝试创建新的非标准 API 行为是没有意义的。请记住,API 越简单,使用起来就越容易。
对于删除发送以下内容,因为服务器在删除发生之前不需要知道任何有关对象状态的信息。
DELETE /emails
POSTDATA: [id:1,id:2]
下一个想法是,如果应用程序在对象的批量更新方面遇到性能问题,那么应该考虑将每个对象分解为多个对象。这样,JSON 有效负载只是大小的一小部分。
例如,在发送响应以更新两封不同电子邮件的“已读”和“已归档”状态时,您必须发送以下内容:
PUT /emails
POSTDATA: [
id:1,
to:"someone@bratwurst.com",
from:"someguy@frommyville.com",
subject:"Try this recipe!",
text:"1LB Pork Sausage, 1 Onion, 1T Black Pepper, 1t Salt, 1t Mustard Powder",
read:true,
archived:true,
importance:2,
labels:["Someone","Mustard"]
,
id:2,
to:"someone@bratwurst.com",
from:"someguy@frommyville.com",
subject:"Try this recipe (With Fix)",
text:"1LB Pork Sausage, 1 Onion, 1T Black Pepper, 1t Salt, 1T Mustard Powder, 1t Garlic Powder",
read:true,
archived:false,
importance:1,
labels:["Someone","Mustard"]
]
我会将电子邮件的可变部分(阅读、归档、重要性、标签)拆分为一个单独的对象,因为其他部分(收件人、发件人、主题、文本)永远不会更新。
PUT /email-statuses
POSTDATA: [
id:15,read:true,archived:true,importance:2,labels:["Someone","Mustard"],
id:27,read:true,archived:false,importance:1,labels:["Someone","Mustard"]
]
另一种方法是利用 PATCH。明确指出您打算更新哪些属性以及应忽略所有其他属性。
PATCH /emails
POSTDATA: [
id:1,
read:true,
archived:true
,
id:2,
read:true,
archived:false
]
人们声明 PATCH 应该通过提供一组更改来实现,其中包含:操作 (CRUD)、路径 (URL) 和值更改。这可能被认为是一种标准实现,但如果您查看整个 REST API,它是一种非直观的一次性实现。还有,上面的实现是怎么GitHub has implemented PATCH。
总而言之,通过批处理操作遵守 RESTful 原则并仍然具有可接受的性能是可能的。
【讨论】:
我同意 PATCH 最有意义,问题是如果您有其他状态转换代码需要在这些属性更改时运行,那么将其作为简单的 PATCH 实现变得更加困难。我不认为 REST 真正适应任何形式的状态转换,因为它应该是无状态的,它不关心它从什么转换到什么,只关心它的当前状态是什么。 嘿 BeniRose,感谢您添加评论,我经常想知道人们是否看到了其中的一些帖子。我很高兴看到人们这样做。有关 REST 的“无状态”性质的资源将其定义为服务器不必跨请求维护状态的问题。因此,我不清楚你描述的是什么问题,你能举个例子吗?【参考方案7】:google drive API 有一个非常有趣的系统来解决这个问题 (see here)。
他们所做的基本上是将不同的请求分组到一个 Content-Type: multipart/mixed
请求中,每个单独的完整请求由一些定义的分隔符分隔。批量请求的标头和查询参数被继承到单个请求(即Authorization: Bearer some_token
),除非它们在单个请求中被覆盖。
示例:(取自他们的docs)
请求:
POST https://www.googleapis.com/batch
Accept-Encoding: gzip
User-Agent: Google-HTTP-Java-Client/1.20.0 (gzip)
Content-Type: multipart/mixed; boundary=END_OF_PART
Content-Length: 963
--END_OF_PART
Content-Length: 337
Content-Type: application/http
content-id: 1
content-transfer-encoding: binary
POST https://www.googleapis.com/drive/v3/files/fileId/permissions?fields=id
Authorization: Bearer authorization_token
Content-Length: 70
Content-Type: application/json; charset=UTF-8
"emailAddress":"example@appsrocks.com",
"role":"writer",
"type":"user"
--END_OF_PART
Content-Length: 353
Content-Type: application/http
content-id: 2
content-transfer-encoding: binary
POST https://www.googleapis.com/drive/v3/files/fileId/permissions?fields=id&sendNotificationEmail=false
Authorization: Bearer authorization_token
Content-Length: 58
Content-Type: application/json; charset=UTF-8
"domain":"appsrocks.com",
"role":"reader",
"type":"domain"
--END_OF_PART--
回应:
HTTP/1.1 200 OK
Alt-Svc: quic=":443"; p="1"; ma=604800
Server: GSE
Alternate-Protocol: 443:quic,p=1
X-Frame-Options: SAMEORIGIN
Content-Encoding: gzip
X-XSS-Protection: 1; mode=block
Content-Type: multipart/mixed; boundary=batch_6VIxXCQbJoQ_AATxy_GgFUk
Transfer-Encoding: chunked
X-Content-Type-Options: nosniff
Date: Fri, 13 Nov 2015 19:28:59 GMT
Cache-Control: private, max-age=0
Vary: X-Origin
Vary: Origin
Expires: Fri, 13 Nov 2015 19:28:59 GMT
--batch_6VIxXCQbJoQ_AATxy_GgFUk
Content-Type: application/http
Content-ID: response-1
HTTP/1.1 200 OK
Content-Type: application/json; charset=UTF-8
Date: Fri, 13 Nov 2015 19:28:59 GMT
Expires: Fri, 13 Nov 2015 19:28:59 GMT
Cache-Control: private, max-age=0
Content-Length: 35
"id": "12218244892818058021i"
--batch_6VIxXCQbJoQ_AATxy_GgFUk
Content-Type: application/http
Content-ID: response-2
HTTP/1.1 200 OK
Content-Type: application/json; charset=UTF-8
Date: Fri, 13 Nov 2015 19:28:59 GMT
Expires: Fri, 13 Nov 2015 19:28:59 GMT
Cache-Control: private, max-age=0
Content-Length: 35
"id": "04109509152946699072k"
--batch_6VIxXCQbJoQ_AATxy_GgFUk--
【讨论】:
【参考方案8】:在我看来,我认为 Facebook 的实施最好。
单个 HTTP 请求使用批处理参数和一个令牌发出。
批量发送一个 json。其中包含“请求”的集合。 每个请求都有一个方法属性(get / post / put / delete / etc ...)和一个 relative_url 属性(端点的 uri),另外 post 和 put 方法允许一个“body”属性,其中字段被更新已发送。
更多信息请访问:Facebook batch API
【讨论】:
以上是关于在 REST Web 服务中处理批处理操作的模式?的主要内容,如果未能解决你的问题,请参考以下文章
JPA:处理 OptimisticLockException 的模式
使用 KissXML 处理 wcf Rest Web 服务 - 我应该如何处理命名空间问题
RESTful Web 服务四种操作POST/DELETE/PUT/GET
如何将我的 Rest Api 服务器与我的 Web Socket 服务器通信
Grails REST Client Builder 在处理来自 Jersey Web 服务的响应时收到 JSON 反序列化错误