天天看點

優雅的實作對外接口,要注意哪些問題?

部落客之前做過XX銀行代收付系統(相當于支付接口),包括現在的oltpapi交易接口和虛拟業務的對外提供資料接口。

總之,當你做了很多項目寫了很多代碼的時候,就需要回過頭來,多總結總結,這樣你會看到更多之前寫代碼的時候看不到的東西,也能更明白為什麼要這樣做。

做接口需要考慮的問題

什麼是接口

接口無非就是用戶端請求你的接口位址,并傳入一堆該接口定義好的參數,通過接口自身的邏輯處理,傳回接口約定好的資料以及相應的資料格式。

接口怎麼開發

接口由于本身的性質,由于和合作方對接資料,是以有以下幾點需要在開發的時候注意:

1、定義接口入參:寫好接口文檔

2、定義接口傳回資料類型:一般都需要封裝成一定格式,确定傳回json還是xml封包等

優雅的實作對外接口,要注意哪些問題?

見如下傳回資料定義格式:

Result

package com.caiex.vb.model;

import java.io.Serializable;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlType;

@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(name = "Result", propOrder = { "resultCode", "resultMsg" })
public class Result implements Serializable {
 private static final long serialVersionUID = 10L;
 protected int resultCode;
 protected String resultMsg;

 public int getResultCode() {
  return this.resultCode;
 }

 public void setResultCode(int value) {
  this.resultCode = value;
 }

 public String getResultMsg() {
  return this.resultMsg;
 }

 public void setResultMsg(String value) {
  this.resultMsg = value;
 }
}      

Response

package com.caiex.vb.model;

import java.io.Serializable;

public class Response implements Serializable {

 private static final long serialVersionUID = 2360867989280235575L;

 private Result result;

 private Object data;

 public Result getResult() {
  if (this.result == null) {
   this.result = new Result();
  }
  return result;
 }

 public void setResult(Result result) {
  this.result = result;
 }

 public Object getData() {
  return data;
 }

 public void setData(Object data) {
  this.data = data;
 }

}      

3、确定通路接口的方式,get or post等等,可以根據restful接口定義規則RESTful API:RESTful API

4、定義一套全局統一并通用的傳回碼,以幫助排查問題;

reponse code
public static int NO_AGENT_RATE = 1119;  //未找到兌換率

 public static int SCHEME_COMMIT_FAIL = 4000;  //方案送出失敗

 public static int SCHEME_CONFIRMATION = 4001;  //方案确認中

 public static int SCHEME_NOT_EXIST = 4002;  //方案不存在

 public static int SCHEME_CANCEL= 4005;  //方案不存在

 //。。。。      

5、統一的異常處理:應該每個系統都需要一套統一的異常處理

package com.caiex.vb.interceptor;

import javax.servlet.http.HttpServletRequest;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;

import com.caiex.vb.model.Response;

@ControllerAdvice
@ResponseBody
public class GlobalExceptionHandler {

 private  Logger  logger = LoggerFactory.getLogger(this.getClass()); 

    /**
     * 所有異常報錯
     * @param request
     * @param exception
     * @return
     * @throws Exception
     */
    @ExceptionHandler(value=Exception.class)  
    public Response allExceptionHandler(HttpServletRequest request,  
            Exception exception) throws Exception  
    {  
     logger.error("攔截到異常:", exception);
        Response response = new Response();
        response.setData(null);
        response.getResult().setResultCode(9999);
        response.getResult().setResultMsg("系統繁忙");
        return response;  
    }  

}      

6、攔截器鍊設定:合作方通路接口的時候,會根據你接口定義好的傳參通路你的接口伺服器,但是會存在接口參數類型錯誤或者格式不對,必傳參數沒傳的問題,甚至一些惡意請求,都可以通過攔截器鍊進行前期攔截,避免造成接口服務的壓力。

還有很重要的一點,加簽驗簽也可以在攔截器設定。繼承WebMvcConfigurerAdapter實作springboot的攔截器鍊。實作HandlerInterceptor方法編寫業務攔截器。

SignInterceptor

package com.caiex.vb.interceptor;


