訂單模塊問題
1、 訂單模塊的橫向和縱向拆表。
在電商平臺中訂單表中的數據會越來越多,爲了更好的業務擴招,需要對數據庫表進行拆分。
橫向拆分就是根據不同的訂單類型拆分爲服裝訂單表、家電訂單表和其他訂單表。
縱向拆分按年份拆分,例如2018年一個表,2020年一個表。
在數據庫表拆分之後,當需要數據間從多個表中查找,這就需要dubbo的提供的特性服務分組 和 分組聚合。
dubbo服務分組
一個接口實現了多個不同數據庫表間的查詢,在dubbo中可以用group區分。
服務端
服務端中添加group屬性,自定義group名稱。
<dubbo:service group="feedback" interface="com.xxx.IndexService" />
<dubbo:service group="member" interface="com.xxx.IndexService" />
客戶端
客戶端需要調用哪個服務端提供的group組,就在reference中通過group屬性,聲明哪一個。
如果客戶端只需要調用feedback
組,這裏只需要聲明feedback
組即可,
<dubbo:reference id="feedbackIndexService" group="feedback" interface="com.xxx.IndexService" />
<dubbo:reference id="memberIndexService" group="member" interface="com.xxx.IndexService" />
也可以通過group=“*”
, 調用任意一個可以組的實現。
<dubbo:reference id="barService" interface="com.foo.BarService" group="*" />
dubbo分組聚合
上面只是對服務進行分組,而dubbo還提供了,服務調用的結果聚合。比如菜單服務,接口一樣,但有多種實現,用group區分,現在消費方需從每種group中調用一次返回結果,合併結果返回,這樣就可以實現聚合菜單項。
客戶端配置
如果需要所有分組進行聚合,只需要將group
設置爲*
, 添加merger
屬性爲true
。
<dubbo:reference interface="com.xxx.MenuService" group="*" merger="true" />
合併指定分組,如果只需要將aaa,bbb進行結果合併,示例如下:
<dubbo:reference interface="com.xxx.MenuService" group="aaa,bbb" merger="true" />
指定方法合併結果,其他未指定的方法,將只調用一個group。
<dubbo:reference interface="com.xxx.MenuService" group="*">
<dubbo:method name="getMenuItems" merger="true" />
</dubbo:reference>
某個方法不合並結果,其它都合併結果。
<dubbo:reference interface="com.xxx.MenuService" group="*" merger="true">
<dubbo:method name="getMenuItems" merger="false" />
</dubbo:reference>
指定合併策略,缺省根據返回值類型自動匹配,如果同一類型有兩個合併器時,需指定合併器的名稱。
<dubbo:reference interface="com.xxx.MenuService" group="*">
<dubbo:method name="getMenuItems" merger="mymerge" />
</dubbo:reference>
如果兩個服務的返回值類型不一樣,需要merger="mymerge"
通過指定一個合併器的類型。
dubbo提供的合併器有:
org.apache.dubbo.rpc.cluster.merger.ArrayMerger
org.apache.dubbo.rpc.cluster.merger.ListMerger
org.apache.dubbo.rpc.cluster.merger.SetMerger
org.apache.dubbo.rpc.cluster.merger.MapMerger
2、如何保證多版本的藍綠上線
在正式環境中,新版本上線時,會和老版本共同發佈在正式業務中,在正式環境中小範圍的測試一段時間,測試新版本的穩定性。
可以通過分組,進行服務的隔離調用,也可以通過dubbo中的多版本,來控制服務間的隔離。
dubbo多版本
當一個接口實現,出現不兼容升級時,可以用版本號過渡,版本號不同的服務間不引用。
可以按照以下的步驟進行版本遷移:
- 0.在低壓力時間段,先升級一半提供者爲新版本。
- 1、再將所有消費者升級爲新版本。
- 2、然後將剩下的一半提供者升級爲新版本。
老版本服務提供者配置:
<dubbo:service interface="com.foo.BarService" version="1.0.0" />
新版本服務提供者配置:
<dubbo:service interface="com.foo.BarService" version="2.0.0" />
老版本服務消費者配置:
<dubbo:reference id="barService" interface="com.foo.BarService" version="1.0.0" />
新版本服務消費者配置:
<dubbo:reference id="barService" interface="com.foo.BarService" version="2.0.0" />
如果不需要區分版本,可以按照以下的方式配置 [1]:
<dubbo:reference id="barService" interface="com.foo.BarService" version="*" />
3、服務限流
在生產環境中,比如服務器只能處理1萬個連接請求,如果超過1萬個連接請求,就會給服務器造成壓力,導致系統不可用。爲了保證服務的高可用,我們需要對請求進行限流。
- 限流措施是系統高可用的一種手段。
- 使用dubbo併發與連接控制進行限流。(實際中不推薦使用)
- 使用漏桶法和令牌桶算法進行限流。
漏桶算法
將所有請求,保存在一個隊列桶中,然後以固定的頻率,從桶中取出請求。
令牌桶算法
下面是從網上找的兩張圖來描述令牌桶算法,令牌以固定頻率向桶中添加令牌,如果桶滿了,令牌丟棄。
當有請求進來時,從桶中拿到令牌,才能進行訪問。如果拿不到令牌拒絕訪問。
漏桶算法和令牌桶算法的區別:
如果10秒內,沒有任何請求,突然進來1萬個請求,漏桶算法還是以固定的頻率進行請求,而令牌算法桶中如果有1萬個令牌,這1萬個請求就會都請求成功。後續再有別的請求時,就會被拒絕。令牌桶算法對業務峯值有很好的承載能力。
示例: 簡單實現一個令牌桶算法。
public class TokenBucket {
//桶的容量
private int bucketNums = 100;
//流入速度
private int rate = 1;
// 當前令牌數量
private int nowTokens;
// 時間
private long timestamp;
private long getNowTime() {
return System.currentTimeMillis();
}
private int min(int tokens) {
if (bucketNums > tokens) {
return tokens;
} else {
return bucketNums;
}
}
public boolean getToke() {
// 記錄來拿令牌的時間
long nowTime = getNowTime();
// 添加令牌 【判斷該有多少個令牌】
nowTokens = nowTokens + (int)((nowTime - timestamp) * rate);
// 添加以後的令牌數量與桶的容量那個小
nowTokens = min(nowTokens);
// 修改拿令牌的時間
timestamp = nowTime;
// 判斷令牌是否足夠。
if (nowTokens >= 1) {
nowTokens -= 1;
return true;
} else {
return false;
}
}
public static void main(String[] args) {
TokenBucket tokenBucker = new TokenBucket();
}
}
業務中的使用:請求進來時,去獲取令牌,獲取成功,返回true。
private static TokenBucket bucket = new TokenBucket();
/**
* 下訂單購票
* @return
*/
@RequestMapping(value = "buyTickets", method = RequestMethod.POST)
public ResponseVO buyTickets(Integer fieldId, String soldSeats, String seatsName) {
try {
if (bucket.getToke()) {
// 驗證售出的票是否爲真。
boolean trueSeats = orderServiceApi.isTrueSeats(fieldId + "", soldSeats);
// 已經銷售的座位裏,有沒有這些座位。
boolean notSoldSeats = orderServiceApi.isNotSoldSeats(fieldId + "", soldSeats);
// 驗證上述兩個內容有一個不爲真,則不創建訂單信息。
if (trueSeats && notSoldSeats) {
// 創建訂單信息
String userId = CurrentUser.getUserId();
if (StringUtils.isBlank(userId)) {
return ResponseVO.serviceFail("用戶未登錄!");
}
OrderVO orderVO = orderServiceApi.saveOrderInfo(fieldId, soldSeats, seatsName, userId);
if (orderVO == null) {
log.error("購票業務異常");
return ResponseVO.serviceFail("購票業務異常");
} else {
return ResponseVO.success(orderVO);
}
} else {
log.error("購票業務異常");
return ResponseVO.serviceFail("訂單中座位編號有問題");
}
} else {
return ResponseVO.serviceFail("購票人數過多,請稍後再試!");
}
} catch (Exception e) {
log.error("購票業務異常", e);
return ResponseVO.serviceFail("購票業務異常");
}
}
4、Hystrix熔斷降級
在多服務環境中,比如訂單服務會調用商品服務、用戶服務、積分服務多個服務,如果商品服務掛掉了或者服務調用超時等問題,下訂單服務就會因爲長時間請求導致下單失敗,導致用戶體驗不佳。此時我們就可以通過熔斷降級的方式,不是直接給用戶反回一個異常,而是返回一個網絡不佳呀,或者其他問題。給用戶一個折中、最小影響的方案返回。
hystrix 應用示例
1、 添加依賴
<!-- hystrix依賴 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
<version>2.0.0.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId>
<version>2.0.0.RELEASE</version>
</dependency>
2、 啓動類添加註解
@EnableHystrixDashboard
@EnableCircuitBreaker
@EnableHystrix
@SpringBootApplication(scanBasePackages = {"com.stylefeng.guns"})
@EnableDubboConfiguration
@EnableAsync
@EnableHystrixDashboard
@EnableCircuitBreaker
@EnableHystrix
public class GatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
}
}
3、 在需要熔斷的方法上,添加註解配置和添加降級回調方法。
@HystrixCommand(fallbackMethod = "error", commandProperties = {
@HystrixProperty(name = "execution.isolation.strategy", value = "THREAD"),
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "4000"),
@HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "10"),
@HystrixProperty(name = "circuitBreaker.errorThresholdPercentage", value = "50")
},
threadPoolProperties = { // hystrix 中線程池配置
@HystrixProperty(name = "coreSize", value = "1"),
@HystrixProperty(name = "maxQueueSize", value = "10"),
@HystrixProperty(name = "keepAliveTimeMinutes", value = "1000"),
@HystrixProperty(name = "queueSizeRejectionThreshold", value = "8"),
@HystrixProperty(name = "metrics.rollingStats.numBuckets", value = "12"),
@HystrixProperty(name = "metrics.rollingStats.timeInMilliseconds", value = "1500")
})
@RequestMapping(value = "buyTickets", method = RequestMethod.POST)
public ResponseVO buyTickets(Integer fieldId, String soldSeats, String seatsName) {
try {
if (bucket.getToke()) {
// 驗證售出的票是否爲真。
boolean trueSeats = orderServiceApi.isTrueSeats(fieldId + "", soldSeats);
// 已經銷售的座位裏,有沒有這些座位。
boolean notSoldSeats = orderServiceApi.isNotSoldSeats(fieldId + "", soldSeats);
// 驗證上述兩個內容有一個不爲真,則不創建訂單信息。
if (trueSeats && notSoldSeats) {
// 創建訂單信息
String userId = CurrentUser.getUserId();
if (StringUtils.isBlank(userId)) {
return ResponseVO.serviceFail("用戶未登錄!");
}
OrderVO orderVO = orderServiceApi.saveOrderInfo(fieldId, soldSeats, seatsName, userId);
if (orderVO == null) {
log.error("購票業務異常");
return ResponseVO.serviceFail("購票業務異常");
} else {
return ResponseVO.success(orderVO);
}
} else {
log.error("購票業務異常");
return ResponseVO.serviceFail("訂單中座位編號有問題");
}
} else {
return ResponseVO.serviceFail("購票人數過多,請稍後再試!");
}
} catch (Exception e) {
log.error("購票業務異常", e);
return ResponseVO.serviceFail("購票業務異常");
}
}
自定義熔斷方法,和fallbackMethod屬性的值和定義的方法名一致。
/**
* 註解在哪個方法上,返回值和方法參數一定要一致。
* @param fieldId
* @param soldSeats
* @param seatsName
* @return
*/
public ResponseVO error(Integer fieldId, String soldSeats, String seatsName) {
return ResponseVO.serviceFail("抱歉,下單人太多,請稍後再試!");
}