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 两张表。

    表拆分图示

  • 相关阅读:
    Notes of Daily Scrum Meeting(12.18)
    Notes of Daily Scrum Meeting(12.17)
    Notes of Daily Scrum Meeting(12.16)
    Notes of Daily Scrum Meeting(12.8)
    Notes of Daily Scrum Meeting(12.5)
    Notes of Daily Scrum Meeting(12.3)
    Notes of Daily Scrum Meeting(11.12)
    Linux中profile、bashrc、bash_profile之间的区别和联系
    Linux GCC编译
    mysql 5.7.16 远程连接
  • 原文地址:https://www.cnblogs.com/sanshengshui/p/11854813.html
Copyright © 2011-2022 走看看