Java 应用 优化手记Word下载.docx
《Java 应用 优化手记Word下载.docx》由会员分享,可在线阅读,更多相关《Java 应用 优化手记Word下载.docx(23页珍藏版)》请在冰豆网上搜索。
测试时发现,程序设计时hibernate映射对象会关联其它表,比如用户表关联了用户详细信息表,这样做的好处是方便我们查询对象,里面已经封装了我们需要的所有信息,但如果我们不需要关联的信息呢?
在查询时hibernate会自动将关联查询一起封装为sql语句,这样等于做了2次select操作,影响查询效率。
在低负载的情况下不明显,随着压力的增加会越来越明显。
解决办法:
删除映射关联或将关联设置为延迟加载。
在开发的时候对于数据库的访问应该有明确的需求分析。
1.1.2createQuery与createNativeQuery
这个问题暂时还没有明确,当hibernate使用createQuery时查询效率就会越来越低,使用createNativeQuery时就不会下降,初步判断为:
hibernate更新自己的查询缓存影响了查询速度,依据是查询不同的信息时,速度越查越慢,但是查询刚才已经查询过的数据,速度是恒定的。
createNativeQuery需要自己封装sql语句,返回的也是基础数据类型或许没有经过hibernate的一级缓存。
当然这些只是猜测。
1.2ConcurrentHashMap
ConcurrentHashMap是Java5中支持高并发、高吞吐量的线程安全HashMap实现。
按照java官方的解释,ConcurrentHashMap是由线程并发专家编写的专门用于高并发情况下的Map。
ConcurrentHashMap允许多个修改操作并发进行,其关键在于使用了锁分离技术。
它使用了多个锁来控制对hash表的不同部分进行的修改。
ConcurrentHashMap内部使用段(Segment)来表示这些不同的部分,每个段其实就是一个小的hashtable,它们有自己的锁。
只要多个修改操作发生在不同的段上,它们就可以并发进行。
lsmp使用ConcurrentHashMap作为cache,每次购彩操作中,都会先将购彩对象存入cache中,再发送给生产系统落单,当每张彩票从生产系统正确落单返回给business后,再到cache中找到刚才的彩票对象,完成后面的购彩操作,将对象从cache中删除。
由于存在超时问题,就起了一个线程定时扫描cache,每次扫描都会遍历整个cache,将彩票对象的生命周期减一(int型数据),当减到0时,此彩票对象超时;
也就是说每次定时任务都会将cache中所有对象操作一遍,要么减少生命周期操作,要么超时操作。
但是测试时发现在高并发下,经常会出现cache中数据丢失数据的问题,症状为:
在压力测试进行10分钟左右,开始出现购彩信息从生产系统正常落单回来后,在cache中找不到对应的彩票对象,造成购彩失败的情况。
低压力情况下也会发生这个问题但错误少,压力越高出错的数据就越多。
经过分析怀疑是cache操作过于频繁造成的,每次定时任务都会对cache中所有数据进行全量更新操作,并且购彩操作还在进行,购彩完成操作也在进行,长时间高并发的进行get操作的同时进行put+remove操作,ConcurrentHashMap的锁控制机制失效了。
根据上面的分析对代码进行了修改,将彩票对象的生命周期从int修改为long(System.currentTimeMillis()),每次定时任务处理cache时(10秒一次),只是将彩票对象从cache中get出来,然后与系统当前时间(System.currentTimeMillis())进行减法操作,查看结果是否大于或等于设置的超时时间,如果没有超时则遍历下一条。
这时对于cache中的数据只进行get操作并没有改变对象的内容,优化代码之前这里每次都要将生命周期减一然后再重新put到cache中,get不影响锁控制的,只有发现超时的对象才进行remove,cache的访问压力变小了;
但是购彩成功的彩票后依然会实时对cache进行remove操作,cache依然长时间处于put+remover操作,错误数量虽然下降了但并没有消失。
再次优化代码,完成购彩成功后只将cache中彩票对象的状态标志位改为“购彩完成”,不实时删除cache中的数据;
修改定时任务增加“判断彩票状态”功能,当定时任务遍历cache时,会判断当前彩票对象“是否超时”+“是否已经完成购彩”,如果都不是则遍历下一条,如果超时或已完成购彩则从cache中remove。
也就是说定时任务执行时彩票对象从cache中统一remove。
这样对于cache的访问就变为:
一直进行put操作,购彩成功后修改状态位重新put回cache,这时cache中的数据只增加没减少,并且cache中的对象不会发生并发访问(因为彩票号都是唯一的);
当定时任务执行时(10秒一次)才集中进行remove操作,并且将remove被定义为synchronized。
定时任务的间隔时间比较长(10秒一次),所以不会导致ConcurrentHashMap的锁控制机制失效的问题。
再次测试后发现,每次定时任务执行时,cache的性能会大幅下降,所以果断的去掉了remove的synchronized,但是确保代码中只有一个地方可以对cache进行remove操作,入口也是唯一的(单例模式)。
测试结果:
在tps:
900的情况下运行8小时,全部成功,无失败记录。
定时任务可以在3秒内完成整个任务(quartz启动任务+rmi发送接收消息+cache遍历)。
对于高并发cache操作的一些总结:
1.cache是放在内存中的,速度快易丢失,会对提高编码难度,特别是高并发下,管理不好会造出数据不同步,所以再使用之前先考虑清楚:
“你确定有必要使用cache吗?
如果确定要用那么cache要承受多大的压力?
如何解决数据不同步的问题?
”
2.对于cache中的对象,能少修改就少修改,能不修改就不修改。
3.对于ConcurrentHashMap来说get操作不影响锁,可以放心并发使用。
4.对于生命周期来说,时间戳比递减数值的好用,它不需要频繁修改cache中对象的内容,缺点是精度不够,可以通过增加定时任务的执行频率来提高精度。
5.尽量避免长时间对cache进行put+remove操作,如果实在避免不了,就将其中一个改为批量操作。
6.在高并发下ConcurrentHashMap确实比HashMap好,用HashMap测试时购彩数据错的一塌糊涂。
但是ConcurrentHashMap的系统开销比HashMap大不少。
7.专用的cache比ConcurrentHashMap好用。
1.3session超时
session管理是web开发中很重要的一环,我们将session放入容器,到了超时时间,webserver会自动删除,也可以手动删除,一般情况下开发人员会在用户退出时调用删除session代码,但是如果用户没有点击退出,只是关闭了浏览器,那只能等到session超时了,小网站一般不会有问题,但网站访问量非常大呢?
session必须要用,那么是不是可以只往里放必要的东西,毕竟内存是有限的。
如果一定要放很多东西,能不能压缩下内容再放进去,办法其实很多。
同理我们在编程的时候有没有把各种极限情况都考虑进去呢?
就像DougLinder说的:
“一个好的程序员应该是那种过单行线都要往两边看的人。
1.4对象生存周期
编程时很多程序员习惯于先把要用到的对象声明出来,然后再写逻辑,这个习惯不错,不过java对于每个new出来的对象,都会分配内存空间并且消耗部分cpu资源。
我走查代码时,发现不少童靴new出对象放在那里,执行了好几行代码后(其中包括查询数据库这样比较耗时的操作),才用到这个对象,期间java是不会回收这个对象的,因为它的生命周期还没到,内存在这段时间被浪费了,当然在一个毫秒内浪费几十个字节的内存无所谓,不过如果这个方法在一秒钟内被调用了几百万次呢?
1.5流水线
现在的cpu架构是按流水线执行的,比如执行for循环,for里面的代码会编译为一个顺序的流水线执行,如果在for循环中加入一条System.out语句,System.out要输出信息并且不属于流水线,因为System.out需要jdk调用操作系统的IO接口,破坏了流水线操作,cpu会将流水线的指令集按顺序一条条压入堆栈(先进后出),等IO完毕后在从堆栈中顺序读取指令恢复流水线操作。
我因为测试需要,在购彩代码中加入了一句System.out测试语句,结果忘了删除,在极限压力测试下,后台报了大量线程处于blocked状态(线程阻塞),而这些线程都指向这条System.out语句;
删除这条语句后再测试系统正常,压测1小时内没发现有blocked的线程。
所以我比别人更痛恨在svn上看到System.out。
1.6高低搭配
编程时经常碰到速度不匹配的代码,比如短信接口,发短信的速度慢,如果把这个接口放到购彩这样要求并发很高的代码中,就会造出极大的性能降低,这时有两个方法,第一修改短信接口,使之处理速度达到或接近购彩接口的速度(这几乎不可能);
第二就需要将短信接口改为异步,购彩不再依赖短信回执信息。
1.7方法间相互调用
我们天天写各种方法、接口;
调用别人给我们的各种方法、接口,但是很少有人说方法之间的调用也是耗时的,虽然耗时极短,但确实是消耗了的,当然这不能说我们把所有操作都要写在一个方法内,必要的结构有利于开发和维护,但是方法和接口泛滥不是一个好习惯,从中找到一个平衡不是一件容易的事情。
1.8伊格尔森定律
你自己的代码如果超过6个月不看,再看的时候和别人写的一样。
1.9调试
EdsgerW.Dijkstra说过:
“如果调试是除虫的过程,那么编程就一定是把臭虫放进来的过程”。
我发现很多童靴总想着开发新东西和热衷于用新技术,但很少人喜欢去调试代码,我说的调试不是像测试人员那样按照步骤一个一个功能点测试,而是更接近于白盒测试,必要时要修改bug保证测试可以顺利完成。
这项工作是十分枯燥的;
调试是烦人的、苦恼的;
被各种bug轮来轮去、被各种低级错误气得直揪头发,但是你却可以在调试中发现很多东西、学到很多东西,别人的思想,自己的思想。
可以说没有比调试代码更能提高程序员水平的方法了。
特别是再有性能要求的时候,比如这段代码要求并发达到1000,但是程序执行没有错误,就是速度上不去,怎么办?
找问题呗,哪里是瓶颈?
测试日志语句到底打在哪一行?
往上一行和往下一行会给测试日志带来什么改变?
哪里可以处理的再快一点?
哪里的代码质量不高?
你要一步一步去分析、去测试。
如果这样你的水平还不能提高,那我也没什么好说的了。
BrainW.Kernighan说过:
“调试代码的难度是首次编写这些代码的两倍,因此,如果你在编写代码的时候就已经发挥了全部的聪明才智,那么按照常理,你将无法凭借自己的智慧去调试这些代码。
”所以如果你能调试出比现有代码更牛叉的代码,只能说明你比写代码的人更加牛叉!
(个人观点)
2OS优化
现在使用的是64位linux,li