整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

360°表单设计指南,快速掌握「表单」设计知识点(下

360°表单设计指南,快速掌握「表单」设计知识点(下)

单元素或者模块,在现实生活中的产品设计里十分常见,那么,你有留意过表单设计都有哪些常见的交互形式和布局样式吗?作为一名设计师,你要怎么做好表单布局,并选择适合目标用户操作习惯的交互方式?一起来看看作者的分析和总结。

上一篇我们聊了关于表单设计的基础知识点。接下来我们来聊聊表单系列的第二篇,表单常见的布局样式和交互形式。

将我自己踩过的坑整理出来,目的是为了帮助那些刚迈入职场的设计师,对表单能有一个更好的了解,从而避免在工作中进入误区,也希望能给PM们提供一些思路。

三、表单的布局形式

1. 常见的表单布局

在表单设计中,通常需要根据信息的容量来选择合理的内容形式来组织表单的内容形式,以此来确保信息屏效比和用户的操作效率。

其中所谓“屏效”比是一个关于界面设计的一个概念,最初起源于谐音“坪效”,“坪效”指的是每坪的面积可以产出多少营业额(营业额/专柜所占总坪数),是一个市场营销领域的概念,而界面设计中的“屏效”则是指屏幕单位时间、单位面积内的信息可以带来多少商业效益/效率的提升。

依据表单的组织方式可以将其分为表单的组织形式分为三种,分别为:基础布局、分组布局、分步骤布局。

1)基础布局

基础平铺是最简单的表单组织形式,将所有需要填写的表单内容项直接罗列在页面上。主要针对表单内容项较少且项目之间无逻辑关系不能按照一定的相关性进行分组的表单。

  • 优势:相对简洁、便于操作,比较适用于完成一个简单快捷的任务;
  • 劣势:表单项数量较大时一次性展示全部信息加重了用户的操作负担,填写效率较低。
  • 使用场景:当需要完成一个简单快速的任务(表单条目数在7个内),比如:输入少量信息即可完成创建、注册登陆表单。

依据表单的尺寸或列数,可将平铺方式分为单列平铺和多列平铺。

① 单列平铺

  • 优势:路径清晰,由上至下,填写效率高、体验好,操作顺畅;
  • 劣势:占用纵向空间。

② 多列平铺

  • 优势:节省页面纵向空间,信息承载量较大,能够放置更多的控件单元。
  • 劣势:“Z”字型的视觉动线较为复杂,填写体验不好,易出错易遗漏。

2)分组表单

分组归纳是基础平铺的演进方式,也是基于基础平铺上的交互设计四法则之一“组织”的应用,在基础平铺的基础上将表单项中相关联的项目进行分组,显得更有规律和组织性,即使表单项较多也不会显得杂乱和压抑,用户在填写表单时的心理压力和视觉疲劳也会得到缓解,操作体验也会更好。

依据视觉样式可以有三种形式,分别为:标题分组、卡片分组和标签分组。

  • 优势:将表单内容进行了分类归组,便于快速定位,能够减轻用户填写表单时的焦虑感和心理压力;
  • 劣势:分组标题的添加进一步增加了页面的垂直空间占用;
  • 使用场景:适用于表单条目数在7个以上,且表单项之间存在关联关系,具备分类归纳的基础条件。

① 标题分组

标题分组是用文字标题对表单项进行分类,当表单的数据信息超过7个输入域,同时关联性没有那么强且可以被分组时,用分组的方式帮用户设置几个休息点,让用户把要填写表单的大任务拆解成几个小任务来完成;缓解用户在输入上的心理压力与视觉疲劳。

注意:分组内设置项要有强关联性,否则不能归为一组,不能因为字段多为了分组去分组。

标题分组对应的详情展示:一项一项上下铺出来,但如果表单详情信息过长,可以考虑将锚点定位,点击锚点定位的标题即可自动定位到该区域,方便用户快速定位浏览位置。

② 卡片分组

卡片分组是在标题分组的基础上给每个分组加上背景做成卡片的形式进行分组。

需要满足数据内容体量很大(7-15个设置项)且超过一屏,关联性更强、数据信息可被分类归纳时,用标题分组不足以给信息做层级区分,为了让用户在操作时更聚焦,同时也需要给用户更明确的操作引导,可通过卡片分组的形式展示,对单独的卡片进行命名。

注意:一个表单项不要分过多的卡片分组,不能每两项做一个分组,这反而会造成用户视觉压力和操作负担。

③ 标签分组

当表单数据信息之间没有特定的关联性,可以并列单独处理,且每个设置项都包含多个输入域,且多个输入域都使用了标题分组,为减少加载时间将表单分页展现的情况下,布局就可以采用标签页布局进行展示操作。

