接口优化具体怎么做的 ?

146 阅读10分钟

接口优化具体怎么做的 ?

1. 引言

在实习期间,我负责过多个核心业务接口的性能优化。优化过程中,我结合日志系统、慢查询监控和 Kibana 等可视化工具进行问题排查,并通过接口的响应时间、并发量、吞吐量和错误率等指标评估优化效果。然后可以从代码逻辑、数据库的查询优化和系统架构等方面考虑。

以下是我在开发过程中总结的经验 test.png

2. 从代码逻辑方面

这部分重点复习go语言编程知识

批量思想

这个很好理解,尽量避免单个请求频繁操作数据库,减少数据库的开销,例如将多条数据合并为一次请求发送到数据库,减少不必要的连接开销。

还有就是在调用RPC接口时,也尽量使用批量化操作,例如在获取多个用户的信息时,将多个用户的 ID 一次性传递给远程接口,接口一次调用返回结果,减少网络开销。

空间换时间

优化代码时,可以通过缓存中间计算结果,减少重复计算的次数,从而降低接口响应时间。

将Slice类型的数据转为Map类型,在获取数据时减少遍历的时间

预处理

对于某些重复的计算或者查询,可以通过预处理的方式将计算结果提前准备好,从而在实际请求时直接获取,避免重复的计算和查询操作,减少延迟。

串行改并行

如果接口的业务逻辑中存在一些可以并行执行的任务,可以利用go语言并发编程的特性实现,结合goroutine和chanel实现,减少处理时间。

优化程序结构

1、尽量减少for循环的嵌套次数

2、避免重复创建对象

3、避免goroutine阻塞

4、减少不必要的内存分配,进行逃逸分析(准备一下逃逸分析的知识)

5、优化数据结构

3.从数据库的查询优化方面

我们公司有 SQL 的慢查询监控,当我们发现接口响应时间比较差的时候,就会去排查 SQL 的问题。我们主要是使用 EXPLAIN 命令来查看 SQL 的执行计划,看看它有没有走索引、走了什么索引、是否有内存排序、去重之类的操作。 初步判定了问题所在之后,我们尝试优化,包括改写SQL或者修改、创建索引。之后再次运行SQL看看效果。如果效果不好,就继续使用 EXPLAIN 命令,再尝试修改。如此循环往复,直到SQL性能达到预期。

索引优化

覆盖索引减少回表操作

原来我们有一个执行非常频繁的 SQL。这个 SQL 查询全部的列,但是业务只会用到其中的三个列 A B C,而且 WHERE 条件里面主要的过滤条件也是这三个列组成的,所以我后面就在这三个列上创建了一个组合索引。 对于这个高频 SQL 来说,新的组合索引就是一个覆盖索引。所以我在创建了索引之后,将 SQL 由 SELECT * 改成了 SELECT A, B, C,完全避免了回表。这么一来,整个查询的查询时间就直接降到了 1ms 以内

总结

正常来说,对于非常高频的 SQL,都要考虑避免回表,那么设计一个合适的覆盖索引就非常重要了。

除了覆盖索引还可以用索引来优化排序

我在公司优化过一个 SQL,这个 SQL 非常简单,就是将某账户的数据搜索出来,然后按照数据的最后更新时间来排序。SQL 大概是 SELECT * FROM xxx WHERE uid = 123 ORDER BY update_time。 如果用户的数据比较多,那么这个语句执行的速度还是比较慢的。

后来我们做了一个比较简单的优化,就是用 uid 和 update_time 创建一个新的索引。从数据库原理上说,在 uid 确认之后,索引内的 update_time 本身就是有序的,所以避免了数据库再次排序的消耗。这样一个优化之后,查询时间从秒级降到了数十毫秒。

总结

在所有的排序场景中,都应该尽量利用索引来排序,这样能够有效减轻数据库的负担,加快响应速度。进一步来说,像 ORDER BY,DISTINCT 等这样的操作也可以用类似的思路。

注意创建索引带来的代价,和创建索引的规则

Sql优化

1、分页优化

在我们的系统里面,最开始有一个分页查询,那时候数据量还不大,所以一直没出什么问题。后来数据量大了之后,我们发现如果往后翻页,页码越大查询越慢。问题关键就在于我们用的 LIMIT 偏移量太大了。

基于游标的方式

 所以后来我就在原本的查询语句的 WHERE 里面加上了一个 WHERE id > max_id 的条件。这个 max_id 就是上一批的最大 ID。这样我就可以保证 LIMIT 的偏移量永远是 0。这样修改之后,查询的速度非常稳定,一直保持在毫秒级。

注意:这种方式实现的分页,无法进行跳页查询

基于延迟关联

