項目難點解決


項目背景:

實驗室項目:

項目需求:

實驗室老師分布的任務

做眼鏡的電商網站

時間是1個月

3個前端,1個后端。

用戶模塊:用到了springMVC攔截器進行用戶權限管理,設計了高服用響應對象,用本地緩存guavacache防止橫向越權

分類模塊:遞歸設計無限層級結構,對復雜對象排重

商品模塊:ftp文件服務器,動態分頁和排序,靜態塊讀取配置

購物車模塊:解決計算精度丟失

支付模塊:通過查閱支付寶demo和解析源碼調通支付寶對接

訂單模塊:訂單生成唯一,定時關單


自我介紹:

我叫吳信翰,今年21歲,畢業於海南大學本科,市場營銷(電子商務方向)專業。因為專業涉及電子商務,所以專業也有開設簡單的計算機知識課程,但我知道那遠遠不夠,由於非常喜歡計算機技術,並期待將來從事該專業方向的工作,因而在校期間十分注重計算機這方面的學習,擁有扎實的Java基礎,良好的編程風格;熟悉Spring,SpringMVC等開源框架。

然而,我深知僅有專業知識是不夠的,社會需要的是高素質復合型人才,因而在校期間除了學習計算機相關知識,我積極參加實驗室的開發工作,參與了電商網站的后端開發,通過親自動手及不斷地向有經驗的工程師請教學習,結束時我已經基本掌握整個Java平台的核心技術,獨立編程能力大大提高。同時也讓我意識到從事Java編程工作團隊合作的重要性。

雖然我的實際工作經驗還不是很豐富,但相信有了扎實的專業基礎知識和一定的項目經驗,加上好學上進的精神,我能夠勝任應聘崗位需求。希望貴公司給我這次機會。


在項目背景上:

這是一個實驗室項目,做的是一個電商網站,人員配置為4人,3個前端1個后端。具體使用時間為1個月零五天,期間因為各自的時間沖突有過真空期,但我們一開始就考慮過各種情況,所以討論后用了更自由的前后端分離開發。

在這個項目里:

后端用的是SSM架構,具體而言還用到了springMvc的攔截器和全局異常處理,spring的IOC和mybatis的ORM,在數據庫方面用到了mysql,因為是前后端分離式開發,完全用接口對接,所以還做了簡單的sql調優。在代碼里,用到了諸如ArrayList、HashMap集合對象和對象排重,用到了分頁插件,以及用ftp服務器進行圖片文件存儲,並用nginx做靜態HTTP服務器和反向代理請求,因為對性能有一點要求,所以做了簡單的調優。

在項目管理方面:

用Maven來管理和隔離項目,用git做版本管理,用logback做日志管理,用postman做接口測試,


項目難點:

實際開發中碰到了各種各樣的bug。

商品價格計算精度的丟失{

關於 java 的 float 和 double

Java 語言支持兩種基本的浮點類型: float 和 double 。java 的浮點類型都依據 IEEE 754 標准。IEEE 754 定義了32 位和 64 位雙精度兩種浮點二進制小數標准。

float(32位) 1+8+23 符號位+冪位+小數位

double(64位) 1+11+52

在 double 的尾數 為: 001100010110011110010111 0000000000000000000000000000 ,省略后面的零,至少需要24位才能正確表示 

因為 float尾數 最多只能表示 23 位,所以 24 位的 001100010110011110010111 在 float 下面經過四舍五入變成了 23 位的 00110001011001111001100 。所以 20014999 在 float 下面變成了 20015000 。

浮點運算很少是精確的,只要是超過精度能表示的范圍就會產生誤差。往往產生誤差不是 因為數的大小,而是因為數的精度。因此,產生的結果接近但不等於想要的結果。尤其在使用 float 和 double 作精確運 算的時候要特別小心。

也就是說 20014999 雖然是在float的表示范圍之內,但 在 IEEE 754 的 float 表示法精度長度沒有辦法表示出 20014999 ,而只能通過四舍五入得到一個近似值。


 使用BigDecimal的string構造器計算

}

配置文件占位符的丟失

