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

本文涉及的产品
云数据库 RDS MySQL Serverless,0.5-2RCU 50GB
云数据库 RDS MySQL Serverless,价值2615元额度,1个月
简介: 随着业务的快速增长,一个线上交易系统在有限的时间内,不但需要维持线上系统的稳定,还要支撑新需求的开发,否则将由于技术支撑不利错失业务发展关键时间窗口。本文分享了一次从企业级架构到互联网架构迁移的工程实践。

因工作变动接手了一个云平台改造项目,该项目属于己经上线且每月有大量交易订单的云平台,之前采用的是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算法

640?wx_fmt=jpeg&wxfrom=5&wx_lazy=1

在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服务器。

640?wx_fmt=jpeg&wxfrom=5&wx_lazy=1

当按订单号查询时系统首先根据订单号长度的不同,来选择是路由到新的切分订单表,还是路由到原订单表。因为新主键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

相关实践学习
基于CentOS快速搭建LAMP环境
本教程介绍如何搭建LAMP环境,其中LAMP分别代表Linux、Apache、MySQL和PHP。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助 &nbsp; &nbsp; 相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
目录
相关文章
|
11天前
|
监控 Java 测试技术
现代化软件开发中的微服务架构设计与实践
随着软件开发的发展,传统的单体应用架构已经无法满足现代化应用的需求。微服务架构作为一种新的设计理念,为软件开发提供了更灵活、可扩展的解决方案。本文将介绍微服务架构的设计原则、实践方法以及相关技术工具,并结合实例展示其在现代化软件开发中的应用。
|
2天前
|
存储 监控 API
构建高效微服务架构:后端开发的现代实践
【5月更文挑战第9天】 在本文中,我们将深入探讨如何在后端开发中构建一个高效的微服务架构。通过分析不同的设计模式和最佳实践,我们将展示如何提升系统的可扩展性、弹性和维护性。我们还将讨论微服务架构在处理复杂业务逻辑和高并发场景下的优势。最后,我们将分享一些实用的工具和技术,以帮助开发者实现这一目标。
|
2天前
|
监控 API 持续交付
构建高效可靠的微服务架构:策略与实践
【5月更文挑战第8天】在当今快速演进的软件开发领域,微服务架构已经成为实现敏捷开发、持续交付和系统弹性的关键模式。本文将探讨构建一个高效且可靠的微服务系统所必须的策略和最佳实践。我们将从服务的划分与设计原则出发,讨论如何通过容器化、服务发现、API网关以及断路器模式来优化系统的可伸缩性和鲁棒性。此外,我们还将涉及监控、日志管理以及CI/CD流程在确保微服务架构稳定运行中的作用。
|
3天前
|
敏捷开发 持续交付 API
构建高效微服务架构:后端开发的现代实践
【5月更文挑战第8天】 在数字化转型的浪潮中,微服务架构已成为企业追求敏捷开发、持续交付和系统弹性的关键解决方案。本文将深入探讨微服务的核心概念,包括其设计原则、优缺点以及如何在后端开发中实现高效的微服务架构。我们将通过实际案例分析,展示微服务如何帮助企业快速适应市场变化,同时保持系统的可维护性和扩展性。
|
3天前
|
监控 云计算 开发者
探索云计算中的无服务器架构:从概念到实践
无服务器架构作为云计算领域的新兴技术,正在以其高效、灵活的特性吸引着越来越多的开发者和企业。本文将深入探讨无服务器架构的概念及其在云计算中的应用,通过实际案例展示如何利用无服务器架构构建可靠、可扩展的应用系统。
|
5天前
|
监控 负载均衡 数据安全/隐私保护
探索微服务架构下的服务网格(Service Mesh)实践
【5月更文挑战第6天】 在现代软件工程的复杂多变的开发环境中,微服务架构已成为构建、部署和扩展应用的一种流行方式。随着微服务架构的普及,服务网格(Service Mesh)作为一种新兴技术范式,旨在提供一种透明且高效的方式来管理微服务间的通讯。本文将深入探讨服务网格的核心概念、它在微服务架构中的作用以及如何在实际项目中落地实施服务网格。通过剖析服务网格的关键组件及其与现有系统的协同工作方式,我们揭示了服务网格提高系统可观察性、安全性和可操作性的内在机制。此外,文章还将分享一些实践中的挑战和应对策略,为开发者和企业决策者提供实用的参考。
|
5天前
|
API 持续交付 开发者
构建高效微服务架构:策略与实践
【5月更文挑战第6天】随着现代软件系统的复杂性增加,微服务架构逐渐成为企业开发的首选模式。本文深入分析了构建高效微服务架构的关键策略,并提供了一套实践指南,帮助开发者在保证系统可伸缩性、灵活性和稳定性的前提下,优化后端服务的性能和可维护性。通过具体案例分析,本文将展示如何利用容器化、服务网格、API网关等技术手段,实现微服务的高可用和敏捷部署。
|
6天前
|
存储 前端开发 Java
Android应用开发中的MVP架构模式实践
【5月更文挑战第5天】随着移动应用开发的复杂性增加,传统的MVC(Model-View-Controller)架构在应对大型项目时显得笨重且不灵活。本文将探讨一种更适应现代Android应用开发的架构模式——MVP(Model-View-Presenter),并展示如何在Android项目中实现该模式以提升代码的可维护性和可测试性。通过对比分析MVP与传统MVC的差异,以及提供一个实际案例,读者将能深入了解MVP的优势和实施步骤。
|
6天前
|
缓存 NoSQL Java
构建高性能微服务架构:Java后端的实践之路
【5月更文挑战第5天】在当今快速迭代和高并发需求的软件开发领域,微服务架构因其灵活性、可扩展性而受到青睐。本文将深入探讨如何在Java后端环境中构建一个高性能的微服务系统,涵盖关键的设计原则、常用的框架选择以及性能优化技巧。我们将重点讨论如何通过合理的服务划分、高效的数据存储策略、智能的缓存机制以及有效的负载均衡技术来提升整体系统的响应速度和处理能力。
|
6天前
|
监控 持续交付 数据库
构建高效可靠的微服务架构:策略与实践
【5月更文挑战第5天】 在当今快速发展的软件开发领域,微服务架构已成为构建可扩展、灵活且容错的系统的首选模式。本文将探讨如何通过一系列经过验证的策略和最佳实践来构建一个高效且可靠的微服务系统。我们将深入分析微服务设计的核心原则,包括服务的细粒度划分、通信机制、数据一致性以及容错处理,并讨论如何利用现代技术栈来实现这些目标。文章将提供一套综合指南,旨在帮助开发者和架构师在保证系统性能的同时,确保系统的稳健性。
23 4