当前位置:首页 > 技术文章 > 正文内容

这才叫 API 接口开发!(api数据接口开发)

arlanguage3个月前 (01-26)技术文章25



01



什么是接口


接口其实是一种规范,在生活中随处可见,比如:不同厂商的水管使用统一的水管接口对接、电脑厂商和配件厂商按照统一的 USB 接口标准进行生产完成配对、应用程序之间通过 API 进行信息交互。


亚马逊集团创始人贝佐斯规定,亚马逊的所有系统、团队之间的交互,都必须通过服务接口,现在云计算技术非常火爆,其起源实际上是亚马逊的这种 API 文化,可以说,API 构建了当前数字化世界的通路。



翻译过来就是:

  1. 所有的团队必须通过服务来对外暴露数据和功能。
  2. 所有的团队之间的功能,必须通过这些服务接口来进行交互。
  3. 网络上不允许任何除服务接口的其它交互方式。(不能通过 Excel 等文件传输)
  4. 不管具体使用任何的技术,都必须符合上面的3条原则
  5. 所有的服务接口必须都被外部化。
  6. 任何不遵循这个指令的人将会被解雇。


所以,软件世界中到处存在的 API,到底应该如何实现?有哪些设计技巧?接下来让我们一起探索吧。




02



接口设计


契约式设计原则


Design by Contract (DbC)是一种设计计算机软件的方法。这种方法要求软件设计者为软件组件定义正式的,精确的并且可验证的接口,这样,为传统的抽象数据类型又增加了先验条件、后验条件和不变式,可以说它是 API 设计的指导书,一个好的 API 实现之前,先要回答三个关键问题:

  1. API 期望什么?
  2. API 要保证什么?
  3. API 要保持不变的什么?


首先,API 必须保证输入是接收者期望的输入条件。比如:API 需要 A、B、C 三个参数,那么使用者要提前准备好这三个参数,当条件不满足时,会拒绝请求,当条件满足时,才会处理请求。


其次,API 要保证输出结果的正确性。API 在处理过程中遇到各种异常或错误,需要在内部做好处理,并最终将正确或期望的结果输出给使用者,而不应该把错误抛给其他系统去处理。


最后,API 必须要保证处理过程中的一致性。比如,同一个 API 被部署在10个服务器上,当输入条件发生改变时,所有服务器的内部的会话、状态、API 的输出也应该保持一致。


契约式设计原则作为指导思想,那么我们在设计 API 过程中,到底应该注意哪些细节?以下是我总结的六大设计原则:


2.1 关注点分离


作为面向对象 SOLID 原则中的“I”,对我们的 API 接口提供了非常有实战意义的指导价值,所以当你在设计一个 API 的时候,不要在一个接口中杂糅多种功能,更不要用很多个 Flag 联动多个入参,这样只会让你的接口变得异常混乱。举个例子,客户只需要一个 USB 接口,而你却给了他一个交换机,他怎么知道那根线插哪里?要插多少根?排列组合都有 N 种,客户直接原地崩溃。所以说,我们在设计接口时,要力争减少使用者的认知负担,提供单一纯粹的功能。


// bad 
type UserService interface {
    CreateUser(name string, age int) error
    GetUser(id int) (User, error)
    UpdateUser(id int, name string, age int) error
    DeleteUser(id int) error
    SendEmail(userID int, message string) error // 发送邮件的职责与用户管理不相关
}


// good
type UserService interface {
    CreateUser(name string, age int) error
    GetUser(id int) (User, error)
    UpdateUser(id int, name string, age int) error
    DeleteUser(id int) error
}


type EmailService interface {
    SendEmail(userID int, message string) error
}


2.2 自我表达


一个好的接口,客户在拿到后能很简单的读懂,在开发联调过程中,能根据错误信息自己修正,那么这个接口就具备了良好的自我表达能力,你可以从以下两大方面去考虑:


接口命名

接口命名应该给使用者提供一种简单且直观的体验,从接口名即可了解此接口的具体功能。


使用被大家接受的通用缩写,如:Information->info,delete->del,message->msg 等。


通过命名含义描述接口,比如获取用户列表:GetUsers。注意此方式适用于 RPC 协议接口风格,RESTful 风格不使用此形式,REST 使用名词 URI+动词形式。


错误信息


