当前位置: 首页 > news >正文

关于系统重构实践的一些思考与总结

文章目录

  • 一、前言
  • 二、系统重构的范式
    • 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…...

DeepSeek:智能时代的AI利器及其应用前景

1.DeepSeek是什么? DeepSeek是一款基于人工智能技术的工具,旨在帮助用户高效处理和分析数据、生成内容、优化工作流程等。无论是数据分析、自然语言处理,还是自动化任务,DeepSeek都能提供强大的支持。其核心技术涵盖了机器学习、深…...

超详细UE4(虚幻4)第一人称射击(FPS)游戏制作教程

超详细UE4(虚幻4)第一人称射击(FPS)游戏制作教程 引言 在游戏开发领域,第一人称射击(FPS)游戏一直是最受欢迎的类型之一。从经典的《反恐精英》(CS)到现代的《使命召唤》(Call of Duty),FPS游戏凭借其紧张刺激的游戏体验和高度沉浸感,吸引了无数玩家。如果你是一…...

电商项目高级篇09-检索服务

电商项目高级篇09-检索服务 1、环境搭建1.1、前端静态文件准备1.2、search服务引入模版引擎1.3、index.html页面复制到templates文件夹下1.4、模仿product项目,引入名称空间1.5、动静分离,静态资源路径位置替换1.6、将1.1的静态资源放到nginx目录下1.7、…...

【网络协议大花园】应用层 http协议的使用小技巧,用好了都不用加班,效率翻两倍(下篇)

本篇会加入个人的所谓鱼式疯言 ❤️❤️❤️鱼式疯言:❤️❤️❤️此疯言非彼疯言 而是理解过并总结出来通俗易懂的大白话, 小编会尽可能的在每个概念后插入鱼式疯言,帮助大家理解的. 🤭🤭🤭可能说的不是那么严谨.但小编初心是能让更多人…...

5 前端系统开发:Vue2、Vue3框架(中):Vue前端工程化组件式开发

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档 文章目录 前言五、前端工程化(使用Vue创建一个完整的企业级前端项目)1 Vue脚手架(Vue-cli)环境准备(1)…...

【Leetcode刷题记录】1456. 定长子串中元音的最大数目---定长滑动窗口即解题思路总结

1456. 定长子串中元音的最大数目 给你字符串 s 和整数 k 。请返回字符串 s 中长度为 k 的单个子字符串中可能包含的最大元音字母数。 英文中的 元音字母 为(a, e, i, o, u)。 这道题的暴力求解的思路是通过遍历字符串 s 的每一个长度为 k 的子串&#xf…...

Rust中使用ORM框架diesel报错问题

1 起初环境没有问题:在Rust开发的时候起初使用的是mingw64平台加stable-x86_64-pc-windows-gnu编译链,当使用到diesel时会报错,如下: x86_64-w64-mingw32/bin/ld.exe: cannot find -lmysql具体信息很长这是主要信息是rust找不到链…...

Java 数据库连接池:HikariCP 与 Druid 的对比

Java 数据库连接池:HikariCP 与 Druid 的对比 数据库连接池:HikariCP 1. 卓越的性能表现 HikariCP 在数据库连接池领域以其卓越的性能脱颖而出。 其字节码经过精心优化,减少了不必要的开销,使得连接获取和释放的速度极快。 在…...

04树 + 堆 + 优先队列 + 图(D1_树(D7_B+树(B+)))

目录 一、基本介绍 二、重要概念 非叶节点 叶节点 三、阶数 四、基本操作 等值查询(query) 范围查询(rangeQuery) 更新(update) 插入(insert) 删除(remove) 五、知识小结 一、基本介绍 B树是一种树数据结构,通常用于数据库和操作系统的文件系统中。 B树…...

MATLAB实现单层竞争神经网络数据分类

一.单层竞争神经网络介绍 单层竞争神经网络(Single-Layer Competitive Neural Network)是一种基于竞争学习的神经网络模型,主要用于数据分类和模式识别。其核心思想是通过神经元之间的竞争机制,使得网络能够自动学习输入数据的特…...

AITables首发:基于AI全自动推理设计数据库,国内首创,跑5分钟相当于架构师设计一周!

AITables仅运行5分钟,整个系统的表都设计好了! 随着AI大模型技术的开源普及和平民化,现如今任何一个人都有可能成为超级个体。但随着企业级业务的膨胀和业务挑战增多,我们势必要跟上AI发展的节奏,让AI帮我们设计软件。…...

Go语言中结构体字面量

结构体字面量(Struct Literal)是在 Go 语言中用于创建和初始化结构体实例的一种语法。它允许你在声明结构体变量的同时,直接为其字段赋值。结构体字面量提供了一种简洁、直观的方式来创建结构体对象。 结构体字面量有两种主要形式&#xff1…...

PaddleOCR 截图自动文字识别

春节假期在家无聊,撸了三个小工具:PC截图编辑/PC录屏(用于meeting录屏)/PC截屏文字识别。因为感觉这三个小工具是工作中常常需要用到的,github上也有很多开源的,不过总有点或多或少的小问题,不利于自己的使用。脚本的编…...

【Blazor学习笔记】.NET Blazor学习笔记

我是大标题 我学习Blazor的顺序是基于Blazor University,然后实际内容不完全基于它,因为它的例子还是基于.NET Core 3.1做的,距离现在很遥远了。 截至本文撰写的时间,2025年,最新的.NET是.NET9了都,可能1…...

UE求职Demo开发日志#21 背包-仓库-装备栏移动物品

