欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

性能优化实战-1

程序员文章站 2022-04-14 12:46:26
...

系统优化大致可以分为:业务优化、设计和程序优化、DBSYSTEM优化、SQL优化四个方面,这四个方面按优先级进行排列,并且越往后越不容易优化,他们之间又相辅相成、互相关联,我们优化的时候可以交替着去实施。下面着重分享一下我们在SQL优化和程序优化等方面的一些经验。

 

一、SQL优化

慢查询是大多数数据库问题的罪魁祸首,我们可以通过改善表结构设计、优化SQL语句、合理的使用索引等方面来最大限度的解决这个问题,下面我们一一进行讲解。

  1、 改善表结构设计

我们可以使用 PROCEDURE ANALYSE()对当前已有应用的表类型的判断,该函数可以对数据表中的列的数据类型提出优化建议, 可以根据应用的实际情况酌情考虑是否实施优化。

SELECT * FROM tbl_name PROCEDURE ANALYSE();

性能优化实战-1
            
    
    博客分类: java性能优化 java性能优化 PROCEDURE ANALYSE()是动态分析表数据得出的结果,分析需要一定的时间。分析结果展示了列名称、列的最小值、列的最大值、列最小值的长度、列最大值的长度、列中empty0的行数,列中null的行数,平均值或者平均长度,给出的列的类型优化建议,从中我们可以看出哪些列适合哪种类型,以减少存储空间、提高查询效率。

 

    2、 explain查询语句

(product)ifaes_test@localhost [ifaes_test]> explain select * from project where uuid='000002a1155c48f389ce85a64f0e2754';

+----+-------------+---------+-------+---------------+---------+---------+-------+------+-------+

| id | select_type | table   | type  | possible_keys | key     | key_len | ref   | rows | Extra |

+----+-------------+---------+-------+---------------+---------+---------+-------+------+-------+

| 1 | SIMPLE     | project | const  | PRIMARY    | PRIMARY | 98    | const |    1 | NULL  |

+----+-------------+---------+-------+---------------+---------+---------+-------+------+-------+

1 row in set (0.00 sec)

          在日常工作中,我们会有时会开慢查询去记录一些执行时间比较久的SQL语句,找出这些SQL语句并不意味着完事了,这时我们常常用到explain这个命令来查看一个这些SQL语句的执行计划,查看该SQL语句有没有使用上了索引,有没有做全表扫描,这都可以通过explain命令来查看。

  id SQL执行顺序的标识,相同的id表示为同一组。SQL执行顺序和ID有关,id值越大越先执行,相同的id按照从上往下的顺序执行。

select_type  select子句的类型,用于标识和其他子句的关联关系,常见的如SIMPLE/PRIMARY/UNION等类型。

(1) SIMPLE(简单SELECT,不使用UNION或子查询等)

(2) PRIMARY(查询中若包含任何复杂的子部分,最外层的select被标记为PRIMARY)

(3) UNION(UNION中的第二个或后面的SELECT语句)

(4) DEPENDENT UNION(UNION中的第二个或后面的SELECT语句,取决于外面的查询)

(5) UNION RESULT(UNION的结果)

(6) SUBQUERY(子查询中的第一个SELECT)

(7) DEPENDENT SUBQUERY(子查询中的第一个SELECT,取决于外面的查询)

(8) DERIVED(派生表的SELECT, FROM子句的子查询)

table select子句是关于哪张表的,有时会有临时表出现。

性能优化实战-1
            
    
    博客分类: java性能优化 java性能优化 

type表示MySQL在表中找到所需行的方式,又称为“访问类型”。常用的类型有:const/eq_reg/ref/ range/ index / all

const表示该表中最多有一条匹配的记录,出现在主键、唯一索引等查询语句中。

eq_reg对于每个来自前面的表的行组合,从该表中读取一行。即前表的行组合和当前SQL是一一对应的关系。

ref 前表的行组合和当前SQL是一对多的关系。

range使用索引来检索给定范围的行,key表示使用的索引名称。(<>>>=<<<=IS NULLbetweenin等等)。

index 使用索引进行检索。

