优化很笼统的词汇,这说明它包含的信息量很大,要处理的事情很多。
这次就详细说说,项目优化,都分哪些。
上目录:
目录
一个一个来,先说
代码优化
代码优化主要对代码结构层次的优化,目的就是更加方便代码的可维护性与可读性
代码结构层次的优化
例如:
- 代码注释(代码规范)
- 工具类的封装(方便代码维护,使结构更加清晰,保证团队代码质量一致)
- 公共部分的提取
- 代码极简化(适度即可)
- 遇见多重if else 可以试试“策略模式”
这些是对代码层次的优化所提出的建议,其他的都不说,只针对最后一项,代码极简化这一问题提出一些方案
看完之后~继续咱的聊啊~然后就是
代码性能优化
列如:
- 使用一些性能比较高的类(bufferInputStream)
- 缓冲区块的大小(4k或者8k)
- 通常要用stringbuffer替代string加号拼接
- 解析大文件的xml数据使用sax替代dom4j,使用分段批量提交来完成大数据量的插入。
- 根据业务情况使用缓存减少对数据库的访问。
- 单线程应尽量使用 HashMap, ArrayList,因为HashTable,Vector使用了同步机制,降低了性能。
- 在finally块中关闭流,断开连接,释放资源。
- 避免在循环条件中使用复杂表达式 。
等等等等这些类似的
然后就是
业务优化
我们做项目的时候业务优化这方面最主要是从用户体验度角度进行考虑,减少用户操 作的步骤提高工作效率,通常有以下几种:
- 可以通过tabindex属性来改变tab键盘的操作顺序
- 可以通过回车键来进行搜索或者提交操作
- 对于单选按钮和复选按钮可以通过操作后面的文本来选择前面的单选按钮以及复选按钮
- 添加的信息要按照id倒序进行排列
- 进行搜索操作时加入js loading操作(不仅告诉用户所进行的请求正在被处理,而且防止用户多次点击提交操作)
- 当进行删除操作的时候要弹出提示框,警告用户要进行删除操作,是否确认,如果删除成功则弹出提示框告诉用户。
- 根据returnURL在用户登录成功后直接跳到想要访问的资源。
- 进行删除操作时通过confirm提示用户是否确认删除操作,操作完后提示操作是否成功。
- 减少用户操作的步骤
- 使用autocomplete插件快速进行搜索
等等等等... ...这些都是从业务角度来说的,
然后就是
数据库优化
1、缓存
首先第一种解决方案就是缓存了。
缓存,我们可以将数据直接缓存在内存中,例如 Map、也可以使用缓存框架如 Redis 等,将一些需要频繁使用的热点数据保存在缓存中,每当用户来访问的时候,就可以直接将缓存中的数据返回给用户,这样可以有效降低服务器的压力。(注意:可以缓存起来使用的数据,一般都不能对实时性要求太高)
2、SQL优化
很多时候程序跑得慢,不是因为设备落后,而是因为数据库 SQL 写的太差劲。
要解决海量数据的问题,数据库优化肯定也是不可避免的。一般来说,我们可以从 SQL 优化、表结构优化、以及数据库分区分表等多个方面来对数据库进行优化。
最常见的SQL语句的基本规范:
1、对查询进行优化,首先应考虑在 where 及 order by 涉及的列上建立索引
2、应尽量避免在 where 子句中对字段进行 null 值判断、
避免使用 !=或<>、
避免使用 or 来连接条件、
in 和 not in 也要慎用,对于连续的数值,能用 BETWEEN 或者EXISTS 就不要用 IN、
避免在 WHERE 子句中对字段进行表达式或者函数操作、 (否则会进行全表扫描)
3、索引固然可以提高相应的 SELECT 的效率,但同时也降低了 INSERT 及 UPDATE 的效。因为 INSERT 或 UPDATE 时有可能会重建索引,所以怎样建索引需要慎重考虑,视具体情况而定。一个表的索引数最好不要超过 6 个,若太多则应考虑一些不常使用到的列上建的索引是否有必要。
4、应尽可能的避免更新 clustered 索引数据列, 因为 clustered 索引数据列的顺序就是表记录的物理存储顺序,一旦该列值改变将导致整个表记录的顺序的调整,会耗费相当大的资源。若应用系统需要频繁更新 clustered 索引数据列,那么需要考虑是否应将该索引建为 clustered 索引。
5、尽量使用数字型字段,若只含数值信息的字段尽量不要设计为字符型,这会降低查询和连接的性能,并会增加存储开销。
6、尽可能的使用 varchar, nvarchar 代替 char, nchar。节省存储空间和提升查询效率。
7、不要使用 select * from
8、尽量避免向客户端返回大数据量,若数据量过大,应该考虑相应需求是否合理。
9、不要有超过 5 个以上的表连接(JOIN),考虑使用临时表或表变量存放中间结果。少用子查询,视图嵌套不要过深,一般视图嵌套不要超过 2 个为宜。
10、将需要查询的结果预先计算好放在表中,查询的时候再Select。这在SQL7.0以前是最重要的手段
当然,其他的不 一 一 细说了
想看详细的看这里👇
这里感谢Bug哥的文章。
说下一条之前我详细解释一下为何避免使用select * ?
然后就是另外一个需要考虑优化的地方
3、热点数据分离
数据库中的数据,虽然是海量数据,但是这些数据并不见得所有数据都是活跃数据,例如用户注册,有的用户注册完就消失的无影无踪了,而有的用户则在不停的登录,因此,对于这两种不同的用户,我们可以将活跃用户分离出来,在主要操作的数据表中只保存活跃用户数据。每次用户登录,先去主表中查看有没有记录,有的话,直接登录,没有的话,再去查看其他表。热点用户只需要通过判断用户在某一段时间内的登录次数,就可以很快分离出热点数据。
(我先声明啊~这只是解题思路~)
4、数据库读写分离
读写分离之后,一方面可以提高数据库的操作效率,另一方面也算是对数据库的一个备份。
5、页面静态化
emmmmm~不知道为啥~很多开发者也好其他架构也好,不是很喜欢页面静态化的写法,但是不得不说,这样确实能一定程度上减轻压力
而页面静态化,我理解其实可以算作是缓存的另外一种形式,相当于直接将相关的页面渲染结果缓存起来。
在我们的 Web 项目中,资源分为两大类:
- 静态资源
- 动态资源
静态资源就是我们常见的 HTML、CSS、JavaScript、图片等资源,这些资源可以不经过服务端处理,就可以直接返回给前端浏览器,浏览器就可以直接显示出来。
动态资源则是指我们项目中的 Servlet 接口、Jsp 文件、Freemarker 等,这些需要经过服务端渲染之后,才可以返回前端的资源。
在实际项目中,静态资源的访问速度要远远高于动态资源,动态资源往往很容易遇到服务器瓶颈、数据库瓶颈,因此,对于一些不经常更新的页面,或者说更新比较缓慢的页面,我们可以通过页面静态化,将一个动态资源保存为静态资源,这样当服务端需要访问的时候,直接将静态资源返回,就可以避免去操作数据库了,降低数据库的压力。
当然还要考虑到数据的长度已经浏览器的容量问题,总不能都一股脑的都塞浏览器里吧~
一般来说,Freemarker、Velocity 等都有相关的方法可以帮助我们快速将动态页面生成静态页面。
这就是页面静态化。
6、合并数据库操作
这个方案的宗旨其实是减少数据库操作的次数,例如多次插入操作,我们可以合并成一条 SQL 搞定。多个不同条件的查询,如果条件允许的话,也可以合并成为一个查询,尽量减少数据库的操作,减少在网络上消耗,同时也降低数据库的压力。
(注意:sql语句有长度的限制,让你合并,没让你将1000条数据合并到一起进行修改操作,那样怕是失了智~)
7、分布式数据库
数据库读写分离之后,无形中增大了代码的复杂度,所以一般还需要借助分布式数据库中间件,这样可以有效提高数据库的弹性,可以方便的随时为数据库扩容,同时也降低代码的耦合度。
8、NoSQL 和 Hadoop
另外,引入 NoSQL 和 Hadoop 也是解决方案之一。NoSQL 突破了关系型数据库中对表结构、字段等定义的条条框框,使用户可以非常灵活方便的操作,另外 NoSQL 通过多个存储块存储数据的特点,使得天然具备操作大数据的优势(快)。不过,老实说,NoSQL 目前还是在互联网项目中比较常见,在传统的企业级应用中还是比较少见。
Hadoop 就不必说了,大数据处理利器。
项目优化
1、缓存
不说,这一块儿的教学太多了,告辞~直接下一个~
2、数据库连接池应该设多大
接下来是正文
数据库连接池的配置是开发者们常常搞出坑的地方,在配置数据库连接池时,有几个可以说是和直觉背道而驰的原则需要明确。
你有一个网站,压力虽然还没到Facebook那个级别,但也有个1万上下的并发访问——也就是说差不多2万左右的TPS。那么这个网站的数据库连接池应该设置成多大呢?结果可能会让你惊讶,因为这个问题的正确问法是:
“这个网站的数据库连接池应该设置成多小呢?”
☞这个视频是Oracle Real World Performance Group发布的,有兴趣的太君~里面请~☜
因为这视频是英文解说且没有字幕,我替大家做一下简单的概括:
视频中对Oracle数据库进行压力测试,9600并发线程进行数据库操作,每两次访问数据库的操作之间sleep 550ms,一开始设置的中间件线程池大小为2048:
压测跑起来之后是这个样子的:
每个请求要在连接池队列里等待33ms,获得连接后执行SQL需要77ms
此时数据库的等待事件是这个熊样的:
各种buffer busy waits,数据库CPU在95%左右
接下来,把中间件连接池减到1024(并发什么的都不变),性能数据变成了这样:
获取链接等待时长没怎么变,但是执行SQL的耗时减少了。下面这张图,上半部分是wait,下半部分是吞吐量
能看到,中间件连接池从2048减半之后,吐吞量没变,但wait事件减少了一半。
接下来,把数据库连接池减到96,并发线程数仍然是9600不变。
96个连接时的性能数据
队列平均等待1ms,执行SQL平均耗时2ms。
wait事件几乎没了,吞吐量上升。
没有调整任何其他东西,仅仅只是缩小了中间件层的数据库连接池,就把请求响应时间从100ms左右缩短到了3ms。
为什么nginx只用4个线程发挥出的性能就大大超越了100个进程的Apache HTTPD?
即使是单核CPU的计算机也能“同时”运行数百个线程。但我们都[应该]知道这只不过是操作系统用时间分片玩的一个小把戏。一颗CPU核心同一时刻只能执行一个线程,然后操作系统切换上下文,核心开始执行另一个线程的代码,以此类推。给定一颗CPU核心,其顺序执行A和B永远比通过时间分片“同时”执行A和B要快,这是一条计算机科学的基本法则。一旦线程的数量超过了CPU核心的数量,再增加线程数系统就只会更慢,而不是更快。
这几乎就是真理了……
上面的说法只能说是接近真理,但还并没有这么简单,有一些其他的因素需要加入。当我们寻找数据库的性能瓶颈时,总是可以将其归为三类:CPU、磁盘、网络。把内存加进来也没有错,但比起磁盘和网络,内存的带宽要高出好几个数量级,所以就先不加了。
如果我们无视磁盘和网络,那么结论就非常简单。在一个8核的服务器上,设定连接/线程数为8能够提供最优的性能,再增加连接数就会因上下文切换的损耗导致性能下降。数据库通常把数据存储在磁盘上,磁盘又通常是由一些旋转着的金属碟片和一个装在步进马达上的读写头组成的。读/写头同一时刻只能出现在一个地方,然后它必须“寻址”到另外一个位置来执行另一次读写操作。所以就有了寻址的耗时,此外还有旋回耗时,读写头需要等待碟片上的目标数据“旋转到位”才能进行操作。使用缓存当然是能够提升性能的,但上述原理仍然成立。
在这一时间段(即"I/O等待")内,线程是在“阻塞”着等待磁盘,此时操作系统可以将那个空闲的CPU核心用于服务其他线程。所以,由于线程总是在I/O上阻塞,我们可以让线程/连接数比CPU核心多一些,这样能够在同样的时间内完成更多的工作。
那么应该多多少呢?这要取决于磁盘。较新型的SSD不需要寻址,也没有旋转的碟片。可别想当然地认为“SSD速度更快,所以我们应该增加线程数”,恰恰相反,无需寻址和没有旋回耗时意味着更少的阻塞,所以更少的线程[更接近于CPU核心数]会发挥出更高的性能。只有当阻塞创造了更多的执行机会时,更多的线程数才能发挥出更好的性能。
网络和磁盘类似。通过以太网接口读写数据时也会形成阻塞,10G带宽会比1G带宽的阻塞少一些,1G带宽又会比100M带宽的阻塞少一些。不过网络通常是放在第三位考虑的,有些人会在性能计算中忽略它们。
上图是PostgreSQL的benchmark数据,可以看到TPS增长率从50个连接数开始变缓。在上面Oracle的视频中,他们把连接数从2048降到了96,实际上96都太高了,除非服务器有16或32颗核心。
计算公式
下面的公式是由PostgreSQL提供的,不过我们认为可以广泛地应用于大多数数据库产品。你应该模拟预期的访问量,并从这一公式开始测试你的应用,寻找最合适的连接数值。
连接数 = ((核心数 * 2) + 有效磁盘数)
核心数不应包含超线程(hyper thread),即使打开了hyperthreading也是。
如果活跃数据全部被缓存了,那么有效磁盘数是0,随着缓存命中率的下降,有效磁盘数逐渐趋近于实际的磁盘数。
这一公式作用于SSD时的效果如何尚未有分析。
按这个公式,你的4核i7数据库服务器的连接池大小应该为((4 * 2) + 1) = 9。取个整就算是是10吧。是不是觉得太小了?跑个性能测试试一下,我们保证它能轻松搞定3000用户以6000TPS的速率并发执行简单查询的场景。如果连接池大小超过10,你会看到响应时长开始增加,TPS开始下降。
这一公式其实不仅适用于数据库连接池的计算,大部分涉及计算和I/O的程序,线程数的设置都可以参考这一公式。
公理总结
你需要一个小连接池,和一个充满了等待连接的线程的队列
如果你有10000个并发用户,设置一个10000的连接池基本等于失了智。1000仍然很恐怖。即是100也太多了。你需要一个10来个连接的小连接池,然后让剩下的业务线程都在队列里等待。连接池中的连接数量应该等于你的数据库能够有效同时进行的查询任务数(通常不会高于2*CPU核心数)。
我们经常见到一些小规模的web应用,应付着大约十来个的并发用户,却使用着一个100连接数的连接池。这会对你的数据库造成极其不必要的负担。
请注意
连接池的大小最终与系统特性相关。
比如一个混合了长事务和短事务的系统,通常是任何连接池都难以进行调优的。最好的办法是创建两个连接池,一个服务于长事务,一个服务于短事务。
再例如一个系统执行一个任务队列,只允许一定数量的任务同时执行,此时并发任务数应该去适应连接池连接数,而不是反过来。
3、高并发方案
(这一块儿我又得单独拉一块儿~先写干货吧~)
纵向扩展(scale-up)
它的目标是提升单机的处理能力,方案又包括:
- 提升单机的硬件性能:通过增加内存、CPU核数、存储容量、或者将磁盘升级成SSD等堆硬件的方式来提升。
- 提升单机的软件性能:使用缓存减少IO次数,使用并发或者异步的方式增加吞吐量。
横向扩展(scale-out)
因为单机性能总会存在极限,所以最终还需要引入横向扩展,通过集群部署以进一步提高并发处理能力,又包括以下2个方向:
1、做好分层架构:
这是横向扩展的提前,因为高并发系统往往业务复杂,通过分层处理可以简化复杂问题,更容易做到横向扩展。
上面这种图是互联网最常见的分层架构,当然真实的高并发系统架构会在此基础上进一步完善。比如会做动静分离并引入CDN,反向代理层可以是LVS+Nginx,Web层可以是统一的API网关,业务服务层可进一步按垂直业务做微服务化,存储层可以是各种异构数据库。
2、各层进行水平扩展:
无状态水平扩容,有状态做分片路由。业务集群通常能设计成无状态的,而数据库和缓存往往是有状态的,因此需要设计分区键做好存储分片,当然也可以通过主从同步、读写分离的方案提升读性能。
高性能的实践方案
- 集群部署,通过负载均衡减轻单机压力。
- 多级缓存,包括静态数据使用CDN、本地缓存、分布式缓存等,以及对缓存场景中的热点key、缓存穿透、缓存并发、数据一致性等问题的处理。
- 分库分表和索引优化,以及借助搜索引擎解决复杂查询问题。
- 考虑NoSQL数据库的使用,比如HBase、TiDB等,但是团队必须熟悉这些组件,且有较强的运维能力。
- 异步化,将次要流程通过多线程、MQ、甚至延时任务进行异步处理。
- 限流,需要先考虑业务是否允许限流(比如秒杀场景是允许的),包括前端限流、Nginx接入层的限流、服务端的限流。
- 对流量进行削峰填谷,通过MQ承接流量。
- 并发处理,通过多线程将串行逻辑并行化。
- 预计算,比如抢红包场景,可以提前计算好红包金额缓存起来,发红包时直接使用即可。
- 缓存预热,通过异步任务提前预热数据到本地缓存或者分布式缓存中。
- 减少IO次数,比如数据库和缓存的批量读写、RPC的批量接口支持、或者通过冗余数据的方式干掉RPC调用。
- 减少IO时的数据包大小,包括采用轻量级的通信协议、合适的数据结构、去掉接口中的多余字段、减少缓存key的大小、压缩缓存value等。
- 程序逻辑优化,比如将大概率阻断执行流程的判断逻辑前置、For循环的计算逻辑优化,或者采用更高效的算法。
- 各种池化技术的使用和池大小的设置,包括HTTP请求池、线程池(考虑CPU密集型还是IO密集型设置核心参数)、数据库和Redis连接池等。
- JVM优化,包括新生代和老年代的大小、GC算法的选择等,尽可能减少GC频率和耗时。
- 锁选择,读多写少的场景用乐观锁,或者考虑通过分段锁的方式减少锁冲突。
上述方案无外乎从计算和 IO 两个维度考虑所有可能的优化点,需要有配套的监控系统实时了解当前的性能表现,并支撑你进行性能瓶颈分析,然后再遵循二八原则,抓主要矛盾进行优化。
高可用的实践方案
- 对等节点的故障转移,Nginx和服务治理框架均支持一个节点失败后访问另一个节点。
- 非对等节点的故障转移,通过心跳检测并实施主备切换(比如redis的哨兵模式或者集群模式、MySQL的主从切换等)。
- 接口层面的超时设置、重试策略和幂等设计。
- 降级处理:保证核心服务,牺牲非核心服务,必要时进行熔断;或者核心链路出问题时,有备选链路。
- 限流处理:对超过系统处理能力的请求直接拒绝或者返回错误码。
- MQ场景的消息可靠性保证,包括producer端的重试机制、broker侧的持久化、consumer端的ack机制等。
- 灰度发布,能支持按机器维度进行小流量部署,观察系统日志和业务指标,等运行平稳后再推全量。
- 监控报警:全方位的监控体系,包括最基础的CPU、内存、磁盘、网络的监控,以及Web服务器、JVM、数据库、各类中间件的监控和业务指标的监控。
- 灾备演练:类似当前的“混沌工程”,对系统进行一些破坏性手段,观察局部故障是否会引起可用性问题。
而高可用的方案主要从冗余、取舍、系统运维3个方向考虑,同时需要有配套的值班机制和故障处理流程,当出现线上问题时,可及时跟进处理。
高扩展的实践方案
- 合理的分层架构:比如上面谈到的互联网最常见的分层架构,另外还能进一步按照数据访问层、业务逻辑层对微服务做更细粒度的分层(但是需要评估性能,会存在网络多一跳的情况)。
- 存储层的拆分:按照业务维度做垂直拆分、按照数据特征维度进一步做水平拆分(分库分表)。
- 业务层的拆分:最常见的是按照业务维度拆(比如电商场景的商品服务、订单服务等),也可以按照核心接口和非核心接口拆,还可以按照请求源拆(比如To C和To B,APP和H5)。
高并发确实是一个复杂且系统性的问题,由于篇幅有限,诸如分布式Trace、全链路压测、柔性事务都是要考虑的技术点。另外,如果业务场景不同,高并发的落地方案也会存在差异,但是总体的设计思路和可借鉴的方案基本类似。
高并发设计同样要秉承架构设计的3个原则:简单、合适和演进。“过早的优化是万恶之源”,不能脱离业务的实际情况,更不要过度设计,合适的方案就是最完美的。
本人才疏学浅,难免犯错,若发现文中有错误遗漏,望不吝赐教。