1. 學習計劃 1、訂單系統實現 2、訂單生成 3、Mycat資料庫分片 2. 訂單系統 2.1. 功能分析 1、在購物車頁面點擊“去結算”按鈕,跳轉到訂單確認頁面 a) 必須要求用戶登錄 b) 使用攔截器實現。 c) 如果用戶未登錄跳轉到登錄頁面。 d) 如果用戶已經登錄,放行。展示確認頁面。 e ...
1. 學習計劃
1、訂單系統實現
2、訂單生成
3、Mycat資料庫分片
2. 訂單系統
2.1. 功能分析
1、在購物車頁面點擊“去結算”按鈕,跳轉到訂單確認頁面
a) 必須要求用戶登錄
b) 使用攔截器實現。
c) 如果用戶未登錄跳轉到登錄頁面。
d) 如果用戶已經登錄,放行。展示確認頁面。
e) 判斷cookie中是否有購物車數據
f) 如果有同步到服務端。
2、訂單確認頁面中選擇收貨地址,選擇支付方式,確認商品列表。
a) 根據用戶id查詢收貨地址列表
b) 展示支付方式列表。
c) 從購物車中取商品列表,從服務端取購物車列表。
3、訂單確認頁面點擊“提交”,生成訂單。
4、展示訂單生成完成,或者跳轉到支付頁面。
2.2. 工程搭建
E3-order
|--E3-order-interface(jar)
|--E3-order-service(war)
E3-order-web(war)
2.3. 展示訂單確認頁面
2.3.1. 功能分析
1、根據id查詢用戶的收貨地址列表(使用靜態數據)
2、從購物車中取商品列表,展示到頁面。調用購物車服務查詢。
2.3.2. Dao層
直接從redis中取購車商品列表。
2.3.3. Service層
收貨地址靜態數據,沒有server。
調用購物車的service查詢購物車商品列表。
2.3.4. Controller
引用購物車服務
請求的url:/order/order-cart
參數:沒有參數
返回值:邏輯視圖
Controller
@Controller public class OrderCartController { @Autowired private CartService cartService; @RequestMapping("/order/order-cart") public String showOrderCart(HttpServletRequest request) { //取用戶信息 TbUser user = (TbUser) request.getAttribute("user"); //取購物車商品列表 List<TbItem> cartList = cartService.getCartList(user.getId()); //把商品列表傳遞給jsp request.setAttribute("cartList", cartList); //返回邏輯視圖 return "order-cart"; } }
2.4. 登錄攔截器
2.4.1. 功能分析
1、從cookie中取token
2、如果沒有取到,沒有登錄,跳轉到sso系統的登錄頁面。攔截
3、如果取到token。判斷登錄是否過期,需要調用sso系統的服務,根據token取用戶信息
4、如果沒有取到用戶信息,登錄已經過期,重新登錄。跳轉到登錄頁面。攔截
5、如果取到用戶信息,用戶已經是登錄狀態,把用戶信息保存到request中。放行
6、判斷cookie中是否有購物車信息,如果有合併購物車
2.4.2. 攔截器實現
/** * 用戶登錄判斷攔截器 * <p>Title: LoginInterceptor</p> * <p>Description: </p> * <p>Company: www.itcast.cn</p> * @version 1.0 */ public class LoginInterceptor implements HandlerInterceptor { @Value("${COOKIE_TOKEN_KEY}") private String COOKIE_TOKEN_KEY; @Value("${COOKIE_CART_KEY}") private String COOKIE_CART_KEY; @Value("${SSO_URL}") private String SSO_URL; @Autowired private UserService userService; @Autowired private CartService cartService; @Override public void afterCompletion(HttpServletRequest arg0, HttpServletResponse arg1, Object arg2, Exception arg3) throws Exception { // TODO Auto-generated method stub } @Override public void postHandle(HttpServletRequest arg0, HttpServletResponse arg1, Object arg2, ModelAndView arg3) throws Exception { // TODO Auto-generated method stub } @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 1、從cookie中取token String token = CookieUtils.getCookieValue(request, COOKIE_TOKEN_KEY); // 2、如果沒有取到,沒有登錄,跳轉到sso系統的登錄頁面。攔截 if (StringUtils.isBlank(token)) { //跳轉到登錄頁面 response.sendRedirect(SSO_URL + "/page/login?redirect=" + request.getRequestURL()); return false; } // 3、如果取到token。判斷登錄是否過期,需要調用sso系統的服務,根據token取用戶信息 E3Result e3Result = userService.getUserByToken(token); // 4、如果沒有取到用戶信息,登錄已經過期,重新登錄。跳轉到登錄頁面。攔截 if (e3Result.getStatus() != 200) { response.sendRedirect(SSO_URL + "/page/login?redirect=" + request.getRequestURL()); return false; } // 5、如果取到用戶信息,用戶已經是登錄狀態,把用戶信息保存到request中。放行 TbUser user = (TbUser) e3Result.getData(); request.setAttribute("user", user); // 6、判斷cookie中是否有購物車信息,如果有合併購物車 String json = CookieUtils.getCookieValue(request, COOKIE_CART_KEY, true); if (StringUtils.isNotBlank(json)) { cartService.mergeCart(user.getId(), JsonUtils.jsonToList(json, TbItem.class)); //刪除cookie中的購物車數據 CookieUtils.setCookie(request, response, COOKIE_CART_KEY, ""); } //放行 return true; } }
2.4.3. Springmvc配置
<!-- 攔截器配置 --> <mvc:interceptors> <mvc:interceptor> <mvc:mapping path="/**"/> <bean class="cn.e3mall.order.interceptor.LoginInterceptor"/> </mvc:interceptor> </mvc:interceptors>
2.4.4. 實現sso系統的回調
2.5. 提交訂單
2.5.1. 功能分析
1、在訂單確認頁面點擊“提交訂單”按鈕生成訂單。
2、請求的url:/order/create
3、參數:提交的是表單的數據。保存的數據:訂單、訂單明細、配送地址。
a) 向tb_order中插入記錄。
- 訂單號需要手動生成。
要求訂單號不能重覆。
訂單號可讀性號。
可以使用redis的incr命令生成訂單號。訂單號需要一個初始值。
- Payment:表單數據
- payment_type:表單數據
- user_id:用戶信息
- buyer_nick:用戶名
- 其他欄位null
b) 向tb_order_item訂單明細表插入數據。
- Id:使用incr生成
- order_id:生成的訂單號
- 其他的都是表單中的數據。
c) tb_order_shipping,訂單配送信息
- order_id:生成的訂單號
- 其他欄位都是表單中的數據。
d) 使用pojo接收表單的數據。
可以擴展TbOrder,在子類中添加兩個屬性一個是商品明細列表,一個是配送信息。
把pojo放到e3-order-interface工程中。
public class OrderInfo extends TbOrder implements Serializable{ private List<TbOrderItem> orderItems; private TbOrderShipping orderShipping; public List<TbOrderItem> getOrderItems() { return orderItems; } public void setOrderItems(List<TbOrderItem> orderItems) { this.orderItems = orderItems; } public TbOrderShipping getOrderShipping() { return orderShipping; } public void setOrderShipping(TbOrderShipping orderShipping) { this.orderShipping = orderShipping; } }
業務邏輯:
1、接收表單的數據
2、生成訂單id
3、向訂單表插入數據。
4、向訂單明細表插入數據
5、向訂單物流表插入數據。
6、返回e3Result。
返回值:e3Result
2.5.2. Dao層
可以使用逆向工程。
2.5.3. Service層
參數:OrderInfo
返回值:e3Result
@Service public class OrderServiceImpl implements OrderService { @Autowired private TbOrderMapper orderMapper; @Autowired private TbOrderItemMapper orderItemMapper; @Autowired private TbOrderShippingMapper orderShippingMapper; @Autowired private JedisClient jedisClient; @Value("${ORDER_GEN_KEY}") private String ORDER_GEN_KEY; @Value("${ORDER_ID_BEGIN}") private String ORDER_ID_BEGIN; @Value("${ORDER_ITEM_ID_GEN_KEY}") private String ORDER_ITEM_ID_GEN_KEY; @Override public e3Result createOrder(OrderInfo orderInfo) { // 1、接收表單的數據 // 2、生成訂單id if (!jedisClient.exists(ORDER_GEN_KEY)) { //設置初始值 jedisClient.set(ORDER_GEN_KEY, ORDER_ID_BEGIN); } String orderId = jedisClient.incr(ORDER_GEN_KEY).toString(); orderInfo.setOrderId(orderId); orderInfo.setPostFee("0"); //1、未付款,2、已付款,3、未發貨,4、已發貨,5、交易成功,6、交易關閉 orderInfo.setStatus(1); Date date = new Date(); orderInfo.setCreateTime(date); orderInfo.setUpdateTime(date); // 3、向訂單表插入數據。 orderMapper.insert(orderInfo); // 4、向訂單明細表插入數據 List<TbOrderItem> orderItems = orderInfo.getOrderItems(); for (TbOrderItem tbOrderItem : orderItems) { //生成明細id Long orderItemId = jedisClient.incr(ORDER_ITEM_ID_GEN_KEY); tbOrderItem.setId(orderItemId.toString()); tbOrderItem.setOrderId(orderId); //插入數據 orderItemMapper.insert(tbOrderItem); } // 5、向訂單物流表插入數據。 TbOrderShipping orderShipping = orderInfo.getOrderShipping(); orderShipping.setOrderId(orderId); orderShipping.setCreated(date); orderShipping.setUpdated(date); orderShippingMapper.insert(orderShipping); // 6、返回e3Result。 return e3Result.ok(orderId); } }
2.5.4. Controller
請求的url:/order/create
參數:使用OrderInfo接收
返回值:邏輯視圖。
業務邏輯:
1、接收表單提交的數據OrderInfo。
2、補全用戶信息。
3、調用Service創建訂單。
4、返回邏輯視圖展示成功頁面
a) 需要Service返回訂單號
b) 當前日期加三天。
在攔截器中添加用戶處理邏輯:
@RequestMapping(value="/order/create", method=RequestMethod.POST) public String createOrder(OrderInfo orderInfo, HttpServletRequest request) { // 1、接收表單提交的數據OrderInfo。 // 2、補全用戶信息。 TbUser user = (TbUser) request.getAttribute("user"); orderInfo.setUserId(user.getId()); orderInfo.setBuyerNick(user.getUsername()); // 3、調用Service創建訂單。 e3Result result = orderService.createOrder(orderInfo); //取訂單號 String orderId = result.getData().toString(); // a)需要Service返回訂單號 request.setAttribute("orderId", orderId); request.setAttribute("payment", orderInfo.getPayment()); // b)當前日期加三天。 DateTime dateTime = new DateTime(); dateTime = dateTime.plusDays(3); request.setAttribute("date", dateTime.toString("yyyy-MM-dd")); // 4、返回邏輯視圖展示成功頁面 return "success"; }
3. Mycat資料庫分片
1 海量數據的存儲問題
如今隨著互聯網的發展,數據的量級也是撐指數的增長,從GB到TB到PB。對數據的各種操作也是愈加的困難,傳統的關係性資料庫已經無法滿足快速查詢與插入數據的需求。這個時候NoSQL的出現暫時解決了這一危機。它通過降低數據的安全性,減少對事務的支持,減少對複雜查詢的支持,來獲取性能上的提升。
但是,在有些場合NoSQL一些折衷是無法滿足使用場景的,就比如有些使用場景是絕對要有事務與安全指標的。這個時候NoSQL肯定是無法滿足的,所以還是需要使用關係性資料庫。如果使用關係型資料庫解決海量存儲的問題呢?此時就需要做資料庫集群,為了提高查詢性能將一個資料庫的數據分散到不同的資料庫中存儲。
1.1 什麼是資料庫分片
簡單來說,就是指通過某種特定的條件,將我們存放在同一個資料庫中的數據分散存放到多個資料庫(主機)上面,以達到分散單台設備負載的效果。
數據的切分(Sharding)根據其切分規則的類型,可以分為兩種切分模式。
(1)一種是按照不同的表(或者Schema)來切分到不同的資料庫(主機)之上,這種切可以稱之為數據的垂直(縱向)切分
(2)另外一種則是根據表中的數據的邏輯關係,將同一個表中的數據按照某種條件拆分到多台資料庫(主機)上面,這種切分稱之為數據的水平(橫向)切分。
1.2 如何實現資料庫分片
當資料庫分片後,數據由一個資料庫分散到多個資料庫中。此時系統要查詢時需要切換不同的資料庫進行查詢,那麼系統如何知道要查詢的數據在哪個資料庫中?當添加一條記錄時要向哪個資料庫中插入呢?這些問題處理起來都是非常的麻煩。
這種情況下可以使用一個資料庫中間件mycat來解決相關的問題。接下來瞭解一下什麼是mycat。
2 Mycat介紹
2.1 什麼是Mycat?
Mycat 背後是阿裡曾經開源的知名產品——Cobar。Cobar 的核心功能和優勢是 MySQL 資料庫分片,此產品曾經廣為流傳,據說最早的發起者對 Mysql 很精通,後來從阿裡跳槽了,阿裡隨後開源的 Cobar,並維持到 2013 年年初,然後,就沒有然後了。
Cobar 的思路和實現路徑的確不錯。基於 Java 開發的,實現了 MySQL 公開的二進位傳輸協議,巧妙地將自己偽裝成一個 MySQL Server,目前市面上絕大多數 MySQL 客戶端工具和應用都能相容。比自己實現一個新的資料庫協議要明智的多,因為生態環境在哪裡擺著。
Mycat 是基於 cobar 演變而來,對 cobar 的代碼進行了徹底的重構,使用 NIO 重構了網路模塊,並且優化了 Buffer 內核,增強了聚合,Join 等基本特性,同時相容絕大多數資料庫成為通用的資料庫中間件。
簡單的說,MyCAT就是:
·一個新穎的資料庫中間件產品支持mysql集群,或者mariadb cluster,提供高可用性數據分片集群。你可以像使用mysql一樣使用mycat。對於開發人員來說根本感覺不到mycat的存在。
2.2 Mycat支持的資料庫
2.3 Mycat的分片策略
2.4 概念說明
2.4.1 邏輯庫(schema) :
前面一節講了資料庫中間件,通常對實際應用來說,並不需要知道中間件的存在,業務開發人員只需要知道資料庫的概念,所以資料庫中間件可以被看做是一個或多個資料庫集群構成的邏輯庫。
2.4.2 邏輯表(table):
既然有邏輯庫,那麼就會有邏輯表,分散式資料庫中,對應用來說,讀寫數據的表就是邏輯表。邏輯表,可以是數據切分後,分佈在一個或多個分片庫中,也可以不做數據切分,不分片,只有一個表構成。
分片表:是指那些原有的很大數據的表,需要切分到多個資料庫的表,這樣,每個分片都有一部分數據,所有分片構成了完整的數據。 總而言之就是需要進行分片的表。
非分片表:一個資料庫中並不是所有的表都很大,某些表是可以不用進行切分的,非分片是相對分片表來說的,就是那些不需要進行數據切分的表。
2.4.3 分片節點(dataNode)
數據切分後,一個大表被分到不同的分片資料庫上面,每個表分片所在的資料庫就是分片節點(dataNode)。
2.4.4 節點主機(dataHost)
數據切分後,每個分片節點(dataNode)不一定都會獨占一臺機器,同一機器上面可以有多個分片資料庫,這樣一個或多個分片節點(dataNode)所在的機器就是節點主機(dataHost),為了規避單節點主機併發數限制,儘量將讀寫壓力高的分片節點(dataNode)均衡的放在不同的節點主機(dataHost)。
2.4.5 分片規則(rule)
前面講了數據切分,一個大表被分成若幹個分片表,就需要一定的規則,這樣按照某種業務規則把數據分到某個分片的規則就是分片規則,數據切分選擇合適的分片規則非常重要,將極大的避免後續數據處理的難度。
3 Mycat的下載及安裝
3.1 安裝環境
1、jdk:要求jdk必須是1.7及以上版本
2、Mysql:推薦mysql是5.5以上版本
3、Mycat:
Mycat的官方網站:
下載地址:
https://github.com/MyCATApache/Mycat-download
3.2 安裝步驟
Mycat有windows、linux多種版本。本教程為linux安裝步驟,windows基本相同。
第一步:下載Mycat-server-xxxx-linux.tar.gz
第二步:將壓縮包解壓縮。建議將mycat放到/usr/local/mycat目錄下。
第三步:進入mycat目錄,啟動mycat
./mycat start
停止:
./mycat stop
mycat 支持的命令{ console | start | stop | restart | status | dump }
Mycat的預設埠號為:8066
4 Mycat的分片
4.1 需求
把商品表分片存儲到三個數據節點上。
4.2 安裝環境分析
兩台mysql資料庫伺服器:
Host1:192.168.25.134
Host2:192.168.25.166
host1環境
操作系統版本 : centos6.4
資料庫版本 : mysql-5.6
mycat版本 :1.4 release
資料庫名 : db1、db3
mysql節點2環境
操作系統版本 : centos6.4
資料庫版本 : mysql-5.6
mycat版本 :1.4 release
資料庫名 : db2
MyCat安裝到節點1上(需要安裝jdk)
4.3 配置schema.xml
4.3.1 Schema.xml介紹
Schema.xml作為MyCat中重要的配置文件之一,管理著MyCat的邏輯庫、表、分片規則、DataNode以及DataSource。弄懂這些配置,是正確使用MyCat的前提。這裡就一層層對該文件進行解析。
schema 標簽用於定義MyCat實例中的邏輯庫
Table 標簽定義了MyCat中的邏輯表
dataNode 標簽定義了MyCat中的數據節點,也就是我們通常說所的數據分片。
dataHost標簽在mycat邏輯庫中也是作為最底層的標簽存在,直接定義了具體的資料庫實例、讀寫分離配置和心跳語句。
註意:若是LINUX版本的MYSQL,則需要設置為Mysql大小寫不敏感,否則可能會發生表找不到的問題。 在MySQL的配置文件中/etc/my.cnf [mysqld] 中增加一行 lower_case_table_names=1 |
4.3.2 Schema.xml配置
<?xml version="1.0"?> <!DOCTYPE mycat:schema SYSTEM "schema.dtd"> <mycat:schema xmlns:mycat="http://org.opencloudb/"> <schema name="e3mall" checkSQLschema="false" sqlMaxLimit="100"> <!-- auto sharding by id (long) --> <table name="tb_item" dataNode="dn1,dn2,dn3" rule="auto-sharding-long" /> </schema> <dataNode name="dn1" dataHost="localhost1" database="db1" /> <dataNode name="dn2" dataHost="localhost2" database="db2" /> <dataNode name="dn3" dataHost="localhost1" database="db3" /> <dataHost name="localhost1" maxCon="1000" minCon="10" balance="0" writeType="0" dbType="mysql" dbDriver="native" switchType="1" slaveThreshold="100"> <heartbeat>select user()</heartbeat> <!-- can have multi write hosts --> <writeHost host="hostM1" url="192.168.25.134:3306" user="root" password="root"> <!-- can have multi read hosts --> </writeHost> </dataHost> <dataHost name="localhost2" maxCon="1000" minCon="10" balance="0" writeType="0" dbType="mysql" dbDriver="native" switchType="1" slaveThreshold="100"> <heartbeat>select user()</heartbeat> <!-- can have multi write hosts --> <writeHost host="hostM1" url="192.168.25.166:3306" user="root" password="root"> <!-- can have multi read hosts --> </writeHost> </dataHost> </mycat:schema>
4.4 配置server.xml
4.4.1 Server.xml介紹
server.xml幾乎保存了所有mycat需要的系統配置信息。最常用的是在此配置用戶名、密碼及許可權。
4.4.2 Server.xml配置
<user name="test"> <property name="password">test</property> <property name="schemas">e3mall</property> <property name="readOnly">false</property> </user>
4.5 配置rule.xml
rule.xml裡面就定義了我們對錶進行拆分所涉及到的規則定義。我們可以靈活的對錶使用不同的分片演算法,或者對錶使用相同的演算法但具體的參數不同。這個文件裡面主要有tableRule和function這兩個標簽。在具體使用過程中可以按照需求添加tableRule
和function。
此配置文件可以不用修改,使用預設即可。
4.6 測試分片
4.6.1 創建表
配置完畢後,重新啟動mycat。使用mysql客戶端連接mycat,創建表。
-- ---------------------------- -- Table structure for tb_item -- ---------------------------- DROP TABLE IF EXISTS `tb_item`; CREATE TABLE `tb_item` ( `id` bigint(20) NOT NULL COMMENT '商品id,同時也是商品編號', `title` varchar(100) NOT NULL COMMENT '商品標題', `sell_point` varchar(500) DEFAULT NULL COMMENT '商品賣點', `price` bigint(20) NOT NULL COMMENT '商品價格,單位為:分', `num` int(10) NOT NULL COMMENT '庫存數量', `barcode` varchar(30) DEFAULT NULL COMMENT '商品條形碼', `image` varchar(500) DEFAULT NULL COMMENT '商品圖片', `cid` bigint(10) NOT NULL COMMENT '所屬類目,葉子類目', `status` tinyint(4) NOT NULL DEFAULT '1' COMMENT '商品狀態,1-正常,2-下架,3-刪除', `created` datetime NOT NULL COMMENT '創建時間', `updated` datetime NOT NULL COMMENT '更新時間', PRIMARY KEY (`id`), KEY `cid` (`cid`), KEY `status` (`status`), KEY `updated` (`updated`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='商品表';
4.6.2 插入數據
將此文件中的數據插入到資料庫:
4.6.3 分片測試
由於配置的分片規則為“auto-sharding-long”,所以mycat會根據此規則自動分片。
每個datanode中保存一定數量的數據。根據id進行分片
經測試id範圍為:
Datanode1:1~5000000
Datanode2:5000000~10000000
Datanode3:10000001~15000000
當15000000以上的id插入時報錯:
[Err] 1064 - can't find any valid datanode :TB_ITEM -> ID -> 15000001
此時需要添加節點了。