博主之前做過恒豐銀行代收付系統(相當于支付接口),包括現在的oltpapi交易接口和虛擬業務的對外提供數據接口。總之,當你做了很多項目寫了很多代碼的時候,就需要回過頭來,多總結總結,這樣你會看到更多之前寫代碼的時候看不到的東西,也能更明白為什么要這樣做。
接口無非就是客戶端請求你的接口地址,并傳入一堆該接口定義好的參數,通過接口自身的邏輯處理,返回接口約定好的數據以及相應的數據格式。
接口由于本身的性質,由于和合作方對接數據,所以有以下幾點需要在開發的時候注意:
(1) 定義接口入參:寫好接口文檔
(2) 定義接口返回數據類型:一般都需要封裝成一定格式,確定返回json還是xml報文等
見如下返回數據定義格式:
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; }}
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) 定義一套全局統一并通用的返回碼,以幫助排查問題:
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@ResponseBodypublic 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方法編寫業務攔截器。
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; @Componentpublic 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; } } }
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@ComponentScanpublic 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過期時間目前暫時定為1天,由于考慮到合作方往往是分布式環境,多臺機器都有可能申請token,為了降低合作方保證token一致性的難度,調用接口創建token成功以后一分鐘以內,再次請求token返回的數據是一樣的。
獲取用于數字簽名的私鑰,第三方獲取的私鑰需妥善保存,并定期更新,私鑰只參與數字簽名,不作為參數傳輸。
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和一些超時問題。
這時候,你可能遇到了高并發和高可用問題。但是,不管遇到什么問題,都不能臆斷和亂改,你得需要找到慢的原因,才能對癥下藥,亂改可能會導致其他問題的出現。首先,解決高并發問題的三個方向是負載均衡,緩存和集群。
(1) 負載均衡
我們使用的是阿里云服務器的負載均衡,后臺分布式服務管理,我們運維小哥哥搭建了一套k8s,可以自由在k8s上擴展服務節點,各個服務結點也能隨內存的使用自動漂移,不用多說,k8s真的很厲害,感興趣的同學可以詳細去學。那么問題來了,阿里云的負載均衡怎么對應到k8s的負載均衡呢?
這個涉及到了k8s的service暴露的一些特點,簡單說就是k8s把所有集群的服務都通過指定的內部負載均衡,在指定的服務器上暴露,然后我們又把這幾個服務器接在阿里云負載均衡下,這個涉及的細節和配置很多。當然,除nginx外,還有其他負載均衡解決方案,軟件硬件都有,硬件如f5等。
阿里云的nginx負載均衡,我們使用的是加權輪詢策略,其實輪詢是最低效的方式;
這就是最基本的負載均衡實例,但這不足以滿足實際需求;目前Nginx服務器的upstream模塊支持6種方式的分配:
負載均衡策略:
輪詢 | 默認方式 |
weight | 權重方式 |
ip_hash | 依據ip分配方式 |
least_conn | 最少連接方式 |
fair(第三方) | 響應時間方式 |
url_hash(第三方) | 依據URL分配方式 |
(2) 集群
首先,通過排查問題,發現是oltpapi接口服務處理請求很慢,大量請求過來,總是超時和中斷連接,這時候,我們想著最簡單的方法就是加機器,給oltp接口服務多加幾臺機器。
嗯,一切都很完美,如預期進行,但是加到一定數量,你發現,怎么不起效果,異步響應還是很慢,或者更直觀的說,消息隊列出現了嚴重的消息堆積。這時候,你發現出現了新的問題或者瓶頸,這個問題已經不是說加oltp服務器能解決了,那么,就需要去重新定位問題。發現是消息堆積,消息堆積就是生產者過快,導致消費者消費不過來,這時候,你就需要增加消費者的消費數量。給風控系統多加幾臺機器,讓消費者和生產者達到一定平衡。
這里有個誤區,你可能以為是rocketmq的broker數量過少,增加broker數量,其實當消費者和生產者保持一樣的速度時,消息肯定不對堆積,按照原始的broker數量就足夠。但是增加broker也會使得消息得到盡快的處理,提升一定效率。
(3) 緩存
當加機器不能解決問題時,或者說沒那么多服務器可使用時,那么就要重代碼層面解決高并發問題。Redis 是一個高性能的key-value數據庫,當獲取數據從數據庫拿很慢時,就可以存儲到redis,從redis取值。
高可用問題應該上升到整個服務的架構問題上,就是說在搭建整體系統是就應該考慮到。高可用問題是以單點故障,訪問速度慢的問題為主導。見 服務高可用
下面對接口開發服務做一些總結:
當接口作為數據源時,還要考慮數據是讓合作方主動過來拉還是數據有變化就推送呢,當然是推的效果更好,但是如何有效的推數據,不推重復數據等都是需要根據實際業務考慮的問題。
當接口服務和合作方都處于分布式情況下,就很容易出現一個訂單號申請多次交易請求,但是根據冪等性,一張彩票只能交易一次,并且每次不管何時請求,結果都應該一樣不會改變。這種情況下,我們怎么保證唯一性呢,我們需要把該訂單和訂單狀態存redis,每次請求時去看是否訂單已存在。但可能這次交易不成功,下次這張票還可以繼續交易,可以生成新的訂單號啊。
redis的setNX是一個很好的解決方案,意思是當存在該key時,返回false,當沒有時,該key和value插入成功。用作檢查訂單是否正在提交,如果是,則阻塞本次請求,避免重復提交 ,可以設置過期時間3s。提交之前鎖定訂單,防止重復提交。
總之,博主發現,在高并發場景下,導致服務崩潰的原因還是redis和數據庫,可能是redis讀寫太慢,或者數據庫的一些sql使用不當,或者沒建索引導致讀寫很慢。
總之,這是一條很漫長的路,我們都需要慢慢積累經驗和學習前人更優秀的解決辦法。
本文鏈接:http://www.www897cc.com/showinfo-26-61910-0.html如何設計一個安全的對外接口,老司機總結了這幾點...
聲明:本網頁內容旨在傳播知識,若有侵權等問題請及時與本網聯系,我們將在第一時間刪除處理。郵件:2376512515@qq.com
上一篇: 未來世界的12個軟件開發預測