import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.commons.lang3.StringUtils;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import com.alibaba.fastjson.JSON;
import com.caiex.redis.service.api.RedisApi;
import com.caiex.vb.model.Response;
import com.caiex.vb.utils.CaiexCheckUtils;

@Component
public class SignInterceptor extends BaseValidator implements HandlerInterceptor{

 private Logger logger = LogManager.getLogger(this.getClass());

 @Resource
 private RedisApi redisApi;


 public void afterCompletion(HttpServletRequest arg0,
   HttpServletResponse arg1, Object arg2, Exception arg3)
   throws Exception {
  // TODO Auto-generated method stub

 }

 public void postHandle(HttpServletRequest arg0, HttpServletResponse arg1,
   Object arg2, ModelAndView arg3) throws Exception {
  // TODO Auto-generated method stub

 }

 public boolean preHandle(HttpServletRequest arg0, HttpServletResponse arg1,
   Object arg2) throws Exception {
  if(isTestIpAddr(arg0)){
   return true;
  }
  String securityKey = redisApi.hGet("securityKey", arg0.getParameter("agentid"));
  if(StringUtils.isEmpty(securityKey)){
   Response response = new Response();
   response.setData(null);
   response.getResult().setResultCode(8001);
   response.getResult().setResultMsg("缺少私鑰, 管道号:" + arg0.getParameter("agentid"));
   logger.error("缺少私鑰, 管道号:" + arg0.getParameter("agentid"));
   InterceptorResp.printJson(arg1, response);
   return false;
  }

  if(StringUtils.isEmpty(arg0.getParameter("sign")) || !arg0.getParameter("sign").equals(CaiexCheckUtils.getSign(arg0.getParameterMap(), securityKey))){
   Response response = new Response();
   response.setData(null);
   response.getResult().setResultCode(3203);
   response.getResult().setResultMsg("參數簽名認證失敗");
   logger.error("參數簽名認證失敗:" + JSON.toJSONString(arg0.getParameterMap()) + " securityKey = " + securityKey);
   InterceptorResp.printJson(arg1, response);
   return false;
  }else{
   return true;
  }
 }
}      

WebAppConfigurer

package com.caiex.oltp.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;

import com.caiex.oltp.interceptor.APILimitRateValidator;
import com.caiex.oltp.interceptor.CommonValidator;
import com.caiex.oltp.interceptor.DDSAuthValidator;
import com.caiex.oltp.interceptor.QueryPriceParamsValidator;
import com.caiex.oltp.interceptor.TradeParamsValidator;


@EnableWebMvc
@Configuration
@ComponentScan
public class WebAppConfigurer extends WebMvcConfigurerAdapter {

   @Bean
   CommonValidator commonInterceptor() {
         return new CommonValidator();
     }

   @Bean
   DDSAuthValidator ddsAuthInterceptor() {
         return new DDSAuthValidator();
     }

   @Bean
   QueryPriceParamsValidator queryPriceParamsInterceptor() {
         return new QueryPriceParamsValidator();
     }

   @Bean
   TradeParamsValidator tradeParamsInterceptor() {
         return new TradeParamsValidator();
     }

  @Bean
   APILimitRateValidator aPILimitRateInterceptor() {
         return new APILimitRateValidator();
     }


     @Override
     public void addInterceptors(InterceptorRegistry registry) {

      //通路速率限制
      registry.addInterceptor(aPILimitRateInterceptor())
      .addPathPatterns("/*/*");
      //.addPathPatterns("/price/getPriceParam");

      //參數簽名認證
         registry.addInterceptor(ddsAuthInterceptor())
         .addPathPatterns("/tradeState/*")
         .addPathPatterns("/recycle/*")
         .addPathPatterns("/matchInfo/*")
         .addPathPatterns("/price/tradeTicketParam");

         //公共參數檢查
         registry.addInterceptor(commonInterceptor())
         .addPathPatterns("/price/tradeTicketParam")
         .addPathPatterns("/tradeState/*")
         .addPathPatterns("/recycle/*");

         //詢價參數校驗
         registry.addInterceptor(queryPriceParamsInterceptor())
         .addPathPatterns("/price/getPriceParam");

         //交易參數檢查
         registry.addInterceptor(tradeParamsInterceptor())
         .addPathPatterns("/price/tradeTicketParam");

         super.addInterceptors(registry);
     }
}      