{  

即DBCP連接池常量配置引用失效

在網上找到了對應的方法,即在name值下用具體的sqlSessionFactory的name替換sqlSessionFactory

查了源碼 注釋中寫到這個配置是在spring上下文中不止一個SqlSessionFactory時用來指定具體的一個,通常是不止一個數據源時使用

注意這里使用的是bean的名字,不是bean的引用。這是由於在開始階段,這個scanner類加載的比較早,過早地構造mybatis 對象實例。

MapperScannerConfigurer這個類在spring ioc容器中創建較早,如果直接引用sqlSessionFactory的話,導致這個bean也過早創建,

而占位符那個類PropertySourcesPlaceholderConfigurer此時還沒有創建,自然未注冊到spring 應用上下文中去,因而導致占位符失效。

另外可以看到下面這個方法已經廢棄。 @Deprecated

}

maven的jar包依賴沖突{

在你想要分析的模塊上右鍵選擇show Dependencies,

右鍵,點擊Exclude即可

}

還有管理員唯一登錄的故障

{

在這個網站中,設計了后台管理員單態登陸,即一個賬號只能有一個登陸實例。很容易想到的就是用application實現,在application中放置一個hashmap,儲存登陸的管理員信息。同時為了實現登陸超時,也在session中存放登陸對象,通過設置session的listener監控session的消亡,移除application中的對象。那么問題來了,如果用戶因為斷電或者任性,非法關閉了瀏覽器,再打開瀏覽器,顯然application中還存在上一次的登陸對象,不能再次登陸了,這樣就造成了用戶永遠無法登陸的問題

利用cookie保存成功登陸的sessionId,然后在application對象再增加一個hashmap,key是sessionId,value依然是管理員對象。每次打開登陸界面時,首先檢查cookie,如果存在就取出sessionId,然后查看application中保存sessionId的hashmap,如果存在這個sessionId,就取出管理員信息,並放在新的session中。

PS:瀏覽器的session機制是這樣的,每次打開瀏覽器服務器會為這個瀏覽器創建一個新的session,它的消失不受瀏覽器的關閉控制。而是服務器自動回收,但是如果瀏覽器關閉再打開,就會產生一個新的session,但並不意味着服務器中找不到上一次打開瀏覽器的session。所以將sessionId保存在cookie中,相當於變相在瀏覽器中找到上一次的session並進行操作。

}

數據庫讀寫分離

{

定義動態數據源,使用ThreadLocal記錄當前線程的數據源key保證線程安全

在進入Service之前,使用AOP來做出判斷,是使用寫庫還是讀庫,判斷依據可以根據方法名判斷,比如說以query、find、get等開頭的就走讀庫,其他的走寫庫。  數據源默認走寫庫

主從復制:

mysql主(稱master)從(稱slave)復制的原理:

1、master將數據改變記錄到二進制日志(binarylog)中,也即是配置文件log-bin指定的文件(這些記錄叫做二進制日志事件,binary log events)

2、slave將master的binary logevents拷貝到它的中繼日志(relay log)

3、slave重做中繼日志中的事件,將改變反映它自己的數據(數據重演)


}

springMVC注解故障

