以前寫過一篇冪等的文章,可是僅僅只是思路,在生產環境確定仍是不可以用的。恰巧同事最近在討論如何作冪等。那麼如何設計冪等可以達到生產可用呢?請看下面代碼:git
public interface IdempotentProcessor {
/**
* 處理冪等執行代碼塊
*
* @param optType the opt type
* @param optId the opt id
* @param initStatus the init status
* @param expireMs the expire ms
* @return the status enum
*/
StatusEnum process(String optType, String optId, StatusEnum initStatus, long expireMs);
/**
* 提交冪等
* @param optType the opt type
* @param optId the opt id
* @return the boolean
*/
boolean confirm(String optType, String optId);
/**
*
* 取消冪等
* @param optType the opt type
* @param optId the opt id
* @return the boolean
*/
boolean cancel(String optType, String optId);
}
複製代碼
那麼你們看到如上接口設計,會發現當執行process方法時執行冪等且還須要 confim一次,同時能夠調用cancel取消冪等。爲何,由於若是在方法執行以前冪等了後續方法執行失敗則出大事了。因此須要confirm、cancel兩個方法保證最終成功和失敗(畫外音:此類設計有點TCC事務有木有?)github
public class ItemProcessor implements IdempotentProcessor {
private static final String CACHE_KEY = "dew:idempotent:item:";
@Override
public StatusEnum process(String optType, String optId, StatusEnum initStatus, long expireMs) {
if (Dew.cluster.cache.setnx(CACHE_KEY + optType + ":" + optId, initStatus.toString(), expireMs / 1000)) {
// 設置成功,表示以前不存在
return StatusEnum.NOT_EXIST;
} else {
// 設置不成功,表示以前存在,返回存在的值
String status = Dew.cluster.cache.get(CACHE_KEY + optType + ":" + optId);
if (status == null || status.isEmpty()) {
// 設置成功,表示以前不存在
return StatusEnum.NOT_EXIST;
} else {
return StatusEnum.valueOf(status);
}
}
}
@Override
public boolean confirm(String optType, String optId) {
long ttl = Dew.cluster.cache.ttl(CACHE_KEY + optType + ":" + optId);
if (ttl > 0) {
Dew.cluster.cache.setex(CACHE_KEY + optType + ":" + optId, StatusEnum.CONFIRMED.toString(), ttl);
}
return true;
}
@Override
public boolean cancel(String optType, String optId) {
Dew.cluster.cache.del(CACHE_KEY + optType + ":" + optId);
return true;
}
}
複製代碼
public class IdempotentHandlerInterceptor extends HandlerInterceptorAdapter {
private DewIdempotentConfig dewIdempotentConfig;
/**
* Instantiates a new Idempotent handler interceptor.
*
* @param dewIdempotentConfig the dew idempotent config
*/
public IdempotentHandlerInterceptor(DewIdempotentConfig dewIdempotentConfig) {
this.dewIdempotentConfig = dewIdempotentConfig;
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
Idempotent idempotent = ((HandlerMethod) handler).getMethod().getAnnotation(Idempotent.class);
if (idempotent == null) {
return super.preHandle(request, response, handler);
}
// 參數設置
String optType = "[" + request.getMethod() + "]" + Dew.Info.name + "/" + request.getRequestURI();
String optIdFlag = StringUtils.isEmpty(idempotent.optIdFlag()) ? dewIdempotentConfig.getDefaultOptIdFlag() : idempotent.optIdFlag();
String optId = request.getHeader(optIdFlag);
if (StringUtils.isEmpty(optId)) {
optId = request.getParameter(optIdFlag);
}
if (StringUtils.isEmpty(optId)) {
// optId不存在,表示忽略冪等檢查,強制執行
return super.preHandle(request, response, handler);
}
if (!DewIdempotent.existOptTypeInfo(optType)) {
long expireMs = idempotent.expireMs() == -1 ? dewIdempotentConfig.getDefaultExpireMs() : idempotent.expireMs();
boolean needConfirm = idempotent.needConfirm();
StrategyEnum strategy = idempotent.strategy() == StrategyEnum.AUTO ? dewIdempotentConfig.getDefaultStrategy() : idempotent.strategy();
DewIdempotent.initOptTypeInfo(optType, needConfirm, expireMs, strategy);
}
switch (DewIdempotent.process(optType, optId)) {
case NOT_EXIST:
return super.preHandle(request, response, handler);
case UN_CONFIRM:
ErrorController.error(request, response, 409,
"The last operation was still going on, please wait.", IdempotentException.class.getName());
return false;
case CONFIRMED:
ErrorController.error(request, response, 423,
"Resources have been processed, can't repeat the request.", IdempotentException.class.getName());
return false;
default:
return false;
}
}
}
複製代碼
第一次請求冪等失敗手動取消冪等,第二次冪等成功web
代碼:bash
@GetMapping(value = "manual-confirm")
@Idempotent(expireMs = 5000)
public Resp<String> testManualConfirm(@RequestParam("str") String str) {
try {
if ("dew-test1".equals(str)){
throw new RuntimeException("處理冪等失敗");
}
DewIdempotent.confirm();
} catch (Exception e) {
DewIdempotent.cancel();
return Resp.serverError(str + "處理冪等失敗");
}
return Resp.success(str);
}
複製代碼
@Test
public void testConfirm() throws IOException, InterruptedException {
//冪等惟一鍵值
//
HashMap<String, String> header = new HashMap<String, String>() {
{
put(DewIdempotentConfig.DEFAULT_OPT_ID_FLAG, "0001");
}
};
//字符串等於dew-test1冪等失敗
Resp<String> error = Resp.generic($.http.get(urlPre + "manual-confirm?str=dew-test1", header), String.class);
System.out.println("冪等失敗 [errorCode=" + error.getCode() + "-errorMsg=" + error.getMessage() + "]");
//字符串等於dew-test2冪等成功
Resp<String> success = Resp.generic($.http.get(urlPre + "manual-confirm?str=dew-test2", header), String.class);
System.out.println("冪等成功 [code=" + success.getCode() + "-body=" + success.getBody() + "]");
}
複製代碼
結果:架構
冪等失敗 [errorCode=500-errorMsg=dew-test1處理冪等失敗]
冪等成功 [code=200-body=dew-test2]
複製代碼
第一次請求冪等成功手動提交confim,第二次冪等失敗app
代碼:框架
@GetMapping(value = "manual-confirm")
@Idempotent(expireMs = 5000)
public Resp<String> testManualConfirm(@RequestParam("str") String str) {
try {
DewIdempotent.confirm();
} catch (Exception e) {
DewIdempotent.cancel();
return Resp.serverError(str + "處理冪等失敗");
}
return Resp.success(str);
}
複製代碼
@Test
public void testConfirm() throws IOException, InterruptedException {
//冪等惟一鍵值
//
HashMap<String, String> header = new HashMap<String, String>() {
{
put(DewIdempotentConfig.DEFAULT_OPT_ID_FLAG, "0001");
}
};
//字符串等於dew-test1冪等成功
Resp<String> success = Resp.generic($.http.get(urlPre + "manual-confirm?str=dew-test1", header), String.class);
System.out.println("冪等成功 [code=" + success.getCode() + "-body=" + success.getBody() + "]");
Thread.sleep(500);
//字符串等於dew-test2冪等失敗
Resp<String> error = Resp.generic($.http.get(urlPre + "manual-confirm?str=dew-test2", header), String.class);
System.out.println("冪等失敗 [errorCode=" + error.getCode() + "-errorMsg=" + error.getMessage() + "]");
}
複製代碼
結果:ide
冪等成功 [code=200-body=dew-test1]
2019-04-23 00:14:17.123 ERROR 8600 --- [nio-8080-exec-2] ms.dew.core.web.error.ErrorController : Request [GET-/idempotent/manual-confirm] 169.254.156.20 , error 423 : Resources have been processed, can't repeat the request. 冪等失敗 [errorCode=423-errorMsg=[]Resources have been processed, can't repeat the request.]
複製代碼
此代碼及思路的項目名爲dew且是微服務一站式解決方案,提供:架構指南、容器優先/兼容Spring Cloud與Istio的框架、最佳實踐及Devops標準化流程。 dew.ms微服務
源代碼github地址:github.com/gudaoxuri/d…測試
各位朋友能夠給這位大佬的項目star一波