具有自表达的错误信息字段,且应该包含两点内容:

A:明确的错误指示。例如:“amount金额字段格式有误”。

B:错误后的解决方案。例如:“token字段缺失,请先登录”。


错误码设计

A:屏蔽内部逻辑。不要轻易暴露内部处理逻辑规律,防止接口被攻击。

B:便于快速定位问题。


错误码定义可以参考 HTTP 状态码,采用分段式,形成“肌肉记忆”,方便定位问题,也易于扩展。


分段

分段描述

1XX

信息,服务器收到请求,需要请求者继续执行操作

2XX

成功,操作被成功接收并处理

3XX

重定向,需要进一步操作以完成请求

4XX

客户端错误,请求包含语法错误或无法完成请求

5XX

服务器错误,服务器在处理请求过程中发生了错误

2.3 互斥穷举


接口之间应该尽量遵守MECE(互斥穷举)原则,不应该提供相互叠加的接口。例如:如果 PUT /orders/1/items/1 接口用于修改订单项,那接口 PUT /orders/1 就不应该具备处理某个 order-item 的能力。


这样做的好处是不会存在功能重复的接口,造成维护和理解上的复杂性。如何做到完全穷举和彼此独立呢?


方案一:使用在线表格设计,标出每个URI的能力。


方案二:使用 DDD 领域建模,聚合根作为根 URL,实体作为二级 URI 设计。聚合根之间无任何联系,实体和聚合根之间责任明确。


举例:


聚合根和实体定义

Order 聚合根

  • 包含多个 OrderItem 实体
  • 负责管理订单的生命周期和相关操作


URI

动词

功能

/orders

GET

获取所有订单

POST

创建新订单

/orders/1

GET

获取特定订单详情

PUT

更新特定订单

/orders/1/items

GET

获取特定订单的所有订单项

/orders/1/items/1

GET

获取特定订单项详情

DELETE

删除特定订单项

PUT

更新特定订单项


2.4 快速失败


接口要快速验证条件,不满足要返回失败,不要等到已经击穿到应用最底部后,再去报错。这是“防御式编程”的一种理念,一个具体的实践就是对于接口入参进行校验。golang 的 validation 插件案例:


// 枚举控制
enum OperaType {
  Get = 0;  // 获取值
  Set = 1;  // 设置值
}
OperaType opera_type = 2[(validate.rules).enum.defined_only = true]; // 操作类型


// 范围控制
int32 page_num = 3 [(validate.rules).int32.gte = 0]; //页码


给接口设置超时时间,异常情况快速失败,也能提高接口并发,但是会造成一些不可用,需要权衡利弊。


2.5 幂等性


什么是幂等?简单来说,就是当一个操作多次执行所产生的影响均与一次执行的影响相同,则它是幂等的。现代服务逐渐往分布式架构发展,服务是分散的,如何保证并行调用的幂等?


以下是常见的几种方式:


使用唯一标识符


为每个请求生成一个唯一的标识符,并在服务端进行验证。如果服务端已经处理过该标识符对应的请求,则直接返回之前的结果,而不进行重复处理。


使用状态机


对于一些有明确状态的资源(如订单、支付等),可以通过状态机来确保操作的幂等性。每个状态转换只能发生一次,重复请求不会改变状态。


使用数据库唯一约束


在数据库层面使用唯一约束来防止重复数据插入。如果重复请求尝试插入相同的数据,数据库会抛出异常,服务端可以捕获并处理该异常。


使用乐观锁


我们在更新资源时检查版本号是否匹配。如果不匹配,说明资源已经被其他事务修改过,当前事务应该回滚。


使用悲观锁


悲观锁假设数据在大多数时间会发生冲突,读取数据时就加锁,防止其他事务修改数据,通常通过数据库的行级锁来实现,如使用“for update”。


使用分布式锁


分布式锁与悲观锁本质上相似,都通过串行化请求处理来实现幂等性。与悲观锁不同的是,分布式锁更轻量。在系统接收请求后,首先尝试获取分布式锁。如果成功获取锁,则执行业务逻辑;如果获取失败,则立即拒绝请求。


2.6 版本化


一个对外开放的服务,极大的概率会发生变化。业务变化可能修改 API 参数或响应数据结构,以及资源之间的关系。一般来说,字段的增加不会影响旧的客户端运行。但是当存在一些破坏性修改时,就需要使用新的版本将数据导向到新的资源地址。


