关于系统重构实践的一些思考与总结
文章目录
- 一、前言
- 二、系统重构的范式
- 1.明确目标和背景
- 2.兼容屏蔽对上层的影响
- 3.设计灰度迁移方案
- 3.1 灰度策略
- 3.2 灰度过程设计
- 3.2.1 case1 业务逻辑变更
- 3.2.2 case2 底层数据变更(数据平滑迁移)
- 3.2.3 case3 在途新旧流程兼容
- 3.2.4 case4 接口变更
- 3.2.5 case5 状态机兼容
- 三、系统重构过程中的风险控制
- 3.1 设计阶段
- 3.1.1 流量梳理
- 3.1.2 异常脏数据梳理
- 3.2 开发阶段
- 3.2.1 单元测试
- 3.2.2 代码CR
- 3.3 测试&预发线上回归阶段
- 3.3.1 线上线下流量回放&流量验证
- 3.3.2 核心主链路回归&异常case回归
- 3.3.3 BCP对账,反向验证数据结果
- 3.4 开城中
- 3.4.1 小批量验证
- 3.4.2 数据迁移过程中的反向校验sql
- 3.4.3 及时回滚
- 3.5 开城后
- 3.5.1 及时响应机制
- 3.5.2 BCP对账
- 3.6.开城后收尾
- 四、回顾与反思
一、前言
前段时间一个老的系统重构项目上线,在这个过程中出现了很多问题,遂想针对在这个过程中遇到的一些问题和实践,记录总结一套系统重构的范式,并回顾反思在这个过程是否有哪些值得改进的地方。
注:这里系统重构的定义,不仅是技术上的重构,更是产品逻辑上的重构。
二、系统重构的范式
系统的重构都需要考虑对原有系统的影响,不会另起一个项目,从头写到尾,还把上下游做相应的修改,这种做法虽然"彻底干净",但是成本太大,所以本文暂不讨论这种方案。
一般来说,老系统的重构都需要做到至少接口层面的兼容,这样可以保证系统的重构不会逸散到系统之外,对于上下游而言重构是无感的。
1.明确目标和背景
对于系统重构,需要最小化影响面,而这个前提就是需要我们明确我们的目标是什么,是字段迁移,还是业务模型的变动,亦或者是业务逻辑的变动。
- 底层存储变动?
- 数据模型变动?
- 业务逻辑变动?
- 迁移过程中是否不可用?

在这个阶段,需要梳理出你的改动对于整个项目的影响,比如影响到哪块业务逻辑,哪些接口。
如果是对整个项目大的重构,需要梳理现有系统的流量入口。
2.兼容屏蔽对上层的影响
核心思路就是尽量在最底层去屏蔽这些影响。
比如底层存储修改,那么是否能在dao层就去兼容,让上层的调用和之前一样。
不同语言有不同的技巧,核心就是方法增强,在原先逻辑上增加附加兼容逻辑。
对于Java而言,可以用切面、框架内注册处理器(如filter),亦或者重写get/set方法等等,
对于Go而言,也是类似,虽然没有切面,但也能利用结构体嵌入、接口和组合的方式实现类似的效果。
这样的好处就是
- 风险可控
- 修改成本低
坏处也有:
- 逻辑里夹杂着特殊逻辑,代码复杂度会提升
3.设计灰度迁移方案
系统重构必然存在新老系统切换的过程,尤其是如果需要灰度验证的话,那么新老系统切换的过程会很长,我们就需要考虑新老系统同时存在的影响。
3.1 灰度策略
系统重构往往需要灰度验证,出发点往往有以下几种:
- 控制影响面
小范围验证,避免新系统的问题对业务的影响过大
- 业务诉求
这点在B端业务里尤其常见,例如每开一些城市前,业务同学都需要和商户侧有对应的宣贯,周知到商户
灰度的策略也有很多,常见的有
by城市、by商户、by流量等,
这个决于业务倾向,这里面着重需要考虑的点在于
- 选用这种灰度是否会影响数据不一致
- 是否对业务有影响
常见的B端业务主要都是by城市维度去进行开城。
3.2 灰度过程设计
3.2.1 case1 业务逻辑变更
对于业务逻辑变更的场景,比较简单,我们往往是封装对应的开关逻辑,命中则走新逻辑,否则走老逻辑。

需要注意的就是在代码中的组织尽量封装成方法/函数,方便后续灰度全量时进行无用代码删除,降低代码复杂度。
3.2.2 case2 底层数据变更(数据平滑迁移)
这里的变更可能是同一张数据表字段使用迁移(典型场景就是敏感字段加密),也可能是不同表的数据。
双写方案