1 创建一个枚举记录来源位置 UENUM(BlueprintType) enum class EMyItemLocation : uint8 {None0,Bag UMETA(DisplayName "Bag"),Armed UMETA(DisplayName "Armed"),WareHouse UMETA(DisplayName "WareHouse"), }; 2 创建一个BagPad和WarePa…...

力扣988. 从叶结点开始的最小字符串

Problem: 988. 从叶结点开始的最小字符串 文章目录 题目描述思路复杂度Code 题目描述 思路 遍历思想(利用二叉树的先序遍历) 在先序遍历的过程中,用一个变量path拼接记录下其组成的字符串,当遇到根节点时再将其反转并比较大小(字典顺序大小&…...

《PYTHON语言程序设计》(2018版)1.7近似π。利用步幅来进行修改

利用循环的步幅来计算派 利用正常的办法, pi 4 *(1-(1/3)(1/5)-(1/7)(1/9)-(1/11)(1/13)-(1/15)) print(pi)利用这段代码得出结果 我们如何利用循环来进行呢 一、思路 首先我们来利用excel表格来计算一下结果 我做了一个设想,让相加的部分先进行相加,然后再进行减法呢?? 结…...

低通滤波算法的数学原理和C语言实现

目录 概述 1 原理介绍 1. 1 基本概念 1.2 一阶RC低通滤波器模型 2 C语言完整实现 2.1 滤波器结构体定义 2.2 初始化函数 2.3 滤波计算函数 3 应用示例 3.1 噪声信号滤波 3.2 输出效果对比 3.3 关键参数选择指南 4 性能优化技巧 4.1 定点数优化 4.2 抗溢出处理 …...

【BUUCTF杂项题】荷兰宽带数据泄露、九连环

一.荷兰宽带数据泄露 打开发现是一个.bin为后缀的二进制文件,因为提示宽带数据泄露,考虑是宽带路由器方向的隐写 补充:大多数现代路由器都可以让您备份一个文件路由器的配置文件,软件RouterPassView可以读取这个路由配置文件。 用…...

Vim 调用外部命令学习笔记

Vim 外部命令集成完全指南 文章目录 Vim 外部命令集成完全指南核心概念理解命令语法解析语法对比 常用外部命令详解文本排序与去重文本筛选与搜索高级 grep 搜索技巧文本替换与编辑字符处理高级文本处理编程语言处理其他实用命令 范围操作示例指定行范围处理复合命令示例 实用技…...

剑指offer20_链表中环的入口节点

链表中环的入口节点 给定一个链表,若其中包含环,则输出环的入口节点。 若其中不包含环,则输出null。 数据范围 节点 val 值取值范围 [ 1 , 1000 ] [1,1000] [1,1000]。 节点 val 值各不相同。 链表长度 [ 0 , 500 ] [0,500] [0,500]。 …...

C++ 基础特性深度解析

目录 引言 一、命名空间(namespace) C 中的命名空间​ 与 C 语言的对比​ 二、缺省参数​ C 中的缺省参数​ 与 C 语言的对比​ 三、引用(reference)​ C 中的引用​ 与 C 语言的对比​ 四、inline(内联函数…...

《C++ 模板》

目录 函数模板 类模板 非类型模板参数 模板特化 函数模板特化 类模板的特化 模板,就像一个模具,里面可以将不同类型的材料做成一个形状,其分为函数模板和类模板。 函数模板 函数模板可以简化函数重载的代码。格式:templa…...

安全突围:重塑内生安全体系:齐向东在2025年BCS大会的演讲

文章目录 前言第一部分:体系力量是突围之钥第一重困境是体系思想落地不畅。第二重困境是大小体系融合瓶颈。第三重困境是“小体系”运营梗阻。 第二部分:体系矛盾是突围之障一是数据孤岛的障碍。二是投入不足的障碍。三是新旧兼容难的障碍。 第三部分&am…...

搭建DNS域名解析服务器(正向解析资源文件)

正向解析资源文件 1)准备工作 服务端及客户端都关闭安全软件 [rootlocalhost ~]# systemctl stop firewalld [rootlocalhost ~]# setenforce 0 2)服务端安装软件:bind 1.配置yum源 [rootlocalhost ~]# cat /etc/yum.repos.d/base.repo [Base…...

scikit-learn机器学习

# 同时添加如下代码, 这样每次环境(kernel)启动的时候只要运行下方代码即可: # Also add the following code, # so that every time the environment (kernel) starts, # just run the following code: import sys sys.path.append(/home/aistudio/external-libraries)机…...

MySQL:分区的基本使用

目录 一、什么是分区二、有什么作用三、分类四、创建分区五、删除分区 一、什么是分区 MySQL 分区(Partitioning)是一种将单张表的数据逻辑上拆分成多个物理部分的技术。这些物理部分(分区)可以独立存储、管理和优化,…...

【SpringBoot自动化部署】

SpringBoot自动化部署方法 使用Jenkins进行持续集成与部署 Jenkins是最常用的自动化部署工具之一,能够实现代码拉取、构建、测试和部署的全流程自动化。 配置Jenkins任务时,需要添加Git仓库地址和凭证,设置构建触发器(如GitHub…...

【Linux】Linux安装并配置RabbitMQ

目录 1. 安装 Erlang 2. 安装 RabbitMQ 2.1.添加 RabbitMQ 仓库 2.2.安装 RabbitMQ 3.配置 3.1.启动和管理服务 4. 访问管理界面 5.安装问题 6.修改密码 7.修改端口 7.1.找到文件 7.2.修改文件 1. 安装 Erlang 由于 RabbitMQ 是用 Erlang 编写的,需要先安…...