華為賽意16
jvm運行時內存主要分5塊,分別是棧、程序計數器、本地方法棧、方法區、堆,其中棧、程序計數器、本地方法棧都是線程私有的,只有元空間和堆是共用的。
比如棧中存放的都是線程相關的資源,棧里面每個線程又可以細分為一個個棧幀,每個棧幀就是一次方法調用,里面存放了這次調用的相關數據,比如局部變量表、動態鏈接、操作數棧、方法返回地址等。一般來說一個線程的棧資源默認為1MB,一個棧幀為1KB,所以遞歸調用太深可能會導致棧溢出。
另外像本地方法棧類似,只不過是給native方法使用的。
再像程序計數器主要就是用來記錄當前指令執行的位置,可以為每個線程保存各自的執行位置,方便線程切換后能恢復原執行的位置
再比如方法區,現在用的1.8也就是元空間,就是用來存放一些元數據的,包括加載的class類、方法、常量、符號引用等
最后一塊就是堆了,也是內存占用最大的一塊,也是我們應該重點關注的地方,因為絕大部分時間GC都發生在這里,像我們現在生產用的jdk1.8,gc用的cms,特點就是停頓時間短,符合toC的業務,像fgc后每次老年代清理不多就要考慮是不是內存泄漏了,這里可以通過jstat看內存變化,如果有明顯特征可以用jmap dump出快照,用mat分析,看看哪個對象深堆最大,一般就是泄漏點
棧幀中的動態鏈接存放的是運行時常量池中該方法所屬類的引用,通過查詢常量池中的符號引用可以拿到目標方法的直接引用,從而完成方法調用
默認初始化大小是10,當滿了之后會進行擴容,默認是1.5倍,擴容會新建一個數組,然后通過arrayCopy操作復制過來,再切換引用完成擴容,一般建議在新建list時就指定好預期大小,盡量減小擴容次數。
思路就是遍歷list,用map進行存儲,key為姓名,value為實例個數,具體實現可以直接使用lambda表達式,stream.collect.group.by
循環依賴是發生在spring中bean的初始化過程中的,bean完成反射實例化后開始進行屬性注入,如果這個時候A依賴了B,B又依賴了A,這個時候就會發生循環依賴。spring在這里使用了三級緩存來幫我們解決這個問題,具體來說,一級緩存singletonobject,存放的是完整的最終的bean,查找時優先從這個map取bean,如果沒找到就會到二級緩存中找,二級緩存存放的就是earlySingletonObject,這里面的就是已經完成實例化,但是未進行初始化的半成品bean,雖然不能對外提供完整服務,但是作為依賴注入還是沒問題的。如果二級緩存中也沒有就會去三級緩存中查詢,三級緩存中存放的就是這個bean的一個工廠類objectFactory,拿到工廠類后如果注入的是代理類就會在這里先生成代理同時把代理類放到二級緩存,接著完成B的初始化,等B完成后再回頭到A的屬性注入,完成A的初始化,完了之后會把A放到一級緩存并清空二三級緩存。
用2層緩存行不行?
光看循環依賴這個問題來說是可以實現的,但是放到整個springbean的生命周期流程來說是不行的,正常情況是先進行bean實例化、屬性注入、初始化、生成代理。如果是二級緩存,意味著無論是否有循環依賴,代理類都必須在屬性注入階段提前生成代理,這是與bean的生命周期相違背的,因此采用三級緩存,先把實例化的bean封裝到objectFactory里,如果有循環依賴且是代理類就不得不先生成代理類,如果沒有循環依賴,就可以和原流程一致,在初始化后進行生成代理。
spring bean生命周期擴展點很多,主要分幾個階段,
第一個就是bean工廠的后置處理器beanFactoryPostProcessor,這個主要是針對beanDefinition或者beanFactory的擴展點,可以在完成beanDefinition掃描后對holder中的beanDefition做一些操作,比如新增屬性等,也可以在這個時機往里面新加一個beanDefinition,像mybatis里的mapper接口就是在這個時機完成掃描和注冊的,mybatis通過import注解引入了一個MapperScanner解析類,實現了importBeanDefinitionRegistrar,從而完成掃描注冊
再一個就是在bean實例化屬性注入之后,在初始化的前后也有擴展點,實現了beanPostProcessor接口的邏輯會在這個時機執行。比如aop的主要入口就在初始化之后執行的。
其他還有一些擴展點,比如aware接口,可以幫你注入applicationContext、beanFactory等等,在beanPostProcess之前執行,所以我們實現了一個beanPostProcess的話可以再實現aware接口,從而拿到上下文
另外還有當bean實例化之后屬性注入前,也有一個擴展點,可以在這里直接返回代理類,跳過后續流程等。
配置中心實現原理
配置中心主要好處有2個,一個是可以統一管理我們的項目配置,第二個就是可以動態刷新配置。像我們現在用的是一個自研的配置中心,利用springboot的自動裝配,在客戶端會新建一個ApplicationListen,用來監聽上下文環境準備好事件,然后在事件中用http長輪詢的方式從服務端獲取變化的配置,服務端會掛起這個請求,當一定時間范圍內如果發生了配置變更,服務端就會返回這個請求,沒有的話就會重新發起一個長輪訓,客戶端拿到這個結果后就可以更新本地緩存的配置了。
用過nacos嗎
nacos的客戶端和服務端交互機制也差不多,不過nacos客戶端拿到的是變化的key,然后會發布一個刷新配置的事件,借助springCloud的@RefreshScope,會去刷新環境配置,然后再刪除所有refresh作用域的目標bean,對應的代理bean還是不會動,當我們再去獲取某一個配置項時,就會重新實例化這個配置項的目標bean,從環境配置中拿到最新值。完成配置的動態更新
索引失效的場景還挺多,比如用了not in,is null這種,還有比如用了like左模糊,還有條件里索引字段用了函數或者計算,或者字段類型不匹配,比如字符類型傳了數值類型,再或者用了or但是有個字段沒有索引會導致另一個索引也失效,還有用了組合索引時,沒有使用索引里左側字段也會導致索引失效,因為組合索引是吧這幾個字段的值拼在一起排序的,如果不用左邊的那也就沒有索引效果了。
如何知道一個索引有沒有生效?
一般可以通過explain命令可以解析下sql語句,看他有沒有走索引,看key字段就知道走了哪些索引,一般來說還要關注type字段,他表示走的索引類型,通常要達到ref或者eq_ref級別就是比較好的,比如用了范圍查詢大部分時候就是range類型,再比如index一般統計類的sql出現的比較多,表示掃描了索引樹,如果extra里面有use index,說明走了覆蓋索引。最后就是all了,這個就是全表掃描了,性能是最低的,一般都是要進行sql優化的。
$表示字符串拼接,如果我們sql中用了$符號,則可以傳任意內容,比如傳表名甚至一段sql,所以這種雖然便利但是很容易導致sql注入。#表示預處理中的占位符,只能替換sql中的value,一般場景我們用#就夠了,但比如像我們在本地用了分庫分表,可能還是要用到$符號來完成切表。
一對多映射
一般使用collection標簽來標記關聯查詢,我們生產上未使用
線程池主要參數有核心線程數,最大線程數,最大線程數超時時間、時間單位、隊列、拒絕策略,工廠對象,一般我們都會用工廠來給不同的業務設定不同的線程池名稱方便排查問題,我們生產上把一些性能敏感的場景都單獨用線程池來并行獲取頁面不同數據源,用來降低耗時,像核心線程數和最大線程數都是根據生產的qps來估算的,大致為QPS*平均耗時即為線程數,隊列設置的SynchronousQueue,相當于0容量,保證接口低延遲,拒絕策略是線程滿了主線程執行保證請求成功率,像這些配置都是放在配置中心的,可以動態調整,除了隊列類型
kafka是一個高性能的MQ,一般生產上用它來做日志聚合、埋點數據上報等操作。他之所以速度快主要分幾個原因,第一個是從他的架構設計來看,他是一個支持水平擴展的分布式系統,一個topic可以被拆成多個partition,也就意味著可以同時往這些分區里面寫,而且partition又被拆成了多個logSegment,這樣通過offset來查找消息時可以更快定位。第二個原因是IO這塊,他利用了操作系統的文件系統緩存機制,可以提前把數據緩存到內存,減少io次數,以及還有用了零拷貝,也是減少了線程切換和數據拷貝時間。第三個原因就是在消費這塊支持消費組,一個消費組的consumer可以并行消費一個topic,提高吞吐量,當然一個partition最多一個consumer消費
消息堆積一般要進行事前監控,以及從消費能力、消息過期等前期設計就要考慮到,從而減小消息堆積發生的概率。當然再設計
仍然可能出現堆積情況,一般來說如果線上已經發生堆積,首先要看下堆積的原因,如果是因為突發流量或者業務量正常上漲導致的堆積,這種情況可以增加消費者加快消費速度,如果是因為消費速度變慢就需要排查代碼問題了。
代碼問題解決,mq堆積了很多消息,怎么不影響最新的業務消息?
這個開始有提過,最好預先設計消息過期時間,這樣老消息可以轉到死信隊列再做補償處理,就不會影響新消息。或者也可以臨時弄個消費者把這些積壓的消息寫到新隊列后續慢慢處理,也不會影響到最新的消息
由于這個業務主要是分發,從整體架構來說,用了cdn加速獲取緩存資源,網關這邊用了多個ng集群來確保高性能和高可用。
具體到業務主要用了場景線程池隔離,下游并發調用、多級緩存caffein\redis、mq異步解耦、接口熔斷限流、用的CMSGC降低單次停頓時間、異步日志埋點等
這里需要用到關聯查詢,可以先通過對姓名分組對更新時間使用聚合函數max,可以得到每個姓名的最后更新時間,然后再寫一個內聯,通過姓名和更新時間即可查詢出每個姓名的最新一條數據
SELECT t1.* FROM t1
INNER JOIN (
SELECT max(id),userName
FROM user
GROUP BY userName
) t2
ON t1.userName= t2.userName AND t1.id= t2.id;
一般來說大的放里面會好一點,這樣意味著內層循環次數更少,循環切換次數更少。