{

封裝ftp服務器接口提供服務支持,我封裝這個接口並通過本地單元測試后就部署到測試環境中開始測試了。沒想到一測試就報NullPointerException異常

從異常棧上可以清楚的看出錯誤原因,是由於請求地址不標准(以 http:// 開頭)導致的。這個錯誤其實很詭異,因為我已經在配置文件中通過XML的方式注入URL屬性值了,而且在本地寫單元測試都能通過,為什么還會屬性注入失敗呢?經過反復的檢查和嘗試,發現只要在class的定義上加@Service注解,問題就會重現,去掉則正常運行。

問題定位

在保留@Service注解的情況下,重新在本地部署並啟動工程,從啟動日志上發現此實現Bean被替換過:

Spring Bean發生替換是因為在同一個WebApplicationContext下,重復注入同一名稱的Bean實例。在日志文件中可以看出被替換后的是我定義的xml文件注入的Bean,即使發生替換也不影響正確運行。經過反復調試發現,只要在類的定義前面加上@Service注解,問題就會重現。

問題排查及解決

(Jmap是JDK自帶的堆分析工具Java Memory Map,可以通過此工具打印出某個Java進程內存內的所有對象大小和數量;建議在測試環境中使用jmap -histo:live命令查詢,執行此命令會觸發一次Full GC)

1、使用jmap分析分析該類實例的個數,
查看發現系統中居然有2個實例!這和我們對“Spring創建Bean默認是單例的”認知不符,那就把進程Dump出來詳細解刨下這2個對象吧!通過Jmap的dump參數把進程鏡像dump出來:
 $ jmap -dump:format=b,file=/tmp/heap.bin 20881
此時可以使用MAT(內存分析工具,Memory Analysis Tool)

JProfiler==MAT插件

並配合Jhat快速定位到此類的實例對象上,通過對象間的引用關系來查找定位原因。(

jhat(Java Head Analyse Tool)是jdk自帶的用來分析java堆快照的工具,具體的使用方法是:

jhat dump_file_name

使用jhat命令打開了之前dump出來的堆快照文件,可以看到,命令成功執行后會在命令執行的本機啟動一個http服務,可以在瀏覽器上打開本機的7000端口查看詳細的分析結果:)

2、首先通過Jhat工具來查看QueryPartnerImpl對象及對象間的引用關系:

點擊鏈接Instances -> Exclude subclasses查看類的實例對象:

點擊查看每個對象屬性注入情況:

通過Jhat展示的對象引用關系看,只有org.springframework.beans.factory.support.DisposableBeanAdapter和java.util.concurrent.ConcurrentHashMap$Node 比較可疑。但DisposableBeanAdapter是用來管理Spring Bean的銷毀,所以和本事故無關,重點就落在java.util.concurrent.ConcurrentHashMap$Node 上了。

通過MAT工具來分析java.util.concurrent.ConcurrentHashMap$Node@0x7aeb05b18的引用關系,通過對象查找工具並輸入對象的內存地址定位:

選中這個對象,右鍵打開菜單選項,選擇:Lists objects -> with incoming references查看都有哪些對象持有此對象(with outgoing references表示此對象擁有哪些對象):

通過上面對象引用追蹤路徑可以看到,queryPartnerImpl@0x7aeafac20最終被DispatcherServlet@0x7ae577e00對象引用。

采用同樣的方式來分析queryPartnerImpl@0x6c41b6f80的對象引用關系:

queryPartnerImpl@0x6c41b6f80最終被ContextLoaderListener@0x6c358f7f8引用。

通過對比發現:

ContextLoaderListener和DispatcherServlet對我們來說非常熟悉,這是在Spring MVC項目中的web.xml中配置的,ContextLoaderListener用來初始化root WebApplicationContext;DispatcherServlet是請求分發控制器,啟動時也會初始化一個自己的WebApplicationContext,並設置parent為root WebApplicationContext,從而形成常說的“父子關系”。DispatcherServlet如果在自己的WebApplicationContext能找到需要用的對象就直接使用,只有在找不到對象的情況下才會去查找父容器里的。

到這里我們找到了引起事故發生的根本原因,但是我們還需要找出引發事故的罪魁禍首!通過前面的分析我們知道這和ContextLoaderListener、DispatcherServlet有關系,那就定位到web.xml的配置文件中來:

在spring/spring-servlet.xml配置文件中我們開啟了注解掃描功能,並且從項目路徑“com.meituan.trip.mobile.hermes”開始掃描:

我們知道Spring會通過@Service注解去實例化一個Bean,屬性如果沒有通過注解注入進來的話,就用默認值。在此配置文件后面就再沒有對queryPartnerImpl的定義,也就不會發生替換的情況。DispatcherServlet只能獲得由注解加載的半成品Bean。

我們在applicationContext.xml中也同樣開啟了注解掃描功能,也是從項目路徑“com.meituan.trip.mobile.hermes”開始掃描,但是在下文的sal/service-out.xml配置文件中,又重新對queryPartnerImpl通過XML定義,所以會發生替換現象。

到這里我們才最終搞清楚發生這次事故的最根本原因,解決辦法是要讓整個系統中只有一個屬性注入成功的queryPartnerImpl對象,途徑有如下幾種:
1)刪除@Service注解:這個方法治標不治本,因為配置、 注解掃描功能后會開啟包括@Service在內的超過6種注解,而這些注解部分在用;

2)掃描隔離:通過配置的屬性use-default-filters並配合include-filter/exclude-filter實現掃描過濾,只掃描指定注解。

