> 提醒:在发布作品前,请把不需要的内容删掉。
方向一:编程语言介绍
编程 是个动词,编程==写代码,写代码为了什么? 为了让计算机干你想要干的事情,比如,马化腾想跟别人聊天,于是写了个聊天软件,这个软件就是一堆代码的集合,这些代码是什么?这些代码是计算机能理解的语言。
那计算能理解的语言是什么呢? 之前,我们已经了解到,它只能理解2进制,0101010...,你总不能人肉输一堆二进制给计算机(虽然最原始的计算机就是这么干的)让它工作吧,这样开发速度太慢了。所以最好的办法就是人输入简单的指令,计算机能把指令转成二进制进行执行,举例如下:
假如 程序员想让计算机 播放一首 歌曲 , 只需要输入指令 ,
open "老男孩.mp3" play
计算机的CPU接收到这样的指令后,会把它转成一堆 只有cpu可以理解的指令,然后再将指令变成各种对应的如下类似二进制
[ op | rs | rt | address/immediate] 35 3 8 68 decimal 100011 00011 01000 00000 00001 000100 binary
最终cpu 去调用你的硬盘上这首歌,通过音箱播放。
上面cpu那段指令太难理解了,如果让你天天写这样的代码,大家非得自杀不可。还好,伟大的计算机先驱们,开发了各种编程语言,让我们只需要通过写一些简单的规则,就能操作计算机工作啦。
编程语言总体分以为机器语言、汇编语言、高级语言,如下
机器语言
由于计算机内部只能接受二进制代码,因此,用二进制代码0和1描述的指令称为机器指令,全部机器指令的集合构成计算机的机器语言,用机器语言编程的程序称为目标程序。只有目标程序才能被计算机直接识别和执行。但是机器语言编写的程序无明显特征,难以记忆,不便阅读和书写,且依赖于具体机种,局限性很大,机器语言属于低级语言。
用机器语言编写程序,编程人员要首先熟记所用计算机的全部指令代码和代码的涵义。手编程序时,程序员得自己处理每条指令和每一数据的存储分配和输入输出,还得记住编程过程中每步所使用的工作单元处在何种状态。这是一件十分繁琐的工作。编写程序花费的时间往往是实际运行时间的几十倍或几百倍。而且,编出的程序全是些0和1的指令代码,直观性差,还容易出错。除了计算机生产厂家的专业人员外,绝大多数的程序员已经不再去学习机器语言了。
机器语言是微处理器理解和使用的,用于控制它的操作二进制代码。
尽管机器语言好像是很复杂的,然而它是有规律的。
存在着多至100000种机器语言的指令。这意味着不能把这些种类全部列出来。
以下是一些示例:
指令部份的示例
0000 代表 加载(LOAD)
0001 代表 存储(STORE)
...
暂存器部份的示例
0000 代表暂存器 A
0001 代表暂存器 B
...
存储器部份的示例
000000000000 代表地址为 0 的存储器
000000000001 代表地址为 1 的存储器
000000010000 代表地址为 16 的存储器
100000000000 代表地址为 2^11 的存储器
集成示例
0000,0000,000000010000 代表 LOAD A, 16
0000,0001,000000000001 代表 LOAD B, 1
0001,0001,000000010000 代表 STORE B, 16
0001,0001,000000000001 代表 STORE B, 1[1]
汇编语言
汇编语言的实质和机器语言是相同的,都是直接对硬件操作,只不过指令采用了英文缩写的标识符,更容易识别和记忆。它同样需要编程者将每一步具体的操作用命令的形式写出来。汇编程序的每一句指令只能对应实际操作过程中的一个很细微的动作。例如移动、自增,因此汇编源程序一般比较冗长、复杂、容易出错,而且使用汇编语言编程需要有更多的计算机专业知识,但汇编语言的优点也是显而易见的,用汇编语言所能完成的操作不是一般高级语言所能够实现的,而且源程序经汇编生成的可执行文件不仅比较小,而且执行速度很快。
汇编的hello world,打印一句hello world, 需要写十多行,也是醉了。
; hello.asm section .data ; 数据段声明 msg db "Hello, world!", 0xA ; 要输出的字符串 len equ $ - msg ; 字串长度 section .text ; 代码段声明 global _start ; 指定入口函数 _start: ; 在屏幕上显示一个字符串 mov edx, len ; 参数三:字符串长度 mov ecx, msg ; 参数二:要显示的字符串 mov ebx, 1 ; 参数一:文件描述符(stdout) mov eax, 4 ; 系统调用号(sys_write) int 0x80 ; 调用内核功能 ; 退出程序 mov ebx, 0 ; 参数一:退出代码 mov eax, 1 ; 系统调用号(sys_exit) int 0x80 ; 调用内核功能
高级语言
高级语言是大多数编程者的选择。和汇编语言相比,它不但将许多相关的机器指令合成为单条指令,并且去掉了与具体操作有关但与完成工作无关的细节,例如使用堆栈、寄存器等,这样就大大简化了程序中的指令。同时,由于省略了很多细节,编程者也就不需要有太多的专业知识。
高级语言主要是相对于汇编语言而言,它并不是特指某一种具体的语言,而是包括了很多编程语言,像最简单的编程语言PASCAL语言也属于高级语言。
高级语言所编制的程序不能直接被计算机识别,必须经过转换才能被执行,按转换方式可将它们分为两类:
编译类:编译是指在应用源程序执行之前,就将程序源代码“翻译”成目标代码(机器语言),因此其目标程序可以脱离其语言环境独立执行(编译后生成的可执行文件,是cpu可以理解的2进制的机器码组成的),使用比较方便、效率较高。但应用程序一旦需要修改,必须先修改源代码,再重新编译生成新的目标文件(* .obj,也就是OBJ文件)才能执行,只有目标文件而没有源代码,修改很不方便。
编译后程序运行时不需要重新翻译,直接使用编译的结果就行了。程序执行效率高,依赖编译器,跨平台性差些。如C、C++、Delphi等
解释类:执行方式类似于我们日常生活中的“同声翻译”,应用程序源代码一边由相应语言的解释器“翻译”成目标代码(机器语言),一边执行,因此效率比较低,而且不能生成可独立执行的可执行文件,应用程序不能脱离其解释器(想运行,必须先装上解释器,就像跟老外说话,必须有翻译在场),但这种方式比较灵活,可以动态地调整、修改应用程序。如Python、Java、PHP、Ruby等语言。
机器语言
优点是最底层,速度最快,缺点是最复杂,开发效率最低
汇编语言
优点是比较底层,速度最快,缺点是复杂,开发效率最低
高级语言
编译型语言执行速度快,不依赖语言环境运行,跨平台差
解释型跨平台好,一份代码,到处使用,缺点是执行速度慢,依赖解释器运行
世界上的编程语言有600多种,但真正大家主流在使用的最多二三十种,不同的语言有自己的特点和擅长领域,随着计算机的不断发展,新语言在不断诞生,也同时有很多老旧的语言慢慢无人用了。有个权威的语言排名网站,可以看到主流的编程语言是哪些
*2019年2月数据(https://www.tiobe.com/tiobe-index/?)
下面介绍下几个主流的编程语言:
C语言:
C语言是一种计算机程序设计语言,它既具有高级语言的特点,又具有汇编语言的特点。它由美国贝尔研究所的D.M.Ritchie于1972年推出,1978年后,C语言已先后被移植到大、中、小及微型机上,它可以作为工作系统设计语言,编写系统应用程序,也可以作为应用程序设计语言,编写不依赖计算机硬件的应用程序。它的应用范围广泛,具备很强的数据处理能力,不仅仅是在软件开发上,而且各类科研都需要用到C语言,适于编写系统软件,三维,二维图形和动画,具体应用比如单片机以及嵌入式系统开发。
C++:
C++是C语言的继承的扩展,它既可以进行C语言的过程化程序设计,又可以进行以抽象数据类型为特点的基于对象的程序设计,还可以进行以继承和多态为特点的面向对象的程序设计。C++擅长面向对象程序设计的同时,还可以进行基于过程的程序设计,因而C++就适应的问题规模而论,大小由之。
C++不仅拥有计算机高效运行的实用性特征,同时还致力于提高大规模程序的编程质量与程序设计语言的问题描述能力。
JAVA:
Java是一种可以撰写跨平台应用软件的面向对象的程序设计语言,是由Sun Microsystems公司于1995年5月推出的Java程序设计语言和Java平台(即JavaSE, JavaEE, JavaME)的总称。Java 技术具有卓越的通用性、高效性、平台移植性和安全性,广泛应用于个人PC、数据中心、游戏控制台、科学超级计算机、移动电话和互联网,同时拥有全球最大的开发者专业社群。在全球云计算和移动互联网的产业环境下,Java更具备了显著优势和广阔前景。
PHP:
PHP(外文名:PHP: Hypertext Preprocessor,中文名:“超文本预处理器”)是一种通用开源脚本语言。语法吸收了C语言、Java和Perl的特点,利于学习,使用广泛,主要适用于Web开发领域
Ruby:
Ruby 是开源的,在Web 上免费提供,但需要一个许可证。[4]
Ruby 是一种通用的、解释的编程语言。
Ruby 是一种真正的面向对象编程语言。
Ruby 是一种类似于 Python 和 Perl 的服务器端脚本语言。
Ruby 可以用来编写通用网关接口(CGI)脚本。
Ruby 可以被嵌入到超文本标记语言(HTML)。
Ruby 语法简单,这使得新的开发人员能够快速轻松地学习 Ruby
GO:
Go 是一个开源的编程语言,它能让构造简单、可靠且高效的软件变得容易。
Go是从2007年末由Robert Griesemer, Rob Pike, Ken Thompson主持开发,后来还加入了Ian Lance Taylor, Russ Cox等人,并最终于2009年11月开源,在2012年早些时候发布了Go 1稳定版本。现在Go的开发已经是完全开放的,并且拥有一个活跃的社区。
由其擅长并发编程
Python:
Python是一门优秀的综合语言, Python的宗旨是简明、优雅、强大,在人工智能、云计算、金融分析、大数据开发、WEB开发、自动化运维、测试等方向应用广泛,已是全球第4大最流行的语言。
方向二:代码技巧分享
1. 使用注释:在代码中使用注释可以帮助其他开发人员理解你的意图和实现细节。尽量写清晰、简洁的注释,并避免在代码中出现冗长的注释。
2. 命名规范:给变量、函数和类起一个有意义的名字,能够更好地表达其功能和作用。遵循一致的命名规范可以提高代码的可读性。
3. DRY原则:遵循"Don’t Repeat Yourself"原则,避免重复的代码。如果发现有重复的代码,可以将其封装成一个函数或类,然后在需要的地方调用。
4. 异常处理:合理处理和抛出异常可以增加代码的健壮性和可靠性。在可能发生异常的地方,使用try-catch语句块捕获异常并进行适当的处理。
5. 单一职责原则:每个函数、类或模块应该只负责一个特定的功能,而不是做太多的事情。这样可以提高代码的可维护性和可测试性。
6. 代码重构:定期回顾和重构代码可以提高代码的可读性和性能。尽量保持代码的简洁和高效,并去除不必要的冗余代码。
7. 使用版本控制:使用版本控制系统(如Git)来管理代码可以方便地跟踪和回滚修改。同时,推荐使用分支来管理不同的功能和实验。
8. 代码复用:尽量避免重复编写相同的代码,可以通过创建函数库、使用第三方库和模块等方式来实现代码的复用。
9. 单元测试:编写单元测试可以帮助你验证代码的正确性和可靠性。使用测试框架来编写自动化测试,并确保测试覆盖率足够高。
10. 代码风格检查:使用代码风格检查工具(如ESLint、Pylint等)可以帮助你保持统一的代码风格,并发现潜在的问题。
方向三:案例分享
1、规范命名
命名是写代码中最频繁的操作,比如类、属性、方法、参数等。好的名字应当能遵循以下几点:
见名知意
比如需要定义一个变量需要来计数
int?i?=?0;
名称 i 没有任何的实际意义,没有体现出数量的意思,所以我们应当指明数量的名称
int?count?=?0;
能够读的出来
2、规范代码格式
好的代码格式能够让人感觉看起来代码更加舒适。
好的代码格式应当遵守以下几点:
- 合适的空格
- 代码对齐,比如大括号要对齐
- 及时换行,一行不要写太多代码
好在现在开发工具支持一键格式化,可以帮助美化代码格式。
3、写好代码注释
在《代码整洁之道》这本书中作者提到了一个观点,注释的恰当用法是用来弥补我们在用代码表达意图时的失败。换句话说,当无法通过读代码来了解代码所表达的意思的时候,就需要用注释来说明。
作者之所以这么说,是因为作者觉得随着时间的推移,代码可能会变动,如果不及时更新注释,那么注释就容易产生误导,偏离代码的实际意义。而不及时更新注释的原因是,程序员不喜欢写注释。
好的注释应当满足一下几点:
- 解释代码的意图,说明为什么这么写,用来做什么
- 对参数和返回值注释,入参代表什么,出参代表什么
- 有警示作用,比如说入参不能为空,或者代码是不是有坑
- 当代码还未完成时可以使用 todo 注释来注释
4、try catch 内部代码抽成一个方法
try catch代码有时会干扰我们阅读核心的代码逻辑,这时就可以把try catch内部主逻辑抽离成一个单独的方法
如下图是Eureka服务端源码中服务下线的实现中的一段代码
整个方法非常长,try中代码是真正的服务下线的代码实现,finally可以保证读锁最终一定可以释放。
5、方法别太长
方法别太长就是字面的意思。一旦代码太长,给人的第一眼感觉就很复杂,让人不想读下去;同时方法太长的代码可能读起来容易让人摸不着头脑,不知道哪一些代码是同一个业务的功能。
我曾经就遇到过一个方法写了2000+行,各种if else判断,我光理清代码思路就用了很久,最终理清之后,就用策略模式给重构了。
所以一旦方法过长,可以尝试将相同业务功能的代码单独抽取一个方法,最后在主方法中调用即可。
6、抽取重复代码
当一份代码重复出现在程序的多处地方,就会造成程序又臭又长,当这份代码的结构要修改时,每一处出现这份代码的地方都得修改,导致程序的扩展性很差。
所以一般遇到这种情况,可以抽取成一个工具类,还可以抽成一个公共的父类。
7、类和方法单一职责
单一职责原则是设计模式的七大设计原则之一,它的核心意思就是字面的意思,一个类或者一个方法只做单一的功能。
就拿Nacos来说,在Nacos1.x的版本中,有这么一个接口HttpAgent
这个类只干了一件事,那就是封装http请求参数,向Nacos服务端发送请求,接收响应,这其实就是单一职责原则的体现。
当其它的地方需要向Nacos服务端发送请求时,只需要通过这个接口的实现,传入参数就可以发送请求了,而不需要关心如何携带服务端鉴权参数、http请求参数如何组装等问题。
8、尽量使用聚合/组合代替继承
继承的弊端:
- 灵活性低。java语言是单继承的,无法同时继承很多类,并且继承容易导致代码层次太深,不易于维护
- 耦合性高。一旦父类的代码修改,可能会影响到子类的行为
所以一般推荐使用聚合/组合代替继承。
聚合/组合的意思就是通过成员变量的方式来使用类。
比如说,OrderService需要使用UserService,可以注入一个UserService而非通过继承UserService。
聚合和组合的区别就是,组合是当对象一创建的时候,就直接给属性赋值,而聚合的方式可以通过set方式来设置。
10、使用设计模式优化代码
在平时开发中,使用设计模式可以增加代码的扩展性。
比如说,当你需要做一个可以根据不同的平台做不同消息推送的功能时,就可以使用策略模式的方式来优化。
短信通知实现:
@Component public?class?SMSMessageNotifier?implements?MessageNotifier?{ ????@Override ????public?boolean?support(int?type)?{ ????????return?type?==?0; ????} ????@Override ????public?void?notify(User?user,?String?content)?{ ????????//调用短信通知的api发送短信 ????} }
app通知实现:
public?class?AppMessageNotifier?implements?MessageNotifier?{ ??? ?@Override ??? ?public?boolean?support(int?type)?{ ???????? return?type?==?1; ????} ??? ?@Override ??? ?public?void?notify(User?user,?String?content) ?{ ???????//调用通知app通知的api ????} }
最后提供一个方法,当需要进行消息通知时,调用notifyMessage,传入相应的参数就行。
@Resource private?List<MessageNotifier>?messageNotifiers; public?void?notifyMessage(User?user,?String?content,?int?notifyType)?{ ????for?(MessageNotifier?messageNotifier?:?messageNotifiers)?{ ????????if?(messageNotifier.support(notifyType))?{ ????????????messageNotifier.notify(user,?content); ????????} ????} }
假设此时需要支持通过邮件通知,只需要有对应实现就行。
11、不滥用设计模式
用好设计模式可以增加代码的扩展性,但是滥用设计模式确是不可取的。
public?void?printPerson(Person?person)?{ ???? StringBuilder?sb?=?new?StringBuilder(); ????if?(StringUtils.isNotBlank(person.getName()))?{ ??????? ?sb.append("姓名:").append(person.getName()); ????} ????if?(StringUtils.isNotBlank(person.getIdCardNo()))?{ ???????? sb.append("身份证号:").append(person.getIdCardNo()); ????} ????//?省略 ????System.out.println(sb.toString()); }
比如上面打印Person信息的代码,用if判断就能够做到效果,你说我要不用责任链或者什么设计模式来优化一下吧,没必要。
12、面向接口编程
在一些可替换的场景中,应该引用父类或者抽象,而非实现。
举个例子,在实际项目中可能需要对一些图片进行存储,但是存储的方式很多,比如可以选择阿里云的OSS,又或者是七牛云,存储服务器等等。所以对于存储图片这个功能来说,这些具体的实现是可以相互替换的。
所以在项目中,我们不应当在代码中耦合一个具体的实现,而是可以提供一个存储接口
public?interface?FileStorage?{ ???? ??? ?String?store(String?ileName,?byte[]?bytes); }
如果选择了阿里云OSS作为存储服务器,那么就可以基于OSS实现一个FileStorage,在项目中哪里需要存储的时候,只要实现注入这个接口就可以了。
@Autowired private?FileStorage?fileStorage;
假设用了一段时间之后,发现阿里云的OSS比较贵,此时想换成七牛云的,那么此时只需要基于七牛云的接口实现FileStorage接口,然后注入到IOC,那么原有代码用到FileStorage根本不需要动,实现轻松的替换。
13、经常重构旧的代码
随着时间的推移,业务的增长,有的代码可能不再适用,或者有了更好的设计方式,那么可以及时的重构业务代码。
就拿上面的消息通知为例,在业务刚开始的时候可能只支持短信通知,于是在代码中就直接耦合了短信通知的代码。但是随着业务的增长,逐渐需要支持app、邮件之类的通知,那么此时就可以重构以前的代码,抽出一个策略接口,进行代码优化。
14、null值判断
空指针是代码开发中的一个难题,作为程序员的基本修改,应该要防止空指针。
可能产生空指针的原因:
- 数据返回对象为null
- 自动拆箱导致空指针
- rpc调用返回的对象可能为空格
所以在需要这些的时候,需要强制判断是否为null。前面也提到可以使用Optional来优雅地进行null值判断。
15、pojo类重写toString方法
pojo一般内部都有很多属性,重写toString方法可以方便在打印或者测试的时候查看内部的属性。
16、魔法值用常量表示
public?void?sayHello(String?province)?{ ????if?("广东省".equals(province))?{ ????????System.out.println("靓仔~~"); ????}?else?{ ????????System.out.println("帅哥~~"); ????} }
代码里,广东省就是一个魔法值,那么就可以将用一个常量来保存
private?static?final?String?GUANG_DONG_PROVINCE?=?"广东省"; public?vo?sayHello(String?province)?{ ????if?(GUANG_DONG_PROVINCE.equals(province))?{ ????????System.out.println("靓仔~~"); ????}?else?{ ????????System.out.println("帅哥~~"); ????} }id
17、资源释放写到finally
比如在使用一个api类锁或者进行IO操作的时候,需要主动写代码需释放资源,为了能够保证资源能够被真正释放,那么就需要在finally中写代码保证资源释放。
如图所示,就是CopyOnWriteArrayList的add方法的实现,最终是在finally中进行锁的释放。
18、使用线程池代替手动创建线程
使用线程池还有以下好处:
- 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
- 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
- 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统 的稳定性,使用线程池可以进行统一的分配,调优和监控。
所以为了达到更好的利用资源,提高响应速度,就可以使用线程池的方式来代替手动创建线程。
19、线程设置名称
在日志打印的时候,日志是可以把线程的名字给打印出来。
如上图,日志打印出来的就是tom猫的线程。
所以,设置线程的名称可以帮助我们更好的知道代码是通过哪个线程执行的,更容易排查问题。
20、涉及线程间可见性加volatile
在RocketMQ源码中有这么一段代码
在消费者在从服务端拉取消息的时候,会单独开一个线程,执行while循环,只要stopped状态一直为false,那么就会一直循环下去,线程就一直会运行下去,拉取消息。
当消费者客户端关闭的时候,就会将stopped状态设置为true,告诉拉取消息的线程需要停止了。但是由于并发编程中存在可见性的问题,所以虽然客户端关闭线程将stopped状态设置为true,但是拉取消息的线程可能看不见,不能及时感知到数据的修改,还是认为stopped状态设置为false,那么就还会运行下去。
针对这种可见性的问题,java提供了一个volatile关键字来保证线程间的可见性。
所以,源码中就加了volatile关键字。
加了volatile关键字之后,一旦客户端的线程将stopped状态设置为true时候,拉取消息的线程就能立马知道stopped已经是false了,那么再次执行while条件判断的时候,就不成立,线程就运行结束了,然后退出。
21、考虑线程安全问题
在平时开发中,有时需要考虑并发安全的问题。
举个例子来说,一般在调用第三方接口的时候,可能会有一个鉴权的机制,一般会携带一个请求头token参数过去,而token也是调用第三方接口返回的,一般这种token都会有个过期时间,比如24小时。
我们一般会将token缓存到Redis中,设置一个过期时间。向第三方发送请求时,会直接从缓存中查找,但是当从Redis中获取不到token的时候,我们都会重新请求token接口,获取token,然后再设置到缓存中。
整个过程看起来是没什么问题,但是实则隐藏线程安全问题。
假设当出现并发的时候,同时来两个线程AB从缓存查找,发现没有,那么AB此时就会同时调用token获取接口。假设A先获取到token,B后获取到token,但是由于CPU调度问题,线程B虽然后获取到token,但是先往Redis存数据,而线程A后存,覆盖了B请求的token。
这下就会出现大问题,最新的token被覆盖了,那么之后一定时间内token都是无效的,接口就请求不通。
针对这种问题,可以使用double check机制来优化获取token的问题。
所以,在实际中,需要多考虑考虑业务是否有线程安全问题,有集合读写安全问题,那么就用线程安全的集合,业务有安全的问题,那么就可以通过加锁的手段来解决。
22、慎用异步
虽然在使用多线程可以帮助我们提高接口的响应速度,但是也会带来很多问题。
事务问题
一旦使用了异步,就会导致两个线程不是同一个事务的,导致异常之后无法正常回滚数据。
cpu负载过高
之前有个小伙伴遇到需要同时处理几万调数据的需求,每条数据都需要调用很多次接口,为了达到老板期望的时间要求,使用了多线程跑,开了很多线程,此时会发现系统的cpu会飙升
意想不到的异常
还是上面的提到的例子,在测试的时候就发现,由于并发量激增,在请求第三方接口的时候,返回了很多错误信息,导致有的数据没有处理成功。
虽然说慎用异步,但不代表不用,如果可以保证事务的问题,或是CPU负载不会高的话,那么还是可以使用的。
23、减小锁的范围
减小锁的范围就是给需要加锁的代码加锁,不需要加锁的代码不要加锁。这样就能减少加锁的时间,从而可以较少锁互斥的时间,提高效率。
比如CopyOnWriteArrayList的addAll方法的实现,lock.lock(); 代码完全可以放到代码的第一行,但是作者并没有,因为前面判断的代码不会有线程安全的问题,不放到加锁代码中可以减少锁抢占和占有的时间。
24、有类型区分时定义好枚举
比如在项目中不同的类型的业务可能需要上传各种各样的附件,此时就可以定义好不同的一个附件的枚举,来区分不同业务的附件。
不要在代码中直接写死,不定义枚举,代码阅读起来非常困难,直接看到数字都是懵逼的。。
25、远程接口调用设置超时时间
比如在进行微服务之间进行rpc调用的时候,又或者在调用第三方提供的接口的时候,需要设置超时时间,防止因为各种原因,导致线程”卡死“在那。
我以前就遇到过线上就遇到过这种问题。当时的业务是订阅kafka的消息,然后向第三方上传数据。在某个周末,突然就接到电话,说数据无法上传了,通过排查线上的服务器才发现所有的线程都线程”卡死“了,最后定位到代码才发现原来是没有设置超时时间。
26、集合使用应当指明初始化大小
比如在写代码的时候,经常会用到List、Map来临时存储数据,其中最常用的就是ArrayList和HashMap。但是用不好可能也会导致性能的问题。
比如说,在ArrayList中,底层是基于数组来存储的,数组是一旦确定大小是无法再改变容量的。但不断的往ArrayList中存储数据的时候,总有那么一刻会导致数组的容量满了,无法再存储其它元素,此时就需要对数组扩容。所谓的扩容就是新创建一个容量是原来1.5倍的数组,将原有的数据给拷贝到新的数组上,然后用新的数组替代原来的数组。
在扩容的过程中,由于涉及到数组的拷贝,就会导致性能消耗;同时HashMap也会由于扩容的问题,消耗性能。所以在使用这类集合时可以在构造的时候指定集合的容量大小。
27、尽量不要使用BeanUtils来拷贝属性
在开发中经常需要对JavaBean进行转换,但是又不想一个一个手动set,比较麻烦,所以一般会使用属性拷贝的一些工具,比如说Spring提供的BeanUtils来拷贝。不得不说,使用BeanUtils来拷贝属性是真的舒服,使用一行代码可以代替几行甚至十几行代码,我也喜欢用。
但是喜欢归喜欢,但是会带来性能问题,因为底层是通过反射来的拷贝属性的,所以尽量不要用BeanUtils来拷贝属性。
比如你可以装个JavaBean转换的插件,帮你自动生成转换代码;又或者可以使用性能更高的MapStruct来进行JavaBean转换,MapStruct底层是通过调用(settter/getter)来实现的,而不是反射来快速执行。
28、使用StringBuilder进行字符串拼接
String?str1?=?"123"; String?str2?=?"456"; String?str3?=?"789"; String?str4?=?str1?+?str2?+?str3;
使用 + 拼接字符串的时候,会创建一个StringBuilder,然后将要拼接的字符串追加到StringBuilder,再toString,这样如果多次拼接就会执行很多次的创建StringBuilder,z执行toString的操作。所以可以手动通过StringBuilder拼接,这样只会创建一次StringBuilder,效率更高。
StringBuilder?sb?=?new?StringBuilder(); String?str?=?sb.append("123").append("456").append("789").toString();
29、@Transactional应指定回滚的异常类型
平时在写代码的时候需要通过rollbackFor显示指定需要对什么异常回滚,原因在这:
默认是只能回滚RuntimeException和Error异常,所以需要手动指定,比如指定成Expection等。
30、谨慎方法内部调用动态代理的方法
如下事务代码
@Service public?class?PersonService?{ ???? ????public?void?update(Person?person)?{ ????????//?处理 ????????updatePerson(person); ????} ????@Transactional(rollbackFor?=?Exception.class) ????public?void?updatePerson(Person?person)?{ ????????//?处理 ????} } update调用了加了@Transactional注解的updatePerson方法,那么此时updatePerson的事务就是失效。
其实失效的原因不是事务的锅,是由AOP机制决定的,因为事务是基于AOP实现的。AOP是基于对象的代理,当内部方法调用时,走的不是动态代理对象的方法,而是原有对象的方法调用,如此就走不到动态代理的代码,就会失效了。
如果实在需要让动态代理生效,可以注入自己的代理对象
31、需要什么字段select什么字段
查询全字段有以下几点坏处:
增加不必要的字段的网络传输
比如有些文本的字段,存储的数据非常长,但是本次业务使用不到,但是如果查了就会把这个数据返回给客户端,增加了网络传输的负担
会导致无法使用到覆盖索引
比如说,现在有身份证号和姓名做了联合索引,现在只需要根据身份证号查询姓名,如果直接select name 的话,那么在遍历索引的时候,发现要查询的字段在索引中已经存在,那么此时就会直接从索引中将name字段的数据查出来,返回,而不会继续去查找聚簇索引,减少回表的操作。
所以建议是需要使用什么字段查询什么字段。比如mp也支持在构建查询条件的时候,查询某个具体的字段。
32、不循环调用数据库
不要在循环中访问数据库,这样会严重影响数据库性能。
比如需要查询一批人员的信息,人员的信息存在基本信息表和扩展表中,错误的代码如下:
java 复制代码 public?List<PersonVO>?selectPersons(List<Long>?personIds)?{ ????List<PersonVO>?persons?=?new?ArrayList<>(personIds.size()); ????List<Person>?personList?=?personMapper.selectByIds(personIds); ????for?(Person?person?:?personList)?{ ????????PersonVO?vo?=?new?PersonVO(); ????????PersonExt?personExt?=?personExtMapper.selectById(person.getId()); ????????//?组装数据 ????????persons.add(vo); ????} ????return?persons; }
遍历每个人员的基本信息,去数据库查找。
正确的方法应该先批量查出来,然后转成map:
public?List<PersonVO>?selectPersons(List<Long>?personIds)?{ ????List<PersonVO>?persons?=?new?ArrayList<>(personIds.size()); ????List<Person>?personList?=?personMapper.selectByIds(personIds); ????????//批量查询,转换成Map ????List<PersonExt>?personExtList?=?personExtMapper.selectByIds(person.getId()); ????Map<String,?PersonExt>?personExtMap?=?personExtList.stream().collect(Collectors.toMap(PersonExt::getPersonId,?Function.identity())); ????for?(Person?person?:?personList)?{ ????????PersonVO?vo?=?new?PersonVO(); ????????//直接从Map中查找 ????????PersonExt?personExt?=?personExtMap.get(person.getId()); ????????//?组装数据 ????????persons.add(vo); ????} ????return?persons; }
33、用业务代码代替多表join
如上面代码所示,原本也可以将两张表根据人员的id进行关联查询。但是不推荐这么,阿里也禁止多表join的操作
而之所以会禁用,是因为join的效率比较低。
MySQL是使用了嵌套循环的方式来实现关联查询的,也就是for循环会套for循环的意思。用第一张表做外循环,第二张表做内循环,外循环的每一条记录跟内循环中的记录作比较,符合条件的就输出,这种效率肯定低。
我们平时写代码由于各种因为,比如什么领导啊,项目经理啊,会一直催进度,导致写代码都来不及思考,怎么快怎么来,cv大法上线,虽然有心想写好代码,但是手确不听使唤。所以我建议装一个阿里的代码规范插件,如果有代码不规范,会有提醒,这样就可以知道哪些是可以优化的了。
如果你有强迫症,相信我,装了这款插件,你的代码会写的很漂亮。