版本信息传输通常有以下几种方式:


URI 前缀控制


这是最常见和直接的版本化方法,通过在 URL 路径中包含版本号来区分不同版本的 API。


Header 控制


通过在 HTTP 请求头中包含版本信息来实现版本化。这种方法可以保持URL的简洁。


示例:

  • 使用自定义头:X-API-Version: 1
  • 使用 Accept 头:Accept: application/vnd.example.v1+json


Query 控制

通过在 URL 中添加查询参数来指定版本号。


总结


比较推荐的做法是使用 URI 前缀。常见的反模式是通过增加 URI 后缀来实现的,例如/users/1/updateV2。这样做的缺陷是版本信息侵入到业务逻辑中,对路由的统一管理带来不便。


使用 Header 和 Query 发送版本信息则较为相似,不同之处在于,使用 URI 前缀在 MVC 框架中实现相对简单,只需要定义好路由即可。使用 Header 和 Query 还需要编写额外的拦截器。




03



接口优化


3.1 批量


示例:

  • Redis 单次查询使用 Mget,Pipline 等方式,批量查询。
  • 循环调用 RPC 接口耗时严重,优化 RPC 接口支持批量查询,替代循环调用。


本质上是减少不必要的网络 IO,但会对下游服务造成一定压力,需要综合考虑,不可滥用。


3.2 异步


示例:

  • 用户请求接口中,需要做业务操作,发站内通知,可以同步执行操作写库,然后异步发通知。


区分核心和非核心操作,核心逻辑同步执行,非核心逻辑异步执行。通常异步方案有:线程/协程池,消息队列,任务调度框架,任务回调。


3.3 并行


示例:

  • 两个操作任务,各耗时200ms,顺序调用需要花费400ms,改为协程调用,总花费只需要200ms。


将单个任务或串行任务拆分为多个子任务,利用多核处理器或分布式系统进行并行处理,提升处理效率。Golang 中可以使用 go 关键字轻松实现并行调用,本文不做赘述。


3.4 空间换时间


示例:

  • 用户信息访问频繁,变更小,通过 redis 缓存用户信息,用内存空间来提升响应速度。
  • 代码中用列表存储要素,循环判断某个要素是否存在,改为使用字典存储,虽然空间占用变大,但是时间复杂度由O(n)降为O(1)。
  • MySQL 数据库额外存储一份索引数据也是一种空间换时间的方案,提升查找了速度。


空间换时间是一种常见的优化策略,通过额外的内存、磁盘等存储介质,减少程序的执行时间。


3.5 池化


示例:

  • 通常我们在使用 MySQL 时,会采用池化技术,复用 MySQL 链接,来达到重复利用的效果,避免了不必要的创建和销毁所带来的性能损耗。
  • 在高并发场景下,Golang 使用协程池来实现协程的管理,提升程序性能,如经典的高性能协程池开源库 ants:https://github.com/panjf2000/ants。


通过预先创建一定数量的连接对象,当需要使用连接时,从池中获取一个可用的连接对象,使用完毕后归还给池,而不是每次都创建和销毁连接对象,从而提高系统性能和资源利用率。


3.6 预处理


示例:

  • 用户打完王者荣耀对局后,通过客服入口举报违规玩家,我们通过预处理,调用各种数据接口和模型计算,并将信息存储下来,当工单流转到工作台时,接口可瞬时将信息拉取出来。


类似空间换时间的缓存思想,但是关注点不一样,我们可以将想要的数据提前计算好,放到缓存或数据库中,当真正需要的时候再进行取用,而不是实时计算,以此来提升接口性能。


3.7 SQL 优化


接口开发过程中难免会和数据库打交道,但数据库非本文的介绍重点,不对数据库的具体优化方案进行详细介绍,只提供一些优化思路,感兴趣请自行查阅相关资料。


深分页


select * from t_records where uid = '{my_uid}' limit X, 30;


这是一常见的分页操作,看起来没啥问题,但是加入 X=5000000 时,数据库 server 调用 innodb 接口,在非主键索引中获取第0到第(5000000+30)条数据,返回给 server 之后,根据 offset=30,逐个抛弃,留下后面的30条,返回给业务。