問題總結

  1. 使用注解並不一定會引起錯誤,但是注解要使用規范,不能亂用。如果通過注解注入,屬性值最好也要通過注解方式注入;
  2. 注解掃描功能雖然很強大、很方便,但是要注意區分掃描范圍及過濾特定注解;
  3. 單元測試能通過的原因:我們一般只指定加載一個配置文件作為測試環境,類實例只會出現一個,故能測試通過;
  4. 最好最重要的一點就是在使用任何框架時,最好按"Best Practice"規范,避免出現一些莫名其妙的問題。

進一步探討

通過閱讀Spring源碼中涉及ContextLoaderListener和DispatcherServlet的部分學習到,ContextLoaderListener在Context初始化的時候會創建一個root WebApplicationContext,並將此對象存儲在ServletContext中,Key為:WebApplicationContext.class.getName() + ".ROOT”;DispatcherServlet在初始化過程也實例化了一個自己的WebApplicationContext,設置在ServletContext中的key為:
FrameworkServlet.class.getName() + ".CONTEXT.”+ getServletName(),同時設置此對象的parent為 ContextLoaderListener定義的 root WebApplicationContext。DispatcherServlet所創建的WebApplicationContext被稱為子容器,子容器可以訪問父容器中的內容,但父容器不能訪問子容器中的內容。
Spring官方在介紹Spring MVC的同時,也給我們介紹了WebApplicationContext的繼承關系:

從圖中可以看出,每個DispatcherServlet都會去實例化一個自己的WebApplicationContext,而這個WebApplicationContext可以獲得root WebApplicationContext中已經實例化好的Bean。


}

性能問題

一、代碼優化{

 1)用枚舉類

2)防止空指針

3)按規范要求寫

4)用prudctList去分頁 不用VO分頁 因為startPage要走DAO層Mapper

}

二、數據庫調優{

1)sql調優

訂單status根據業務場景加索引,區分度低 影響插入更新

保證庫存一致性用悲觀鎖

悲觀鎖:明確主鍵使用行鎖,若無結果集則不上鎖

mybatis的selective 會使用if判斷空,若新new一個product對象會側面提高sql效率

@Schedulde

1. ScheduledAnnotationBeanPostProcessor

功能介紹:

負責@Schedule注解的掃描, 構建ScheduleTask向ScheduledTaskRegistrar中注冊, 調用ScheduledTaskRegistrar.afterPropertiesSet()


ScheduledTaskRegistrar

功能介紹:

它是Schedule中所支持的三種Task的一個容器, 內部維護了這些Task List和executor的引用, 並負責將Task置入executor中執行

TaskScheduler

功能介紹:

TaskScheduler是Spring中專門用於進行定時操作的接口, 主要的實現類有三個 ThreadPoolTaskScheduler, ConcurrentTaskScheduler, TimerManagerTaskScheduler, 前兩個delegate juc里面的ScheduledExecutor, 最后一個delegate commonj.timers.TimerManager. 這個類的作用主要是將task和executor用ReschedulingRunnable包裝起來進行生命周期管理

schedule底層其實是一個線程池,時間戳來觸發任務,通過周期更新下一次執行的時間戳.因為是線程池,所以是多線程並發執行的

調用的TaskScheduler的ConcurrentTaskScheduler實現類,默認單個線程執行。 

調度器本質上還是通過juc的ScheduledExecutorService進行的 
調度器啟動后你無法通過修改系統時間達到讓它馬上執行的效果 
被@Schedule注解的方法如果有任何Throwable出現, 不會中斷后續Task, 默認只會打印Error日志,定時任務不會同時被觸發。 

建立索引,冗余字段

自帶的慢查詢日志或者開源的慢查詢系統定位到具體的出問題的SQL,然后使用explain、profile等工具來逐步調優,最后經過測試達到效果后上線。

利用索引的最左匹配的原則

