因工做變更接手了一個雲平臺改造項目,該項目屬於己經上線且每個月有大量交易訂單的雲平臺,以前採用的是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倍。
待續
待續