从企业级架构到互联网架构迁移的工程实践

本文涉及的产品
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 RDS MySQL,高可用系列 2核4GB
简介: 随着业务的快速增长,一个线上交易系统在有限的时间内,不但需要维持线上系统的稳定,还要支撑新需求的开发,否则将由于技术支撑不利错失业务发展关键时间窗口。本文分享了一次从企业级架构到互联网架构迁移的工程实践。

因工作变动接手了一个云平台改造项目,该项目属于己经上线且每月有大量交易订单的云平台,之前采用的是SpringMVC+Hibernate+FreeMarker+MySql架构,集web前端和接口为一体。经过对业务增长趋势的评估,预计将在数月之后无法支撑原有业务的增长。

当前架构主要存在如下问题:

1、扩展维护困难

2、性能逐渐缓慢。

随着业务的快速增长逼迫我们对现有架构进行重构。由于是线上交易系统,留个我们改造的时间非常有限,不但需要维持线上系统的稳定还要支撑新需求的开发,否则将由于技术支撑不利错失业务发展关键时间窗口,基于务实的原则我们制定如下步骤进行逐步改造。

一、去hibernate迁移至mybatis

从hibernate迁移至mybatis, DAO层基本上需要重写一遍,其中主要工作量为理解原hibernate DAO层逻辑并翻译成sql,主要是细心活。其中需要注意的是mybatis动态表名的传入,需要将mapper的statementType类型修改为STATEMENT,并将SQL语句中#{}都改为${}。在使用${}传参过程中,需要特别注意SQL注入攻击危险。一般会在SpringMVC层将敏感字符转义。比如 ">" 用“>”表示,网上有很多封装函数,或者apache common lang包的StringEscapeUtils.escapeHtml()。

二、取掉sql之间的表关联

去掉sql之间的表关联,传统关系型数据库理论中的三范式在互联网的数据库模型中是不适用的,主要造成的问题是无法进行分表分库。这就要求所有dao方法必须保持单表操作。

保持单表操作为分表改造奠定了改造基础。

三、Service层对原子DAO业务逻辑进行组装

在取掉表关联后需要改造所有实体结构。首先取掉实体之间的一对一,多对一,多对多关联关系,将实体之间的引用关系修改为对实体ID的引用。同时为了上层方便使用需要引入业务BO对象,在service层调用多个原子的dao方法并组装成业务BO对象。

四、分表

在单张表超过2000万条记录后,mysql的查询性能开始降低,表变更字段等待时间漫长。分表后提升性能和扩展性后又带点以下问题:

  1. 路由策略的选择。

  2. 如何根据主键、订单号等路由到正确的表。

  3. 分表后分页查询

  4. 如何保证上线后分表数据平滑从老库过渡到新库。

1、路由策略的选择

首先,我们对数据库中所有的表记录进行分析,统计每张表的数据量大小。经过统计后我们发现随着业务的增长业务数据也会快速进行增长的表主要为订单表、订单明细表。其它的表在近两年内并不会随着业务的增长而快速增长。所以只需要对订单表、订单明细表进行拆分。

路由策略选择不可能做到完美,世界本来也是不完美的,关键是在合适的阶段选择合适的策略,即能满足商业战略时间窗口点又能在追求技术完美型中寻找平衡点。我们预测了业务近5年的发展目标为现有业5倍的增长,发现按月进行拆分可以保证每月数据量均低于2000万条,基于务实的原则我们选择了按月进行分表的路由策略。

经过多方面考虑在能够兼固效率和降低改造复杂度的思路提出老数据老办法,新数据新办法。老数据中的主键己经生成,如果按新的主键策略重新生成,会牵扯到所有关联表中的ID都需要进行替换,这样会增加改造的复杂度和工作量,所以最终考虑将新数据按照新的主键生成策略进行生成。当按月分表仍不能满足业务支持要求时,可以再次以日信息计算更细粒度的拆分策略,例如可按周为单位进行表折分。

2、根据主键或订单号选择正确的表

主键的生成算法  自增ID的生成,参考twitter  Snowflake算法



在Java语言系统中,可以通过Long来表示主键,Long类型包含64个位,正好可以存储该ID, 1至41位的二进制数值用来表示日期时间戳,43至53位可以表示1024台主机,我们可以为每台API服务器分配一个工作机器ID,43至55位可以生成线程唯一的序列号。预留的工作机器ID可以作为南北双活机房的路由判断条件,如1,2,3,4号工作机器ID路由到北机房API服务器,5,6,7,8工作机号ID路由到南机房API服务器。


当按订单号查询时系统首先根据订单号长度的不同,来选择是路由到新的切分订单表,还是路由到原订单表。因为新主键ID会包含日期信息,系统会根据主键解读出日期信息,根据月份的不同来选择该数据库对应的月份表,如果读取不出日期信息就可以判断出为原订单表。

