干货!如何平稳用户无感知的完成系统重构升级

{"type":"doc","content":[{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"前言"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我们在实际开发系统的过程当中,很有可能会遇到需要进行系统重构升级的情况,需要重构的原因可能是之前的设计不合理,导致现在维护起来非常的困难,也有可能是现在的业务发展非常迅速,需要进行分库分表了又或者之前用的是单机的本地的文件存储,现在需要用到统一的网络存储。总而言之,就是当初的系统设计已经不符合现在发展需要了,需要进行重构和升级。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"而这其中会可能会涉及到代码逻辑的变更,数据存储的变更(如DB或者文件存储等)或者第三方接口的变更。在这样一个新旧的切换过程当中,怎么样才能让用户无感知,平稳地进行过渡?"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"有人说可能说可以停服,然后迁数据,迁完后切新逻辑,然而先不说会有一段不可接受的不可用时间,就说在迁移过程中,我们如何保证能一次迁移成功呢?再退一步,就算数据迁移成功了,但是如果代码逻辑有漏洞,我们又该如何快速回退到旧版本呢?这可不单单是切回旧代码就好了,要知道这段时间可能产生了新版本的数据,这些新数据可也要迁回旧版本。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"重构升级系统的过程可能会遇到这么多问题,那我们有什么办法可以平稳且用户无感知地完成系统升级吗?今天就给大家提供一个通用的系统重构升级的框架。里面很多具体的逻辑得按不同系统的实际情况来,但是整体思路却是通用且可靠的。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"场景模拟"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我们先来模拟一个简单的场景,并看看实际情况中应该如何操作。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"假设我们一开始有个"},{"type":"codeinline","content":[{"type":"text","text":"users"}]},{"type":"text","text":"表存储学生数据,表结构以及一些数据如下:"}]},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/a1/a1a461be332fb3459b1886562d71a7ef.png","alt":null,"title":"","style":[{"key":"width","value":"100%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"后面随着业务发展,我们需要记录学生的语文成绩,然后我们在"},{"type":"codeinline","content":[{"type":"text","text":"users"}]},{"type":"text","text":"表加了"},{"type":"codeinline","content":[{"type":"text","text":"score"}]},{"type":"text","text":"字段,如下"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/2e/2e78c08a3b8b455e2e310773f477db78.png","alt":null,"title":null,"style":null,"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"过一段时间我们发现又需要记录数学分数了,后面还可能需要记录英语分数等等。这时候不可能一次次加字段,现有的表设计又极不满足我们的需求,所以只好对现在的系统进行重构升级了。我们想用两个表来存数据:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"students"}]},{"type":"text","text":"表"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/44/44df31b522f08f34721c0d7c25850ed7.png","alt":null,"title":"","style":[{"key":"width","value":"100%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"mark表"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https://static001.geekbang.org/infoq/29/299b6c950e8f65e11bf80383e97e23cb.png","alt":null,"title":"","style":[{"key":"width","value":"100%"},{"key":"bordertype","value":"none"}],"href":"","fromPaste":false,"pastePass":false}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"这时候我们会面临几个问题:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"代码逻辑的切换:包括增删改查"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"表结构的变更"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"数据的迁移"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"迁移过程中用户无感知"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如何来升级呢?"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"步骤"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"numberedlist","attrs":{"start":"1","normalizeStart":1},"content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":1,"align":null,"origin":null},"content":[{"type":"text","text":"在旧代码的增删改查的地方写好新逻辑和建好新的表,但是一开始线上并不调用新逻辑和写入新表,仅仅在测试环境调用和写入,线上仍调用旧逻辑。如:"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" ```"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" if($is_dev){"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" //新逻辑:如增删改查students表和mark表"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" }else{"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" //旧逻辑:如增删改查users表"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" }"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" ```"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"numberedlist","attrs":{"start":"2","normalizeStart":"2"},"content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":2,"align":null,"origin":null},"content":[{"type":"text","text":"测试新逻辑没问题了,线上同时"},{"type":"text","marks":[{"type":"strong"}],"text":"双写"},{"type":"text","text":"新旧表(包括增删改),如:"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" ```php"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" //新写入逻辑:如增删改students表和mark表"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" //旧写入逻辑:如增删改users表"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" if($is_dev){"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" //新读取逻辑:如查students表和mark表"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" }else{"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" //旧读取逻辑:如查users表"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" }"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" ```"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"numberedlist","attrs":{"start":"3","normalizeStart":"3"},"content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":3,"align":null,"origin":null},"content":[{"type":"text","text":"进行数据迁移,把原来"},{"type":"codeinline","content":[{"type":"text","text":"users"}]},{"type":"text","text":"表的数据迁到"},{"type":"codeinline","content":[{"type":"text","text":"students"}]},{"type":"text","text":"表和"},{"type":"codeinline","content":[{"type":"text","text":"mark"}]},{"type":"text","text":"表"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"numberedlist","attrs":{"start":"4","normalizeStart":"4"},"content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":4,"align":null,"origin":null},"content":[{"type":"text","text":"然后让系统运行一段时间,然后再对"},{"type":"codeinline","content":[{"type":"text","text":"users"}]},{"type":"text","text":"表和"},{"type":"codeinline","content":[{"type":"text","text":"students"}]},{"type":"text","text":"表、"},{"type":"codeinline","content":[{"type":"text","text":"mark"}]},{"type":"text","text":"表的数据进行"},{"type":"text","marks":[{"type":"strong"}],"text":"对账"},{"type":"text","text":",如果有数据不一致的情况,说明我们之前双写的时候有遗漏的地方,需要补全,如果没有不一致,说明我们写入的地方都已经对齐了,现在新旧数据是已经能一直保持一致了,那下面就是切读的地方了。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"numberedlist","attrs":{"start":"5","normalizeStart":"5"},"content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":5,"align":null,"origin":null},"content":[{"type":"text","text":"把读的地方改成只读新的,如下"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" ```php"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" //新写入逻辑:如增删改students表和mark表"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" //旧写入逻辑:如增删改users表"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" //新读取逻辑:如查students表和mark表"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" ```"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"numberedlist","attrs":{"start":"6","normalizeStart":"6"},"content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":6,"align":null,"origin":null},"content":[{"type":"text","text":"系统运行一段时间,没发现问题之后把写的改成只写新的,如下"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" ```php"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" //新写入逻辑:如增删改students表和mark表"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" //新读取逻辑:如查students表和mark表"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":" ```"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"完成之后我们的系统就平稳的完成迁移了。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"分析"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"整个过程可能看起来很繁琐,没关系,我们一步一步来分析其必要性。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"第一步是先写好新逻辑,并进行充分的测试,这当然是必要的啦,算是升级前的准备"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"第二步是我们升级的起始步骤了,我们需要双写数据。在代码发布过程中,即使有的访问到了新代码双写了,有的还是访问旧代码单写也没关系,因为会有第三步数据迁移的过程。这一步和第三步是为了保证数据无感知迁移(不用停服迁移数据),保证迁移后数据的一致性。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"第四步是对账过程,是为了找出我们写入遗漏的地方,这是因为我们不能保证对于一个庞大的系统,你一次改造就能改到了所有的地方,所以留一段时间对写入进行对账是非常有必要的,有则改之。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"上面的第四步保证了我们新旧的数据已经是一致的了,这时候第五步我们就可以很放心的把我们读从旧的地方改到读新的地方了。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"第六步也是很重要的,我们要先让系统平稳运行一段时间再切成单读新表,因为这个过程中,如果我们发现系统新逻辑有问题,我们可以很多地切回读旧逻辑,因为我们写入还是双写的,旧数据还是有写入,直接切回去是没有问题的。这就避免了无法回滚,或者回滚后数据丢失的问题。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"注意第五步和第六步最好分开进行,也就是说不要一次就改单读和单写,不然在发布代码的过程中,某些机器还是旧代码,这是用户访问系统的时候可能先访问到新机器写入了数据,然后又访问到旧机器读的是旧表,这时候就会读不到刚写入的数据。这种临界情况虽然极端,但是在大访问量的基数还是可能出现的"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"总结"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"可以看到,上面的系统升级重构的思路是比较细致的,但是确实是非常平稳,且不需要停服就能完成升级,即使系统非常的复杂,升级重构的逻辑和存储结构大变样也能适用。当然在实际过程中大家也可以根据实际情况(小系统小改动)进行一些步骤的合并或者缩短时间。"}]}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章