ALL 进行完整的表扫描,即全表扫描。

 possible_keys 列指出MySQL能使用哪个索引在该表中找到行,方便我们合理利用索引来优化SQL语句。

 key 列显示MySQL实际决定使用的键(索引)。如果没有选择索引,键是NULL。要想强制MySQL使用或忽视possible_keys列中的索引,在查询中使用FORCE INDEXUSE INDEX或者IGNORE INDEX

 key_len 列显示MySQL决定使用的键长度。如果键是NULL,则长度为NULL。使用的索引的长度。在不损失精确性的情况下,长度越短越好 

 ref 表示上述表的连接匹配条件,即哪些列或常量被用于查找索引列上的值

 rows 列显示MySQL认为它执行查询时必须检查的行数。

 

   3、  合理的使用索引

首先要说明一点,合理的使用索引可以提高检索(查询)的效率,但是插入/更新/删除等操作时都需要对索引进行维护,所以索引并不是越多越好。

在用户红包表、用户加息券表中

性能优化实战-1
            
    
    博客分类: java性能优化 java性能优化 

user_redenvelope表有主键索引,tender_id的单列普通索引,user_idstatus的联合索引,use_expire_time_statusstatus的联合索引。下面分别说说各索引的用处,主键索引这里就不详述了。

idx_user_redenvelope_tender_id          查询投资记录所使用的红包时使用;

idx_user_redenvelope_user_id_status             查询指定用户可用红包/不可用红包时使用

idx_user_redenvelope_use_expire_time_status     处理过期红包时使用,后面会讲到如何使用索引。

用户加息券表的索引和和用户红包表的索引差不多,这里就不赘述。

和用户一一对应的记录表:user_base_infouser_cacheuser_identifyuser_company_infouser_scoreuser_vip等添加user_id唯一索引;

投资记录表project_invest、待收记录表project_collection、待还记录表project_repaymentproject_id列建立普通索引,产品表product、借贷表borrowproject_id列建立唯一索引。债权相关的表和投资相关表类似。

    下面举一个索引使用不当的例子:

性能优化实战-1
            
    
    博客分类: java性能优化 java性能优化 

user_cache表总创建了两个索引:uq_user_cache_user_id针对用户id的唯一索引和idx_user_cache_user_id_user_nature是将user_iduser_nature组合在一起的联合索引。上面的查询语句中,两个索引一个也没用到。

我们来考虑一下索引的有效性,uq_user_cache_user_id索引可以用来防重并且在多数查询时可以通过user_cache.user_id=xxx来使用唯一索引,提高查询效率。另一个就是idx_user_cache_user_id_user_nature,我们的期望是想通过和user的关联来查询某一类型的用户,但是事与愿违,所以该索引是无效的。那么我们怎么做?

         1、删除idx_user_cache_user_id_user_nature索引;

         2、user表添加冗余字段user_nature,或者将user_nature列直接移至user表;

 

  4、  其他优化建议

a)       表关联查询时,能使用内连接(逗号分隔或者inner join)的尽量不使用外连接(left join or right join),能使用join的尽量不使用子查询;

查询投资记录,关联项目记录时使用内联

select * from project_invest i inner join project p on i.project_id= p.uuid ;

select * from project_invest i left join project p on i.project_id= p.uuid ;

查询没有进行过投资的注册用户,使用外联代替子查询:

select * from user where uuid not in (select user_id from project_invest );

select u.* from user u left join project_invest i on i.user_id = u.uuid and i.user_id is null;

 

b)       对于排重(或分组)的SQL,可以使用下面两种,推荐使用方案1

方案1select distinct min,max from tbl;

方案2select min, max from tbl group by min,max;

c)       索引使用最左匹配原则,在条件查询时,如果组合索引中的首列没有出现,mysql优化器将放弃使用索引。

如下的第三条语句MySQL优化器将放弃使用索引

select * from user_redenvelope where use_expire_time>='2017-01-04 23:59:59' and use_expire_time<='2017-01-13 00:00:00' and status='0';

select * from user_redenvelope where use_expire_time>='2017-01-04 23:59:59' and use_expire_time<='2017-01-13 00:00:00';

select * from user_redenvelope where status='0';

 

d)       散列分布不均的列不推荐使用索引,比如状态列、枚举类型的列等等;

e)       尽量避免select * 操作,从库里读取的数据越多查询越慢。

f)       固定长度的效率更高,尽可能的使用非空约束(通过表结构优化来进行处理)。

 

二、代码优化

1、  缓存服务

对于访问频率比较高的或者不常更新的数据,我们可以考虑使用缓存服务,来提高系统的响应效率,减小数据库服务器的压力。比如前台首页、理财频道、投资详情页等页面。