3、如何保证上线后分表数据平滑从老表过渡到新表

首先系统配置统一的分表切割时间公共变量,在插入订单时先判断是否在分表切换时间点之前,如果在分表切换时间点之前则将订单数据插入到老表,否则将订单数据按当前月份不同插入到新的拆分月表。

4、分表后分页查询

在订单分表后存在的主要难点是分表后数据的分页查询操作。假设以2016-07-26 00:00:01 开始按月分表,查询2016-05-01 00:00:01至2016-09-11  12:00:05期间的所有订单分解为如下几步:

(1)通过开始时间、结束时间、分表时间计算出需要的路由信息集合。

  1. 格式:表名|起始日期|结束日期

  2. 路由集合:

Order|2016-05-01 00:00:01|2016-07-26 00:00:01

Order_2016_07|2016-07-26 00:00:01|2016-07-01 23:59:59

Order_2016_08|2016-08-01 00:00:01|2016-08-31 23:59:59

Order_2016_09|2016-09-01 00:00:01|2016-09-11 23:59:59

(2)按分页信息(pageNo,pageSize)及路由信息集合查询订单基本信息集合。

a.遍历路由集合返回总记录数及表概括信息集合。

        i. 表概括信息定义:表名、起始行数、记录数、路由信息;

        ii. 表概括信息集合:

  1. Order、1、137、Order|2016-05-0100:00:01|2016-07-26 00:00:01

  2. Order_2016_07、137、10、Order_2016_07|2016-07-2600:00:01|2016-07-01 23:59:59

  3. Order_2016_08、147、32、2016-08-0100:00:01|2016-08-31 23:59:59

  4. Order_2016_09、179、10、2016-09-0100:00:01|2016-09-11 23:59:59

        iii. 方法描述:

private RouteTableResult getRouteTableResult(OrderSearchModel searchModel,List<String> routeTables) {

        Integer sumRow = new Integer(0);

        Map<String, RouteTable> routeTableCountMap = new TreeMap<String,RouteTable>();

        RouteTableResult routeTableResult = new RouteTableResult();

        for (String routeTable : routeTables) {

            String[] routeTableArray = routeTable.split("\\|");

            if (routeTableArray.length == 3) {

                String tableName =getTableByRouteTableAndSetSearchModel(searchModel, routeTableArray);

                Integer orderCount = ticketOrderDao.searchOrderCount(tableName,searchModel);

                Integer startIndex = sumRow.intValue();

                RouteTable routeInfo = new RouteTable(startIndex, orderCount, routeTable);

                routeTableCountMap.put(tableName, routeInfo);

                sumRow += orderCount;

            }

        }

        routeTableResult.setRouteTableCountMap(routeTableCountMap);

        routeTableResult.setSumRow(sumRow);

        returnrouteTableResult;

    }

b.根据分页信息,查询出该分页需要跨越的表路由信息集合,具体算法如下:

    i. 遍历概括信息集合

    ii. 当开始行和结束行与当前路由区间有交集则说明有数据在该表内并将该表加入遍历路由集合;

    iii. 如果路由表信息集合中有数据且不满足上述条件则退出;

    iv. 返回需要跨越的表路由信息集合;

    v. 方法描述:

private List<String> getRouteTables(OrderSearchModel searchModel,Map<String,RouteTable> routeTableCountMap) {

        List<String> routeTableInfoList = new ArrayList<String>();

        Integer startIndex = (searchModel.getPageNo() - 1) * searchModel.getPageSize();

        Integer endIndex = startIndex + searchModel.getPageSize() -1;

        for (Entry<String, RouteTable> entry : routeTableCountMap.entrySet()) {

            RouteTable routeTable = entry.getValue();

            //开始行和结束行与当间路由区有交集

            if( !(startIndex > routeTable.getEndIndex()) && !(endIndex < routeTable.getStartIndex())){

                routeTableInfoList.add(routeTable.getRouteInfo());

            //如果路由表信息集合中有数据且不满足上述条件则退出

            }elseif (routeTableInfoList.size()>0) {

                break;

            }

        }

        returnrouteTableInfoList;

    }

