撥亂反正-重構是門藝術活

  • 2019 年 11 月 14 日
  • 筆記

背景圖

前言

引用自: 《重構 改善既有程式碼的設計》

重構是在不改變軟體可觀察行為的前提下改善其內部結構。當你面對一個最需要重構的遺留系統時,其規模之大、歷史之久、程式碼品質之差,常會使得添加單元測試或者理解其邏輯都成為不可能的任務。此時你唯一能依靠的就是那些已經被證明是行為保持的重構手法: 用絕對安全的手法從焦油坑中整理出可測試的介面,給它添加測試,以此作為繼續重構的立足點。

因為我們部門內容平台的文章系統之前遺留了很多問題,急需解決這些具有"壞味道"的程式碼。最後因為其他人手頭裡都有其他工作,最後這些任務就交給了我。以下是急需解決的問題。

  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 兩張表。

表拆分圖示