天天低头写代码,可你知道什么是代码级性能优化吗?(上)


服务器配置:4核CPU 8G内存 共4台
MQ:RabbitMQ
数据库:DB2
SOA框架:公司内部封装的Dubbo
缓存框架:Redis,Memcached

统一配置管理系统:公司内部开发的系统



  1. 单台40TPS,加到4台服务器能到60TPS,扩展性几乎没有。
  2. 在实际生产环境中,经常出现数据库死锁导致整个服务中断不可用。
  3. 数据库事务乱用,导致事务占用时间太长。
  4. 在实际生产环境中,服务器经常出现内存溢出和CPU时间被占满。
  5. 程序开发的过程中,考虑不全面,容错很差,经常因为一个小bug而导致服务不可用。
  6. 程序中没有打印关键日志,或者打印了日志,信息却是无用信息没有任何参考价值。
  7. 配置信息和变动不大的信息依然会从数据库中频繁读取,导致数据库IO很大。
  8. 项目拆分不彻底,一个tomcat中会布署多个项目WAR包。
  9. 因为基础平台的bug,或者功能缺陷导致程序可用性降低。
  10. 程序接口中没有限流策略,导致很多vip商户直接拿我们的生产环境进行压测,直接影响真正的服务可用性。
  11. 没有故障降级策略,项目出了问题后解决的时间较长,或者直接粗暴的回滚项目,但是不一定能解决问题。
  12. 没有合适的监控系统,不能准实时或者提前发现项目瓶颈。



1、数据库死锁优化解决

我们从第二条开始分析,先看一个基本例子展示数据库死锁的发生:


注:在上述事例中,会话B会抛出死锁异常,死锁的原因就是A和B二个会话互相等待。

分析:出现这种问题就是我们在项目中混杂了大量的事务+for update语句,针对数据库锁来说有下面三种基本锁:


for update语句和gap locknext-key lock锁相混合使用,又没有注意用法的时候,就非常容易出现死锁的情况。

那我们用大量的锁的目的是什么,经过业务分析发现,其实就是为了防重,同一时刻有可能会有多笔支付单发到相应系统中,而防重措施是通过在某条记录上加锁的方式来进行。

针对以上问题完全没有必要使用悲观锁的方式来进行防重,不仅对数据库本身造成极大的压力,同时也会把对于项目扩展性来说也是很大的扩展瓶颈,我们采用了三种方法来解决以上问题:

  • 使用Redis来做分布式锁,Redis采用多个来进行分片,其中一个Redis挂了也没关系,重新争抢就可以了。
  • 使用主键防重方法,在方法的入口处使用防重表,能够拦截所有重复的订单,当重复插入时数据库会报一个重复错,程序直接返回。
  • 使用版本号的机制来防重。

以上三种方式都必须要有过期时间,当锁定某一资源超时的时候,能够释放资源让竞争重新开始。


2、数据库事务占用时间过长

伪代码示例:

项目中类似这样的程序有很多,经常把类似httpClient,或者有可能会造成长时间超时的操作混在事务代码中,不仅会造成事务执行时间超长,而且也会严重降低并发能力。

那么我们在用事务的时候,遵循的原则是快进快出,事务代码要尽量小。针对以上伪代码,我们要用httpClient这一行拆分出来,避免同事务性的代码混在一起,这不是一个好习惯。


3、CPU时间被占满分析

下面以我之前分析的一个案例作为问题的起始点,首先看下面的图:


项目在压测的过程中,cpu一直居高不下,那么通过分析得出如下分析:

  • 数据库连接池影响

我们针对线上的环境进行模拟,尽量真实的在测试环境中再现,采用数据库连接池为咱们默认的C3P0。

那么当压测到二万批,100个用户同时访问的时候,并发量突然降为零!报错如下:


那么针对以上错误跟踪C3P0源码,以及在网上搜索资料:
http://blog.sina.com.cn/s/blog_53923f940100g6as.html

发现C3P0在大并发下表现的性能不佳。

  • 线程池使用不当引起



以上代码的场景是每一次并发请求过来,都会创建一个线程,将DUMP日志导出进行分析发现,项目中启动了一万多个线程,而且每个线程都极为忙碌,彻底将资源耗尽。

那么问题到底在哪里呢???就在这一行!

在并发的情况下,无限制的申请线程资源造成性能严重下降,在图表中显抛物线形状的元凶就是它!!!那么采用这种方式最大可以产生多少个线程呢??答案是:Integer的最大值!看如下源码:


那么尝试修改成如下代码:


修改完成以后,并发量重新上升到100以上TPS,但是当并发量非常大的时候,项目GC(垃圾回收能力下降),分析原因还是因Executors.newFixedThreadPool(50)这一行,虽然解决了产生无限线程的问题,但是当并发量非常大的时候,采用newFixedThreadPool这种方式,会造成大量对象堆积到队列中无法及时消费,看源码如下:


可以看到采用的是无界队列,也就是说队列是可以无限的存放可执行的线程,造成大量对象无法释放和回收。


4、日志打印问题

先看下面这段日志打印程序:


服务器配置:4核CPU 8G内存 共4台
MQ:RabbitMQ
数据库:DB2
SOA框架:公司内部封装的Dubbo
缓存框架:Redis,Memcached

统一配置管理系统:公司内部开发的系统



  1. 单台40TPS,加到4台服务器能到60TPS,扩展性几乎没有。
  2. 在实际生产环境中,经常出现数据库死锁导致整个服务中断不可用。
  3. 数据库事务乱用,导致事务占用时间太长。
  4. 在实际生产环境中,服务器经常出现内存溢出和CPU时间被占满。
  5. 程序开发的过程中,考虑不全面,容错很差,经常因为一个小bug而导致服务不可用。
  6. 程序中没有打印关键日志,或者打印了日志,信息却是无用信息没有任何参考价值。
  7. 配置信息和变动不大的信息依然会从数据库中频繁读取,导致数据库IO很大。
  8. 项目拆分不彻底,一个tomcat中会布署多个项目WAR包。
  9. 因为基础平台的bug,或者功能缺陷导致程序可用性降低。
  10. 程序接口中没有限流策略,导致很多vip商户直接拿我们的生产环境进行压测,直接影响真正的服务可用性。
  11. 没有故障降级策略,项目出了问题后解决的时间较长,或者直接粗暴的回滚项目,但是不一定能解决问题。
  12. 没有合适的监控系统,不能准实时或者提前发现项目瓶颈。



1、数据库死锁优化解决

我们从第二条开始分析,先看一个基本例子展示数据库死锁的发生:


注:在上述事例中,会话B会抛出死锁异常,死锁的原因就是A和B二个会话互相等待。

分析:出现这种问题就是我们在项目中混杂了大量的事务+for update语句,针对数据库锁来说有下面三种基本锁:


for update语句和gap locknext-key lock锁相混合使用,又没有注意用法的时候,就非常容易出现死锁的情况。

那我们用大量的锁的目的是什么,经过业务分析发现,其实就是为了防重,同一时刻有可能会有多笔支付单发到相应系统中,而防重措施是通过在某条记录上加锁的方式来进行。

针对以上问题完全没有必要使用悲观锁的方式来进行防重,不仅对数据库本身造成极大的压力,同时也会把对于项目扩展性来说也是很大的扩展瓶颈,我们采用了三种方法来解决以上问题:

  • 使用Redis来做分布式锁,Redis采用多个来进行分片,其中一个Redis挂了也没关系,重新争抢就可以了。
  • 使用主键防重方法,在方法的入口处使用防重表,能够拦截所有重复的订单,当重复插入时数据库会报一个重复错,程序直接返回。
  • 使用版本号的机制来防重。

以上三种方式都必须要有过期时间,当锁定某一资源超时的时候,能够释放资源让竞争重新开始。


2、数据库事务占用时间过长

伪代码示例:

项目中类似这样的程序有很多,经常把类似httpClient,或者有可能会造成长时间超时的操作混在事务代码中,不仅会造成事务执行时间超长,而且也会严重降低并发能力。

那么我们在用事务的时候,遵循的原则是快进快出,事务代码要尽量小。针对以上伪代码,我们要用httpClient这一行拆分出来,避免同事务性的代码混在一起,这不是一个好习惯。


3、CPU时间被占满分析

下面以我之前分析的一个案例作为问题的起始点,首先看下面的图:


项目在压测的过程中,cpu一直居高不下,那么通过分析得出如下分析:

  • 数据库连接池影响

我们针对线上的环境进行模拟,尽量真实的在测试环境中再现,采用数据库连接池为咱们默认的C3P0。

那么当压测到二万批,100个用户同时访问的时候,并发量突然降为零!报错如下:


那么针对以上错误跟踪C3P0源码,以及在网上搜索资料:
http://blog.sina.com.cn/s/blog_53923f940100g6as.html

发现C3P0在大并发下表现的性能不佳。

  • 线程池使用不当引起



以上代码的场景是每一次并发请求过来,都会创建一个线程,将DUMP日志导出进行分析发现,项目中启动了一万多个线程,而且每个线程都极为忙碌,彻底将资源耗尽。

那么问题到底在哪里呢???就在这一行!

在并发的情况下,无限制的申请线程资源造成性能严重下降,在图表中显抛物线形状的元凶就是它!!!那么采用这种方式最大可以产生多少个线程呢??答案是:Integer的最大值!看如下源码:


那么尝试修改成如下代码:


修改完成以后,并发量重新上升到100以上TPS,但是当并发量非常大的时候,项目GC(垃圾回收能力下降),分析原因还是因Executors.newFixedThreadPool(50)这一行,虽然解决了产生无限线程的问题,但是当并发量非常大的时候,采用newFixedThreadPool这种方式,会造成大量对象堆积到队列中无法及时消费,看源码如下:


可以看到采用的是无界队列,也就是说队列是可以无限的存放可执行的线程,造成大量对象无法释放和回收。


4、日志打印问题

先看下面这段日志打印程序:

猜你喜欢

转载自blog.csdn.net/andyliulin/article/details/80890657