2021年5月

背景

今年,我们做了一个平台项目,平台嘛,免不了对外提供Http/Https的接口,有时候会听到研发同学说:“咱们接口就都是POST方法了,不要定义其他方法,没啥大用。”我不禁想起了RESTFUL风格,其实HTTP协议的这些动词,是有专门含义和用法的,正好可以通过此文回忆和记录下,看看是不是其他方法,没啥大用。

HTTP不同的动词

我们可以定义两种编程逻辑,分别是业务逻辑和控制逻辑。

业务逻辑。就是你实现业务需求的功能的代码,就是跟用户需求强相关的代码。比如,把用户提交的数据保存起来,查询用户的数据,完成一个订单交易,为用户退款……等等,这些是业务逻辑。控制逻辑。就是我们用于控制程序运行的非功能性的代码。比如,用于控制程序循环的变量和条件,使用多线程或分布式的技术,使用HTTP/TCP协议,使用什么样数据库,什么样的中间件……等等,这些跟用户需求完全没关系的东西。

网络协议也是一样的,一般来说,几乎所有的主流网络协议都有两个部分,一个是协议头,一个是协议体。协议头中是协议自己要用的数据,协议体才是用户的数据。所以,协议头主要是用于协议的控制逻辑,而协议体则是业务逻辑。
HTTP的动词(或是Method)是在协议头中,所以,其主要用于控制逻辑。
下面是HTTP的动词规范,一般来说,REST API 需要开发人员严格遵循下面的标准规范。
http协议动词.png
其中,PUT 和 PACTH 都是更新业务资源信息,如果资源对象不存在则可以新建一个,但他们两者的区别是,PUT 用于更新一个业务对象的所有完整信息,就像是我们通过表单提交所有的数据,而 PACTH 则对更为API化的数据更新操作,只需要更需要更新的字段(参看 RFC 5789 )。
注意:我在这个表格的最后一列中加入了“是否幂等”的,API的幂等对于控制逻辑来说是一件很重要的事。所谓幂等,就是该API执行多次和执行一次的结果是完全一样的,没有副作用。
POST 用于新增加数据,比如,新增一个交易订单,这肯定不能是幂等的
DELETE 用于删除数据,一个数据删除多次和删除一次的结果是一样的,所以,是幂等的
PUT 用于全部数更新,所以,是幂等的。
PATCH用于局部更新,比如,更新某个字段 cnt = cnt+1,明显不可以能是幂等操作。
幂等这个特性对于远程调用是一件非常关键的事,就是说,远程调用有很多时候会因为网络原因导致调用timeout,对于timeout的请求,我们是无法知道服务端是否已经是收到请求并执行了,此时,我们不能贸然重试请求,对于不是幂等的调用来说,这会是灾难性的。比如像转帐这样的业务逻辑,转一次和转多次结果是不一样的,如果重新的话有可能就会多转了一次。所以,这个时候,如果你的API遵从了HTTP动词的规范,那么你写起程序来就可以明白在哪些动词下可以重试,而在哪些动词下不能重试。如果你把所有的API都用POST来表达的话,就完全失控了。

除了幂等这样的控制逻辑之外,你可能还会有如下的这些控制逻辑的需求:

缓存。通过CDN或是网关对API进行缓存,很显然,我们要在查询GET 操作上建议缓存。
流控。你可以通过HTTP的动词进行更粒度的流控,比如:限制API的请用频率,在读操作上和写操作上应该是不一样的。
路由。比如:写请求路由到写服务上,读请求路由到读服务上。
权限。可以获得更细粒度的权限控制和审计。
监控。因为不同的方法的API的性能都不一样,所以,可以区分做性能分析。
压测。当你需要压力测试API时,如果没有动词的区分的话,我相信你的压力测试很难搞吧。
……等等
也许,你会说,我的业务太简单了,没有必要搞这么复杂。OK,没有问题,但是我觉得你最差的情况下,也是需要做到“读写分离”的,就是说,至少要有两个动词,GET 表示是读操作,POST表示是写操作。

Restful复杂查询

一般来说,对于查询类的API,主要就是要完成四种操作:排序,过滤,搜索,分页。下面是一些相关的规范。参考于两个我觉得写的最好的Restful API的规范文档,Microsoft REST API Guidelines,Paypal API Design Guidelines。

排序。对于结果集的排序,使用 sort 关键字,以及 {field_name}|{asc|desc},{field_name}|{asc|desc} 的相关语法。比如,某API需要返回公司的列表,并按照某些字段排序,如:GET /admin/companies?sort=rank|asc 或是 GET /admin/companies?sort=rank|asc,zip_code|desc

过滤。对于结果集的过滤,使用 filter 关键字,以及 {field_name} op{value} 的语法。比如: GET /companies?category=banking&location=china 。但是,有些时候,我们需要更为灵活的表达式,我们就需要在URL上构造我们的表达式。这里需要定义六个比较操作:=,<,>,<=,>=,以及三个逻辑操作:and,or,not。(表达式中的一些特殊字符需要做一定的转义,比如:>= 转成 ge)于是,我们就会有如下的查询表达式:GET /products?$filter=name eq 'Milk' and price lt 2.55 查找所有的价柗小于2.55的牛奶。

搜索。对于相关的搜索,使用 search 关键字,以及关键词。如:GET /books/search?description=algorithm 或是直接就是全文搜索 GET /books/search?key=algorithm 。

分页。对于结果集进行分页处理,分页必需是一个默认行为,这样不会产生大量的返回数据。

使用page和per_page代表页码和每页数据量,比如:GET /books?page=3&per_page=20。
可选。上面提到的page方式为使用相对位置来获取数据,可能会存在两个问题:性能(大数据量)与数据偏差(高频更新)。此时可以使用绝对位置来获取数据:事先记录下当前已获取数据里最后一条数据的ID、时间等信息,以此获取 “该ID之前的数据” 或 “该时刻之前的数据”。示例:GET /news?max_id=23454345&per_page=20 或 GET /news?published_before=2011-01-01T00:00:00Z&per_page=20。
另外,对于一些更为复杂的操作,建议通过分别调用多个API的方式来完成,虽然这样会增加网络请求的次数,但是这样的可以让后端程序和数据耦合度更小,更容易成为微服务的架构。

最后,如果你想在Rest中使用像GraphQL那样的查询语言,你可以考虑一下类似 OData 的解决方案。OData 是 Open Data Protocol 的缩写,最初由 Microsoft 于 2007 年开发。它是一种开放协议,使您能够以简单和标准的方式创建和使用可查询和可互操作的 RESTful API。

总结

所以,可以回答开始的问题了,这些HTTP动词不是不是没啥大用,应该是非常有用,软件程序员对其正确使用,应该是作为一名工程师的“正常”操作,就像家里装修的时候,泥水匠贴瓷砖时,需要保证每片瓷砖贴的严丝合缝一样。接口动词的正确选用,也是对于调用方的负责,否则别人调用错误,你在家里,也没法好好休息,老是跑来找你这个接口要怎么用,怎么搞,岂不是大大浪费沟通效率。