二. 异常日志
1. 错误码
- 错误码为字符串类型,共 5 位,分成两个部分:错误产生来源+四位数字编号
错误产生来源分为 A/B/C,A 表示错误来源于用户,比如参数错误,用户安装版本过低,用户支付超时等问题;B 表示错误来源于当前系统,往往是业务逻辑出错,或程序健壮性差等问题;C 表示错误来源于第三方服务,比如 CDN 服务出错,消息投递超时等问题;
2. 异常处理
- 事务场景中,抛出异常被 catch 后,如果需要回滚,一定要注意手动回滚事务。
- 不要在 finally 块中使用 return。
- 在调用 RPC、二方包、或动态生成类的相关方法时,捕捉异常必须使用 Throwable类来进行拦截。
- 方法的返回值可以为 null,防止 NPE 是调用者的责任
- 防止 NPE,是程序员的基本修养
3. 日志
- 应用中不可直接使用日志系统(Log4j、Logback)中的 API,而应依赖使用日志框架(SLF4J、JCL–Jakarta Commons Logging)中的 API,使用门面模式的日志框架,有利于维护和各个类的日志处理方式统一。
private static final Logger logger = LoggerFactory.getLogger(Test.class);
- 所有日志文件至少保存 15 天,对于当天日志,以“应用名.log”来保存,保存在/home/admin/应用名/logs/目录下,过往日志格式为: {logname}.log.{保存日期},日期格式:yyyy-MM-dd
以 aap 应用为例,日志保存在/home/admin/aapserver/logs/aap.log,历史日志名称为aap.log.2016-08-01
- 应用中的扩展日志(如打点、临时监控、访问日志等)命名方式:
appName_logType_logName.log。logType:日志类型,如stats/monitor/access 等;logName:日志描述。 - 在日志输出时,字符串变量之间的拼接使用占位符的方式(性能更好)。
:logger.debug("Processing trade with id: {} and symbol: {}", id, symbol);
- 对于 trace/debug/info 级别的日志输出,必须进行日志级别的开关判断
// 如果判断为真,那么可以输出 trace 和 debug 级别的日志
if (logger.isDebugEnabled()) {
logger.debug("Current ID is: {} and name is: {}", id, getName());
}
- 异常信息应该包括两类信息:案发现场信息和异常堆栈信息。
:logger.error("inputParams:{} and errorMessage:{}", 各类参数或者对象 toString(), e.getMessage(), e);
- 注意日志输出的级别,error 级别只记录系统逻辑出错、异常或者重要的错误信息。可以使用 warn 日志级别来记录用户输入参数错误的情况,避免用户投诉时,无所适从。
三. 单元测试
- 单元测试应该是全自动执行的,并且非交互式的。测试用例通常是被定期执行的,执行过程必须完全自动化才有意义。输出结果需要人工检查的测试不是一个好的单元测试。单元测试中不准使用 System.out 来进行人肉验证,必须使用 assert 来验证。
- 对于单元测试,要保证测试粒度足够小,有助于精确定位问题。单测粒度至多是类级别,一般是方法级别。
- 单元测试代码必须写在如下工程目录:src/test/java,不允许写在业务代码目录下。
四. 安全规约
- 隶属于用户个人的页面或者功能必须进行权限控制校验。
- 用户请求传入的任何参数必须做有效性验证
- 用户请求传入的任何参数必须做有效性验证
- 在使用平台资源,譬如短信、邮件、电话、下单、支付,必须实现正确的防重放的机制,如数量限制、疲劳度控制、验证码校验,避免被滥刷而导致资损。
五. MySQL
1. 建表规约
- 表达是与否概念的字段,必须使用 is_xxx 的方式命名,数据类型是 unsigned tinyint(1 表示是,0 表示否)。
- 表名、字段名必须使用小写字母或数字,禁止出现数字开头,禁止两个下划线中间只出现数字。
- 表名不使用复数名词。
- 主键索引名为 pk_字段名;唯一索引名为 uk_字段名;普通索引名则为 idx_字段名。
- 小数类型为 decimal,禁止使用 float 和 double(否则有精度损失的风险)。
- 表必备三字段:id, create_time, update_time。
其中 id 必为主键,类型为 bigint unsigned、单表时自增、步长为 1。create_time, update_time的类型均为 datetime 类型,前者现在时表示主动式创建,后者过去分词表示被动式更新。
- 表的命名最好是遵循“业务名称_表的作用”。库名与应用名称尽量一致。
- 单表行数超过 500 万行或者单表容量超过 2GB,才推荐进行分库分表。
2. 索引规约
- 业务上具有唯一特性的字段,即使是组合字段,也必须建成唯一索引。(即使在应用层做了非常完善的校验控制,只要没有唯一索引,根据墨菲定律,必然有脏数据产生。)
- 超过三个表禁止 join。需要 join 的字段,数据类型保持绝对一致;多表关联查询时,保证被关联的字段需要有索引。
- 在 varchar 字段上建立索引时,必须指定索引长度,没必要对全字段建立索引,根据实际文本区分度决定索引长度。
- 利用延迟关联或者子查询优化超多分页场景。
MySQL 并不是跳过 offset 行,而是取 offset+N 行,然后返回放弃前 offset 行,返回 N 行,那当offset 特别大的时候,效率就非常的低下,要么控制返回的总页数,要么对超过特定阈值的页数进行 SQL改写。
// 先快速定位需要获取的 id 段,然后再关联:
SELECT t1.* FROM 表 1 as t1, (select id from 表 1 where 条件 LIMIT 100000,20 ) as t2 where t1.id=t2.id
- SQL 性能优化的目标:至少要达到 range 级别,要求是 ref 级别,如果可以是 consts最好。
3. SQL语句
- 不要使用 count(列名)或 count(常量)来替代 count(*)
- count(distinct col) 计算该列除 NULL 之外的不重复行数,注意 count(distinct col1, col2) 如果其中一列全为 NULL,那么即使另一列有不同的值,也返回为 0。
- 当某一列的值全是 NULL 时,count(col)的返回结果为 0,但 sum(col)的返回结果为NULL,因此使用 sum()时需注意 NPE 问题
SELECT IFNULL(SUM(column), 0) FROM table;
- 使用 ISNULL()来判断是否为 NULL 值, NULL 与任何值的直接比较都为 NULL。
- 代码中写分页查询逻辑时,若 count 为 0 应直接返回,避免执行后面的分页语句
- 对于数据库中表记录的查询和变更,只要涉及多个表,都需要在列名前加表的别名(或表名)进行限定。
- SQL 语句中表的别名前加 as,并且以 t1、t2、t3、…的顺序依次命名。
- in 操作能避免则避免,若实在避免不了,需要仔细评估 in 后边的集合元素数量,控制在 1000 个之内。
4. ORM映射
- 在表查询中,一律不要使用 * 作为查询的字段列表,需要哪些字段必须明确写明。
- 不要用 resultClass 当返回参数,即使所有类属性名与数据库字段一一对应,也需要定义<resultMap>;反过来,每一个表也必然有一个<resultMap>与之对应。
- sql.xml 配置参数使用:#{},#param# 不要使用${} 此种方式容易出现 SQL 注入。
- 更新数据表记录时,必须同时更新记录对应的 update_time 字段值为当前时间。
- @Transactional 事务不要滥用。事务会影响数据库的 QPS,另外使用事务的地方需要考虑各方面的回滚方案,包括缓存回滚、搜索引擎回滚、消息补偿、统计修正等。