zoukankan      html  css  js  c++  java
  • 拨乱反正-重构是门艺术活

    背景图

    前言

    引用自: 《重构 改善既有代码的设计》

    重构是在不改变软件可观察行为的前提下改善其内部结构。当你面对一个最需要重构的遗留系统时,其规模之大、历史之久、代码质量之差,常会使得添加单元测试或者理解其逻辑都成为不可能的任务。此时你唯一能依靠的就是那些已经被证明是行为保持的重构手法: 用绝对安全的手法从焦油坑中整理出可测试的接口,给它添加测试,以此作为继续重构的立足点。

    因为我们部门内容平台的文章系统之前遗留了很多问题,急需解决这些具有"坏味道"的代码。最后因为其他人手头里都有其他工作,最后这些任务就交给了我。以下是急需解决的问题。

    1. 内容平台新增/更新/取消/删除文章,同步各集团下文章行为状态,消息链路过长的问题。
    2. article分享表停止规模新增,之前未做插入前的记录判断,通过新增的操作来进行记录留存。
    3. 文章表拆除大字段到分表,如content、content_draft等字段。

    链路过长概述

    内容平台新增/更新/取消/删除文章,同步各集团下文章行为状态,消息链路过长的问题。

    • 问题导火索: 运营后台文章发布,发送消息到marketing-base

    • 慢链路,链路过长

      • mysql数据同步,单条执行n次

      • es索引数据同步,dubbo接口调用n次

    链路过长
    图1 链路图

    链路过长剖解及解决思路

    具体问题,具体对待

    //开启同步开关的集团
            List<Integer> groupList = autoSyncStatusService.getAutoSyncGroupByManageType(MANAGE_TYPE_GROUP_ARTICLE); 
    
    	for (Integer groupId : syncSubjectList) {
                    SiteGroupInfoDTO siteGroupInfo = siteSPI.getGroupInfoById(groupId);
                    Set<String> groupBrandSet = carOnSaleManage.getGroupBrandSet(siteGroupInfo);
                    List<String> matchedBrandCodes = extractBrandCodesFromArticleLabel(article.getLabelInfos());
                    if (CollectionUtils.isEmpty(matchedBrandCodes) || CollectionUtils.containsAny(groupBrandSet, matchedBrandCodes)) {
                        ArticleGroupMaterialBO groupMaterialBO =
                                ArticleBeanConverter.convertMaterial2GroupMaterial(article, groupId, groupList);
                        // 设置对应的集团主题id
                        ArticleGroupSubjectBO groupSubjectBO =
                                articleGroupSubjectService.getGroupSubjectBySoucheId(groupId, article.getSubjectId());
                        if (Objects.nonNull(groupSubjectBO.getId())) {
                            groupMaterialBO.setSubjectId(groupSubjectBO.getId());
                            groupMaterialBO.setMaterialId(myArticleId);
                            articleGroupMaterialService.addArticleGroupMaterial(groupMaterialBO);
                        }
                }
            } else {
                    //查询同步的文章数据是否存在
                    List<ArticleGroupMaterialBO> list = articleGroupMaterialService.getListByMaterialId(myArticleId);
                    for (ArticleGroupMaterialBO a : list) {
                        if (groupList.contains(a.getGroupId())) {
                            articleGroupMaterialService.changeRecommendStatus(a.getId(), a.getGroupId(), recommend, article.getLastOperatorName(), article.getLastOperatorName());
                        }
                    }
            }
    
    • 第4行中我们可以看到这里有一个for循环♻️,假设开启同步开关的集体有1000家,则第18行中mysql插入操作就需要执行1000次。

    • 第24行这里同样有一个for循环体♻️,则26行内部的es数据同步则需要调用1000次。它的实现如下:

      @Override
          public boolean changeRecommendStatus(int id, int groupId, int recommended, String lastOperatorUserId, String lastOperatorName) {
              final boolean success = articleGroupMaterialDAO.changeRecommendStatus(
                      id, groupId, recommended, lastOperatorUserId, lastOperatorName) > 0;
              if (success) {
                  //更新索引,更改推荐状态
                  articleSearchManage.updateArticleIndex(ArticleIndexUtil.getUpdateRecommendIndex(recommended, id, lastOperatorName));
              }
              return success;
          }
      

      解决思路

      Mybatis批量插入

      对于第一个循环♻️体中,我们需要将数据批量添加到数据库,mybatis提供了将list集合循环添加到数据库的方法。

      1. mapper层中创建 insertForeach(List < Fund > list) 方法,返回值是批量添加的数据条数
      public interface FundMapper {
      	int insertForeach(List<Fund> list);
      }
      
      1. mybatis的xml文件中的insert语句如下
      <?xml version="1.0" encoding="UTF-8" ?>
      <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
      <mapper namespace="com.center.manager.mapper.FundMapper">
      
      	<insert id="insertForeach" parameterType="java.util.List" useGeneratedKeys="false">
          			insert into fund
          			( id,fund_name,fund_code,date_x,data_y,create_by,create_date,update_by,update_date,remarks,del_flag)
          			values
          			<foreach collection="list" item="item" index="index" separator=",">
          				(
          					#{item.id},
          					#{item.fundName},
          					#{item.fundCode},
          					#{item.dateX},
          					#{item.dataY},
          					#{item.createBy},
          					#{item.createDate},
          					#{item.updateBy},
          					#{item.updateDate},
          					#{item.remarks},
          					#{item.delFlag}
          				)
          		     </foreach>		
          </insert>    
      </mapper>
      
      ES批量更新

      com.souche.elastic.search.api.IndexService

      方法:BulkUpdateResponse bulkUpdate(String index, Map<String, Object> event, String query, String origin)
      
      参数:
      
          index:要操作的索引
      
          event:更新的数据,可以只包含需要更新的字段,相当于mysql的update语句中的set语句中的字段
      
          query:query中的条件相当于mysql中的where,具体语法与下面的搜索接口中【querys:string 复杂的复合查询 不同字段的OR 查询】相同
      
          origin:操作源,一般写调用方自己的应用名,用于区分不同调用方
      
      返回值:
      
          BulkUpdateResponse:
      
            {
      
              requestId:本次操作的唯一标示
      
              status:状态,目前返回默认都是true
      
              updated:成功更新的条数
      
              failed:更新失败的条数
      
              message:第一条更新失败的原因
      
            }
      
      调用示例:
      
      1Map<String, Object> data = new HashMap<>();
      2        data.put("id", 20);
      3        data.put("title", "xue yin");
      4        data.put("content", "kuang dao");
      5        BulkUpdateResponse response = indexService.bulkUpdate("test_index", data, "address=bj AND contry=cn", "shenfl");
      

      这条更新将test_index索引中所有 address是bj并且contry是cn 的数据的 title更新成‘xue yin’ content更新成‘kuang dao’,注意:address和contry两个字段在索引中需要加索引

    Article表插入逻辑优化,停止规模新增概述

    Article逻辑优化剖解及解决思路

    具体问题及解决思路

    当前article数据表数据量:

    select count(*) as 总数 from article;
    

    结果如下:

    总数
    369737
    
      @Override
        public String addSharedArticle(ArticleBO articleBO) {
            ArticleDO articleDO = new ArticleDO();
            BeanUtils.copyProperties(articleBO, articleDO);
            String shortUUID = UUIDUtil.getShortUUID();
            articleDO.setUid(shortUUID);
            if (articleDAO.addSharedArticle(articleDO) > 0) {
                return shortUUID;
            }
            return StringUtil.EMPTY_STRING;
        }
    

    从上面这个业务逻辑实现类中,我们可以看到事实上我们想得到的是插入表数据的uid。但是之前的逻辑中,我们并没有判断该条数据是否已经存在,我们需要在上面代码中判断数据是否存在,已存在,查询最后一天数据的uid返回给上层。不存在的话,执行插入操作。

    文章表拆除大字段到分表

    article_material表结构设计

    article_material | CREATE TABLE `article_material` (
      `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
      `my_article_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '内容平台我的文章id',
      `status` tinyint(3) unsigned NOT NULL COMMENT '1-待发布、2-发布、3-取消发布',
      `subject_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '主题id',
      `platform_id` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '平台id',
      `source` varchar(32) NOT NULL DEFAULT '' COMMENT '版块',
      `crawler_article_id` varchar(32) NOT NULL DEFAULT '0' COMMENT '爬虫的文章id',
      `title` varchar(64) NOT NULL DEFAULT '' COMMENT '标题',
      `cover_img` varchar(128) NOT NULL COMMENT '封面图',
      `summary` varchar(255) NOT NULL DEFAULT '' COMMENT '摘要',
      `labels` varchar(512) NOT NULL DEFAULT '' COMMENT '标签',
      `label_infos` varchar(1024) NOT NULL DEFAULT '' COMMENT '标签详细信息',
      `content` text NOT NULL COMMENT '内容,用户看到的',
      `content_imgs` text NOT NULL COMMENT '内容中图片',
      `content_videos` varchar(255) NOT NULL DEFAULT '' COMMENT '内容中视频',
      `content_draft` text NOT NULL COMMENT '草稿内容,编辑后保存到这里,发布后内容会复制到content,此字段清空',
      `content_imgs_draft` text NOT NULL COMMENT '草稿内容的图片,同上',
      `content_videos_draft` varchar(255) NOT NULL DEFAULT '' COMMENT '草稿内容的视频',
      `recommended` tinyint(1) unsigned NOT NULL DEFAULT '0' COMMENT '0-不推荐、1-推荐',
      `author_user_id` varchar(64) NOT NULL DEFAULT '' COMMENT '作者userId',
      `author_name` varchar(16) NOT NULL COMMENT '作者名称',
      `last_operator_user_id` varchar(64) NOT NULL DEFAULT '' COMMENT '最后操作人userId',
      `last_operator_name` varchar(16) NOT NULL COMMENT '最后操作人名字',
      `publish_date` datetime DEFAULT NULL COMMENT '发布时间',
      `publisher_user_id` varchar(64) NOT NULL DEFAULT '' COMMENT '发布者userId',
      `publisher_name` varchar(16) NOT NULL DEFAULT '' COMMENT '发布者名字',
      `pv` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '流量pv',
      `uv` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '流量uv',
      `share_count` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '分享次数',
      `share_people_count` int(10) unsigned NOT NULL DEFAULT '0' COMMENT '分享人数',
      `date_create` datetime NOT NULL,
      `date_update` datetime NOT NULL,
      `date_delete` datetime DEFAULT NULL,
      `deleted` bigint(20) unsigned NOT NULL DEFAULT '0' COMMENT '0 表示未删除,删除后是毫秒级时间戳',
      PRIMARY KEY (`id`),
      UNIQUE KEY `uniq_id` (`my_article_id`),
      KEY `idx_title_label_status` (`subject_id`,`platform_id`,`title`,`label_infos`(255),`source`)
    ) ENGINE=InnoDB AUTO_INCREMENT=861 DEFAULT CHARSET=utf8 COMMENT='文章素材库,给集团提供文章素材'
    

    上表中content, content_imgs,content_videos都是text类型等大字段,对于这种类型,我们需要把这种类型的表拆分成2张表 article_metedata和article_content 两张表。

    表拆分图示

  • 相关阅读:
    向对象(OO)程序设计
    gVim安装vim-template插件后提示Undefined variable vim_template_subtype/Press ENTER or type command to continue
    基于JQuery easyui,gson的批量新增/修改和删除-servlet版
    Java正则表达式-匹配正负浮点数
    自己写的ORM工具
    秋色园学习测试项目
    把aspx页面生成的cs文件放到其他类库中,以实现对其的封装操作.
    杭州蚂蚁中台技术部-22届应届生-校招实习
    博客园开博
    开发随手记
  • 原文地址:https://www.cnblogs.com/sanshengshui/p/11854813.html
Copyright © 2011-2022 走看看