大表 左關聯 小表,很慢;小表 左關聯 大表,很快。

   字段類型選擇

 

     選擇合理的字段,往往可以大大減少數據庫行數據的大小,並提高索引匹配的效率,進而大大提升數據庫性能。使用更小的數據類型,如日期采用date代替datetime、類型或標記使用tinyint代替smallint和int、使用定長字段代替非定長字段(如char代替varchar),都能或多或少減少數據行大小,提高數據庫緩沖池的命中率。而作為表字段中特殊的一員―主鍵,其選型更會對表索引的穩定和效率帶來很大的影響,一般建議考慮數據庫自增或自主維護的唯一數值。

 

     高分離度字段建立索引

 

     對於查詢來講,高分離度字段往往意味着精准或部分精准的條件。相對來講是最好優化的一種場景,只需要對分離度較高的字段單獨建立索引即可。當然實際使用中會有更多細節需要摸索。精確條件在各業務中基本都會用到,在越復雜的業務場景中,精確條件優先的原則,將是最有效的優化方案。需要注意的是,盡管高分離度字段單獨建立索引效率很高,但過多的索引會影響表寫入的效率,所以需要謹慎添加。這一點iDB Cloud中有大表索引的建議可以參考。


 覆蓋索引(Covering Index)

 

     通俗一點理解,就是執行計划可以通過索引完成數據查找和結果集獲取,而無需回表(去緩沖池或磁盤查找數據)。而由於MySQL的索引機制限制,一次查詢時,將只用到一個索引或將兩個索引聚合(index_merge)起來使用,所以意味着復雜的業務場景中,單獨對每個字段建立索引可能沒有什么用處。所以對於一些特定的查詢場景,建立合適的組合索引,應用覆蓋索引方法可以避免大量隨機I/O,是更為推薦的優化方案(如果執行計划Explain的Extra中有Using Index,就說明使用了覆蓋索引)。但實際業務總是會比索引本身更復雜,業務中需要查找或者獲取的字段信息往往是很多的,而組合索引並不能涵蓋所有的字段(否則我們將擁有一個比數據還要龐大的索引)。此時,為了應用覆蓋索引,就需要使用主鍵延遲關聯(Deferred Join),即先通過組合索引中包含的字段條件,初步查詢出相對較小的結果集(面向結果集原則),該結果集只包含主鍵字段;然后通過獲取到的這個主鍵隊列,再對數據表做關聯。


慢查詢優化基本步驟

-explain rows是指標 一般rows大的慢 區分是表鎖還是行鎖 ,type = ALL表明是全表掃描

0.先運行看看是否真的很慢,注意設置SQL_NO_CACHE
1.where條件單表查,鎖定最小返回記錄表。這句話的意思是把查詢語句的where都應用到表中返回的記錄數最小的表開始查起,單表每個字段分別查詢,看哪個字段的區分度最高
2.explain查看執行計划,是否與1預期一致(從鎖定記錄較少的表開始查詢)
3.order by limit 形式的sql語句讓排序的表優先查
4.了解業務方使用場景
5.加索引時參照建索引的幾大原則
6.觀察結果,不符合預期繼續從0分析


2)架構層面的調優

這一類調優包括讀寫分離、多從庫負載均衡、水平和垂直分庫分表等方面,一般需要的改動較大,但是頻率沒有SQL調優高,而且一般需要DBA來配合參與。那么什么時候需要做這些事情?我們可以通過內部監控報警系統(比如Zabbix),定期跟蹤一些指標數據是否達到瓶頸,一旦達到瓶頸或者警戒值,就需要考慮這些事情。通常,DBA也會定期監控這些指標值。

3)連接池調優

我們的應用為了實現數據庫連接的高效獲取、對數據庫連接的限流等目的,通常會采用連接池類的方案,即每一個應用節點都管理了一個到各個數據庫的連接池。隨着業務訪問量或者數據量的增長,原有的連接池參數可能不能很好地滿足需求,這個時候就需要結合當前使用連接池的原理、具體的連接池監控數據和當前的業務量作一個綜合的判斷,通過反復的幾次調試得到最終的調優參數。




}


