大家好,我是Tom哥。作为后端研发同学,为了几两银子没日没夜的和各种人事物打交道。要想成长得更快,就要学会总结,找到规律,并善用这些规律。比如工作,虽然事情很多,事情繁琐,但是如果按照性质来分类的话,我觉得可以分为两类:1.商务类,比如:产品会有红包活动,而且下周一就要上线了,所以研发的同学们就算周末加班不睡觉也只是叽叽喳喳的冲了出去。2.技术,比如:架构升级,系统优化等。这种东西对技术能力有一定的要求,通常需要有一定项目经验的同学做主。业务类的内容很大程度上取决于产品同学的节奏。研发更多是一个被动的角色。我们能做的就是多和产品聊天,“实时”了解最新的产品动向,培养自己的商业嗅觉,给自己预留一定的缓冲时间进行技术研究和技术储备。工作过一段时间的同学普遍都有这样的体会,产品变化的节奏很快,时常倒转,搞得研发苦不堪言。至于技术类,则比较温和,但也很考验研发的技术实力。今天我们就来说说界面性能优化的技巧。1.本地缓存。本地缓存的最大优点是应用程序和缓存在同一个进程中。请求缓存非常快,没有过多的网络开销。在不需要集群支持的单体应用或者集群的情况下,各个节点之间不需要互相通知,在某些场景下使用本地缓存是比较合适的。缺点也是因为缓存是和应用耦合在一起的。多个应用程序不能直接共享缓存。每个应用程序或集群的每个节点都需要维护自己独立的缓存,这是一种内存浪费。常用的本地缓存框架有Guava、Caffeine等,都是独立的jar包,可以直接导入到项目中使用。我们可以根据自己的需要灵活选择自己想要的框架。使用门槛较低,可自行上网搜索相应教程,此处不再展开。本地缓存适用于两种场景:缓存内容的时效性不高,可以接受一定的延迟,可以设置较短的过期时间,被动失效更新保持数据的新鲜度。缓存的内容不会改变。例如:订单号和uid的映射关系一旦创建就不会改变。注意问题:内存Cache数据项的上限控制,避免内存占用过多导致应用瘫痪。内存数据逐出策略。虽然实现很简单,但存在许多潜在的陷阱。最好选择一些成熟的开源框架。2.分布式缓存使用本地缓存可以很容易地为你的应用服务器带来“状态”,而且很容易受到内存大小的限制。分布式缓存采用分布式概念,集群部署,独立运维,容量无限。虽然会有网络传输丢失,但1~2ms的延迟与其更多的优势相比可以忽略不计。优秀的分布式缓存系统是著名的Memcached和Redis。与关系型数据库和缓存存储相比,读写性能上的差距可谓天壤之别。单个redis节点已经可以达到8W+的QPS。在方案设计时,尽量将读写压力从数据库转移到缓存上,有效保护脆弱的关系型数据库。注意问题:如果缓存的命中率太低,不能起到抗压的作用,压力还是会推到下游的存储层。缓存空间的大小要根据具体的业务场景进行评估,防止空间不足导致部分热点数据被替换。缓存数据一致性。缓存快速膨胀的问题。缓存接口的平均RT、最大RT、最小RT。缓存QPS。网络出口流量。客户端连接数。3、并行梳理业务流程,画出时序图,区分哪些是串行的?哪些是平行的?充分利用多核CPU的并行处理能力。如下图所示,如果存在上下文依赖则使用串行处理,否则使用并行处理。JDK的CompletableFuture提供了非常丰富的API,处理串行、并行、组合、错误处理的方法有50多种,可以满足我们的场景需求。之前写的文章:获取CompletableFuture,并发异步编程和串行编程有什么区别?4、异步接口的RT响应时间由内部业务逻辑的复杂程度决定。执行过程越简单,界面耗时就越少。因此,通常的做法是将接口内部的非核心逻辑剥离出来,异步执行。下图是一个电商订单创建界面。创建订单记录并将其插入数据库是我们的核心需求。至于后续的用户通知,比如给用户发送短信,如果失败了,也不影响主流程的完成。我们会将这些操作从主进程中分离出来。常见的业务实践是下单成功后向MQ服务器发送异步消息,消费者监听主题进行异步消费执行。发布/订阅模型也可以支持一些新的消费任务的快速接入。5.Pooling技术TCP的三次握手是非常消耗性能的,所以我们引入了Keep-Alive长连接来避免频繁的创建和销毁连接。池化技术也类似。它缓存了许多可重用的对象并将它们放在一个池中。在使用它们的时候,申请一个实例对象,用完后再放回池中。池化技术的核心是资源的“预分配”和“循环利用”。常见的池化技术有:线程池、内存池、数据库连接池、HttpClient连接池等。连接池的几个重要参数:最小连接数、空闲连接数、最大连接数。例如创建线程池:newThreadPoolExecutor(3,15,5,TimeUnit.MINUTES,newArrayBlockingQueue<>(10),newThreadFactoryBuilder().setNameFormat("data-thread-%d").build(),(r,执行者)->{(rinstanceofBaseRunnable){((BaseRunnable)r).rejectedExecute();}});6、分库分表MySQL底层innodb存储引擎采用B+树结构,三层结构支持千万级数据存储。当然,互联网现在拥有非常庞大的用户群。如此庞大的用户量,单张表通常难以支撑业务需求。将一张大表拆分成多个结构相同的物理表,可以大大缓解存储和访问的压力。分库分表也可能带来很多问题:分库分表后,数据会在分表中产生数据倾斜。如何创建一个全局唯一的主键id。数据如何路由到哪个分片。每个问题都需要很长时间来解释。这里主要说一下接口性能优化方案的总结,就不赘述了。分库分表,目前市面上流行的开源框架是sharding-jdbc,已经捐赠给Apache开始孵化。之前写的文章:为什么要分库分表?7、SQL优化虽然分库分表,可以从存储维度上减轻很多压力,但是“富贵不过三代”,我们还是要学会精打细算,比如,所有数据库操作都是通过SQL执行的。错误的SQL会对接口性能产生很大影响。比如:搞一个深度翻页,每次数据库引擎都要预检很多数据。索引缺失,全表扫描。一条SQL可以同时查询上万条数据。SQL优化方面有很多经验。比如在查询SQL的时候,尽量不要使用select*,而是选择特定的字段。如果只有一个查询结果(或最大值,最小值),建议使用limit1。索引不宜过多,一般控制在5个以内。尽量避免在where语句中使用或连接条件。或者可能使索引无效,从而进行全表扫描。尽量避免在有很多重复数据的字段上建立索引,比如性别。对where和orderby涉及的列建立索引,避免全表扫描。更多...关于SQL优化的内容很多,这里就不展开了。8、预计算很多业务的计算逻辑比较复杂,比如展示网站PV的页面,微信的幸运红包等,如果在用户访问界面的瞬间触发计算逻辑,这些逻辑计算时间通常较长,难以满足用户的实时性要求。一般我们都是提前计算,然后将计算好的数据预热到缓存中。访问接口时,我们只需要读取缓存即可。是不是一下子就快了很多?9、事务相关的业务逻辑有事务需求,多表写操作必须保证事务特性。但是,事务本身会消耗大量的性能。为了尽快结束,不至于长时间占用数据库连接资源,我们一般需要缩小事务的范围。将大量查询逻辑放在事务之外。另外,在事务内部,一般不进行远程RPC接口访问,一般需要很长时间。10、海量数据处理如果数据量太大,除了使用关系数据库的分库分表,我们还可以使用NoSQL。如:MongoDB、Hbase、Elasticsearch、TiDB。NoSQL采用分区架构,可以更好的支持海量数据存储,但是在事务方面可能就没那么友好了。每个NoSQL框架都有自己的特点,包括支持搜索、列式存储和文档存储的功能。您可以根据自己的业务场景选择合适的框架。11.批量读写目前的电脑CPU处理速度还是很高的,IO一般都是瓶颈,比如:磁盘IO,网络IO。有这样一个场景,查询100个人的账户余额?有两种设计方案:方案一:开票单查询接口,调用方调用内部循环100次。方案二:服务商开通批量查询接口,调用方只需查询一次即可。您认为哪个选项更好?答案不言而喻,一定是选项二。数据库写操作也是如此。为了提高性能,我们一般使用批量更新。12.锁的粒度。对于并发业务,为了防止数据的并发更新干扰数据的正确性,我们通常会使用锁,这涉及到一次只能由一个线程处理的独占资源。问题是锁是成对出现的,有锁就释放锁。对于非竞争资源,我们不需要锁在锁里面,这样会严重影响系统的并发能力。控制锁的范围是我们考虑的重点。之前写过一篇关于常用锁的文章,讲了13种锁的实现。13.上下文传递Tom哥和他的团队对小伙伴有要求。该代码必须具有代码审查链接。审查学生的代码经常会发现问题。当需要一条数据的时候,如果没有RPC接口去查,比如用户信息这样的通用接口。因为之前会用到,所以肯定是检查过的。但是我们知道,方法调用是以栈帧的形式传递的。当一个方法被执行出栈时,方法内部的局部变量也会被回收。以后如果要用到这些信息,只能再检查一遍。如果能定义一个Context对象,存储和传递一些中间信息,将大大减轻后续过程中重新查询的压力。14.空间大小如何创建一个集合并不简单,我们很快就会写出下面的代码。List
