因工做变更接手了一个云平台改造项目,该项目属于己经上线且每个月有大量交易订单的云平台,以前采用的是SpringMVC+Hibernate+FreeMarker+MySql架构,集web前端和接口为一体。通过对业务增加趋势的评估,预计将在数月以后没法支撑原有业务的增加。当前架构主要存在以下问题:一、扩展维护困难、二、性能逐渐缓慢。随着业务的快速增加逼迫咱们对现有架构进行重构。因为是线上交易系统,留个咱们改造的时间很是有限,不但须要维持线上系统的稳定还要支撑新需求的开发,不然将因为技术支撑不利错失业务发展关键时间窗口,基于务实的原则咱们制定以下步骤进行逐步改造。前端
从hibernate迁移至mybatis, DAO层基本上须要重写一遍,其中主要工做量为理解原hibernate DAO层逻辑并翻译成sql,主要是细心活。其中须要注意的是mybatis动态表名的传入,须要将mapper的statementType类型修改成STATEMENT,并将SQL语句中#{}都改成${}。在使用${}传参过程当中,须要特别注意SQL注入攻击危险。通常会在SpringMVC层将敏感字符转义。好比">"用“>”表示,网上有不少封装函数,或者apache common lang包的StringEscapeUtils.escapeHtml()。java
去掉sql之间的表关联,传统关系型数据库理论中的三范式在互联网的数据库模型中是不适用的,主要形成的问题是没法进行分表分库。这就要求全部dao方法必须保持单表操做,保持单表操做为分表改造奠基了改造基础。mysql
在去掉表关联后须要改造全部实体结构。首先取掉实体之间的一对一,多对一,多对多关联关系,将实体之间的引用关系修改成对实体ID的引用。同时为了上层方便使用须要引入业务BO对象,在service层调用多个原子的dao方法并组装成业务BO对象。web
在单张表超过2000万条记录后,mysql的查询性能开始下降,表变动字段等待时间漫长。分表后提高性能和扩展性后又带来以下问题:(1) 分表后老数据如何处理(路由策略)?(1)、如何根据主键、订单号等路由到正确的表?(2)、分表后如何进行分页查询?(3)、如何保证上线后分表数据平滑从老库过渡到新库?redis
首先,咱们对数据库中全部的表记录进行分析,统计每张表的数据量大小。通过统计后咱们发现随着业务的增加业务数据也会快速进行增加的表主要为订单表、订单明细表。其它的表在近两年内并不会随着业务的增加而快速增加。因此只须要对订单表、订单明细表进行拆分。路由策略选择不可能作到完美,世界原本也是不完美的,关键是在合适的阶段选择合适的策略,即能知足商业战略时间窗口点又能在追求技术完美型中寻找平衡点。咱们预测了业务近5年的发展目标为现有业5倍的增加,发现按月进行拆分能够保证每个月数据量均低于2000万条,基于务实的原则咱们选择了按月进行分表的路由策略。通过多方面考虑在可以兼固效率和下降改造复杂度的思路提出老数据老办法,新数据新办法。老数据中的主键己经生成,若是按新的主键策略从新生成,会牵扯到全部关联表中的ID都须要进行替换,这样会增长改造的复杂度和工做量,因此最终考虑将新数据按照新的主键生成策略进行生成。当按月分表仍不能知足业务支持要求时,能够再次以日信息计算更细粒度的拆分策略,例如可按周为单位进行表折分。算法
主键的生成算 自增ID的生成,参考twitter Snowflake算法sql
(图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服务器。apache
(图2)缓存
当按订单号查询时系统首先根据订单号长度的不一样,来选择是路由到新的切分订单表,仍是路由到原订单表。由于新主键ID会包含日期信息,系统会根据主键解读出日期信息,根据月份的不一样来选择该数据库对应的月份表,若是读取不出日期信息就能够判断出为原订单表。
首先系统配置统一的分表切割时间公共变量,在插入订单时先判断是否在分表切换时间点以前,若是在分表切换时间点以前则将订单数据插入到老表,不然将订单数据按当前月份不一样插入到新的拆分月表。
在订单分表后存在的主要难点是分表后数据的分页查询操做。假设以2016-07-26 00:00:01 开始按月分表,查询2016-05-01 00:00:01至2016-09-11 12:00:05 期间的全部订单分解为以下几步:
(1) 经过开始时间、结束时间、分表时间计算出须要的路由信息集合。
a) 格式:表名|起始日期|结束日期
b) 路由集合: Order|2016-05-01 00:00:00|2016-07-26 00:00:01
Order_2016_07|2016-07-26 00:00:00|2016-07-01 23:59:59
Order_2016_08|2016-08-01 00:00:00|2016-08-31 23:59:59
Order_2016_09|2016-09-01 00:00:00|2016-09-11 12:00:05
(2) 按分页信息(pageNo,pageSize)及路由信息集合查询订单基本信息集合。
a) 遍历路由集合返回总记录数及表归纳信息集合。
i. 表归纳信息定义:表名、起始行数、记录数、路由信息;
ii. 表归纳信息集合:
1. Order、一、13七、Order|2016-05-01 00:00:00|2016-07-26 00:00:01
2. Order_2016_0七、13七、十、Order_2016_07|2016-07-26 00:00:00|2016-07-01 23:59:59
3. Order_2016_0八、14七、3二、2016-08-01 00:00:00|2016-08-31 23:59:59
4. Order_2016_0九、17九、十、2016-09-01 00:00:00|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); return routeTableResult; }
b) 根据分页信息,查询出该分页须要跨越的表路由信息集合,具体算法以下:
i. 遍历归纳信息集合
ii. 当开始行和结束行与当前路由区有交集则说明有数据在该表内
iii. 并将该表加入遍历路由集合;
iv. 若是路由表信息集合中有数据且不知足上述条件则退出;
v. 返回须要跨越的表路由信息集合;
vi. 方法描述:
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()); //若是路由表信息集合中有数据且不知足上述条件则退出 }else if (routeTableInfoList.size()>0) { break; } } return routeTableInfoList; }
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(OrderSearchModel searchModel, 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 pageSize = originalPageSize - 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; } } } return OrderList; }
(3) 根据Order集合组装OrderBo集合
(4) 根据返回的总数及分页信息组装分页结果
为了提升性能,首先配置mysql主从分离,经过Spring多数据源来实现动态切换。引入三级缓存主要分为:(1)、线程级:当同一线程请求时,线程级缓存绑定在线程间ThreadLocal变量上,能够下降线程间切换形成的时间开销。(2)、进程级:进程级缓存在同一jvm中共享缓存,减速少跨进程间网络开销。(3)、跨进程的集中式缓存:使用redis或memcache内存缓存来下降对数据库系统的冲击。在作完以上优化后,咱们的接口响应速度提升了近5倍。
待续
待续