首页的新手专享和精选投资列表缓存2s

    /**

     *

     * 首页--新手专享投资列表

     * @return

     * @throws Exception

     */

    @RequestMapping(value="/index/getNoviceProject")

    @ResponseBody

    @Cacheable(expire=ExpireTime.TWO_SEC)

    public Object getNoviceProject() throws Exception {

        …

    }

    /**

     *

     * 首页--精选投资列表

     * @return

     * @throws Exception

     */

    @RequestMapping(value="/index/getChoiceProject")

    @ResponseBody

    @Cacheable(expire=ExpireTime.TWO_SEC)

public Object getChoiceProject() throws Exception {

    ….

}

理财频道缓存5s

    /**

     *  产品列表查询

     * @author  FangJun

     */

    @Cacheable(key = CacheConstant.KEY_PROJECT_LIST, expire = ExpireTime.FIVE_SEC)

    public Page<ProjectRecord> findProjectPage(ProjectRecord model) {

       

    }

 

2、  队列服务

对于优先级不是特别高的操作,可以考虑将其加入队列的方式进行处理。比如前台用户注册,注册成功以后不仅需要向useruser_cacheuser_company_info(user_base_info)user_identityuser_vipuser_scoreuser_invite等用户相关的表中添加记录,还需要在账户中心进行注册,根据活动方案的规则发放相应的红包、加息券等操作。活动方案相对用户信息和账户信息来说显得不是那么迫切,不需要即时入库,可以考虑将活动方案加入队列服务。活动方案加入队列服务之前,用户注册的并发量一直上不去。活动方案加入队列之后,用户注册的并发可以达到30TPS,用户注册的并发量有了显著的提升。

活动方案加入队列后带来了一个新的问题,就是假如后台配置的活动方案太多,队列服务又需要一个一个的进行处理,在高并发的情况下会出现大量的消息堆积。有时甚至两个小时的用户并发注册,队列服务跑一天都处理不完。

接着怎么办?拆服务

梳理活动方案的类别,将活动方案拆分成:注册送好礼--送红包、注册送好礼--送加息券、注册好友送好礼--送红包、注册好友送好礼--送加息券、注册 --送积分五个小类。然后分别如队列进行处理。

类似的,我们对调度任务的队列服务按放款、还款、项目撤销的类别进行了服务拆分。

private User addUserForRegister(final UserModel model){

        //用户相关表持久化操作

        //活动方案入队列    注册的时候拆分队列

        MqActPlanModel redActModel = new MqActPlanModel(MqConstant.OPERATE_ACTPLAN_REGISTER_GIFT_REDPACKET, user,null,null);

        RabbitUtils.actPlan(redActModel);

        MqActPlanModel rateActModel = new MqActPlanModel(MqConstant.OPERATE_ACTPLAN_REGISTER_GIFT_RATECOUPON, user,null,null);

        RabbitUtils.actPlan(rateActModel);

        MqActPlanModel friendRedActModel = new MqActPlanModel(MqConstant.OPERATE_ACTPLAN_REGISTER_FRIEND_GIFT_REDPACKET, user,null,null);

        RabbitUtils.actPlan(friendRedActModel);

        MqActPlanModel friendRateActModel = new MqActPlanModel(MqConstant.OPERATE_ACTPLAN_REGISTER_FRIEND_GIFT_RATECOUPON, user,null,null);

        RabbitUtils.actPlan(friendRateActModel);

        MqActPlanModel scoreActModel = new MqActPlanModel(MqConstant.OPERATE_ACTPLAN_REGISTER_SCORE, user,null,null);

        RabbitUtils.actPlan(scoreActModel);

        returnuser;

    }

 

3、  单项持久化改为批量持久化

在前期的活动方案中,红包、加息券的发放是按用户和活动规则是一条条进行的,活动方案匹配成功后将红包(加息券)发放给用户,生成一条用户红包(加息券)记录和发放日志记录。跟踪队列服务发现在活动规则较多、用户注册并发量大的时候,队列的处理效率低的吓人。根据业务场景,我们将同一个用户的多个活动合并,批量持久化到数据库。在经过将队列服务拆分和单项操作改为批量操作后,活动方案的处理能力从0.2TPS提升到10-30TPS

类似的,调度任务、账户中心等地也从原来的单条持久化改为批量持久化。

性能优化实战-1
            
    
    博客分类: java性能优化 java性能优化 

4、  定时任务

程序员在处理定时任务的时候喜欢简单粗暴的方式,就像这样的:

#将红包标记为已过期

update user_redenvelope set status=#{status} where use_expire_time < now() and status=#{preStatus}