7、token令牌和sign數字簽名實作資料保密性。

建立令牌(Token)

為保證請求的合法性,我們提供第三方建立令牌接口,某些接口需要通過token驗證消息的合法性,以免遭受非法攻擊。

token過期時間目前暫時定為1天,由于考慮到合作方往往是分布式環境,多台機器都有可能申請token,為了降低合作方保證token一緻性的難度,調用接口建立token成功以後一分鐘以内,再次請求token傳回的資料是一樣的。

擷取私鑰

擷取用于數字簽名的私鑰,第三方擷取的私鑰需妥善儲存,并定期更新,私鑰隻參與數字簽名,不作為參數傳輸。

數字簽名方式:

參數簽名;簽名方式:所有值不為null的參數(不包括本參數)均參與數字簽名,按照“參數名+參數值+私鑰”的格式得到一個字元串,再将這個字元串MD5一次就是這個參數的值。(示例:h15adc39y9ba59abbe56e057e60f883g),是以需要先擷取私鑰。

驗簽方式:

将使用者的所有非null參數放入定義好排序規則的TreeSet中進行排序,再用StringBuilder按照按照“參數名+參數值+私鑰”的格式得到一個字元串(私鑰從redis拿),再将這個字元串MD5一次就是這個參數的值。将這個值與使用者傳來的sign簽名對比,相同則通過,否則不通過。

private String createToken(){
  String utk = "Msk!D*"+System.currentTimeMillis()+"UBR&FLP";
  logger.info("create token   --- "+Md5Util.md5(utk));
  return Md5Util.md5(utk);
 }      

8、接口限流

有時候伺服器壓力真的太大,以防交易接口被擠死,就可以對一些其他不影響主要業務功能并且計算量大的接口做限流處理。RateLimit--使用guava來做接口限流,當接口超過指定的流量時,就不處理該接口的請求。詳細可看RateLimit。也可參考其他限流架構。

9、協定加密,http更新成https;

為什麼要更新呢,為了保證資料的安全性。當使用https通路時,資料從用戶端到服務斷,服務端到用戶端都加密,即使黑客抓包也看不到傳輸内容。當然還有其他好處,這裡不多講。但這也是開發接口項目需要注意的一個問題。

如何提高接口的高并發和高可用

接口開發好了,接下來就讨論接口的可用性問題。首先我們要将高并發和高可用區分一下,畢竟高可用是在可用的情況,隻是很慢或者效率不高。其實也可以歸為一類問題,但是不重要啦,重要的是怎麼提高你寫的接口的通路速度和性能。

接口的高并發解決方案(其實沒有唯一答案,業界針對不同業務也有很多不同的方法)

當通路一個接口擷取資料時,發現傳回很慢,或者總是逾時,如果排除網絡的原因,那就是接口伺服器壓力太大,處理不過來了。在世界杯期間,我們檢視背景日志總是connection by reset和borker pipe和一些逾時問題。

這時候,你可能遇到了高并發和高可用問題。但是,不管遇到什麼問題,都不能臆斷和亂改,你得需要找到慢的原因,才能對症下藥,亂改可能會導緻其他問題的出現。首先,解決高并發問題的三個方向是負載均衡,緩存和叢集。

負載均衡

我們使用的是阿裡雲伺服器的負載均衡,背景分布式服務管理,我們運維小哥哥搭建了一套k8s,可以自由在k8s上擴充服務節點,各個服務結點也能随記憶體的使用自動漂移,不用多說,k8s真的很厲害,感興趣的同學可以詳細去學。

那麼問題來了,阿裡雲的負載均衡怎麼對應到k8s的負載均衡呢?這個涉及到了k8s的service暴露的一些特點,簡單說就是k8s把所有叢集的服務都通過指定的内部負載均衡,在指定的伺服器上暴露,然後我們又把這幾個伺服器接在阿裡雲負載均衡下,這個涉及的細節和配置很多。

