目前IT行业高速发展,对开发者的综合素质要求也越来越高。不单单是编程的知识点,其它方面也同样会影响到软件的最终交付质量。
比如,数据库的表结构和索引设计缺陷可能会带来软件上的架构缺陷和性能风险;工程结构混乱导致后续维护艰难;没有鉴权的漏洞代码极易被黑客攻击等等。
本手册以开发者为中心视角,划分为编程规范、异常日志、MySQL数据库三大块,致力于开发者编写出高效率、高质量代码。
1)代码中的命名均不能以下划线或美元符号开始,也不能以下划线或美元符号结束。
反例:_name / __name / $Object / name_ / name$ / Object$
2)代码中的命名严禁使用拼音与英文混合的方式,更不允许直接使用中文的方式。
反例:ZhiFuAmount [支付金额] / getJiFenById() [积分] / string 名字 = "张"
正例:payAmount [支付金额] / getCreditById() [积分] / string name = "张"
3)类名使用UpperCamelCase风格,必须遵从大驼峰形式
反例:errorHandler / htmlformatter / json_formatter / XMLService
正例:ErrorHandler / HtmlFormatter / JsonFormatter / XmlService
4)方法名、参数名、成员变量、局部变量同统一使用lowerCamelCase风格,必须遵从小驼峰形式。(Go语言可根据场景选择小驼峰或大驼峰)
正例:getTypeNameById() / userName / inputUserId
5)常量命名全部大写,单词间用下划线隔开。力求语义表达清楚,不要嫌名字长。
反例:MAX_COUNT
正例:MAX_STOCK_COUNT
6)抽象类命名使用Abstract或Base开头;异常类命名使用Exception结尾;测试类命名以它要测试的类的名称开始,以Test结尾。
7)中括号是数组类型的一部分,数组定义如下:int[] args。
反例:使用int args[]的方式来定义。
8)布尔类型的变量以is开头
正例:isFirst / isDeleted
9)包名统一使用小写
正例:github.com/beego/bee/utils
10)杜绝完成不规范的缩写,避免望文不知义。
反例:AbstrctClass 缩写成 AbsClass;Condition 缩写成 Condi
11)如果使用到了设计模式,建议在类名中体现出具体模式。
正例:class ActivityFactory
class ActivityAbstract
class ActivityInterface
12)枚举类名建议带上Enum后缀,枚举成员名称需全大写,单词间用下划线隔开。
正例:枚举类名:OrderStatusEnum / 成员名称:FINISH、UNKNOWN_STATUS
1)不允许任何魔法值(即未经定义的常量)直接出现在代码里。
反例:string key = "ono#order_refund#" + tradeId;
2)不要使用一个常量类维护所有常量,应该按常量功能进行归类,分开维护。大而全的常量类,非得使用查找功能才能定位修改的常量,不利于理解和维护。
如:缓存相关的常量应放在类:CacheConsts下;系统配置相关的常量应放在类:ConfigConsts下。
3)常量的复用层次有五层,跨应用共享常量、应用内共享常量、子工程共享常量、包内共享常量、类内共享常量。
A、跨应用共享常量:放置在二方库内,通常以composer引入方式放置在vendor中
B、应用内共享常量:放置在一方库的constant目录下
C、子工程共享常量:即在当前子工程的constant目录下
D、包内共享常量:即在当前包下的单独constant目录下
E、类内共享常量:直接在类内部定义
4)如果变量值仅在一个范围内变化,且带有名称之外的延伸属性,定义为枚举值。如星期。
正例: public Enum { MONDAY(1), TUESDAY(2), WEDNESDAY(3), THURSDAY(4), FRIDAY(5), SATURDAY(6),
SUNDAY(7);}
1)大括号的使用约定。如果大括号内为空,则简洁地写成{}即可,不需要换行;如果是非空代码块,则:
A、左大括号前不换行
B、左大括号后换行
C、右大括号前换行
D、右大括号后还有else、catch、while等代码则不换行;表示终止的右大括号后必须换行
2)左小括号和字符间不出现空格;同理,右小括号和字符间也不出现空格。
反例:if( a == b )
3)if、for、while、switch、do等保留字与括号间都必须架空格。
4)任何二目、三目运算符的左右两边都需要加一个空格。
正例:string a = b / int max = a > b
5)缩进一律采用4个空格。如果使用tab字符缩进,必须设置一个tab为4个空格。
6)单行字符数限制不超过120个,超出需要换行,换行时遵循如下原则:
A、第二行相对第一行缩进4个空格,从第三行开始不再缩进。
B、运算符与下文一起换行。
C、方法调用的点符号与下文一起换行。
D、在多个参数超长,在逗号后换行。
E、在括号前不要换行。
反例:
131StringBuffer sb = new StringBuffer();
2
3//超过 120 个字符的情况下,不要在括号前换行
4
5sb.append("zi").append("xin")...append
6
7("huang");
8
9//参数很多的方法调用可能超过 120 个字符,不要在逗号前换行
10
11method(args1, args2, args3, ...
12
13, argsX);
正例:
61StringBuffer sb = new StringBuffer();
2//超过 120 个字符的情况下,换行缩进 4 个空格,并且方法前的点符号一起换行
3sb.append("zi").append("xin")...
4.append("huang")...
5.append("huang")...
6.append("huang");
7)方法参数在定义和传入时,多个参数逗号后面必须加空格。
正例:method("a", "B", "c")
8)方法体内的执行语句组、变量的定义语句组、不同业务逻辑之间或者不同语义之间插入一个空行。相同业务逻辑和语义之间不需要插入空行。
没有必要连续插入多个空行进行隔开。
1)获取单例对象需要保证线程安全,其中的方法也要保证线程安全。
资源驱动类、工具类、单例工厂类都需要注意。
2)高并发时,同步调用应该去考虑锁的性能损耗。能用无锁数据结构,就不要用锁;能用锁区块,就不要锁整个方法体;能用对象锁,就不要用类锁。
尽可能使锁的代码块工作量尽可能的小,避免在锁区块中调用RPC方法。
3)对多个资源、数据库表、对象同时加锁时,需要保持一致的加锁顺序,否则可能会造成死锁。
线程一需要对表A、B、C依次全部加锁后才能进行更新操作。那么线程二的加锁顺序也必须是A、B、C,否则可能会出现死锁。
4)并发修改同一记录时,避免更新丢失,需要加锁。要么在应用层加锁,要么在缓存层加锁,要么在数据库层加乐观锁,使用version作为更新依据。
1)在一个switch块内,每个case要么通过break/return等来终止,要么注释说明程序将继续执行到哪一个case为止;在一个switch块内,都必须包含一个default语句并且放在最后,即使它什么代码都没有。
2)在if、else、for、while、do等语句中必须使用大括号。即使只有一行代码,避免使用单行的形式:if(condition) statements;
3)表达异常的分支时,少用if-else嵌套。
反例:
171if (condition1) {
2 if (condition2) {
3 if (condition3) {
4 if (condition4) {
5 // todo
6 } else {
7 // todo
8 }
9 } else {
10 // todo
11 }
12 } else {
13 // todo
14 }
15} else {
16 // todo
17}
正例:
121if (condition1) {
2 return obj1
3}
4if (condition2) {
5 return obj2
6}
7if (condition3) {
8 return obj3
9}
10if (condition4) {
11 return obj4
12}
4)除了常用方法(如getXxx、isXxx)等外,不要在条件判断中执行其它复杂的语句,将复杂逻辑判断的结果赋值给一个有意义的布尔变量名,以提高阅读性。
反例:
31if ((file.Open(fileName, "w") != null) && (...) && (...)) {
2 ...
3}
正例:
41boolean existed = (file.Open(fileName, "w") != null) && (...) && (...);
2if (existed) {
3 ...
4}
5)循环体中的语句要考虑性能,以下操作尽量移至循环体外处理,如定义对象、变量、获取数据库连接,进行不必要的try-catch操作。
1)类、类属性、类方法的注释必须使用Javadoc规范,使用/**内容*/格式,不得使用 //x x x 方式。
2)所有的抽象方法(包括接口中方法)必须要用Javadoc注释,除了有返回值、参数、异常说明外,还必须指出该方法做什么事情,实现什么功能。
3)方法内部单行注释,在被注释语句上方另起一行,使用 // 注释。方法内部多行注释使用 /* */ 注释,注意与代码对齐。
4)所有枚举类型字段必须要有注释,说明每个数据项的用途。
5)代码修改的同时,注释也要进行相应的修改,尤其是参数、返回值、异常、核心逻辑等的修改。
1)异常不要用来做流程控制、条件控制,因为异常的处理效率比条件分支低。
2)对大段代码块使用try-catch是不负责任的表现,catch时请分清稳定代码和非稳定代码,稳定代码指的是无论如何不会出错的代码。对于非稳定代码的catch尽可能进行区分异常类型,再做对应的异常处理。
3)捕获异常是为了处理它,不要捕获了却什么都不处理儿抛弃之。如果不想处理它,请把该异常抛给它的调用者。最外层的业务使用者,必须处理异常,将其转化为用户可理解的内容。
4)有try块放到了事务中,catch异常后,如果需要回滚事务,一定要注意手动回滚事务。
5)finally块必须对资源对象、流对象进行关闭,有异常也要做try-catch。
6)不能在finally块中使用return,finally块中的return返回后方法结束执行,不会再执行try块中的return语句。
finally无论如何都会执行
1)日志文件推荐至少保存15天,因为有些异常具备以“周”为频次发生的特点。
2)对trace、debug、info级别的日志输出,必须使用条件输出形式或者使用占位符的方式。
说明:logger.debug("Processing trade with id: " + id + " symbol: " + symbol);
如果日志级别是 warn,上述日志不会打印,但是会执行字符串拼接操作,如果 symbol 是对象,
会执行 toString()方法,浪费了系统资源,执行了上述操作,最终日志却没有打印。
正例:
71// 条件
2if (logger.isDebugEnabled()) {
3 logger.debug("Processing trade with id: " + id + " symbol: " + symbol);
4}
5
6// 占位符
7logger.debug("Processing trade with id: {} symbol : {} ", id, symbol);
3)谨慎记录日志,避免重复打印日志,浪费磁盘空间。
思考:这些日志真的有人看吗?看到这条日志你能做什么?能不能给问题排查带来好处?
4)异常信息应该包含两类信息:案发现场信息和异常堆栈信息。如果不处理,那么通过关键字throws往上抛出。
1)表达是与否概念的字段,必须使用is_xxx的方式命名,数据类型是unsigned tinyint (1表示是,0表示否)。
2)表名、字段名必须使用小写字母或数字,禁止出现数字开头,禁止两个下划线中间只出现数字。数据库字段名的修改代价很大,因为无法进行预发布,所以字段名称需要慎重考虑。
反例:ReturnOrder / userAddress / level_3_name
正例:return_order / user_address / level3_name
3)表名不使用复数名词。表名只是实体内容,不应该表示实体数量。
4)禁用保留字,如desc、range、match、delayed等。详细请参考MySQL官方保留字。
5)主键索引名为pk_字段名;唯一索引名为uk_字段名;普通索引名为idx_字段名;联合索引名为idx_字段名1_字段名2。
6)小数类型为decimal,禁止使用float和double。
float和double在存储时,会存在精度损失的问题。如果存储的数据范围超过decimal的范围时,建议将数据拆成整数和小数分开存储。
7)如果存储的字符串长度几乎相等,使用char定长字符串类型。
8)varchar是可变字符类型,不预先分配存储空间,长度不要超过5000。如果存储长度大于此值,定义字段类型为text,独立出来一张表,用主键来对应,避免影响其它字段索引效率。
9)表必备六个字段:id、is_delete、create_time、update_time、delete_time、modify_time。id必为自增主键,类型为unsigned bigint,基本上不能用为业务逻辑(考虑分库分表情况);create_time、update_time、delete_time类型均为integer(10);modify_time的类型为timestamp,自更新(ON UPDATE CURRENT_TIMESTAMP)。
10)如果修改字段含义或对字段表示的状态追加时,需要及时更新字段注释。
11)字段允许适当冗余,以提高查询性能,但必须考虑数据一致性。冗余字段应遵循:
A、不是频繁修改的字段
B、不是varchar超长字段,更不能是text字段
12)单表行数超过500万行或者单表容量超过2GB,才推荐分库分表。
如果预计3年后的数据量根本达不到这个级别,请不要在创建表时就考虑分库分表。
13)使用合适的字符存储长度,不但能节约数据表空间、节约索引存储,更重要的是提升索引速度。
1)业务上具有唯一特性为字段,即使是多个字段的组合,也必须建成唯一索引。
说明:不要以为唯一索引影响了insert速度,这个速度损耗可以忽略不计,但大大提高了查找速度;另外,即使应用层做了非常完善的校验控制,只要没有唯一索引,根据墨菲定律,必然会有脏数据产生。
2)超过三个表禁止join。需要join的字段,数据类型必须绝对一致;多表关联查询时,保证被关联的字段需要有索引。
说明:即使双表join也要注意表索引、SQL性能。
3)在varchar字段上建立索引时,必须执行索引长度,没必要对全字段建立索引,根据实际文本区分度决定索引长度即可。
说明:索引的长度和区分度是一对矛盾体,一般对字符串类型数据,长度为20的索引,区分度会高达90%以上,可以使用 count(distinct left(列名, 索引长度))/count(*) 的区分度来确定。
4)页面搜索严禁左模糊或者全模糊,如果需要请走搜索引擎来解决。
说明:索引文件具有B-Tree的最左前缀匹配特性,如果左边的值未确定,那么无法使用此索引。
5)如果有 order by 的场景,请注意利用索引的有序性。order by 最后的字段是组合索引的一部分,并且放在索引组合顺序的最后,避免出现 file_sort 的情况,影响查询性能。
正例:where a=? and b=? order by c; 索引:a_b_c
反例:索引中有范围查询,那么索引有序性无法利用,如:where a>10 order by b; 索引 a_b 无法排序。
6)SQL性能优化目标:至少要达到range级别,要求是ref级别,如果是consts最好。
A、index,索引物理文件全扫描,速度非常慢
B、range,对索引进行范围检索
C、ref,使用普通的索引(normal、index)
D、consts,单表中最多只有一个匹配行(主键或者唯一索引)
7)创建索引时避免有如下极端误解:
A、宁滥勿缺,误认为一个查询就需要建一个索引
B、宁缺毋滥,误认为索引会消耗空间、严重拖慢更新和新增速度
C、抵制唯一索引,误认为业务的唯一性一律需要在应用层通过“先查后插”方式解决
1)不要使用 count(列名) 或 count(常量) 来代替 count(*)。
说明:count(*)会统计值为NULL的行,而count(列名)不会统计此列为NULL值的行。
2)count(distinct col) 计算该列除NULL之外的不重复列数,注意 count(distinct col1, col2) 如果其中一列全为NULL,那么即使另一列有不同的值,也返回0。
3)当某一列的值全是NULL时,count(col)的返回结果为0,但sum(col)的返回结果为NULL,因此使用sum()时需要注意NPE(空指针异常)问题。
正例:SELECT IF(ISNULL(SUM(g)), 0, SUM(g)) FROM table;
4)在代码中写分页查询逻辑时,若count为0时应直接返回,避免执行后面的分页语句。
5)不得使用外键与级联,一列外键概念必须在应用层解决。
说明:外键与级联更新适用于单机低并发,不适合分布式、高并发集群;级联更新是强阻塞,存在数据库更新风暴的风险;外键影响数据库更新速度。
6)禁止使用存储过程,存储过程难以调试和扩展,更没有移植性。
7)IN 操作能避免则避免,若实在避免不了,需要仔细评估 IN 后面的集合元素数量,控制在1000个之内。
1)在表查询中,一律不要使用 * 作为查询的字段列表,需要哪些字段必须明确写明。
说明:A、增加查询分析器解析成本;B、增减字段容易与resultMap配置不一致
2)不要用resultClass当返回参数,即使所有类属性名与数据库字段一一对应,也需要定义。
说明:配置映射关系,使数据库字段与DO类解耦,方便维护
3)更新数据表记录时,必须同时更新记录对应的update_time为当前时间。
4)不要写一个大而全的数据更新接口。不管是不是自己的目标更新字段,都进行 update table set c1=value1,c2=value2,c3=value3; 这是错误的做法。执行SQL时,不要更新无改动的字段,一是易出错;二是效率低;三是增加binlog存储。