后端接口性能优化分析-多线程优化(下)

本文涉及的产品
云数据库 Redis 版,社区版 2GB
推荐场景:
搭建游戏排行榜
简介: 后端接口性能优化分析-多线程优化

后端接口性能优化分析-多线程优化(中):https://developer.aliyun.com/article/1413669


缓存雪崩


描述:缓存雪崩是指缓存中数据大批量到过期时间,而查询数据量巨大,引起数据库压力过大甚至down机。和缓存击穿不同的是,缓存击穿是并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。


解决方案


1)缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生。

2)如果缓存系统是分布式部署,将热点数据均匀分布在不同的缓存节点中。

3)设置热点数据永远不过期。


缓存更新


失效:应用程序先从cache取数据,没有得到,则从数据库中取数据,成功后,放到缓存中。


命中:应用程序从cache中取数据,取到后返回。


更新:先把数据存到数据库中,成功后,再让缓存失效。


总结


缓存的刷新策略选择:失效刷新还是定时刷新。 因为监控到很多接口RT总是有规律的变慢,这是因为都是在缓存失效的时候,需要从db及其他模块组装数据,然后推到缓存,这时所有请求都走不了缓存,在流量大的时候也有可能成为致命的因素。如果是这种情况,例如首页推荐商品、推荐帖子等等访问量大且相同的场景可以通过定时刷新的方式。 Keys*命令线上严禁使用:Redis是单线程,该命令的执行将会导致所有后续请求阻塞,影响整个系统性能。


Redis相比传统数据库更快且具有更强的抗并发能力。然而,与本地缓存相比,Redis缓存仍然较慢。前面提到的Redis访问速度大约在3-5毫秒之间,而使用本地缓存几乎可以忽略不计。


如果频繁访问Redis获取大量数据,将会导致大量的序列化和反序列化操作,这会显著增加young gc频率,也会增加CPU负载。


  1. 序列化和反序列化操作:在 Java 应用程序中,当需要从 Redis 获取大量数据时,数据首先需要从 Redis 服务器传输到客户端,然后需要将 Redis 中的字节流转换为 Java 对象。这个过程被称为反序列化。当应用程序处理完数据后,Java 对象需要转换为字节流并发送回 Redis 服务器,这个过程被称为序列化。序列化和反序列化操作会占用大量的 CPU 资源,因为它们需要执行大量的计算和内存操作。


  1. young gc 频率增加:当应用程序执行大量的序列化和反序列化操作时,会产生大量的临时对象。这些临时对象的生命周期非常短,很快就会被 Java 虚拟机 (JVM) 的 young gc(年轻代垃圾回收)清除。因此,频繁的序列化和反序列化操作会导致 young gc 频率增加,从而影响系统的性能。


  1. CPU 负载增加:序列化和反序列化操作会占用大量的 CPU 资源,因为它们需要执行大量的计算和内存操作。当应用程序需要从 Redis 获取大量数据时,CPU 需要执行大量的序列化和反序列化操作,这会导致 CPU 负载增加。如果 CPU 负载过高,可能会导致系统性能下降,甚至导致系统崩溃。


以下是针对 “当应用程序执行大量的序列化和反序列化操作时,会产生大量的临时对象 ”的解释

在 Java 应用程序中,当执行大量的序列化和反序列化操作时,会产生大量的临时对象。这是因为在序列化和反序列化过程中,Java 对象会被转换为字节流(序列化),然后字节流会被转换为 Java 对象(反序列化)。在这个过程中,每个 Java 对象在转换为字节流和反序列化为 Java 对象时,都会产生一个临时的字节流对象和 Java 对象。例如,假设有一个 Java 对象 person,当对其进行序列化操作时,首先会将 person 对象转换为一个字节流(例如,保存在一个 byte[] 数组中)。在这个过程中,会产生一个临时的 person 对象和一个临时 byte[] 对象。然后,当需要将这个字节流反序列化为一个 Java 对象时,会将这个 byte[] 对象转换为一个新的 person 对象。在这个过程中,又会产生一个临时 byte[] 对象和一个临时 person 对象。因此,执行大量的序列化和反序列化操作时,会产生大量的临时对象。这些临时对象的生命周期非常短,很快就会被 Java 虚拟机 (JVM) 的垃圾回收机制清除。尽管这些临时对象的生命周期短暂,但它们仍然会占用一定的内存资源,并且在被垃圾回收之前,它们会经历 young gc(年轻代垃圾回收)过程。因此,大量的临时对象会导致 young gc 频率增加,从而影响系统的性能。