c.查询该分页下的订单列表,具体算法如下:

    i.首先设置最后一次遍历的表为需要跨越路由信息集合的第一张表;

    ii.设置己读条数readCount等于0;

    iii.遍历需要跨越路由信息集合;

    iv. 根据路由信息返回表名及设置搜索条件;

    v. 根据表名获取路由概要信息;

    vi.计算开始行号,如果当前表名和最后遍历的表名相同,则开始行号等于(当前的页数-1)*原请求页面大小(originalPageSize)-当前表路由概要信息起始行,否则开行号设置为0;

    vii. 计算当前页面大小pageSize为原请求页面大小(originalPageSize) – 己读条数(readCount);

    viii. 设置搜索条件起始行号、当前页面大小;

    ix.设置最后一次遍历的表为当前表;

    x.根据当前表名、搜索条件调用dao返回订单基本信息列表,并加入订单总列表集合;

    xi.己读数增加当前订单列表大小;

    xii. 如果己读数大于等于原请求页面大小则跳出循环,否则继续循环;

    xiii.返回订单总列表集合;

    xiv. 方法描述:

private List<Order> getOrderListByRoutePageTable(OrderSearchModelsearchModel,

            Integer originalPageSize, Map<String, RouteTable> routeTableCountMap,

            List<String> routePageTables) {

        Integer readCount = 0;

        List<Order> orderList = new ArrayList<Order>();

        if (routePageTables != null && routePageTables.size() > 0) {

            String[] routeTableArrayFirst = routePageTables.get(0).split("\\|");

            String lastTableName = null ;

            if (routeTableArrayFirst.length == 3) {

                lastTableName = routeTableArrayFirst[0];

            }

            for (String routeTable : routePageTables) {

                String[] routeTableArray = routeTable.split("\\|");

                if (routeTableArray.length != 3) {

                    break;

                }

                String tableName = getTableByRouteTableAndSetSearchModel(searchModel, routeTableArray);

                RouteTable routeTableInfo = routeTableCountMap.get(tableName);

                Integer startRow = 0;

                if( tableName.equals(lastTableName)){

                    startRow = (searchModel.getPageNo()-1)*originalPageSize - routeTableInfo.getStartIndex();

                }

                Integer pageSizeoriginalPageSize - readCount;

                searchModel.setStartRow(startRow);

                searchModel.setPageSize(pageSize);

                lastTableName = tableName;

                List<Order> orderListPage = orderDao.searchOrderList(tableName,searchModel);

                orderList.addAll(orderListPage);

                readCount += orderListPage.size();

                if (readCount.intValue() >= originalPageSize) {

                    break;

                }

            }

        }

        returnorderList;

    }

(3)根据Order集合组装OrderBo集合

(4)根据返回的总数及分页信息组装分页结果

五、mysql主从分离、引入三级缓存

为了提高性能,首先配置mysql主从分离,通过Spring多数据源来实现动态切换。三级缓存主要分为:(1)、线程级:当同一线程请求时,线程级缓存绑定在线程间ThreadLocal变量上,可以降低线程间切换造成的时间开销。(2)、进程级:进程级缓存在同一jvm中共享缓存,减速少跨进程间网络开销。(3)、跨进程的集中式缓存:一般使用redis、memcache内存缓存来降低对数据库系统的冲击。在做完以上优化后,我们的接口响应速度提高了近5倍。

六、结束语

关于分布式服务化、异地南北机房双活,这里留个作业,日后成文和大家分享。


本文作者:熊孝鹏

本文转载自微信公众号 中生代技术 freshmanTechnology

