在 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服务的最佳实践

如何将我的 Rest Api 服务器与我的 Web Socket 服务器通信

Grails REST Client Builder 在处理来自 Jersey Web 服务的响应时收到 JSON 反序列化错误