5.预取思想:提前初始化到缓存


对于访问量较低的接口来说,通常首次接口的响应时间较长。原因是JVM需要加载类、Spring Aop首次动态代理,以及新建连接等。这使得首次接口请求时间明显比后续请求耗时长。


然而在流量较低的接口中,这种影响会更大。用户可能尝试多次请求,但依然经常出现超时,严重影响了用户体验。每次服务发布完成后,接口超时失败率都会大量上升!


预取思想很容易理解,就是提前把要计算查询的数据,初始化到缓存。如果你在未来某个时间需要用到某个经过复杂计算的数据,才实时去计算的话,可能耗时比较大。这时候,我们可以采取预取思想,提前把将来可能需要的数据计算好,放到缓存中,等需要的时候,去缓存取就行。


场景举例:


  • 例如地区数据或者一些数据字典数据,可以在项目启动时预加载到缓存中,在使用时从缓存获取,提升性能;
  • 部分报表类数据,关联业务表很多,实时计算比较耗时,可以通过定时任务,在晚上业务不繁忙时,将数据生成好存放到ElasticSearch中,从Es中查询,提供性能。


项目启动执行方法:


  • 可以通过实现ApplicationRunner接口中的run方法,实现启动时执行。方法执行时,项目已经初始化完毕,是可以正常提供服务
public class DataInitUtil implements ApplicationRunner{
  @Override
    public void run(ApplicationArguments args) throws Exception {
        System.out.println("在项目启动时,会执行这个方法中的代码");
    }
}

在这里还有一个数据过期策略,其实也和该思想息息相关


参考阿里云社区文章,当一张表的数据量太大的情况下,如果不按照索引和日期进行部分扫描而出现全表扫描的情况,对DB的查询性能是非常有影响的,建议合理的设计数据过期策略,历史数据定期放入history表,或者备份到离线表中,减少线上大量数据的存储。


6.池化思想:预分配与循环使用


大家应该都记得,我们为什么需要使用线程池


线程池可以帮我们管理线程,避免增加创建线程和销毁线程的资源损耗。


如果你每次需要用到线程,都去创建,就会有增加一定的耗时,而线程池可以重复利用线程,避免不必要的耗时。 池化技术不仅仅指线程池,很多场景都有池化思想的体现,它的本质就是预分配与循环使用

比如TCP三次握手,大家都很熟悉吧,它为了减少性能损耗,引入了Keep-Alive长连接,避免频繁的创建和销毁连接。当然,类似的例子还有很多,如数据库连接池、HttpClient连接池。


  • TCP 三次握手是建立 TCP 连接的过程,其中涉及到客户端和服务器之间的三次交互。这个过程虽然保证了连接的可靠性,但同时也带来了性能损耗,因为每次连接都需要进行序列化、反序列化、验证等操作。为了解决这个问题,TCP 协议引入了 Keep-Alive 机制,允许在一段时间内保持连接处于活动状态,而无需进行三次握手。在 Keep-Alive 连接期间,如果需要发送数据,可以直接发送,而不需要重新进行三次握手。


池化思想本质


  • 如果你每次需要用到线程,都去创建,就会有增加一定的耗时;
  • 线程池可以重复利用线程,避免不必要的耗时;
  • 池化技术不仅仅指线程池,很多场景都有池化思想的体现,它的本质就是预分配与循环使用。


7.事件回调思想:拒绝阻塞等待


如果你调用一个系统B的接口,但是它处理业务逻辑,耗时需要10s甚至更多。然后你是一直阻塞等待,直到系统B的下游接口返回,再继续你的下一步操作吗?这样显然不合理


我们参考IO多路复用模型。即我们不用阻塞等待系统B的接口,而是先去做别的操作。等系统B的接口处理完,通过事件回调通知,我们接口收到通知再进行对应的业务操作即可。


IO多路复用模型