标签分组是以tab标签页的形式将不同分组的表单项进行并列分组的方式;这种方式一般比较少用,也不推荐,因为页面上只能看到一个分组的内容,比较容易遗漏。

注意事项:

  1. tab标签的填写没有先后顺序的规则,标签页彼此之间没有特定的关联性,可独立去设置。也就是说先填写tab1还是先填写tab2,对表单的其他tab项没有任何影响,不存在联动关系。
  2. 对表单信息的分类可以有效的降低视觉噪音,帮助用户关注重要的填写内容,根据表单数据信息的优先级进行分类,将优先级高的放在表单前面,优先级低的放在表单后面,或进行折叠收起。

具体该如何选择呢?

  1. 如果每个组之间有逻辑先后顺序,那么推荐使用分步表单。
  2. 如果每个组之间关联性较强,就不适合分开,推荐使用锚点定位。
  3. 如果每个组既没有逻辑先后顺序,也没有关联性时,推荐使用标签分组。

3)基础分步表单

步骤引导是将需要填写的表单信息按照线性流程进行组织,配备步骤条告知用户完整的流程和进度,在全部步骤表单填写完整后确认信息,流程结束后给予用户操作的结果及反馈。

  • 优势:任务流程清晰,明确当前用户目标,减少用户负担;及时的反馈校验,也避免填写完成后才发现中间的表单填写有误,降低用户的犯错成本。
  • 劣势:无法通览全部内容,每一步的回溯成本较高
  • 适用场景:适用于具有明确线性逻辑的任务场景。

4)高级表单

高级表单适用于内容项复杂,多任务嵌套使用的场景,常见有动态表单、动态表格、折叠面板、弹窗/抽屉编辑等。

① 动态可编辑表单

表单内容项是不固定的,用户可以按照实际业务需求对某些内容项进行动态增减。常见形式有一个固定的表单,通过增减按钮可以设置表单数目,一般动态表单数目≤3,并且每个输入框不需要单独的标题使用。

② 可编辑表格

和动态表单的交互逻辑基本一致,外观上是以表格形式展示,增减的动态数据数目建议3~6个。

建议条目表单数2~5项时使用,以使得每行内容可被完整呈现。

③ 折叠面板编辑

折叠面板:适用于表单中明显嵌套子任务的模式,收起状态下只读子任务设置,展开状态则可以对子任务的设置进行编辑修改。

建议条目表单数在6~8项时使用。

④ 规则树

应用于规则编辑场景。适用于页面中需要添加一个或多个对象,且每个对象都需要添加或编辑多组数据的情况。

⑤ 语句式表单

让用户在预设的结构来完成语句,常用于设置、编辑规则类表单,表单读起来更友好更人性化。

2. 影响表单布局的要素

影响表单布局与构成元素选择的几个要素:

内容数量:内容的多少会影响设计所选择的容器、内容布局;如果内容较多,除了布局还要考虑采用分组、分步等形式去有序组织信息。

复杂程度:表单逻辑也是伴随内容的多少而同比增加的,内容少则关系相对简单,内容多则关系复杂。

逻辑结构:常见的有串行结构(各表单内容之间是线性关系)、并列结构(有多组表单,各组是并列关系)、更复杂的甚至有串行与并行嵌套结合的结构。

所处容器:表单内容所处的容器有页面、抽屉、弹窗、气泡,容器所能承载内容的多少也在逐步减少。在设计中我们根据打断感、与上一级关联程度、内容复杂度进行容器选择。

来源页面关联:如果与来源页面关联强,则建议使用弹窗、抽屉等容器,可以停留在之前操作页面上,缺点则是用户操作的沉浸感偏弱;如果与来源页面关联弱且信息量较大,则建议使用页面,同时在页面中填写表单的沉浸感也会更强。

3. 如何判断采用哪种布局方式

关于使用何种布局方式的判断,应从信息的复杂度和关联性两个维度去梳理。根据信息的复杂度和相关性模型,选用相应的信息呈现方式,选用合理的布局方案来承载详情页的内容。

下图是为了能更直观的让设计师明确面对不同复杂程度的表单如何设计,根据信息的复杂度和相关性模型来进行选择。

(来源:Ant Design)

四、表单的交互形式

在B端产品中,大致可以将表单操作的交互方式分为6种,依据使用频率从低到高分别为:原位编辑、侧边抽屉、气泡卡片、新开页面、浮层弹窗、页面跳转,在选择交互方式的时候需要根据使用场景和业务需求。

1. 原位编辑

