單元素或者模塊,在現實生活中的產品設計里十分常見,那么,你有留意過表單設計都有哪些常見的交互形式和布局樣式嗎?作為一名設計師,你要怎么做好表單布局,并選擇適合目標用戶操作習慣的交互方式?一起來看看作者的分析和總結。
上一篇我們聊了關于表單設計的基礎知識點。接下來我們來聊聊表單系列的第二篇,表單常見的布局樣式和交互形式。
將我自己踩過的坑整理出來,目的是為了幫助那些剛邁入職場的設計師,對表單能有一個更好的了解,從而避免在工作中進入誤區,也希望能給PM們提供一些思路。
在表單設計中,通常需要根據信息的容量來選擇合理的內容形式來組織表單的內容形式,以此來確保信息屏效比和用戶的操作效率。
其中所謂“屏效”比是一個關于界面設計的一個概念,最初起源于諧音“坪效”,“坪效”指的是每坪的面積可以產出多少營業額(營業額/專柜所占總坪數),是一個市場營銷領域的概念,而界面設計中的“屏效”則是指屏幕單位時間、單位面積內的信息可以帶來多少商業效益/效率的提升。
依據表單的組織方式可以將其分為表單的組織形式分為三種,分別為:基礎布局、分組布局、分步驟布局。
1)基礎布局
基礎平鋪是最簡單的表單組織形式,將所有需要填寫的表單內容項直接羅列在頁面上。主要針對表單內容項較少且項目之間無邏輯關系不能按照一定的相關性進行分組的表單。
依據表單的尺寸或列數,可將平鋪方式分為單列平鋪和多列平鋪。
① 單列平鋪
② 多列平鋪
2)分組表單
分組歸納是基礎平鋪的演進方式,也是基于基礎平鋪上的交互設計四法則之一“組織”的應用,在基礎平鋪的基礎上將表單項中相關聯的項目進行分組,顯得更有規律和組織性,即使表單項較多也不會顯得雜亂和壓抑,用戶在填寫表單時的心理壓力和視覺疲勞也會得到緩解,操作體驗也會更好。
依據視覺樣式可以有三種形式,分別為:標題分組、卡片分組和標簽分組。
① 標題分組
標題分組是用文字標題對表單項進行分類,當表單的數據信息超過7個輸入域,同時關聯性沒有那么強且可以被分組時,用分組的方式幫用戶設置幾個休息點,讓用戶把要填寫表單的大任務拆解成幾個小任務來完成;緩解用戶在輸入上的心理壓力與視覺疲勞。
注意:分組內設置項要有強關聯性,否則不能歸為一組,不能因為字段多為了分組去分組。
標題分組對應的詳情展示:一項一項上下鋪出來,但如果表單詳情信息過長,可以考慮將錨點定位,點擊錨點定位的標題即可自動定位到該區域,方便用戶快速定位瀏覽位置。
② 卡片分組
卡片分組是在標題分組的基礎上給每個分組加上背景做成卡片的形式進行分組。
需要滿足數據內容體量很大(7-15個設置項)且超過一屏,關聯性更強、數據信息可被分類歸納時,用標題分組不足以給信息做層級區分,為了讓用戶在操作時更聚焦,同時也需要給用戶更明確的操作引導,可通過卡片分組的形式展示,對單獨的卡片進行命名。
注意:一個表單項不要分過多的卡片分組,不能每兩項做一個分組,這反而會造成用戶視覺壓力和操作負擔。
③ 標簽分組
當表單數據信息之間沒有特定的關聯性,可以并列單獨處理,且每個設置項都包含多個輸入域,且多個輸入域都使用了標題分組,為減少加載時間將表單分頁展現的情況下,布局就可以采用標簽頁布局進行展示操作。
標簽分組是以tab標簽頁的形式將不同分組的表單項進行并列分組的方式;這種方式一般比較少用,也不推薦,因為頁面上只能看到一個分組的內容,比較容易遺漏。
注意事項:
具體該如何選擇呢?
3)基礎分步表單
步驟引導是將需要填寫的表單信息按照線性流程進行組織,配備步驟條告知用戶完整的流程和進度,在全部步驟表單填寫完整后確認信息,流程結束后給予用戶操作的結果及反饋。
4)高級表單
高級表單適用于內容項復雜,多任務嵌套使用的場景,常見有動態表單、動態表格、折疊面板、彈窗/抽屜編輯等。
① 動態可編輯表單
表單內容項是不固定的,用戶可以按照實際業務需求對某些內容項進行動態增減。常見形式有一個固定的表單,通過增減按鈕可以設置表單數目,一般動態表單數目≤3,并且每個輸入框不需要單獨的標題使用。
② 可編輯表格
和動態表單的交互邏輯基本一致,外觀上是以表格形式展示,增減的動態數據數目建議3~6個。
建議條目表單數2~5項時使用,以使得每行內容可被完整呈現。
③ 折疊面板編輯
折疊面板:適用于表單中明顯嵌套子任務的模式,收起狀態下只讀子任務設置,展開狀態則可以對子任務的設置進行編輯修改。
建議條目表單數在6~8項時使用。
④ 規則樹
應用于規則編輯場景。適用于頁面中需要添加一個或多個對象,且每個對象都需要添加或編輯多組數據的情況。
⑤ 語句式表單
讓用戶在預設的結構來完成語句,常用于設置、編輯規則類表單,表單讀起來更友好更人性化。
影響表單布局與構成元素選擇的幾個要素:
內容數量:內容的多少會影響設計所選擇的容器、內容布局;如果內容較多,除了布局還要考慮采用分組、分步等形式去有序組織信息。
復雜程度:表單邏輯也是伴隨內容的多少而同比增加的,內容少則關系相對簡單,內容多則關系復雜。
邏輯結構:常見的有串行結構(各表單內容之間是線性關系)、并列結構(有多組表單,各組是并列關系)、更復雜的甚至有串行與并行嵌套結合的結構。
所處容器:表單內容所處的容器有頁面、抽屜、彈窗、氣泡,容器所能承載內容的多少也在逐步減少。在設計中我們根據打斷感、與上一級關聯程度、內容復雜度進行容器選擇。
來源頁面關聯:如果與來源頁面關聯強,則建議使用彈窗、抽屜等容器,可以停留在之前操作頁面上,缺點則是用戶操作的沉浸感偏弱;如果與來源頁面關聯弱且信息量較大,則建議使用頁面,同時在頁面中填寫表單的沉浸感也會更強。
關于使用何種布局方式的判斷,應從信息的復雜度和關聯性兩個維度去梳理。根據信息的復雜度和相關性模型,選用相應的信息呈現方式,選用合理的布局方案來承載詳情頁的內容。
下圖是為了能更直觀的讓設計師明確面對不同復雜程度的表單如何設計,根據信息的復雜度和相關性模型來進行選擇。
(來源:Ant Design)
在B端產品中,大致可以將表單操作的交互方式分為6種,依據使用頻率從低到高分別為:原位編輯、側邊抽屜、氣泡卡片、新開頁面、浮層彈窗、頁面跳轉,在選擇交互方式的時候需要根據使用場景和業務需求。
原位編輯是一種由內容展示演變而來的狀態,其編輯內容也為展示內容,單擊的時候切換為編輯狀態,可編輯內容,屬于輕量型的信息采集表單。
一般出現在表格或者卡片內,單個的字段展示(例如新建文檔標題等)也可能出現,正常情況下就是展示狀態,當鼠標懸浮時hover時提示可編輯,點擊字段內容或特定操作按鈕即激活為可編輯狀態。
氣泡卡片是一種類似于彈窗的對話框,但是比彈框要輕量很多,屬于超輕量的對話框,氣泡卡片內通常只包含一個輕量化的操作,允許用戶在當前界面快速對某一個操作進行編輯同時不需要打斷主任務流,可以隨取隨用,通常是非模態的,不對主頁面流程和操作具有阻斷性。
觸發生效機制可以是設置項點擊即生效,也可以多個設置項選擇后,觸發操作按鈕生效(操作按鈕建議不超過2個),觸發機制可以根據項目實際需求而定。
抽屜彈窗也被稱為側彈窗,彈窗抽屜和彈窗很類似,使用場景和親密度都是一樣的。相比彈窗,抽屜的側邊彈出的交互方式,其操作成本和用戶使用心理負擔會小很多,流暢性次于原位編輯與氣泡卡片交互但但優于頁面跳轉。
通常在主視窗的局部位置滑動出現,占用整個窗口高度,抽屜的承載能力大于彈窗,根據數據信息選擇彈窗或抽屜,允許承載較長的表單內容。
和模態一樣,滑出的內容是與上下文存在關系的,允許用戶在主視窗中查看參考信息,建議條目表單數>8項時使用。
注意事項:如果系統大部分用的彈窗,就優先選用彈窗,如果表單內增加了更多字段,可以換成抽屜彈窗。
新開頁面指的是保持當前頁面不變,在主頁面進行操作后在瀏覽器中新開標簽頁用以展示新頁面,瀏覽器停留的頁面可以是當前頁面也可以是新開的標簽頁。
彈窗交互是表單交互比較常見的交互方式,也具有較強的信息承載能力,同時拓展性也更強,在原位編輯與氣泡卡片無法滿足交互時選擇彈窗/抽屜交互,用戶在不離開當前頁面的情況下進行插入性操作,用戶也可隨時退出操作。
依據主頁面交互阻斷性可將彈窗分為模態彈窗和非模態彈窗兩種形式。
1)模態彈窗
模態彈窗以頁面對話框的形式呈現,體現頁面和彈窗之間的一種層級關系,激活彈窗時,用戶不能離開主頁面的流程,對主頁面的交互具有一定的阻斷性,不能繼續主頁面中的操作,必須關閉彈窗后才能繼續主頁面的操作。
2)非模態彈窗
非模態彈窗指的是用戶在不離開主頁面的情況下,可在當前頁面中打開多個浮層彈窗并對其內容進行編輯;
激活彈窗時,用戶可以離開當前的主頁面及相關流程對彈窗內容進行編輯,同時隨時可以回到主頁面及相關流程繼續操作,和模態彈窗的主要區別是對主頁面流程沒有阻斷性。
新頁面為當前頁面的分支流程,不會干涉用戶對于主頁面的操作,頁面功能是獨立的。
如果是初始化類型操作,超出了彈窗/抽屜的承載量,涉及錄入內容比較多的時候,有大量的信息要一項一項審核,就建議跳轉到頁面再進行新的操作,跳轉頁面體量較大,頁面更加穩定。
首先第一原則:不濫用表單的交互形式。
表單的交互設計,有時候往往會被設計所忽略,或者所有交互都采用彈窗,本可以氣泡卡片一步解決,使用彈窗卻要兩步完成,本需要界面跳轉承載復雜表單,卻使用彈窗不停滾動。
表單交互方式的選擇,我們可以參考 Ant Design 表單設計規范,從關聯性和復雜度進行判斷,在選擇時,我們優先考慮信息的復雜度,其次再考慮相關性。
根據內容的多少及親密程度來決定,我們設計時應選用哪種交互方式,或者可以直接根據內容承載量做判斷也是可以的,從少到多依次為:氣泡卡片 – 原位編輯 – 彈窗 – 抽屜 – 頁面跳轉- 新開頁面。
具體選擇:
關于不同交互方式的特點:
表單在設計時一般有2種適配方式,一種是固定適配,一種是間距適配。
1)固定適配
設計需要注意設計時,需要保證最小分辨率能夠正常顯示,表單中信息寬度固定,不隨分辨率變化而變化。該方式適合用于表單頁面的適配中。
當采用弱分組布局時,隨分辨率變小,數據項自動掉下來,其他保持不變。
這里最小分辨率大家根據自己公司情況而定,我在設計時設定1366X768為最小分辨率。下圖是百度統計流量研究所,大家可以看看數據,具體以自身公司而定,因為一些單位可能還在使用1280X720的分辨率,那么就設定1280為最小兼容的分辨率。
1)間距適配
和移動端類似,間距固定,組件自適應。
該適應方式在彈窗、抽屜中較為實用,表單頁中不太推薦使用該方式,因為當分辨率變大,眼動的視覺變大,不利于信息瀏覽。
關于表單設計其實還有很多可以深挖的空間,不管是To C 還是To B,都是為了實現用戶的需求、幫用戶解決問題。
我自己剛接觸B端產品的時候,還是習慣性的希望能把產品做的美觀,“高大上”。后來在工作中慢慢地發現每個項目的背后思考更為重要,把更多的精力投入到沉淀行業知識、研究產品架構、梳理交互方式和創新視覺表現上,輔助業務挖掘,為誰而設計很重要,從趨于相同的表象中找到產品獨有的閃光點,從而切實解決問題。
以上便是個人對表單設計經驗總結和方法沉淀,以及對部分問題的理解和分析,有不足或疏漏的地方的歡迎交流或留言補充。
長達16000+字,文章很長,感謝您的耐心閱讀。希望能夠通過這篇文章給到大家更多的啟發。文章中如果有不嚴謹、錯誤的地方希望大家給予指正。
下期預告:全方位解析在表單設計中,常見的設計疑問?
參考文獻:
本文由 @三原設計 原創發布于人人都是產品經理,未經許可,禁止轉載。
題圖來自Unsplash ,基于 CC0 協議
該文觀點僅代表作者本人,人人都是產品經理平臺僅提供信息存儲空間服務。
京報快訊(記者 裴劍飛)你的京牌小客車指標審核申請通過了?從今年1月1日起,北京市小客車數量調控新政正式實施,增加了“家庭申請”的渠道,并對原先的指標配置時間進行了調整。
今日(4月9日)9時開始,申請人就可以查看今年首次申請的審核結果。根據北京市小客車指標辦日前發布的“復核流程說明”,記者梳理了8個關注度較高的問題。
需要提醒的是,不管是個人申請者還是家庭申請者,如果對審核結果有異議應在15日內(即4月23日前)提出復核。若復核不通過,則不得再次提出復核申請。如果因為信息填報錯誤導致未通過的,不應當申請復核。
問題一:對審核結果有異議怎么辦?
應在15日內提出復核
北京市小客車數量調控新政實施后,原先每年6次搖號加上1次新能源指標配置的方式也進行了調整,改為每年2次搖號加上1次新能源指標配置。新政規定,單位、家庭和個人可于每年1月1日至3月8日、8月1日至10月8日提交配置指標申請。
其中,1月1日至3月8日提交的申請,指標管理機構于3月9日歸集發送至相關部門進行審核,相關部門應當于4月8日前反饋審核結果,申請單位、家庭和個人可在指定網站或各區政府設置的對外辦公窗口查詢審核結果。不管是“個人申請者”還是“家庭申請者”,對審核結果有異議的,都應當于4月23日前(15日內)提出復核申請,相應審核單位應當于5月24日前反饋復核結果。
申請審核未通過的原因類別和對應的審核主管單位。北京市小客車指標辦官網截圖
問題二:哪些原因會導致審核不通過?
包括戶籍、居住證、駕駛證、婚姻狀態等多種信息
根據北京市小客車指標辦發布的“復核流程說明”,個人(含家庭申請人、多車轉移申請人及夫妻變更/離婚析產轉移車輛申請人)資格審核未通過的,可以打開未通過人員的配置指標申請表查看審核未通過原因。
官方也公布了一批會導致審核未通過的原因和相對應的審核主管單位。其中包括,北京市戶籍居民身份信息,持居住證的非北京市戶籍人員居住證信息、個人駕駛證信息、名下登記車輛情況信息、個人繳納個人所得稅信息、個人《北京市工作居住證》信息、個人婚姻狀態配偶信息等內容,這些內容如果填報有誤,都有可能導致審核未通過。
問題三:怎樣申請復核?
先查未通過原因,若非填報錯誤,則根據提示“申請復核”
對于申請審核不通過的家庭申請者,可在4月23日前在網站(https://xkczb.jtw.beijing.gov.cn/)登錄系統進入用戶中心,在配置指標功能區點擊“我的家庭申請”查詢審核未通過原因。
對于申請審核不通過的個人申請者,需要在4月23日前辦理復核申請,并于當年的5月25日起查看復核結果。申請人在系統登錄后進入用戶中心,在配置指標功能區點擊“我的個人申請”查詢申請表信息及審核未通過原因。
不管是家庭申請人還是個人申請人,首先都應查詢審核未通過原因,仔細檢查所填信息是否有誤,如確認填報無誤,可根據提示進行“申請復核”操作或攜帶相關證明材料到相關審核部門申請復核,相應審核單位于5月24日前反饋復核結果。
問題四:如果復核還未通過怎么辦?
不可再次提出復核申請
如對審核結果有異議,可根據查詢到的未通過原因,于復核申請期限內向相關審核部門提出復核申請,未在復核申請期限內提出復核申請的,視為放棄復核,如需繼續申請,應當在之后的申報期內重新申請。
需要強調的是,申請單位、家庭和個人提出復核申請但復核不通過的,不可再次提出復核申請,如需繼續申請指標,應當重新申請。如果申請單位、家庭和個人對審核部門做出的審核結果有異議但未在規定時間內提出復核申請的,視為放棄復核,如需繼續申請指標,應當重新申請。
問題五:信息填報錯誤導致未通過的,可以申請復核嗎?
不應當提出復核申請
對于一些市民而言,確實會存在將申請信息填報錯誤的情況,也會導致審核未通過。
據北京市小客車指標辦介紹,復核工作是對審核不通過的信息進行再次核對,若審核未通過結果是由于申請人信息填報錯誤導致的,不應當提出復核申請,可于之后的申報期內修改申請后提交。
問題六:哪些情況需要家庭申請者變更申請?
如因出生或死亡導致家庭申請人人數增減但家庭主申請人未發生變化
對于家庭申請者而言,家庭人員的增減都會導致“家庭總積分”的變化。因此,北京市小客車指標新政規定:每年的1月1日-3月8日、8月1日-10月8日,兩個時間段可進行變更申請操作。
具體來看,申請有效期內,如因出生或死亡導致家庭申請人人數增減但家庭主申請人未發生變化的,應當變更申請。家庭主申請人可登錄系統進入用戶中心,在配置指標功能區點擊“我的家庭申請”,在申請表頁面點擊“變更申請”,按系統提示逐步操作,可刪除或增加相關申請人;也可攜帶本人有效身份證件及復印件就近到各區對外辦公窗口辦理。
問題七:哪些情況需要家庭申請者重新申請?
更換主申請人、申請人婚姻狀況發生變化等
申請有效期內,除了因出生或死亡導致家庭申請人人數增減且家庭主申請人未發生變化的情況之外,都需要重新申請。
需要重新申請的具體情形包括但不局限于:更換主申請人;更換其他申請人;主申請人或其他家庭申請人的申請信息(證件類型、證件號碼、駕駛證件、婚姻狀況)發生變化。每年的1月1日-3月8日、8月1日-10月8日,兩個時間段可進行重新申請操作。
申請有效期內,如因出生或死亡導致家庭申請人人數增減但家庭主申請人未發生變化的,應當變更申請;發生其他變化的應當重新申請。變更申請和重新申請均隨下一次指標配置進行審核。通過審核后,家庭總積分重新計算。
問題八:為何個稅信息審核未通過?
需近五年(含)連續在京繳納個稅,可以斷月,不能斷年
根據北京市小客車指標新政,參與京牌小客車指標配置的條件之一是:近五年(含)連續在本市繳納個人所得稅。一些申請者由于工作地點變更或工作中斷等原因,可能會導致在京個稅繳納出現斷檔情況。因此,這也是審核信息未通過的一項重要原因。
根據規定,“近五年(含)連續在本市繳納個人所得稅”的要求是,申請人從申請年(延期審核按申請有效期截止年)的上一年開始往前推算連續五年,每年在京繳納個人所得稅,且納稅額大于零,可以斷月,不能斷年,以稅款入庫日期為準(如有斷年,補繳無效)。
根據北京市小客車指標辦日前發布的“復核流程說明”,遇到“個稅信息審核未通過”的申請者可以登錄自然人電子稅務局網頁端(網址https://etax.chinatax.gov.cn/),根據系統提示完成查詢個人繳稅情況、核對已繳稅款等操作。
更多的審核未通過情況和具體復核操作流程可以查看北京市小客車指標辦官網:
https://xkczb.jtw.beijing.gov.cn/bszn/202148/1617863272613_1.html
新京報記者 裴劍飛
編輯 白爽 校對 王心
者:jialiangsun
最近做了一些服務性能優化,文章池服務平均耗時跟p99耗時都下降80%左右,事件底層頁服務平均耗時下降50%多左右,主要優化項目中一些不合理設計,例如服務間使用json傳輸數據,監控上報處理邏輯在主流程中,重復數據每次都請求下游服務,多個耗時操作串行請求等,這些問題都對服務有著嚴重的性能影響。
在服務架構設計時通常可以使用一些中間件去提升服務性能,例如使用mysql,redis,kafka等,因為這些中間件有著很好的讀寫性能。除了使用中間件提升服務性能外,也可以通過探索它們通過什么樣的底層設計實現的高性能,將這些設計應用到我們的服務架構中。
常用的性能優化方法可以分為以下幾種:
性能優化,緩存為王,所以開始先介紹一下緩存。緩存在我們的架構設計中無處不在的,常規請求是瀏覽器發起請求,請求服務端服務,服務端服務再查詢數據庫中的數據,每次讀取數據都會至少需要兩次網絡I/O,性能會差一些,我們可以在整個流程中增加緩存來提升性能。首先是瀏覽器測,可以通過Expires、Cache-Control、Last-Modified、Etag等相關字段來控制瀏覽器是否使用本地緩存。
其次我們可以在服務端服務使用本地緩存或者一些中間件來緩存數據,例如redis。redis之所以這么快,主要因為數據存儲在內存中,不需要讀取磁盤,因為內存讀取速度通常是磁盤的數百倍甚至更多;
然后在數據庫測,通常使用的是mysql,mysql的數據存儲到磁盤上,但是mysql為了提升讀寫性能,會利用bufferpool緩存數據頁。mysql讀取時會按照頁的粒度將數據頁讀取到bufferpool中,bufferpool中的數據頁使用LRU算法淘汰長期沒有用到的頁面,緩存最近訪問的數據頁。
此外小到cpu的l1、l2、l3級cache,大到瀏覽器緩存都是為了提高性能,緩存也是進行服務性能優化的重要手段,使用緩存時需要考慮以下幾點。
使用緩存時可以使用redis或者機器內存來緩存數據,使用redis的好處可以保證不同機器讀取數據的一致性,但是讀取redis會增加一次I/O,使用內存緩存數據時可能會出現讀取數據不一致,但是讀取性能好。例如文章的閱讀數數據,如果使用機器內存作為緩存,容易出現不同機器上緩存數據的不一致,用戶不同刷次會請求到不同服務端機器,讀取的閱讀數不一致,可能會出現閱讀數變小的情況,用戶體驗不好。對于閱讀數這種經常變更的數據比較適合使用redis來統一緩存。
也可以將兩者結合提升服務的性能,例如在內容池服務,利用redis跟機器內存緩存熱點文章詳情,優先讀取機器內存中的數據,數據不存在的時候會讀取redis中的緩存數據,當redis中的數據也不存在的時候,會讀取下游持久化存儲中的全量數據。其中內存級緩存過期時間為15s,在數據變更的時候不保證數據一致性,通過數據自然過期來保證最終一致性。redis中緩存數據需要保證與持久化存儲中數據一致性,如何保證一致性在后續講解。可以根據自己的業務場景可以選擇合適的緩存方案。
使用緩存時可以使用redis或者機器內存來緩存數據,使用redis的好處可以保證不同機器讀取數據的一致性,但是讀取redis會增加一次I/O,使用內存緩存數據時可能會出現讀取數據不一致,但是讀取性能好。例如文章的閱讀數數據,如果使用機器內存作為緩存,容易出現不同機器上緩存數據的不一致,用戶不同刷次會請求到不同服務端機器,讀取的閱讀數不一致,可能會出現閱讀數變小的情況,用戶體驗不好。對于閱讀數這種經常變更的數據比較適合使用redis來統一緩存。
也可以將兩者結合提升服務的性能,例如在內容池服務,利用redis跟機器內存緩存熱點文章詳情,優先讀取機器內存中的數據,數據不存在的時候會讀取redis中的緩存數據,當redis中的數據也不存在的時候,會讀取下游持久化存儲中的全量數據。其中內存級緩存過期時間為15s,在數據變更的時候不保證數據一致性,通過數據自然過期來保證最終一致性。redis中緩存數據需要保證與持久化存儲中數據一致性,如何保證一致性在后續講解。可以根據自己的業務場景可以選擇合適的緩存方案。
1、緩存雪崩:緩存雪崩是指緩存中的大量數據同時失效或者過期,導致大量的請求直接讀取到下游數據庫,導致數據庫瞬時壓力過大,通常的解決方案是將緩存數據設置的過期時間隨機化。在事件服務中就是利用固定過期時間+隨機值的方式進行文章的淘汰,避免緩存雪崩。
2、 緩存穿透:緩存穿透是指讀取下游不存在的數據,導致緩存命中不了,每次都請求下游數據庫。這種情況通常會出現在線上異常流量攻擊或者下游數據被刪除的狀況,針對緩存穿透可以使用布隆過濾器對不存在的數據進行過濾,或者在讀取下游數據不存在的情況,可以在緩存中設置空值,防止不斷的穿透。事件服務可能會出現查詢文章被刪除的情況,就是利用設置空值的方法防止被刪除數據的請求不斷穿透到下游。
3、 緩存擊穿: 緩存擊穿是指某個熱點數據在緩存中被刪除或者過期,導致大量的熱點請求同時請求數據庫。解決方案可以對于熱點數據設置較長的過期時間或者利用分布式鎖避免多個相同請求同時訪問下游服務。在新聞業務中,對于熱點新聞經常會出現這種情況,事件服務利用golang的singlefilght保證同一篇文章請求在同一時刻只有一個會請求到下游,防止緩存擊穿。
4、熱點key: 熱點key是指緩存中被頻繁訪問的key,導致緩存該key的分片或者redis訪問量過高。可以將可熱點key分散存儲到多個key上,例如將熱點key+序列號的方式存儲,不同key存儲的值都是相同的,在訪問時隨機訪問一個key,分散原來單key分片的壓力;此外還可以將key緩存到機器內存中,避免redis單節點壓力過大,在新聞業務中,對于熱點文章就是采用這種方式,將熱點文章存儲到機器內存中,避免存儲熱點文章redis單分片請求量過大。
key val=> key1 val 、 key2 val、 key3 val 、 key4 val
緩存的大小是有限的,因為需要對緩存中數據進行淘汰,通常可以采用隨機、LRU或者LFU算法等淘汰數據。LRU是一種最常用的置換算法,淘汰最近最久未使用的數據,底層可以利用map+雙端隊列的數據結構實現。
最原生的LRU算法是存在一些問題的,不知道大家在使用過有沒有遇到過問題。首先需要注意的是在數據結構中有互斥鎖,因為golang對于map的讀寫會產生panic,導致服務異常。使用互斥鎖之后會導致整個緩存性能變差,可以采用分片的思想,將整個LRUCache分為多個,每次讀取時讀取其中一個cache片,降低鎖的粒度來提升性能,常見的本地緩存包通常就利用這種方式實現的。
type LRUCache struct {
sync.Mutex
size int
capacity int
cache map[int]*DLinkNode
head, tail *DLinkNode
}
type DLinkNode struct {
key,value int
pre, next *DLinkNode
}
mysql也會利用LRU算法對buffer pool中的數據頁進行淘汰。由于mysql存在預讀,在讀取磁盤時并不是按需讀取,而是按照整個數據頁的粒度進行讀取,一個數據頁會存儲多條數據,除了讀取當前數據頁,可能也會將接下來可能用到的相鄰數據頁提前緩存到bufferpool中,如果下次讀取的數據在緩存中,直接讀取內存即可,不需要讀取磁盤,但是如果預讀的數據頁一直沒有被訪問,那就會存在預讀失效的情況,淘汰原來使用到的數據頁。mysql將buffer pool中的鏈表分為兩部分,一段是新生代,一段是老生代,新老生代的默認比是7:3,數據頁被預讀的時候會先加到老生代中,當數據頁被訪問時才會加載到新生代中,這樣就可以防止預讀的數據頁沒有被使用反而淘汰熱點數據頁。此外mysql通常會存在掃描表的請求,會順序請求大量的數據加載到緩存中,然后將原本緩存中所有熱點數據頁淘汰,這個問題通常被稱為緩沖池污染,mysql中的數據頁需要在老生代停留時間超過配置時間才會老生代移動到新生代時來解決緩存池污染。
redis中也會利用LRU進行淘汰過期的數據,如果redis將緩存數據都通過一個大的鏈表進行管理,在每次讀寫時將最新訪問的數據移動到鏈表隊頭,那樣會嚴重影響redis的讀寫性能,此外會增加額外的存儲空間,降低整體存儲數量。redis是對緩存中的對象增加一個最后訪問時間的字段,在對對象進行淘汰的時候,會采用隨機采樣的方案,隨機取5個值,淘汰最近訪問時間最久的一個,這樣就可以避免每次都移動節點。但是LRU也會存在緩存污染的情況,一次讀取大量數據會淘汰熱點數據,因此redis可以選擇利用LFU進行淘汰數據,是將原來的訪問時間字段變更為最近訪問時間+訪問次數的一個字段,這里需要注意的是訪問次數并不是單純的次數累加,而是根據最近訪問時間跟當前時間的差值進行時間衰減的,簡單說也就是訪問越久以及訪問次數越少計算得到的值也越小,越容易被淘汰。
typedef struct redisObject {
unsigned type:4;
unsigned encoding:4;
unsigned lru:LRU_BITS; /* LRU time (relative to global lru_clock) or
* LFU data (least significant 8 bits frequency
* and most significant 16 bits access time). */
int refcount;
void *ptr;
} obj ;
可以看出不同中間件對于傳統的LRU淘汰策略都進行了一定優化來保證服務性能,我們也可以參考不同的優化策略在自己的服務中進行緩存key的淘汰。
當數據庫中的數據變更時,如何保證緩存跟數據庫中的數據一致,通常有以下幾種方案:更新緩存再更新DB,更新DB再更新緩存,先更新DB再刪除緩存,刪除緩存再更新DB。這幾種方案都有可能會出現緩存跟數據庫中的數據不一致的情況,最常用的還是更新DB再刪除緩存,因為這種方案導致數據不一致的概率最小,但是也依然會存在數據不一致的問題。例如在T1時緩存中無數據,數據庫中數據為100,線程B查詢緩存沒有查詢到數據,讀取到數據庫的數據100然后去更新緩存,但是此時線程A將數據庫中的數據更新為99,然后在T4時刻刪除緩存中的數據,但是此時緩存中還沒有數據,在T5的時候線程B才更新緩存數據為100,這時候就會導致緩存跟數據庫中的數據不一致。
為保證緩存與數據庫數據的一致性。常用的解決方案有兩種,一種是延時雙刪,先刪除緩存,后續更新數據庫,休眠一會再刪除緩存。文章池服務中就是利用這種方案保證數據一致性,如何實現延遲刪除,是通過go語言中channel實現簡單延時隊列,沒有引入第三方的消息隊列,主要為了防止服務的復雜化;另外一種可以訂閱DB的變更binlog,數據更新時只更新DB,通過消費DB的binlog日志,解析變更操作進行緩存變更,更新失敗時不進行消息的提交,通過消息隊列的重試機制實現最終一致性。
redis在版本6.0之前都是號稱單線程模型,主要是利用epllo管理用戶海量連接,使用一個線程通過事件循環來處理用戶的請求,優點是避免了線程切換和鎖的競爭,以及實現簡單,但是缺點也比較明顯,不能有效的利用cpu的多核資源。隨著數據量和并發量的越來越大,I/O成了redis的性能瓶頸點,因此在6.0版本引入了多線程模型。redis的多線程將處理過程最耗時的sockect的讀取跟解析寫入由多個I/O 并發完成,對于命令的執行過程仍然由單線程完成。
mysql的主從同步過程從數據庫通過I/Othread讀取住主庫的binlog,將日志寫入到relay log中,然后由sqlthread執行relaylog進行數據的同步。其中sqlthread就是由多個線程并發執行加快數據的同步,防止主從同步延遲。sqlthread多線程化也經歷了多個版本迭代,按表維度分發到同一個線程進行數據同步,再到按行維度分發到同一個線程。
小到線程的并發處理,大到redis的集群,以及kafka的分topic分區都是通過多個client并行處理提高服務的讀寫性能。在我們的服務設計中可以通過創建多個容器對外服務提高服務的吞吐量,服務內部可以將多個串行的I/O操作改為并行處理,縮短接口的響應時長,提升用戶體驗。對于I/O存在相互依賴的情況,可以進行多階段分批并行化處理,另外一種常見的方案就是利用DAG加速執行,但是需要注意的是DAG會存在開發維護成本較高的情況,需要根據自己的業務場景選擇合適的方案。并行化也不是只有好處沒有壞處的,并行化有可能會導致讀擴散嚴重,以及線程切換頻繁存在一定的性能影響。
kafka的消息發送并不是直接寫入到broker中的,發送過程是將發送到同一個topic同一個分區的消息通過main函數的partitioner組件發送到同一個隊列中,由sender線程不斷拉取隊列中消息批量發送到broker中。利用批量發送消息處理,節省大量的網絡開銷,提高發送效率。
redis的持久化方式有RDB跟AOF兩種,其中AOF在執行命令寫入內存后,會寫入到AOF緩沖區,可以選擇合適的時機將AOF緩沖區中的數據寫入到磁盤中,刷新到磁盤的時間通過參數appendfsync控制,有三個值always、everysec、no。其中always會在每次命令執行完都會刷新到磁盤來保證數據的可靠性;everysec是每秒批量寫入到磁盤,no是不進行同步操作,由操作系統決定刷新到寫回磁盤,當redis異常退出時存在丟數據的風險。AOF命令刷新到磁盤的時機會影響redis服務寫入性能,通常配置為everysec批量寫入到磁盤,來平衡寫入性能和數據可靠性。
我們讀取下游服務或者數據庫的時候,可以一次多查詢幾條數據,節省網絡I/O;讀取redis的還可以利用pipeline或者lua腳本處理多條命令,提升讀寫性能;前端請求js文件或者小圖片時,可以將多個js文件或者圖片合并到一起返回,減少前端的連接數,提升傳輸性能。同樣需要注意的是批量處理多條數據,有可能會降低吞吐量,以及本身下游就不支持過多的批量數據,此時可以將多條數據分批并發請求。對于事件底層頁服務中不同組件下配置的不同文章id,會統一批量請求下游內容服務獲取文章詳情,對于批量的條數也會做限制,防止單批數據量過大。
redis的AOF重寫是利用bgrewriteaof命令進行AOF文件重寫,因為AOF是追加寫日志,對于同一個key可能存在多條修改修改命令,導致AOF文件過大,redis重啟后加載AOF文件會變得緩慢,導致啟動時間過長。可以利用重寫命令將對于同一個key的修改只保存一條記錄,減小AOF文件體積。
大數據領域的Hbase、cassandra等nosql數據庫寫入性能都很高,它們的底層存儲數據結構就是LSM樹(log structured merge tree),這種數據結構的核心思想是追加寫,積攢一定的數據后合并成更大的segement,對于數據的刪除也只是增加一條刪除記錄。同樣對一個key的修改記錄也有多條。這種存儲結構的優點是寫入性能高,但是缺點也比較明顯,數據存在冗余和文件體積大。主要通過線程進行段合并將多個小文件合并成更大的文件來減少存儲文件體積,提升查詢效率。
對于kafka進行傳輸數據時,在生產者端和消費者端可以開啟數據壓縮。生產者端壓縮數據后,消費者端收到消息會自動解壓,可以有效減小在磁盤的存儲空間和網絡傳輸時的帶寬消耗,從而降低成本和提升傳輸效率。需要注意生產者端和消費者端指定相同的壓縮算法。
在降本增效的浪潮中,降低redis成本的一種方式,就是對存儲到redis中的數據進行壓縮,降低存儲成本,重構后的內容微服務通過持久化存儲全量數據,采用snappy壓縮,壓縮后只是原來數據的40%-50%;
還有一種方式是將服務之間的調用從http的json改為trpc的pb協議,因為pb協議編碼后的數據更小,提升傳輸效率,在服務優化時,將原來請求tab的協議從json轉成pb,降低幾毫秒的時延,此外內容微服務存儲的數據采用flutbuffer編碼,相比較于protobuffer有著更高的壓縮比跟更快的編解碼速度;
對于JS/CSS多個文件下發也可以進行混淆和壓縮傳遞;對于存儲在es中的數據也可以手動調用api進行段合并,減小存儲數據的體積,提高查詢速度;在我們工作中還有一個比較常見的問題是接口返回的冗余數據特別多,一個接口服務下發的數據大而全,而不是對于當前場景做定制化下發,不滿足接口最小化原則,白白浪費了很多帶寬資源和降低傳輸效率。
redis通過單線程避免了鎖的競爭,避免了線程之間頻繁切換才有這很好的讀寫性能。
go語言中提供了atomic包,主要用于不同線程之間的數據同步,不需要加鎖,本質上就是封裝了底層cpu提供的原子操作指令。此外go語言最開始的調度模型時GM模型,所有的內核級線程想要執行goroutine需要加鎖從全局隊列中獲取,所以不同線程之間的競爭很激烈,調度效率很差。
后續引入了P(Processor),每一個M(thread)要執行G(gorontine)的時候需要綁定一個P,其中P中會有一個待執行G的本地隊列,只由當前M可以進行讀寫(少數情況會存在偷其他協程的G),讀取P本地隊列時不需要進行加鎖,通過降低鎖的競爭大幅度提升調度G的效率。
mysql利用mvcc實現多個事務進行讀寫并發時保證數據的一致性和隔離型,也是解決讀寫并發的一種無鎖化設計方案之一。它主要通過對每一行數據的變更記錄維護多個版本鏈來實現的,通過隱藏列rollptr和undolog來實現快照讀。在事務對某一行數據進行操作時,會根據當前事務id以及事務隔離級別判斷讀取那個版本的數據,對于可重復讀就是在事務開始的時候生成readview,在后續整個事務期間都使用這個readview。mysql中除了使用mvcc避免互斥鎖外,bufferpool還可以設置多個,通過多個bufferpool降低鎖的粒度,提升讀寫性能,也是一種優化方案。
日常工作 在讀多寫少的場景下可以利用atomic.value存儲數據,減少鎖的競爭,提升系統性能,例如配置服務中數據就是利用atomic.value存儲的;syncmap為了提升讀性能,優先使用atomic進行read操作,然后再進行加互斥鎖操作進行dirty的操作,在讀多寫少的情況下也可以使用syncmap。
秒殺系統的本質就是在高并發下準確的增減商品庫存,不出現超賣少賣的問題。因此所有的用戶在搶到商品時需要利用互斥鎖進行庫存數量的變更。互斥鎖的存在必然會成為系統瓶頸,但是秒殺系統又是一個高并發的場景,所以如何進行互斥鎖優化是提高秒殺系統性能的一個重要優化手段。
無鎖化設計方案之一就是利用消息隊列,對于秒殺系統的秒殺操作進行異步處理,將秒殺操作發布一個消息到消息隊列中,這樣所有用戶的秒殺行為就形成了一個先進先出的隊列,只有前面先添加到消息隊列中的用戶才能搶購商品成功。從隊列中消費消息進行庫存變更的線程是個單線程,因此對于db的操作不會存在沖突,不需要加鎖操作。
另外一種優化方式可以參考golang的GMP模型,將庫存分成多份,分別加載到服務server的本地,這樣多機之間在對庫存變更的時候就避免了鎖的競爭。如果本地server是單進程的,因此也可以形成一種無鎖化架構;如果是多進程的,需要對本地庫存加鎖后在進行變更,但是將庫存分散到server本地,降低了鎖的粒度,提高整個服務性能。
mysql的InnoDB存儲引擎在創建主鍵時通常會建議使用自增主鍵,而不是使用uuid,最主要的原因是InnoDB底層采用B+樹用來存儲數據,每個葉子結點是一個數據頁,存儲多條數據記錄,頁面內的數據通過鏈表有序存儲,數據頁間通過雙向鏈表存儲。由于uuid是無序的,有可能會插入到已經空間不足的數據頁中間,導致數據頁分裂成兩個新的數據頁以便插入新數據,影響整體寫入性能。
此外mysql中的寫入過程并不是每次將修改的數據直接寫入到磁盤中,而是修改內存中buffer pool內存儲的數據頁,將數據頁的變更記錄到undolog和binlog日志中,保證數據變更不丟失,每次記錄log都是追加寫到日志文件尾部,順序寫入到磁盤。對數據進行變更時通過順序寫log,避免隨機寫磁盤數據頁,提升寫入性能,這種將隨機寫轉變為順序寫的思想在很多中間件中都有所體現。
kakfa中的每個分區是一個有序不可變的消息隊列,新的消息會不斷的添加的partition的尾部,每個partition由多個segment組成,一個segment對應一個物理日志文件,kafka對segment日志文件的寫入也是順序寫。順序寫入的好處是避免了磁盤的不斷尋道和旋轉次數,極大的提高了寫入性能。
順序寫主要會應用在存在大量磁盤I/O操作的場景,日常工作中創建mysql表時選擇自增主鍵,或者在進行數據庫數據同步時順序讀寫數據,避免底層頁存儲引擎的數據頁分裂,也會對寫入性能有一定的提升。
redis對于命令的執行過程是單線程的,單機有著很好的讀寫性能,但是單機的機器容量跟連接數畢竟有限,因此單機redis必然會存在讀寫上限跟存儲上限。redis集群的出現就是為了解決單機redis的讀寫性能瓶頸問題,redis集群是將數據自動分片到多個節點上,每個節點負責數據的一部分,每個節點都可以對外提供服務,突破單機redis存儲限制跟讀寫上限,提高整個服務的高并發能力。除了官方推出的集群模式,代理模式codis等也是將數據分片到不同節點,codis將多個完全獨立的redis節點組成集群,通過codis轉發請求到某一節點,來提高服務存儲能力和讀寫性能。
同樣的kafka中每個topic也支持多個partition,partition分布到多個broker上,減輕單臺機器的讀寫壓力,通過增加partition數量可以增加消費者并行消費消息,提高kafka的水平擴展能力和吞吐量。
新聞每日會生產大量的圖文跟視頻數據,底層是通過tdsql存儲,可以分采分片化的存儲思想,將圖文跟視頻或者其他介質存儲到不同的數據庫或者數據表中,同一種介質每日的生產量也會很大,這時候就可以對同一種介質拆分成多個數據表,進一步提高數據庫的存儲量跟吞吐量。另外一種角度去優化存儲還可以將冷熱數據分離,最新的數據采用性能好的機器存儲,之前老數據訪問量低,采用性能差的機器存儲,節省成本。
在微服務重構過程中,需要進行數據同步,將總庫中存儲的全量數據通過kafka同步到內容微服務新的存儲中,預期同步qps高達15k。由于kafka的每個partition只能通過一個消費者消費,要達到預期qps,因此需要創建750+partition才能夠實現,但是kafka的partition過多會導致rebalance很慢,影響服務性能,成本和可維護行都不高。采用分片化的思想,可以將同一個partition中的數據,通過一個消費者在內存中分片到多個channel上,不同的channel對應的獨立協程進行消費,多協程并發處理消息提高消費速度,消費成功后寫入到對應的成功channel,由統一的offsetMaker線程消費成功消息進行offset提交,保證消息消費的可靠性。
為提升寫入性能,mysql在寫入數據的時候,對于在bufferpool中的數據頁,直接修改bufferpool的數據頁并寫redolog;對于不在內存中的數據頁并不會立刻將磁盤中的數據頁加載到bufferpool中,而是僅僅將變更記錄在緩沖區,等后續讀取磁盤上的數據頁到bufferpool中時會進行數據合并,需要注意的是對于非唯一索引才會采用這種方式,對于唯一索引寫入的時候需要每次都將磁盤上的數據讀取到bufferpool才能判斷該數據是否已存在,對于已存在的數據會返回插入失敗。
另外mysql查詢例如select * from table where name='xiaoming' 的查詢,如果name字段存在二級索引,由于這個查詢是*,表示需要所在行的所有字段,需要進行回表操作,如果僅需要id和name字段,可以將查詢語句改為select id , name from tabler where name='xiaoming' ,這樣只需要在name這個二級索引上就可以查到所需數據,避免回表操作,減少一次I/O,提升查詢速度。
web應用中可以使用緩存、合并css和js文件等,避免或者減少http請求,提升頁面加載速度跟用戶體驗。
在日常移動端開發應用中,對于多tab的數據,可以采用懶加載的方式,只有用戶切換到新的tab之后才會發起請求,避免很多無用請求。服務端開發隨著版本的迭代,有些功能字段端上已經不展示,但是服務端依然會返回數據字段,對于這些不需要的數據字段可以從數據源獲取上就做下線處理,避免無用請求。另外在數據獲取時可以對請求參數的合法性做準確的校驗,例如請求投票信息時,運營配置的投票ID可能是“” 或者“0”這種不合法參數,如果對請求參數不進行校驗,可能會存在很多無用I/O請求。另外在函數入口處通常會請求用戶的所有實驗參數,只有在實驗期間才會用到實驗參數,在實驗下線后并沒有下線ab實驗平臺的請求,可以在非實驗期間下線這部分請求,提升接口響應速度。
golang作為現代原生支持高并發的語言,池化技術在它的GMP模型就存在很大的應用。對于goroutine的銷毀就不是用完直接銷毀,而是放到P的本地空閑隊列中,當下次需要創建G的時候會從空閑隊列中直接取一個G復用即可;同樣的對于M的創建跟銷毀也是優先從全局隊列中獲取或者釋放。此外golang中sync.pool可以用來保存被重復使用的對象,避免反復創建和銷毀對象帶來的消耗以及減輕gc壓力。
mysql等數據庫也都提供連接池,可以預先創建一定數量的連接用于處理數據庫請求。當請求到來時,可以從連接池中選擇空閑連接來處理請求,請求結束后將連接歸還到連接池中,避免連接創建和銷毀帶來的開銷,提升數據庫性能。
在日常工作中可以創建線程池用來處理請求,在請求到來時同樣的從鏈接池中選擇空閑的線程來處理請求,處理結束后歸還到線程池中,避免線程創建帶來的消耗,在web框架等需要高并發的場景下非常常見。
異步處理在數據庫中同樣應用廣泛,例如redis的bgsave,bgrewriteof就是分別用來異步保存RDB跟AOF文件的命令,bgsave執行后會立刻返回成功,主線程fork出一個線程用來將內存中數據生成快照保存到磁盤,而主線程繼續執行客戶端命令;redis刪除key的方式有del跟unlink兩種,對于del命令是同步刪除,直接釋放內存,當遇到大key時,刪除操作會讓redis出現卡頓的問題,而unlink是異步刪除的方式,執行后對于key只做不可達的標識,對于內存的回收由異步線程回收,不阻塞主線程。
mysql的主從同步支持異步復制、同步復制跟半同步復制。異步復制是指主庫執行完提交的事務后立刻將結果返回給客戶端,并不關心從庫是否已經同步了數據;同步復制是指主庫執行完提交的事務,所有的從庫都執行了該事務才將結果返回給客戶端;半同步復制指主庫執行完后,至少一個從庫接收并執行了事務才返回給客戶端。有多種主要是因為異步復制客戶端寫入性能高,但是存在丟數據的風險,在數據一致性要求不高的場景下可以采用,同步方式寫入性能差,適合在數據一致性要求高的場景使用。 此外對于kafka的生產者跟消費者都可以采用異步的方式進行發送跟消費消息,但是采用異步的方式有可能會導致出現丟消息的問題。對于異步發送消息可以采用帶有回調函數的方式,當發送失敗后通過回調函數進行感知,后續進行消息補償。
在做服務性能優化中,發現之前的一些監控上報,曝光上報等操作都在主流程中,可以將這部分功能做異步處理,降低接口的時延。此外用戶發布新聞后,會將新聞寫入到個人頁索引,對圖片進行加工處理,標題進行審核,或者給用戶增加活動積分等操作,都可以采用異步處理,這里的異步處理是將發送消息這個動作發送消息到消息隊列中,不同的場景消費消息隊列中的消息進行各自邏輯的處理,這種設計保證了寫入性能,也解耦不同場景業務邏輯,提高系統可維護性。
本文主要總結進行服務性能優化的幾種方式,每一種方式在我們常用的中間件中都有所體現,我想這也是我們常說多學習這些中間件的意義,學習它們不僅僅是學會如何去使用它們,也是學習它們底層優秀的設計思想,理解為什么要這樣設計,這種設計有什么好處,后續我們在架構選型或者做服務性能優化時都會有一定的幫助。此外性能優化方式也給出了具體的落地實踐,
希望通過實際的應用例子加強對這種優化方式的理解。此外要做服務性能優化,還是要從自身服務架構出發,分析服務調用鏈耗時分布跟cpu消耗,優化有問題的rpc調用和函數。
*請認真填寫需求信息,我們會在24小時內與您取得聯系。