这个SQL语句在数据量不大的时候执行没有什么问题,一旦数据量上去之后就会卡死,甚至产生死锁。

那么怎么优化?我们先看看定时器里的方法

    @Scheduled(cron = "0 */5 * * * ?")

    publicvoid doCouponExpiredHandle() {    

        userRedenvelopeService.expiredHandle();// 红包过期处理

        userRateCouponService.expiredHandle();// 加息券过期处理

    }

上面的@Scheduled注解表示这是一个定时操作,cron表达式表明每五分钟执行一次。也就是说该方法会每五分钟执行一次,发现有过期的红包或者加息券就标记为已过期。

既然是每五分钟执行一次,那么我们查询的时间范围应该是可以继续缩小的,比如查询10分钟以内已经过了有效期但状态为未过期的记录。然后将状态标记为已过期并将结果分批次批量入库。

    /**

     * 红包过期处理

     */

    @Override

    publicint expiredHandle() {

        Date now = DateUtils.getNow();       

        List<String> ids = dao.findExpireList(DateUtils.rollMinute(now, -10), now, OperateEnum.STATUS_NO_USE.getValue());

        List<UserRedenvelope> list = new ArrayList<>(ids.size());

        if(CollectionUtils.isNotEmpty(ids)){

            for(String id:ids){

                UserRedenvelope url = new UserRedenvelope();

                url.setUuid(id);

                url.setStatus(OperateEnum.STATUS_EXPIRED.getValue());

                list.add(url);

            }

            //批量更新

            List<List<UserRedenvelope>> splitList = ObjectUtils.splitList(list, 200);

            for (List<UserRedenvelope> ls : splitList) {

                dao.updateBatchStatus(ls);

            }

        }

        return Constant.INT_ONE;

    }

Mapper对应的SQL如下:

<!-- 查询过期红包 -->

    <select id="findExpireList" resultType="String">

        select uuid

        from user_redenvelope

        where use_expire_time between #{startTime} and #{endTime} and status=#{preStatus}

    </select>

    <!-- 批量更新过期红包的状态-->

    <update id="updateBatchStatus" parameterType="java.util.List">

        <foreach collection="list" item="item" open="" separator=";" close=""> 

            update user_redenvelope  set STATUS = #{item.status}  where uuid = #{item.uuid}

        </foreach>

    </update>

我们总结一下:

     查询一定时间范围内状态为未过期的红包记录;

     将记录标记为已过期;

     将红包记录分批次批量入库;

这里为了提高查询的效率,我们对use_expire_timestatus创建了联合索引并且将范围查询由>=<=调整为between and

那么我们能不能直接将上面的查询语句调整为update语句呢?答案是:不推荐。对于数据量较大的平台,不建议直接批量更新,因为没办法确定更新记录的数量。数据量太大依然会卡死,甚至产生死锁。其他的定时操作也可以按照这样的步骤来进行优化。

另外一个优化点就是缩小事务的范围,定时任务多是批量操作。简单的定时操作通过一个update语句就能搞定,还有一些定时操作相对就复杂的多,涉及很多的业务处理,逻辑判断,而这些批量操作的数据之间又是相互隔离,互不影响的,比如订单超时处理、自动审核、自动下架等等。我们没有必要在这些定时任务的入口加上事务,只需要在处理单笔交易时加上事务支持即可。

    //@Transactional(readOnly = false)//订单超时处理入口取消事务支持

    publicvoid investTimeoutHandle() {

        …

    }

 

    /**

     * 投资超时处理,在单笔交易中添加事务支持

     * @param invest

     */

    privatevoid itemInvestTimeoutHandle(OutTimeProjectInvest invest){

        TransactionDefinition definition = new DefaultTransactionDefinition();

        TransactionStatus ts = transactionManager.getTransaction(definition);

        try {

             //处理超时相关业务

            transactionManager.commit(ts);

        } catch (Exception e) {

            transactionManager.rollback(ts);

            LOGGER.error(e.getMessage(),e);

            throwe;

        }  

    }

 

5、  后台查询

根据多年的从业经验在关联查询这方面定了一个规则:后台的关联查询不得超过3张表。起初大家都不是很理解,总觉得规定这么死很难去实现产品经理或者客户的需求。众所周知,后台查询通常都会进行一些跨表的关联查询,有的时候为了满足业务需要甚至需要关联56张表进行查询,SQL语句写的那叫一个漂亮,能整几十甚至上百行。但是数据量一旦上去,就卡的不行不行的。那么我们怎么去做既能保证SQL高效,还能保证能够实现产品经理或者客户的需求呢?