IO多路复用模型是一种并发模型,它允许我们在一个线程中同时处理多个I/O操作,例如读取和写入。在这种模型中,我们可以使用事件驱动的方式,即在调用系统B的接口时,我们不等待其返回,而是通过事件回调机制来通知。具体来说,当我们的应用程序调用系统B的接口时,如果该接口需要处理的业务逻辑比较耗时,我们的应用程序可以继续执行其他任务,而不是等待系统B的接口返回。当系统B的接口处理完后,它会通过事件回调机制通知我们的应用程序。我们的应用程序在收到通知后,再进行对应的业务操作即可。这种方式的优点是,它提高了应用程序的并发性能,因为它允许我们的应用程序在等待系统B的接口返回时,继续执行其他任务。这样,我们不仅可以减少等待时间,还可以提高CPU资源的利用率。此外,通过事件回调机制,我们的应用程序可以更加灵活地处理各种事件,例如系统B的接口返回成功、失败或出现异常等。总之,采用IO多路复用模型和事件驱动的方式,我们可以更加高效地处理耗时的系统B接口调用,提高应用程序的并发性能和用户体验。


在IO多路复用中,其中最典型的就是Redis,Redis明明是单线程的,但是为什么这么快?


回到这个问题本身,其实就是两个因素,完全基于内存、IO多路复用。


下面从多进程、多线程、基于单进程的 IO 多路复用 三个角度来分析:


多进程


对于并发情况,假如一个进程不行,那搞多个进程不就可以同时处理多个客户端连接了么?


多进程这种方式的确可以解决了服务器在同一时间能处理多个客户端连接请求的问题,但是仍存在一些缺点:


  • fork()等系统调用会使得进程上下文进行切换,效率较低
  • 进程创建的数量随着连接请求的增加而增加。比如 10w 个请求,就要 fork 10w 个进程,开销太大
  • 进程与进程之间的地址空间是私有、独立的,使得进程之间的数据共享变得困难


进程间的消息通信主要有以下几种方式:


  1. 管道(Pipe):这是最基本的进程间通信方式,数据只能单向流动,并且只能在具有亲缘关系的进程之间使用。管道分为匿名管道和命名管道,匿名管道主要用于父子进程间的通信,命名管道则允许无亲缘关系进程间的通信。
  2. 信号(Signal):这是一种比较复杂的通信方式,信号是一种异步通信方式,可以将信号看作是一种软件中断。进程间可以发送各种信号,而系统定义了每种信号的默认行为,如终止进程、忽略信号等。
  3. 消息队列(Message Queue):消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冡区大小受限等缺点。
  4. 共享内存(Shared Memory):共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是在已有的物理内存上开辟出一片可以被其他或多个进程共同使用的内存区域,多个进程可以直接对这片内存区域进行读写操作。
  5. 信号量(Semaphore):主要作为控制多个进程之间的同步关系。信号量是一个非负整数的计数器,是系统范围的资源,可以由多个进程共享。
  6. 套接字(Socket):可用于不同机器之间的进程通信,是最灵活的进程间通信方法。


多线程


线程是运行在进程上下文的逻辑流,一个进程可以包含多个线程,多个线程运行在同一进程上下文中,因此可共享这个进程地址空间的所有内容,解决了进程与进程之间通信难的问题。


同时,由于一个线程的上下文要比一个进程的上下文小得多,所以线程的上下文切换,要比进程的上下文切换效率高得多。


IO 多路复用


简单理解就是:一个服务端进程可以同时处理多个套接字描述符。


  • 多路:多个客户端连接(连接就是套接字描述符)
  • 复用:使用单进程就能够实现同时处理多个客户端的连接


以上是通过增加进程和线程的数量来并发处理多个套接字,免不了上下文切换的开销,而 IO 多路复用只需要一个进程就能够处理多个套接字,从而解决了上下文切换的问题。


其发展可以分 select->poll→epoll 三个阶段来描述。


如何简单理解 select/poll/epoll 呢?


举例说明


领导分配员工开发任务,有些员工还没完成。如果领导要每个员工的工作都要验收 check,那在未完成的员工那里,只能阻塞等待,等待他完成之后,再去 check 下一位员工的任务,造成性能问题。


select


领导找个 Team Leader(后文简称 TL),负责代自己 check 每位员工的开发任务。


