如何实现RetryTemplate重试RestTemplate时复用同一请求ID?
问题:RetryTemplate重试时RestTemplate重复生成请求ID的解决方案
我们有一个用于修改数据的HTTP PUT接口,请求方偶尔会因未收到响应而重试,但服务端可能已处理成功,导致重复修改数据。常规解决方案是在请求头携带固定请求ID,同一请求的所有重试使用同一个ID,服务端据此过滤重复请求。
当前遇到的问题是:RetryTemplate重试时会重新调用RestTemplate,每次重试都会生成不同的请求ID。现有RestTemplate配置如下:
@Bean public RestTemplate defaultRestTemplate(HttpComponentsClientHttpRequestFactory defaultHttpClientRequestFactory) { return new RestTemplateBuilder() .requestFactory(() -> defaultHttpClientRequestFactory) .additionalRequestCustomizers( request -> request.getHeaders().set(RequestConstants.HEADER_CLIENT_REQUEST_ID, UUID.randomUUID().toString()) ) .build(); }
这是RetryTemplate的设计特性,我们需要找到**符合DRY原则(不编写自定义包装方法)**的实现方式,确保以下测试用例能够通过:
@SpringBootTest public class DefaultHttpRetryTemplateConfigurationTest { @Autowired protected RestTemplate defaultRestTemplate; @Autowired protected RetryTemplate defaultHttpRetryTemplate; @Test void defaultHttpRetryTemplate_providesSameRequestIdInRetryRequests_butDifferentRequestIdsInOriginalRequests() throws IOException, InterruptedException, ExecutionException { String[] requestIds = new String[2]; Future<String> future = Executors.newSingleThreadExecutor().submit(() -> { return testRequest(); }); requestIds[0] = testRequest(); requestIds[1] = future.get(); Assert.assertNotEquals(requestIds[0], requestIds[1]); // 不同的原始请求使用不同的请求ID } private String testRequest() throws IOException, InterruptedException { try (MockWebServer mockWebServer = new MockWebServer()) { mockWebServer.enqueue(new MockResponse().setResponseCode(500)); mockWebServer.enqueue(new MockResponse().setResponseCode(200)); mockWebServer.start(); HttpUrl url = mockWebServer.url("/test"); HttpEntity<String> entity = new HttpEntity<>("{}"); this.defaultHttpRetryTemplate.execute(retryCallback -> { return this.defaultRestTemplate.exchange(url.uri(), HttpMethod.PUT, entity, String.class); }); RecordedRequest[] requests = new RecordedRequest[2]; requests[0] = mockWebServer.takeRequest(); requests[1] = mockWebServer.takeRequest(); String[] uuids = new String[2]; uuids[0] = requests[0].getHeader(RequestConstants.HEADER_CLIENT_REQUEST_ID); uuids[1] = requests[1].getHeader(RequestConstants.HEADER_CLIENT_REQUEST_ID); Assert.assertEquals(uuids[0], uuids[1]); // 同一请求的重试使用相同的请求ID return uuids[0]; } } }
解决方案
利用ThreadLocal存储请求ID,结合Spring Retry的RetryListener和RestTemplate的自定义配置,实现同一重试周期内复用请求ID、不同请求隔离ID的效果,完全基于Spring扩展点实现,无需自定义包装方法:
1. 定义请求ID持有类(ThreadLocal存储)
public class RequestIdHolder { private static final ThreadLocal<String> REQUEST_ID = new ThreadLocal<>(); public static void setRequestId(String requestId) { REQUEST_ID.set(requestId); } public static String getRequestId() { return REQUEST_ID.get(); } public static void clear() { REQUEST_ID.remove(); } }
2. 修改RestTemplate配置,从ThreadLocal获取请求ID
@Bean public RestTemplate defaultRestTemplate(HttpComponentsClientHttpRequestFactory defaultHttpClientRequestFactory) { return new RestTemplateBuilder() .requestFactory(() -> defaultHttpClientRequestFactory) .additionalRequestCustomizers( request -> { String requestId = RequestIdHolder.getRequestId(); // 仅当ThreadLocal中无ID时才生成新ID if (requestId == null) { requestId = UUID.randomUUID().toString(); RequestIdHolder.setRequestId(requestId); } request.getHeaders().set(RequestConstants.HEADER_CLIENT_REQUEST_ID, requestId); } ) .build(); }
3. 配置RetryTemplate,通过RetryListener管理请求ID生命周期
@Bean public RetryTemplate defaultHttpRetryTemplate() { RetryTemplate retryTemplate = new RetryTemplate(); // 配置重试策略(根据你的需求调整,示例为最多重试2次) SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy(); retryPolicy.setMaxAttempts(2); retryTemplate.setRetryPolicy(retryPolicy); // 添加RetryListener,在重试周期开始时生成ID,结束时清理ThreadLocal retryTemplate.registerListener(new RetryListener() { @Override public <T, E extends Throwable> boolean open(RetryContext context, RetryCallback<T, E> callback) { // 首次执行时生成请求ID if (RequestIdHolder.getRequestId() == null) { RequestIdHolder.setRequestId(UUID.randomUUID().toString()); } return true; } @Override public <T, E extends Throwable> void close(RetryContext context, RetryCallback<T, E> callback, Throwable throwable) { // 重试结束后清理ThreadLocal,避免内存泄漏 RequestIdHolder.clear(); } @Override public <T, E extends Throwable> void onError(RetryContext context, RetryCallback<T, E> callback, Throwable throwable) { // 重试出错时保持ID不变,无需额外操作 } }); return retryTemplate; }
方案说明
- 同一RetryTemplate执行周期内,
RetryListener的open方法会生成一次请求ID并存入ThreadLocal,后续重试时RestTemplate会直接从ThreadLocal获取该ID,不会重新生成。 - 不同的原始请求(如多线程场景),
ThreadLocal是线程隔离的,因此会生成不同的请求ID,满足测试用例的要求。 - 完全基于Spring提供的扩展点实现,没有编写自定义包装方法,符合DRY原则。
内容的提问来源于stack exchange,提问作者Zyl
相关产品推荐
相关产品推荐

