撥亂反正-重構是門藝術活
- 2019 年 11 月 14 日
- 筆記
前言
引用自: 《重構 改善既有程式碼的設計》
重構是在不改變軟體可觀察行為的前提下改善其內部結構。當你面對一個最需要重構的遺留系統時,其規模之大、歷史之久、程式碼品質之差,常會使得添加單元測試或者理解其邏輯都成為不可能的任務。此時你唯一能依靠的就是那些已經被證明是行為保持的重構手法: 用絕對安全的手法從焦油坑中整理出可測試的介面,給它添加測試,以此作為繼續重構的立足點。
因為我們部門內容平台的文章系統之前遺留了很多問題,急需解決這些具有"壞味道"的程式碼。最後因為其他人手頭裡都有其他工作,最後這些任務就交給了我。以下是急需解決的問題。
- 內容平台新增/更新/取消/刪除文章,同步各集團下文章行為狀態,消息鏈路過長的問題。
- article分享錶停止規模新增,之前未做插入前的記錄判斷,通過新增的操作來進行記錄留存。
- 文章表拆除大欄位到分表,如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
集合循環添加到資料庫的方法。- mapper層中創建
insertForeach
(List < Fund > list) 方法,返回值是批量添加的數據條數
public interface FundMapper { int insertForeach(List<Fund> list); }
- 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兩個欄位在索引中需要加索引
- mapper層中創建
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 兩張表。