TL 的做法是:遍历问各个员工“完成了么?”,完成的待 CR check 无误后合并到 Git 分支,对于其他未完成的,休息一会儿后再去遍历…


但是这样存在一个问题就是,这个TL存在能力短板问题,最多只能管理1024个员工,并且很多员工的任务没有完成,而且短时间内也完不成的话,TL 还是会不停的去遍历问询,影响效率。


select函数:

int select(int maxfdp1,fd_set *readset,fd_set *writeset,fd_set *exceptset,const struct timeval *timeout);

select 函数监视的文件描述符分 3 类,分别是 writefds、readfds、和 exceptfds。调用后 select 函数会阻塞,直到有描述符就绪(有数据可读、可写、或者有 except),或者超时(timeout 指定等待时间,如果立即返回设为 null 即可),函数返回。当 select 函数返回后,可以通过遍历 fdset,来找到就绪的描述符。


select 具有良好的跨平台支持,其缺点在于单个进程能够监视的文件描述符的数量存在最大限制,在 Linux 上一般为 1024。


poll


换一个能力更强的 New Team Leader(后文简称 NTL),可以管理更多的员工,这个 NTL 可以理解为 poll。


poll函数:

intpoll(structpollfd*fds, nfds_t nfds,int timeout);
typedef struct pollfd{ 
    int fd; // 需要被检测或选择的文件描述符 
    short events; // 对文件描述符fd上感兴趣的事件 
    short revents; // 文件描述符fd上当前实际发生的事件
} 
pollfd_t;

poll 改变了文件描述符集合的描述方式,使用了 pollfd 结构而不是 select 的 fd_set 结构,使得 poll 支持的文件描述符集合限制远大于 select 的 1024。


epoll


在上一步 poll 方式的 NTL 基础上,改进一下 NTL 的办事方法:遍历一次所有员工,如果任务没有完成,告诉员工待完成之后,其应该做 xx 操作(制定一些列的流程规范)。这样 NTL 只需要定期 check 指定的关键节点就好了。这就是 epoll。


epoll 是 Linux 内核为处理大批量文件描述符而作了改进的 poll,是 Linux 下多路复用 IO 接口 select/poll 的增强版本,它能显著提高程序在大量并发连接中只有少量活跃的情况下的系统 CPU 利用率。


IO 多路复用在 Redis 中的应用


Redis 服务器是一个事件驱动程序, 服务器处理的事件分为时间事件(fork出的子进程中,处理如AOF持久化任务等)和文件事件(Redis主进程中,主要处理客户端的连接请求与响应)两类。


由于 Redis 的文件事件是单进程,单线程模型,但是确保持着优秀的吞吐量,IO 多路复用起到了主要作用。


文件事件是对套接字操作的抽象,每当一个套接字准备好执行连接应答、写入、读取、关闭等操作时,就会产生一个文件事件。因为一个服务器通常会连接多个套接字,所以多个文件事件有可能会并发地出现。


IO 多路复用程序负责监听多个套接字并向文件事件分派器传送那些产生了事件的套接字。文件事件分派器接收 IO 多路复用程序传来的套接字,并根据套接字产生的事件的类型,调用相应的事件处理器。


总结


Redis 6.0 之后的版本开始选择性使用多线程模型。


Redis 选择使用单线程模型处理客户端的请求主要还是因为 CPU 不是 Redis 服务器的瓶颈,使用多线程模型带来的性能提升并不能抵消它带来的开发成本和维护成本,系统的性能瓶颈也主要在网络 I/O 操作上;


而 Redis 引入多线程操作也是出于性能上的考虑,对于一些大键值对的删除操作,通过多线程非阻塞地释放内存空间也能减少对 Redis 主线程阻塞的时间,提高执行的效率。

