还在手写重试逻辑?一篇搞定重试工具(附实战案例)

还在手写重试逻辑?一篇搞定重试工具(附实战案例) 一、背景与痛点在复杂的分布式系统或与外部服务交互的应用中临时性故障Transient Fault是常态。例如数据库连接因网络抖动而失败HTTP请求远程服务超时读取的文件暂时被占用如果在业务代码中直接编写重试逻辑会导致代码臃肿、重复、难以维护。为了解决这一问题设计并实现了一个通用的重试工具类RetryUtil支持自定义重试次数、重试间隔以及重试条件处理需要重试的场景。二、核心实现通用重试工具类 RetryUtilRetryUtil工具类将提供一个通用的重试方法支持自定义重试次数、重试间隔、重试条件等。可以用于多种需要重试的场景。通用的重试工具类RetryUtil123456789101112131415161718192021222324252627282930313233343536373839404142434445importjava.util.concurrent.TimeUnit;importjava.util.function.Supplier;/*** 重试工具类* title RetryUtil* author pingping_ye* date 2025-03-14 16:24*/publicclassRetryUtil {/*** 通用重试方法* param T 返回值类型* param supplier 要执行的逻辑返回值为 T 的 Supplier* param maxRetries 最大重试次数* param retryInterval 重试间隔单位秒* param retryCondition 重试条件如果返回 true则继续重试* return 执行结果*/publicstaticT T retry(SupplierT supplier,intmaxRetries,intretryInterval,java.util.function.PredicateException retryCondition) {intretryCount 0;while(retryCount maxRetries) {try{returnsupplier.get();// 执行业务逻辑}catch(Exception e) {retryCount;if(retryCount maxRetries || !retryCondition.test(e)) {thrownewRuntimeException(重试失败达到最大重试次数或不符合重试条件, e);}try{TimeUnit.SECONDS.sleep(retryInterval);// 等待重试间隔}catch(InterruptedException ie) {Thread.currentThread().interrupt();thrownewRuntimeException(线程中断异常, ie);}}}thrownewIllegalStateException(重试逻辑异常未正确执行);}}三、工具类的优势通用性RetryUtil类可以用于任何需要重试逻辑的场景只需传入具体的业务逻辑和重试条件。灵活性可以通过参数配置重试次数、重试间隔和重试条件。简洁性将重试逻辑封装在一个工具类中避免在业务代码中重复编写重试逻辑。四、实战案例微信二维码获取接口的重试改造改造前重试逻辑与业务代码耦合业务方法改造前123456789101112131415161718publicWxQrCode getQrCode(WeChatOfficialIdpScanCodeConfig config) {MpAccessToken mpAccessToken this.getDynamoDbWxAccessToken();GetQRCodeParam getQRCodeParam newGetQRCodeParam();String request JsonUtil.toJson(getQRCodeParam);//注意点一String response HttpUtil.post(String.format(WX_OFFICIAL_QRCODE_URL, mpAccessToken.getAccess_token()), request);//判断有没有token过期String errCode JsonUtil.getContext().parse(response).read($.errcode, String.class);if(ObjectUtil.isNotNull(errCode) 42001.equals(errCode) ||40001.equals(errCode)) {mpAccessToken this.getDynamoDbWxAccessToken();//注意点二这个是坏味道的代码实现response HttpUtil.post(String.format(WX_OFFICIAL_QRCODE_URL, mpAccessToken.getAccess_token()), request);}checkResponse(response);returnJsonUtil.toBean(response, WxQrCode.class);}问题重试逻辑只处理了token过期的情况且只能重试一次代码中混杂了业务逻辑和重试控制。改造后使用 RetryUtil 分离关注点业务方法改造后1234567891011121314151617181920212223publicWxQrCode getTicket(WeChatOfficialIdpScanCodeConfig config) {returnRetryUtil.retry(()-{WeChatService weChatService SpringContext.getBean(WeChatService.class);String accessToken weChatService.getAccessToken(config.getClientId(), config.getClientSecret());if(StrUtil.isBlank(accessToken)) {thrownewRuntimeException(请求生成带参数的二维码异常accessToken为空);}GetQRCodeParam getQRCodeParam newGetQRCodeParam();String request JsonUtil.toJson(getQRCodeParam);String response HttpUtil.post(String.format(WX_OFFICIAL_QRCODE_URL, accessToken), request);if(StrUtil.isBlank(response)) {thrownewRuntimeException(请求生成带参数的二维码异常响应报文为空);}String errCode JsonUtil.getContext().parse(response).read($.errcode, String.class);if(42001.equals(errCode) ||40001.equals(errCode)) {thrownewRuntimeException(请求生成带参数的二维码异常,接口响应错误码errCodeerrCode);}returnJsonUtil.toBean(response, WxQrCode.class);},3,1,e - einstanceofRuntimeException);}改进点逻辑清晰业务代码只关注做什么重试控制由工具类负责。可配置性强重试次数、间隔、条件一目了然。易于扩展未来如果需要修改重试策略只需调整参数无需改动核心业务。其他场景-重试HTTP请求12345678publicString fetchHttpData(String url) {returnRetryUtil.retry(() - HttpUtil.get(url),// 要执行的逻辑3,// 最大重试次数2,// 重试间隔秒e - einstanceofIOException// 重试条件只重试 IOException);}其他场景-重试数据库查询12345678publicUser getUserById(intuserId) {returnRetryUtil.retry(() - userRepository.findById(userId),// 要执行的逻辑5,// 最大重试次数1,// 重试间隔秒e - einstanceofSQLException// 重试条件只重试 SQLException);}五、注意事项幂等性被重试的业务逻辑必须是幂等的否则多次执行可能导致数据不一致。异常类型重试条件应精确匹配预期的临时性异常避免将业务错误如参数校验失败也进行重试。线程中断工具类正确处理了InterruptedException保留了线程的中断状态符合Java并发最佳实践。死循环风险注意while(true)的退出条件当前实现通过retryCount maxRetries保证一定会退出。六、RetryUtil vs 专业重试框架依赖大小无纯JDK~100KB~200KB学习成本极低中等中等功能丰富度基础丰富注解模板丰富熔断限流重试适用场景小型项目、简单重试Spring生态项目微服务、云原生应用当前工具类功能基础可以扩展支持更复杂的重试场景。