場景:一次迭代在灰度環境發版時,測試反饋說我開發的那個功能,查詢介面有部分欄位資料是空的,後續排查日誌,發現日誌如下:
feign.RetryableException: cannot retry due to redirection, in streaming mode executing POST
下面是業務、環境和分析過程下面是業務、環境和分析過程:
介面的業務場景 :我這個介面類似是那種報表統計的介面,它會請求多個微服務,把請求到的資料,統一返回給前端,相當於設計模式中的門面模式了。
後續由於這個介面 是序列請求其他微服務的,速度有些慢,後面修改程式碼從序列請求,改成並行(多執行緒)獲取資料
運維那邊是通過判斷http請求中cookie 或者 header中的某個資料,來區分請求是否要把流量打到灰度。
分析得出:應該是介面非同步請求的時候cookie丟失,沒走到灰度環境,找不到 這次迭代新開發的介面,導致的重定向到錯誤頁面了。
驗證:由於我程式碼是通過@Async非同步註解,實現並行請求的,臨時把五個介面的非同步註解註釋掉了,灰度在發版驗證,資料能返回正常,說明流量打到灰度了
說明問題就是併發請求的時候,子執行緒獲取不到 主執行緒的request 頭資訊,導致沒有走到灰度
下圖就是灰度環境的 流程圖:
問題定位出來了,解決方案就是:讓子執行緒能獲取到主執行緒的 request 頭資訊,主執行緒把 資料透傳到子執行緒。
我使用的是RequestContextHolder來透傳資料
什麼是 RequestContextHolder?
RequestContextHolder 是spring mvc的一個工具類,顧名思義,持有上下文的Request容器
如何使用:
//獲取當前執行緒 request請求的屬性
RequestAttributes requestAttributes = RequestContextHolder.currentRequestAttributes();
//設定當前執行緒 request請求的屬性
RequestContextHolder.setRequestAttributes(attributes);
RequestContextHolder的 會用到的幾個方法
- currentRequestAttributes:獲得當前執行緒請求的屬性(頭資訊之類的)
- setRequestAttributes(attributes):設定當前執行緒 屬性(設定頭資訊)
- resetRequestAttributes:刪除當前執行緒 繫結的屬性
下面是他們的原始碼,可以簡單看一下,原理是通過ThreadLocal來繫結資料的:
private static final ThreadLocal<RequestAttributes> requestAttributesHolder =
new NamedThreadLocal<>("Request attributes");
private static final ThreadLocal<RequestAttributes> inheritableRequestAttributesHolder =
new NamedInheritableThreadLocal<>("Request context");
//獲得當前執行緒請求的屬性(頭資訊之類的)
@Nullable
public static RequestAttributes getRequestAttributes() {
RequestAttributes attributes = requestAttributesHolder.get();
if (attributes == null) {
attributes = inheritableRequestAttributesHolder.get();
}
return attributes;
}
//設定當前執行緒 屬性(設定頭資訊)
public static void setRequestAttributes(@Nullable RequestAttributes attributes) {
setRequestAttributes(attributes, false);
}
//設定當前執行緒 屬性(設定頭資訊)
public static void setRequestAttributes(@Nullable RequestAttributes attributes, boolean inheritable) {
if (attributes == null) {
resetRequestAttributes();
}
else {
if (inheritable) {
inheritableRequestAttributesHolder.set(attributes);
requestAttributesHolder.remove();
}
else {
requestAttributesHolder.set(attributes);
inheritableRequestAttributesHolder.remove();
}
}
}
//刪除當前執行緒 繫結的屬性
public static void resetRequestAttributes() {
requestAttributesHolder.remove();
inheritableRequestAttributesHolder.remove();
}
下面我編寫了一套遇到問題的程式碼例子,以及解決的程式碼:
TestUserController
測試介面
@Slf4j
@RestController
@RequestMapping(value = "/v1/testUser")
public class TestUserController {
@Autowired
ITestRequestService testRequestService;
@ApiOperation(value = "聚合資料介面(一)-序列獲取資料")
@RequestMapping(value = "/listUser", method = RequestMethod.GET)
public Resp<List<User>> listUser(@RequestHeader(value = "token",required = false)String token){
TimeInterval timeInterval = DateUtil.timer();
DataResp dataResp = testRequestService.getDateResp();
log.info("聚合資料介面(一)-序列獲取資料 總耗時:{}毫秒",timeInterval.interval());
return Resp.buildDataSuccess(dataResp).setTimeInterval(timeInterval.interval());
}
@ApiOperation(value = "聚合資料介面(二)-並行獲取資料@Async (子執行緒獲取不到token)")
@RequestMapping(value = "/listUser2", method = RequestMethod.GET)
public Resp<List<User>> listUser2(@RequestHeader(value = "token",required = false)String token) throws ExecutionException, InterruptedException {
TimeInterval timeInterval = DateUtil.timer();
DataResp dataResp = testRequestService.getDateResp2();
log.info("聚合資料介面(二)-並行獲取資料@Async (子執行緒獲取不到token) 總耗時:{}毫秒",timeInterval.interval());
return Resp.buildDataSuccess(dataResp).setTimeInterval(timeInterval.interval());
}
@ApiOperation(value = "聚合資料介面(三)-並行獲取資料(子執行緒能獲取到token)")
@RequestMapping(value = "/listUser3", method = RequestMethod.GET)
public Resp<List<User>> listUser3(@RequestHeader(value = "token",required = false)String token) throws ExecutionException, InterruptedException {
TimeInterval timeInterval = DateUtil.timer();
DataResp dataResp = testRequestService.getDateResp3();
log.info("聚合資料介面(三)-並行獲取資料(子執行緒能獲取到token) 總耗時:{}毫秒",timeInterval.interval());
return Resp.buildDataSuccess(dataResp).setTimeInterval(timeInterval.interval());
}
}
TestRequestService
聚合資料的類
@Service
public class TestRequestService implements ITestRequestService {
@Autowired
IUserService userService;
@Autowired
IOrderService orderService;
/**
* 自定義 - 執行緒池
*/
private static final ThreadPoolExecutor executorService = new ThreadPoolExecutor(50, 200,
180L, TimeUnit.SECONDS,
new LinkedBlockingQueue<Runnable>(3000), new ThreadFactory() {
final ThreadFactory defaultFactory = Executors.defaultThreadFactory();
@Override
public Thread newThread(Runnable r) {
Thread thread = defaultFactory.newThread(r);
thread.setName("testRequest - " + thread.getName());
return thread;
}
}, new ThreadPoolExecutor.CallerRunsPolicy());
/**
* 聚合介面-序列獲取資料
* @return
*/
@Override
public DataResp getDateResp(){
//獲取使用者列表
List<User> userList = userService.listUser_1();
//獲取訂單列表
List<Order> orderList = orderService.listOrder_1();
return DataResp.builder().userList(userList).orderList(orderList).build();
};
/**
* 聚合介面-並行獲取資料(@Async) 頭資訊傳到子執行緒
* @return
*/
@Override
public DataResp getDateResp2() throws ExecutionException, InterruptedException {
//獲取使用者列表 start
Future<List<User>> userListFuture = userService.listUser_2();
List<User> userList = userListFuture.get();
//獲取使用者列表 end
//獲取訂單列表 start
Future<List<Order>> orderListFuture = orderService.listOrder_2();
List<Order> orderList = orderListFuture.get();
//獲取訂單列表 end
return DataResp.builder().userList(userListFuture.get()).orderList(orderList).build();
};
/**
* 聚合介面-並行獲取資料
* @return
*/
@Override
public DataResp getDateResp3() throws ExecutionException, InterruptedException {
RequestAttributes attributes = RequestContextHolder.getRequestAttributes();
//獲取使用者列表 start
Future<List<User>> userListFuture = CompletableFuture.supplyAsync(() -> {
RequestContextHolder.setRequestAttributes(attributes);
try {
List<User> resp = userService.listUser_3();
return resp;
}finally {
RequestContextHolder.resetRequestAttributes();
}
}, executorService);
List<User> userList = userListFuture.get();
//獲取使用者列表 end
//獲取訂單列表 start
Future<List<Order>> orderListFuture = CompletableFuture.supplyAsync(() -> {
RequestContextHolder.setRequestAttributes(attributes);
try {
List<Order> resp = orderService.listOrder_3();
return resp;
}finally {
RequestContextHolder.resetRequestAttributes();
}
}, executorService);
List<Order> orderList = orderListFuture.get();
//獲取訂單列表 end
return DataResp.builder().userList(userListFuture.get()).orderList(orderList).build();
};
}
下面是兩個請求 使用者和訂單請求類
OrderService 請求訂單的服務的聚合方法
@Slf4j
@Service
public class OrderService implements IOrderService {
/**
* 獲取訂單code列表
* @return
*/
@Override
public List<String> listOrderCode(){
//使用httpUtil 模擬 feign請求服務介面 start
String reqUrl = Config.baseUrl.concat("/v1/order/list");
HttpRequest httpRequest = HttpUtil.createGet(reqUrl);
//設定請求頭資訊
String token = WebUtil.getCurrentRequestHeaderToken();
httpRequest.header("token",token);
HttpResponse httpResponse = httpRequest.execute();
String body = httpResponse.body();
Resp<List<String>> respData = JSONUtil.toBean(body, Resp.class);
//使用httpUtil 模擬 feign請求服務介面 end
if(respData.isSuccess()){
return respData.getData();
}
return null;
};
/**
* 根據訂單code獲取 訂單資料
* @param orderCode
* @return
*/
@Override
public Order getOrder(String orderCode){
//使用httpUtil 模擬 feign請求服務介面 start
String reqUrl = StrUtil.format(Config.baseUrl.concat("/v1/order/get?orderCode={}"),orderCode);
HttpRequest httpRequest = HttpUtil.createGet(reqUrl);
//設定請求頭資訊
String token = WebUtil.getCurrentRequestHeaderToken();
httpRequest.header("token",token);
HttpResponse httpResponse = httpRequest.execute();
String body = httpResponse.body();
Gson gson = new Gson();
Resp<Order> respData = gson.fromJson(body , new TypeToken<Resp<Order>>(){}.getType());
//使用httpUtil 模擬 feign請求服務介面 end
if(respData.isSuccess()){
return respData.getData();
}
return null;
};
/**
* 獲取訂單列表(序列獲取)
* @return
*/
@Override
public List<Order> listOrder_1(){
//獲取訂單列表 start
List<Order> orderList = new ArrayList<>();
List<String> orderCodes = listOrderCode();
orderCodes.stream().forEach(orderCode->{
Order order = getOrder(orderCode);
orderList.add(order);
});
//獲取訂單列表 end
return orderList;
};
/**
* 獲取訂單列表(並行獲取資料)
* stream也改成了parallelStream 並行for迴圈
* @return
*/
@Async
@Override
public Future<List<Order>> listOrder_2(){
log.info("listOrder_2 當前執行緒是:{}",Thread.currentThread().getName());
//獲取訂單列表 start
List<Order> orderList = new ArrayList<>();
List<String> orderCodes = listOrderCode();
if(CollUtil.isNotEmpty(orderCodes)){
orderCodes.parallelStream().forEach(orderCode->{
Order order = getOrder(orderCode);
if(order!=null){
orderList.add(order);
}
});
}
//獲取訂單列表 end
return new AsyncResult<List<Order>>(orderList);
};
/**
* 獲取訂單列表(並行獲取資料)(把主執行緒的request的資料 透傳給 子執行緒和子子執行緒)
* @return
*/
@Override
public List<Order> listOrder_3(){
//獲取訂單列表 start
List<Order> orderList = new ArrayList<>();
List<String> orderCodes = listOrderCode();
if(CollUtil.isNotEmpty(orderCodes)){
RequestAttributes attributes = RequestContextHolder.getRequestAttributes();
orderCodes.parallelStream().forEach(orderCode->{
RequestContextHolder.setRequestAttributes(attributes);
try {
Order order = getOrder(orderCode);
if(order!=null){
orderList.add(order);
}
}finally {
RequestContextHolder.resetRequestAttributes();
}
});
}
//獲取訂單列表 end
return orderList;
};
}
UserService 請求訂單的服務的聚合方法
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Future;
@Slf4j
@Service
public class UserService implements IUserService {
@Override
public List<Integer> listUserId(){
//使用httpUtil 模擬 feign請求服務介面 start
String reqUrl = Config.baseUrl.concat("/v1/user/list");
HttpRequest httpRequest = HttpUtil.createGet(reqUrl);
//設定請求頭資訊
String token = WebUtil.getCurrentRequestHeaderToken();
httpRequest.header("token",token);
HttpResponse httpResponse = httpRequest.execute();
String body = httpResponse.body();
Resp<List<Integer>> respData = JSONUtil.toBean(body, Resp.class);
//使用httpUtil 模擬 feign請求服務介面 end
if(respData.isSuccess()){
return respData.getData();
}
return null;
};
@Override
public User getUser(Integer userId){
//使用httpUtil 模擬 feign請求服務介面 start
String reqUrl = StrUtil.format(Config.baseUrl.concat("/v1/user/get?userId={}"),userId);
HttpRequest httpRequest = HttpUtil.createGet(reqUrl);
//設定請求頭資訊
String token = WebUtil.getCurrentRequestHeaderToken();
httpRequest.header("token",token);
HttpResponse httpResponse = httpRequest.execute();
String body = httpResponse.body();
Gson gson = new Gson();
Resp<User> respData = gson.fromJson(body , new TypeToken<Resp<User>>(){}.getType());
//使用httpUtil 模擬 feign請求服務介面 end
if(respData.isSuccess()){
return respData.getData();
}
return null;
};
@Override
public List<User> listUser_1(){
//獲取使用者列表 start
List<User> userList = new ArrayList<>();
List<Integer> userIds = listUserId();
userIds.stream().forEach(userId->{
User user = getUser(userId);
userList.add(user);
});
//獲取使用者列表 end
return userList;
};
@Async
@Override
public Future<List<User>> listUser_2(){
log.info("listUser_2 當前執行緒是:{}",Thread.currentThread().getName());
//獲取使用者列表 start
List<User> userList = new ArrayList<>();
List<Integer> userIds = listUserId();
if(CollUtil.isNotEmpty(userIds)){
userIds.parallelStream().forEach(userId->{
User user = getUser(userId);
if(user!=null){
userList.add(user);
}
});
}
//獲取使用者列表 end
return new AsyncResult<List<User>>(userList);
};
@Override
public List<User> listUser_3(){
//獲取使用者列表 start
List<User> userList = new ArrayList<>();
List<Integer> userIds = listUserId();
if(CollUtil.isNotEmpty(userIds)){
RequestAttributes attributes = RequestContextHolder.getRequestAttributes();
userIds.parallelStream().forEach(userId->{
RequestContextHolder.setRequestAttributes(attributes);
try {
User user = getUser(userId);
if(user!=null){
userList.add(user);
}
}finally {
RequestContextHolder.resetRequestAttributes();
}
});
}
//獲取使用者列表 end
return userList;
};
}
OrderController 你可以理解成其他其他微服務的介面(模擬寫的一個介面,用來測試 請求介面的時候是否攜帶 請求頭了)
@Slf4j
@RestController
@RequestMapping(value = "/v1/order")
public class OrderController {
@ApiOperation(value = "獲取訂單編號列表")
@RequestMapping(value = "/list", method = RequestMethod.GET)
public Resp<List<String>> list(HttpServletRequest request){
String token = request.getHeader("token");
if(StrUtil.isBlank(token)){
return Resp.buildFail("介面不存在 404");
}
List<String> userIds = new ArrayList<>();
userIds.add("11111");
userIds.add("22222");
userIds.add("33333");
userIds.add("44444");
userIds.add("55555");
userIds.add("6666");
userIds.add("7777");
handleBusinessTime();
return Resp.buildDataSuccess(userIds);
}
@ApiOperation(value = "獲取訂單詳情")
@ApiImplicitParams({
@ApiImplicitParam(name = "orderCode", value = "訂單CODE", paramType = "query"),
})
@RequestMapping(value = "/get", method = RequestMethod.GET)
public Resp<Order> get(HttpServletRequest request,@RequestParam(value = "orderCode")String orderCode){
String token = request.getHeader("token");
if(StrUtil.isBlank(token)){
return Resp.buildFail("介面不存在 404");
}
handleBusinessTime();
String name = StrUtil.format("訂單-{}-名",orderCode);
return Resp.buildDataSuccess(Order.builder().code(orderCode).orderName(name).build());
}
/**
* 這方法 模擬處理業務或者 去運算元據庫 消耗的時間
*/
public static void handleBusinessTime(){
//去資料庫查詢資料耗時 start
int[] sleepTime = NumberUtil.generateRandomNumber(300,800,1);
try {
//Thread.sleep 休眠的時候 相當於 業務操作,或者請求資料庫的需要消耗的時間
Thread.sleep(sleepTime[0]);
} catch (InterruptedException e) {
e.printStackTrace();
}
//去資料庫查詢資料耗時 end
}
}
@Slf4j
@RestController
@RequestMapping(value = "/v1/user")
public class UserController {
@ApiOperation(value = "獲取使用者列表-id")
@ApiImplicitParams({
@ApiImplicitParam(name = "orderCode", value = "訂單編號", paramType = "query"),
})
@RequestMapping(value = "/list", method = RequestMethod.GET)
public Resp<List<Integer>> list(HttpServletRequest request){
String token = request.getHeader("token");
if(StrUtil.isBlank(token)){
return Resp.buildFail("介面不存在 404");
}
List<Integer> userIds = new ArrayList<>();
userIds.add(1);
userIds.add(2);
userIds.add(3);
userIds.add(4);
userIds.add(5);
handleBusinessTime();
return Resp.buildDataSuccess(userIds);
}
@ApiOperation(value = "根據使用者ID獲取 使用者資訊")
@ApiImplicitParams({
@ApiImplicitParam(name = "userId", value = "使用者ID", paramType = "query"),
})
@RequestMapping(value = "/get", method = RequestMethod.GET)
public Resp<User> get(HttpServletRequest request,@RequestParam(value = "userId")Integer userId){
String token = request.getHeader("token");
if(StrUtil.isBlank(token)){
return Resp.buildFail("介面不存在 404");
}
handleBusinessTime();
String name = StrUtil.format("使用者{}號",userId);
return Resp.buildDataSuccess(User.builder().id(userId).name(name).build());
}
/**
* 這方法 模擬處理業務或者 去運算元據庫 消耗的時間
*/
public static void handleBusinessTime(){
//去資料庫查詢資料耗時 start
int[] sleepTime = NumberUtil.generateRandomNumber(300,800,1);
try {
//Thread.sleep 休眠的時候 相當於 業務操作,或者請求資料庫的需要消耗的時間
Thread.sleep(sleepTime[0]);
} catch (InterruptedException e) {
e.printStackTrace();
}
//去資料庫查詢資料耗時 end
}
}
下面三個介面的由來:
- /v1/testUser/listUser 介面:就是序列呼叫其他服務介面 ,效能比較慢
- /v1/testUser/listUser2 介面:是通過@Async 非同步註解,並行呼叫其他 系統的介面,效能是提升上去了,但灰度環境 是需要根據請求頭裡面的資料判斷是否把流量打到灰度環境
- /v1/testUser/listUser3介面:對@Async註解沒有找到透傳 主執行緒request頭資訊的方案,就使用執行緒池+CompletableFuture.supplyAsync的方式 每次執行非同步執行緒的時候,把主執行緒的 請求引數設定到子執行緒,然後通過try-finally 引數使用完之後RequestContextHolder.resetRequestAttributes() 刪除引數。
注意:parallelStream它也是屬於並行流操作,也要設定 請求頭資訊,雖說子執行緒(getDateResp3方法)能獲取到主執行緒的請求頭資訊了,但是parallelStream 又相當於子執行緒的子執行緒了,它是獲取不到的 主執行緒的attributes的,當時我就是沒在parallelStream設定attributes,它沒有走到灰度環境, 讓我 耗費了兩個多小時,程式碼加了四五次日誌輸出,才把這個問題定位出來,這是一個坑。。。
下面是程式碼:
基於這個問題,我還寫了一篇 spring boot使用@Async的文章,大家感興趣可以去看看 傳送門~
我已經把上述程式碼例子放到gitee了,大家感興趣可以clone 傳送門~