🔔目的
提升后端API性能的主要目的是为了提高系统整体的响应速度、并发能力以及可用性。主要原因包括:
-
提高用户体验
后端API性能好可以减少响应延迟,给用户流畅的体验。
-
提高系统吞吐量
优化API性能可以提高系统的整体吞吐量,处理更多用户请求。
-
节省服务器资源成本
优化API提高资源利用率,减少对计算、内存、网络等资源的占用。
-
提高系统稳定性
良好的API性能可以防止请求积压造成的链路阻塞,减少超时、服务降级等问题。
-
促进业务发展
性能良好的API可以支撑更复杂的业务场景,为产品迭代和业务增长提供保障。
-
提升运维效率
代码和架构优化可以降低重复工作量,减少故障排查时间。
-
增强系统容错能力
优化后可以应对更大的流量冲击和失效情形。
🔔优化概述
-
代码优化
通过算法优化、减少IO等方式优化程序,使其高效运行。
-
缓存使用
通过Redis、Memcache等缓存数据库缓存常用数据,减少数据库查询。
-
CDN加速
使用CDN缓存静态资源,减少服务器压力。
-
异步处理
通过消息队列、事件驱动等方式实现异步处理,提高并发能力。
-
服务拆分
将服务拆分为小的单元服务,采用微服务架构。
-
流量控制
通过限流、降级等方式控制流量并保护服务稳定运行。
-
数据库优化
优化数据库模式,使用索引、读写分离等技术提升数据库效率。
-
并发优化
通过线程池、非阻塞IO等方式提升系统并发性能。
-
服务器扩容
垂直扩容服务器或利用云服务横向扩容,增强处理能力。
🔔具体实践
-
线程池化
池化技术(Pooling)的关键思想就是重用,其目的是为了避免每次需要资源时都要重复创建和销毁,从而提高性能和资源利用率。
以数据库连接池为例,不使用连接池的时候,每次操作数据库都需要:- 创建数据库连接
- 执行sql语句
- 关闭数据库连接
这样重复创建和关闭连接会非常耗费资源。
而引入连接池后,可以提前创建好一定数量的连接,放入连接池待用。需要时直接从池中取出已有连接使用,操作完毕再放回池中,而不需要重复创建连接。
同样,线程池也是提前创建线程,组成线程池待用,需要时直接派一个空闲线程执行任务,从而避免了频繁创建和销毁线程的资源消耗。
池化技术重在提高资源的重复利用率。目的是为了提高性能,减少不必要的性能开销。现在几乎所有的连接资源都会使用池化技术进行管理。 -
批量入库
对于需要批量插入或者更新到数据库的操作,可以先批量处理逻辑完之后再统一一次性插入数据库,这样做的优势在于
-
减少网络交互,提高写入效率。
向数据库批量插入可以减少客户端与数据库之间的网络往返。
-
可以对数据进行预处理。
可以在入库前对数据进行过滤、转换等优化。
-
减少索引更新开销。
可以关闭索引,批量插入后再重建索引,从而减少索引更新带来的开销。
-
可以执行更复杂的SQL逻辑。
批处理可以构建更复杂的SQL逻辑完成数据导入。
-
提高数据库并发能力。
批量写入可以聚合成少量大事务,可以减少数据库并发Transaction的数量。
-
-
异步执行
在设计接口时,对于一些非核心业务逻辑,如果这部分逻辑执行时间长且不影响主业务流程,我们可以考虑“异步”执行这些逻辑。
具体来说,可以通过以下技术方案实现:-
使用消息队列,让主业务逻辑快速返回,将非核心逻辑作为消息放入队列异步执行。
-
设计异步线程或定时任务,在主线程返回后,异步线程负责后台执行非核心逻辑。
-
利用事件编程模型,主业务逻辑触发事件,事件监听者异步响应事件执行非核心逻辑。
-
非核心逻辑作为微服务单独部署,主业务快速调用微服务,微服务后台异步执行逻辑。
-
使用reactor模式,主线程接收请求触发非核心逻辑,再通过多线程异步执行非核心处理。
常见的异步有:
- 多线程 - 在新线程中执行异步任务,主线程不等待异步线程结束即返回。
- 事件/回调 - 主线程注册回调,异步任务完成后由系统调用回调函数,通知主线程。
- Future/Promise - 主线程返回一个future对象,异步线程设置future的结果,主线程可以获取future的结果。
- Reactor模式 - 基于事件循环的模型,主线程接收请求,dispatch事件给异步线程池处理。
-消息队列 - 主线程产生消息,通过消息队列进行异步处理。如 RabbitMQ, Kafka。 - Observable - 主线程注册Observer,由Observable异步调用Observer的回调方法。如 RxJava。
- Async/Await - 使用async标记的函数自动异步执行,await可以等待异步函数结果。
- 协程 - 可以手动控制协程的切换,实现异步处理。如Goroutine。
-
-
使用缓存
恰当地使用缓存,可以大大的提升接口的性能。
使用缓存的主要好处有:
-
减少数据库查询,降低后端负载
缓存可以存储热点数据,减少对数据库的查询,降低后端存储系统的压力。 -
加速读取速度
从缓存读取数据比数据库查询要快得多,可以显著提高访问速度。 -
改善用户体验
加速系统响应,用户会感受到更流畅的用户体验。 -
提高系统扩展能力
缓存层可以作为数据库前的缓冲层,让系统支持更高的负载。 -
降低基础设施成本
减少存储系统扩容提升需求,降低整体IT成本。 -
保护核心数据系统
缓存可充当外部系统与核心存储之间的屏障。 -
帮助实现高可用性
缓存可作为备份数据源,在主数据源不可用时提供冗余数据访问。
常见的缓存有:
-
Redis - 基于内存的键值缓存,支持多种数据结构,性能极高。
-
Memcached - 简单的内存键值缓存,没有Redis丰富的数据结构。
-
-
慢查询优化
可以从以下几点优化:
- 数据库结构优化
- 合理设计表结构,避免冗余数据。
- 对于高并发修改的字段,拆分到单独表中。
- 对访问频繁的列建立索引。
- SQL语句优化
- 尽量避免全表扫描,先通过索引字段过滤数据。
- 避免在索引列上做函数转换。
- 对多个表Join时,保证Join条件列有索引。
- 合理利用慢查询日志分析和调优查询。
- 数据库参数优化
- 调整max_connections、table_open_cache等系统变量。
- 调整innodb_buffer_pool_size、innodb_log_file_size等InnoDB存储引擎参数。
- 架构优化
- 对热点数据进行缓存。
- 对可读数据库使用主从复制分离读写。
- 拆分数据库,分散压力。
- 使用索引代替Join查询。
- 程序优化
- 避免N+1问题,使用Join提前预加载关联数据。
- 避免频繁小请求数据库,可以批处理或异步处理。
- 数据库结构优化
-
锁粒度避免过粗
在设计并发程序时,使用锁(mutex)来保护共享资源,但锁的范围不能设计得过于宽泛,这称为锁的粒度问题。
过粗的锁粒度意味着锁的范围过于宽泛,例如对整个应用只有一把大锁。这会带来以下问题:-
锁争用过于频繁,并发程度低
-
可能会发生死锁
-
锁的获取和释放频繁上下文切换,性能消耗严重
因此需要注意锁粒度的选择:
-
只在访问共享资源时加锁,不要锁住无关代码
-
可以将一个大锁拆分为多个细粒度的锁
-
根据代码逻辑设计锁的范围,避免锁过多或过少
-
不同的线程访问不同资源应该用不同的锁
示例
private void A(){ } //共享方法 private void B(){ } private int C(){ synchronized(this){ A(); B(); } }
修改为
private void A(){ } //共享方法 private void B(){ } private int C(){ A(); synchronized(this){ B(); } }
-
-
串行改并行
在设计程序时,原本采用了串行逻辑,即一个任务完成后再执行下一个任务。这种模型导致任务只能顺序执行,整体吞吐量受到限制。
为提高吞吐量,可以考虑将程序逻辑改为并行执行。具体做法是:- 把任务进行拆分,同一类任务使用多个实例并行地执行。
- 对于需要顺序的任务,可以使用消息队列将任务异步化,提高并行程度。
- 对串行的业务流程进行重构,看哪些环节可以通过多线程、异步来并行执行。
- 对串行访问的共享资源,使用锁或CAS算法来控制并发访问。
- 使用线程池、actor模型等并发框架,提高程序对多核CPU的利用率。
通过程序逻辑从串行改为并行,可以显著提升系统整体的吞吐量和处理能力。需要注意资源竞争和死锁等并发问题。适当保留关键串行流程来实现正确性。
比如 串行
改成
并行
🔔写在最后
如果大家对相关文章感兴趣,可以关注公众号"架构殿堂",会持续更新AIGC,java基础面试题, netty, spring boot, spring cloud等系列文章,一系列干货随时送达!