JVM調優{

性能指標:接口響應時間

  1. [root@flybird ~]# ab -n1000 -c10 http://www.baidu.com/index.html  
  2. # ab -n 請求數量  -c並發數量 地址

對nginx做ab測試,nignx提升吞吐量而不是平均單次響應時間


調整老年代大小

從run-edit configurations-default-application 

VM options輸入-server -XX:PermSize=1536M -XX:MaxPermSize=1536m

設為活躍數據的1.2~1.5

測試工具一開始用的是學長開發的gui接口測試工具

檢測工具(gc time、gc count、各個分代的內存大小變化、機器的Load值與CPU使用率、JVM的線程數等)

起因是然后在測試並發時,發現每次測試期間都會小卡,初步判斷為接口響應時間慢,所以就使用jstar工具檢測並檢查業務代碼

發現每次並發數量過多就會引發full gc,從日志上看是永久代內存溢出。

項目使用的是JDK1.7,永久代已經發生過一次轉移,符號引用轉移到了native 字面量靜態變量轉移到了堆

所以基本可以確定Perm的使用量和加載的類個數相關,在控制總請求數的同時並發數量多時會觸發full gc,

就在危險的邊緣試探,用jmap把內存中存活的對象dump下來

發現sun.reflect.DelegatingClassLoader對象竟然有2000多個

基本可以定位是反射類加載器導致的

反射原理分析

DelegatingClassLoader裝載了很多GeneratedMethodAccessorXXX類,

使用jhat命令打開了之前dump出來的堆快照文件

點擊鏈接Instances -> Exclude subclasses查看類的實例對象:發現時並發接口測試時使用到的對象

用MAT工具分析引用關系反復查看才知道原來是接口測試工具的鍋

他是利用反射構造類和執行方法,來避免編譯時異常來達到接口測試效果


因為該工具集成了很多且前后端一同使用,就通過把 逗比同學說不開源

永久代-XX:permsize 調大 並把垃圾收集器換成parNew+cms+serial old的組合


養成了監控的習慣

YGC升級失敗

CMS並行GC是大多數應用的最佳選擇,然而, CMS並不是完美的,在使用CMS的過程中會產生2個最讓人頭痛的問題:

  1. promotion failed
  2. concurrent mode failure

第一個問題promotion failed是在進行Minor GC時,Survivor Space放不下,對象只能放入老年代,而此時老年代也放不下造成的,多數是由於老年帶有足夠的空閑空間,但是由於碎片較多,這時如果新生代要轉移到老年帶的對象比較大,所以,必須盡可能提早觸發老年代的CMS回收來避免這個問題(promotion failed時老年代CMS還沒有機會進行回收,又放不下轉移到老年帶的對象,因此會出現下一個問題concurrent mode failure,需要stop-the-wold GC- Serail Old)。

第二個問題concurrent mode failure是在執行CMS GC的過程中同時業務線程將對象放入老年代,而此時老年代空間不足,這時CMS還沒有機會回收老年帶產生的,或者在做Minor GC的時候,新生代救助空間放不下,需要放入老年代,而老年代也放不下而產生的。

盡管CMS使用一個叫做分配擔保的機制,每次Minor GC之后要保證新生代的空間survivor + eden > 老年帶的空閑時間,但是對象分配是不可預測的,總會有寫對象分配在老年帶是滿足不了的。

由於CMS采用標記清除算法,默認並不使用標記整理算法

解決這個問題的辦法就是可以讓CMS在進行一定次數的Full GC(標記清除)的時候進行一次標記整理算法,調大新生代

當出現concurrent mode failure的現象時,就意味着此時JVM將繼續采用Stop-The-World的方式來進行Full GC,這種情況下,CMS就沒什么意義了,造成concurrent mode failure的原因是當minor GC進行時,舊生代所剩下的空間小於Eden區域+From區域的空間,或者在CMS執行老年帶的回收時有業務線程試圖將大的對象放入老年帶,導致CMS在老年帶的回收慢於業務對象對老年帶內存的分配。

解決這個問題的通用方法是調低觸發CMS GC執行的閥值,JDK1.6默認為92 調整位之前的68

CMS:GC線程和業務線程交互進行

我記得在標記階段時,會把對象和對應的對象頭數據保存在兩個棧中,如果晉升失敗的話,就把該對象的對象頭復原...




}





注意!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系我们删除。



 
粤ICP备14056181号  © 2014-2020 ITdaan.com