前言
引用自: 《重构 改善既有代码的设计》
重构是在不改变软件可观察行为的前提下改善其内部结构. 当你面对一个最需要重构的遗留系统时, 其规模之大, 历史之久, 代码质量之差, 常会使得添加单元测试或者理解其逻辑都成为不可能的任务. 此时你唯一能依靠的就是那些已经被证明是行为保持的重构手法: 用绝对安全的手法从焦油坑中整理出可测试的接口, 给它添加测试, 以此作为继续重构的立足点.
因为我们部门内容平台的文章系统之前遗留了很多问题, 急需解决这些具有 "坏味道" 的代码. 最后因为其他人手头里都有其他工作, 最后这些任务就交给了我. 以下是急需解决的问题.
内容平台新增 / 更新 / 取消 / 删除文章, 同步各集团下文章行为状态, 消息链路过长的问题.
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: 第一条更新失败的原因
}
调用示例:
- Map<String, Object> data = new HashMap<>();
- data.put("id", 20);
- data.put("title", "xue yin");
- data.put("content", "kuang dao");
- 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 两张表.
表拆分图示
来源: https://www.cnblogs.com/sanshengshui/p/11854813.html