相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore     ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库 ECS 实例和一台目标数据库 RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
目录
相关文章
|
1月前
|
API 数据库 Android开发
构建高效Android应用:探究Kotlin多线程优化策略
【2月更文挑战第14天】随着移动设备性能的日益强大,用户对应用程序的响应速度和流畅性要求越来越高。在Android开发中,合理利用多线程技术是提升应用性能的关键手段之一。Kotlin作为一种现代的编程语言,其协程特性为开发者提供了更为简洁高效的多线程处理方式。本文将深入探讨使用Kotlin进行Android多线程编程的最佳实践,包括协程的基本概念、优势以及在实际项目中的应用场景和性能优化技巧,旨在帮助开发者构建更加高效稳定的Android应用。
|
2月前
|
人工智能 Java API
Python 潮流周刊#28:两种线程池、四种优化程序的方法
Python 潮流周刊#28:两种线程池、四种优化程序的方法
22 1
|
2月前
|
缓存 负载均衡 算法
后端架构设计中的优化技巧
【2月更文挑战第9天】 后端架构设计是一个复杂而关键的工作,不仅需要考虑系统的可靠性和扩展性,还需要保证系统的高性能。本文将介绍一些后端架构设计中的优化技巧,包括数据库设计、缓存优化、负载均衡等方面的内容,帮助开发者在设计后端架构时更好地提升系统性能。
28 1
|
1月前
|
人工智能 JSON 前端开发
【Spring boot实战】Springboot+对话ai模型整体框架+高并发线程机制处理优化+提示词工程效果展示(按照框架自己修改可对接市面上百分之99的模型)
【Spring boot实战】Springboot+对话ai模型整体框架+高并发线程机制处理优化+提示词工程效果展示(按照框架自己修改可对接市面上百分之99的模型)
|
15天前
|
Java
深入理解Java并发编程:线程池的应用与优化
【4月更文挑战第3天】 在Java并发编程中,线程池是一种重要的资源管理工具,它能有效地控制和管理线程的数量,提高系统性能。本文将深入探讨Java线程池的工作原理、应用场景以及优化策略,帮助读者更好地理解和应用线程池。
|
30天前
|
监控 Java
Java并发编程中的线程池优化技巧
在Java并发编程中,线程池扮演着至关重要的角色。本文将深入探讨如何优化Java线程池,从线程池的创建与配置、任务队列的选择、拒绝策略的制定、线程池状态的监控等多个方面进行详细阐述。通过本文的阅读,您将了解到如何合理地利用线程池,提高系统的并发性能,从而更好地应对各种并发场景。
|
2天前
|
缓存 负载均衡 数据库
优化后端性能:提升Web应用响应速度的关键策略
在当今数字化时代,Web应用的性能对于用户体验至关重要。本文探讨了如何通过优化后端架构和技术手段,提升Web应用的响应速度。从数据库优化、缓存机制到异步处理等多个方面进行了深入分析,并提出了一系列实用的优化策略,以帮助开发者更好地应对日益增长的用户访问量和复杂的业务需求。
8 1
|
4天前
|
SQL 关系型数据库 数据库
【后端面经】【数据库与MySQL】SQL优化:如何发现SQL中的问题?
【4月更文挑战第12天】数据库优化涉及硬件升级、操作系统调整、服务器/引擎优化和SQL优化。SQL优化目标是减少磁盘IO和内存/CPU消耗。`EXPLAIN`命令用于检查SQL执行计划,关注`type`、`possible_keys`、`key`、`rows`和`filtered`字段。设计索引时考虑外键、频繁出现在`where`、`order by`和关联查询中的列,以及区分度高的列。大数据表改结构需谨慎,可能需要停机、低峰期变更或新建表。面试中应准备SQL优化案例,如覆盖索引、优化`order by`、`count`和索引提示。优化分页查询时避免大偏移量,可利用上一批的最大ID进行限制。
26 3
|
4天前
|
Java 程序员 编译器
Java中的线程同步与锁优化策略
【4月更文挑战第14天】在多线程编程中,线程同步是确保数据一致性和程序正确性的关键。Java提供了多种机制来实现线程同步,其中最常用的是synchronized关键字和Lock接口。本文将深入探讨Java中的线程同步问题,并分析如何通过锁优化策略提高程序性能。我们将首先介绍线程同步的基本概念,然后详细讨论synchronized和Lock的使用及优缺点,最后探讨一些锁优化技巧,如锁粗化、锁消除和读写锁等。
|
29天前
|
安全 Java
Java中的多线程编程实践与优化
本文将深入探讨在Java编程中多线程的应用以及如何通过优化提高程序性能。通过分析多线程编程的原理和常见问题,结合实际案例剖析多线程应用中的挑战和解决方法,帮助读者更好地理解和运用多线程技术。