简述
要想正式成为一位杰出的后端开发人员,科水狼高操控性的服务项目USB是两个关键分项,高质量开发人员都是对操控性反反复复榨取的。下列剖析了许多提高USB操控性的控制技术方案,期望对我们略有协助。
1、资料库索引
当USB再次出现操控性难题时,最难想不到的是加进索引,索引强化是付出最轻的强化,所以效用很显著。索引强化主要就从呵呵两个视角考量:
SQL与否加进索引? 索引与否施行? 索引结构设计与否科学合理?
1.1 SQL与否加进索引
在测试阶段要是考量资料库表的索引结构设计,对许多时常做为索引前提、order by、group by 前面的表头,且统计数据发展性高的表头,能考量建立索引。
测试阶段就把建立索引JAVA写好放到上架待继续执行JAVA文档中,防止上架时忘记索引建立。
能透过explain继续执行查阅方案,查阅SQL继续执行情形:
sql拷贝标识符explain select * from user where name like %张;也能透过指示show create table user,检查和整首表的索引情形。
sql拷贝标识符show create table user假如某一表忘掉加进某一索引,能透过alter table add index指示加进索引。
sql拷贝标识符alter table user add index idx_name (name);注意:在统计数据量很大的表中建立索引,最好选择在业务不繁忙时间段,防止影响线上业务。
1.2 索引不施行
有时候虽然加进了索引,但是索引可能会失效,如下图剖析了索引失效的14种情形:
(www.bilibili.com/video/BV1yN…
1.3 索引结构设计与否科学合理
索引不是结构设计越多越好,结构设计必需要科学合理,例如:
优先考量结构设计联合索引,适当使用覆盖索引; 索引个数尽量不要超过5个; 索引最好选择统计数据发展性较高的表头,如性别太多重复表头就不适合建立索引;
2、慢SQL强化
在索引强化之后,还能进一步强化慢SQL语句,如下剖析10条慢sql强化提议:
3、异步继续执行
对许多耗时操作或者不影响主要就业务的逻辑,能采用异步继续执行,来提高操控性。
开始
用户注册
短信发送
日志写入
积分赠送
结束
为了降低USB耗时,及时返回结果,能把短信发送、日志写入及积分赠送透过异步继续执行。
类似的场景还有:用户下订单之后的消息发送及赠送积分也能放到异步处理。
常见的异步实现:线程池、消息队列MQ、Spring注解@Async、异步框架CompletableFuture、Spring ApplicationEvent事件。
4、批量调用
资料库操作或或者远程调用时,能批量操作就不要for循环调用。
两个简单例子,我们平时两个列表明细统计数据插入资料库时,不要在for循环一条一条插入,提议两个批次几百条,进行批量插入,减少多次IO,提议使用Mybatis 的foreach操作,不过数量也不要一次太多(100),MP的saveBatch、或者PreparedStatement 的addBatch();同理远程调用类似,比如你查阅营销标签与否命中,能两个标签两个标签去查,也能批量标签去查,那批量进行,效率就更高。java拷贝标识符//反例 for(int i=0;i<n;i++){singleUpdate(param) } //正例 batchUpdate(param);打个比喻:
拷贝标识符假如你需要搬一万块砖到两个地方,你有一辆汽车,汽车一次能放适量的砖(最多放800), 你能选择一次运送一块砖,也能一次运送800,你觉得哪种方式更方便,时间消耗更少?5、统计数据预加载
统计数据预加载策略,顾名思义是提前把部分要用到的统计数据,初始化到缓存。假如你在未来某一时间需要用到某一经过复杂计算的统计数据,才实时去计算的话,可能耗时比较大。这时候,我们能采取预取思想,提前把可能需要的统计数据计算好,放到缓存中,等需要的时候,去缓存取就行。这将大幅度提高USB操控性。
场景举例:
例如地区统计数据或cSearch中,从Es中查阅,提供操控性。项目启动继续执行方法:
能透过实现ApplicationRunnerUSB中的run方法,实现启动时继续执行。方法继续执行时,项目已经初始化完毕,是能正常提供服务项目java拷贝标识符public class DataInitUtil implements ApplicationRunner{ @Override public void run(ApplicationArguments args) throws Exception { System.out.println(“在项目启动时,会继续执行这个方法中的标识符”); } } 使用注解@PostConstruct,需要在项目继续执行之前继续执行许多方法,就在目标方法上加进该注解, 存在难题:若继续执行方法耗时过长,会导致项目在方法继续执行期间无法提供服务项目。实现CommandLineRunnerUSB 然后在run方法里面调用需要调用的方法即可, 能透过java -jar demo.jar arg1传参;实现ApplicationListenerUSBtypescript拷贝标识符@Component public class ApplicationListenerImpl implements ApplicationListener<ApplicationStartedEvent> { @Override public void onApplicationEvent(ApplicationStartedEvent event) { System.out.println(“listener”); } }**总结:**注解方式@PostConstruct 始终最先继续执行
假如监听的是ApplicationStartedEvent 事件,则一定会在CommandLineRunner和ApplicationRunner 之前继续执行;假如监听的是ApplicationReadyEvent 事件,则一定会在CommandLineRunner和ApplicationRunner 之后继续执行;CommandLineRunner和ApplicationRunner 默认是ApplicationRunner先继续执行,假如双方指定了@Order 则按照@Order的大小顺序继续执行,小的先继续执行。6、使用缓存控制技术
在适当的业务场景,恰当地使用缓存,是能大大提高USB操控性的。缓存其实是一种空间换时间的思想,是你把要查的统计数据,提前放好到缓存里面,需要时,直接查缓存,而防止去查资料库或者计算的过程。
这里的缓存包括:Redis缓存,JVM本地缓存,memcached,或者Map等等。我举个我工作中,一次使用缓存强化的结构设计吧,比较简单,但是思路很有借鉴的意义。 场景举例:
开始
根据用户账号
查阅基本信息
进行首页展示
结束
这里用户的基本信息包含积分、期望工作地等很多信息,首页需要根据这些信息,个性化展示统计数据;假如每次都去查阅,比较费时,能考量将统计数据缓存到redis,提高操控性。
7、池化控制技术
池化控制技术最常见的是线程池应用:
假如你每次需要用到线程,都去建立,就会有增加一定的耗时;线程池能重复利用线程,防止不必要的耗时;池化控制技术不仅仅指线程池,很多场景都有池化思想的体现,它的本质是预分配与循环使用。8、事件回调
假如你调用两个系统B的USB,但是它处理业务逻辑,耗时需要10s甚至更多。然后你是一直阻塞等待,直到系统B的下游USB返回,再继续你的下一步操作吗?这样显然不科学合理。我们能采用事件回调机制,即我们不用阻塞等待系统B的USB,而是先去做别的操作。等系统B的USB处理完,透过事件回调通知,我们USB收到通知再进行对应的业务操作即可。如IO多路复用模型实现。9、串行改为并行调用
假设我们结构设计两个APP首页的USB,它需要查用户获奖经历、需要查资格证书、需要查岗位信息等等。那你是两个两个USB串行调,还是并行调用呢?
能使用CompletableFuture 并行调用提高操控性,类似也能使用多线程处理。
perl拷贝标识符// 查阅获奖经历 LambdaQueryWrapper<RewardExp> rewardExpQuery = new LambdaQueryWrapper<RewardExp>() .eq(RewardExp::getResumeId, resume.getId()) .eq(RewardExp::getDelFlag, NORMAL).orderByDesc(RewardExp::getDate); CompletableFuture<List<RewardExp>> rewardExpFuture = CompletableFuture.supplyAsync(() -> rewardExpMapper.selectList(rewardExpQuery) );//查阅资格证书 LambdaQueryWrapper<Credential> credentialQuery = new LambdaQueryWrapper<Credential>() .eq(Credential::getResumeId, resume.getId()) .eq(Credential::getDelFlag, NORMAL).orderByDesc(Credential::getDate); CompletableFuture<List<Credential>> credentialFuture = CompletableFuture.supplyAsync(() -> credentialMapper.selectList(credentialQuery) );//查阅岗位信息 LambdaQueryWrapper<ResumeJobs> jobsQuery = new LambdaQueryWrapper<ResumeJobs>() .eq(ResumeJobs::getResumeId, resume.getId()).eq(ResumeJobs::getDelFlag, NORMAL); CompletableFuture<List<ResumeJobs>> jobsFuture = CompletableFuture.supplyAsync(() -> resumeJobsMapper.selectList(jobsQuery) ); CompletableFuture.allOf(rewardExpFuture, credentialFuture, jobsFuture).join();10、锁的粒度和范围控制
在高并发场景,为了防止超卖等情形,我们时常需要加锁来保护共享资源。但是,假如加锁的粒度过粗,是很影响USB操控性的。 什么是加锁粒度呢?举两个生活中的示例: 例如:你在家里上卫生间,你只需要把卫生间锁住,而不用把家里的每个房间都锁住。 无论是使用synchronized加锁还是redis分布式锁,只需要在共享临界资源加锁即可,不涉及共享资源的,就不必要加锁。 如下示例标识符,只是方法A涉及共享资源操作,就只需要锁住A方法就好; 很多小伙伴难一锅端,全锁住,假如锁住的标识符耗时,其实是比较影响操控性的。 错误:
scss拷贝标识符void wrongMethod(){ synchronized (this) { doMethodB(); A(); } }正确:
scss拷贝标识符void rightMethod(){ doMethodB();synchronized (this) { A(); } }11、部分统计数据暂存文档中
假如USB耗时瓶颈就在资料库插入操作这里,用批量操作等策略,效用还不理想,就能考量用文档或者消息队列、redis等暂存。有时候批量统计数据放到文档,会比插入资料库效率更高。 该策略的主要就思想:是在大统计数据量时,将业务统计数据写入文档中,再透过异步的方式去消费文档中的统计数据,执行对应的业务逻辑,减少资料库DB的瞬时压力。
场景举例:
开始
调用三方USB
统计数据汇总
统计数据存储
结束
由于季度考勤统计数据量很大,逐个接收处理汇总很费时,能采用将统计数据按15天分组写入到两个文档中,然后异步去消费文档中统计数据进行汇总处理。
12、防止长事务难题
长事务在DB服务项目端的表现是session持续时间长;期间可能伴随cpu、内存升高,严重者可导致DB服务项目端整体响应缓慢,导致在线应用无法使用;所以在线高并发业务中应该尽量防止长事务的发生。产生长事务的原因,除了sql本身可能存在难题外,和应用层的事务控制逻辑也有很大的关系。如何防止长事务难题:
RPC远程调用不要放到事务里面;许多查阅相关的操作,尽量放到事务之外;事务中防止处理太多统计数据;并发场景下,尽量防止使用@Transactional注解声明式事务粒度太大,使用TransactionTemplate的编程式事务灵活控制事务的范围。如何解决长事务难题:
增加对长事务的监控,记录长事务的logId,根据logId能查阅到整个请求调用链日志,能明确是哪个服务项目的哪个USB的哪个方法产生的;根据日志检查和与否存在慢SQL;检查和对应服务项目与否存在RPC远程调用包裹在事务中;检查和与否USB中使用了@Transactional注解声明式事务。 程序示例:scss拷贝标识符@Transactional public int createUser(User user){ //保存用户信息 userDao.save(user); passCertDao.updateFlag(user.getPassId()); // 该方法为远程RPCUSBsendEmailRpc(user.getEmail());return user.getUserId(); }直接使用@Transactional 注解,Spring的声明式事务,整个方法都在事务中,所以里面存在远程RPC调用,难再次出现长事务难题。
13、深度分页难题
资料库的深度分页难题,比较影响USB操控性,如下所示SQL语句:
bash拷贝标识符select id,name,balance from account where create_time> 2020-09-19 limit 100000,10;limit 100000,10意味着会扫描100010行,丢弃掉前100000行,最后返回10行。即使create_time,也会回表很多次。 能透过标签记录法和延迟关联法来强化深分页难题。1. 标签记录法是标记呵呵上次查阅到哪一条了,下次再来查的时候,从该条开始往下扫描。就好像看书一样,上次看到哪里了,你就折叠呵呵或者夹个书签,下次来看的时候,直接就翻到啦。
bash拷贝标识符select id,name,balance FROM account where id > 100000 limit 10;这样的话,前面无论翻多少页,操控性都会不错的,因为命中了id主键索引。但是这种方式有局限性:需要一种类似连续自增的表头,所以需要前端把上次最大值传给后端。
2. 延迟关联法延迟关联法,是把前提转移到主键索引树,然后减少回表。强化后的SQL如下:
sql拷贝标识符select acct1.id,acct1.name,acct1.balance FROM account acct1 INNER JOIN (SELECT a.id FROM account a WHERE a.create_time > 2020-09-19 limit 100000, 10) AS acct2 on acct1.id= acct2.id;强化思路是,先透过idx_create_time二级索引树查阅到满足前提的主键ID,再与原表透过主键ID内连接,这样前面直接走了主键索引了,同时也减少了回表。
14、程序逻辑强化
强化程序逻辑、程序标识符,是能节省耗时的。比如,你的程序建立多不必要的对象、或者程序逻辑混乱,多次重复查资料库、又或者你的实现逻辑算法不是最高效的等等。 常见思路:透过使用visiol建模工具或者其他方法,剖析清楚标识符逻辑,检查和与否存在不必要的对象建立、逻辑调用或者标识符细节之类的,与否符合许多编码规范。
15、压缩传输内容
因为文档太大传入会比较消耗网络带宽资源,进一步影响到服务项目USB的耗时消耗。例如后端需要返回图片到前端加载,该强化策略能找UI在不影响图片清晰度的前提下,对图片进行适当压缩,节省网络带宽,提高USB操控性。
16、考量分库分表及Nosql
假如确实是因为统计数据量太大导致的操控性难题,能考量使用分库分表策略,包括垂直拆分和水平拆分,在程序查阅sql中操控性得到提高。 同时也能考量ElasticSearch,对大统计数据查阅都有很好的操控性支持。 场景举例: 之前有个业务需求,需要查阅用户报表统计数据,报表统计数据包括用户维度的很多属性信息,用户表统计数据量很大,查阅时需要join很多表,假如用关系型资料库存在严重操控性难题,如下图所示
开始
查阅用户表
查阅积分表
查阅订单
查阅发票
查阅实名信息等
结束
**解决方案:**透过XxlJob定时任务,在晚上业务不繁忙时,将用户报表统计数据离线生成好,并存放到ElasticSearch中,界面上展示时后端直接透过Es查阅,提高了查阅USB操控性。
17、线程池结构设计
我们使用线程池,是让任务并行处理,更高效地完成任务。但是有时候,假如线程池结构设计不科学合理,USB执行效率则不太理想。
假如核心线程过小,则达不到很好的并行效用;假如阻塞队列不科学合理,不仅仅是阻塞的难题,甚至可能会OOM;假如线程池不区分业务隔离,有可能核心业务被边缘业务拖垮,如下图:假如不做线程池隔离,在普通USB出难题时,就会影响核心业务USB,再次出现生产事故。普通USB
消息推送
写入日志
核心USB
首页加载
登录USB
线程池隔离
18、其他难题
在抽奖活动或者投票活动等高并发场景下,瞬间再次出现很大流量的场景,会导致线程打满或者DB压力,因此对高并发场景下,USB一定要接入限流设置,如Guava限流等;有时候操作文档流或者其他资源,用完之后记得关闭,假如忘掉关闭也会导致资源占用过高,影响操控性。总结
解决服务项目USB操控性难题,是开发人员进阶的必经之路。 总的来说操控性强化通用方法是:从用户发起请求的整个链路分析,将分隔相关环节加上log日志,打印环节耗时,找到USB操控性难题再次出现位置,再结合以上介绍的强化方案进行处理。
作者:美丽的程序人生
链接:
https://juejin.cn/post/7239908637094821949