當然,除nginx外,還有其他負載均衡解決方案,軟體硬體都有,硬體如f5等。

阿裡雲的nginx負載均衡,我們使用的是權重輪詢政策,其實輪詢是最低效的方式;

這就是最基本的負載均衡執行個體,但這不足以滿足實際需求;目前Nginx伺服器的upstream子產品支援6種方式的配置設定:

負載均衡政策

優雅的實作對外接口,要注意哪些問題?

叢集

首先,通過排查問題,發現是oltpapi接口服務處理請求很慢,大量請求過來,總是逾時和中斷連接配接,這時候,我們想着最簡單的方法就是加機器,給oltp接口服務多加幾台機器。

嗯,一切都很完美,如預期進行,但是加到一定數量,你發現,怎麼不起效果,異步響應還是很慢,或者更直覺的說,消息隊列出現了嚴重的消息堆積。這時候,你發現出現了新的問題或者瓶頸,這個問題已經不是說加oltp伺服器能解決了,那麼,就需要去重新定位問題。

發現是消息堆積,消息堆積就是生産者過快,導緻消費者消費不過來,這時候,你就需要增加消費者的消費數量。給風控系統多加幾台機器,讓消費者和生産者達到一定平衡。

這裡有個誤區,你可能以為是rocketmq的broker數量過少,增加broker數量,其實當消費者和生産者保持一樣的速度時,消息肯定不對堆積,按照原始的broker數量就足夠。但是增加broker也會使得消息得到盡快的處理,提升一定效率。

緩存

當加機器不能解決問題時,或者說沒那麼多伺服器可使用時,那麼就要重代碼層面解決高并發問題。Redis 是一個高性能的key-value資料庫,當擷取資料從資料庫拿很慢時,就可以存儲到redis,從redis取值。

用ConcurrentHashMap緩存對象,并設定過期時間

redis緩存資料,結合spring定時任務定時擷取不會經常改動的key

提高使用redis的效率:比如使用mGet一次擷取多個key

....等

接口高可用問題

高可用問題應該上升到整個服務的架構問題上,就是說在搭建整體系統是就應該考慮到。高可用問題是以單點故障,通路速度慢的問題為主導。

redis主從分布式(redis的單點故障和通路速度的提高和主從備份)

分布式dubbo服務的zookeeper主從叢集

strom的主從叢集

...等

總結

下面對接口開發服務做一些總結:

1.是拉還是推:

當接口作為資料源時,還要考慮資料是讓合作方主動過來拉還是資料有變化就推送呢,當然是推的效果更好,但是如何有效的推資料,不推重複資料等都是需要根據實際業務考慮的問題。

2.多台分布式伺服器上,怎麼保證交易的幂等和訂單的唯一性

當接口服務和合作方都處于分布式情況下,就很容易出現一個訂單号申請多次交易請求,但是根據幂等性,一張彩票隻能交易一次,并且每次不管何時請求,結果都應該一樣不會改變。

這種情況下,我們怎麼保證唯一性呢,我們需要把該訂單和訂單狀态存redis,每次請求時去看是否訂單已存在。但可能這次交易不成功,下次這張票還可以繼續交易,可以生成新的訂單号啊。

redis的setNX是一個很好的解決方案,意思是當存在該key時,傳回false,當沒有時,該key和value插入成功。用作檢查訂單是否正在送出,如果是,則阻塞本次請求,避免重複送出 ,可以設定過期時間3s。送出之前鎖定訂單,防止重複送出。

3.處理時間超過10s,自動傳回該訂單交易失敗

總之,部落客發現,在高并發場景下,導緻服務崩潰的原因還是redis和資料庫,可能是redis讀寫太慢,或者資料庫的一些sql使用不當,或者沒建索引導緻讀寫很慢。

原文連結:

https://blog.csdn.net/xiaolizh/article/details/83011031

版權聲明:本文為CSDN部落客「xiaolizh」的原創文章,遵循CC 4.0 BY-SA版權協定,轉載請附上原文出處連結及本聲明。

優雅的實作對外接口,要注意哪些問題?