当前位置: 首页 > 科技观察

一种灰度界面迁移解决方案_1

时间:2023-03-17 17:54:49 科技观察

在互联网快速迭代的背景下,系统为了实现快速上线,往往会选择最快的开发模式,比如我们常见的mvp版本迭代。大部分业务系统对于未来的业务发展都是不确定的,所以随着时间的推移,往往会遇到各种瓶颈,比如系统性能、无法适配业务逻辑等,这时候可能涉及到系统架构的升级.系统升级往往包括两个基本部分:接口迁移重构和数据迁移重构。在系统架构升级的过程中,最重要的是保证系统的稳定性,即用户感知不到。因此,本文的目的是提供一种可以灰显回滚的设计思路,实现架构的稳定升级。场景在我们系统的迭代过程中,经常会涉及到重构、数据源切换、接口迁移等场景。为了保证系统的顺利上线,接口迁移过程中要保证回滚和灰度。接口迁移还可能涉及数据迁移,两者的先后顺序不应影响系统的稳定性。综上所述,接口迁移的目标是灰度化,即新旧接口的使用是可控的。它可以回滚。如果在使用新界面时出现异常,可以快速回滚到旧界面。不侵入业务逻辑,不改动原有业务逻辑代码,迁移完成后下线,防止直接入侵修改造成不可逆的影响。系统运行流畅后关闭老界面,即访问老数据源,老界面可以顺利下线迁移方案本文主要提供界面迁移和数据迁移的思路,会有第3条守则中的核心实践。(代码只提供思路,不提供可以直接运行的代码)1.整体迁移方案下图是接口迁移的思路,参考了cglib的jdk代理方式。假设你有一个接口类(目标类)需要迁移,那么你需要重写一个代理类作为被迁移的接口。目标类和代理类的选择由一个开关控制,涉及两个层面:master开关:用于控制是否全面切换到新接口,当接口迁移稳定上线,数据迁移完成时(ifany),thegrayscaleswitch:可以设置一个灰度开关列表,用于控制哪些接口/数据需要通过代理接口。对于不同的接口逻辑,代理接口的实现逻辑也会不同。具体场景如下所述。2.单条数据查询对于单条数据,可以通过数据源来判断来源。基于灰度和回滚的原理,目标类和代理类的路由规则如下:首先判断主控开关,如果主控开关打开,则表示迁移完成,验证通过并完成验证。这时候就用到了代理接口。可以实现接口和数据的封闭,达到我们迁移的目的。如果旧数据表中不存在该数据,则无论新表中是否存在该数据,我们都可以直接使用代理接口采集新数据的接口逻辑。如果数据存在于旧数据表中,但不存在于灰名单中,则此时使用目标类(回滚时可以这样做),使用原来的接口方法,即旧逻辑,会不影响系统功能。如果数据存在于旧数据表中,但在灰度列表中,则说明该数据已经迁移,需要验证。这时候就可以使用代理类(灰度可以做到)来实现新的接口逻辑。3、多条数据查询不同于单条数据查询。我们需要查询新表和旧表中所有符合条件的数据。多次数据查询涉及到数据重复的问题(即旧表和新表都会存在数据),所以需要对数据进行去重,然后合并返回结果。4、数据更新因为数据迁移到系统灰度的过程中有一个中间时间,所以我们在更新数据的时候应该使用双写来保持新旧表数据的一致性。同时,要关闭界面和数据,首先要判断总控开关是否打开。如果总开关打开,数据更新只需要更新新表即可。5.数据插入关闭数据和接口,我们需要切换增量数据,所以直接使用代理类,将数据插入新表,控制旧表的数据增量,迁移时只需要考虑存量数据数据。在实践中,比如在零售场景中,每个店铺都有一个唯一标识的storeid,那么我们的灰度列表可以存储storeid的列表,通过storedimension进行灰度化,对影响范围进行粒度化。1.代理分发逻辑分发逻辑是核心逻辑。重复数据删除规则和接口/存储层代理转发都是基于这套逻辑来控制的:首先确定主开关,当主开关打开时,迁移完成。这时候,都是通过代理类来获取新的接口逻辑和数据源。判断灰度切换,如果灰度存储包含在灰度进程中,则通过代理类使用新的接口;否则,使用原界面的旧逻辑实现界面切换。新的数据转发给代理类,关闭新的逻辑和数据,防止产生增量数据。批量查询接口需要转发给代理类,因为涉及到新旧数据去重合并的过程。/***是否启用代理**@paramctxcontext*@return是:启用代理,否:禁用代理*/publicBooleanenableProxy(ProxyEnableContextctx){if(ctx==null){returnfalse;}//判断主开关if(主开关打开){//表示数据迁移完成,所有接口都切换了returntrue;}if(单店操作){if(有旧数据源){//判断是否在灰名单中,如果是,则返回true;否则返回假;}else{//新数据返回true;}}else{//批量查询,需要通过代理合并新旧数据源returntrue;}}2。接口代理接口代理主要是通过aspects拦截,通过注解方法来实现。代理注解如下@Target({ElementType.METHOD})@Retention(RetentionPolicy.RUNTIME)public@interfaceEnableProxy{//用于标识代理类ClassproxyClass();//用于标识转发代理类的方法,默认取目标类的方法名StringmethodName()default"";//对于单条数据的查询,可以指定key的参数索引位置,解析后转发intkeyIndex()default-1;}切面的实现核心逻辑是拦截注解,并根据代理分发逻辑判断是否使用代理类。如果使用代理类,需要解析代理类型、方法名、参数,然后转发。@Component@Aspect@Slf4jpublicclassProxyAspect{//核心代理类@ResourceprivateProxyManagerproxyManager;//注解拦截@Pointcut("@annotation(***)")privatevoidproxy(){}@Around("proxy()")@SuppressWarnings("rawtypes")publicObjectaround(ProceedingJoinPointjoinPoint)throwsThrowable{尝试{MethodSignaturemethodSignature=(MethodSignature)joinPoint.getSignature();类clazz=joinPoint.getTarget().getClass();StringmethodName=methodSignature.getMethod().getName();Class[]parameterTypes=methodSignature.getParameterTypes();对象[]args=joinPoint.getArgs();//获取方法注解EnableProxyenableProxyAnnotation=ReflectUtils.getMethodAnnotation(clazz,EnableProxy.class,methodName,parameterTypes);if(enableProxyAnnotation==null){//没有找到注解,直接放手returnjoinPoint.proc需要();}//判断是否使用代理BooleanenableProxy=enableProxy(clazz,methodName,args,enableProxyAnnotation);if(!enableProxy){//不开启代理,直接放开returnjoinPoint.proceed();}//默认取目标类的方法名。methodName=StringUtils.isNotBlank(enableProxyAnnotation.methodName())?());方法proxyMethod=ReflectUtils.getMethod(enableProxyAnnotation.proxyClass(),methodName,parameterTypes);if(bean==null||proxyMethod==null){//没有代理类和代理方法,直接走原逻辑returnjoinPoint.proceed();}//通过反射,转发代理类方法returnReflectUtils.invoke(bean,proxyMethod,joinPoint.getArgs());}赶上(BizExceptiononbizException){//业务方法异常,直接抛出throwbizException;}catch(Throwablethrowable){//其他异常,做日志感知throwthrowable;}}}3。然后将逻辑转发给ProxyManager,由代理类管理器负责数据分发、去重、合并、更新、插入等操作。切面拦截它,切面判断是否使用代理接口。如果不需要代理接口(即数据源是旧的,没有灰度化),继续使用目标接口。如果需要代理接口(即数据源是新旧数据迁移后灰度列表),调用代理接口方法,存储层逻辑会在代理接口方法中进一步转发,ProxyManage会统一关闭界面。在单条数据的查询逻辑中,只需要调用代理存储层服务查询新的数据源即可,逻辑比较简单。比如单个店铺的信息查询,我们核心控制器ProxyManager的方法逻辑可以这样实现:publicTgetById(Longid,BooleanenableProxy){if(enableProxy){//启用代理,并去代理存储层查询服务returnproxyRepository.getById(id);}else{//不开启代理,返回原存储层的服务targetRepository.getById(id);}}多数据查询+去重多数据的去重逻辑是类似的,去重规则如下:新表和旧表不存在,数据被淘汰,不返回结果。新表没有旧表数据的信息。旧表没有新表数据的信息。旧表和新表都有数据(迁移完成)。此时判断主控是否打开,数据是否在灰名单,使用新的表数据;否则根据上述去重逻辑使用旧表数据。查询接口可以抽象成统一的方法查询旧数据,业务定义,使用supply函数封装查询逻辑查询新数据,业务定义,使用supply函数封装查询逻辑合并去重,以及统一合并工具的核心流程抽象如下图所示,目标接口的目标方法会被切面拦截并转发给代理接口。代理接口调用数据源的地方,可以进一步转发给ProxyManager进行查询合并。如果总开关没有打开,说明还没有进行全量数据的迁移和验证,所以还是要检查旧的数据源(防止数据遗漏)。如果开关打开,则表示迁移完成,此时不会再调用原来的存储层服务,达到关闭旧数据源的目的。比如批量查询门店列表,可以通过这种方式进行合并。核心实现如下:}//1.查询旧数据Supplier>oldSupplier=()->targetRepository.queryList(ids);//2.查询新数据Supplier>newSupplier=()->proxyRepository.queryList(ids);//3.根据合并规则进行合并,依赖合并工具(合并逻辑抽象后的工具类)returnProxyHelper.mergeWithSupplier(oldSupplier,newSupplier,idMapping);}合并工具类实现如下:publicclassProxyHelper{/***核心去重逻辑,判断是否使用新表数据**@paramexistOldData是否有旧数据*@paramexistNewData是否有新数据*@paramidStoreid*@return是否使用新表数据*/publicstaticbooleanuseNewData(BooleanexistOldData,BooleanexistNewData,Longid){if(!existOldData&&!existNewData){//两个表都不返回true;}elseif(!existNewData){//新表没有returnfalse;}elseif(!existOldData){//旧表没有returntrue;}else{//新表和旧表都有,判断开关和灰度开关返回主开关打开或在灰度列表中}}/***合并新/旧表数据**@paramoldSupplier旧表数据*@paramnewSupplier新表数据*@return合并去重数据*/publicstaticListmergeWithSupplier(Supplier>oldSupplier,Supplier>newSupplier,FunctionidMapping){Listold=Collections.emptyList();if(主开关未打开){//开关未完成,需要查询旧数据源old=oldSupplier.get();}returnmerge(idMapping,old,newSupplier.get());}/***去重并合并新旧数据**@paramidMappingstoreid映射函数*@paramoldData旧数据*@paramnewData新数据*@return合并结果*/publicstaticListmerge(FunctionidMapping,ListoldData,ListnewData){if(CollectionUtils.isEmpty(oldData)&&CollectionUtils.isEmpty(newData)){返回Collections.emptyList();}if(CollectionUtils.isEmpty(oldData)){返回newData;}if(CollectionUtils.isEmpty(newData)){返回oldData;}MapoldMap=oldData.stream().collect(Collectors.toMap(idMapping,Function.identity(),(a,b)->a));MapnewMap=newData.stream().collect(Collectors.toMap(idMapping,Function.identity(),(a,b)->a));返回ListUtils.union(oldData,newData).stream().map(idMapping).distinct().map(id->{booleanexistOldData=oldMap.containsKey(id);booleanexistNewData=newMap.containsKey(id);布尔useNewData=useNewData(existOldData,existNewData,id);返回useNewData?newMap.get(id):oldMap.get(id);}).filter(Objects::nonNull).collect(Collectors.toList());}}增量数据代码省略,直接执行代理存储层的insert方法即可更新数据。更新数据需要双写。如果主开关打开(即迁移完成),可以停止旧数据的写入,因为不会再读取了@Transactional(rollbackFor=Throwable.class)publicBooleanupdate(Tt){如果(t==null){返回false;}if(主开关未打开){//数据未迁移//双写更新,如果有,保持数据一致,可能不是所有场景都适用,但是在系统升级的过程中,工程师面临的最终目标应该是一致的,即让系统稳定上线,出现问题时安全回滚。本文的实现逻辑是通过注解和切面转发目标接口的方法,转发给代理类接口,从而切换到新的逻辑和新的数据源,ProxyManager适配代理分发逻辑数据源完成数据分发。查询、更新和添加逻辑。团队引进天猫校园技术团队,致力于服务校园人群,提升校园人群生活品质,提供一整套校园saas解决方案的技术团队。