做接口需要考慮的問(wèn)題 什么是接口 接口無(wú)非就是客戶端請(qǐng)求你的接口地址,并傳入一堆該接口定義好的參數(shù),通過(guò)接口自身的邏輯處理,返回接口約定好的數(shù)據(jù)以及相應(yīng)的數(shù)據(jù)格式。
接口怎么開(kāi)發(fā) 接口由于本身的性質(zhì),由于和合作方對(duì)接數(shù)據(jù),所以有以下幾點(diǎn)需要在開(kāi)發(fā)的時(shí)候注意:
1.定義接口入?yún)ⅲ簩懞媒涌谖臋n
2.定義接口返回?cái)?shù)據(jù)類型:一般都需要封裝成一定格式,確定返回json還是xml報(bào)文等
見(jiàn)如下返回?cái)?shù)據(jù)定義格式:
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.確定訪問(wèn)接口的方式,get or post等等,可以根據(jù)restful接口定義規(guī)則RESTful API:RESTful API
4.定義一套全局統(tǒng)一并通用的返回碼,以幫助排查問(wèn)題;
public static int NO_AGENT_RATE = 1119 ; //未找到兌換率 public static int SCHEME_COMMIT_FAIL = 4000 ; //方案提交失敗 public static int SCHEME_CONFIRMATION = 4001 ; //方案確認(rèn)中 public static int SCHEME_NOT_EXIST = 4002 ; //方案不存在 public static int SCHEME_CANCEL= 4005 ; //方案不存在 //。。。。
5.統(tǒng)一的異常處理:應(yīng)該每個(gè)系統(tǒng)都需要一套統(tǒng)一的異常處理
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()); /** * 所有異常報(bào)錯(cuò) * @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("系統(tǒng)繁忙" ); return response; } }
6.攔截器鏈設(shè)置:合作方訪問(wèn)接口的時(shí)候,會(huì)根據(jù)你接口定義好的傳參訪問(wèn)你的接口服務(wù)器,但是會(huì)存在接口參數(shù)類型錯(cuò)誤或者格式不對(duì),必傳參數(shù)沒(méi)傳的問(wèn)題,甚至一些惡意請(qǐng)求,都可以通過(guò)攔截器鏈進(jìn)行前期攔截,避免造成接口服務(wù)的壓力。
學(xué)習(xí)資料: Java進(jìn)階視頻資源
還有很重要的一點(diǎn),加簽驗(yàn)簽也可以在攔截器設(shè)置。繼承WebMvcConfigurerAdapter實(shí)現(xiàn)springboot的攔截器鏈。實(shí)現(xiàn)HandlerInterceptor方法編寫業(yè)務(wù)攔截器。
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("缺少私鑰, 渠道號(hào):" + arg0.getParameter("agentid" )); logger.error("缺少私鑰, 渠道號(hào):" + 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("參數(shù)簽名認(rèn)證失敗" ); logger.error("參數(shù)簽名認(rèn)證失?。?quot; + 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 @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) { //訪問(wèn)速率限制 registry.addInterceptor(aPILimitRateInterceptor()) .addPathPatterns("/*/*" ); //.addPathPatterns("/price/getPriceParam"); //參數(shù)簽名認(rèn)證 registry.addInterceptor(ddsAuthInterceptor()) .addPathPatterns("/tradeState/*" ) .addPathPatterns("/recycle/*" ) .addPathPatterns("/matchInfo/*" ) .addPathPatterns("/price/tradeTicketParam" ); //公共參數(shù)檢查 registry.addInterceptor(commonInterceptor()) .addPathPatterns("/price/tradeTicketParam" ) .addPathPatterns("/tradeState/*" ) .addPathPatterns("/recycle/*" ); //詢價(jià)參數(shù)校驗(yàn) registry.addInterceptor(queryPriceParamsInterceptor()) .addPathPatterns("/price/getPriceParam" ); //交易參數(shù)檢查 registry.addInterceptor(tradeParamsInterceptor()) .addPathPatterns("/price/tradeTicketParam" ); super .addInterceptors(registry); } }
7.token令牌和sign數(shù)字簽名實(shí)現(xiàn)數(shù)據(jù)保密性。
創(chuàng)建令牌(Token) 為保證請(qǐng)求的合法性,我們提供第三方創(chuàng)建令牌接口,某些接口需要通過(guò)token驗(yàn)證消息的合法性,以免遭受非法攻擊。
token過(guò)期時(shí)間目前暫時(shí)定為1天,由于考慮到合作方往往是分布式環(huán)境,多臺(tái)機(jī)器都有可能申請(qǐng)token,為了降低合作方保證token一致性的難度,調(diào)用接口創(chuàng)建token成功以后一分鐘以內(nèi),再次請(qǐng)求token返回的數(shù)據(jù)是一樣的。
獲取私鑰 獲取用于數(shù)字簽名的私鑰,第三方獲取的私鑰需妥善保存,并定期更新,私鑰只參與數(shù)字簽名,不作為參數(shù)傳輸。
數(shù)字簽名方式:
參數(shù)簽名;簽名方式:所有值不為null的參數(shù)(不包括本參數(shù))均參與數(shù)字簽名,按照“參數(shù)名+參數(shù)值+私鑰”的格式得到一個(gè)字符串,再將這個(gè)字符串MD5一次就是這個(gè)參數(shù)的值。(示例:h15adc39y9ba59abbe56e057e60f883g
),所以需要先獲取私鑰。
驗(yàn)簽方式:
將用戶的所有非null參數(shù)放入定義好排序規(guī)則的TreeSet中進(jìn)行排序,再用StringBuilder按照按照“參數(shù)名+參數(shù)值+私鑰”的格式得到一個(gè)字符串(私鑰從redis拿),再將這個(gè)字符串MD5一次就是這個(gè)參數(shù)的值。將這個(gè)值與用戶傳來(lái)的sign簽名對(duì)比,相同則通過(guò),否則不通過(guò)。
private String createToken () { String utk = "Msk!D*" +System.currentTimeMillis()+"UBR&FLP" ; logger.info("create token --- " +Md5Util.md5(utk)); return Md5Util.md5(utk); }
8.接口限流
有時(shí)候服務(wù)器壓力真的太大,以防交易接口被擠死,就可以對(duì)一些其他不影響主要業(yè)務(wù)功能并且計(jì)算量大的接口做限流處理。RateLimit--使用guava來(lái)做接口限流,當(dāng)接口超過(guò)指定的流量時(shí),就不處理該接口的請(qǐng)求。詳細(xì)可看RateLimit。也可參考其他限流框架。
9.協(xié)議加密,http升級(jí)成https;
為什么要升級(jí)呢,為了保證數(shù)據(jù)的安全性。當(dāng)使用https訪問(wèn)時(shí),數(shù)據(jù)從客戶端到服務(wù)斷,服務(wù)端到客戶端都加密,即使黑客抓包也看不到傳輸內(nèi)容。當(dāng)然還有其他好處,這里不多講。但這也是開(kāi)發(fā)接口項(xiàng)目需要注意的一個(gè)問(wèn)題。
如何提高接口的高并發(fā)和高可用 接口開(kāi)發(fā)好了,接下來(lái)就討論接口的可用性問(wèn)題。首先我們要將高并發(fā)和高可用區(qū)分一下,畢竟高可用是在可用的情況,只是很慢或者效率不高。其實(shí)也可以歸為一類問(wèn)題,但是不重要啦,重要的是怎么提高你寫的接口的訪問(wèn)速度和性能。
接口的高并發(fā)解決方案(其實(shí)沒(méi)有唯一答案,業(yè)界針對(duì)不同業(yè)務(wù)也有很多不同的方法) 當(dāng)訪問(wèn)一個(gè)接口獲取數(shù)據(jù)時(shí),發(fā)現(xiàn)返回很慢,或者總是超時(shí),如果排除網(wǎng)絡(luò)的原因,那就是接口服務(wù)器壓力太大,處理不過(guò)來(lái)了。在世界杯期間,我們查看后臺(tái)日志總是connection by reset和borker pipe和一些超時(shí)問(wèn)題。
這時(shí)候,你可能遇到了高并發(fā)和高可用問(wèn)題。但是,不管遇到什么問(wèn)題,都不能臆斷和亂改,你得需要找到慢的原因,才能對(duì)癥下藥,亂改可能會(huì)導(dǎo)致其他問(wèn)題的出現(xiàn)。首先,解決高并發(fā)問(wèn)題的三個(gè)方向是負(fù)載均衡,緩存和集群。
學(xué)習(xí)資料: Java進(jìn)階視頻資源
負(fù)載均衡 我們使用的是阿里云服務(wù)器的負(fù)載均衡,后臺(tái)分布式服務(wù)管理,我們運(yùn)維小哥哥搭建了一套k8s,可以自由在k8s上擴(kuò)展服務(wù)節(jié)點(diǎn),各個(gè)服務(wù)結(jié)點(diǎn)也能隨內(nèi)存的使用自動(dòng)漂移,不用多說(shuō),k8s真的很厲害,感興趣的同學(xué)可以詳細(xì)去學(xué)。那么問(wèn)題來(lái)了,阿里云的負(fù)載均衡怎么對(duì)應(yīng)到k8s的負(fù)載均衡呢?
這個(gè)涉及到了k8s的service暴露的一些特點(diǎn),簡(jiǎn)單說(shuō)就是k8s把所有集群的服務(wù)都通過(guò)指定的內(nèi)部負(fù)載均衡,在指定的服務(wù)器上暴露,然后我們又把這幾個(gè)服務(wù)器接在阿里云負(fù)載均衡下,這個(gè)涉及的細(xì)節(jié)和配置很多。當(dāng)然,除nginx外,還有其他負(fù)載均衡解決方案,軟件硬件都有,硬件如f5等。
阿里云的nginx負(fù)載均衡,我們使用的是加權(quán)輪詢策略,其實(shí)輪詢是最低效的方式;
這就是最基本的負(fù)載均衡實(shí)例,但這不足以滿足實(shí)際需求;目前Nginx服務(wù)器的upstream模塊支持6種方式的分配:
負(fù)載均衡策略
輪詢 默認(rèn)方式 weight 權(quán)重方式 ip_hash 依據(jù)ip分配方式 least_conn 最少連接方式 fair(第三方) 響應(yīng)時(shí)間方式 url_hash(第三方) 依據(jù)URL分配方式
集群 首先,通過(guò)排查問(wèn)題,發(fā)現(xiàn)是oltpapi接口服務(wù)處理請(qǐng)求很慢,大量請(qǐng)求過(guò)來(lái),總是超時(shí)和中斷連接,這時(shí)候,我們想著最簡(jiǎn)單的方法就是加機(jī)器,給oltp接口服務(wù)多加幾臺(tái)機(jī)器。
嗯,一切都很完美,如預(yù)期進(jìn)行,但是加到一定數(shù)量,你發(fā)現(xiàn),怎么不起效果,異步響應(yīng)還是很慢,或者更直觀的說(shuō),消息隊(duì)列出現(xiàn)了嚴(yán)重的消息堆積。這時(shí)候,你發(fā)現(xiàn)出現(xiàn)了新的問(wèn)題或者瓶頸,這個(gè)問(wèn)題已經(jīng)不是說(shuō)加oltp服務(wù)器能解決了,那么,就需要去重新定位問(wèn)題。發(fā)現(xiàn)是消息堆積,消息堆積就是生產(chǎn)者過(guò)快,導(dǎo)致消費(fèi)者消費(fèi)不過(guò)來(lái),這時(shí)候,你就需要增加消費(fèi)者的消費(fèi)數(shù)量。給風(fēng)控系統(tǒng)多加幾臺(tái)機(jī)器,讓消費(fèi)者和生產(chǎn)者達(dá)到一定平衡。
這里有個(gè)誤區(qū),你可能以為是rocketmq的broker數(shù)量過(guò)少,增加broker數(shù)量,其實(shí)當(dāng)消費(fèi)者和生產(chǎn)者保持一樣的速度時(shí),消息肯定不對(duì)堆積,按照原始的broker數(shù)量就足夠。但是增加broker也會(huì)使得消息得到盡快的處理,提升一定效率。
緩存 當(dāng)加機(jī)器不能解決問(wèn)題時(shí),或者說(shuō)沒(méi)那么多服務(wù)器可使用時(shí),那么就要重代碼層面解決高并發(fā)問(wèn)題。Redis 是一個(gè)高性能的key-value數(shù)據(jù)庫(kù),當(dāng)獲取數(shù)據(jù)從數(shù)據(jù)庫(kù)拿很慢時(shí),就可以存儲(chǔ)到redis,從redis取值。
用ConcurrentHashMap緩存對(duì)象,并設(shè)置過(guò)期時(shí)間 redis緩存數(shù)據(jù),結(jié)合spring定時(shí)任務(wù)定時(shí)獲取不會(huì)經(jīng)常改動(dòng)的key 提高使用redis的效率:比如使用mGet一次獲取多個(gè)key 接口高可用問(wèn)題 高可用問(wèn)題應(yīng)該上升到整個(gè)服務(wù)的架構(gòu)問(wèn)題上,就是說(shuō)在搭建整體系統(tǒng)是就應(yīng)該考慮到。高可用問(wèn)題是以單點(diǎn)故障,訪問(wèn)速度慢的問(wèn)題為主導(dǎo)。見(jiàn) 服務(wù)高可用
redis主從分布式(redis的單點(diǎn)故障和訪問(wèn)速度的提高和主從備份) 分布式dubbo服務(wù)的zookeeper主從集群 總結(jié) 下面對(duì)接口開(kāi)發(fā)服務(wù)做一些總結(jié):
1.是拉還是推: 當(dāng)接口作為數(shù)據(jù)源時(shí),還要考慮數(shù)據(jù)是讓合作方主動(dòng)過(guò)來(lái)拉還是數(shù)據(jù)有變化就推送呢,當(dāng)然是推的效果更好,但是如何有效的推數(shù)據(jù),不推重復(fù)數(shù)據(jù)等都是需要根據(jù)實(shí)際業(yè)務(wù)考慮的問(wèn)題。
2.多臺(tái)分布式服務(wù)器上,怎么保證交易的冪等和訂單的唯一性 當(dāng)接口服務(wù)和合作方都處于分布式情況下,就很容易出現(xiàn)一個(gè)訂單號(hào)申請(qǐng)多次交易請(qǐng)求,但是根據(jù)冪等性,一張彩票只能交易一次,并且每次不管何時(shí)請(qǐng)求,結(jié)果都應(yīng)該一樣不會(huì)改變。這種情況下,我們?cè)趺幢WC唯一性呢,我們需要把該訂單和訂單狀態(tài)存redis,每次請(qǐng)求時(shí)去看是否訂單已存在。但可能這次交易不成功,下次這張票還可以繼續(xù)交易,可以生成新的訂單號(hào)啊。
redis的setNX是一個(gè)很好的解決方案,意思是當(dāng)存在該key時(shí),返回false,當(dāng)沒(méi)有時(shí),該key和value插入成功。用作檢查訂單是否正在提交,如果是,則阻塞本次請(qǐng)求,避免重復(fù)提交 ,可以設(shè)置過(guò)期時(shí)間3s。提交之前鎖定訂單,防止重復(fù)提交。
3.處理時(shí)間超過(guò)10s,自動(dòng)返回該訂單交易失敗 總之,在高并發(fā)場(chǎng)景下,導(dǎo)致服務(wù)崩潰的原因還是redis和數(shù)據(jù)庫(kù),可能是redis讀寫太慢,或者數(shù)據(jù)庫(kù)的一些sql使用不當(dāng),或者沒(méi)建索引導(dǎo)致讀寫很慢。
該文章在 2024/1/13 16:40:19 編輯過(guò)