先通过where条件提取出主键,在将该表与原数据表关联,通过主键id提取数据行,而不是通过原来的二级索引提取数据行

总结

很多时候因为测试环境数据量太小,这种性能问题根本不会被发现。所以所有使用分页的查询都应该考虑引入类似的查询条件。

2、使用小表驱动大表

  • 如果主查询的数据集大,则使用In
  • 如果子查询的数据集大,则使用exist

3、避免索引失效的场景

避免大事物

大事务会锁住大量数据,影响数据库性能。尽量将大的事务拆分成多个小事务,以减少锁竞争和数据库压力。 尽量不在事物里面调用RPC接口。

并发优化

这部分可以将数据库的隔离级别以及锁机制,分布式锁等知识串联在一起

1、注意避免死锁

4. 从系统架构方面

缓存

Cache Aside

是一种比较简单的缓存模式,数据库和缓存都被看成一个独立的数据源,在读数据的时候,会先读取缓存的数据,如果缓存不存在,则读取数据库的数据,并将数据写到缓存当中;在写入数据时会先写入数据库再写缓存。

Read Through

也是一个很常用的缓存模式。Read Through 是指在读缓存的时候,如果缓存未命中,那么缓存会代替业务代码去数据库中加载数据。

这种模式有两个异步变种,一种是异步写回缓存,一种是完全异步加载数据,然后写回缓存。当然,不管是什么变种,Read Through 都不能解决缓存一致性的问题。

Write Through

就是在写入数据的时候,只写入缓存,然后缓存会代替我们的去更新数据库。但是,Write Through 没有要求先写数据库还是先写缓存,不过一般也是先写数据库。

Write Back

是指我们在更新数据的时候,只把数据更新到缓存中就返回。后续会有一个组件监听缓存中过期的 key,在过期的时候将数据刷新到数据库中。显然,只是监听过期 key 的话还是会有问题,比如说关闭缓存的时候还是需要把缓存中的数据全部刷新到数据库里。

Write Back 有一个硬伤,就是如果缓存突然宕机,那么还没有刷新到数据库的数据就彻底丢失了。这也限制了 Write Back 模式在现实中的应用。不过要是缓存能够做到高可用,也就不容易崩溃,也可以考虑使用。

因此 Write Back 除了有数据丢失的问题,在缓存一致性的表现上,比其他模式要好。

Singleflight

是指当缓存未命中的时候,访问同一个 key 的线程或者协程中只有一个会去真的加载数据,其他都在原地等待。 这个模式最大的优点就是可以减轻访问数据库的并发量。比如说如果同一时刻有 100 个线程要访问 key1,那么最终也只会有 1 个线程去数据库中加载数据。这个模式的缺点是如果并发量不高,那么基本没有效果。所以热点之类的数据就很适合用这个模式。

延迟双删

类似于删除缓存的做法,它在第一次删除操作之后设定一个定时器,在一段时间之后再次执行删除。

它的一致性问题不是很严重。虽然会降低缓存的命中率,但是我们的业务并发也没有特别高,写请求是很少的。命中率降低一点点是完全可以接受的。

异步思想

限流

幂等

对同一个资源进行多次相同操作他们所产生的影响都是一样的。

一致性负载均衡

5、具体的实践经历

1、优化数据导出接口

2、优化群积分修改接口

在项目里,群积分修改接口在高并发场景下报错率高达 100%,问题在于多个请求同时修改群积分时产生了数据竞争,并且部分请求因网络波动等原因重复发起,导致积分数据计算混乱。我采用了Redis锁和幂等性的设计方案。

对于幂等性设计,我为每个修改积分的请求生成一个唯一的 ID,将请求的关键信息和处理结果存储在 Redis 中。当收到请求时,先检查 Redis 中是否已有该请求 ID 对应的处理结果,若有则直接返回,避免重复处理。

我引入了乐观锁机制。具体做法是,在数据库的积分表中增加一个版本号字段。当一个请求要修改群积分时,先查询当前积分和版本号,在执行更新操作时,通过 SQL 语句判断版本号是否和查询时一致,如果一致则更新积分并将版本号加1,若不一致则说明数据已被其他请求修改,当前请求需要重新获取最新数据进行处理。

可能出现的问题

(1)版本号溢出:定期重置版本号

(2)版本号冲突:设置最大重试次和异步补偿机制

什么是幂等:对同一个资源进行多次相同操作他们所产生的影响都是一样的

3、锁的实现

利用 Redis 来实现分布式锁的时候,所谓的锁就是一个普通的键值对。而加锁就是使用 SETNX 命令,排他地设置一个键值对。如果 SETNX 命令设置键值对成功了,那么说明加锁成功。如果没有设置成功,说明这个时候有人持有了锁,需要等待别人释放锁。而相应地,释放锁就是删除这个键值对。