原位编辑是一种由内容展示演变而来的状态,其编辑内容也为展示内容,单击的时候切换为编辑状态,可编辑内容,属于轻量型的信息采集表单。

一般出现在表格或者卡片内,单个的字段展示(例如新建文档标题等)也可能出现,正常情况下就是展示状态,当鼠标悬浮时hover时提示可编辑,点击字段内容或特定操作按钮即激活为可编辑状态。

  • 优势:快捷易操作,随时启用与退出,主流程的操作流畅度高。
  • 劣势:编辑状态较为隐蔽,不宜被察觉,特殊状态才会被触发。
  • 适用场景:适用于输入内容较少,适用于频率较低,同时属于主流程分支的场景。

2. 气泡卡片

气泡卡片是一种类似于弹窗的对话框,但是比弹框要轻量很多,属于超轻量的对话框,气泡卡片内通常只包含一个轻量化的操作,允许用户在当前界面快速对某一个操作进行编辑同时不需要打断主任务流,可以随取随用,通常是非模态的,不对主页面流程和操作具有阻断性。

触发生效机制可以是设置项点击即生效,也可以多个设置项选择后,触发操作按钮生效(操作按钮建议不超过2个),触发机制可以根据项目实际需求而定。

  • 优势:简单快捷易操作、主流程的操作流畅度高。
  • 劣势:扩展性不强,承载的信息不易过多。
  • 适用场景:适用于快速编辑和输入的场景。常用于条件筛选的设置,点击或hover后显示气泡卡片内容(建议不超过5个设置项)。

3. 抽屉编辑

抽屉弹窗也被称为侧弹窗,弹窗抽屉和弹窗很类似,使用场景和亲密度都是一样的。相比弹窗,抽屉的侧边弹出的交互方式,其操作成本和用户使用心理负担会小很多,流畅性次于原位编辑与气泡卡片交互但但优于页面跳转。

通常在主视窗的局部位置滑动出现,占用整个窗口高度,抽屉的承载能力大于弹窗,根据数据信息选择弹窗或抽屉,允许承载较长的表单内容。

和模态一样,滑出的内容是与上下文存在关系的,允许用户在主视窗中查看参考信息,建议条目表单数>8项时使用。

注意事项:如果系统大部分用的弹窗,就优先选用弹窗,如果表单内增加了更多字段,可以换成抽屉弹窗。

  • 优势:承载的信息量有较大的弹性空间。
  • 劣势:由于信息集中在一侧,导致视觉焦点不稳定,如果长时间工作,会产生不平衡的感觉。
  • 适用场景:适用于当前任务流中插入临时任务的场景。

4. 新开页面

新开页面指的是保持当前页面不变,在主页面进行操作后在浏览器中新开标签页用以展示新页面,浏览器停留的页面可以是当前页面也可以是新开的标签页。

  • 优势:页面之间相互独立,互补不干扰。
  • 劣势:用户的焦点丢失,注意力分散(因为系统中大部分的操作在同一个页面中完成)。
  • 适用场景:适用于需要参照一些文档来帮助用户完成表单录入操作的场景。

5. 窗口式表单(浮层弹窗)

弹窗交互是表单交互比较常见的交互方式,也具有较强的信息承载能力,同时拓展性也更强,在原位编辑与气泡卡片无法满足交互时选择弹窗/抽屉交互,用户在不离开当前页面的情况下进行插入性操作,用户也可随时退出操作。

依据主页面交互阻断性可将弹窗分为模态弹窗和非模态弹窗两种形式。

1)模态弹窗

模态弹窗以页面对话框的形式呈现,体现页面和弹窗之间的一种层级关系,激活弹窗时,用户不能离开主页面的流程,对主页面的交互具有一定的阻断性,不能继续主页面中的操作,必须关闭弹窗后才能继续主页面的操作。

  • 优势:简单易操作,承载的信息量有较大的弹性空间
  • 缺点:浮层弹窗给主操作流程造成较强的割裂,降低输入的流畅度。
  • 适用场景:适用于主流程步骤中需要分支任务的场景。

2)非模态弹窗

非模态弹窗指的是用户在不离开主页面的情况下,可在当前页面中打开多个浮层弹窗并对其内容进行编辑;

激活弹窗时,用户可以离开当前的主页面及相关流程对弹窗内容进行编辑,同时随时可以回到主页面及相关流程继续操作,和模态弹窗的主要区别是对主页面流程没有阻断性。

  • 优势:同时进行多个操作,阻断性弱。
  • 劣势:学习成本高,容易产生混乱,误操作概率高。
  • 适用场景:适用于多任务处理情况有较高的要求的场景。

6. 页面跳转

