背景
這是我當時約面試的時間表,其實面試多了你會發現一個規律,如果一個公司你一面過的很順利,後面不管三面四面還是五面,都會比較順利。因為大家的提問方式,角度都很類似,很多時候都是在跟不同的面試官說同樣的話。
多數的公司總體上面試都是以自我介紹+項目介紹+項目細節/難點提問+基礎知識點考核+算法題這個流程下來的。有些公司可能還會問幾個實際的場景類的問題,這個環節阿里是必問的,這種問題通常是沒有正確答案的,就看個人的理解,個人的積累了。剩下的就沒啥了,都是換湯不換藥,聊項目就看你自己對你自己的項目是否理解的透徹,比如經常問你你為什麼選擇這個技術,為什麼這麼處理之類的,常考的基礎的知識點就那麼多,最後算法就是靠刷題。
這篇我主要把常考的問題po一下,,頻率出現過高的我都用(必考)標註了。由於內容實在太多,所以只列舉了部分題目和答案,關於全部面試題和答案(文末獲取)
ZooKeepe
- CAP定理
- ZAB協議
- leader選舉算法和流程
Redis
- Redis的應用場景
- Redis支持的數據類型(必考)
- zset跳錶的數據結構(必考)
- Redis的數據過期策略(必考)
- Redis的LRU過期策略的具體實現
- 如何解決Redis緩存雪崩,緩存穿透問題
- Redis的持久化機制(必考)
- Redis的管道pipeline
Mysql
- 事務的基本要素
- 事務隔離級別(必考)
- 如何解決事務的併發問題(髒讀,幻讀)(必考)
- MVCC多版本併發控制(必考)
- binlog,redolog,undolog都是什麼,起什麼作用
- InnoDB的行鎖/表鎖
- myisam和innodb的區別,什麼時候選擇myisam
- 為什麼選擇B+樹作為索引結構(必考)
- 索引B+樹的葉子節點都可以存哪些東西(必考)
- 查詢在什麼時候不走(預期中的)索引(必考)
- sql如何優化
- explain是如何解析sql的
- order by原理
JVM
- 運行時數據區域(內存模型)(必考)
- 垃圾回收機制(必考)
- 垃圾回收算法(必考)
- Minor GC和Full GC觸發條件
- GC中Stop the world(STW)
- 各垃圾回收器的特點及區別
- 雙親委派模型
- JDBC和雙親委派模型關係
Java基礎
- HashMap和ConcurrentHashMap區別(必考)
- ConcurrentHashMap的數據結構(必考)
- 高併發HashMap的環是如何產生的
- volatile作用(必考)
- Atomic類如何保證原子性(CAS操作)(必考)
- synchronized和Lock的區別(必考)
- 為什麼要使用線程池(必考)
- 核心線程池ThreadPoolExecutor的參數(必考)
- ThreadPoolExecutor的工作流程(必考)
- 如何控制線程池線程的優先級
- 線程之間如何通信
- Boolean佔幾個字節
- jdk1.8/jdk1.7都分別新增了哪些特性
- Exception和Error
Spring
- Spring的IOC/AOP的實現(必考)
- 動態代理的實現方式(必考)
- Spring如何解決循環依賴(三級緩存)(必考)
- Spring的後置處理器
- Spring的@Transactional如何實現的(必考)
- Spring的事務傳播級別
- BeanFactory和ApplicationContext的聯繫和區別
其他
- 高併發系統的限流如何實現
- 高併發秒殺系統的設計
- 負載均衡如何設計
補充
另外還會考一些計算機網絡,操作系統啊之類的。像消息隊列,RPC框架這種考的比較少。計算機網絡就是分層啊,tcp/udp啊,三次握手之類的。操作系統就是進程與線程啊,進程的數據結構以及如何通信之類的。數據結構的排序算法也比較常考,考的話一定會讓你手寫個快排。剩下的算法題就靠LeetCode的積累了。其實非算法崗考的算法題都蠻簡單的,很多題完全就是考察你智力是否正常,稍微難點的涉及到一些算法思想的按照LeetCode題目類型的分類,每種題做一兩道基本就能完全應付面試了。
面試感受及評價
除了外企,體驗最好的就是阿里。絕對的脫穎而出,無論是面試官的專業程度還是面試官對參與面試人員的態度都完全突出於其他公司。非常的尊重人,以及會引導我去作出正確的回答,唯一就是阿里的HR是非常強勢的,永遠有一票否決權。而有些公司面試官會故意誤導你,想方設法讓你說出錯誤的答案,並且有些態度極其傲慢,讓人感覺很不尊重人。這裡點名批評面試體驗最差的兩家公司:美團和Boss直聘。
外企的話,體驗都很好。微軟是英文面的,亞馬遜不是。這倆都是以算法為主,微軟除了算法還聊了操作系統和計算機網絡,亞馬遜聊了較長時間的項目細節。
最後
最後說下自己的情況,17年在京東實習,19年7月離職。正式工作時間很短,就一年(算實習兩年),而且19年有半年的時間準備考研所以有半年的空檔期,這也是為什麼我被很多HR掛了的原因。雖然Offer沒拿幾個,但是一半多都面到HR面了,所以對於兩三年經驗的感覺整理的問題還是比較有代表性的。
面試題答案部分
ZooKeeper
CAP定理:
一個分佈式系統不可能同時滿足以下三種,一致性(C:Consistency),可用性(A:Available),分區容錯性(P:Partition Tolerance).在此ZooKeeper保證的是CP,ZooKeeper不能保證每次服務請求的可用性,在極端環境下,ZooKeeper可能會丟棄一些請求,消費者程序需要重新請求才能獲得結果。另外在進行leader選舉時集群都是不可用,所以說,ZooKeeper不能保證服務可用性。(Base理論CA強一致性和最終一致性)
ZAB協議:
ZAB協議包括兩種基本的模式:崩潰恢復和消息廣播。當整個 Zookeeper 集群剛剛啟動或者Leader服務器宕機、重啟或者網絡故障導致不存在過半的服務器與 Leader 服務器保持正常通信時,所有服務器進入崩潰恢復模式,首先選舉產生新的 Leader 服務器,然後集群中 Follower 服務器開始與新的 Leader 服務器進行數據同步。當集群中超過半數機器與該 Leader 服務器完成數據同步之後,退出恢復模式進入消息廣播模式,Leader 服務器開始接收客戶端的事務請求生成事物提案來進行事務請求處理。
選舉算法和流程:FastLeaderElection(默認提供的選舉算法)
目前有5臺服務器,每臺服務器均沒有數據,它們的編號分別是1,2,3,4,5,按編號依次啟動,它們的選擇舉過程如下:
- 服務器1啟動,給自己投票,然後發投票信息,由於其它機器還沒有啟動所以它收不到反饋信息,服務器1的狀態一直屬於Looking。
- 服務器2啟動,給自己投票,同時與之前啟動的服務器1交換結果,由於服務器2的編號大所以服務器2勝出,但此時投票數沒有大於半數,所以兩個服務器的狀態依然是LOOKING。
- 服務器3啟動,給自己投票,同時與之前啟動的服務器1,2交換信息,由於服務器3的編號最大所以服務器3勝出,此時投票數正好大於半數,所以服務器3成為leader,服務器1,2成為follower。
- 服務器4啟動,給自己投票,同時與之前啟動的服務器1,2,3交換信息,儘管服務器4的編號大,但之前服務器3已經勝出,所以服務器4只能成為follower。
- 服務器5啟動,後面的邏輯同服務器4成為follower。
Redis
應用場景
- 緩存
- 共享Session
- 消息隊列系統
- 分佈式鎖
單線程的Redis為什麼快
- 純內存操作
- 單線程操作,避免了頻繁的上下文切換
- 合理高效的數據結構
- 採用了非阻塞I/O多路複用機制
Redis 的數據結構及使用場景
- String字符串:字符串類型是 Redis 最基礎的數據結構,首先鍵都是字符串類型,而且 其他幾種數據結構都是在字符串類型基礎上構建的,我們常使用的 set key value 命令就是字符串。常用在緩存、計數、共享Session、限速等。
- Hash哈希:在Redis中,哈希類型是指鍵值本身又是一個鍵值對結構,哈希可以用來存放用戶信息,比如實現購物車。
- List列表(雙向鏈表):列表(list)類型是用來存儲多個有序的字符串。可以做簡單的消息隊列的功能。
- Set集合:集合(set)類型也是用來保存多個的字符串元素,但和列表類型不一 樣的是,集合中不允許有重複元素,並且集合中的元素是無序的,不能通過索引下標獲取元素。利用 Set 的交集、並集、差集等操作,可以計算共同喜好,全部的喜好,自己獨有的喜好等功能。
- Sorted Set有序集合(跳錶實現):Sorted Set 多了一個權重參數 Score,集合中的元素能夠按 Score 進行排列。可以做排行榜應用,取 TOP N 操作。
Redis 的數據過期策略
Redis 中數據過期策略採用定期刪除+惰性刪除策略
- 定期刪除策略:Redis 啟用一個定時器定時監視所有的 key,判斷key是否過期,過期的話就刪除。這種策略可以保證過期的 key 最終都會被刪除,但是也存在嚴重的缺點:每次都遍歷內存中所有的數據,非常消耗 CPU 資源,並且當 key 已過期,但是定時器還處於未喚起狀態,這段時間內 key 仍然可以用。
- 惰性刪除策略:在獲取 key 時,先判斷 key 是否過期,如果過期則刪除。這種方式存在一個缺點:如果這個 key 一直未被使用,那麼它一直在內存中,其實它已經過期了,會浪費大量的空間。
- 這兩種策略天然的互補,結合起來之後,定時刪除策略就發生了一些改變,不在是每次掃描全部的 key 了,而是隨機抽取一部分 key 進行檢查,這樣就降低了對 CPU 資源的損耗,惰性刪除策略互補了為檢查到的key,基本上滿足了所有要求。但是有時候就是那麼的巧,既沒有被定時器抽取到,又沒有被使用,這些數據又如何從內存中消失?沒關係,還有內存淘汰機制,當內存不夠用時,內存淘汰機制就會上場。淘汰策略分為:
- 當內存不足以容納新寫入數據時,新寫入操作會報錯。(Redis 默認策略)
- 當內存不足以容納新寫入數據時,在鍵空間中,移除最近最少使用的 Key。(LRU推薦使用)
- 當內存不足以容納新寫入數據時,在鍵空間中,隨機移除某個 Key。
- 當內存不足以容納新寫入數據時,在設置了過期時間的鍵空間中,移除最近最少使用的 Key。這種情況一般是把 Redis 既當緩存,又做持久化存儲的時候才用。
- 當內存不足以容納新寫入數據時,在設置了過期時間的鍵空間中,隨機移除某個 Key。
- 當內存不足以容納新寫入數據時,在設置了過期時間的鍵空間中,有更早過期時間的 Key 優先移除。
Redis的LRU具體實現:
傳統的LRU是使用棧的形式,每次都將最新使用的移入棧頂,但是用棧的形式會導致執行select *的時候大量非熱點數據佔領頭部數據,所以需要改進。Redis每次按key獲取一個值的時候,都會更新value中的lru字段為當前秒級別的時間戳。Redis初始的實現算法很簡單,隨機從dict中取出五個key,淘汰一個lru字段值最小的。在3.0的時候,又改進了一版算法,首先第一次隨機選取的key都會放入一個pool中(pool的大小為16),pool中的key是按lru大小順序排列的。接下來每次隨機選取的keylru值必須小於pool中最小的lru才會繼續放入,直到將pool放滿。放滿之後,每次如果有新的key需要放入,需要將pool中lru最大的一個key取出。淘汰的時候,直接從pool中選取一個lru最小的值然後將其淘汰。
如何解決 Redis 緩存雪崩問題
- 使用 Redis 高可用架構:使用 Redis 集群來保證 Redis 服務不會掛掉
- 緩存時間不一致,給緩存的失效時間,加上一個隨機值,避免集體失效
- 限流降級策略:有一定的備案,比如個性推薦服務不可用了,換成熱點數據推薦服務
如何解決 Redis 緩存穿透問題
- 在接口做校驗
- 存null值(緩存擊穿加鎖)
- 布隆過濾器攔截:將所有可能的查詢key 先映射到布隆過濾器中,查詢時先判斷key是否存在布隆過濾器中,存在才繼續向下執行,如果不存在,則直接返回。布隆過濾器將值進行多次哈希bit存儲,布隆過濾器說某個元素在,可能會被誤判。布隆過濾器說某個元素不在,那麼一定不在。
Redis的持久化機制
Redis為了保證效率,數據緩存在了內存中,但是會週期性的把更新的數據寫入磁盤或者把修改操作寫入追加的記錄文件中,以保證數據的持久化。Redis的持久化策略有兩種: 1. RDB:快照形式是直接把內存中的數據保存到一個dump的文件中,定時保存,保存策略。當Redis需要做持久化時,Redis會fork一個子進程,子進程將數據寫到磁盤上一個臨時RDB文件中。當子進程完成寫臨時文件後,將原來的RDB替換掉。 1. AOF:把所有的對Redis的服務器進行修改的命令都存到一個文件裡,命令的集合。使用AOF做持久化,每一個寫命令都通過write函數追加到appendonly.aof中。aof的默認策略是每秒鐘fsync一次,在這種配置下,就算髮生故障停機,也最多丟失一秒鐘的數據。缺點是對於相同的數據集來說,AOF的文件體積通常要大於RDB文件的體積。根據所使用的fsync策略,AOF的速度可能會慢於RDB。Redis默認是快照RDB的持久化方式。對於主從同步來說,主從剛剛連接的時候,進行全量同步(RDB);全同步結束後,進行增量同步(AOF)。
Redis和memcached的區別
- 存儲方式上:memcache會把數據全部存在內存之中,斷電後會掛掉,數據不能超過內存大小。redis有部分數據存在硬盤上,這樣能保證數據的持久性。
- 數據支持類型上:memcache對數據類型的支持簡單,只支持簡單的key-value,,而redis支持五種數據類型。
- 用底層模型不同:它們之間底層實現方式以及與客戶端之間通信的應用協議不一樣。redis直接自己構建了VM機制,因為一般的系統調用系統函數的話,會浪費一定的時間去移動和請求。
- value的大小:redis可以達到1GB,而memcache只有1MB。
Redis併發競爭key的解決方案
- 分佈式鎖+時間戳
- 利用消息隊列
Redis與Mysql雙寫一致性方案
先更新數據庫,再刪緩存。數據庫的讀操作的速度遠快於寫操作的,所以髒數據很難出現。可以對異步延時刪除策略,保證讀請求完成以後,再進行刪除操作。
Redis的管道pipeline
對於單線程阻塞式的Redis,Pipeline可以滿足批量的操作,把多個命令連續的發送給Redis Server,然後一一解析響應結果。Pipelining可以提高批量處理性能,提升的原因主要是TCP連接中減少了“交互往返”的時間。pipeline 底層是通過把所有的操作封裝成流,redis有定義自己的出入輸出流。在 sync() 方法執行操作,每次請求放在隊列裡面,解析響應包。
Mysql
事務的基本要素
- 原子性:事務是一個原子操作單元,其對數據的修改,要麼全都執行,要麼全都不執行
- 一致性:事務開始前和結束後,數據庫的完整性約束沒有被破壞。
- 隔離性:同一時間,只允許一個事務請求同一數據,不同的事務之間彼此沒有任何干擾。
- 持久性:事務完成後,事務對數據庫的所有更新將被保存到數據庫,不能回滾。
事務的併發問題
- 髒讀:事務A讀取了事務B更新的數據,然後B回滾操作,那麼A讀取到的數據是髒數據
- 不可重複讀:事務A多次讀取同一數據,事務B在事務A多次讀取的過程中,對數據作了更新並提交,導致事務A多次讀取同一數據時,結果不一致。
- 幻讀:A事務讀取了B事務已經提交的新增數據。注意和不可重複讀的區別,這裡是新增,不可重複讀是更改(或刪除)。select某記錄是否存在,不存在,準備插入此記錄,但執行 insert 時發現此記錄已存在,無法插入,此時就發生了幻讀。
MySQL事務隔離級別
在MySQL可重複讀的隔離級別中並不是完全解決了幻讀的問題,而是解決了讀數據情況下的幻讀問題。而對於修改的操作依舊存在幻讀問題,就是說MVCC對於幻讀的解決時不徹底的。通過索引加鎖,間隙鎖,next key lock可以解決幻讀的問題。
Mysql的邏輯結構
- 最上層的服務類似其他CS結構,比如連接處理,授權處理。
- 第二層是Mysql的服務層,包括SQL的解析分析優化,存儲過程觸發器視圖等也在這一層實現。
- 最後一層是存儲引擎的實現,類似於Java接口的實現,Mysql的執行器在執行SQL的時候只會關注API的調用,完全屏蔽了不同引擎實現間的差異。比如Select語句,先會判斷當前用戶是否擁有權限,其次到緩存(內存)查詢是否有相應的結果集,如果沒有再執行解析sql,優化生成執行計劃,調用API執行。
SQL執行順序
SQL的執行順序:from---where--group by---having---select---order by
MVCC,redolog,undolog,binlog
- undoLog 也就是我們常說的回滾日誌文件 主要用於事務中執行失敗,進行回滾,以及MVCC中對於數據歷史版本的查看。由引擎層的InnoDB引擎實現,是邏輯日誌,記錄數據修改被修改前的值,比如"把id='B' 修改為id = 'B2' ,那麼undo日誌就會用來存放id ='B'的記錄”。當一條數據需要更新前,會先把修改前的記錄存儲在undolog中,如果這個修改出現異常,,則會使用undo日誌來實現回滾操作,保證事務的一致性。當事務提交之後,undo log並不能立馬被刪除,而是會被放到待清理鏈表中,待判斷沒有事物用到該版本的信息時才可以清理相應undolog。它保存了事務發生之前的數據的一個版本,用於回滾,同時可以提供多版本併發控制下的讀(MVCC),也即非鎖定讀。
- redoLog 是重做日誌文件是記錄數據修改之後的值,用於持久化到磁盤中。redo log包括兩部分:一是內存中的日誌緩衝(redo log buffer),該部分日誌是易失性的;二是磁盤上的重做日誌文件(redo log file),該部分日誌是持久的。由引擎層的InnoDB引擎實現,是物理日誌,記錄的是物理數據頁修改的信息,比如“某個數據頁上內容發生了哪些改動”。當一條數據需要更新時,InnoDB會先將數據更新,然後記錄redoLog 在內存中,然後找個時間將redoLog的操作執行到磁盤上的文件上。不管是否提交成功我都記錄,你要是回滾了,那我連回滾的修改也記錄。它確保了事務的持久性。
- MVCC多版本併發控制是MySQL中基於樂觀鎖理論實現隔離級別的方式,用於讀已提交和可重複讀取隔離級別的實現。在MySQL中,會在表中每一條數據後面添加兩個字段:最近修改該行數據的事務ID,指向該行(undolog表中)回滾段的指針。Read View判斷行的可見性,創建一個新事務時,copy一份當前系統中的活躍事務列表。意思是,當前不應該被本事務看到的其他事務id列表。
- binlog由Mysql的Server層實現,是邏輯日誌,記錄的是sql語句的原始邏輯,比如"把id='B' 修改為id = ‘B2’。binlog會寫入指定大小的物理文件中,是追加寫入的,當前文件寫滿則會創建新的文件寫入。產生:事務提交的時候,一次性將事務中的sql語句,按照一定的格式記錄到binlog中。用於複製和恢復在主從複製中,從庫利用主庫上的binlog進行重播(執行日誌中記錄的修改邏輯),實現主從同步。業務數據不一致或者錯了,用binlog恢復。
binlog和redolog的區別
- redolog是在InnoDB存儲引擎層產生,而binlog是MySQL數據庫的上層服務層產生的。
- 兩種日誌記錄的內容形式不同。MySQL的binlog是邏輯日誌,其記錄是對應的SQL語句。而innodb存儲引擎層面的重做日誌是物理日誌。
- 兩種日誌與記錄寫入磁盤的時間點不同,binlog日誌只在事務提交完成後進行一次寫入。而innodb存儲引擎的重做日誌在事務進行中不斷地被寫入,並日志不是隨事務提交的順序進行寫入的。
- binlog不是循環使用,在寫滿或者重啟之後,會生成新的binlog文件,redolog是循環使用。
- binlog可以作為恢復數據使用,主從複製搭建,redolog作為異常宕機或者介質故障後的數據恢復使用。
Mysql如何保證一致性和持久性
MySQL為了保證ACID中的一致性和持久性,使用了WAL(Write-Ahead Logging,先寫日誌再寫磁盤)。Redo log就是一種WAL的應用。當數據庫忽然掉電,再重新啟動時,MySQL可以通過Redo log還原數據。也就是說,每次事務提交時,不用同步刷新磁盤數據文件,只需要同步刷新Redo log就足夠了。
InnoDB的行鎖模式
- 共享鎖(S):用法lock in share mode,又稱讀鎖,允許一個事務去讀一行,阻止其他事務獲得相同數據集的排他鎖。若事務T對數據對象A加上S鎖,則事務T可以讀A但不能修改A,其他事務只能再對A加S鎖,而不能加X鎖,直到T釋放A上的S鎖。這保證了其他事務可以讀A,但在T釋放A上的S鎖之前不能對A做任何修改。
- 排他鎖(X):用法for update,又稱寫鎖,允許獲取排他鎖的事務更新數據,阻止其他事務取得相同的數據集共享讀鎖和排他寫鎖。若事務T對數據對象A加上X鎖,事務T可以讀A也可以修改A,其他事務不能再對A加任何鎖,直到T釋放A上的鎖。在沒有索引的情況下,InnoDB只能使用表鎖。
為什麼選擇B+樹作為索引結構
- Hash索引:Hash索引底層是哈希表,哈希表是一種以key-value存儲數據的結構,所以多個數據在存儲關係上是完全沒有任何順序關係的,所以,對於區間查詢是無法直接通過索引查詢的,就需要全表掃描。所以,哈希索引只適用於等值查詢的場景。而B+ 樹是一種多路平衡查詢樹,所以他的節點是天然有序的(左子節點小於父節點、父節點小於右子節點),所以對於範圍查詢的時候不需要做全表掃描
- 二叉查找樹:解決了排序的基本問題,但是由於無法保證平衡,可能退化為鏈表。
- 平衡二叉樹:通過旋轉解決了平衡的問題,但是旋轉操作效率太低。
- 紅黑樹:通過捨棄嚴格的平衡和引入紅黑節點,解決了 AVL旋轉效率過低的問題,但是在磁盤等場景下,樹仍然太高,IO次數太多。
- B+樹:在B樹的基礎上,將非葉節點改造為不存儲數據純索引節點,進一步降低了樹的高度;此外將葉節點使用指針連接成鏈表,範圍查詢更加高效。
B+樹的葉子節點都可以存哪些東西
可能存儲的是整行數據,也有可能是主鍵的值。B+樹的葉子節點存儲了整行數據的是主鍵索引,也被稱之為聚簇索引。而索引B+ Tree的葉子節點存儲了主鍵的值的是非主鍵索引,也被稱之為非聚簇索引
覆蓋索引
指一個查詢語句的執行只用從索引中就能夠取得,不必從數據表中讀取。也可以稱之為實現了索引覆蓋。
查詢在什麼時候不走(預期中的)索引
- 模糊查詢 %like
- 索引列參與計算,使用了函數
- 非最左前綴順序
- where對null判斷
- where不等於
- or操作有至少一個字段沒有索引
- 需要回表的查詢結果集過大(超過配置的範圍)
數據庫優化指南
- 創建並使用正確的索引
- 只返回需要的字段
- 減少交互次數(批量提交)
- 設置合理的Fetch Size(數據每次返回給客戶端的條數)
JVM
運行時數據區域
- 程序計數器:程序計數器是一塊較小的內存空間,它可以看作是當前線程所執行的字節碼的行號指示器。在虛擬機的概念模型裡,字節碼解釋器工作時就是通過改變這個計數器的值來選取下一條需要執行的字節碼指令,分支、循環、跳轉、異常處理、線程恢復等基礎功能都需要依賴這個計數器來完成。是線程私有”的內存。
- Java虛擬機棧:與程序計數器一樣,Java虛擬機棧(Java Virtual Machine Stacks)也是線程私有的,它的生命週期與線程相同。虛擬機棧描述的是Java方法執行的內存模型:每個方法在執行的同時都會創建一個棧幀 ,用於存儲局部變量表、操作數棧、動態鏈接、方法出口等信息。每一個方法從調用直至執行完成的過程,就對應著一個棧幀在虛擬機棧中入棧到出棧的過程。
- 本地方法棧:本地方法棧(Native Method Stack)與虛擬機棧所發揮的作用是非常相似的,它們之間的區別不過是虛擬機棧為虛擬機執行Java方法(也就是字節碼)服務,而本地方法棧則為虛擬機使用到的Native方法服務。
- Java堆:對於大多數應用來說,Java堆是Java虛擬機所管理的內存中最大的一塊。Java堆是被所有線程共享的一塊內存區域,在虛擬機啟動時創建。此內存區域的唯一目的就是存放對象實例,幾乎所有的對象實例都在這裡分配內存。
分代回收
HotSpot JVM把年輕代分為了三部分:1個Eden區和2個Survivor區(分別叫from和to)。一般情況下,新創建的對象都會被分配到Eden區(一些大對象特殊處理),這些對象經過第一次Minor GC後,如果仍然存活,將會被移到Survivor區。對象在Survivor區中每熬過一次Minor GC,年齡就會增加1歲,當它的年齡增加到一定程度時,就會被移動到年老代中。
因為年輕代中的對象基本都是朝生夕死的,所以在年輕代的垃圾回收算法使用的是複製算法,複製算法的基本思想就是將內存分為兩塊,每次只用其中一塊,當這一塊內存用完,就將還活著的對象複製到另外一塊上面。複製算法不會產生內存碎片。
在GC開始的時候,對象只會存在於Eden區和名為“From”的Survivor區,Survivor區“To”是空的。緊接著進行GC,Eden區中所有存活的對象都會被複制到“To”,而在“From”區中,仍存活的對象會根據他們的年齡值來決定去向。年齡達到一定值(年齡閾值,可以通過-XX:MaxTenuringThreshold來設置)的對象會被移動到年老代中,沒有達到閾值的對象會被複制到“To”區域。經過這次GC後,Eden區和From區已經被清空。這個時候,“From”和“To”會交換他們的角色,也就是新的“To”就是上次GC前的“From”,新的“From”就是上次GC前的“To”。不管怎樣,都會保證名為To的Survivor區域是空的。Minor GC會一直重複這樣的過程,直到“To”區被填滿,“To”區被填滿之後,會將所有對象移動到年老代中。
常見的垃圾回收機制
- 引用計數法:引用計數法是一種簡單但速度很慢的垃圾回收技術。每個對象都含有一個引用計數器,當有引用連接至對象時,引用計數加1。當引用離開作用域或被置為null時,引用計數減1。雖然管理引用計數的開銷不大,但這項開銷在整個程序生命週期中將持續發生。垃圾回收器會在含有全部對象的列表上遍歷,當發現某個對象引用計數為0時,就釋放其佔用的空間。
- 可達性分析算法:這個算法的基本思路就是通過一系列的稱為“GC Roots”的對象作為起始點,從這些節點開始向下搜索,搜索所走過的路徑稱為引用鏈,當一個對象到GC Roots沒有任何引用鏈相連(用圖論的話來說,就是從GC Roots到這個對象不可達)時,則證明此對象是不可用的。
G1和CMS的比較
- CMS收集器是獲取最短回收停頓時間為目標的收集器,因為CMS工作時,GC工作線程與用戶線程可以併發執行,以此來達到降低手機停頓時間的目的(只有初始標記和重新標記會STW)。但是CMS收集器對CPU資源非常敏感。在併發階段,雖然不會導致用戶線程停頓,但是會佔用CPU資源而導致引用程序變慢,總吞吐量下降。
- CMS僅作用於老年代,是基於標記清除算法,所以清理的過程中會有大量的空間碎片。
- CMS收集器無法處理浮動垃圾,由於CMS併發清理階段用戶線程還在運行,伴隨程序的運行自熱會有新的垃圾不斷產生,這一部分垃圾出現在標記過程之後,CMS無法在本次收集中處理它們,只好留待下一次GC時將其清理掉。
- G1是一款面向服務端應用的垃圾收集器,適用於多核處理器、大內存容量的服務端系統。G1能充分利用CPU、多核環境下的硬件優勢,使用多個CPU(CPU或者CPU核心)來縮短STW的停頓時間,它滿足短時間停頓的同時達到一個高的吞吐量。
- 從JDK 9開始,G1成為默認的垃圾回收器。當應用有以下任何一種特性時非常適合用G1:Full GC持續時間太長或者太頻繁;對象的創建速率和存活率變動很大;應用不希望停頓時間長(長於0.5s甚至1s)。
- G1將空間劃分成很多塊(Region),然後他們各自進行回收。堆比較大的時候可以採用,採用複製算法,碎片化問題不嚴重。整體上看屬於標記整理算法,局部(region之間)屬於複製算法。
- G1 需要記憶集 (具體來說是卡表)來記錄新生代和老年代之間的引用關係,這種數據結構在 G1 中需要佔用大量的內存,可能達到整個堆內存容量的 20% 甚至更多。而且 G1 中維護記憶集的成本較高,帶來了更高的執行負載,影響效率。所以 CMS 在小內存應用上的表現要優於 G1,而大內存應用上 G1 更有優勢,大小內存的界限是6GB到8GB。
哪些對象可以作為GC Roots
- 虛擬機棧(棧幀中的本地變量表)中引用的對象。
- 方法區中類靜態屬性引用的對象。
- 方法區中常量引用的對象。
- 本地方法棧中JNI(即一般說的Native方法)引用的對象。
GC中Stop the world(STW)
在執行垃圾收集算法時,Java應用程序的其他所有除了垃圾收集收集器線程之外的線程都被掛起。此時,系統只能允許GC線程進行運行,其他線程則會全部暫停,等待GC線程執行完畢後才能再次運行。這些工作都是由虛擬機在後臺自動發起和自動完成的,是在用戶不可見的情況下把用戶正常工作的線程全部停下來,這對於很多的應用程序,尤其是那些對於實時性要求很高的程序來說是難以接受的。
但不是說GC必須STW,你也可以選擇降低運行速度但是可以併發執行的收集算法,這取決於你的業務。
垃圾回收算法
- 停止-複製:先暫停程序的運行,然後將所有存活的對象從當前堆複製到另一個堆,沒有被複制的對象全部都是垃圾。當對象被複制到新堆時,它們是一個挨著一個的,所以新堆保持緊湊排列,然後就可以按前述方法簡單,直接的分配了。缺點是一浪費空間,兩個堆之間要來回倒騰,二是當程序進入穩定態時,可能只會產生極少的垃圾,甚至不產生垃圾,儘管如此,複製式回收器仍會將所有內存自一處複製到另一處。
- 標記-清除:同樣是從堆棧和靜態存儲區出發,遍歷所有的引用,進而找出所有存活的對象。每當它找到一個存活的對象,就會給對象一個標記,這個過程中不會回收任何對象。只有全部標記工作完成的時候,清理動作才會開始。在清理過程中,沒有標記的對象會被釋放,不會發生任何複製動作。所以剩下的堆空間是不連續的,垃圾回收器如果要希望得到連續空間的話,就得重新整理剩下的對象。
- 標記-整理:它的第一個階段與標記/清除算法是一模一樣的,均是遍歷GC Roots,然後將存活的對象標記。移動所有存活的對象,且按照內存地址次序依次排列,然後將末端內存地址以後的內存全部回收。因此,第二階段才稱為整理階段。
- 分代收集算法:把Java堆分為新生代和老年代,然後根據各個年代的特點採用最合適的收集算法。新生代中,對象的存活率比較低,所以選用複製算法,老年代中對象存活率高且沒有額外空間對它進行分配擔保,所以使用“標記-清除”或“標記-整理”算法進行回收。
Minor GC和Full GC觸發條件
- Minor GC觸發條件:當Eden區滿時,觸發Minor GC。
- Full GC觸發條件:
- 調用System.gc時,系統建議執行Full GC,但是不必然執行
- 老年代空間不足
- 方法區空間不足
- 通過Minor GC後進入老年代的平均大小大於老年代的可用內存
- 由Eden區、From Space區向To Space區複製時,對象大小大於To Space可用內存,則把該對象轉存到老年代,且老年代的可用內存小於該對象大小
JVM類加載過程
類從被加載到虛擬機內存中開始,到卸載出內存為止,它的整個生命週期包括:加載、驗證、準備、解析、初始化、使用和卸載7個階段。
- 加載:通過一個類的全限定名來獲取定義此類的二進制字節流,將這個字節流所代表的靜態存儲結構轉化為方法區的運行時數據結構,在內存中生成一個代表這個類的Class對象,作為方法去這個類的各種數據的訪問入口
- 驗證:驗證是連接階段的第一步,這一階段的目的是確保Class文件的字節流中包含的信息符合當前虛擬機的要求,並且不會危害虛擬自身的安全。
- 準備:準備階段是正式為類變量分配內存並設置類變量初始值的階段,這些變量所使用的內存都將在方法去中進行分配。這時候進行內存分配的僅包括類變量(static),而不包括實例變量,實例變量將會在對象實例化時隨著對象一起分配在Java堆中。
- 解析:解析階段是虛擬機將常量池內的符號(Class文件內的符號)引用替換為直接引用(指針)的過程。
- 初始化:初始化階段是類加載過程的最後一步,開始執行類中定義的Java程序代碼(字節碼)。
雙親委派模型
雙親委派的意思是如果一個類加載器需要加載類,那麼首先它會把這個類請求委派給父類加載器去完成,每一層都是如此。一直遞歸到頂層,當父加載器無法完成這個請求時,子類才會嘗試去加載。
JVM鎖優化和膨脹過程
- 自旋鎖:自旋鎖其實就是在拿鎖時發現已經有線程拿了鎖,自己如果去拿會阻塞自己,這個時候會選擇進行一次忙循環嘗試。也就是不停循環看是否能等到上個線程自己釋放鎖。自適應自旋鎖指的是例如第一次設置最多自旋10次,結果在自旋的過程中成功獲得了鎖,那麼下一次就可以設置成最多自旋20次。
- 鎖粗化:虛擬機通過適當擴大加鎖的範圍以避免頻繁的拿鎖釋放鎖的過程。
- 鎖消除:通過逃逸分析發現其實根本就沒有別的線程產生競爭的可能(別的線程沒有臨界量的引用),或者同步塊內進行的是原子操作,而“自作多情”地給自己加上了鎖。有可能虛擬機會直接去掉這個鎖。
- 偏向鎖:在大多數的情況下,鎖不僅不存在多線程的競爭,而且總是由同一個線程獲得。因此為了讓線程獲得鎖的代價更低引入了偏向鎖的概念。偏向鎖的意思是如果一個線程獲得了一個偏向鎖,如果在接下來的一段時間中沒有其他線程來競爭鎖,那麼持有偏向鎖的線程再次進入或者退出同一個同步代碼塊,不需要再次進行搶佔鎖和釋放鎖的操作。
- 輕量級鎖:當存在超過一個線程在競爭同一個同步代碼塊時,會發生偏向鎖的撤銷。當前線程會嘗試使用CAS來獲取鎖,當自旋超過指定次數(可以自定義)時仍然無法獲得鎖,此時鎖會膨脹升級為重量級鎖。
- 重量級鎖:重量級鎖依賴對象內部的monitor鎖來實現,而monitor又依賴操作系統的MutexLock(互斥鎖)。當系統檢查到是重量級鎖之後,會把等待想要獲取鎖的線程阻塞,被阻塞的線程不會消耗CPU,但是阻塞或者喚醒一個線程,都需要通過操作系統來實現。
什麼情況下需要開始類加載過程的第一個階段加載
- 遇到new、getstatic、putstatic或invokestatic這4條字節碼指令時,如果類沒有進行過初始化,則需要先觸發其初始化。生成這4條指令的最常見的Java代碼場景是:使用new關鍵字實例化對象的時候、讀取或設置一個類的靜態字段(被final修飾、已在編譯期把結果放入常量池的靜態字段除外)的時候,以及調用一個類的靜態方法的時候。
- 使用java.lang.reflect包的方法對類進行反射調用的時候,如果類沒有進行過初始化,則需要先觸發其初始化。
- 當初始化一個類的時候,如果發現其父類還沒有進行過初始化,則需要先觸發其父類的初始化。
- 當虛擬機啟動時,用戶需要指定一個要執行的主類(包含main()方法的那個類),虛擬機會先初始化這個主類。
i++操作的字節碼指令
- 將int類型常量加載到操作數棧頂
- 將int類型數值從操作數棧頂取出,並存儲到到局部變量表的第1個Slot中
- 將int類型變量從局部變量表的第1個Slot中取出,並放到操作數棧頂
- 將局部變量表的第1個Slot中的int類型變量加1
- 表示將int類型數值從操作數棧頂取出,並存儲到到局部變量表的第1個Slot中,即i中
Java基礎
HashMap和ConcurrentHashMap
由於HashMap是線程不同步的,雖然處理數據的效率高,但是在多線程的情況下存在著安全問題,因此設計了CurrentHashMap來解決多線程安全問題。
HashMap在put的時候,插入的元素超過了容量(由負載因子決定)的範圍就會觸發擴容操作,就是rehash,這個會重新將原數組的內容重新hash到新的擴容數組中,在多線程的環境下,存在同時其他的元素也在進行put操作,如果hash值相同,可能出現同時在同一數組下用鏈表表示,造成閉環,導致在get時會出現死循環,所以HashMap是線程不安全的。
HashMap的環:若當前線程此時獲得ertry節點,但是被線程中斷無法繼續執行,此時線程二進入transfer函數,並把函數順利執行,此時新表中的某個位置有了節點,之後線程一獲得執行權繼續執行,因為併發transfer,所以兩者都是擴容的同一個鏈表,當線程一執行到e.next = new table[i] 的時候,由於線程二之前數據遷移的原因導致此時new table[i] 上就有ertry存在,所以線程一執行的時候,會將next節點,設置為自己,導致自己互相使用next引用對方,因此產生鏈表,導致死循環。
在JDK1.7版本中,ConcurrentHashMap維護了一個Segment數組,Segment這個類繼承了重入鎖ReentrantLock,並且該類裡面維護了一個 HashEntry[] table數組,在寫操作put,remove,擴容的時候,會對Segment加鎖,所以僅僅影響這個Segment,不同的Segment還是可以併發的,所以解決了線程的安全問題,同時又採用了分段鎖也提升了併發的效率。在JDK1.8版本中,ConcurrentHashMap摒棄了Segment的概念,而是直接用Node數組+鏈表+紅黑樹的數據結構來實現,併發控制使用Synchronized和CAS來操作,整個看起來就像是優化過且線程安全的HashMap。
HashMap如果我想要讓自己的Object作為K應該怎麼辦
- 重寫hashCode()是因為需要計算存儲數據的存儲位置,需要注意不要試圖從散列碼計算中排除掉一個對象的關鍵部分來提高性能,這樣雖然能更快但可能會導致更多的Hash碰撞;
- 重寫equals()方法,需要遵守自反性、對稱性、傳遞性、一致性以及對於任何非null的引用值x,x.equals(null)必須返回false的這幾個特性,目的是為了保證key在哈希表中的唯一性;
volatile
volatile在多處理器開發中保證了共享變量的“ 可見性”。可見性的意思是當一個線程修改一個共享變量時,另外一個線程能讀到這個修改的值。(共享內存,私有內存)
Atomic類的CAS操作
CAS是英文單詞CompareAndSwap的縮寫,中文意思是:比較並替換。CAS需要有3個操作數:內存地址V,舊的預期值A,即將要更新的目標值B。CAS指令執行時,當且僅當內存地址V的值與預期值A相等時,將內存地址V的值修改為B,否則就什麼都不做。整個比較並替換的操作是一個原子操作。如 Intel 處理器,比較並交換通過指令的 cmpxchg 系列實現。
CAS操作ABA問題:
如果在這段期間它的值曾經被改成了B,後來又被改回為A,那CAS操作就會誤認為它從來沒有被改變過。Java併發包為了解決這個問題,提供了一個帶有標記的原子引用類“AtomicStampedReference”,它可以通過控制變量值的版本來保證CAS的正確性。
Synchronized和Lock的區別
- 首先synchronized是java內置關鍵字在jvm層面,Lock是個java類。
- synchronized無法判斷是否獲取鎖的狀態,Lock可以判斷是否獲取到鎖,並且可以主動嘗試去獲取鎖。
- synchronized會自動釋放鎖(a 線程執行完同步代碼會釋放鎖 ;b 線程執行過程中發生異常會釋放鎖),Lock需在finally中手工釋放鎖(unlock()方法釋放鎖),否則容易造成線程死鎖。
- 用synchronized關鍵字的兩個線程1和線程2,如果當前線程1獲得鎖,線程2線程等待。如果線程1阻塞,線程2則會一直等待下去,而Lock鎖就不一定會等待下去,如果嘗試獲取不到鎖,線程可以不用一直等待就結束了。
- synchronized的鎖可重入、不可中斷、非公平,而Lock鎖可重入、可判斷、可公平(兩者皆可)
- Lock鎖適合大量同步的代碼的同步問題,synchronized鎖適合代碼少量的同步問題。
AQS理論的數據結構
AQS內部有3個對象,一個是state(用於計數器,類似gc的回收計數器),一個是線程標記(當前線程是誰加鎖的),一個是阻塞隊列。
如何指定多個線程的執行順序
- 設定一個 orderNum,每個線程執行結束之後,更新 orderNum,指明下一個要執行的線程。並且喚醒所有的等待線程。
- 在每一個線程的開始,要 while 判斷 orderNum 是否等於自己的要求值,不是,則 wait,是則執行本線程。
為什麼要使用線程池
- 減少創建和銷燬線程的次數,每個工作線程都可以被重複利用,可執行多個任務。
- 可以根據系統的承受能力,調整線程池中工作線程的數目,放置因為消耗過多的內存,而把服務器累趴下
核心線程池ThreadPoolExecutor內部參數
- corePoolSize:指定了線程池中的線程數量
- maximumPoolSize:指定了線程池中的最大線程數量
- keepAliveTime:線程池維護線程所允許的空閒時間
- unit: keepAliveTime 的單位。
- workQueue:任務隊列,被提交但尚未被執行的任務。
- threadFactory:線程工廠,用於創建線程,一般用默認的即可。
- handler:拒絕策略。當任務太多來不及處理,如何拒絕任務。
線程池的拒絕策略
- ThreadPoolExecutor.AbortPolicy:丟棄任務並拋出RejectedExecutionException異常。
- ThreadPoolExecutor.DiscardPolicy:丟棄任務,但是不拋出異常。
- ThreadPoolExecutor.DiscardOldestPolicy:丟棄隊列最前面的任務,然後重新提交被拒絕的任務
- ThreadPoolExecutor.CallerRunsPolicy:由調用線程(提交任務的線程)處理該任務
線程池的線程數量怎麼確定
- 一般來說,如果是CPU密集型應用,則線程池大小設置為N+1。
- 一般來說,如果是IO密集型應用,則線程池大小設置為2N+1。
- 在IO優化中,線程等待時間所佔比例越高,需要越多線程,線程CPU時間所佔比例越高,需要越少線程。這樣的估算公式可能更適合:最佳線程數目 = ((線程等待時間+線程CPU時間)/線程CPU時間 )* CPU數目
ThreadLocal的原理和實現
ThreadLoal 變量,線程局部變量,同一個 ThreadLocal 所包含的對象,在不同的 Thread 中有不同的副本。ThreadLocal 變量通常被private static修飾。當一個線程結束時,它所使用的所有 ThreadLocal 相對的實例副本都可被回收。
一個線程內可以存在多個 ThreadLocal 對象,所以其實是 ThreadLocal 內部維護了一個 Map ,這個 Map 不是直接使用的 HashMap ,而是 ThreadLocal 實現的一個叫做 ThreadLocalMap 的靜態內部類。而我們使用的 get()、set() 方法其實都是調用了這個ThreadLocalMap類對應的 get()、set() 方法。
HashSet和HashMap
HashSet的value存的是一個static finial PRESENT = newObject()。而HashSet的remove是使用HashMap實現,則是map.remove而map的移除會返回value,如果底層value都是存null,顯然將無法分辨是否移除成功。
Boolean佔幾個字節
未精確定義字節。Java語言表達式所操作的boolean值,在編譯之後都使用Java虛擬機中的int數據類型來代替,而boolean數組將會被編碼成Java虛擬機的byte數組,每個元素boolean元素佔8位。
Spring
什麼是三級緩存
- 第一級緩存:單例緩存池singletonObjects。
- 第二級緩存:早期提前暴露的對象緩存earlySingletonObjects。(屬性還沒有值對象也沒有被初始化)
- 第三級緩存:singletonFactories單例對象工廠緩存。
創建Bean的整個過程
- getBean方法肯定不陌生,必經之路,然後調用doGetBean,進來以後首先會執行transformedBeanName找別名,看你的Bean上面是否起了別名。然後進行很重要的一步,getSingleton,這段代碼就是從你的單例緩存池中獲取Bean的實例。那麼你第一次進來肯定是沒有的,緩存裡肯定是拿不到的。也就是一級緩存裡是沒有的。那麼它怎麼辦呢?他會嘗試去二級緩存中去拿,但是去二級緩存中拿並不是無條件的,首先要判斷isSingletonCurrentlyInCreation(beanName)他要看你這個對象是否正在創建當中,如果不是直接就退出該方法,如果是的話,他就會去二級緩存earlySingletonObjects裡面取,如果沒拿到,它還接著判斷allowEarlyReference這個東西是否為true。它的意思是說,是否允許讓你從單例工廠對象緩存中去拿對象。默認為true。好了,此時如果進來那麼就會通過singletonFactory.getObject()去單例工廠緩存中去拿。然後將緩存級別提升至二級緩存也就早期暴露的緩存。
- getSingleton執行完以後會走dependsOn方法,判斷是否有dependsOn標記的循環引用,有的話直接卡死,拋出異常。比如說A依賴於B,B依賴於A 通過dependsOn註解去指定。此時執行到這裡就會拋出異常。這裡所指並非是構造函數的循環依賴。
- beforeSingletonCreation在這裡方法裡。就把你的對象標記為了早期暴露的對象。提前暴露對象用於創建Bean的實例。
- 緊接著就走創建Bean的流程開始。在創建Bean之前執行了一下resolveBeforeInstantiation。它的意思是說,代理AOPBean定義註冊信息但是這裡並不是實際去代理你的對象,因為對象還沒有被創建。只是代理了Bean定義信息,還沒有被實例化。把Bean定義信息放進緩存,以便我想代理真正的目標對象的時候,直接去緩存裡去拿。
- 接下來就真正的走創建Bean流程,首先走進真正做事兒的方法doCreateBean然後找到createBeanInstance這個方法,在這裡面它將為你創建你的Bean實例信息(Bean的實例)。如果說創建成功了,那麼就把你的對象放入緩存中去(將創建好的提前曝光的對象放入singletonFactories三級緩存中)將對象從二級緩存中移除因為它已經不是提前暴露的對象了。但是。如果說在createBeanInstance這個方法中在創建Bean的時候它會去檢測你的依賴關係,會去檢測你的構造器。然後,如果說它在創建A對象的時候,發現了構造器裡依賴了B,然後它又會重新走getBean的這個流程,當在走到這裡的時候,又發現依賴了A此時就會拋出異常。為什麼會拋出異常,因為,走getBean的時候他會去從你的單例緩存池中去拿,因為你這裡的Bean還沒有被創建好。自然不會被放進緩存中,所以它是在緩存中拿不到B對象的。反過來也是拿不到A對象的。造成了死循環故此直接拋異常。這就是為什麼Spring IOC不能解決構造器循環依賴的原因。因為你還沒來的急放入緩存你的對象是不存在的。所以不能創建。同理@Bean標註的循環依賴方法也是不能解決的,跟這個同理。那麼多例就更不能解決了。為什麼?因為在走createBeanInstance的時候,會判斷是否是單例的Bean定義信息mbd.isSingleton();如果是才會進來。所以多例的Bean壓根就不會走進來,而是走了另一段邏輯,這裡不做介紹。至此,構造器循環依賴和@Bean的循環依賴還有多例Bean的循環依賴為什麼不能解決已經解釋清楚。然後如果說,Bean創建成功了。那麼會走後面的邏輯。
- 將創建好的Bean放入緩存,addSingletonFactory方法就是將你創建好的Bean放入三級緩存中。並且移除早期暴露的對象。
- 通過populateBean給屬性賦值,我們知道,創建好的對象,並不是一個完整的對象,裡面的屬性還沒有被賦值。所以這個方法就是為創建好的Bean為它的屬性賦值。並且調用了我們實現的的XXXAware接口進行回調初始化,。然後調用我們實現的Bean的後置處理器,給我們最後一次機會去修改Bean的屬性。
Spring如何解決循環依賴問題
Spring使用了三級緩存解決了循環依賴的問題。在populateBean()給屬性賦值階段裡面Spring會解析你的屬性,並且賦值,當發現,A對象裡面依賴了B,此時又會走getBean方法,但這個時候,你去緩存中是可以拿的到的。因為我們在對createBeanInstance對象創建完成以後已經放入了緩存當中,所以創建B的時候發現依賴A,直接就從緩存中去拿,此時B創建完,A也創建完,一共執行了4次。至此Bean的創建完成,最後將創建好的Bean放入單例緩存池中。
BeanFactory和ApplicationContext的區別
- BeanFactory是Spring裡面最低層的接口,提供了最簡單的容器的功能,只提供了實例化對象和拿對象的功能。
- ApplicationContext應用上下文,繼承BeanFactory接口,它是Spring的一各更高級的容器,提供了更多的有用的功能。如國際化,訪問資源,載入多個(有繼承關係)上下文 ,使得每一個上下文都專注於一個特定的層次,消息發送、響應機制,AOP等。
- BeanFactory在啟動的時候不會去實例化Bean,中有從容器中拿Bean的時候才會去實例化。ApplicationContext在啟動的時候就把所有的Bean全部實例化了。它還可以為Bean配置lazy-init=true來讓Bean延遲實例化
動態代理的實現方式,AOP的實現方式
- JDK動態代理:利用反射機制生成一個實現代理接口的匿名類,在調用具體方法前調用InvokeHandler來處理。
- CGlib動態代理:利用ASM(開源的Java字節碼編輯庫,操作字節碼)開源包,將代理對象類的class文件加載進來,通過修改其字節碼生成子類來處理。
- 區別:JDK代理只能對實現接口的類生成代理;CGlib是針對類實現代理,對指定的類生成一個子類,並覆蓋其中的方法,這種通過繼承類的實現方式,不能代理final修飾的類。
Spring的的事務傳播機制
- REQUIRED(默認):支持使用當前事務,如果當前事務不存在,創建一個新事務。
- SUPPORTS:支持使用當前事務,如果當前事務不存在,則不使用事務。
- MANDATORY:強制,支持使用當前事務,如果當前事務不存在,則拋出Exception。
- REQUIRES_NEW:創建一個新事務,如果當前事務存在,把當前事務掛起。
- NOT_SUPPORTED:無事務執行,如果當前事務存在,把當前事務掛起。
- NEVER:無事務執行,如果當前有事務則拋出Exception。
- NESTED:嵌套事務,如果當前事務存在,那麼在嵌套的事務中執行。如果當前事務不存在,則表現跟REQUIRED一樣。
Spring的後置處理器
- BeanPostProcessor:Bean的後置處理器,主要在bean初始化前後工作。
- InstantiationAwareBeanPostProcessor:繼承於BeanPostProcessor,主要在實例化bean前後工作;AOP創建代理對象就是通過該接口實現。
- BeanFactoryPostProcessor:Bean工廠的後置處理器,在bean定義(bean definitions)加載完成後,bean尚未初始化前執行。
- BeanDefinitionRegistryPostProcessor:繼承於BeanFactoryPostProcessor。其自定義的方法postProcessBeanDefinitionRegistry會在bean定義(bean definitions)將要加載,bean尚未初始化前真執行,即在BeanFactoryPostProcessor的postProcessBeanFactory方法前被調用。
消息隊列
為什麼需要消息隊列
解耦,異步處理,削峰/限流
Kafka的文件存儲機制
Kafka中消息是以topic進行分類的,生產者通過topic向Kafka broker發送消息,消費者通過topic讀取數據。然而topic在物理層面又能以partition為分組,一個topic可以分成若干個partition。partition還可以細分為segment,一個partition物理上由多個segment組成,segment文件由兩部分組成,分別為“.index”文件和“.log”文件,分別表示為segment索引文件和數據文件。這兩個文件的命令規則為:partition全局的第一個segment從0開始,後續每個segment文件名為上一個segment文件最後一條消息的offset值。
Kafka 如何保證可靠性
如果我們要往 Kafka 對應的主題發送消息,我們需要通過 Producer 完成。前面我們講過 Kafka 主題對應了多個分區,每個分區下面又對應了多個副本;為了讓用戶設置數據可靠性, Kafka 在 Producer 裡面提供了消息確認機制。也就是說我們可以通過配置來決定消息發送到對應分區的幾個副本才算消息發送成功。可以在定義 Producer 時通過 acks 參數指定。這個參數支持以下三種值:
- acks = 0:意味著如果生產者能夠通過網絡把消息發送出去,那麼就認為消息已成功寫入 Kafka 。在這種情況下還是有可能發生錯誤,比如發送的對象無能被序列化或者網卡發生故障,但如果是分區離線或整個集群長時間不可用,那就不會收到任何錯誤。在 acks=0 模式下的運行速度是非常快的(這就是為什麼很多基準測試都是基於這個模式),你可以得到驚人的吞吐量和帶寬利用率,不過如果選擇了這種模式, 一定會丟失一些消息。
- acks = 1:意味若 Leader 在收到消息並把它寫入到分區數據文件(不一定同步到磁盤上)時會返回確認或錯誤響應。在這個模式下,如果發生正常的 Leader 選舉,生產者會在選舉時收到一個 LeaderNotAvailableException 異常,如果生產者能恰當地處理這個錯誤,它會重試發送悄息,最終消息會安全到達新的 Leader 那裡。不過在這個模式下仍然有可能丟失數據,比如消息已經成功寫入 Leader,但在消息被複制到 follower 副本之前 Leader發生崩潰。
- acks = all(這個和 request.required.acks = -1 含義一樣):意味著 Leader 在返回確認或錯誤響應之前,會等待所有同步副本都收到悄息。如果和min.insync.replicas 參數結合起來,就可以決定在返回確認前至少有多少個副本能夠收到悄息,生產者會一直重試直到消息被成功提交。不過這也是最慢的做法,因為生產者在繼續發送其他消息之前需要等待所有副本都收到當前的消息。
Kafka消息是採用Pull模式,還是Push模式
Kafka最初考慮的問題是,customer應該從brokes拉取消息還是brokers將消息推送到consumer,也就是pull還push。在這方面,Kafka遵循了一種大部分消息系統共同的傳統的設計:producer將消息推送到broker,consumer從broker拉取消息。push模式下,當broker推送的速率遠大於consumer消費的速率時,consumer恐怕就要崩潰了。最終Kafka還是選取了傳統的pull模式。Pull模式的另外一個好處是consumer可以自主決定是否批量的從broker拉取數據。Pull有個缺點是,如果broker沒有可供消費的消息,將導致consumer不斷在循環中輪詢,直到新消息到t達。為了避免這點,Kafka有個參數可以讓consumer阻塞知道新消息到達。
Kafka是如何實現高吞吐率的
- 順序讀寫:kafka的消息是不斷追加到文件中的,這個特性使kafka可以充分利用磁盤的順序讀寫性能
- 零拷貝:跳過“用戶緩衝區”的拷貝,建立一個磁盤空間和內存的直接映射,數據不再複製到“用戶態緩衝區”
- 文件分段:kafka的隊列topic被分為了多個區partition,每個partition又分為多個段segment,所以一個隊列中的消息實際上是保存在N多個片段文件中
- 批量發送:Kafka允許進行批量發送消息,先將消息緩存在內存中,然後一次請求批量發送出去
- 數據壓縮:Kafka還支持對消息集合進行壓縮,Producer可以通過GZIP或Snappy格式對消息集合進行壓縮
Kafka判斷一個節點還活著的兩個條件
- 節點必須可以維護和 ZooKeeper 的連接,Zookeeper 通過心跳機制檢查每個節點的連接
- 如果節點是個 follower,他必須能及時的同步 leader 的寫操作,延時不能太久
Dubbo
Dubbo的容錯機制
- 失敗自動切換,當出現失敗,重試其它服務器。通常用於讀操作,但重試會帶來更長延遲。可通過 retries="2" 來設置重試次數
- 快速失敗,只發起一次調用,失敗立即報錯。通常用於非冪等性的寫操作,比如新增記錄。
- 失敗安全,出現異常時,直接忽略。通常用於寫入審計日誌等操作。
- 失敗自動恢復,後臺記錄失敗請求,定時重發。通常用於消息通知操作。
- 並行調用多個服務器,只要一個成功即返回。通常用於實時性要求較高的讀操作,但需要浪費更多服務資源。可通過 forks="2" 來設置最大並行數。
- 廣播調用所有提供者,逐個調用,任意一臺報錯則報錯。通常用於通知所有提供者更新緩存或日誌等本地資源信息
Dubbo註冊中心掛了還可以繼續通信麼
可以,因為剛開始初始化的時候,消費者會將提供者的地址等信息拉取到本地緩存,所以註冊中心掛了可以繼續通信。
Dubbo框架設計結構
- 服務接口層:該層是與實際業務邏輯相關的,根據服務提供方和服務消費方的業務設計對應的接口和實現。
- 配置層:對外配置接口,以ServiceConfig和ReferenceConfig為中心,可以直接new配置類,也可以通過spring解析配置生成配置類。
- 服務代理層:服務接口透明代理,生成服務的客戶端Stub和服務器端Skeleton,以ServiceProxy為中心,擴展接口為ProxyFactory。
- 服務註冊層:封裝服務地址的註冊與發現,以服務URL為中心,擴展接口為RegistryFactory、Registry和RegistryService。可能沒有服務註冊中心,此時服務提供方直接暴露服務。
- 集群層:封裝多個提供者的路由及負載均衡,並橋接註冊中心,以Invoker為中心,擴展接口為Cluster、Directory、Router和LoadBalance。將多個服務提供方組合為一個服務提供方,實現對服務消費方來透明,只需要與一個服務提供方進行交互。
- 監控層:RPC調用次數和調用時間監控,以Statistics為中心,擴展接口為MonitorFactory、Monitor和MonitorService。
- 遠程調用層:封將RPC調用,以Invocation和Result為中心,擴展接口為Protocol、Invoker和Exporter。Protocol是服務域,它是Invoker暴露和引用的主功能入口,它負責Invoker的生命週期管理。Invoker是實體域,它是Dubbo的核心模型,其它模型都向它靠擾,或轉換成它,它代表一個可執行體,可向它發起invoke調用,它有可能是一個本地的實現,也可能是一個遠程的實現,也可能一個集群實現。
- 信息交換層:封裝請求響應模式,同步轉異步,以Request和Response為中心,擴展接口為Exchanger、ExchangeChannel、ExchangeClient和ExchangeServer。
- 網絡傳輸層:抽象mina和netty為統一接口,以Message為中心,擴展接口為Channel、Transporter、Client、Server和Codec。
- 數據序列化層:可複用的一些工具,擴展接口為Serialization、 ObjectInput、ObjectOutput和ThreadPool。
操作系統
進程和線程
- 進程是操作系統資源分配的最小單位,線程是CPU任務調度的最小單位。一個進程可以包含多個線程,所以進程和線程都是一個時間段的描述,是CPU工作時間段的描述,不過是顆粒大小不同。
- 不同進程間數據很難共享,同一進程下不同線程間數據很易共享。
- 每個進程都有獨立的代碼和數據空間,進程要比線程消耗更多的計算機資源。線程可以看做輕量級的進程,同一類線程共享代碼和數據空間,每個線程都有自己獨立的運行棧和程序計數器,線程之間切換的開銷小。
- 進程間不會相互影響,一個線程掛掉將導致整個進程掛掉。
- 系統在運行的時候會為每個進程分配不同的內存空間;而對線程而言,除了CPU外,系統不會為線程分配內存(線程所使用的資源來自其所屬進程的資源),線程組之間只能共享資源。
進程的組成部分
進程由進程控制塊(PCB)、程序段、數據段三部分組成。
進程的通信方式
- 無名管道:半雙工的,即數據只能在一個方向上流動,只能用於具有親緣關係的進程之間的通信,可以看成是一種特殊的文件,對於它的讀寫也可以使用普通的read、write 等函數。但是它不是普通的文件,並不屬於其他任何文件系統,並且只存在於內存中。
- FIFO命名管道:FIFO是一種文件類型,可以在無關的進程之間交換數據,與無名管道不同,FIFO有路徑名與之相關聯,它以一種特殊設備文件形式存在於文件系統中。
- 消息隊列:消息隊列,是消息的鏈接表,存放在內核中。一個消息隊列由一個標識符(即隊列ID)來標識。
- 信號量:信號量是一個計數器,信號量用於實現進程間的互斥與同步,而不是用於存儲進程間通信數據。
- 共享內存:共享內存指兩個或多個進程共享一個給定的存儲區,一般配合信號量使用。
進程間五種通信方式的比較
- 管道:速度慢,容量有限,只有父子進程能通訊。
- FIFO:任何進程間都能通訊,但速度慢。
- 消息隊列:容量受到系統限制,且要注意第一次讀的時候,要考慮上一次沒有讀完數據的問題。
- 信號量:不能傳遞複雜消息,只能用來同步。
- 共享內存區:能夠很容易控制容量,速度快,但要保持同步,比如一個進程在寫的時候,另一個進程要注意讀寫的問題,相當於線程中的線程安全,當然,共享內存區同樣可以用作線程間通訊,不過沒這個必要,線程間本來就已經共享了同一進程內的一塊內存。
死鎖的4個必要條件
- 互斥條件:一個資源每次只能被一個線程使用;
- 請求與保持條件:一個線程因請求資源而阻塞時,對已獲得的資源保持不放;
- 不剝奪條件:進程已經獲得的資源,在未使用完之前,不能強行剝奪;
- 循環等待條件:若干線程之間形成一種頭尾相接的循環等待資源關係。
如何避免(預防)死鎖
- 破壞“請求和保持”條件:讓進程在申請資源時,一次性申請所有需要用到的資源,不要一次一次來申請,當申請的資源有一些沒空,那就讓線程等待。不過這個方法比較浪費資源,進程可能經常處於飢餓狀態。還有一種方法是,要求進程在申請資源前,要釋放自己擁有的資源。
- 破壞“不可搶佔”條件:允許進程進行搶佔,方法一:如果去搶資源,被拒絕,就釋放自己的資源。方法二:操作系統允許搶,只要你優先級大,可以搶到。
- 破壞“循環等待”條件:將系統中的所有資源統一編號,進程可在任何時刻提出資源申請,但所有申請必須按照資源的編號順序提出(指定獲取鎖的順序,順序加鎖)。
計算機網路
Get和Post區別
- Get是不安全的,因為在傳輸過程,數據被放在請求的URL中;Post的所有操作對用戶來說都是不可見的。
- Get傳送的數據量較小,這主要是因為受URL長度限制;Post傳送的數據量較大,一般被默認為不受限制。
- Get限制Form表單的數據集的值必須為ASCII字符;而Post支持整個ISO10646字符集。
- Get執行效率卻比Post方法好。Get是form提交的默認方法。
- GET產生一個TCP數據包;POST產生兩個TCP數據包。(非必然,客戶端可靈活決定)
Http請求的完全過程
- 瀏覽器根據域名解析IP地址(DNS),並查DNS緩存
- 瀏覽器與WEB服務器建立一個TCP連接
- 瀏覽器給WEB服務器發送一個HTTP請求(GET/POST):一個HTTP請求報文由請求行(request line)、請求頭部(headers)、空行(blank line)和請求數據(request body)4個部分組成。
- 服務端響應HTTP響應報文,報文由狀態行(status line)、相應頭部(headers)、空行(blank line)和響應數據(response body)4個部分組成。
- 瀏覽器解析渲染
tcp和udp區別
- TCP面向連接,UDP是無連接的,即發送數據之前不需要建立連接。
- TCP提供可靠的服務。也就是說,通過TCP連接傳送的數據,無差錯,不丟失,不重複,且按序到達;UDP盡最大努力交付,即不保證可靠交付。
- TCP面向字節流,實際上是TCP把數據看成一連串無結構的字節流,UDP是面向報文的,UDP沒有擁塞控制,因此網絡出現擁塞不會使源主機的發送速率降低(對實時應用很有用,如IP電話,實時視頻會議等)
- 每一條TCP連接只能是點到點的,UDP支持一對一,一對多,多對一和多對多的交互通信。
- TCP首部開銷20字節,UDP的首部開銷小,只有8個字節。
- TCP的邏輯通信信道是全雙工的可靠信道,UDP則是不可靠信道。
tcp和udp的優點
- TCP的優點:可靠,穩定 TCP的可靠體現在TCP在傳遞數據之前,會有三次握手來建立連接,而且在數據傳遞時,有確認、窗口、重傳、擁塞控制機制,在數據傳完後,還會斷開連接用來節約系統資源。TCP的缺點:慢,效率低,佔用系統資源高,易被攻擊 TCP在傳遞數據之前,要先建連接,這會消耗時間,而且在數據傳遞時,確認機制、重傳機制、擁塞控制機制等都會消耗大量的時間,而且要在每臺設備上維護所有的傳輸連接,事實上,每個連接都會佔用系統的CPU、內存等硬件資源。而且,因為TCP有確認機制、三次握手機制,這些也導致TCP容易被人利用,實現DOS、DDOS、CC等攻擊。
- UDP的優點:快,比TCP稍安全 UDP沒有TCP的握手、確認、窗口、重傳、擁塞控制等機制,UDP是一個無狀態的傳輸協議,所以它在傳遞數據時非常快。沒有TCP的這些機制,UDP較TCP被攻擊者利用的漏洞就要少一些。但UDP也是無法避免攻擊的,比如:UDP Flood攻擊…… UDP的缺點:不可靠,不穩定 因為UDP沒有TCP那些可靠的機制,在數據傳遞時,如果網絡質量不好,就會很容易丟包。基於上面的優缺點,那麼:什麼時候應該使用TCP:當對網絡通訊質量有要求的時候,比如:整個數據要準確無誤的傳遞給對方,這往往用於一些要求可靠的應用,比如HTTP、HTTPS、FTP等傳輸文件的協議,POP、SMTP等郵件傳輸的協議。在日常生活中,常見使用TCP協議的應用如下:瀏覽器,用的HTTP FlashFXP,用的FTP Outlook,用的POP、SMTP Putty,用的Telnet、SSH QQ文件傳輸。什麼時候應該使用UDP:當對網絡通訊質量要求不高的時候,要求網絡通訊速度能儘量的快,這時就可以使用UDP。比如,日常生活中,常見使用UDP協議的應用如下:QQ語音 QQ視頻 TFTP。
三次握手
- 第一次握手:建立連接時,客戶端發送syn包(syn=x)到服務器,並進入SYN_SENT狀態,等待服務器確認;SYN:同步序列編號(Synchronize Sequence Numbers)。
- 第二次握手:服務器收到syn包,必須確認客戶的SYN(ack=x+1),同時自己也發送一個SYN包(syn=y),即SYN+ACK包,此時服務器進入SYN_RECV狀態;
- 第三次握手:客戶端收到服務器的SYN+ACK包,向服務器發送確認包ACK(ack=y+1),此包發送完畢,客戶端和服務器進入ESTABLISHED(TCP連接成功)狀態,完成三次握手。
為什麼不能兩次握手
TCP是一個雙向通信協議,通信雙方都有能力發送信息,並接收響應。如果只是兩次握手, 至多隻有連接發起方的起始序列號能被確認, 另一方選擇的序列號則得不到確認
四次揮手
- 客戶端進程發出連接釋放報文,並且停止發送數據。釋放數據報文首部,FIN=1,其序列號為seq=u(等於前面已經傳送過來的數據的最後一個字節的序號加1),此時,客戶端進入FIN-WAIT-1(終止等待1)狀態。TCP規定,FIN報文段即使不攜帶數據,也要消耗一個序號。
- 服務器收到連接釋放報文,發出確認報文,ACK=1,ack=u+1,並且帶上自己的序列號seq=v,此時,服務端就進入了CLOSE-WAIT(關閉等待)狀態。TCP服務器通知高層的應用進程,客戶端向服務器的方向就釋放了,這時候處於半關閉狀態,即客戶端已經沒有數據要發送了,但是服務器若發送數據,客戶端依然要接受。這個狀態還要持續一段時間,也就是整個CLOSE-WAIT狀態持續的時間。
- 客戶端收到服務器的確認請求後,此時,客戶端就進入FIN-WAIT-2(終止等待2)狀態,等待服務器發送連接釋放報文(在這之前還需要接受服務器發送的最後的數據)。
- 服務器將最後的數據發送完畢後,就向客戶端發送連接釋放報文,FIN=1,ack=u+1,由於在半關閉狀態,服務器很可能又發送了一些數據,假定此時的序列號為seq=w,此時,服務器就進入了LAST-ACK(最後確認)狀態,等待客戶端的確認。
- 客戶端收到服務器的連接釋放報文後,必須發出確認,ACK=1,ack=w+1,而自己的序列號是seq=u+1,此時,客戶端就進入了TIME-WAIT(時間等待)狀態。注意此時TCP連接還沒有釋放,必須經過2∗∗MSL(最長報文段壽命)的時間後,當客戶端撤銷相應的TCB後,才進入CLOSED狀態。
- 服務器只要收到了客戶端發出的確認,立即進入CLOSED狀態。同樣,撤銷TCB後,就結束了這次的TCP連接。可以看到,服務器結束TCP連接的時間要比客戶端早一些
為什麼連接的時候是三次握手,關閉的時候卻是四次握手
因為當Server端收到Client端的SYN連接請求報文後,可以直接發送SYN+ACK報文。其中ACK報文是用來應答的,SYN報文是用來同步的。但是關閉連接時,當Server端收到FIN報文時,很可能並不會立即關閉SOCKET,所以只能先回復一個ACK報文,告訴Client端,"你發的FIN報文我收到了"。只有等到我Server端所有的報文都發送完了,我才能發送FIN報文,因此不能一起發送。故需要四步握手。
數據結構與算法
排序算法
- 冒泡排序
- 選擇排序:選擇排序與冒泡排序有點像,只不過選擇排序每次都是在確定了最小數的下標之後再進行交換,大大減少了交換的次數
- 插入排序:將一個記錄插入到已排序的有序表中,從而得到一個新的,記錄數增1的有序表
- 快速排序:通過一趟排序將序列分成左右兩部分,其中左半部分的的值均比右半部分的值小,然後再分別對左右部分的記錄進行排序,直到整個序列有序。
<code>int partition(int a[], int low, int high){
int key = a[low];
while( low while(low = key) high--;
a[low] = a[high];
while(low a[high] = a[low];
}
a[low] = key;
return low;
}
void quick_sort(int a[], int low, int high){
if(low >= high) return;
int keypos = partition(a, low, high);
quick_sort(a, low, keypos-1);
quick_sort(a, keypos+1, high);
}/<code>
- 堆排序:假設序列有n個元素,先將這n建成大頂堆,然後取堆頂元素,與序列第n個元素交換,然後調整前n-1元素,使其重新成為堆,然後再取堆頂元素,與第n-1個元素交換,再調整前n-2個元素…直至整個序列有序。
- 希爾排序:先將整個待排記錄序列分割成為若干子序列分別進行直接插入排序,待整個序列中的記錄基本有序時再對全體記錄進行一次直接插入排序。
- 歸併排序:把有序表劃分成元素個數儘量相等的兩半,把兩半元素分別排序,兩個有序表合併成一個
實際問題
高併發系統的設計與實現
在開發高併發系統時有三把利器用來保護系統:緩存、降級和限流。
- 緩存:緩存比較好理解,在大型高併發系統中,如果沒有緩存數據庫將分分鐘被爆,系統也會瞬間癱瘓。使用緩存不單單能夠提升系統訪問速度、提高併發訪問量,也是保護數據庫、保護系統的有效方式。大型網站一般主要是“讀”,緩存的使用很容易被想到。在大型“寫”系統中,緩存也常常扮演者非常重要的角色。比如累積一些數據批量寫入,內存裡面的緩存隊列(生產消費),以及HBase寫數據的機制等等也都是通過緩存提升系統的吞吐量或者實現系統的保護措施。甚至消息中間件,你也可以認為是一種分佈式的數據緩存。
- 降級:服務降級是當服務器壓力劇增的情況下,根據當前業務情況及流量對一些服務和頁面有策略的降級,以此釋放服務器資源以保證核心任務的正常運行。降級往往會指定不同的級別,面臨不同的異常等級執行不同的處理。根據服務方式:可以拒接服務,可以延遲服務,也有時候可以隨機服務。根據服務範圍:可以砍掉某個功能,也可以砍掉某些模塊。總之服務降級需要根據不同的業務需求採用不同的降級策略。主要的目的就是服務雖然有損但是總比沒有好。
- 限流:限流可以認為服務降級的一種,限流就是限制系統的輸入和輸出流量已達到保護系統的目的。一般來說系統的吞吐量是可以被測算的,為了保證系統的穩定運行,一旦達到的需要限制的閾值,就需要限制流量並採取一些措施以完成限制流量的目的。比如:延遲處理,拒絕處理,或者部分拒絕處理等等。
常見的限流算法:
常見的限流算法有計數器、漏桶和令牌桶算法。漏桶算法在分佈式環境中消息中間件或者Redis都是可選的方案。發放令牌的頻率增加可以提升整體數據處理的速度,而通過每次獲取令牌的個數增加或者放慢令牌的發放速度和降低整體數據處理速度。而漏桶不行,因為它的流出速率是固定的,程序處理速度也是固定的。
秒殺併發情況下庫存為負數問題
- for update顯示加鎖
- 把udpate語句寫在前邊,先把數量-1,之後select出庫存如果>-1就commit,否則rollback。
<code>update products set quantity = quantity-1 WHERE id=3;
select quantity from products WHERE id=3 for update;/<code>
- update語句在更新的同時加上一個條件
<code>quantity = select quantity from products WHERE id=3;
update products set quantity = ($quantity-1) WHERE id=3 and queantity = $quantity;/<code>
最後
有想獲取全部面試題和答案的朋友:轉發文章並關注我,後臺私信【面試資料】即可免費獲取
閱讀更多 小兮妹妹吖 的文章