1.开启双写(老->新)
目的:让增量的数据也能同步更新到的新数据,
2.数据迁移脚本
目的:让存量的数据刷到新的数据中
经历第二步骤后,旧数据已经全量同步到新数据中。
3.开城(切读&写)
开启开城开关,流量入口开始从旧逻辑切换到新逻辑,新逻辑会读&写新数据,同时会双写老数据
双写老数据目的:切换开关后出现的数据也能同步老数据,如果出现问题,回滚开关时可以实现无损回滚
4.关闭双写
目的:双写毕竟还是冗余操作,当全量开城时,我们需要将这部分逻辑给去了,避免影响系统性能
5.下线老逻辑代码
目的:减少无用代码,降低代码复杂度
以上是较为完整的迁移过程,实际上可以根据自己的实际场景对步骤做一定程度上的简化。
比如我是典型的B端业务,实际迁移过程无需那么平滑,甚至允许一定时间内停止对外提供服务(比如停服维护),那么我们可以无需第一步的双写(老->新),只需在开城后再增量刷一遍数据即可。

3.2.3 case3 在途新旧流程兼容
旧系统中有一些流程结束并不是简单的通过开关就能进行新旧逻辑的切换,
比如B端的审批流,我们就需要考虑在切换新系统后,兼容老的审批回调。
这里主要有两种思路:
- 老回调逻辑生效,双写新逻辑
- 老回调逻辑失效,新逻辑重新推审
具体方案得根据具体业务而定。
3.2.4 case4 接口变更
原有接口如果无法承载新业务逻辑的交互,如果不能确定影响范围,那么只能在原有请求字段中新增字段,不允许修改和删减,保证向后兼容。
有一种case比较特殊,就是专门给前端交互用的接口,如果新的业务形态下,页面的交互已经变化,那么可以废弃原来的接口,另起接口去支持新的功能交互。
3.2.5 case5 状态机兼容
状态机变化需要明确新老状态之间的映射。其本质就是case2底层数据变更的思路。
三、系统重构过程中的风险控制
3.1 设计阶段
3.1.1 流量梳理
在系统重构,尤其是老系统重构的过程中,流量梳理是设计阶段必须要做的一件事情。
因为老系统往往存在大量无用逻辑,以及各种各样的复杂业务接口。在设计之前我们必须要梳理出当前系统对外部开放在使用的能力。
这里列举一些常见的流量入口:
- http(VIP、域名)
- rpc(如thrift&grpc、dubbo、kepler等)
有了范围,我们接下来做的就是考虑我们本次修改可能会对这些接口造成哪些影响。
3.1.2 异常脏数据梳理
线上脏数据梳理是非常容易被我们忽视的,而这造成的结果往往就是数据迁移/开城后会出现一些特殊case,严重的会引发事故。
在我们不熟悉旧系统迭代的情况下,我们很难考虑到线上数据有多少是在”我们意料的规则之外“的。
”我以为这个数据是这样的,可是线上有脏数据导致…“
”啊,还有数据是这样的?“
这往往是我们事后才发现的问题。
我们应该把这块前置,在设计阶段就要梳理出这个表/数据的用法,以及是否存在一些出乎我们意料的数据,尤其要多对比新旧逻辑下,这些数据可能造成的影响。
也许这个脏数据在老系统规则下能正常运行,但是在新系统规则下它就是会出问题!
我们需要多写sql,去统计当前线上库的数据分布,并去分析新旧逻辑下的影响。
3.2 开发阶段
3.2.1 单元测试
好的单测是可复用的,拥有完整的mock和断言能力。
系统重构设计修改的方法最好是要有对应的单测case。
3.2.2 代码CR
代码CR可以规避部分因代码bug引发的错误。
但是实际项目开发中,因为排期紧、代码改动量过大等原因,大型系统重构的实践效果往往不是很理想。
3.3 测试&预发线上回归阶段
3.3.1 线上线下流量回放&流量验证
流量回放是用于验证老接口是否发生变化的最好工具。
其核心是用线上的流量到线下测试环境进行流量验证,对比前后差异这里依赖基础设施的建设。
如果没有完善的流量回放工具,则可以写个脚本,用来对比流量的接口。
不过对于缺失的mock能力,可以通过copy线上的数据到线下来进行模拟。
3.3.2 核心主链路回归&异常case回归
核心主链路必须要回归,避免新系统对线上核心流程造成影响
3.3.3 BCP对账,反向验证数据结果
针对核心业务流程,以及数据迁移脚本中的数据对比,都可以用BCP对账来进行反向校验。
这里主要针对一些核心的流程去写对账,对账逻辑可以依据一些特定的数据规则来写,从结果层面去验证数据的正确性。
常见实现的方式有:
- binlog+sql
- binlog+延迟mq+sql
- 定时任务+hive
通过这样写反向的sql校验,可以低成本的去对结果进行兜底,及时感知到异常case。
需要注意的是,这种BCP对账的sql要注意sql耗时时间,虽然是离线库,但是性能太差也是不可接受的,运行时间过长的建议采用定时任务+hive表的方式。
3.4 开城中
3.4.1 小批量验证
正式开城时,我们必须要有小规模验证的过程。
比如数据迁移时,我针对单个商户进行迁移,然后回归具体的case,如果没有问题再逐步放量。
3.4.2 数据迁移过程中的反向校验sql
数据迁移脚本运行后,我们如何确定我们的执行结果是正确的,除了打印必要的日志和结果通知,还有准备对应的反向校验的sql,来验证我本次刷数是符合预期的。
BCP的报警其实也是同理(不过在大批量数据变更的情况下,BCP报警可能会出现延迟)。
3.4.3 及时回滚
如果出现问题及时回归,按照预期设定的回滚预案进行回滚。
具体的回滚策略如下:
- 代码回滚
- 灰度开关
- 数据库刷数回退(有的公司可能会支持这种能力)
3.5 开城后
3.5.1 及时响应机制
开城后,可以建立业产研开城群,方便开城后问题的沟通,及时跟进和止损。
3.5.2 BCP对账
同前面,略
3.6.开城后收尾
多次的重构,会留下很多无用代码,徒增代码复杂度,良好的习惯就是在新系统全部开城后,删除无用代码。
四、回顾与反思
真实项目中的挑战远不止这些,还有诸如对老系统不熟悉,排期压力大等挑战,这些buff叠加进而导致老系统重构中的风险不可控。
在最近的重构需求中,我大致统计了下本次重构记录的19个线上问题,其原因分布如下
- 未考虑到的线上脏数据 4
- 代码bug漏出 13
- 产品逻辑&技术方案问题 2
主要原因:
- 旧系统逻辑产研了解都较少,这个虽然不是直接原因,但确实很大程度影响了项目风险控制
- 无一个有效的流量回放&流量对比的工具来控制影响面
- 测试阶段回归不全面,导致bug漏出
- 设计阶段缺少统计线上数据分布,忽略了”脏数据“在新系统逻辑下的影响
大家说的脏数据,其实不单单是在原先系统中的不合规/不该存在的数据,也有一些是因为历史迭代而产生的”正常数据“,这些数据可能在老系统运行正常,但是在新逻辑下就不一定适用
对于及时感知线上问题方面,其中BCP报警起了不可或缺的作用,上述问题中有7个是通过BCP报警发现,由此可见BCP对账对于系统重构的重要性。
相关文章:
关于系统重构实践的一些思考与总结
文章目录 一、前言二、系统重构的范式1.明确目标和背景2.兼容屏蔽对上层的影响3.设计灰度迁移方案3.1 灰度策略3.2 灰度过程设计3.2.1 case1 业务逻辑变更3.2.2 case2 底层数据变更(数据平滑迁移)3.2.3 case3 在途新旧流程兼容3.2.4 case4 接口变更3.2.5…...
电介质超表面中指定涡旋的非线性生成
涡旋光束在众多领域具有重要应用,但传统光学器件产生涡旋光束的方式限制了其在集成系统中的应用。超表面的出现为涡旋光束的产生带来了新的可能性,尤其是在非线性领域,尽管近些年来已经有一些研究,但仍存在诸多问题,如…...
学习日记-250202
现在开始要继续写我的日记了......(也可以当作笔记吧) 一.论文 Prompt Transfer for Dual-Aspect Cross Domain Cognitive Diagnosis 主要内容: 主要是加入prompt提示, 为重叠实体设计个性化的提示,为非重叠实体设计共…...
pytorch实现简单的情感分析算法
人工智能例子汇总:AI常见的算法和例子-CSDN博客 在PyTorch中实现中文情感分析算法通常涉及以下几个步骤:数据预处理、模型定义、训练和评估。下面是一个简单的实现示例,使用LSTM模型进行中文情感分析。 1. 数据预处理 首先,我…...
【Rust自学】16.3. 共享状态的并发
喜欢的话别忘了点赞、收藏加关注哦,对接下来的教程有兴趣的可以关注专栏。谢谢喵!(・ω・) 16.3.1. 使用共享来实现并发 还记得Go语言有一句名言是这么说的:Do not communicate by sharing memory; instead, share me…...
git 新项目
新项目git 新建的项目如何进行git 配置git git config --global user.name "cc" git config --global user.email ccexample.com配置远程仓库路径 // 添加 git remote add origin http://gogs/cc/mc.git //如果配错了,删除 git remote remove origin初…...
【LeetCode 刷题】回溯算法-子集问题
此博客为《代码随想录》二叉树章节的学习笔记,主要内容为回溯算法子集问题相关的题目解析。 文章目录 78.子集90.子集II 78.子集 题目链接 class Solution:def subsets(self, nums: List[int]) -> List[List[int]]:res, path [], []def dfs(start: int) ->…...
LLMs之DeepSeek:Math-To-Manim的简介(包括DeepSeek R1-Zero的详解)、安装和使用方法、案例应用之详细攻略
LLMs之DeepSeek:Math-To-Manim的简介(包括DeepSeek R1-Zero的详解)、安装和使用方法、案例应用之详细攻略 目录 Math-To-Manim的简介 1、特点 2、一个空间推理测试—考察不同大型语言模型如何解释和可视化空间关系 3、DeepSeek R1-Zero的简介:处理更…...
2025年2月2日(网络编程 tcp)
tcp 循环服务 import socketdef main():# 创建 socket# 绑定tcp_server socket.socket(socket.AF_INET, socket.SOCK_STREAM)tcp_server.bind(("", 8080))# socket 转变为被动tcp_server.listen(128)while True:# 产生专门为链接进来的客户端服务的 socketprint(&qu…...
WSL2中安装的ubuntu搭建tftp服务器uboot通过tftp下载
Windows中安装wsl2,wsl2里安装ubuntu。 1. Wsl启动后 1)Windows下ip ipconfig 以太网适配器 vEthernet (WSL (Hyper-V firewall)): 连接特定的 DNS 后缀 . . . . . . . : IPv4 地址 . . . . . . . . . . . . : 172.19.32.1 子网掩码 . . . . . . . .…...
C#从XmlDocument提取完整字符串
方法1:通过XmlDocument的OuterXml属性,见XmlDocument类 该方法获得的xml字符串是不带格式的,可读性差 方法2:利用XmlWriterSettings控制格式等一系列参数,见XmlWriterSettings类 例子: using System.IO; …...
Ubuntu 下 nginx-1.24.0 源码分析 main函数 — ngx_cdecl 宏
ngx_cdecl 宏 int ngx_cdecl main(int argc, char *const *argv) ngx_cdecl 定义在: ngx_config.h 中: #define ngx_cdecl 这里是一个空的 define 参考: nginx中的ngx_cdecl-CSDN博客 __cdecl 是一种调用约定(Calling Con…...
2025-工具集合整理
科技趋势 github-rank 🕷️Github China/Global User Ranking, Global Warehouse Star Ranking (Github Action is automatically updated daily). 科技爱好者周刊 制图工具 D2 D2 A modern diagram scripting language that turns text to diagrams 文档帮助 …...
OpenAI 实战进阶教程 - 第一节:OpenAI API 架构与基础调用
目标 掌握 OpenAI API 的基础调用方法。理解如何通过 API 进行内容生成。使用实际应用场景帮助零基础读者理解 API 的基本用法。 一、什么是 OpenAI API? OpenAI API 是一种工具,允许开发者通过编程方式与 OpenAI 的强大语言模型(例如 gpt-…...
Alibaba开发规范_编程规约之集合框架:最佳实践与常见陷阱
文章目录 引言1. hashCode与equals方法的覆写1.1 规则1.2 解释1.3 代码示例正例反例 2. ArrayList的subList方法2.1 规则2.2 解释2.3 代码示例正例反例 3. Map的keySet、values和entrySet方法3.1 规则3.2 解释3.3 代码示例正例反例 4. Collections类返回的不可变集合4.1 规则4.…...
NPM 使用介绍
NPM 使用介绍 引言 NPM(Node Package Manager)是Node.js生态系统中的一个核心工具,用于管理JavaScript项目的依赖包。无论是开发一个小型脚本还是构建大型应用程序,NPM都能极大地提高开发效率。本文将详细介绍NPM的使用方法,包括安装、配置、依赖管理、包发布等,帮助您…...
小红的小球染色期望
B-小红的小球染色_牛客周赛 Round 79 题目描述 本题与《F.R小红的小球染色期望》共享题目背景,但是所求内容与范围均不同,我们建议您重新阅读题面。 有 n 个白色小球排成一排。小红每次将随机选择两个相邻的白色小球,将它们染成红色。小红…...
基于SpringBoot的新闻资讯系统的设计与实现(源码+SQL脚本+LW+部署讲解等)
专注于大学生项目实战开发,讲解,毕业答疑辅导,欢迎高校老师/同行前辈交流合作✌。 技术范围:SpringBoot、Vue、SSM、HLMT、小程序、Jsp、PHP、Nodejs、Python、爬虫、数据可视化、安卓app、大数据、物联网、机器学习等设计与开发。 主要内容:…...
计算机网络——流量控制
流量控制的基本方法是确保发送方不会以超过接收方处理能力的速度发送数据包。 通常的做法是接收方会向发送方提供某种反馈,如: (1)停止&等待 在任何时候只有一个数据包在传输,发送方发送一个数据包,…...
基于python的Kimi AI 聊天应用
因为这几天deepseek有点状况,导致apikey一直生成不了,用kimi练练手。这是一个基于 Moonshot AI 的 Kimi 接口开发的聊天应用程序,使用 Python Tkinter 构建图形界面。 项目结构 项目由三个主要Python文件组成: 1. main_kimi.py…...
2 [GitHub遭遇严重供应链投毒攻击]
近日,有黑客针对 Discord Top.gg 的GitHub 账户发起了供应链攻击,此次攻击导致账户密码、凭证和其他敏感信息被盗,同时也影响到了大量开发人员。 Checkmarx 在一份技术报告中提到,黑客在这次攻击中使用了多种TTP,其中…...
C++游戏开发实战:从引擎架构到物理碰撞
📝个人主页🌹:一ge科研小菜鸡-CSDN博客 🌹🌹期待您的关注 🌹🌹 1. 引言 C 是游戏开发中最受欢迎的编程语言之一,因其高性能、低延迟和强大的底层控制能力,被广泛用于游戏…...
代码讲解系列-CV(一)——CV基础框架
文章目录 一、环境配置IDE选择一套完整复现安装自定义cuda算子 二、Linux基础文件和目录操作查看显卡状态压缩和解压 三、常用工具和pipeline远程文件工具版本管理代码辅助工具 随手记录下一个晚课 一、环境配置 pytorch是AI框架用的很多,或者 其他是国内的框架 an…...
【前端知识】常用CSS样式举例
文章目录 一、Flex盒子布局1. Flexbox 的基本概念2. Flex 容器的属性2.1 display2.2 flex-direction2.3 flex-wrap2.4 justify-content2.5 align-items2.6 align-content 3. Flex 项目的属性3.1 order3.2 flex-grow3.3 flex-shrink3.4 flex-basis3.5 flex3.6 align-self 4. 示例…...
P_all: 投影矩阵(Projection Matrix)
P_all 是所有摄像头的投影矩阵(Projection Matrix)的集合。每个摄像头的投影矩阵 Pi 是一个 34 的矩阵,用于将世界坐标系中的 3D 点 X[X,Y,Z,1]T 投影到该摄像头的 2D 图像平面上的点 u[u,v,1]T。投影关系可以表示为: uPiX 其中…...
机器学习--概览
一、机器学习基础概念 1. 定义 机器学习(Machine Learning, ML):通过算法让计算机从数据中自动学习规律,并利用学习到的模型进行预测或决策,而无需显式编程。 2. 与编程的区别 传统编程机器学习输入:规…...
Python算法详解:贪心算法
贪心算法(Greedy Algorithm)是一种通过选择当前最优解以期望达到全局最优解的算法思想。它在每一步选择时只考虑当前状态下的局部最优,而不关心全局问题的复杂性。这种算法简单高效,适用于某些特定问题,尤其是存在贪心…...
gesp(C++六级)(10)洛谷:P10722:[GESP202406 六级] 二叉树
gesp(C六级)(10)洛谷:P10722:[GESP202406 六级] 二叉树 题目描述 小杨有⼀棵包含 n n n 个节点的二叉树,且根节点的编号为 1 1 1。这棵二叉树任意⼀个节点要么是白色,要么是黑色。之后小杨会对这棵二叉树…...
7.DP算法
DP 在C中,动态规划(Dynamic Programming,DP)是一种通过将复杂问题分解为重叠子问题来高效求解的算法设计范式。以下是DP算法的核心要点和实现方法: 一、动态规划的核心思想 重叠子问题:问题可分解为多个重…...
2025年2月2日(tcp3次握手4次挥手)
TCP(三次握手和四次挥手)是建立和关闭网络连接的标准过程,确保数据在传输过程中可靠无误。下面是详细解释: 1. 三次握手(TCP连接建立过程) 三次握手是为了在客户端和服务器之间建立一个可靠的连接&#x…...