经验一:尽量将多表改为单表查询;

拿菜单【用户积分】举例,以下为【用户积分】列表页面的截图:

性能优化实战-1
            
    
    博客分类: java性能优化 java性能优化 

我们可以看到用户积分列表页展示了用户名、总积分、有效积分、冻结积分和消费积分等信息。列表中除了用户名是user表的属性外,其他列的信息都来自user_vip表。假设我们通过将useruser_vip的关联调整为单表查询,我们该以哪张表为主进行查询呢?

方案一:分页查询以user_vip为主表,在service层通过user_vipuser_id查询出user表的用户名;

方案二:分页查询以user为主表,在service层通过useruuid查询user_vip的相关信息;

貌似这两种方案都行的通,但是细看页面我们发现搜索框是支持根据用户名进行模糊查询的,所以方案一被否决,方案二胜出。

这个问题貌似难度不够高,假设用户提出要求,我们需要按照用户名、总积分、有效积分、冻结积分和消费积分等信息进行检索或者排序,那方案二显然不能满足需求,怎么办?

经验二:适当的时候可以考虑添加冗余字段;

接着上面的问题,测试环境的用户量已经超过一百万,关联查询显然已经不合适,那么我们就可以考虑在user_vip表中将user表的用户名列冗余添加到user_vip表中。这个例子可能不够好,我们举个相对比较恰当的例子吧。

project(产品+借贷)做了分类,我们在前台的理财频道中需要用到项目的类别名称,通过关联查询project_type表确实是一个方案,但是不够好。我们可以考虑在project表中添加类别名称列的冗余来取消对project_type表的关联查询。

经验三:通过添加索引提高查询效率;

表的数据量过大时,我们还可以采取通过添加索引的方式来提高查询效率,这里我就不展开讲解了。当然其他的优化方案在这里也可以派上用场。

 

6、  Excel导出

Excel大批量数据导出一直是我们系统的弱项,2.0里导出操作的数据量超过一万就会卡死。我们先看看excel导出经过了哪几个步骤:

1)       从数据库中查询出符合条件的记录;

2)       对记录中的数据进行加工处理,如将状态、枚举等类型转为可描述的信息等;

3)       将记录写入到文件或输出流中;

我们看到纵向操作很难去优化,那么我们分析一下数据量超过一万条就卡死的原因是什么,是在那一步产生的?

通过跟踪服务器的相关指标(线程、CPU、内存、相关服务),我们发现在做导出操作的时候,服务器的CPU和内存都飙升的很高。那么我们能不能通过调节JVM的相关参数来避免导出的时候出现卡死的现象呢?答案是:不推荐。

我们通过excel导出的步骤可以推测:导出卡死主要是因为数据量太大,导致jvm的内存爆表。那么如何优化呢?

我们在查询的时候模仿后台列表页面的分页操作,在导出的第一步将一次查询改为分页查询,每次最多读取5000条记录(每页导出的记录数根据实际情况来定)。目前我们已经做到可以支持百万级的数据导出,而不会出现卡死的现象。

除此之外,我们还做了什么?

我们发现,一般而言,导出的列都是和页面展示的列对应的,在后台通过硬编码来决定导出的列确实是一件吃力不讨好的事情,我们对需要导出的列做了封装,页面展示哪些列,导出就是哪些列,不需要在后台进行硬编码。

另外一个问题是让人头疼的状态、枚举等列在导出时的转换问题,我们定义了@DictType的注解,在代码里将繁琐的状态、枚举等类型的信息转换全部去除。

 

 

 

  • 性能优化实战-1
            
    
    博客分类: java性能优化 java性能优化 
  • 大小: 55 KB
  • 性能优化实战-1
            
    
    博客分类: java性能优化 java性能优化 
  • 大小: 17.5 KB
  • 性能优化实战-1
            
    
    博客分类: java性能优化 java性能优化 
  • 大小: 14 KB
  • 性能优化实战-1
            
    
    博客分类: java性能优化 java性能优化 
  • 大小: 16.4 KB
  • 性能优化实战-1
            
    
    博客分类: java性能优化 java性能优化 
  • 大小: 60.9 KB
  • 性能优化实战-1
            
    
    博客分类: java性能优化 java性能优化 
  • 大小: 19 KB
相关标签: java 性能优化