三覺(jué) 阿里開發(fā)者 2023-07-04 09:02 發(fā)表于浙江
(相關(guān)資料圖)
阿里妹導(dǎo)讀
本文主要從我們?yōu)槭裁葱枰狢R?CR面臨哪些挑戰(zhàn)?CR的最佳實(shí)踐幾個(gè)方面分析,希望可以給讀者一些參考。
為什么需要CR?
代碼質(zhì)量
定性來(lái)看,大家都認(rèn)可Code Review(后文簡(jiǎn)稱CR)能 顯著改善代碼質(zhì)量 ,但國(guó)內(nèi)量化的研究結(jié)果比較少,以下引用業(yè)界比較知名的幾個(gè)定量研究結(jié)果:
Capers Jones分析了超過(guò)12,000個(gè)軟件開發(fā)項(xiàng)目,其中使用正式代碼審查的項(xiàng)目, 潛在缺陷發(fā)現(xiàn)率約在60-65%之間 ;大部分的測(cè)試,潛在缺陷發(fā)現(xiàn)率僅在30%左右。
Steve McConnel在《Code Complete》中提到:僅僅依靠軟件測(cè)試效能有限–單測(cè)平均缺陷發(fā)現(xiàn)率只有25%,功能測(cè)試35%,集成測(cè)試45%,相反, 設(shè)計(jì)和代碼審查可以達(dá)到55%到60%。
SmartBear研究了一個(gè)歷時(shí)3月,包括10名開發(fā)人員完成的10,000行代碼的工程,通過(guò)引入CR在接下來(lái)的6個(gè)月中可以 節(jié)省近6成的bug修復(fù)成本 。
技術(shù)交流
SmartBear研究報(bào)告中這段話比較能表達(dá)CR對(duì)技術(shù)交流的價(jià)值,引用如下:
Actually writing the source code, however, is a solitary activity . Since developers tend to create code in quiet places, collaboration is limited to occasional whiteboard drawings and a few shared interfaces. No one catches the obvious bugs; no one is making sure the documentation matches the code. Peer code review puts the collaborative element back into this phase of the software development process.
Google認(rèn)為CR參與 塑造了公司的工程師文化 ,CR也與筆者所在部門一貫倡導(dǎo)的「 極致透明 」的文化相契合,資深同學(xué)的CR,對(duì) 團(tuán)隊(duì)內(nèi)新人的快速成長(zhǎng) 也很有幫助。
卓越工程
Linux的創(chuàng)始人Linus Torvalds有句名言:Talk is cheap, Show me the code,代碼是程序員的作品,CR只是一種提升代碼質(zhì)量的工具, 敢于Show出自己的代碼,并用開放的心態(tài)去優(yōu)化完善 ,才是每個(gè)程序員審視自我,從優(yōu)秀到卓越的關(guān)鍵所在。
既然CR好處這么多,大多團(tuán)隊(duì)也在實(shí)踐,但為什么效果差強(qiáng)人意呢,主要是CR在大型項(xiàng)目實(shí)踐中面臨諸多挑戰(zhàn)。
CR面臨哪些挑戰(zhàn)?
挑戰(zhàn)1:CR的代碼改動(dòng)范圍過(guò)大
筆者觀察,很多項(xiàng)目落實(shí)CR的最大挑戰(zhàn)是項(xiàng)目進(jìn)度壓力很大,發(fā)布計(jì)劃倒排根本沒(méi)有給CR預(yù)留時(shí)間,所以大部分CR是在臨近提測(cè)前(甚至有些是邊測(cè)試邊進(jìn)行)集中進(jìn)行,面對(duì)動(dòng)輒上千行的代碼變動(dòng),評(píng)審者需要花大量時(shí)間和代碼提交者交流了解業(yè)務(wù)邏輯,迫于時(shí)間壓力大多只會(huì)檢查最基本的編碼規(guī)范問(wèn)題,而沒(méi)有達(dá)到CR預(yù)期的效果。SmartBear公司對(duì) CR 節(jié)奏的研究指出:每次大于400行的CR每千行代碼缺陷發(fā)現(xiàn)率幾乎為零。
那么怎樣的提交粒度比較合適呢?筆者的經(jīng)驗(yàn)是和 單元測(cè)試case匹配 (這里問(wèn)題來(lái)了,沒(méi)時(shí)間寫單元測(cè)試怎么辦,本文姊妹篇再來(lái)聊聊單元測(cè)試),完成一個(gè)功能,跑一個(gè)單元測(cè)試驗(yàn)證邏輯,然后commit一次。如下圖,Aone(阿里內(nèi)部的研發(fā)平臺(tái))提供的功能內(nèi)置支持按照提交版本分批DIFF,分批Review。
挑戰(zhàn)2:CR對(duì)評(píng)審者全局知識(shí)要求很高
CR一般由團(tuán)隊(duì)內(nèi)資深的技術(shù)同學(xué)進(jìn)行,對(duì)于大型復(fù)雜項(xiàng)目的CR需要評(píng)審者對(duì)編碼規(guī)范、分布式架構(gòu)設(shè)計(jì)原則、業(yè)務(wù)知識(shí)有全面的了解,舉個(gè)例子,下面是某服務(wù)提供的等級(jí)查詢接口關(guān)鍵代碼CR片段:
- public Level queryLevel(LevelQueryRequest request) {+ public Level queryLevelWithExpireRefresh(LevelQueryRequest request) { Level result = levelRepository.findLevelWithoutInit(request.getId()); if (null == result || isExpired(result.getEndTime())) { // 如果等級(jí)為空,兜底返回L0;等級(jí)已過(guò)期,實(shí)時(shí)返回默認(rèn)等級(jí),并異步刷新 if (result == null) { result = levelRepository.buildInitLevel(request.getId(), LevelEnum.L0); } //查詢?yōu)榭?,或者已過(guò)期發(fā)送消息,刷新等級(jí)- LevelRefreshRequest refreshRequest = buildRefreshRequest(request);- levelWriteService.refreshLevel(message.getId(), refreshRequest); + RefreshMessage refreshMessage = buildRefreshMessage(request);+ refreshMessageProducer.sendMessage(refreshMessage); } return result;}- public class RefreshMessageListener extends AbstractMessageListener {+ public class RefreshMessageListener extends AbstractOrderlyMessageListener { @Autowired- private LevelWriteService levelWriteService; + private LevelWriteRegionalService levelWriteRegionalService; @Override protected boolean process(String tags, String msgId, String receivedMsg) { RefreshMessage message = JSON.parseObject(receivedMsg, RefreshMessage.class); if (message == null || message.getId() == null) { log.warn(\"message is invalid, ignored, src={}\", receivedMsg); return true; } LevelRefreshRequest refreshRequest = buildRefreshRequest(message);- levelWriteService.refreshLevel(message.getId(), refreshRequest);+ levelWriteRegionalService.refreshLevel(message.getId(), refreshRequest); return true; }}
面對(duì)上面代碼改動(dòng),要進(jìn)行富有成效的CR,下面是代碼評(píng)審者必須掌握的業(yè)務(wù)和技術(shù)知識(shí):
為什么存在等級(jí)為空的情況? 為什么要設(shè)計(jì)成讀時(shí)寫? 為什么不是直接計(jì)算等級(jí),而需要用消息隊(duì)列? 為什么要用Regional(區(qū)域化)接口和有序消息刷新等級(jí)?挑戰(zhàn)3:CR價(jià)值最大化需要團(tuán)隊(duì)具備卓越工程基因
前文提到CR有助于團(tuán)隊(duì)內(nèi)的技術(shù)交流,下面是幾個(gè)筆者親歷的Case,通過(guò)對(duì)典型CR問(wèn)題的廣泛討論不僅提升了業(yè)務(wù)代碼的質(zhì)量,而且探索到了技術(shù)創(chuàng)新點(diǎn),逐步建立起團(tuán)隊(duì)追求技術(shù)卓越的氛圍:
CASE1:一個(gè)業(yè)務(wù)使用3個(gè)時(shí)間穿越開關(guān) 背景
時(shí)間穿越是營(yíng)銷類業(yè)務(wù)系統(tǒng)最常使用的工具之一,通過(guò)全局控制,可以提前測(cè)試某個(gè)在未來(lái)開始的業(yè)務(wù)功能。筆者接觸到的一個(gè)業(yè)務(wù)由2個(gè)服務(wù)A、B組成,A是一個(gè)老應(yīng)用,使用了一個(gè)開關(guān),后來(lái)B又在不同業(yè)務(wù)場(chǎng)景中使用了2個(gè)新的開關(guān),在一次CR中發(fā)現(xiàn)了一個(gè)業(yè)務(wù)重復(fù)使用3個(gè)不同開關(guān)的問(wèn)題,由此展開了一次討論。
private static final String CODE = \"BENEFIT_TIME_THROUGH\";public Date driftedNow(String userId) { try { TimeMockResultresult = timeThroughService.getFutureTime(CODE, userId); if (result.isSuccess()) { return new Date(result.getData()); } } catch (Throwable t) { log.error(\"timeThroughService error. userId={}\", userId, t); } return new Date();}
觀點(diǎn)1:徹底服務(wù)化
按照DRY(Don"t Repeat Yourself)原則,最理想的方案是把該業(yè)務(wù)用到的時(shí)間穿越開關(guān)統(tǒng)一由一個(gè)服務(wù)提供,因?yàn)闀r(shí)間穿越工具是借助動(dòng)態(tài)配置中心推送開關(guān)到本地,然后做內(nèi)存計(jì)算;如果統(tǒng)一成服務(wù)后,A和B都需要依賴遠(yuǎn)程服務(wù)調(diào)用,而B是一個(gè)高并發(fā)的使用場(chǎng)景,會(huì)有較大性能損耗。
觀點(diǎn)2:富客戶端
把統(tǒng)一開關(guān)包裝成一個(gè)三方庫(kù),獨(dú)立提供jar包供A、B服務(wù)分別依賴,這樣解決了前面方案的性能消耗問(wèn)題。但兩個(gè)應(yīng)用需要同步做更新和升級(jí)。
觀點(diǎn)3:配置統(tǒng)一,重復(fù)代碼三處收攏為兩處
該觀點(diǎn)認(rèn)為徹底服務(wù)化和富客戶端屬于兩種極端,可以采取折中方案容忍部分代碼重復(fù),但使用相同的時(shí)間穿越開關(guān)。
總結(jié):
這個(gè)Case的討論涉及到一個(gè)公共邏輯抽取的方案權(quán)衡問(wèn)題,進(jìn)程內(nèi)調(diào)用的富客戶端性能損耗低,但后期維護(hù)和升級(jí)困難,而且過(guò)于復(fù)雜的客戶端邏輯容易引發(fā)依賴方包沖突、啟動(dòng)耗時(shí)增加等問(wèn)題;徹底服務(wù)化只需要保持接口契約一致可以實(shí)現(xiàn)較快迭代,但對(duì)服務(wù)提供者SLA要求高;為了平衡前兩者的問(wèn)題,微服務(wù)架構(gòu)中的SideCar模式則是在功能性的應(yīng)用容器旁部署另一個(gè)非功能性容器,使得開發(fā)團(tuán)隊(duì)可以對(duì)主應(yīng)用和SideCar進(jìn)行獨(dú)立管理。關(guān)于這個(gè)問(wèn)題網(wǎng)上有很多討論內(nèi)容讀者可以進(jìn)一步了解學(xué)習(xí)。
CASE2:SSR(服務(wù)端渲染)API穩(wěn)定性優(yōu)化 背景
上圖是一個(gè)典型的服務(wù)端渲染服務(wù)架構(gòu),SSR服務(wù)通過(guò)加載配置,對(duì)每個(gè)模塊進(jìn)行獨(dú)立數(shù)據(jù)組裝,并整體返回結(jié)果到端側(cè)。一般應(yīng)用在電商系統(tǒng)復(fù)雜只讀頁(yè)面的動(dòng)態(tài)搭建,如首頁(yè)、商品詳情頁(yè)、導(dǎo)購(gòu)頻道等。下面是組裝數(shù)據(jù)部分的待評(píng)審代碼片段。
// 提交任務(wù)ioTaskList.stream().forEach(t ->futures.add(pool.submit(() ->t.service.invoke())));// 阻塞獲取任務(wù)結(jié)果futures.stream().forEach(f ->{ try { result.add(f.get()); } catch (Exception e) { log.error(e.getMessage(), e); }});
Step1:增加固定超時(shí)控制 // 提交任務(wù)ioTaskList.stream().forEach(t ->futures.add(pool.submit(() ->t.service.invoke())));// 阻塞獲取任務(wù)結(jié)果futures.stream().forEach(f ->{ try {- result.add(f.get());+ result.add(f.get(1000, TimeUnit.MICROSECONDS)); } catch (Exception e) { log.error(e.getMessage(), e); }});
Step2:自適應(yīng)超時(shí)控制 public abstract class BaseServiceimplements Service { @Override public T invoke(ServiceContext context) { Entry entry = null; try { // 根據(jù)service類別構(gòu)造降級(jí)資源 String resourceName = \"RESOURCE_\" + name(); entry = SphU.entry(resourceName); try { // 未觸發(fā)降級(jí),正常調(diào)用后端服務(wù) return realInvoke(context); } catch (Exception e) { // 業(yè)務(wù)異常,記錄錯(cuò)誤日志,返回出錯(cuò)信息 return failureResult(context); } } catch (BlockException e) { // 被降級(jí),可以fail fast或返回兜底數(shù)據(jù) return degradeResult(context); } finally { entry.exit(); } } public abstract T realInvoke();}
Step3:自適應(yīng)超時(shí)控制+自定義資源key public abstract class BaseServiceimplements Service { @Override public T invoke(ServiceContext context) { Entry entry = null; try { // 這里的key由service實(shí)現(xiàn),融合了服務(wù)類型和自定義key構(gòu)造降級(jí)資源 String resourceName = \"RESOURCE_\" + key(context); entry = SphU.entry(resourceName); try { // 未觸發(fā)降級(jí),正常調(diào)用后端服務(wù) return realInvoke(context); } catch (Exception e) { // 業(yè)務(wù)異常,記錄錯(cuò)誤日志,返回出錯(cuò)信息 return failureResult(context); } } catch (BlockException e) { // 被降級(jí),可以fail fast或返回兜底數(shù)據(jù) return degradeResult(context); } finally { entry.exit(); } } public abstract String key(ServiceContext context); public abstract T realInvoke();}
總結(jié):
// 提交任務(wù)ioTaskList.stream().forEach(t ->futures.add(pool.submit(() ->t.service.invoke())));// 阻塞獲取任務(wù)結(jié)果futures.stream().forEach(f ->{ try {- result.add(f.get());+ result.add(f.get(1000, TimeUnit.MICROSECONDS)); } catch (Exception e) { log.error(e.getMessage(), e); }});
public abstract class BaseServiceimplements Service { @Override public T invoke(ServiceContext context) { Entry entry = null; try { // 根據(jù)service類別構(gòu)造降級(jí)資源 String resourceName = \"RESOURCE_\" + name(); entry = SphU.entry(resourceName); try { // 未觸發(fā)降級(jí),正常調(diào)用后端服務(wù) return realInvoke(context); } catch (Exception e) { // 業(yè)務(wù)異常,記錄錯(cuò)誤日志,返回出錯(cuò)信息 return failureResult(context); } } catch (BlockException e) { // 被降級(jí),可以fail fast或返回兜底數(shù)據(jù) return degradeResult(context); } finally { entry.exit(); } } public abstract T realInvoke();}
Step3:自適應(yīng)超時(shí)控制+自定義資源key public abstract class BaseServiceimplements Service { @Override public T invoke(ServiceContext context) { Entry entry = null; try { // 這里的key由service實(shí)現(xiàn),融合了服務(wù)類型和自定義key構(gòu)造降級(jí)資源 String resourceName = \"RESOURCE_\" + key(context); entry = SphU.entry(resourceName); try { // 未觸發(fā)降級(jí),正常調(diào)用后端服務(wù) return realInvoke(context); } catch (Exception e) { // 業(yè)務(wù)異常,記錄錯(cuò)誤日志,返回出錯(cuò)信息 return failureResult(context); } } catch (BlockException e) { // 被降級(jí),可以fail fast或返回兜底數(shù)據(jù) return degradeResult(context); } finally { entry.exit(); } } public abstract String key(ServiceContext context); public abstract T realInvoke();}
總結(jié):
public abstract class BaseServiceimplements Service { @Override public T invoke(ServiceContext context) { Entry entry = null; try { // 這里的key由service實(shí)現(xiàn),融合了服務(wù)類型和自定義key構(gòu)造降級(jí)資源 String resourceName = \"RESOURCE_\" + key(context); entry = SphU.entry(resourceName); try { // 未觸發(fā)降級(jí),正常調(diào)用后端服務(wù) return realInvoke(context); } catch (Exception e) { // 業(yè)務(wù)異常,記錄錯(cuò)誤日志,返回出錯(cuò)信息 return failureResult(context); } } catch (BlockException e) { // 被降級(jí),可以fail fast或返回兜底數(shù)據(jù) return degradeResult(context); } finally { entry.exit(); } } public abstract String key(ServiceContext context); public abstract T realInvoke();}
Step1很容易理解,增加1000ms超時(shí)設(shè)置可以避免某個(gè)數(shù)據(jù)源嚴(yán)重超時(shí)導(dǎo)致整個(gè)渲染API不穩(wěn)定,做到fail fast,但核心挑戰(zhàn)在于多長(zhǎng)的超時(shí)時(shí)間算合理;Step2通過(guò)依賴降級(jí)組件,根據(jù)不同數(shù)據(jù)源服務(wù)設(shè)置不同超時(shí)時(shí)間,實(shí)現(xiàn)了自適應(yīng)超時(shí)控制;Step3相比Step2改動(dòng)非常小,不了解業(yè)務(wù)背景可能不清楚它們的區(qū)別,Step2的降級(jí)控制作用在服務(wù)類別上,比如營(yíng)銷服務(wù)、推薦服務(wù)各自觸發(fā)降級(jí),但還有一類數(shù)據(jù)源服務(wù)其實(shí)是網(wǎng)關(guān)類型,內(nèi)部耗時(shí)會(huì)根據(jù)某個(gè)或某些參數(shù)不同有較大差異,例如TPP(阿里內(nèi)部的算法平臺(tái),不同算法邏輯共享一個(gè)網(wǎng)關(guān)API,但不同算法復(fù)雜度耗時(shí)差異巨大)服務(wù)就是一個(gè)典型,所以Step3允許自定義key()實(shí)現(xiàn)更精細(xì)的超時(shí)控制。
團(tuán)隊(duì)由CR引發(fā)的技術(shù)深入討論和持續(xù)優(yōu)化形成了這套自動(dòng)化降級(jí)能力,上圖是實(shí)際線上運(yùn)行效果,可以看到系統(tǒng)隨著依賴數(shù)據(jù)源服務(wù)RT的抖動(dòng)實(shí)現(xiàn)了自動(dòng)化自適應(yīng)降級(jí)和恢復(fù)。
CR有沒(méi)有最佳實(shí)踐?
Code Review的邊界
對(duì)于什么是一個(gè)好代碼,上圖從可靠、可維護(hù)和功能完備做了劃分。筆者認(rèn)為CR并非包治百病的銀彈,它也有它的能力邊界。把設(shè)計(jì)方案交給設(shè)計(jì)評(píng)審,把業(yè)務(wù)邏輯驗(yàn)證交給單測(cè);把編碼規(guī)范交給靜態(tài)代碼掃描(Static Code Analysis),剩下部分再由Peer Review做最后一道把關(guān)。CR引發(fā)的技術(shù)傳承、技術(shù)交流以及由此形成的追求卓越的團(tuán)隊(duì)文化,才是它的最大價(jià)值。
出發(fā)點(diǎn):程序員的初心
歸根結(jié)底,程序員的好奇心和匠心才是提升代碼質(zhì)量的根本,目前筆者所在部門已經(jīng)在晉升考核中增加了CR環(huán)節(jié),爛代碼會(huì)被一票否決,這就需要日常工作中不斷追求技術(shù)卓越,在平時(shí)多下功夫。
看不見(jiàn)的手:自動(dòng)代碼掃描
之前在某社區(qū)看到有個(gè)熱帖討論程序員的工作是不是勞動(dòng)密集型,某個(gè)回帖比較形象「我們的工作本應(yīng)是CPU密集型,結(jié)果卻成了IO密集型」。基本的編碼規(guī)范完全可以借助代碼自動(dòng)化掃描識(shí)別,而這個(gè)占比也是比較高,可以有效降低CR成本。業(yè)界的CheckStyle、FindBug都有完善的CI/CD插件支持,阿里云也提供了IDE 智能編碼插件 ,內(nèi)置了編碼規(guī)范支持。
看得見(jiàn)的手:Team Leader的重視
喊口號(hào)沒(méi)有用,只有躬身入局。身邊幾位參加晉升同學(xué)CR的評(píng)審官普遍感受是:代碼質(zhì)量分布通常會(huì)團(tuán)隊(duì)化,不要指望個(gè)別優(yōu)秀的同學(xué)帶動(dòng)團(tuán)隊(duì)的整體水平提升。確實(shí)如此,代碼質(zhì)量需要Team Leader高頻參與CR,技術(shù)文化的形成需要主管以身作則。
參考: https://en.wikipedia.org/wiki/Code_review https://smartbear.com/learn/code-review/agile-code-review-process/ Lessons From Google: How Code Reviews Build Company Culture 阿里巴巴Java編碼規(guī)約:https://github.com/alibaba/p3c 五種Code Review反模式:https://blogs.oracle.com/javamagazine/post/five-code-review-antipatterns Capers Jones對(duì)軟件質(zhì)量的研究分享:http://sqgne.org/presentations/2012-13/Jones-Sep-2012.pdf Modern Code Review: A Case Study at Google:https://sback.it/publications/icse2018seip.pdf 智能編碼插件:https://developer.aliyun.com/tool/cosy
關(guān)鍵詞:
相關(guān)內(nèi)容
- 卓越工程之如何做好Code Review a>
- 奮斗圖片_奮斗圖片 a>
- 烏克蘭總統(tǒng)澤連斯基與法國(guó)總統(tǒng)馬克龍通話 當(dāng)前動(dòng)態(tài) a>
- cdr打開ai文件空白-cdr能打開ai文件|視點(diǎn) a>
- 新手機(jī)怎么查序列號(hào)和激活日期_新手機(jī)怎么查序列號(hào) 全球最資訊 a>
- 讓子彈飛女演員趙銘GIF_子彈民女趙銘 子彈大胸女 趙銘不雅寫真流出 讓子彈飛 趙銘子彈趙銘-天天頭條 a>
- 今熱點(diǎn):怎么樣才能瘦大腿最快的速度(怎樣迅速瘦大腿) a>
- 今日訊!險(xiǎn)企上半年“補(bǔ)血”855億元 超去年全年 a>
- 信息:零費(fèi)率頻現(xiàn) 銀行理財(cái)產(chǎn)品“促銷戰(zhàn) a>
- 宣布日本核廢水排海符合標(biāo)準(zhǔn),IAEA被韓媒追問(wèn):是不是收了日本100萬(wàn)歐元?中國(guó)國(guó)家原子能機(jī)構(gòu)發(fā)聲-即時(shí) a>
- 年僅30歲!知名健身網(wǎng)紅離世,去世前3天曾說(shuō)脖子痛...這種病有哪些特殊信號(hào)? a>
- 董明珠亮相預(yù)制菜裝備產(chǎn)業(yè)大會(huì)!有人說(shuō)現(xiàn)在的年輕人大部分都不愿意自己做飯,董明珠認(rèn)為...... 世界今日?qǐng)?bào) a>
- 半年A股首發(fā)募資超兩千億 科創(chuàng)板創(chuàng)業(yè)板占比過(guò)七成 訊息 a>
- 全球滾動(dòng):星巴克4個(gè)月內(nèi)提價(jià)2小次_星巴克又漲價(jià)了 a>
- 江蘇省泰興市 蹚出強(qiáng)村富民新路徑|天天觀熱點(diǎn) a>
- 報(bào)到證丟了怎么補(bǔ)辦流程(報(bào)到證丟了怎么補(bǔ)辦)_即時(shí)看 a>
- 當(dāng)前最新:沈暉限高令撤銷 威馬汽車?yán)Ь畴y撤 a>
- 焦點(diǎn)信息:上海沿浦:6月29日接受機(jī)構(gòu)調(diào)研,國(guó)盛證券、亞太財(cái)險(xiǎn)等多家機(jī)構(gòu)參與 a>
- 前沿?zé)狳c(diǎn):俄媒:佩斯科夫稱“所有攻擊莫斯科的無(wú)人機(jī)均被摧毀或失效” a>
- 全球熱點(diǎn)評(píng)!蘇丹首都圈地區(qū)沖突加劇 蘇丹武裝部隊(duì)意圖切斷快速支援部隊(duì)補(bǔ)給線 a>
-
1美股道指下跌582.05點(diǎn),創(chuàng)近40個(gè)月新低
-
2中央氣象臺(tái)發(fā)布強(qiáng)對(duì)流天氣藍(lán)色預(yù)警
-
3湖北鐵路客運(yùn)3月25日起逐步恢復(fù)
-
4東京將推遲2020殘奧會(huì)
-
5哈佛大學(xué)校長(zhǎng)及妻子確診新冠病毒
-
6中央氣象臺(tái)發(fā)布藍(lán)色預(yù)警 部分地區(qū)降溫12℃以上
-
74月中旬公布20考研國(guó)家線
-
8大都市與野生動(dòng)物如何和諧相處
-
9美國(guó)總統(tǒng)希望韓提供醫(yī)療設(shè)備 文在寅考慮如有剩余將幫助
-
10韓國(guó)嚴(yán)防美輸入病例 承諾“支援”美方