新页面为当前页面的分支流程,不会干涉用户对于主页面的操作,页面功能是独立的。

如果是初始化类型操作,超出了弹窗/抽屉的承载量,涉及录入内容比较多的时候,有大量的信息要一项一项审核,就建议跳转到页面再进行新的操作,跳转页面体量较大,页面更加稳定。

  • 优势:信息承载能力强;有利于用户对业务流程有更清晰的认识,从而使得主流程的操作流畅度高。
  • 劣势:输入字段较多,难于给予及时性反馈。
  • 适用场景:适用于特别重要的功能表单的填写场景。

7. 如何选择表单交互方式

首先第一原则:不滥用表单的交互形式。

表单的交互设计,有时候往往会被设计所忽略,或者所有交互都采用弹窗,本可以气泡卡片一步解决,使用弹窗却要两步完成,本需要界面跳转承载复杂表单,却使用弹窗不停滚动。

表单交互方式的选择,我们可以参考 Ant Design 表单设计规范,从关联性和复杂度进行判断,在选择时,我们优先考虑信息的复杂度,其次再考虑相关性。

根据内容的多少及亲密程度来决定,我们设计时应选用哪种交互方式,或者可以直接根据内容承载量做判断也是可以的,从少到多依次为:气泡卡片 – 原位编辑 – 弹窗 – 抽屉 – 页面跳转- 新开页面。

具体选择:

  1. 当信息复杂度低,同时相关性高时,我们可以选择原位编辑/气泡卡片、弹窗的交互方式。
  2. 当信息复杂度高,但关联性也较高时,我们可以使用抽屉、全屏弹窗的交互方式。
  3. 当信息复杂度高或信息独立时,我们可以使用页面的交互方式。

关于不同交互方式的特点:

  • 气泡卡片:承载内容比较少,直观,即见即所得。
  • 弹窗:通过小面积的弹窗进行轻量化的编辑,方便快速进行增、删、改、查;输入项较少,一般不会有滚动条。
  • 抽屉:与弹窗式相似,通过小面积的侧边栏进行编辑;可承载比弹窗更复杂一些的表单内容,可以有滚动条。
  • 页面跳转:最常用方式,适用于绝大部分的表单,支持构建复杂的表单。

五、最后

1. 表单页面要考虑适配方式

表单在设计时一般有2种适配方式,一种是固定适配,一种是间距适配。

1)固定适配

设计需要注意设计时,需要保证最小分辨率能够正常显示,表单中信息宽度固定,不随分辨率变化而变化。该方式适合用于表单页面的适配中。

当采用弱分组布局时,随分辨率变小,数据项自动掉下来,其他保持不变。

这里最小分辨率大家根据自己公司情况而定,我在设计时设定1366X768为最小分辨率。下图是百度统计流量研究所,大家可以看看数据,具体以自身公司而定,因为一些单位可能还在使用1280X720的分辨率,那么就设定1280为最小兼容的分辨率。

1)间距适配

和移动端类似,间距固定,组件自适应。

该适应方式在弹窗、抽屉中较为实用,表单页中不太推荐使用该方式,因为当分辨率变大,眼动的视觉变大,不利于信息浏览。

2. 总结

关于表单设计其实还有很多可以深挖的空间,不管是To C 还是To B,都是为了实现用户的需求、帮用户解决问题。

我自己刚接触B端产品的时候,还是习惯性的希望能把产品做的美观,“高大上”。后来在工作中慢慢地发现每个项目的背后思考更为重要,把更多的精力投入到沉淀行业知识、研究产品架构、梳理交互方式和创新视觉表现上,辅助业务挖掘,为谁而设计很重要,从趋于相同的表象中找到产品独有的闪光点,从而切实解决问题。

以上便是个人对表单设计经验总结和方法沉淀,以及对部分问题的理解和分析,有不足或疏漏的地方的欢迎交流或留言补充。

长达16000+字,文章很长,感谢您的耐心阅读。希望能够通过这篇文章给到大家更多的启发。文章中如果有不严谨、错误的地方希望大家给予指正。

下期预告:全方位解析在表单设计中,常见的设计疑问?

参考文献:

  1. 来源链接:https://ant.design/docs/spec/research-form-cn(来源:Ant Design)
  2. 表单设计需要注意 http://t.cn/EhMmZPf
  3. 表单设计http://www.woshipm.com/pd/4147841.html
  4. 《Ant Design表单设计》来源链接:https://ant.design/docs/spec/research-form-cn
  5. 《web表单设计》

本文由 @三原设计 原创发布于人人都是产品经理,未经许可,禁止转载。

题图来自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调用和函数。