【Java面试高频-集合】- 读写的场景设计集合是怎么样?对于读多写少要如何设计的呢?对于读少写多又该如何设计呢?
Java集合-讀寫的場景
1 讀多寫少的場景
**CopyOnWrite的思想。**讀數(shù)據(jù)不加鎖,寫數(shù)據(jù)的時候利用拷貝的副本來執(zhí)行。
ReadWriteLock的思想也可以。
那么 寫線程現(xiàn)在把副本數(shù)組給修改完了,現(xiàn)在怎么才能讓讀線程感知到這個變化呢?
配合volatile關鍵字的使用,核心就是讓一個變量被寫線程給修改之后,立馬讓其他線程可以讀到這個變量引用的最近的值。 所以一旦線程搞定了副本數(shù)組的修改之后,那么就可以用volatile寫的方式,把這個副本數(shù)組賦值給volatile修飾的那個數(shù)組的引用變量了。只要一賦值給那個volatile修飾的變量,立馬就會對讀線程可見,大家就能看到最新的數(shù)組了。
那么要是多個線程要同時更新呢?那搞出來 多個副本會不會有什么問題?
不會,因為加入一個lock鎖的機制,也就是同一時間只能有一個線程可以更新。
2 讀少寫多的場景
優(yōu)化思路總結(jié):
- 給每個用戶信息配一把鎖,把用戶信息存在數(shù)組中,array[drive_id]=driver_info.如果數(shù)據(jù)量不大,這種方案可行。如果數(shù)據(jù)量非常大,會非常耗內(nèi)存;
- 水平切分,按照driver_id進行哈希,比如對10000進行取模,則把司機信息切分成1w組,用1w個map來存放司機信息。面對大數(shù)據(jù)時這種方法非常適合;
- 去掉鎖,存放的時候仍然以KV的map來存放,但是value不是在單獨的driver_info, 而是先給driver_info生成一個簽名,然后將簽名+driver_info分兩步寫到定長的內(nèi)存中,作為value存放。取得時候,對value中的driver_info先生成簽名,然后跟取出來的簽名比較。如果一樣,說明driver_info沒有發(fā)生竟態(tài)寫。
(1) 需求緣起
【業(yè)務場景】
有一類寫多讀少的場景:大部分請求是對數(shù)據(jù)進行修改,少部分請求對數(shù)據(jù)讀取;
例子1: 例子1:滴滴打車,某個司機地理位置信息的變化(可能每幾秒鐘有一個修改),以及司機地理位置的讀取(用戶打車的時候查看某個司機的地理位置)。
void SetDriverInfo(long driver_id, DriverInfoi); // 大量請求調(diào)用修改司機信息,可能主要是GPS位置的修改
DriverInfo GetDriverInfo(long driver_id); // 少量請求查詢司機信息
【底層實現(xiàn)】
具體到底層實現(xiàn)往往就是一個Map(本質(zhì)上是一個定長key,定長value的緩存結(jié)構(gòu))來存儲司機的信息,或者某個類型的計數(shù)。
【臨界資源】
這個Map存儲了所有信息,當并發(fā)讀寫訪問時,它作為臨界資源,在讀寫之前,一般要進行加鎖操作,以司機信息為例:
void SetDriverInfo(long driver_id, DriverInfoinfo){WriteLock (m_lock);Map= info;UnWriteLock(m_lock);}DriverInfo GetDriverInfo(long driver_id){DriverInfo t;ReadLock(m_lock);t= Map;UnReadLock(m_lock);return t;}【并發(fā)鎖瓶頸】
假設滴滴有100w司機同時在線,每個司機沒5秒更新一次經(jīng)緯度狀態(tài),那么每秒就有20w次寫并發(fā)操作。假設滴滴日訂單1000w個,平均每秒大概也有300個下單,對應到查詢并發(fā)量,可能是1000級別的并發(fā)讀操作。
上述實現(xiàn)方案沒有任何問題,但在并發(fā)量很大的時候(每秒20w寫,1k讀),鎖m_lock會成為潛在瓶頸,在這類高并發(fā)環(huán)境下寫多讀少的業(yè)務倉井,如何來進行優(yōu)化,是本文將要討論的問題。
(2) 水平切分+鎖粒度優(yōu)化
上文中之所以鎖沖突嚴重,是因為所有司機都公用一把鎖,鎖的粒度太粗(可以認為是一個數(shù)據(jù)庫的“庫級別鎖”),是否可能進行水平拆分(類似于數(shù)據(jù)庫里的分庫),把一個庫鎖變成多個庫鎖,來提高并發(fā),降低鎖沖突呢?顯然是可以的,把1個Map水平切分成多個Map即可:
void SetDriverInfo(long driver_id, DriverInfoinfo){i= driver_id % N; // 水平拆分成N份,N個Map,N個鎖WriteLock (m_lock [i]); //鎖第i把鎖Map[i]<driver_id>= info; // 操作第i個MapUnWriteLock (m_lock[i]); // 解鎖第i把鎖 }每個Map的并發(fā)量(變成了1/N)和數(shù)據(jù)量都降低(變成了1/N)了,所以理論上,鎖沖突會成平方指數(shù)降低。
分庫之后,仍然是庫鎖,有沒有辦法變成數(shù)據(jù)庫層面所謂的“行級鎖”呢,難道要把x條記錄變成x個Map嗎,這顯然是不現(xiàn)實的。
(3) Map變Array+最細鎖粒度優(yōu)化
假設driver_id是遞增生成的,并且緩存的內(nèi)存比較大,是可以把Map優(yōu)化成Array,而不是拆分成N個Map,是有可能把鎖的粒度細化到最細的(每個記錄一個鎖)。
void SetDriverInfo(long driver_id, DriverInfoinfo){index= driver_id;WriteLock (m_lock [index]); //超級大內(nèi)存,一條記錄一個鎖,鎖行鎖Array[index]= info; //driver_id就是Array下標UnWriteLock (m_lock[index]); // 解鎖行鎖 }和上一個方案相比,這個方案使得鎖沖突降到了最低,但鎖資源大增,在數(shù)據(jù)量非常大的情況下,一般不這么搞。數(shù)據(jù)量比較小的時候,可以一個元素一個鎖的(典型的是連接池,每個連接有一個鎖表示連接是否可用)。
上文中提到的另一個例子,用戶操作類型計數(shù),操作類型是有限的,即使一個type一個鎖,鎖的沖突也可能是很高的,還沒有方法進一步提高并發(fā)呢?
(3)把鎖去掉,變成無鎖緩存
【無鎖的結(jié)果】
void AddCountByType(long type /*, int count*/){//不加鎖Array[type]++; // 計數(shù)++//Array[type] += count; // 計數(shù)增加count }如果這個緩存不加鎖,當然可以達到最高的并發(fā),但是多線程對緩存中同一塊定長數(shù)據(jù)進行操作時,有可能出現(xiàn)不一致的數(shù)據(jù)塊,這個方案為了提高性能,犧牲了一致性。在讀取計數(shù)時,獲取到了錯誤的數(shù)據(jù),是不能接受的(作為緩存,允許cache miss,卻不允許讀臟數(shù)據(jù))。
加上簽名之后,不但緩存要寫入定長value本身,還要寫入定長簽名(例如16bitCRC校驗):
(1)線程1對緩存進行操作,對key想要寫入value1,寫入簽名v1-sign;
(2)線程2對緩存進行操作,對key想要寫入value2,寫入簽名v2-sign;
(3)如果不加鎖,線程1和線程2對同一個定長區(qū)域進行一個并發(fā)的寫操作,可能每個線程寫成功一半,導致出現(xiàn)臟數(shù)據(jù)產(chǎn)生,最終的結(jié)果即不是value1也不是value2,而是一個亂七八糟的不符合預期的值value-unexpected,但簽名,一定是v1-sign或者v2-sign中的任意一個;
畫外音:16bit/32bit的寫可以保證原子性。
(4)數(shù)據(jù)讀取的時候,不但要取出value,還要像消息接收方收到消息一樣,校驗一下簽名,如果發(fā)現(xiàn)簽名不一致,緩存則返回NULL,即cache miss;
總結(jié)
以上是生活随笔為你收集整理的【Java面试高频-集合】- 读写的场景设计集合是怎么样?对于读多写少要如何设计的呢?对于读少写多又该如何设计呢?的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 设节屏幕亮度
- 下一篇: JavaWeb商城项目笔记--- Day