优化方案:


// 基于主键或索引的优化
SELECT * FROM t_records WHERE id > last_seen_id ORDER BY id ASC LIMIT 30


// 使用子查询优化
SELECT * FROM t_records 
WHERE id IN (
  SELECT id FROM t_records WHERE uid = '{my_uid}' ORDER BY id LIMIT 100000, 30
) ORDER BY id;


// 延迟关联(Deferred Join)
SELECT a.* FROM t_records a
JOIN (
  SELECT id FROM t_records WHERE uid = '{my_uid}' ORDER BY id LIMIT 100000, 30
) b ON a.id = b.id;


// 反向分页 适用于查最新内容的场景
SELECT * FROM t_records ORDER BY id DESC LIMIT 30;


如果并发较高,可以考虑使用缓存的方案解决性能问题。


避免大事务


为了提高接口并发量,需要避免大事务。当需要更新多条数据时,避免一次性更新过多的数据。因为 update,delete 语句会对索引加锁,如果更新的记录数过多,会锁住太多的数据,由于执行时间较长,会严重限制数据库的并发量。因此,要考虑多个操作是否真的有必要放在一个事务中,将核心操作放到同一事务中,非核心操作在事务外面单独执行。


索引


加索引能大大提高数据查询效率,这个在接口设计之初也会考虑到,其中的一些索引设计和优化技巧,这里不做过多赘述。


分库分表


分库:是为了解决数据库连接资源不足问题,和磁盘 IO 的性能瓶颈问题。


分表:是为了解决单表数据量太大,sql 语句查询数据时,即使走了索引也非常耗时问题,此外还可以解决消耗 cpu 资源问题。


不过,分库分表的成本太高,非必要尽量不要采用。单表千万级也不一定要分,要看字段和索引的具体的存储情况而定。


归档历史数据


在设计数据库表之初,就要结合业务发展,考虑数据库量级增长趋势,考虑好归档方案,防止库表膨胀过大,对接口性能产生影响。


3.8 锁粒度控制


为解决高并发场景下,多个线程同时修改数据时会造成数据不一致情况,通常我们会:加锁。但是如果锁加的不好,导致粒度太粗,也会影响接口性能,比如你要用卫生间,就不要把客厅门也锁上。


// 同步执行保存操作
func (f *FileOperations) DoSave(fileUrl string) {
  f.mu.Lock()
  defer f.mu.Unlock()
  // 创建目录
        f.mkdir()
    // 上传文件
  f.uploadFile(fileUrl)
        //发送消息
  f.sendMessage(fileUrl)
}


为了避免高并发场景下创建相同目录,第二次创建目录失败影响业务功能,我们加锁,但是也对上传文件和发送消息上锁,导致粒度太粗,影响了接口性能,降低了并发,好的做法是只对 mkdir 函数进行加锁,减小锁粒度。


3.9 合适的存储


MySQL 换 Redis:当查询压力较大时,可以使用 redis 缓存数据。


Redis 换本地缓存:Redis 相比传统数据库速度要快很多,大概是毫秒级别,但是使用本地缓存的耗时可以忽略不计,而且从 redis 取数据会存在大量的序列化和反序列化过程,增加 CPU 负担。


Redis 换 Memcached:Redis 基于单线程,可能存在阻塞导致性能降低,但是 Memcached 是多线程模型,在某些高并发场景下,性能可能会更高。


MySQL 换 ES:后台系统假如需要复杂的查询,使用索引的查询场景比较灵活,使用 MySQL 可能就存在瓶颈了,可以考虑同步数据到 ES 中,提高查询检索性能。


MySQL 换 HBase:如果 MySQL 中的数据过于庞大,可以考虑将历史数据转存至 HBase 中,以提供高效的查询性能,通过设置合理的 RowKey 能进一步提高查询效率。


3.10 压缩传输内容


示例:

  • protobuf 协议相较于 json 或 xml,有更高的性能是因为其采用 varints 编码算法进行了二进制数据压缩。
  • HTTP 协议中的一个头部字段 Content-Encoding,服务端可采用 Gzip、Deflate、Br 等算法压缩,提升传输效率。




04



接口安全


4.1 加密


敏感数据在传输过程中可能被抓包,想要保证数据安全,需要对数据进行加密,常见的加密方式有对称加密和非对称加密:


对称加密:加密和解密过程中,使用相同的密钥,常见的对称加密算法有:DES、AES。优点是计算速度快,缺点为任一方密钥泄露都会导致信息不安全。


非对称加密:服务端生成一对密钥,私钥放在服务端,公钥发布给其他人使用,这种方式比较安全,但是速度会慢一些。


4.2 加签验签


数据加签即由服务端用传输数据生产一串无法伪造的字符串,保证数据在传输过程中不被篡改。通常使用比较多的是 MD5 算法,将提交的数据通过某种方式组合,使用 MD5 生成加密串作为签名。当服务端接受请求时,就可以使用特定的组合方式生成签名进行对比,判断请求是否合法,此外可以将时间戳带入,增加超时校验,防止被抓包恶意攻击。


4.3 token 授权


客户端输入用户名和密码,服务端校验成功后返回一个唯一值 Token 并缓存在服务端,后续客户端访问需要携带此 Token,服务端验证如果存在,则表明请求合法。同时我们要对 Token 设置合理的有效期,避免被抓包爬取数据。但是 Token 方案有缺点,比如:在分布式系统中,Token 方案依赖分布式缓存,会让我们的系统变得复杂。现在大部分系统会使用 JWT 来生成 Token,这种方式是无状态的,天然支持分布式。


4.4 防重放


某些特定场合下要保证一个请求只能处理一次,如支付场景。这是我们可以使用 timestamp+nonce 方案,每次请求会生成 nonce 到集合中,服务端处理请求时会先判断该 nonce 是否在集合中,如果存在则为非法请求。永久保存 nonce 代价非常大,可以结合 timestamp 优化,比如超过 3min 认定为非法请求。


4.5 限流


用户是真实用户,但是想频繁调用接口搞垮你的系统,或者调用量很大的情况下保证系统不宕机,这时我们可以使用限流方案来保护系统。常见的限流算法:令牌桶算法、漏桶算法、滑动窗口、计数器等。


4.6 黑白名单机制


对用户进行分析,将恶意用户纳入黑名单,避免系统被频繁攻击。白名单类似,比如对某个用户开放特殊调用权限,通过加白实现。


4.7 数据脱敏掩码


密码、手机号、身份证等敏感信息一般需要脱敏掩码展示,在保存时也不能明文存储,一般使用哈希算法进行加密,常见的如 SHA-256,也可加盐来增加安全性。


4.8 防 SQL 注入


常见的 SQL 注入方式


基于字符串的 SQL 注入:' OR '1'='1',这种方式通常用于绕过登录验证。


基于联合查询的 SQL 注入:使用 UNION 关键字将恶意查询与原始语句结合,获取数据库敏感信息。如:' UNION SELECT username, password FROM user'


基于盲注的SQL注入:攻击者无法直接看到查询结果,通过观察响应来推断数据库信息。


基于堆叠查询的 SQL 注入:在一个语句中插入多个 SQL 语句,如:'; DROP TABLE users;'


防 SQL 注入应对方案


使用参数化查询:

  • 使用参数化查询(也称为预处理语句)可以有效防止 SQL 注入,因为参数化查询会将输入数据与 SQL 语句分开处理。
  • 在 Go 语言中,可以使用 database/sql 包的 Prepare 和 Exec 方法。


使用 ORM 框架:

  • 使用 ORM 框架(如 GORM)可以自动处理参数化查询,减少手动拼接 SQL 的风险。


输入验证和过滤:

  • 对用户输入进行严格的验证和过滤,拒绝包含特殊字符的输入。
  • 可以使用正则表达式来限制输入格式。


最小权限原则:

  • 数据库用户应仅具有执行所需操作的最小权限,防止在 SQL 注入成功后造成更大损害。


使用存储过程:

  • 将 SQL 逻辑封装在存储过程中,限制直接执行 SQL 语句的机会。


错误信息隐藏:

  • 在生产环境中隐藏详细的错误信息,防止攻击者获取数据库结构信息。


4.9 权限控制


我们需要对用户进行权限校验,来保证接口能被正确的使用。一般有三种粒度,第一校验是否登录,第二校验接口功能权限,第三校验接口数据权限。通常内网系统,我们会使用 RBAC 来进行权限控制。


