精华内容
下载资源
问答
  • 部门存在或不是生产部门,如下图: 是否是工单信息不完整? 检查工单,工作中心,部门等信息完整无误! 提示部门存在或不是生产部门存在性: 则检查对应部门是否失效或者被强行删除 检查...

    用户反馈工单变更单提示错误:
    此部门不存在或不是生产部门,如下图:
    这里写图片描述
    是否是工单信息不完整?
    检查工单,工作中心,部门等信息完整无误!
    提示部门不存在或不是生产部门?
    存在性:
    则检查对应部门是否失效或者被强行删除
    检查部门信息,“暂无异常”…
    部门属性:生产部门选项在哪里呢?默认录入生产部门界面生产部门是被隐藏的,经网友提示是否启用高级成本。
    检查模块启用之高级成本,未启用。
    重新启用高级成本:
    这里写图片描述
    生产部门属性终于显示:
    这里写图片描述
    重新勾上,再取消高级成本模块。

    用户重做变更单,错误消失!

    总结:在新增部门时,默认不会勾选生产部门,生产部门选项默认被隐藏,在启用高级成本管理时,会显示并可修改保存。

    展开全文
  • 坚持服务过程中的总结和创造,不断提升后勤处服务管理理水平,按公司要求基本完成了后勤保障的工作任务,为公司的安全运行工作做出了积极的贡献,回顾这一半年来的工作,虽然取得了一些较好的成绩,但也存在一些不容...
  • 仓储部门2019年终工作... 二、20xx年仓储物流工作中遇到的问题 仓储物流工作的进步是显著的,然而,也存在许多问题和不足。如接收货中的货物损坏及包装破损处理不及时;出库配送中的串货错发、交接不明、货物丢失;?..
  • 分布式系统常见问题总结

    万次阅读 2019-09-02 15:58:26
    五、总结 上文应该描述的非常清楚了,没什么总结了,对于秒杀系统,再次重复下我个人经验的两个架构优化思路: (1)尽量将请求拦截在系统上游(越上游越好); (2)读多写少的常用多使用缓存(缓存抗读压力); ...

    微信搜索:“二十同学” 公众号,欢迎关注一条不一样的成长之路

    秒杀系统架构优化思路

    一、秒杀业务为什么难做

    1)im系统,例如qq或者微博,每个人都读自己的数据(好友列表、群列表、个人信息);

    2)微博系统,每个人读你关注的人的数据,一个人读多个人的数据;

    3)秒杀系统,库存只有一份,所有人会在集中的时间读和写这些数据,多个人读一个数据。

    例如:小米手机每周二的秒杀,可能手机只有1万部,但瞬时进入的流量可能是几百几千万。

    又例如:12306抢票,票是有限的,库存一份,瞬时流量非常多,都读相同的库存。读写冲突,锁非常严重,这是秒杀业务难的地方。那我们怎么优化秒杀业务的架构呢?

    二、优化方向

    优化方向有两个(今天就讲这两个点):

    (1)将请求尽量拦截在系统上游(不要让锁冲突落到数据库上去)。传统秒杀系统之所以挂,请求都压倒了后端数据层,数据读写锁冲突严重,并发高响应慢,几乎所有请求都超时,流量虽大,下单成功的有效流量甚小。以12306为例,一趟火车其实只有2000张票,200w个人来买,基本没有人能买成功,请求有效率为0。

    (2)充分利用缓存,秒杀买票,这是一个典型的读多些少的应用场景,大部分请求是车次查询,票查询,下单和支付才是写请求。一趟火车其实只有2000张票,200w个人来买,最多2000个人下单成功,其他人都是查询库存,写比例只有0.1%,读比例占99.9%,非常适合使用缓存来优化。好,后续讲讲怎么个“将请求尽量拦截在系统上游”法,以及怎么个“缓存”法,讲讲细节。

    三、常见秒杀架构

    常见的站点架构基本是这样的(绝对不画忽悠类的架构图)


    (1)浏览器端,最上层,会执行到一些JS代码

    (2)站点层,这一层会访问后端数据,拼html页面返回给浏览器

    (3)服务层,向上游屏蔽底层数据细节,提供数据访问

    (4)数据层,最终的库存是存在这里的,mysql是一个典型(当然还有会缓存)

    这个图虽然简单,但能形象的说明大流量高并发的秒杀业务架构,大家要记得这一张图。

    后面细细解析各个层级怎么优化。

    四、各层次优化细节

    第一层,客户端怎么优化(浏览器层,APP层)

    问大家一个问题,大家都玩过微信的摇一摇抢红包对吧,每次摇一摇,就会往后端发送请求么?回顾我们下单抢票的场景,点击了“查询”按钮之后,系统那个卡呀,进度条涨的慢呀,作为用户,我会不自觉的再去点击“查询”,对么?继续点,继续点,点点点。。。有用么?平白无故的增加了系统负载,一个用户点5次,80%的请求是这么多出来的,怎么整?

    (a)产品层面,用户点击“查询”或者“购票”后,按钮置灰,禁止用户重复提交请求;

    (b)JS层面,限制用户在x秒之内只能提交一次请求;

    APP层面,可以做类似的事情,虽然你疯狂的在摇微信,其实x秒才向后端发起一次请求。这就是所谓的“将请求尽量拦截在系统上游”,越上游越好,浏览器层,APP层就给拦住,这样就能挡住80%+的请求,这种办法只能拦住普通用户(但99%的用户是普通用户)对于群内的高端程序员是拦不住的。firebug一抓包,http长啥样都知道,js是万万拦不住程序员写for循环,调用http接口的,这部分请求怎么处理?

    第二层,站点层面的请求拦截

    怎么拦截?怎么防止程序员写for循环调用,有去重依据么?ip?cookie-id?…想复杂了,这类业务都需要登录,用uid即可。在站点层面,对uid进行请求计数和去重,甚至不需要统一存储计数,直接站点层内存存储(这样计数会不准,但最简单)。一个uid,5秒只准透过1个请求,这样又能拦住99%的for循环请求。

    5s只透过一个请求,其余的请求怎么办?缓存,页面缓存,同一个uid,限制访问频度,做页面缓存,x秒内到达站点层的请求,均返回同一页面。同一个item的查询,例如车次,做页面缓存,x秒内到达站点层的请求,均返回同一页面。如此限流,既能保证用户有良好的用户体验(没有返回404)又能保证系统的健壮性(利用页面缓存,把请求拦截在站点层了)。

    页面缓存不一定要保证所有站点返回一致的页面,直接放在每个站点的内存也是可以的。优点是简单,坏处是http请求落到不同的站点,返回的车票数据可能不一样,这是站点层的请求拦截与缓存优化。

    好,这个方式拦住了写for循环发http请求的程序员,有些高端程序员(黑客)控制了10w个肉鸡,手里有10w个uid,同时发请求(先不考虑实名制的问题,小米抢手机不需要实名制),这下怎么办,站点层按照uid限流拦不住了。

    第三层 服务层来拦截(反正就是不要让请求落到数据库上去)

    服务层怎么拦截?大哥,我是服务层,我清楚的知道小米只有1万部手机,我清楚的知道一列火车只有2000张车票,我透10w个请求去数据库有什么意义呢?没错,请求队列!

    对于写请求,做请求队列,每次只透有限的写请求去数据层(下订单,支付这样的写业务)

    1w部手机,只透1w个下单请求去db

    3k张火车票,只透3k个下单请求去db

    如果均成功再放下一批,如果库存不够则队列里的写请求全部返回“已售完”。

    对于读请求,怎么优化?cache抗,不管是memcached还是redis,单机抗个每秒10w应该都是没什么问题的。如此限流,只有非常少的写请求,和非常少的读缓存mis的请求会透到数据层去,又有99.9%的请求被拦住了。

    当然,还有业务规则上的一些优化。回想12306所做的,分时分段售票,原来统一10点卖票,现在8点,8点半,9点,...每隔半个小时放出一批:将流量摊匀。

    其次,数据粒度的优化:你去购票,对于余票查询这个业务,票剩了58张,还是26张,你真的关注么,其实我们只关心有票和无票?流量大的时候,做一个粗粒度的“有票”“无票”缓存即可。

    第三,一些业务逻辑的异步:例如下单业务与 支付业务的分离。这些优化都是结合 业务 来的,我之前分享过一个观点“一切脱离业务的架构设计都是耍流氓”架构的优化也要针对业务。

    好了,最后是数据库层

    浏览器拦截了80%,站点层拦截了99.9%并做了页面缓存,服务层又做了写请求队列与数据缓存,每次透到数据库层的请求都是可控的。db基本就没什么压力了,闲庭信步,单机也能扛得住,还是那句话,库存是有限的,小米的产能有限,透这么多请求来数据库没有意义。

    全部透到数据库,100w个下单,0个成功,请求有效率0%。透3k个到数据,全部成功,请求有效率100%。

    五、总结

    上文应该描述的非常清楚了,没什么总结了,对于秒杀系统,再次重复下我个人经验的两个架构优化思路:

    (1)尽量将请求拦截在系统上游(越上游越好);

    (2)读多写少的常用多使用缓存(缓存抗读压力);

    浏览器和APP:做限速

    站点层:按照uid做限速,做页面缓存

    服务层:按照业务做写请求队列控制流量,做数据缓存

    数据层:闲庭信步

    并且:结合业务做优化

    六、Q&A

    问题1、按你的架构,其实压力最大的反而是站点层,假设真实有效的请求数有1000万,不太可能限制请求连接数吧,那么这部分的压力怎么处理?

    答:每秒钟的并发可能没有1kw,假设有1kw,解决方案2个:

    (1)站点层是可以通过加机器扩容的,最不济1k台机器来呗。

    (2)如果机器不够,抛弃请求,抛弃50%(50%直接返回稍后再试),原则是要保护系统,不能让所有用户都失败。

     

    问题2、“控制了10w个肉鸡,手里有10w个uid,同时发请求” 这个问题怎么解决哈?

    答:上面说了,服务层写请求队列控制

     

    问题3限制访问频次的缓存,是否也可以用于搜索?例如A用户搜索了“手机”,B用户搜索“手机”,优先使用A搜索后生成的缓存页面?

    答:这个是可以的,这个方法也经常用在“动态”运营活动页,例如短时间推送4kw用户app-push运营活动,做页面缓存。

     

    问题4:如果队列处理失败,如何处理?肉鸡把队列被撑爆了怎么办?

    答:处理失败返回下单失败,让用户再试。队列成本很低,爆了很难吧。最坏的情况下,缓存了若干请求之后,后续请求都直接返回“无票”(队列里已经有100w请求了,都等着,再接受请求也没有意义了)

     

    问题5:站点层过滤的话,是把uid请求数单独保存到各个站点的内存中么?如果是这样的话,怎么处理多台服务器集群经过负载均衡器将相同用户的响应分布到不同服务器的情况呢?还是说将站点层的过滤放到负载均衡前?

    答:可以放在内存,这样的话看似一台服务器限制了5s一个请求,全局来说(假设有10台机器),其实是限制了5s 10个请求,解决办法:

    1)加大限制(这是建议的方案,最简单)

    2)在nginx层做7层均衡,让一个uid的请求尽量落到同一个机器上

     

    问题6:服务层过滤的话,队列是服务层统一的一个队列?还是每个提供服务的服务器各一个队列?如果是统一的一个队列的话,需不需要在各个服务器提交的请求入队列前进行锁控制?

    答:可以不用统一一个队列,这样的话每个服务透过更少量的请求(总票数/服务个数),这样简单。统一一个队列又复杂了。

     

    问题7:秒杀之后的支付完成,以及未支付取消占位,如何对剩余库存做及时的控制更新

    答:数据库里一个状态,未支付。如果超过时间,例如45分钟,库存会重新会恢复(大家熟知的“回仓”),给我们抢票的启示是,开动秒杀后,45分钟之后再试试看,说不定又有票哟~

     

    问题8:不同的用户浏览同一个商品 落在不同的缓存实例显示的库存完全不一样 请问老师怎么做缓存数据一致或者是允许脏读?

    答:目前的架构设计,请求落到不同的站点上,数据可能不一致(页面缓存不一样),这个业务场景能接受。但数据库层面真实数据是没问题的。

     

    问题9:就算处于业务把优化考虑“3k张火车票,只透3k个下单请求去db”那这3K个订单就不会发生拥堵了吗?

    答:(1)数据库抗3k个写请求还是ok的;(2)可以数据拆分;(3)如果3k扛不住,服务层可以控制透过去的并发数量,根据压测情况来吧,3k只是举例;

     

    问题10;如果在站点层或者服务层处理后台失败的话,需不需要考虑对这批处理失败的请求做重放?还是就直接丢弃?

    答:别重放了,返回用户查询失败或者下单失败吧,架构设计原则之一是“fail fast”。

     

    问题11.对于大型系统的秒杀,比如12306,同时进行的秒杀活动很多,如何分流?

    答:垂直拆分

     

    问题12、额外又想到一个问题。这套流程做成同步还是异步的?如果是同步的话,应该还存在会有响应反馈慢的情况。但如果是异步的话,如何控制能够将响应结果返回正确的请求方?

    答:用户层面肯定是同步的(用户的http请求是夯住的),服务层面可以同步可以异步。

     

    问题13、秒杀群提问:减库存是在那个阶段减呢?如果是下单锁库存的话,大量恶意用户下单锁库存而不支付如何处理呢?

    答:数据库层面写请求量很低,还好,下单不支付,等时间过完再“回仓”,之前提过了。

     

    细聊分布式ID生成方法

    一、需求缘起

    几乎所有的业务系统,都有生成一个记录标识的需求,例如:

    (1)消息标识:message-id

    (2)订单标识:order-id

    (3)帖子标识:tiezi-id

    这个记录标识往往就是数据库中的唯一主键,数据库上会建立聚集索引(cluster index),即在物理存储上以这个字段排序。

     

    这个记录标识上的查询,往往又有分页或者排序的业务需求,例如:

    (1)拉取最新的一页消息:selectmessage-id/ order by time/ limit 100

    (2)拉取最新的一页订单:selectorder-id/ order by time/ limit 100

    (3)拉取最新的一页帖子:selecttiezi-id/ order by time/ limit 100

    所以往往要有一个time字段,并且在time字段上建立普通索引(non-cluster index)。

     

    我们都知道普通索引存储的是实际记录的指针,其访问效率会比聚集索引慢,如果记录标识在生成时能够基本按照时间有序,则可以省去这个time字段的索引查询:

    select message-id/ (order by message-id)/limit 100

    再次强调,能这么做的前提是,message-id的生成基本是趋势时间递增的

     

    这就引出了记录标识生成(也就是上文提到的三个XXX-id)的两大核心需求:

    (1)全局唯一

    (2)趋势有序

    这也是本文要讨论的核心问题:如何高效生成趋势有序的全局唯一ID。

     

    二、常见方法、不足与优化

    【常见方法一:使用数据库的 auto_increment 来生成全局唯一递增ID】

    优点:

    (1)简单,使用数据库已有的功能

    (2)能够保证唯一性

    (3)能够保证递增性

    (4)步长固定

    缺点:

    (1)可用性难以保证:数据库常见架构是一主多从+读写分离,生成自增ID是写请求,主库挂了就玩不转了

    (2)扩展性差,性能有上限:因为写入是单点,数据库主库的写性能决定ID的生成性能上限,并且难以扩展

    改进方法:

    (1)增加主库,避免写入单点

    (2)数据水平切分,保证各主库生成的ID不重复


    如上图所述,由1个写库变成3个写库,每个写库设置不同的auto_increment初始值,以及相同的增长步长,以保证每个数据库生成的ID是不同的(上图中库0生成0,3,6,9…,库1生成1,4,7,10,库2生成2,5,8,11…)

    改进后的架构保证了可用性,但缺点是:

    (1)丧失了ID生成的“绝对递增性”:先访问库0生成0,3,再访问库1生成1,可能导致在非常短的时间内,ID生成不是绝对递增的(这个问题不大,我们的目标是趋势递增,不是绝对递增)

    (2)数据库的写压力依然很大,每次生成ID都要访问数据库

    为了解决上述两个问题,引出了第二个常见的方案

     

    【常见方法二:单点批量ID生成服务】

    分布式系统之所以难,很重要的原因之一是“没有一个全局时钟,难以保证绝对的时序”,要想保证绝对的时序,还是只能使用单点服务,用本地时钟保证“绝对时序”。数据库写压力大,是因为每次生成ID都访问了数据库,可以使用批量的方式降低数据库写压力。


    如上图所述,数据库使用双master保证可用性,数据库中只存储当前ID的最大值,例如0。ID生成服务假设每次批量拉取6个ID,服务访问数据库,将当前ID的最大值修改为5,这样应用访问ID生成服务索要ID,ID生成服务不需要每次访问数据库,就能依次派发0,1,2,3,4,5这些ID了,当ID发完后,再将ID的最大值修改为11,就能再次派发6,7,8,9,10,11这些ID了,于是数据库的压力就降低到原来的1/6了。

    优点

    (1)保证了ID生成的绝对递增有序

    (2)大大的降低了数据库的压力,ID生成可以做到每秒生成几万几十万个

    缺点

    (1)服务仍然是单点

    (2)如果服务挂了,服务重启起来之后,继续生成ID可能会不连续,中间出现空洞(服务内存是保存着0,1,2,3,4,5,数据库中max-id是5,分配到3时,服务重启了,下次会从6开始分配,4和5就成了空洞,不过这个问题也不大)

    (3)虽然每秒可以生成几万几十万个ID,但毕竟还是有性能上限,无法进行水平扩展

    改进方法

    单点服务的常用高可用优化方案是“备用服务”,也叫“影子服务”,所以我们能用以下方法优化上述缺点(1):


    如上图,对外提供的服务是主服务,有一个影子服务时刻处于备用状态,当主服务挂了的时候影子服务顶上。这个切换的过程对调用方是透明的,可以自动完成,常用的技术是vip+keepalived,具体就不在这里展开。

     

    【常见方法三:uuid】

    上述方案来生成ID,虽然性能大增,但由于是单点系统,总还是存在性能上限的。同时,上述两种方案,不管是数据库还是服务来生成ID,业务方Application都需要进行一次远程调用,比较耗时。有没有一种本地生成ID的方法,即高性能,又时延低呢?

    uuid是一种常见的方案:string ID =GenUUID();

    优点

    (1)本地生成ID,不需要进行远程调用,时延低

    (2)扩展性好,基本可以认为没有性能上限

    缺点

    (1)无法保证趋势递增

    (2)uuid过长,往往用字符串表示,作为主键建立索引查询效率低,常见优化方案为“转化为两个uint64整数存储”或者“折半存储”(折半后不能保证唯一性)

     

    【常见方法四:取当前毫秒数】

    uuid是一个本地算法,生成性能高,但无法保证趋势递增,且作为字符串ID检索效率低,有没有一种能保证递增的本地算法呢?

    取当前毫秒数是一种常见方案:uint64 ID = GenTimeMS();

    优点

    (1)本地生成ID,不需要进行远程调用,时延低

    (2)生成的ID趋势递增

    (3)生成的ID是整数,建立索引后查询效率高

    缺点

    (1)如果并发量超过1000,会生成重复的ID

    我去,这个缺点要了命了,不能保证ID的唯一性。当然,使用微秒可以降低冲突概率,但每秒最多只能生成1000000个ID,再多的话就一定会冲突了,所以使用微秒并不从根本上解决问题。

     

    【常见方法五:类snowflake算法】

    snowflake是twitter开源的分布式ID生成算法,其核心思想是:一个long型的ID,使用其中41bit作为毫秒数,10bit作为机器编号,12bit作为毫秒内序列号。这个算法单机每秒内理论上最多可以生成1000*(2^12),也就是400W的ID,完全能满足业务的需求。

    借鉴snowflake的思想,结合各公司的业务逻辑和并发量,可以实现自己的分布式ID生成算法。

    举例,假设某公司ID生成器服务的需求如下:

    (1)单机高峰并发量小于1W,预计未来5年单机高峰并发量小于10W

    (2)有2个机房,预计未来5年机房数量小于4个

    (3)每个机房机器数小于100台

    (4)目前有5个业务线有ID生成需求,预计未来业务线数量小于10个

    (5)…

    分析过程如下:

    (1)高位取从2016年1月1日到现在的毫秒数(假设系统ID生成器服务在这个时间之后上线),假设系统至少运行10年,那至少需要10年*365天*24小时*3600秒*1000毫秒=320*10^9,差不多预留39bit给毫秒数

    (2)每秒的单机高峰并发量小于10W,即平均每毫秒的单机高峰并发量小于100,差不多预留7bit给每毫秒内序列号

    (3)5年内机房数小于4个,预留2bit给机房标识

    (4)每个机房小于100台机器,预留7bit给每个机房内的服务器标识

    (5)业务线小于10个,预留4bit给业务线标识


    这样设计的64bit标识,可以保证:

    (1)每个业务线、每个机房、每个机器生成的ID都是不同的

    (2)同一个机器,每个毫秒内生成的ID都是不同的

    (3)同一个机器,同一个毫秒内,以序列号区区分保证生成的ID是不同的

    (4)将毫秒数放在最高位,保证生成的ID是趋势递增的

    缺点

    (1)由于“没有一个全局时钟”,每台服务器分配的ID是绝对递增的,但从全局看,生成的ID只是趋势递增的(有些服务器的时间早,有些服务器的时间晚)

    最后一个容易忽略的问题

    生成的ID,例如message-id/ order-id/ tiezi-id,在数据量大时往往需要分库分表,这些ID经常作为取模分库分表的依据,为了分库分表后数据均匀,ID生成往往有“取模随机性”的需求,所以我们通常把每秒内的序列号放在ID的最末位,保证生成的ID是随机的。

    又如果,我们在跨毫秒时,序列号总是归0,会使得序列号为0的ID比较多,导致生成的ID取模后不均匀。解决方法是,序列号不是每次都归0,而是归一个0到9的随机数,这个地方。

     

    一分钟了解互联网动静分离架构

    一、静态页面

    静态页面,是指互联网架构中,几乎不变的页面(或者变化频率很低),例如:

    • 首页等html页面

    • js/css等样式文件

    • jpg/apk等资源文件

    静态页面,有与之匹配的技术架构来加速,例如:

    • CDN

    • nginx

    • squid/varnish

     

    二、动态页面

    动态页面,是指互联网架构中,不同用户不同场景访问,都不一样的页面,例如:

    • 百度搜索结果页

    • 淘宝商品列表页

    • 速运个人订单中心页

    这些页面,不同用户,不同场景访问,大都会动态生成不同的页面。

     

    动态页面,有与之匹配的技术架构,例如:

    • 分层架构

    • 服务化架构

    • 数据库,缓存架构

     

    三、互联网动静分离架构

    动静分离是指,静态页面与动态页面分开不同系统访问的架构设计方法。

    一般来说:

    • 静态页面访问路径短,访问速度快,几毫秒

    • 动态页面访问路径长,访问速度相对较慢(数据库的访问,网络传输,业务逻辑计算),几十毫秒甚至几百毫秒,对架构扩展性的要求更高

    • 静态页面与动态页面以不同域名区分

     

    四、页面静态化

    既然静态页面访问快,动态页面生成慢,有没有可能,将原本需要动态生成的站点提前生成好,使用静态页面加速技术来访问呢?

    这就是互联网架构中的“页面静态化”优化技术。

     

    举例,如下图,58同城的帖子详情页,原本是需要动态生成的:

     

    • 浏览器发起http请求,访问/detail/12348888x.shtml 详情页

    • web-server层从RESTful接口中,解析出帖子id是12348888

    • service层通过DAO层拼装SQL语句,访问数据库

    • 最终获取数据,拼装html返回浏览器

     

    而“页面静态化”是指,将帖子ID为12348888的帖子12348888x.shtml提前生成好,由静态页面相关加速技术来加速:

    这样的话,将极大提升访问速度,减少访问时间,提高用户体验。

     

    五、页面静态化的适用场景

    页面静态化优化后速度会加快,那能不能所有的场景都使用这个优化呢?哪些业务场景适合使用这个架构优化方案呢?

     

    一切脱离业务的架构设计都是耍流氓,页面静态化,适用于:总数据量不大,生成静态页面数量不多的业务。例如:

    • 58速运的城市页只有几百个,就可以用这个优化,只需提前生成几百个城市的“静态化页面”即可

    • 一些二手车业务,只有几万量二手车库存,也可以提前生成这几万量二手车的静态页面

    • 像58同城这样的信息模式业务,有几十亿的帖子量,就太适合于静态化(碎片文件多,反而访问慢)

    六、总结

    “页面静态化”是一种将原本需要动态生成的站点提前生成静态站点的优化技术。

    总数据量不大,生成静态页面数量不多的业务,非常适合于“页面静态化”优化。

     

    数据库读写分离架构,为什么我不喜欢

    RD:单库数据量太大,数据库扛不住了,我要申请一个数据库从库,读写分离。

    DBA:数据量多少?

    RD:5000w左右。

    DBA:读写吞吐量呢?

    RD:读QPS约200,写QPS约30左右。

    对于互联网某些业务场景,并不是很喜欢数据库读写分离架构

    一、读写分离

    什么是数据库读写分离?

    答:一主多从,读写分离,主动同步,是一种常见的数据库架构,一般来说:

    • 主库,提供数据库写服务

    • 从库,提供数据库读服务

    • 主从之间,通过某种机制同步数据,例如mysql的binlog

    一个组从同步集群通常称为一个“分组”

     

    分组架构究竟解决什么问题?

    答:大部分互联网业务读多写少,数据库的读往往最先成为性能瓶颈,如果希望:

    • 线性提升数据库读性能

    • 通过消除读写锁冲突提升数据库写性能

    此时可以使用分组架构。

     

    一句话,分组主要解决“数据库读性能瓶颈”问题,在数据库扛不住读的时候,通常读写分离,通过增加从库线性提升系统读性能。

     

    二、水平切分

    什么是数据库水平切分?

    答:水平切分,也是一种常见的数据库架构,一般来说:

    • 每个数据库之间没有数据重合,没有类似binlog同步的关联

    • 所有数据并集,组成全部数据

    • 会用算法,来完成数据分割,例如“取模”

    一个水平切分集群中的每一个数据库,通常称为一个“分片”

     

    水平切分架构究竟解决什么问题?

    答:大部分互联网业务数据量很大,单库容量容易成为瓶颈,如果希望:

    • 线性降低单库数据容量

    • 线性提升数据库写性能

    此时可以使用水平切分架构。

     

    一句话总结,水平切分主要解决“数据库数据量大”问题,在数据库容量扛不住的时候,通常水平切分。

     

    三、为什么不喜欢读写分离

    对于互联网大数据量,高并发量,高可用要求高,一致性要求高,前端面向用户的业务场景,如果数据库读写分离:

    • 数据库连接池需要区分:读连接池,写连接池

    • 如果要保证读高可用,读连接池要实现故障自动转移

    • 有潜在的主库从库一致性问题

    • 如果面临的是“读性能瓶颈”问题,增加缓存可能来得更直接,更容易一点

    • 关于成本,从库的成本比缓存高不少

    • 对于云上的架构,以阿里云为例,主库提供高可用服务,从库不提供高可用服务

     

    所以,上述业务场景下,楼主建议使用缓存架构来加强系统读性能,替代数据库主从分离架构。

     

    当然,使用缓存架构的潜在问题:如果缓存挂了,流量全部压到数据库上,数据库会雪崩。不过幸好,云上的缓存一般都提供高可用的服务。

     

    四、总结

    • 读写分离,解决“数据库读性能瓶颈”问题

    • 水平切分,解决“数据库数据量大”问题

    • 对于互联网大数据量,高并发量,高可用要求高,一致性要求高,前端面向用户的业务场景,微服务缓存架构,可能比数据库读写分离架构更合适

    互联网分层架构的本质

     

    上图是一个典型的互联网分层架构:

    • 客户端层:典型调用方是browser或者APP

    • 站点应用层:实现核心业务逻辑,从下游获取数据,对上游返回html或者json

    • 数据-缓存层:加速访问存储

    • 数据-数据库层:固化数据存储

     

    如果实施了服务化,这个分层架构图可能是这样:

    中间多了一个服务层

     

    同一个层次的内部,例如端上的APP,以及web-server,也都有进行MVC分层:

    • view层:展现

    • control层:逻辑

    • model层:数据

     

    可以看到,每个工程师骨子里,都潜移默化的实施着分层架构。

     

    那么,互联网分层架构的本质究竟是什么呢?

    如果我们仔细思考会发现,不管是跨进程的分层架构,还是进程内的MVC分层,都是一个“数据移动”,然后“被处理”“被呈现”的过程,归根结底一句话:互联网分层架构,是一个数据移动,处理,呈现的过程,其中数据移动是整个过程的核心

     

    如上图所示:

    数据处理和呈现要CPU计算,CPU是固定不动的

    • db/service/web-server都部署在固定的集群上

    • 端上,不管是browser还是APP,也有固定的CPU处理

     

    数据是移动的

    • 跨进程移动:数据从数据库和缓存里,转移到service层,到web-server层,到client层

    • 同进程移动:数据从model层,转移到control层,转移到view层

     

    数据要移动,所以有两个东西很重要:

    • 数据传输的格式

    • 数据在各层次的形态

     

    先看数据传输的格式,即协议很重要:

    • service与db/cache之间,二进制协议/文本协议是数据传输的载体

    • web-server与service之间,RPC的二进制协议是数据传输的载体

    • client和web-server之间,http协议是数据传输的载体

     

    再看数据在各层次的形态,以用户数据为例:

    • db层,数据是以“行”为单位存在的row(uid, name, age)

    • cache层,数据是以kv的形式存在的kv(uid -> User)

    • service层,会把row或者kv转化为对程序友好的User对象

    • web-server层,会把对程序友好的User对象转化为对http友好的json对象

    • client层:最终端上拿到的是json对象

     

    结论:互联网分层架构的本质,是数据的移动。

     

    为什么要说这个,这将会引出“分层架构演进”的核心原则与方法:

    • 让上游更高效的获取与处理数据,复用

    • 让下游能屏蔽数据的获取细节,封装

     

    弄清楚这个原则与方法,再加上一些经验积累,就能回答网友经常在评论中提出的这些问题了:

    • 是否需要引入DAO层,什么时机引入

    • 是否需要服务化,什么时机服务化

    • 是否需要抽取通用中台业务,什么时机抽取

    • 是否需要前后端分离,什么时机分离

    (网友们的这些提问,其实很难回答。在不了解业务发展阶段,业务规模,数据量并发量的情况下,妄下YES或NO的结论,本身就是不负责任的。)

    更具体的分层架构演进细节,下一篇和大家细究。

     

    总结

      • 互联网分层架构的本质,是数据的移动

      • 互联网分层架构中,数据的传输格式(协议)与数据在各层次的形态很重要

      • 互联网分层架构演进的核心原则与方法:封装与复用

     

    互联网分层架构之-DAO与服务化

    互联网分层架构的本质,是数据的移动。

    互联网分层架构演进的核心原则:

    • 上游更高效的获取与处理数据,复用

    • 下游能屏蔽数据的获取细节,封装

    本文主要解答两个问题:

    • 后端架构,什么时候进行DAO层的抽象

    • 后端架构,什么时候进行数据服务层的抽象

     

    核心问题一:什么时候进行DAO层的抽象

    一个业务系统最初的后端结构如上:

    • web-server层从db层获取数据并进行加工处理

    • db层存储数据

     

    此时,web-server层如何获取底层的数据呢?

    web-server层获取数据的一段伪代码如上,不用纠结代码的细节,也不用纠结不同编程语言与不同数据库驱动的差异,其获取数据的过程大致为:

    • 创建一个与数据库的连接,初始化资源

    • 根据业务拼装一个SQL语句

    • 通过连接执行SQL语句,并获得结果集

    • 通过游标遍历结果集,取出每行数据,亦可从每行数据中取出属性数据

    • 关闭数据库连接,回收资源

     

    如果业务不复杂,这段代码写1次2次还可以,但如果业务越来越复杂,每次都这么获取数据,就略显低效了,有大量冗余、重复、每次必写的代码。

     

    如何让数据的获取更加高效快捷呢?

     

    通过技术手段实现:

    • 与类的映射

    • 属性与成员的映射

    • SQL与函数的映射

    绝大部分公司正在用的ORM,DAO等技术,就是一种分层抽象,可以提高数据获取的效率,屏蔽连接,游标,结果集这些复杂性。

     

     

    结论

    手写代码从DB中获取数据,成为通用痛点的时候,应该抽象出DAO层,简化数据获取过程,提高数据获取效率,向上游屏蔽底层的复杂性。

     

    核心问题二:什么时候要进行数据服务层的抽象

    抽象出DAO层之后,系统架构并不会一成不变:

    • 随着业务越来越复杂,业务系统会不断进行垂直拆分

    • 随着数据量越来越大,数据库会进行水平切分

    • 随着读并发的越来越大,会增加缓存降低数据库的压力

     

    于是系统架构变成了这个样子:

    业务系统垂直拆分数据库水平切分缓存这些都是常见的架构优化手段。

     

    此时,web-server层如何获取底层的数据呢?

    根据楼主的经验,以用户数据为例,流程一般是这样的:

    • 先查缓存:先用uid尝试从缓存获取数据,如果cache hit,数据获取成功,返回User实体,流程结束

    • 确定路由:如果cache miss,先查询路由配置,确定uid落在哪个数据库实例的哪个库上

    • 查询DB:通过DAO从对应库获取uid对应的数据实体User

    • 插入缓存:将kv(uid, User)放入缓存,以便下次缓存查询数据能够命中缓存

     

    如果业务不复杂,这段代码写1次2次还可以,但如果业务越来越复杂,每次都这么获取数据,就略显低效了,有大量冗余、重复、每次必写的代码。

     

    特别的,业务垂直拆分成非常多的子系统之后:

    • 一旦底层有稍许变化,所有上游的系统都需要升级修改

    • 子系统之间很可能出现代码拷贝

    • 一旦拷贝代码,出现一个bug,多个子系统都需要升级修改

     

    不相信业务会垂直拆分成多个子系统?举两个例子:

    • 58同城有招聘、房产、二手、二手车、黄页等5大头部业务,都需要访问用户数据

    • 58到家有月嫂、保姆、丽人、速运、平台等多个业务,也都需要访问用户数据

    如果每个子系统都需要关注缓存,分库,读写分离的复杂性,调用层会疯掉的。

     

    如何让数据的获取更加高效快捷呢?

    服务化,数据服务层的抽象势在必行。

     

    通过抽象数据服务层:

    • web-server层可以通过RPC接口,像调用本地函数一样调用远端的数据

    • 数据服务层,只有这一处需要关注缓存,分库,读写分离这些复杂性

    结论

    业务越来越复杂,垂直拆分的系统越来越多,数据库实施了水平切分,数据层实施了缓存加速之后,底层数据获取复杂性成为通用痛点的时候,应该抽象出数据服务层,简化数据获取过程,提高数据获取效率,向上游屏蔽底层的复杂性。

    互联网分层架构是一个很有意思的问题,服务化的引入,并不是越早越好:

    • 请求处理时间可能会增加

    • 运维可能会更加复杂

    • 定位问题可能会更加麻烦

    千万别鲁莽的在“微服务”大流之下,草率的进行微服务改造,看似“高大上架构”的背后,隐藏着更多并未接触过的“大坑”。还是那句话,架构和业务的特点和阶段有关:一切脱离业务的架构设计,都是耍流氓

    这一篇先到这里,分层架构,还有很多内容要和大家聊:

    • 后端架构,是否需要抽取中台业务,什么时机抽取

    • 后端架构,是否需要前后端分离,什么时机分离

    • 前端架构,如何进行分层实践

    末了,再次强调下,互联网分层架构的本质,是数据的移动。

    互联网分层架构演进的核心原则,是让上游更高效的获取与处理数据,让下游能屏蔽掉数据的复杂性获取细节。

     

    互联网架构为什么要做服务化?

    近期参加一些业界的技术大会,“微服务架构”的话题非常之火,也在一些场合聊过服务化架构实践,最近几期文章期望用通俗易懂的语言聊聊了个人对服务化以及微服务架构的理解,希望能给大伙一些启示。如果有遗漏,也欢迎大家补充。

     

    一、互联网高可用架构,为什么要服务化?

    【服务化之前高可用架构】

    在服务化之前,互联网的高可用架构大致是这样一个架构:


    (1)用户端是浏览器browser,APP客户端

    (2)后端入口是高可用的nginx集群,用于做反向代理

    (3)中间核心是高可用的web-server集群,研发工程师主要编码工作就是在这一层

    (4)后端存储是高可用的db集群,数据存储在这一层

     


    更典型的,web-server层是通过DAO/ORM等技术来访问数据库的。

     

    可以看到,最初都是没有服务层的,此时架构会碰到一些什么痛点呢?

    【架构痛点一:代码到处拷贝】

    举一个最常见的业务的例子->用户数据的访问,绝大部分公司都有一个数据库存储用户数据,各个业务都有访问用户数据的需求:


    在有用户服务之前,各个业务线都是自己通过DAO写SQL访问user库来存取用户数据,这无形中就导致了代码的拷贝。

     

    【架构痛点二:复杂性扩散】

    随着并发量的越来越高,用户数据的访问数据库成了瓶颈,需要加入缓存来降低数据库的读压力,于是架构中引入了缓存,由于没有统一的服务层,各个业务线都需要关注缓存的引入导致的复杂性:


    对于用户数据的写请求,所有业务线都要升级代码:

    (1)先淘汰cache

    (2)再写数据

    对于用户数据的读请求,所有业务线也都要升级代码:

    (1)先读cache,命中则返回

    (2)没命中则读数据库

    (3)再把数据放入cache

    这个复杂性是典型的“业务无关”的复杂性,业务方需要被迫升级。

     

    随着数据量的越来越大,数据库需要进行水平拆分,于是架构中又引入了分库分表,由于没有统一的服务层,各个业务线都需要关注分库分表的引入导致的复杂性:


    这个复杂性也是典型的“业务无关”的复杂性,业务方需要被迫升级。

    包括bug的修改,发现一个bug,多个地方都需要修改。

     

    【架构痛点三:库的复用与耦合】

    服务化并不是唯一的解决上述两痛点的方法,抽象出统一的“库”是最先容易想到的解决:

    (1)代码拷贝

    (2)复杂性扩散

    的方法。抽象出一个user.so,负责整个用户数据的存取,从而避免代码的拷贝。至于复杂性,也只有user.so这一个地方需要关注了。

     

    解决了旧的问题,会引入新的问题,库的版本维护与业务线之间代码的耦合:

    业务线A将user.so由版本1升级至版本2,如果不兼容业务线B的代码,会导致B业务出现问题;

    业务线A如果通知了业务线B升级,则是的业务线B会无故做一些“自身业务无关”的升级,非常郁闷。当然,如果各个业务线都是拷贝了一份代码则不存在这个问题。

     

    【架构痛点四:SQL质量得不到保障,业务相互影响】

    业务线通过DAO访问数据库:


    本质上SQL语句还是各个业务线拼装的,资深的工程师写出高质量的SQL没啥问题,经验没有这么丰富的工程师可能会写出一些低效的SQL,假如业务线A写了一个全表扫描的SQL,导致数据库的CPU100%,影响的不只是一个业务线,而是所有的业务线都会受影响。

     

    【架构痛点五:疯狂的DB耦合】

    业务线不至访问user数据,还会结合自己的业务访问自己的数据:


    典型的,通过join数据表来实现各自业务线的一些业务逻辑。

    这样的话,业务线A的table-user与table-A耦合在了一起,业务线B的table-user与table-B耦合在了一起,业务线C的table-user与table-C耦合在了一起,结果就是:table-user,table-A,table-B,table-C都耦合在了一起。

    随着数据量的越来越大,业务线ABC的数据库是无法垂直拆分开的,必须使用一个大库(疯了,一个大库300多个业务表 =_=)。

     

    【架构痛点六:…

     

    二、服务化解决什么问题?

    为了解决上面的诸多问题,互联网高可用分层架构演进的过程中,引入了“服务层”。


    以上文中的用户业务为例,引入了user-service,对业务线响应所用用户数据的存取。引入服务层有什么好处,解决什么问题呢?

    【好处一:调用方爽】

    有服务层之前:业务方访问用户数据,需要通过DAO拼装SQL访问

    有服务层之后:业务方通过RPC访问用户数据,就像调用一个本地函数一样,非常之爽

    User = UserService::GetUserById(uid);

    传入一个uid,得到一个User实体,就像调用本地函数一样,不需要关心序列化,网络传输,后端执行,网络传输,范序列化等复杂性。

     

    【好处二:复用性,防止代码拷贝】

    这个不展开叙述,所有user数据的存取,都通过user-service来进行,代码只此一份,不存在拷贝。

    升级一处升级,bug修改一处修改。

     

    【好处三:专注性,屏蔽底层复杂度】


    在没有服务层之前,所有业务线都需要关注缓存、分库分表这些细节。

     


    在有了服务层之后,只有服务层需要专注关注底层的复杂性了,向上游屏蔽了细节。

     

    【好处四:SQL质量得到保障】


    原来是业务向上游直接拼接SQL访问数据库。

     


    有了服务层之后,所有的SQL都是服务层提供的,业务线不能再为所欲为了。底层服务对于稳定性的要求更好的话,可以由更资深的工程师维护,而不是像原来SQL难以收口,难以控制。

     

    【好处五:数据库解耦】


    原来各个业务的数据库都混在一个大库里,相互join,难以拆分。


    服务化之后,底层的数据库被隔离开了,可以很方便的拆分出来,进行扩容。

     

    【好处六:提供有限接口,无限性能】

    在服务化之前,各业务线上游想怎么操纵数据库都行,遇到了性能瓶颈,各业务线容易扯皮,相互推诿。

    服务化之后,服务只提供有限的通用接口,理论上服务集群能够提供无限性能,性能出现瓶颈,服务层一处集中优化。

     

    业务层是否也需要服务化?

    观点:

    • 互联网分层架构的本质,是数据的移动

    • 互联网分层架构演进的核心原则:是让上游更高效的获取与处理数据,让下游能屏蔽数据的获取细节

    • 当手写代码从DB中获取数据,成为通用痛点的时候,就应该抽象出DAO层,简化数据获取过程,提高数据获取效率,向上游屏蔽底层的复杂性

    • 当业务越来越复杂,垂直拆分的系统越来越多,数据库实施了水平切分,数据层实施了缓存加速之后,底层数据获取复杂性成为通用痛点的时候,就应该抽象出数据服务层,简化数据获取过程,提高数据获取效率,向上游屏蔽底层的复杂性

    文本将要解答的问题是:

    • 基础数据的访问需要服务化,业务层是否需要服务化

    • 如果需要服务化,什么时候服务化

     

    基础数据的访问服务化之后,一个业务系统的后端架构如上:

    • web-server通过RPC接口,从基础数据service获取数据

    • 基础数据service通过DAO,从db/cache获取数据

    • db/cache存储数据

     

    随着时间的推移,系统架构并不会一成不变:

    • 随着业务越来越复杂,业务会不断进行垂直拆分

    • 随着数据越来越复杂,基础数据service也会越来越多

     

    于是系统架构变成了上图这个样子,业务垂直拆分,有若干个基础数据服务:

    • 垂直业务要通过多个RPC接口访问不同的基础数据service,service共有是服务化的特征

    • 每个基础数据service访问自己的数据存储,数据私有也是服务化的特征

     

    这个架构图中的依赖关系是不是看上去很别扭?

    • 基础数据service与存储层之前连接关系很清晰

    • 业务web-server层与基础数据service层之间的连接关系错综复杂,变成了蜘蛛网

     

    再举一个更具体的例子,58同城列表页web-server如何获取底层的数据?

    • 首先调用商业基础service,获取商业广告帖子数据,用于顶部置顶/精准的广告帖子展示

    • 再调用搜索基础service,获取自然搜索帖子数据,用于中部自然搜索帖子展示

    • 再调用推荐基础service,获取推荐帖子数据,用于底部推荐帖子展示

    • 再调用用户基础service,获取用户数据,用于右侧用户信息展示

     

    如果只有一个列表页这么写还行,但如果有招聘、房产、二手、二手车、黄页…等多个大部分是共性数据,少部分是个性数据的列表页,每次都这么获取数据,就略显低效了,有大量冗余、重复、每次必写的代码。

     

    特别的,不同业务上游列表页都依赖于底层若干相同服务:

    • 一旦一个服务RPC接口有稍许变化,所有上游的系统都需要升级修改

    • 子系统之间很可能出现代码拷贝

    • 一旦拷贝代码,出现一个bug,多个子系统都需要升级修改

     

    如何让数据的获取更加高效快捷呢?

    业务服务化,通用业务服务层的抽象势在必行。

     

    通过抽象通用业务服务层,例如58同城“通用列表服务”:

    • web-server层,可以通过RPC接口,像调用本地函数一样,调用通用业务service,一次性获取所有通用数据

    • 通用业务service,也可以通过多次调用基础数据service提供的RPC接口,分别获取数据,底层数据获取的复杂性,全都屏蔽在了此处 

     

    是不是连接关系也看起来更清晰?

     

    这样的好处是:

    • 复杂的从基础服务获取数据代码,只有在通用业务service处写了一次,没有代码拷贝

    • 底层基础数据service接口发生变化,只有通用业务service一处需要升级修改

    • 如果有bug,不管是底层基础数据service的bug,还是通用业务service的bug,都只有一处需要升级修改

    • 业务web-server获取数据更便捷,获取所有数据,只需一个RPC接口调用

     

    结论

    业务越来越复杂,垂直拆分的系统越来越多,基础数据服务越来越多,底层数据获取复杂性成为通用痛点的时候,应该抽象出通用业务服务,简化数据获取过程,提高数据获取效率,向上游屏蔽底层的复杂性。

     

    最后再强调两点:

    • 是否需要抽象通用业务服务,和业务复杂性,以及业务发展阶段有关,不可一概而论

    • 需要抽象什么通用业务服务,和具体业务相关

     

    互联网分层架构,为啥要前后端分离?

    通用业务服务化之后,系统的典型后端结构如上:

    • web-server通过RPC接口,从通用业务服务获取数据

    • biz-service通过RPC接口,从多个基础数据service获取数据

    • 基础数据service通过DAO,从独立db/cache获取数据

    • db/cache存储数据

     

    随着时间的推移,系统架构并不会一成不变,业务越来越复杂,改版越来越多,此时web-server层虽然使用了MVC架构,但以下诸多痛点是否似曾相识?

    • 产品追求绚丽的效果,并对设备兼容性要求高,这些需求不断折磨着使用MVC的Java工程师们(本文以Java举例)

    • 不管是PC,还是手机H5,还是APP,应用前端展现的变化频率远远大于后端逻辑的变化频率(感谢那些喜欢做改版的产品经理),改velocity模版并不是Java工程师喜欢和擅长的工作

     

    此时,为了缓解这些问题,一般会成立单独的前端FE部门,来负责交互与展现的研发,其职责与后端Java工程师分离开,但痛点依然没有完全解决:

    • 一点点展现的改动,需要Java工程师们重新编译,打包,上线,重启tomcat,效率极低

    • 原先Java工程师负责所有MVC的研发工作,现在分为Java和FE两块,需要等前端和后端都完成研发,才能一起调试整体效果,不仅增加了沟通成本,任何一块出问题,都可能导致项目延期

     

    更具体的,看一个这样的例子,最开始产品只有PC版本,此时其系统分层架构如下:

    客户端,web-server,service,非常清晰。

     

    随着业务的发展,产品需要新增Mobile版本,Mobile版本和PC版本大部分业务逻辑都一样,唯一的区别是屏幕比较小

    • 信息展现的条数会比较少,即调用service服务时,传入的参数会不一样

    • 产品功能会比较少,大部分service的调用一样,少数service不需要调用

    • 展现,交互会有所区别

     

    由于工期较紧,Mobile版本的web-server一般怎么来呢?

    没错,把PC版本的工程拷贝一份,然后再做小量的修改

    • service调用的参数有些变化

    • 大部分service的调用一样,少数service的调用去掉

    • 修改展现,交互相关的代码

     

    业务继续发展,产品又需要新增APP版本,APP版本和Mobile版本业务逻辑完全相同,唯一的区别是:

    • Mobile版本返回html格式的数据,APP版本返回json格式的数据,然后进行本地渲染

     

    由于工期较紧,APP版本的web-server一般怎么来呢?

    没错,把Mobile版本的工程拷贝一份,然后再做小量的修改

    • 把拼装html数据的代码,修改为拼装json数据

     

    这么迭代,演化,发展,架构会变成这个样子:

    • ,是PC,Mobile,APP

    • web-server接入,是PC站,M站,APP站

    • 服务层,通用的业务服务,以及基础数据服务

     

    这个架构图中的依赖关系是不是看上去很别扭?

    • 端到web-server之间连接关系很清晰

    • web-server与service之间的连接关系变成了蜘蛛网

     

    PC/H5/APP的web-server层大部分业务是相同的,只有少数的逻辑/展现/交互不一样:

    • 一旦一个服务RPC接口有稍许变化,所有web-server系统都需要升级修改

    • web-server之间存在大量代码拷贝

    • 一旦拷贝代码,出现一个bug,多个子系统都需要升级修改

     

    如何让数据的获取更加高效快捷,如何让数据生产与数据展现解耦分离呢?

    前后端分离的分层抽象势在必行。

    通过前后端分离分层抽象:

    • 站点展示层,node.js,负责数据的展现与交互,由FE维护

    • 站点数据层,web-server,负责业务逻辑与json数据接口的提供,由Java工程师维护

     

    这样的好处是:

    • 复杂的业务逻辑与数据生成,只有在站点数据层处写了一次,没有代码拷贝

    • 底层service接口发生变化,只有站点数据层一处需要升级修改

    • 底层service如果有bug,只有站点数据层一处需要升级修改

    • 站点展现层可以根据产品的不同形态,传入不同的参数,调用不同的站点数据层接口

     

    除此之外:

    • 产品追求绚丽的效果,并对设备兼容性要求高,不再困扰Java工程师,由更专业的FE对接

    • 一点点展现的改动,不再需要Java工程师们重新编译,打包,上线,重启tomcat

    • 约定好json接口后,Java和FE分开开发,FE可以用mock的接口自测,不再等待一起联调

     

    结论

    业务越来越复杂,端上的产品越来越多,展现层的变化越来越快越来越多,站点层存在大量代码拷贝,数据获取复杂性成为通用痛点的时候,就应该进行前后端分离分层抽象,简化数据获取过程,提高数据获取效率,向上游屏蔽底层的复杂性。

    最后再强调两点:

    • 是否需要前后端分离,和业务复杂性,以及业务发展阶段有关,不可一概而论

    • 本文强调的前后端分离的思路,实际情况下有多种实现方式,文章并没有透彻展开实现细节

    展开全文
  • 如何写好部门工作总结

    千次阅读 2006-02-13 10:36:00
    客服部经理最重要的是领导好部门的一班人。至于总结,那跟常规总结也没多大区别的。主要是成绩,简单写问题,简单写今后努力方向或工作打算。工作总结的写法如下 总结,就是把某一时期已经做过的工作,进行一次全面...
    客服部经理最重要的是领导好部门的一班人。至于总结,那跟常规总结也没多大区别的。主要是成绩,简单写问题,简单写今后努力方向或工作打算。

    工作总结的写法如下

    总结,就是把某一时期已经做过的工作,进行一次全面系统的总检查、总评价,进行一次具体的总分析、总研究;也就是看看取得了哪些成绩,存在哪些缺点和不足,有什么经验、提高。
    (一)基本情况。
    1.总结必须有情况的概述和叙述,有的比较简单,有的比较详细。这部分内容主要是对工作的主客观条件、有利和不利条件以及工作的环境和基础等进行分析。

    2.成绩和缺点。这是总结的中心。总结的目的就是要肯定成绩,找出缺点。成绩有哪些,有多大,表现在哪些方面,是怎样取得的;缺点有多少,表现在哪些方面,是什么性质的,怎样产生的,都应讲清楚。
    3.经验和教训。做过一件事,总会有经验和教训。为便于今后的工作,须对以往工作的经验和教训进行分析、研究、概括、集中,并上升到理论的高度来认识。
    4.今后的打算。根据今后的工作任务和要求,吸取前一时期工作的经验和教训,明确努力方向,提出改进措施等。
    (二)写好总结需要注意的问题
    1.总结前要充分占有材料。最好通过不同的形式,听取各方面的意见,了解有关情况,或者把总结的想法、意图提出来,同各方面的干部、群众商量。一定要避免领导出观点,到群众中找事实的写法。
    2.一定要实事求是,成绩不夸大,缺点不缩小,更不能弄虚作假。这是分析、得出教训的基础。
    3.条理要清楚。总结是写给人看的,条理不清,人们就看不下去,即使看了也不知其所以然,这样就达不到总结的目的。
    4.要剪裁得体,详略适宜。材料有本质的,有现象的;有重要的,有次要的,写作时要去芜存精。总结中的问题要有主次、详略之分,该详的要详,该略的要略。
    5.总结的具体写作,可先议论,然后由专人写出初稿,再行讨论、修改。最好由主要负责人执笔,或亲自主持讨论、起草、修改。
    展开全文
  • 华为C语言编程规范(精华总结

    万次阅读 多人点赞 2020-03-24 09:48:55
    4、每一个 .c 文件应有一个同名 .h 文件,用于声明需要对外公开的接口 如果一个.c文件不需要对外公布任何接口,则其就不应当存在,除非它是程序的入口,如main函数所在的文件。 现有某些产品中,习惯一个.c文件对应...

    目录

    1、代码总体原则

    2、头文件

    2、函数

    3、标识符命名与定义

    4、变量

    5、宏、常量

    6、表达式

    7、注释

    8、排版与格式

    9、代码编辑编译 


    “编写程序应该以人为本,计算机第二。”                                                               

                                                                                                                                           ——Steve McConnell

    “无缘进华为,但可以用华为的标准要求自己。”                                                               

                                                                                                                                           ——不脱发的程序猿


    1、代码总体原则

    1、清晰第一 

    清晰性是易于维护、易于重构的程序必需具备的特征。代码首先是给人读的,好的代码应当可以像文章一样发声朗诵出来。

    目前软件维护期成本占整个生命周期成本的40%~90%。根据业界经验,维护期变更代码的成本,小型系统是开发期的5倍,大型系统(100万行代码以上)可以达到100倍。业界的调查指出,开发组平均大约一半的人力用于弥补过去的错误,而不是添加新的功能来帮助公司提高竞争力。

    一般情况下,代码的可阅读性高于性能,只有确定性能是瓶颈时,才应该主动优化。

    2、简洁为美

    简洁就是易于理解并且易于实现。代码越长越难以看懂,也就越容易在修改时引入错误。写的代码越多,意味着出错的地方越多,也就意味着代码的可靠性越低。因此,我们提倡大家通过编写简洁明了的代码来提升代码可靠性。

    废弃的代码(没有被调用的函数和全局变量)要及时清除,重复代码应该尽可能提炼成函数。

    3、选择合适的风格,与代码原有风格保持一致

    产品所有人共同分享同一种风格所带来的好处,远远超出为了统一而付出的代价。在公司已有编码规范的指导下,审慎地编排代码以使代码尽可能清晰,是一项非常重要的技能。 如果重构/ / 修改其他风格的代码时,比较明智的做法是根据 现有 代码 的 现有风格继续编写代码,或者使用格式转换工具进行转换成公司内部风格。


    2、头文件

    对于C语言来说,头文件的设计体现了大部分的系统设计。 不合理的头文件布局是编译时间过长的根因,不合理的头文件实际上反映了不合理的设计。

    1、头文件中适合放置接口的声明,不适合放置实现

    头文件是模块(Module)或单元(Unit)的对外接口。头文件中应放置对外部的声明,如对外提供的函数声明、宏定义、类型定义等。

    要求:

    • 内部使用的函数(相当于类的私有方法)声明不应放在头文件中。
    • 内部使用的宏、枚举、结构定义不应放入头文件中。
    • 变量定义不应放在头文件中,应放在.c文件中。
    • 变量的声明尽量不要放在头文件中,亦即尽量不要使用全局变量作为接口。变量是模块或单元的内部实现细节,不应通过在头文件中声明的方式直接暴露给外部,应通过函数接口的方式进行对外暴露。 即使必须使用全局变量,也只应当在.c中定义全局变量,在.h中仅声明变量为全局的。

    2、头文件应当职责单一,切忌依赖复杂

    头文件过于复杂,依赖过于复杂是导致编译时间过长的主要原因。很多现有代码中头文件过大,职责过多,再加上循环依赖的问题,可能导致为了在.c中使用一个宏,而包含十几个头文件。

    错误示例:某平台定义WORD类型的头文件:

    #include <VXWORKS.H>
    #include <KERNELLIB.H>
    #include <SEMLIB.H>
    #include <INTLIB.H>
    #include <TASKLIB.H>
    #include <MSGQLIB.H>
    #include <STDARG.H>
    #include <FIOLIB.H>
    #include <STDIO.H>
    #include <STDLIB.H>
    #include <CTYPE.H>
    #include <STRING.H>
    #include <ERRNOLIB.H>
    #include <TIMERS.H>
    #include <MEMLIB.H>
    #include <TIME.H>
    #include <WDLIB.H>
    #include <SYSLIB.H>
    #include <TASKHOOKLIB.H>
    #include <REBOOTLIB.H>
    …
    typedef unsigned short WORD;
    …

    这个头文件不但定义了基本数据类型WORD,还包含了stdio.h syslib.h等等不常用的头文件。如果工程中有10000个源文件,而其中100个源文件使用了stdio.h的printf,由于上述头文件的职责过于庞大,而WORD又是每一个文件必须包含的,从而导致stdio.h/syslib.h等可能被不必要的展开了9900次,大大增加了工程的编译时间。

    3、头文件应向稳定的方向包含 

    头文件的包含关系是一种依赖,一般来说,应当让不稳定的模块依赖稳定的模块,从而当不稳定的模块发生变化时,不会影响(编译)稳定的模块。

    就我们的产品来说,依赖的方向应该是: 产品依赖于平台,平台依赖于标准库。某产品线平台的代码中已经包含了产品的头文件,导致平台无法单独编译、发布和测试,是一个非常糟糕的反例。除了不稳定的模块依赖于稳定的模块外,更好的方式是两个模块共同依赖于接口,这样任何一个模块的内部实现更改都不需要重新编译另外一个模块。在这里,我们假设接口本身是最稳定的。

    4、每一个 .c 文件应有一个同名 .h 文件,用于声明需要对外公开的接口

    如果一个.c文件不需要对外公布任何接口,则其就不应当存在,除非它是程序的入口,如main函数所在的文件。

    现有某些产品中,习惯一个.c文件对应两个头文件,一个用于存放对外公开的接口,一个用于存放内部需要用到的定义、声明等,以控制.c文件的代码行数。编者不提倡这种风格。这种风格的根源在于源文件过大,应首先考虑拆分.c文件,使之不至于太大。另外,一旦把私有定义、声明放到独立的头文件中,就无法从技术上避免别人include之,难以保证这些定义最后真的只是私有的。

    5、禁止头文件循环依赖

    头文件循环依赖,指a.h包含b.h,b.h包含c.h,c.h包含a.h之类导致任何一个头文件修改,都导致所有包含了a.h/b.h/c.h的代码全部重新编译一遍。而如果是单向依赖,如a.h包含b.h,b.h包含c.h,而c.h不包含任何头文件,则修改a.h不会导致包含了b.h/c.h的源代码重新编译。

    6、  .c/.h文件禁止包含用不到的头文件

    很多系统中头文件包含关系复杂,开发人员为了省事起见,可能不会去一一钻研,直接包含一切想到的头文件,甚至有些产品干脆发布了一个god.h,其中包含了所有头文件,然后发布给各个项目组使用,这种只图一时省事的做法,导致整个系统的编译时间进一步恶化,并对后来人的维护造成了巨大的麻烦。

    7、  头文件应当自包含 

    简单的说,自包含就是任意一个头文件均可独立编译。如果一个文件包含某个头文件,还要包含另外一个头文件才能工作的话,就会增加交流障碍,给这个头文件的用户增添不必要的负担。

    示例:如果a.h不是自包含的,需要包含b.h才能编译,会带来的危害:每个使用a.h头文件的.c文件,为了让引入的a.h的内容编译通过,都要包含额外的头文件b.h。额外的头文件b.h必须在a.h之前进行包含,这在包含顺序上产生了依赖。

    注意:该规则需要与“.c/.h文件禁止包含用不到的头文件”规则一起使用,不能为了让a.h自包含,而在a.h中包含不必要的头文件。a.h要刚刚可以自包含,不能在a.h中多包含任何满足自包含之外的其他头文件。

    8、总是编写内部 #include 保护符( #define  保护)

    多次包含一个头文件可以通过认真的设计来避免。如果不能做到这一点,就需要采取阻止头文件内容被包含多于一次的机制。通常的手段是为每个文件配置一个宏,当头文件第一次被包含时就定义这个宏,并在头文件被再次包含时使用它以排除文件内容。所有头文件都应当使用#define 防止头文件被多重包含,命名格式为FILENAME_H,为了保证唯一性,更好的命名是PROJECTNAME_PATH_FILENAME_H。

    注:没有在宏最前面加上单下划线"_",是因为一般以单下划线"_"和双下划线"__"开头的标识符为ANSIC等使用,在有些静态检查工具中,若全局可见的标识符以"_"开头会给出告警。

    定义包含保护符时,应该遵守如下规则:

    • 保护符使用唯一名称;

    • 不要在受保护部分的前后放置代码或者注释。

    正确示例:假定VOS工程的timer模块的timer.h,其目录为VOS/include/timer/timer.h,应按如下方式保护:

    #ifndef VOS_INCLUDE_TIMER_TIMER_H
    #define VOS_INCLUDE_TIMER_TIMER_H
    ...
    #endif
    
    也可以使用如下简单方式保护:
    
    #ifndef TIMER_H
    #define TIMER_H
    ...
    #endif

    例外情况:头文件的版权声明部分以及头文件的整体注释部分(如阐述此头文件的开发背景、使用注意事项等)可以放在保护符(#ifndef XX_H)前面。

    9、禁止在头文件中定义变量

    在头文件中定义变量,将会由于头文件被其他.c文件包含而导致变量重复定义。

    10、只能通过包含头文件的方式使用其他 .c 提供的接口,禁止在.c 中通过 extern 的方式使用外部函数接口、变量

    若a.c使用了b.c定义的foo()函数,则应当在b.h中声明extern int foo(int input);并在a.c中通过#include <b.h>来使用foo。禁止通过在a.c中直接写extern int foo(int input);来使用foo,后面这种写法容易在foo改变时可能导致声明和定义不一致。

    11、禁止在 extern "C" 中包含头文件

    在extern "C"中包含头文件,会导致extern "C"嵌套,Visual Studio对extern "C"嵌套层次有限制,嵌套层次太多会编译错误。
    在extern "C"中包含头文件,可能会导致被包含头文件的原有意图遭到破坏。

    错误示例:

    extern “C”
    {
    #include “xxx.h”
    ...
    }
    

    正确示例:

    #include “xxx.h”
    extern “C”
    {
    ...
    } 

    12、一个模块通常包含多个 .c 文件,建议放在同一个目录下,目录名即为模块名。为方便外部使用者,建议每一个模块提供一个 .h ,文件名为目录名

    需要注意的是,这个.h并不是简单的包含所有内部的.h,它是为了模块使用者的方便,对外整体提供的模块接口。以Google test(简称GTest)为例,GTest作为一个整体对外提供C++单元测试框架,其1.5版本的gtest工程下有6个源文件和12个头文件。但是它对外只提供一个gtest.h,只要包含gtest.h即可使用GTest提供的所有对外提供的功能,使用者不必关系GTest内部各个文件的关系,即使以后GTest的内部实现改变了,比如把一个源文件c拆成两个源文件,使用者也不必关心,甚至如果对外功能不变,连重新编译都不需要。对于有些模块,其内部功能相对松散,可能并不一定需要提供这个.h,而是直接提供各个子模块或者.c的头文件。

    比如产品普遍使用的VOS,作为一个大模块,其内部有很多子模块,他们之间的关系相对比较松散,就不适合提供一个vos.h。而VOS的子模块,如Memory(仅作举例说明,与实际情况可能有所出入),其内部实现高度内聚,虽然其内部实现可能有多个.c和.h,但是对外只需要提供一个Memory.h声明接口。

    13、如果一个模块包含多个子模块,则建议每一个子模块提供一个对外的 .h,文件名为子模块名

    降低接口使用者的编写难度

    14、头文件不要使用非习惯用法的扩展名,如 .inc 

    目前很多产品中使用了.inc作为头文件扩展名,这不符合c语言的习惯用法。在使用.inc作为头文件扩展名的产品,习惯上用于标识此头文件为私有头文件。但是从产品的实际代码来看,这一条并没有被遵守,一个.inc文件被多个.c包含比比皆是。

    除此之外,使用.inc还导致source insight、Visual stduio等IDE工具无法识别其为头文件,导致很多功能不可用,如“跳转到变量定义处”。虽然可以通过配置,强迫IDE识别.inc为头文件,但是有些软件无法配置,如Visual Assist只能识别.h而无法通过配置识别.inc。

    15、同一产品统一包含头文件排列方式

    常见的包含头文件排列方式:功能块排序、文件名升序、稳定度排序。

    正确示例1:以升序方式排列头文件可以避免头文件被重复包含:

    #include <a.h>
    #include <b.h>
    #include <c/d.h>
    #include <c/e.h>
    #include <f.h>

    正确示例2:以稳定度排序,建议将不稳定的头文件放在前面,如把产品的头文件放在平台的头文件前面:

    #include <product.h>
    #include <platform.h>

    相对来说,product.h修改的较为频繁,如果有错误,不必编译platform.h就可以发现product.h的错误,可以部分减少编译时间。


    2、函数

    函数设计的精髓:编写整洁函数,同时把代码有效组织起来。

    整洁函数要求:代码简单直接、不隐藏设计者的意图、用干净利落的抽象和直截了当的控制语句将函数有机组织起来。

    代码的有效组织包括:逻辑层组织和物理层组织两个方面。逻辑层,主要是把不同功能的函数通过某种联系组织起来,主要关注模块间的接口,也就是模块的架构。物理层,无论使用什么样的目录或者名字空间等,需要把函数用一种标准的方法组织起来。例如:设计良好的目录结构、函数名字、文件组织等,这样可以方便查找。

    1、一个函数仅完成一件功能

    一个函数实现多个功能给开发、使用、维护都带来很大的困难。

    将没有关联或者关联很弱的语句放到同一函数中,会导致函数职责不明确,难以理解,难以测试和改动。

    2、重复代码应该尽可能提炼成函数

    重复代码提炼成函数可以带来维护成本的降低。

    重复代码是我司不良代码最典型的特征之一。在“代码能用就不改”的指导原则之下,大量的烟囱式设计及其实现充斥着各产品代码之中。新需求增加带来的代码拷贝和修改,随着时间的迁移,产品中堆砌着许多类似或者重复的代码。

    项目组应当使用代码重复度检查工具,在持续集成环境中持续检查代码重复度指标变化趋势,并对新增重复代码及时重构。当一段代码重复两次时,即应考虑消除重复,当代码重复超过三次时,应当立刻着手消除重复。

    3、避免函数过长,新增函数不超过 50 行 (非空非注释行) 

    过长的函数往往意味着函数功能不单一,过于复杂。

    函数的有效代码行数,即NBNC(非空非注释行)应当在[1,50]区间。

    例外:某些实现算法的函数,由于算法的聚合性与功能的全面性,可能会超过50行。

    延伸阅读材料: 业界普遍认为一个函数的代码行不要超过一个屏幕,避免来回翻页影响阅读;一般的代码度量工具建议都对此进行检查,例如Logiscope的函数度量:"Number of Statement" (函数中的可执行语句数)建议不超过20行,QA C建议一个函数中的所有行数(包括注释和空白行)不超过50行。

    4、避免函数的代码块嵌套过深,新增函数的代码块嵌套不超过4层

    函数的代码块嵌套深度指的是函数中的代码控制块(例如:if、for、while、switch等)之间互相包含的深度。每级嵌套都会增加阅读代码时的脑力消耗,因为需要在脑子里维护一个“栈”(比如,进入条件语句、进入循环„„)。应该做进一步的功能分解,从而避免使代码的阅读者一次记住太多的上下文。优秀代码参考值:[1, 4]。

    错误示例:代码嵌套深度为5层:

    void serial (void)
    {
        if (!Received)
        {
            TmoCount = 0;
             switch (Buff)
            {
                case AISGFLG:
                    if ((TiBuff.Count > 3)&& ((TiBuff.Buff[0] == 0xff) || (TiBuf.Buff[0] == CurPa.ADDR)))
                    {
                        Flg7E = false;
                        Received = true;
                    }
                    else
                    {
                        TiBuff.Count = 0;
                        Flg7D = false;
                        Flg7E = true;
                    }
                    break;
                default:
                    break;
            }
        }
    }

    5、 可重入函数应避免使用共享变量;若需要使用,则应通过互斥手段(关中断、信号量)对其加以保护

    可重入函数是指可能被多个任务并发调用的函数。在多任务操作系统中,函数具有可重入性是多个任务可以共用此函数的必要条件。共享变量指的全局变量和static变量。编写C语言的可重入函数时,不应使用static局部变量,否则必须经过特殊处理,才能使函数具有可重入性。

    示例:函数square_exam返回g_exam平方值。那么如下函数不具有可重入性。

    int g_exam;
    unsigned int example( int para )
    {
        unsigned int temp;
        g_exam = para; // (**)
        temp = square_exam ( );
        return temp;
    }

    此函数若被多个线程调用的话,其结果可能是未知的,因为当(**)语句刚执行完后,另外一个使用本函数的线程可能正好被激活,那么当新激活的线程执行到此函数时,将使g_exam赋于另一个不同的para值,所以当控制重新回到“temp =square_exam ( )”后,计算出的temp很可能不是预想中的结果。此函数应如下改进。 

    int g_exam;
    unsigned int example( int para )
    {
        unsigned int temp;
        [申请信号量操作] // 若申请不到“信号量”,说明另外的进程正处于
        g_exam = para; //给g_exam赋值并计算其平方过程中(即正在使用此
        temp = square_exam( ); // 信号),本进程必须等待其释放信号后,才可继
        [释放信号量操作] // 续执行。其它线程必须等待本线程释放信号量后
        // 才能再使用本信号。
        return temp;
    }

    6、对参数的合法性检查,由调用者负责还是由接口函数负责,应在项目组/模块内应统一规定。缺省由调用者负责。

    对于模块间接口函数的参数的合法性检查这一问题,往往有两个极端现象,即:要么是调用者和被调用者对参数均不作合法性检查,结果就遗漏了合法性检查这一必要的处理过程,造成问题隐患;要么就是调用者和被调用者均对参数进行合法性检查,这种情况虽不会造成问题,但产生了冗余代码,降低了效率。

    7、对函数的错误返回码要全面处理

    一个函数(标准库中的函数/第三方库函数/用户定义的函数)能够提供一些指示错误发生的方法。这可以通过使用错误标记、特殊的返回数据或者其他手段,不管什么时候函数提供了这样的机制,调用程序应该在函数返回时立刻检查错误指示。

    8、设计高扇入,合理扇出(小于7)的函数

    扇出是指一个函数直接调用(控制)其它函数的数目,而扇入是指有多少上级函数调用它。

    扇出过大,表明函数过分复杂,需要控制和协调过多的下级函数;而扇出过小,例如:总是1,表明函数的调用层次可能过多,这样不利于程序阅读和函数结构的分析,并且程序运行时会对系统资源如堆栈空间等造成压力。通常函数比较合理的扇出(调度函数除外)通常是3~5。 

    扇出太大,一般是由于缺乏中间层次,可适当增加中间层次的函数。扇出太小,可把下级函数进一步分解多个函数,或合并到上级函数中。当然分解或合并函数时,不能改变要实现的功能,也不能违背函数间的独立性。扇入越大,表明使用此函数的上级函数越多,这样的函数使用效率高,但不能违背函数间的独立性而单纯地追求高扇入。公共模块中的函数及底层函数应该有较高的扇入。

    较良好的软件结构通常是顶层函数的扇出较高,中层函数的扇出较少,而底层函数则扇入到公共模块中。

    9、废弃代码(没有被调用的函数和变量) ) 要及时清除

    程序中的废弃代码不仅占用额外的空间,而且还常常影响程序的功能与性能,很可能给程序的测试、维护等造成不必要的麻烦。

    10、函数不变参数使用const 

    不变的值更易于理解/跟踪和分析,把const作为默认选项,在编译时会对其进行检查,使代码更牢固/更安全。

    正确示例:C99标准 7.21.4.4 中strncmp 的例子,不变参数声明为const。

    int strncmp(const char *s1, const char *s2, register size_t n)
    {
        register unsigned char u1, u2;
        while (n-- > 0)
        {
            u1 = (unsigned char) *s1++;
            u2 = (unsigned char) *s2++;
            if (u1 != u2)
            {
                return u1 - u2;
            }
            if (u1 == '\0')
            {
                return 0;
            }
        }
        return 0;
    }

    11、函数应避免使用全局变量、静态局部变量和 I/O 操作,不可避免的地方应集中使用

    带有内部“存储器”的函数的功能可能是不可预测的,因为它的输出可能取决于内部存储器(如某标记)的状态。这样的函数既不易于理解又不利于测试和维护。在C语言中,函数的static局部变量是函数的内部存储器,有可能使函数的功能不可预测。

    错误示例:如下函数,其返回值(即功能)是不可预测的。

    unsigned int integer_sum( unsigned int base )
    {
        unsigned int index;
        static unsigned int sum = 0;// 注意,是static类型的。
        // 若改为auto类型,则函数即变为可预测。
        for (index = 1; index <= base; index++)
        {
            sum += index;
        }
        return sum;
    }

    12、检查函数所有非参数输入的有效性,如数据文件、公共变量等

    函数的输入主要有两种:一种是参数输入;另一种是全局变量、数据文件的输入,即非参数输入。函数在使用输入参数之前,应进行有效性检查。

    13、 函数的参数个数不超过5个

    函数的参数过多,会使得该函数易于受外部(其他部分的代码)变化的影响,从而影响维护工作。函数的参数过多同时也会增大测试的工作量。

    函数的参数个数不要超过5个,如果超过了建议拆分为不同函数。

    14、除打印类函数外,不要使用可变长参函数。

    可变长参函数的处理过程比较复杂容易引入错误,而且性能也比较低,使用过多的可变长参函数将导致函数的维护难度大大增加。

    15、在源文件范围内声明和定义的所有函数,除非外部可见,否则应该增加static关键字

    如果一个函数只是在同一文件中的其他地方调用,那么就用static声明。使用static确保只是在声明它的文件中是可见的,并且避免了和其他文件或库中的相同标识符发生混淆的可能性。

    正确示例:建议定义一个STATIC宏,在调试阶段,将STATIC定义为static,版本发布时,改为空,以便于后续的打热补丁等操作。

    #ifdef _DEBUG
    #define STATIC static
    #else
    #define STATIC
    #endif

    3、标识符命名与定义

    标识符的命名规则历来是一个敏感话题,典型的命名风格如unix风格、windows风格等,从来无法达成共识。实际上,各种风格都有其优势也有其劣势,而且往往和个人的审美观有关。我们对标识符定义主要是为了让团队的代码看起来尽可能统一,有利于代码的后续阅读和修改,产品可以根据自己的实际需要指定命名风格,规范中不再做统一的规定。

    1、标识符的命名要清晰、明了,有明确含义,同时使用完整的单词或大家基本可以理解的缩写,避免使人产生误解

    尽可能给出描述性名称,不要节约空间,让别人很快理解你的代码更重要。

    正确示例:

    int error_number;
    int number_of_completed_connection;

    错误示例:

    int n;
    int nerr;
    int n_comp_conns;

    2、除了常见的通用缩写以外,不使用单词缩写,不得使用汉语拼音

    较短的单词可通过去掉“元音”形成缩写,较长的单词可取单词的头几个字母形成缩写,一些单词有大家公认的缩写,常用单词的缩写必须统一。协议中的单词的缩写与协议保持一致。对于某个系统使用的专用缩写应该在注视或者某处做统一说明。

    正确示例:一些常见可以缩写的例子:

    argument 可缩写为 arg
    buffer 可缩写为 buff
    clock 可缩写为 clk
    command 可缩写为 cmd
    compare 可缩写为 cmp
    configuration 可缩写为 cfg
    device 可缩写为 dev
    error 可缩写为 err
    hexadecimal 可缩写为 hex
    increment 可缩写为 inc
    initialize 可缩写为 init
    maximum 可缩写为 max
    message 可缩写为 msg
    minimum 可缩写为 min
    parameter 可缩写为 para
    previous 可缩写为 prev
    register 可缩写为 reg
    semaphore 可缩写为 sem
    statistic 可缩写为 stat
    synchronize 可缩写为 sync
    temp 可缩写为 tmp

    3、产品/项目组内部应保持统一的命名风格

    Unix like和windows like风格均有其拥趸,产品应根据自己的部署平台,选择其中一种,并在产品内部保持一致。

    4、用正确的反义词组命名具有互斥意义的变量或相反动作的函数等

    正确示例:

    add/remove begin/end create/destroy
    insert/delete first/last get/release
    increment/decrement put/get add/delete
    lock/unlock open/close min/max
    old/new start/stop  next/previous
    source/target show/hide  send/receive
    source/destination copy/paste up/down

    5、尽量避免名字中出现数字编号,除非逻辑上的确需要编号

    错误示例:如下命名,使人产生疑惑。

    #define EXAMPLE_0_TEST_
    #define EXAMPLE_1_TEST_

    正确示例:应改为有意义的单词命名。

    #define EXAMPLE_UNIT_TEST_
    #define EXAMPLE_ASSERT_TEST_

    6、标识符前不应添加模块、项目、产品、部门的名称作为前缀

    很多已有代码中已经习惯在文件名中增加模块名,这种写法类似匈牙利命名法,导致文件名不可读,并且带来带来如下问题:

    • 第一眼看到的是模块名,而不是真正的文件功能,阻碍阅读;
    • 文件名太长;
    • 文件名和模块绑定,不利于维护和移植。若foo.c进行重构后,从a模块挪到b模块,若foo.c
    • 中有模块名,则需要将文件名从a_module_foo.c改为b_module_foo.c。

    7、平台/ / 驱动等适配代码的标识符命名风格保持和平台

    涉及到外购芯片以及配套的驱动,这部分的代码变动(包括为产品做适配的新增代码),应该保持原有的风格。

    8、重构/修改部分代码时,应保持和原有代码的命名风格一致

    根据源代码现有的风格继续编写代码,有利于保持总体一致。

    9、文件命名统一采用小写字符

    因为不同系统对文件名大小写处理会不同(如MS的DOS、Windows系统不区分大小写,但是Linux系统则区分),所以代码文件命名建议统一采用全小写字母命名。

    10、全局变量应增加“g_” 前缀,静态变量应增加“s_”

    首先,全局变量十分危险,通过前缀使得全局变量更加醒目,促使开发人员对这些变量的使用更加小心。

    其次,从根本上说,应当尽量不使用全局变量,增加g_和s_前缀,会使得全局变量的名字显得很丑陋,从而促使开发人员尽量少使用全局变量。

    11、禁止使用单字节命名变量,但 允许 定义i 、j、k作为局部循环变量

    12、 不建议使用匈牙利命名法

    匈牙利命名法是一种编程时的命名规范。基本原则是:变量名=属性+类型+对象描述。匈牙利命名法源于微软,然而却被很多人以讹传讹的使用。而现在即使是微软也不再推荐使用匈牙利命名法。历来对匈牙利命名法的一大诟病,就是导致了变量名难以阅读,这和本规范的指导思想也有冲突,所以本规范特意强调,变量命名不应采用匈牙利命名法,而应该想法使变量名为一个有意义的词或词组,方便代码的阅读。

    变量命名需要说明的是变量的含义,而不是变量的类型。在变量命名前增加类型说明,反而降低了变量的可读性;更麻烦的问题是,如果修改了变量的类型定义,那么所有使用该变量的地方都需要修改。

    13、使用名词或者形容词+名词方式命名变量 

    14、函数命名应以函数要执行的动作命名,一般采用动词或者动词+名词的结构

    正确示例:找到当前进程的当前目录:

    DWORD GetCurrentDirectory( DWORD BufferLength, LPTSTR Buffer );

    15、函数指针除了前缀,其他按照函数的命名规则命名

    16、对于数值或者字符串等等常量的定义,建议采用全大写字母,单词之间加下划线“_”的方式命名(枚举同样建议使用此方式定义)

    正确示例:

    #define PI_ROUNDED 3.14

    17、除了头文件或编译开关等特殊标识定义,宏定义不能使用下划线“_”开头和结尾

    一般来说,‟_‟开头、结尾的宏都是一些内部的定义,ISO/IEC 9899(俗称C99)中有如下的描述(6.10.8 Predefined macro names):

    None of these macro names (这里上面是一些内部定义的宏的描述),nor the identifier defined,shall be the subject of a #define or a #undef preprocessing directive.Any other predefined macro names shall begin with a leading underscore fol lowedby an uppercase letter ora second underscore.


    4、变量

    1、一个变量只有一个功能,不能把一个变量用作多种用途

    一个变量只用来表示一个特定功能,不能把一个变量作多种用途,即同一变量取值不同时,其代表的意义也不同。

    错误示例:具有两种功能的反例

    WORD DelRelTimeQue( void )
    {
        WORD Locate;
        Locate = 3; 
        Locate = DeleteFromQue(Locate); /* Locate具有两种功能:位置和函数DeleteFromQue的返回值 */
        return Locate;
    }

    正确做法:使用两个变量

    WORD DelRelTimeQue( void )
    {
        WORD Ret;
        WORD Locate;
        Locate = 3;
        Ret  = DeleteFromQue(Locate);
        return Ret;
    }

    2、结构功能单一,不要设计面面俱到的数据结构 

    相关的一组信息才是构成一个结构体的基础,结构的定义应该可以明确的描述一个对象,而不是一组相关性不强的数据的集合。设计结构时应力争使结构代表一种现实事务的抽象,而不是同时代表多种。结构中的各元素应代表同一事务的不同侧面,而不应把描述没有关系或关系很弱的不同事务的元素放到同一结构中。

    错误示例:如下结构不太清晰、合理。

    typedef struct STUDENT_STRU
    {
        unsigned char name[32]; /* student's name */
        unsigned char age; /* student's age */
        unsigned char sex; /* student's sex, as follows */
        /* 0 - FEMALE; 1 - MALE */
        unsigned char teacher_name[32]; /* the student teacher's name */
        unsigned char teacher_sex; /* his teacher sex */
    } STUDENT;

    正确示例:若改为如下,会更合理些。

    typedef struct TEACHER_STRU
    {
        unsigned char name[32]; /* teacher name */
        unsigned char sex; /* teacher sex, as follows */
        /* 0 - FEMALE; 1 - MALE */
        unsigned int teacher_ind; /* teacher index */
    } TEACHER;
    
    typedef struct STUDENT_STRU
    {
        unsigned char name[32]; /* student's name */
        unsigned char age; /* student's age */
        unsigned char sex; /* student's sex, as follows */
        /* 0 - FEMALE; 1 - MALE */
        unsigned int teacher_ind; /* his teacher index */
    } STUDENT; 

    3、不用或者少用全局变量

    单个文件内部可以使用static的全局变量,可以将其理解为类的私有成员变量。

    全局变量应该是模块的私有数据,不能作用对外的接口使用,使用static类型定义,可以有效防止外部文件的非正常访问,建议定义一个STATIC宏,在调试阶段,将STATIC定义为static,版本发布时,改为空,以便于后续的打补丁等操作。

    4、防止局部变量与全局变量同名

    尽管局部变量和全局变量的作用域不同而不会发生语法错误,但容易使人误解。

    5、通讯过程中使用的结构,必须注意字节序

    通讯报文中,字节序是一个重要的问题,我司设备使用的CPU类型复杂多样,大小端、32位/64位的处理器也都有,如果结构会在报文交互过程中使用,必须考虑字节序问题。由于位域在不同字节序下,表现看起来差别更大,所以更需要注意对于这种跨平台的交互,数据成员发送前,都应该进行主机序到网络序的转换;接收时,也必须进行网络序到主机序的转换。

    6、严禁使用未经初始化的变量作为右值 

    在首次使用前初始化变量,初始化的地方离使用的地方越近越好。

    7、构造仅有一个模块或函数可以修改、创建,而其余有关模块或函数只访问的全局变量,防止多个不同模块或函数都可以修改、创建同一全局变量的现象

    降低全局变量耦合度。

    8、使用面向接口编程思想,通过 API 访问数据:如果本模块的数据需要对外部模块开放 ,应提供接口函数来设置、获取,同时注意全局数据的访问互斥

    避免直接暴露内部数据给外部模型使用,是防止模块间耦合最简单有效的方法。定义的接口应该有比较明确的意义,比如一个风扇管理功能模块,有自动和手动工作模式,那么设置、查询工作模块就可以定义接口为SetFanWorkMode,GetFanWorkMode;查询转速就可以定义为GetFanSpeed;风扇支持节能功能开关,可以定义EnabletFanSavePower等。

    9、明确全局变量的初始化顺序,避免跨模块的初始化依赖 

    系统启动阶段,使用全局变量前,要考虑到该全局变量在什么时候初始化,使用全局变量和初始化全局变量,两者之间的时序关系,谁先谁后,一定要分析清楚,不然后果往往是低级而又灾难性的。

    10、尽量减少没有必要的数据类型默认转换与强制转换

    当进行数据类型强制转换时,其数据的意义、转换后的取值等都有可能发生变化,而这些细节若考虑不周,就很有可能留下隐患。

    错误示例:如下赋值,多数编译器不产生告警,但值的含义还是稍有变化。

    char ch;
    unsigned short int exam;
    ch = -1;
    exam = ch; // 编译器不产生告警,此时exam为0xFFFF。

    5、宏、常量

    1、用宏定义表达式时,要使用完备的括号 

    因为宏只是简单的代码替换,不会像函数一样先将参数计算后,再传递。

    错误示例:如下定义的宏都存在一定的风险

    #define RECTANGLE_AREA(a, b) a * b
    #define RECTANGLE_AREA(a, b) (a * b)
    #define RECTANGLE_AREA(a, b) (a) * (b)

    正确示例:

    #define RECTANGLE_AREA(a, b) ((a) * (b))

    这是因为:如果定义 #define RECTANGLE_AREA(a, b) a * b  或 #define RECTANGLE_AREA(a, b) (a * b)则 c/RECTANGLE_AREA(a, b)  将扩展成 c/a * b , c  与 b 本应该是除法运算,结果变成了乘法运算,造成错误。

    如果定义 #define RECTANGLE_AREA(a, b) (a * b)则 RECTANGLE_AREA(c + d, e + f) 将扩展成: (c + d * e + f), d 与 e 先运算,造成错误。 

    2、将宏所定义的多条表达式放在大括号中

    3、使用宏时,不允许参数发生变化

    错误示例:

    #define SQUARE(a) ((a) * (a))
    int a = 5;
    int b;
    b = SQUARE(a++); // 结果:a = 7,即执行了两次增。

    正确示例:

    b = SQUARE(a);
    a++; // 结果:a = 6,即只执行了一次增。

    同时也建议即使函数调用,也不要在参数中做变量变化操作,因为可能引用的接口函数,在某个版本升级后,变成了一个兼容老版本所做的一个宏,结果可能不可预知。

    4、不允许直接使用魔鬼数字 

    使用魔鬼数字的弊端:代码难以理解;如果一个有含义的数字多处使用,一旦需要修改这个数值,代价惨重。

    使用明确的物理状态或物理意义的名称能增加信息,并能提供单一的维护点。

    解决途径:对于局部使用的唯一含义的魔鬼数字,可以在代码周围增加说明注释,也可以定义局部const变量,变量命名自注释。对于广泛使用的数字,必须定义const全局变量/宏;同样变量/宏命名应是自注释的。0作为一个特殊的数字,作为一般默认值使用没有歧义时,不用特别定义。

    5、除非必要,应尽可能使用函数代替宏

    宏对比函数,有一些明显的缺点:

    • 宏缺乏类型检查,不如函数调用检查严格;
    • 宏展开可能会产生意想不到的副作用,如#define SQUARE(a) (a) * (a)这样的定义,如果是SQUARE(i++),就会导致i被加两次;如果是函数调用double square(double a) {return a * a;}则不会有此副作用;
    • 以宏形式写的代码难以调试难以打断点,不利于定位问题;
    • 宏如果调用的很多,会造成代码空间的浪费,不如函数空间效率高。

    错误示例:下面的代码无法得到想要的结果:

    #define MAX_MACRO(a, b) ((a) > (b) ? (a) : (b))
    
    int MAX_FUNC(int a, int b) {
        return ((a) > (b) ? (a) : (b));
    }
    
    int testFunc()
    {
        unsigned int a = 1;
        int b = -1;
        printf("MACRO: max of a and b is: %d\n", MAX_MACRO(++a, b));
        printf("FUNC : max of a and b is: %d\n", MAX_FUNC(a, b));
        return 0;
    }

    上面宏代码调用中,由于宏缺乏类型检查,a和b的比较变成无符号数的比较,结果是a < b,所以a只加了一次,所以最终的输出结果是:

    MACRO: max of a and b is: -1
    FUNC : max of a and b is: 2

    6、常量建议使用 const 定义代替宏

    “尽量用编译器而不用预处理”,因为#define经常被认为好象不是语言本身的一部分。看下面的语句:

    #define ASPECT_RATIO 1.653

    编译器会永远也看不到ASPECT_RATIO这个符号名,因为在源码进入编译器之前,它会被预处理程序去掉,于是ASPECT_RATIO不会加入到符号列表中。如果涉及到这个常量的代码在编译时报错,就会很令人费解,因为报错信息指的是1.653,而不是ASPECT_RATIO。如果ASPECT_RATIO不是在你自己写的头文件中定义的,你就会奇怪1.653是从哪里来的,甚至会花时间跟踪下去。这个问题也会出现在符号调试器中,因为同样地,你所写的符号名不会出现在符号列表中。
    解决这个问题的方案很简单:不用预处理宏,定义一个常量:

    const double ASPECT_RATIO = 1.653;

    这种方法很有效,但有两个特殊情况要注意。首先,定义指针常量时会有点不同。因为常量定义一般是放在头文件中(许多源文件会包含它),除了指针所指的类型要定义成const外,重要的是指针也经常要定义成const。例如,要在头文件中定义一个基于char*的字符串常量,你要写两次const:

    const char * const authorName = "Scott Meyers";

    延伸阅读材料:关于const和指针的使用,这里摘录两段ISO/IEC 9899(俗称C99)的描述: 

    7、宏定义中尽量不使用 return 、 goto 、 continue 、 break等改变程序流程的语句

    如果在宏定义中使用这些改变流程的语句,很容易引起资源泄漏问题,使用者很难自己察觉。

    错误示例:在某头文件中定义宏CHECK_AND_RETURN:

    #define CHECK_AND_RETURN(cond, ret) {if (cond == NULL_PTR) {return ret;}}
    //然后在某函数中使用(只说明问题,代码并不完整):
    pMem1 = VOS_MemAlloc(...);
    CHECK_AND_RETURN(pMem1 , ERR_CODE_XXX)
    pMem2 = VOS_MemAlloc(...);
    CHECK_AND_RETURN(pMem2 , ERR_CODE_XXX) /*此时如果pMem2==NULL_PTR,则pMem1未释放函数就返回了,造成内存泄漏。*/

    所以说,类似于CHECK_AND_RETURN这些宏,虽然能使代码简洁,但是隐患很大,使用须谨慎。 


    6、表达式

    1、表达式的值在标准所允许的任何运算次序下都应该是相同的

    2、函数调用不要作为另一个函数的参数使用,否则对于代码的调试、阅读都不利

    错误示例:如下代码不合理,仅用于说明当函数作为参数时,由于参数压栈次数不是代码可以控制的,可能造成未知的输出:

    int g_var;
    
    int fun1()
    {
        g_var += 10;
        return g_var;
    }
    
    int fun2()
    {
        g_var += 100;
        return g_var;
    }
    
    int main(int argc, char *argv[], char *envp[])
    {
        g_var = 1;
        printf("func1: %d, func2: %d\n", fun1(), fun2());
        g_var = 1;
        printf("func2: %d, func1: %d\n", fun2(), fun1());
    }

    上面的代码,使用断点调试起来也比较麻烦,阅读起来也不舒服,所以不要为了节约代码行,而写这种代码。

    3、赋值语句不要写在 if 等语句中,或者作为函数的参数使用

    因为if语句中,会根据条件依次判断,如果前一个条件已经可以判定整个条件,则后续条件语句不会再运行,所以可能导致期望的部分赋值没有得到运行。

    错误示例:

    int main(int argc, char *argv[], char *envp[])
    {
        int a = 0;
        int b;
        if ((a == 0) || ((b = fun1()) > 10))
        {
            printf("a: %d\n", a);
        }
        printf("b: %d\n", b);
    }

    作用函数参数来使用,参数的压栈顺序不同可能导致结果未知。

    4、用括号明确表达式的操作顺序,避免过分依赖默认优先级

    使用括号强调所使用的操作符,防止因默认的优先级与设计思想不符而导致程序出错;同时使得代码更为清晰可读,然而过多的括号会分散代码使其降低了可读性。

    5、赋值操作符不能使用在产生布尔值的表达式上 

    示例:

    x = y;
    if (x != 0)
    {
        foo ();
    }

    不能写成:

    if (( x = y ) != 0)
    {
        foo ();
    }

    或者更坏的:

    if (x = y)
    {
        foo ();
    }

    7、注释

     1、优秀的代码可 以自我解释,不通过注释即可轻易读懂

    优秀的代码不写注释也可轻易读懂,注释无法把糟糕的代码变好,需要很多注释来解释的代码往往存在坏味道,需要重构。

    错误示例:注释不能消除代码的坏味道:

    /* 判断m是否为素数*/
    /* 返回值:: 是素数,: 不是素数*/
    int p(int m)
    {
        int k = sqrt(m);
        for (int i = 2; i <= k; i++)
            if (m % i == 0)
                break; /* 发现整除,表示m不为素数,结束遍历*/
        /* 遍历中没有发现整除的情况,返回*/
        if (i > k)
            return 1;
        /* 遍历中没有发现整除的情况,返回*/
        else
            return 0;
    }

    重构代码后,不需要注释:

    int IsPrimeNumber(int num)
    {
        int sqrt_of_num = sqrt (num);
        for (int i = 2; i <= sqrt_of_num; i++)
        {
            if (num % i == 0)
            {
                return FALSE;
            }
        }
        return TRUE;
    }

    2、注释的内容要清楚、明了,含义准确,防止注释二义性

    有歧义的注释反而会导致维护者更难看懂代码,正如带两块表反而不知道准确时间。

    3、在代码的功能、意图层次上进行注释,即注释解释 代码难以直接表达的意图 , 而不是重复描述代码

    注释的目的是解释代码的目的、功能和采用的方法,提供代码以外的信息,帮助读者理解代码,防止没必要的重复注释信息。对于实现代码中巧妙的、晦涩的、有趣的、重要的地方加以注释。注释不是为了名词解释(what),而是说明用途(why)。

    4、修改代码时,维护代码周边的所有注释,以保证注释与代码的一致性,不再有用的注释要删除

    不要将无用的代码留在注释中,随时可以从源代码配置库中找回代码;即使只是想暂时排除代码,也要留个标注,不然可能会忘记处理它。

    5、文件头部应进行注释,注释必须列出:版权说明、版本号、生成日期、作者姓名、工号、内容、功能说明、与其它文件的关系、修改日志等,头文件的注释中还应有函数功能简要说明

    正确示例:下面这段头文件的头注释比较标准,当然,并不局限于此格式,但上述信息建议要包含在内。

    6、函数声明处注释描述函数功能、性能及用法,包括输入和输出参数、函数返回值、可重入的要求等;定义处详细描述函数功能和实现要点,如实现的简要步骤、实现的理由、 设计约束等

    重要的、复杂的函数,提供外部使用的接口函数应编写详细的注释。

    7、全局变量要有较详细的注释,包括对其功能、取值范围以及存取时注意事项等的说明

    正确示例:

    /* The ErrorCode when SCCP translate */
    /* Global Title failure, as follows */ /* 变量作用、含义*/
    /* 0 -SUCCESS 1 -GT Table error */
    /* 2 -GT error Others -no use */ /* 变量取值范围*/
    /* only function SCCPTranslate() in */
    /* this modual can modify it, and other */
    /* module can visit it through call */
    /* the function GetGTTransErrorCode() */ /* 使用方法*/
    BYTE g_GTTranErrorCode;

    8、注释应放在其代码上方相邻位置或右方,不可放在下面,如放于上方则需与其上面的代码用空行隔开,且与下方代码缩进相同

    正确示例:

    /* active statistic task number */
    #define MAX_ACT_TASK_NUMBER 1000
    #define MAX_ACT_TASK_NUMBER 1000 /* active statistic task number */
    可按如下形式说明枚举/数据/联合结构。
    /* sccp interface with sccp user primitive message name */
    enum SCCP_USER_PRIMITIVE
    {
        N_UNITDATA_IND, /* sccp notify sccp user unit data come */
        N_NOTICE_IND, /* sccp notify user the No.7 network can not transmission this message */
        N_UNITDATA_REQ, /* sccp user's unit data transmission request*/
    };

    9、对于 switch语句下的case语句,如果因为特殊情况需要处理完一个case后进入下一个case处理,必须在该case语句处理完、下一个case语句前加上明确的注释

    这样比较清楚程序编写者的意图,有效防止无故遗漏break语句。

    case CMD_FWD:
        ProcessFwd();
        /* now jump into c ase CMD_A */
    case CMD_A:
        ProcessA();
        break;
    //对于中间无处理的连续case,已能较清晰说明意图,不强制注释。
    switch (cmd_flag)
        {
            case CMD_A:
            case CMD_B:
        {
            ProcessCMD();
            break;
        }
        ……
    }

    10、避免在注释中使用缩写,除非是业界通用或子系统内标准化的缩写

    11、同一产品或项目组统一注释风格

    12、避免在一行代码或表达式的中间插入注释

    除非必要,不应在代码或表达中间插入注释,否则容易使代码可理解性变差

    13、注释应考虑程序易读及外观排版的因素,使用的语言若是中、英兼有的,建议多使用中文,除非能用非常流利准确的英文表达,对于有外籍员工的,由产品确定注释语言

    注释语言不统一,影响程序易读性和外观排版,出于对维护人员的考虑,建议使用中文。

    14、文件头、函数头、全局常量变量、类型定义的注释格式采用工具可识别的格式

    采用工具可识别的注释格式,例如doxygen格式,方便工具导出注释形成帮助文档。以doxygen格式为例,文件头,函数和全部变量的注释的示例如下:


    8、排版与格式

    1、程序块采用缩进风格编写, 每级缩进为4个空格

    2、相对独立的程序块之间、变量说明之后必须加空行 

    错误示例:如下例子不符合规范。

    if (!valid_ni(ni))
    {
        // program code
        ...
    }
    repssn_ind = ssn_data[index].repssn_index;
    repssn_ni = ssn_data[index].ni;

    正确示例:

    if (!valid_ni(ni))
    {
        // program code
        ...
    }
    
    repssn_ind = ssn_data[index].repssn_index;
    repssn_ni = ssn_data[index].ni;

    3、一条语句不能过长,如不能拆分需要分行写。一行到底多少字符换行比较合适,产品可以自行确定

    对于目前大多数的PC来说,132比较合适(80/132是VTY常见的行宽值);对于新PC宽屏显示器较多的产品来说,可以设置更大的值。换行时有如下建议:

    • 换行时要增加一级缩进,使代码可读性更好;
    • 低优先级操作符处划分新行;换行时操作符应该也放下来,放在新行首;
    • 换行时建议一个完整的语句放在一行,不要根据字符数断行。

    正确示例:

    if ((temp_flag_var == TEST_FLAG)
    &&(((temp_counter_var - TEST_COUNT_BEGIN) % TEST_COUNT_MODULE) >= TEST_COUNT_THRESHOLD))
    {
        // process code
    }

    4、多个短语句(包括赋值语句)不允许写在同一行内 ,即一行只写一条语句

    错误示例:

    int a = 5; int b= 10; //不好的排版

    正确示例:

    int a = 5;
    int b= 10;

    5、if 、 for 、 do 、 while 、 case 、 switch 、 default 等语句独占一行

    执行语句必须用缩进风格写,属于if、for、do、while、case、switch、default等下一个缩进级别;

    一般写if、for、do、while等语句都会有成对出现的„{}‟,对此有如下建议可以参考:if、for、do、while等语句后的执行语句建议增加成对的“{}”;如果if/else配套语句中有一个分支有“{}”,那么另一个分支即使一行代码也建议增加“{}”;添加“{”的位置可以在if等语句后,也可以独立占下一行;独立占下一行时,可以和if在一个缩进级别,也可以在下一个缩进级别;但是如果if语句很长,或者已经有换行,建议“{”使用独占一行的写法。

    6、在两个以上的关键字、变量、常量进行对等操作时,它们之间的操作符之前、之后或者前后要加空格 ; 进行非对等操作时,如果是关系密切的立即操作符(如-> > ),后不应加空格

    采用这种松散方式编写代码的目的是使代码更加清晰。

    在已经非常清晰的语句中没有必要再留空格,如括号内侧(即左括号后面和右括号前面)不需要加空格,多重括号间不必加空格,因为在C语言中括号已经是最清晰的标志了。在长语句中,如果需要加的空格非常多,那么应该保持整体清晰,而在局部不加空格。给操作符留空格时不要连续留两个以上空格。

    正确示例:

    1、逗号、分号只在后面加空格。

    int a, b, c;

    2、比较操作符, 赋值操作符"="、 "+=",算术操作符"+"、"%",逻辑操作符"&&"、"&",位域操作符"<<"、"^"等双目操作符的前后加空格。 

    if (current_time >= MAX_TIME_VALUE)
    a = b + c;
    a *= 2;
    a = b ^ 2;

    3、"!"、"~"、"++"、"--"、"&"(地址操作符)等单目操作符前后不加空格。

    *p = 'a'; // 内容操作"*"与内容之间
    flag = !is_empty; // 非操作"!"与内容之间
    p = &mem; // 地址操作"&" 与内容之间
    i++; 

     4、"->"、"."前后不加空格。

    p->id = pid; // "->"指针前后不加空格

    5、if、for、while、switch等与后面的括号间应加空格,使if等关键字更为突出、明显。

    if (a >= b && c > d)

    7、注释符(包括/**/、//)与注释内容之间要用一个空格进行分隔

    8、源程序中关系较为紧密的代码应尽可能相邻


    9、代码编辑编译 

    1、使用编译器的最高告警级别,理解所有的告警,通过修改代码而不是降低告警级别来消除所有告警

    编译器是你的朋友,如果它发出某个告警,这经常说明你的代码中存在潜在的问题。

    2、在产品软件(项目组)中,要统一编译开关、静态检查选项以及相应告警清除策略

    如果必须禁用某个告警,应尽可能单独局部禁用,并且编写一个清晰的注释,说明为什么屏蔽。某些语句经编译/静态检查产生告警,但如果你认为它是正确的,那么应通过某种手段去掉告警信息。

    4、本地构建工具(如 PC-Lint)的配置应该和持续集成的一致

    两者一致,避免经过本地构建的代码在持续集成上构建失败

    5、 使用版本控制(配置管理)系统,及时签入通过本地构建的代码,确保签入的代码不会影响构建成功

    及时签入代码降低集成难度。

    6、要小心地使用编辑器提供的块拷贝功能编程

    以上为自我总结,感兴趣的同志,推荐阅读全文,也就60余页。

    展开全文
  • 大数据在ETL处理过程中,使用的工具有kettle, dataStage,infomatica等等。kettle是属于开源免费的etl 工具。基于java开发,内置了很多强大的...下面简单对日常中使用kettle中出现的问题做一个总结,方便自己也方便大...
  • 软件测试总结——常见的面试问题(一)

    万次阅读 多人点赞 2019-10-12 18:40:58
    并发测试方法通过模拟用户并发访问,测试多用户并发访问同一个应用、同一个模块或者数据记录时是否存在死锁或其者他性能问题。 也就是说,这种测试关注点是多个用户同时(并发)对一个模块或操作进行加压。   ...
  • DevOps面试问题总结1

    千次阅读 2018-11-16 13:25:20
    DevOps是一组过程、方法与系统的统称,用于促进开发(应用程序/软件工程)、技术运营和质量保障(QA)部门之间的沟通、协作与整合。下面为大家分享DevOps系列的面试问题 持续整合问题 问题一:持续集成是什么意思...
  • 面试中常见的问题总结

    千次阅读 2019-05-25 14:14:17
    面试中常见的问题总结前言面试中最常见的问题1. 请做个自我介绍 / 谈谈你自己2. 你的工作经历有一段空白期,能解释一下吗?3. 为什么你想来这儿工作?关于我们公司你了解多少?4. 你的工作经验欠缺,如何能胜任这项...
  • citrix 问题总结 1

    千次阅读 2011-07-20 23:59:51
    一、 安装问题 11、在安装Citrix Presentation Server 4.0时提示Error 10001 12、在安装web interface时,提示“error 1609” 13、在添加删除程序中安装Web Interface 4.0时,提示“Applicatio
  • 六大IT运维服务管理问题总结

    千次阅读 2013-01-08 16:17:13
    在现有的IT运维管理系统中,都存在哪些问题是另IT管理员头疼的呢?我们今天在这里为大家总结了六大IT运维服务管理问题。   1.管理现状问题:支撑企业业务运行的IT系统主要由大量的网络设备、主机系统和应用系统...
  •  “没有‘成绩单’,没有价值,IT部门还有存在的意义吗?”这个问题常常是CIO心头沉甸甸的重负。“管理中最难的就是绩效管理”,一位商学院教授如此感叹。记者本次采访的9位CIO无一不把“价值与贡献”作为头等大事...
  • 2011年是不寻常的一年,出现了国际金融动荡、国内财政政策紧缩、国际安全环境变幻等诸多问题,2011年也是产品信息部的变革之年,面临着...与此同时,也存在着很多不足和问题。 现在我就2011年部门工作情况汇报如下:
  • 部门 姓名 年龄 技术部 市场部 财务部 ***技术部 ***市场部 ...
  • 某Java大佬在地表最强Java企业(阿里)面试总结

    万次阅读 多人点赞 2020-08-23 19:48:06
    ) 由于永久代内存经常不够用或发生内存泄露,爆出异常java.lang.OutOfMemoryError: PermGen 字符串存在永久代中,容易出现性能问题和内存溢出。 类及方法的信息等比较难确定其大小,因此对于永久代的大小指定比较...
  • 功能测试方法总结/常见面试问题

    万次阅读 2017-05-04 10:12:24
    一、功能测试 ...其次,测试所链接的页面是否存在;最后,保证Web应用系统上没有孤立的页面,所谓孤立页面是指没有链接指向该页面,只有知道正确的URL地址才能访问。 链接测试可以自动进行,现在
  • 面试总结

    千次阅读 2016-04-25 20:05:55
    前言今天去饿了么面试,总体来说很失望,没搞明白对方所问这些问题的重点在哪里(当时脑袋一片空白),个人觉得面试官自己也描述不清楚,那么下面是自己回去后总结问题以及自己深思熟虑后想到的一些知识点。...
  • 问题总结: 1.测试用例覆盖考虑不全面,测试用例设计方法使用不够完美,无法熟练掌握测试用例设计的方法。 2.测试用例设计之前,思维分析能力薄弱,无法完全理解需求。 1.知识总结 1.1 软件工程要点 1.1.1...
  • 总结swift语言常见的20个问题和回答

    万次阅读 多人点赞 2014-07-23 16:27:23
    作为苹果在WWDC 2014上发布的新编程语言,Swift绝对是当前的热门话题。Swift由苹果开发者工具部门总监克里斯·拉特纳(Chris Lattner)耗时四年开发而成,苹果宣称Swift的特点是:...这总结了20个常见的问题和回答。
  • 软件公司按照软件流程划分部门导致的问题思考在软件公司一般的组织结构是事业部模式,就是公司软件领域下设多个事业部(按照业务领域划分),我们都知道软件产品/项目的全过程分为项目前期、需求分析、设计开发...
  • 2016年度工作总结暨2017年度工作计划编写要求: 1、总结2016年度的主要工作内容及完成情况,请...4、对本部门管理的合理化建议,列举存在问题并提出改进意见; 5、2017年度个人工作计划:主要目标、具体措施及办法。
  • 测试管理中可能存在问题及分析

    千次阅读 2019-01-02 12:30:00
    文中前两部分简要介绍了软件测试管理及测试的范围,方法及重要性,之后对当前国内中小型软件企业在测试及测试管理中可能存在问题进 行了简单的介绍与分析,最后介绍了一些较好的解决方法。 1、软件测试及测试管理...
  • http://journal.shouxi.net/html/qikan/zgyx/zgzyyxxzz/20081151/ggygl/20100108094525593_500384.html【关键词】 执业药师 制度 问题与建议 我国从...笔者尝试从目前执业药师存在问题进行粗浅的总结分析,并提出
  • 超详细的MySQL三万字总结

    万次阅读 多人点赞 2021-08-22 21:27:51
    判断数据库是否已经存在,不存在则创建数据库 CREATE DATABASE IF NOT EXISTS 数据库名; 创建数据库并指定字符集 CREATE DATABASE 数据库名 CHARACTER SET 字符集; 案例: -- 直接创建数据库 db1 create database ...
  • 分享一篇牛人的工作总结

    千次阅读 多人点赞 2019-06-19 10:31:36
    转自http://www.ituring.com.cn/article/497377,让大家感受一下什么叫优秀的架构师,我比较喜欢在总结里大量构图(而且喜欢在excel、ppt中直接绘制拼接,不受专业绘图工具的约束),喜欢了就收藏,而且还和性能监控...
  • 详细到令人发指,老哥靠不断总结还是去了大公司拿到27kOffer
  • 总结面经中常考的MYSQL面试问题(一)

    万次阅读 多人点赞 2018-03-02 11:18:18
    25、 对于关系型数据库而言,索引是相当重要的概念,请回答有关索引的几个问题: a)、索引的目的是什么? 快速访问数据表中的特定信息,提高检索速度 创建唯一性索引,保证数据库表中每一行数据的唯一性。 ...
  • 2019新的一年已经到来,如何制定个人it工作计划呢?...一篇完美的工作总结必须是因人而异,他有总结者自己的“影子”。 1、统揽全局,先写大纲 根据对年度的“整体”性思考,然后罗列出工作总结的“大纲”,最后...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 78,659
精华内容 31,463
关键字:

总结部门存在的问题