生产级幂等解决方案

栏目: Java · 发布时间: 5年前

内容简介:之前写过一篇幂等的文章,但是仅仅只是思路,在生产环境肯定还是不能够用的。恰巧同事最近在讨论如何做幂等。那么如何设计幂等能够达到生产可用呢?请看下面代码:那么大家看到如上接口设计,会发现当执行process方法时执行幂等且还需要 confim一次,同时可以调用cancel取消幂等。为什么,因为如果在方法执行之前幂等了后续方法执行失败则出大事了。所以需要confirm、cancel两个方法保证最终成功和失败(画外音:此类设计有点TCC事务有木有?)第一次请求幂等失败手动取消幂等,第二次幂等成功

之前写过一篇幂等的文章,但是仅仅只是思路,在生产环境肯定还是不能够用的。恰巧同事最近在讨论如何做幂等。那么如何设计幂等能够达到生产可用呢?请看下面代码:

幂等处理接口:

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事务有木有?)

幂等实现类

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;
        }
    }

}
复制代码

测试用例

测试场景一

第一次请求幂等失败手动取消幂等,第二次幂等成功

代码:

@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,第二次幂等失败

代码:

@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() + "]");
}
复制代码

结果:

幂等成功 [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.]
复制代码

以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

跟小贤学运营

跟小贤学运营

陈维贤 / 机械工业出版社 / 2016-12-9 / 69.00

这是一部能帮助运营新人快速构建互联网运营方法论和快速掌握互联网运营实操的著作,是小贤在百度贴吧和小红书成长经历和运营经验的复盘。书中包含5大运营主题、40余种运营工具和渠道、50余种运营方法和技巧、100余个真实接地气的运营案例,能迅速帮助运营新人掌握全套实操技能和构建完整运营体系。 本书的视角和知识体系都比较立体化: 既有百度这样的互联网巨头运营规范和思路,又有小红书这样的明星创业公......一起来看看 《跟小贤学运营》 这本书的介绍吧!

RGB转16进制工具
RGB转16进制工具

RGB HEX 互转工具

MD5 加密
MD5 加密

MD5 加密工具

HEX CMYK 转换工具
HEX CMYK 转换工具

HEX CMYK 互转工具