在實(shí)際工作過程中,我們經(jīng)常會(huì)遇到某些功能或者需求,他的實(shí)時(shí)性要求不高嫌术,從而選擇通過另起一個(gè)線程的方式去實(shí)現(xiàn)哀澈,以防止主流程的阻塞,帶給用戶更好的體驗(yàn)度气。
但是在實(shí)際使用的過程中割按,常常會(huì)遇到各種各樣的問題,例如線程安全磷籍,變量丟失等适荣;
這里我描述下我在實(shí)際開發(fā)中遇到的問題:
需求描述:
實(shí)現(xiàn)提醒用戶有新的優(yōu)惠券可領(lǐng)的功能
實(shí)現(xiàn)方案:
方案1:
使用數(shù)據(jù)庫(kù)實(shí)現(xiàn),創(chuàng)建一張表用來記錄每一張優(yōu)惠券對(duì)應(yīng)每一個(gè)用戶的狀態(tài)院领,當(dāng)用戶查看以后修改記錄狀態(tài)弛矛;
優(yōu)勢(shì):層次很細(xì),細(xì)到用戶對(duì)每一張優(yōu)惠券都有一個(gè)記錄比然,能完全實(shí)現(xiàn)需求丈氓,且能持久化;
劣勢(shì):需要使用到數(shù)據(jù)庫(kù)强法,創(chuàng)建一張新的表万俗,開發(fā)量大,后期表數(shù)據(jù)量比較大拟烫,數(shù)據(jù)記錄基本是優(yōu)惠券數(shù)量*用戶數(shù)量该编;
方案2:
利用redis的hashMap數(shù)據(jù)結(jié)構(gòu)實(shí)現(xiàn)迄本,當(dāng)有新的優(yōu)惠券產(chǎn)生時(shí)硕淑,設(shè)置用戶對(duì)應(yīng)的優(yōu)惠券狀態(tài)為true,點(diǎn)擊查看以后修改為false嘉赎;
優(yōu)勢(shì):操作速度快置媳,開發(fā)效率快,基本能實(shí)現(xiàn)需求公条;
劣勢(shì):無法做到像上面一樣針對(duì)每一張優(yōu)惠券拇囊,如果需要控制優(yōu)惠券過期時(shí),取消原有的提醒就無法做到
這里我選擇方案是第二種靶橱,因?yàn)闃I(yè)務(wù)本身寥袭,并不需要那么細(xì),只要起到提醒的作用就好关霸,容錯(cuò)率高传黄,即使提醒不對(duì),點(diǎn)進(jìn)去也沒關(guān)系队寇;
實(shí)現(xiàn)過程中遇到的問題
在實(shí)現(xiàn)過程中我都選擇了異步去處理這些邏輯膘掰;
創(chuàng)建時(shí),異步的對(duì)用戶添加狀態(tài)
CompletableFuture.runAsync(() -> {
Result<List<Long>> result = userFeignService.findAllUserId();
Map<Long,Integer> couponFlagMap= Maps.newHashMap();
Optional.ofNullable(result.getData()).orElse(new ArrayList<>()).forEach(userId->couponFlagMap.put(userId,1));
RedisTemplateUtil.hPutAll(RedisKeyConstantUser.USER_COUPON_MESSAGE_KEY,couponFlagMap);
}, commonExecutorService);
這里采用異步的方式主要是因?yàn)椴樵冇脩舻牟僮骺赡艽嬖诤臅r(shí)佳遣,為防止影響主業(yè)務(wù)识埋,所以采用這種方式凡伊;
在實(shí)際調(diào)試過程中,發(fā)現(xiàn)當(dāng)我異步調(diào)用的時(shí)候窒舟,發(fā)現(xiàn)我日志中設(shè)置的trceId沒有帶過去系忙,導(dǎo)致查看日志的時(shí)候無法將這部分的請(qǐng)求日志跟主線程中的對(duì)應(yīng)起來,這是為什么呢惠豺?
下面是我的feign攔截器代碼
@Component
@Slf4j
public class FeignInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate requestTemplate) {
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
if (null != requestAttributes) {
//追加mdc
HttpServletRequest request = ((ServletRequestAttributes) requestAttributes).getRequest();
String requestNo = MDC.get("requestNO");
if (StringUtils.isNotBlank(requestNo)) {
requestTemplate.header("request-no", requestNo);
}
}
}
}
調(diào)試的時(shí)候發(fā)現(xiàn)這里拿到的requestAttributes一直是空笨觅,進(jìn)入getRequestAttributes()方法查看
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 = (RequestAttributes)requestAttributesHolder.get();
if (attributes == null) {
attributes = (RequestAttributes)inheritableRequestAttributesHolder.get();
}
return attributes;
}
發(fā)現(xiàn)RequestAttributes是從requestAttributesHolder或者inheritableRequestAttributesHolder中獲取到的,然而這兩變量的類型都是ThreadLocal耕腾,這個(gè)類我們很熟悉啊见剩,是針對(duì)當(dāng)前線程的局部變量,那么原因就找到了扫俺,是由于線程引發(fā)的線程變量不一致的問題苍苞;因?yàn)槲覀兺獠康闹骶€程和里面的線程不是同一個(gè),所以導(dǎo)致這里獲取不到RequestAttributes狼纬;
所以當(dāng)我們?cè)诹硪粋€(gè)服務(wù)里面通過RequestAttributes去獲取請(qǐng)求中攜帶的信息時(shí)獲取不到任何內(nèi)容羹呵,因?yàn)檫@個(gè)RequestAttributes是一個(gè)新的,并沒有把原來的例如token疗琉,requestNo等數(shù)據(jù)攜帶過來冈欢,導(dǎo)致后續(xù)的一些業(yè)務(wù)處理產(chǎn)生不可預(yù)知的問題;
解決方案:
自己手動(dòng)設(shè)置盈简,將主線程的數(shù)據(jù)先拿出來凑耻,然后在請(qǐng)求之前手動(dòng)設(shè)置進(jìn)去;例如
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
String requestNo = MDC.get("requestNO");
CompletableFuture.runAsync(() -> {
MDC.put("requestNO", requestNo);
RequestContextHolder.setRequestAttributes(requestAttributes);
}, commonExecutorService);
當(dāng)我們?cè)谑褂枚嗑€程的方式進(jìn)行接口調(diào)用的時(shí)候柠贤,這個(gè)尤其需要注意香浩。