相关实践学习
如何在云端创建MySQL数据库
开始实验后,系统会自动创建一台自建MySQL的 源数据库 ECS 实例和一台 目标数据库 RDS。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助 &nbsp; &nbsp; 相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
目录
相关文章
|
3天前
|
Cloud Native 安全 API
云原生架构下的微服务治理策略与实践####
—透过云原生的棱镜,探索微服务架构下的挑战与应对之道 本文旨在探讨云原生环境下,微服务架构所面临的关键挑战及有效的治理策略。随着云计算技术的深入发展,越来越多的企业选择采用云原生架构来构建和部署其应用程序,以期获得更高的灵活性、可扩展性和效率。然而,微服务架构的复杂性也带来了服务发现、负载均衡、故障恢复等一系列治理难题。本文将深入分析这些问题,并提出一套基于云原生技术栈的微服务治理框架,包括服务网格的应用、API网关的集成、以及动态配置管理等关键方面,旨在为企业实现高效、稳定的微服务架构提供参考路径。 ####
20 5
|
6天前
|
监控 Go API
Go语言在微服务架构中的应用实践
在微服务架构的浪潮中,Go语言以其简洁、高效和并发处理能力脱颖而出,成为构建微服务的理想选择。本文将探讨Go语言在微服务架构中的应用实践,包括Go语言的特性如何适应微服务架构的需求,以及在实际开发中如何利用Go语言的特性来提高服务的性能和可维护性。我们将通过一个具体的案例分析,展示Go语言在微服务开发中的优势,并讨论在实际应用中可能遇到的挑战和解决方案。
|
4天前
|
负载均衡 监控 Cloud Native
云原生架构下的微服务治理策略与实践####
在数字化转型浪潮中,企业纷纷拥抱云计算,而云原生架构作为其核心技术支撑,正引领着一场深刻的技术变革。本文聚焦于云原生环境下微服务架构的治理策略与实践,探讨如何通过精细化的服务管理、动态的流量调度、高效的故障恢复机制以及持续的监控优化,构建弹性、可靠且易于维护的分布式系统。我们将深入剖析微服务治理的核心要素,结合具体案例,揭示其在提升系统稳定性、扩展性和敏捷性方面的关键作用,为读者提供一套切实可行的云原生微服务治理指南。 ####
|
4天前
|
消息中间件 缓存 Cloud Native
云原生架构下的性能优化实践与挑战####
随着企业数字化转型的加速,云原生架构以其高度解耦、弹性伸缩和快速迭代的特性,成为现代软件开发的首选模式。本文深入探讨了云原生环境下性能优化的关键策略与面临的主要挑战,通过案例分析,揭示了如何有效利用容器化、微服务、动态调度等技术手段提升应用性能,同时指出了在复杂云环境中确保系统稳定性和高效性的难题,为开发者和架构师提供了实战指南。 ####
18 3
|
4天前
|
运维 Kubernetes Cloud Native
深入理解云原生架构:从理论到实践
【10月更文挑战第38天】本文将引导读者深入探索云原生技术的核心概念,以及如何将这些概念应用于实际的软件开发和运维中。我们将从云原生的基本定义出发,逐步展开其背后的设计哲学、关键技术组件,并以一个具体的代码示例来演示云原生应用的构建过程。无论你是云原生技术的初学者,还是希望深化理解的开发者,这篇文章都将为你提供有价值的见解和实操指南。
|
4天前
|
Kubernetes Cloud Native 持续交付
云原生技术在现代应用架构中的实践与思考
【10月更文挑战第38天】随着云计算的不断成熟和演进,云原生(Cloud-Native)已成为推动企业数字化转型的重要力量。本文从云原生的基本概念出发,深入探讨了其在现代应用架构中的实际应用,并结合代码示例,展示了云原生技术如何优化资源管理、提升系统弹性和加速开发流程。通过分析云原生的优势与面临的挑战,本文旨在为读者提供一份云原生转型的指南和启示。
18 3
|
4天前
|
运维 Kubernetes Cloud Native
云原生技术在现代应用架构中的实践与挑战####
本文深入探讨了云原生技术的核心概念、关键技术组件及其在实际项目中的应用案例,分析了企业在向云原生转型过程中面临的主要挑战及应对策略。不同于传统摘要的概述性质,本摘要强调通过具体实例揭示云原生技术如何促进应用的灵活性、可扩展性和高效运维,同时指出实践中需注意的技术债务、安全合规等问题,为读者提供一幅云原生技术实践的全景视图。 ####
|
7天前
|
监控 API 持续交付
后端开发中的微服务架构实践与挑战####
本文深入探讨了微服务架构在后端开发中的应用,分析了其优势、面临的挑战以及最佳实践策略。不同于传统的单体应用,微服务通过细粒度的服务划分促进了系统的可维护性、可扩展性和敏捷性。文章首先概述了微服务的核心概念及其与传统架构的区别,随后详细阐述了构建微服务时需考虑的关键技术要素,如服务发现、API网关、容器化部署及持续集成/持续部署(CI/CD)流程。此外,还讨论了微服务实施过程中常见的问题,如服务间通信复杂度增加、数据一致性保障等,并提供了相应的解决方案和优化建议。总之,本文旨在为开发者提供一份关于如何在现代后端系统中有效采用和优化微服务架构的实用指南。 ####
|
9天前
|
消息中间件 设计模式 运维
后端开发中的微服务架构实践与挑战####
本文深入探讨了微服务架构在现代后端开发中的应用,通过实际案例分析,揭示了其在提升系统灵活性、可扩展性及促进技术创新方面的显著优势。同时,文章也未回避微服务实施过程中面临的挑战,如服务间通信复杂性、数据一致性保障及部署运维难度增加等问题,并基于实践经验提出了一系列应对策略,为开发者在构建高效、稳定的微服务平台时提供有价值的参考。 ####
|
9天前
|
Cloud Native API 云计算
云原生架构的深度探索与实践####
本文深入探讨了云原生架构的核心概念、技术特点及其在现代软件开发中的应用实践。通过分析云原生架构如何促进企业数字化转型,提升业务敏捷性与可扩展性,本文旨在为读者提供一个全面而深入的理解框架。我们将从云原生的定义出发,逐步深入到其关键技术组件、最佳实践案例及面临的挑战与解决方案,为开发者和企业决策者提供宝贵的参考与启示。 ####