4.10 使用 HTTPS


  • 数据加密:HTTPS 使用 SSL/TLS 协议对数据进行加密,确保数据在传输过程中的机密性和完整性,防止数据被窃取或篡改。
  • 身份验证:通过 SSL 证书验证服务器的身份,确保用户访问的是真实的网站,防止中间人攻击。
  • 完整性保护:确保数据在传输过程中不被篡改,如果数据被篡改,接收方可以检测到这种篡改,并拒绝接受这些数据。




05



接口维护


5.1 文档


本人强烈反对单独使用文档维护接口,因为一旦文档和代码割裂只会逐渐缺失维护不再更新,要善于利用工具让代码和文档关联,自动更新。比如:开源的 swagger。


5.2 日志


  1. 合理区分和使用 error、warn、info、debug、trace5 种日志级别。error 较严重,对业务有影响,warn 是警告,对业务影响不大,需要开发关注。
  2. 日志要打印方法、入参和出参,方便定位问题,携带关键的信息参数,如 userId,traceId 等关键信息。
  3. 统一使用合理的日志格式,包含基础的信息如:当前时间戳、日志级别、线程 ID 等。
  4. 遇到条件分支,如有必要,可以在分支首行打印日志,方便定位问题,理清逻辑走向。
  5. 使用异步方式输出日志,可以提升接口性能。
  6. 禁止在线上开启 DEBUG,可能会打满磁盘影响业务系统运行。
  7. 日志文件可以考虑单招等级分离,比如 Nginx 的 access.log 和 error.log。
  8. 核心功能模块建议打印完整的日志,方便快速定位问题。




06



总结


以上只是个人的一些总结,接口开发是一件比较复杂的事情,以上技巧仅供大家在写接口的时候作为参考。希望大家都能写出性能高、安全稳定易维护的好接口!完结撒花

扫描二维码推送至手机访问。

版权声明:本文由AR编程网发布,如需转载请注明出处。

本文链接:http://www.arlanguage.com/post/1143.html

分享给朋友:

“这才叫 API 接口开发!(api数据接口开发)” 的相关文章

Nginx动静分离简单实现示例讲解

简述本文主要介绍如何实现Nginx动静分离。动静分离动静分离是根据一定规则把静态文件(html、css、js、jpg等)和动态文件(jsp,.do等)区分开来,采用静态文件和动态文件分开部署,以提高用户访问静态文件的速度,降低对后台应用的访问,提高服务器响应速度和性能。静态文件由Nginx服务器处理...

nginx命令——学习记录 nginx详细教程

nginx命令--学习记录查看80端口是否被占用netstat -ntlpnginx命令#/usr/local/nginx/sbin/nginx 启动命令#/usr/local/nginx/sbin/nginx -t 检测配置文件 #/usr/local/nginx/sbin/ngin...

一文教会你如何使用 iLogtail SPL 处理日志

作者:阿柄随着流式处理的发展,出现了越来越多的工具和语言,使得数据处理变得更加高效、灵活和易用。在此背景下,SLS 推出了 SPL(SLS Processing Language) 语法,以此统一查询、端上处理、数据加工等的语法,保证了数据处理的灵活性。iLogtail 作为日志、时序数据采集器,在...

宝塔面板——永久免费的服务器运维面板

宝塔面板是什么?宝塔面板,简单好用的服务器运维面板。官称永久免费。Linux版请确保纯净系统安装(支持CentOS、Ubuntu、Debian、Fedora、deepin),Web端管理,一键创建网站、FTP、数据库、SSL;安全管理,计划任务,文件管理,PHP多版本共存及切换;自带LNMP与LAM...

nginx入门——nginx访问日志(六)

日志功能在ngx_http_log_module模块中定义,实现了以指定格式写入请求日志。我们先来看一个nginx配置文件:http { include mime.types; default_type application/octet-stream;...

Nginx 如何代理转发传递真实 ip 地址?

Nginx 是一个高性能的反向代理服务器,也是一个非常流行的负载均衡器和 HTTP 缓存。其轻量级的设计和高并发处理能力使得它广泛应用于各种 Web 服务中。在使用 Nginx 作为反向代理服务器时,一个常见的问题是如何在代理转发过程中传递客户端的真实 IP 地址。默认情况下,Nginx 会将客户端...