程序员编码最佳实践

1、代码

1.1、不要在for循环中调用数据库查询(调用接口)。

因为for循环的次数是不确定的,开发的时候你认为可能只会循环10次左右,压力不大。但是线上数据可能会循环1千次甚至1万次,这将对数据库或第三方系统造成极大的压力。如图下所示,循环调用调用了selectList方法。

image-20210318112731913

正确的做法是:

  • 数据库:用in关键词来做批量查询,如果in的数量超过500,还需分批查询再汇总。
  • 第三方接口:应该新增一个批量查询的接口。

1.2、正确的打印异常

打印异常一定至少要有两个关键信息:

  1. 时间
  2. 错误的堆栈信息

我们不能依赖`e.printStackTrace来输出堆栈信息,他并没纳入日志输出框架,也没有创建时间。

1
2
3
4
5
6
try{
//code xxx
}catch( Exception e){
//错误的做法
e.printStackTrace();
}

正确是做法是必须使用日志框架,且还需要附上出错位置的信息,这样能方便定位。甚至还可以附上一些关键信息。

1
2
3
4
5
6
7
8
try{
// code xxx
}catch( Exception e){
//正确的做法1
log.error("add account error:",e);
//正确的做法2
log.error("add account error. OutOrderCode:"+outOrderCode,e);
}

1.3正确的处理异常

  1. 异常不能被自己吞掉,这样一旦出了问题,日志上没有反馈,非常难排查。

  2. 如果需要回滚,那么即使你捕捉了异常之后,还是需要往上层throw,否则不会出发事务回滚。

  3. 如果调用第三方接口(feign)返回的结果是失败。那么你需要在本地判断返回结果,然后抛出异常,否则不会回滚。但是如果这个第三方接口不重要可以不判断结果,这种情况下建议使用线程池异步执行。

    1
    2
    3
    4
    5
    6
    //正确的做法
    ResultInfo<> result = client.addAccount(account);
    if (result.getCode() != ResultCode.SUCCESS.getCode()) {
    //抛出异常时,记得付上第三方的失败信息。这样非常容易排查问题。
    throw new ServiceException("入账失败," + addAccountResult.getMsg());
    }
  4. 业务异常直接使用ServiceException来封装,他会将你的提示信息优化的返回给用户。

1.4、及时删除废弃的代码

注释或者废弃的代码需要删掉。既然他没参与运行了,留在那里只会成为垃圾。

1.5、记得合并重复代码

提高复用率

1.6、不要随意修改配置文件(feign)

不同的环境(测试、生产)对应不同的配置文件。有的人为了方便,直接将本地环境的配置文件改成了测试服务器的地址。然后又不消息提交到了正式环境,特别是前端项目。如此一来,其他人就会错误的使用了被修改的本地配置文件,导致开发时出现了非预期的现象。排查问题占用了时间。

前端(angular)可以使用下面的命令来使用不同环境的配置文件:

1
2
3
ng serve -e test
//或者(不同angular-cli有些许区别)
ng serve -c test

另外一点是一定不能修改feign的地址。比如:

1
2
3
4
5
6
7
//原本: 
//@FeignClient(path = "pms-base", name = "pms-base")
//下面是错误的做法
@FeignClient(url = "http://192.168.10.204:9022/pms-biz")
public interface BaseClient {
// code xxx
}

如果这个配置的改动再测试环境都体现不出问题,只能上正式环境才能看出问题。但是当正式环境出问题的时候就已经完了,而且这种问题很难排查。

1.7、记得写上注释,越详细越好

  1. 在方法上做注释,说明该方法的作用,场景和注意事项。
  2. 对于业务逻辑复杂的业务,代码中也要写注释,而且越详细越好,方便后面维护代码。

1.8、对于不重要的依赖要解耦

比如入住、退房、入账等核心流程可能会依赖一些不重要的边缘服务,例如上报信息、记录日志等。如果边缘服务宕机了,不应该影响主流程的使用。所以对于边缘服务的调用应该解耦,要么使用mq要么使用线程池异步调用。

1.9、不要使用statement或者$符号,小心sql注入

sql注入的原理可以看这里。为了避免sql注入的情况发生,禁止在mybatis模板使用中$符号,除非该参数不是外部传入的。例如下面这个表名虽然使用了${dynamicTableName},但是该值是内部产生的,所以可以使用。但是千万不能把$符号放到外部的参数上。

1
2
3
<update id="fakeDelete" parameterType="jte.pms.biz.model.FoodTicket">
UPDATE ${dynamicTableName} set is_delete=#{isDelete} where hotel_code=#{hotelCode} and order_code=#{orderCode}
</update>

1.10、正确的使用feign

调用feign一定要判断结果,如果失败,需要将返回的失败原因告知调用者。最好使用异常的方式包装错误信息。

1
2
3
4
5
6
//正确的做法
ResultInfo<> result = client.addAccount(account);
if (result.getCode() != ResultCode.SUCCESS.getCode()) {
//抛出异常时,记得付上第三方的失败信息。这样非常容易排查问题。
throw new ServiceException("入账失败," + addAccountResult.getMsg());
}

对于不需要判断结果的feign调用,肯定该接口不够重要(因为调用成功还是失败你都没关心),所以应该改成异步调用,或者说使用mq解耦。

1.11、不要在视图模板中加入任何复杂的逻辑

不要将业务逻辑放到controllercontroller只做参数的校验。业务逻辑应该统统写到service,就算仅仅只是一个查询服务。

1.12、定时任务不要扎堆运行

编写动态的定时任务时,需要对定时任务有一个大概的判断:上线之后会不会集中运行。集中运行是指成百上千个同类型的任务在同一个时刻被触发,这种情况下数据库是扛不住这种并发的。正确的做法应该是使用mq来消峰。定时任务触发之后,仅仅只是把这个任务投递到MQ,具体执行任务要给下游将这个mq消息消费掉。

定时任务Mq消峰

1.13、一定要校验参数

对于提供给外部的接口都要做参数校验,比如该参数是否为必填。如果不做参数校验可能会发生严重后果。比如下面有一段mybaits的sql。

1
2
3
4
5
6
<sql>
select * from t_user
<if test="id != null">
id = #{id}
</if>
</sql>

id本应该是一个必填参数,但是调用者并未填写该参数。那么将会执行一个恐怖的全表查询。假设这个表中有几百万条数据,那么结果将是灾难性的。

1
2
---恐怖的全表扫描
select * from t_user;

1.14、使用全局静态变量时,要考虑并发、内存、集群的因素

有时我们需要维护一些全局变量,比如调用第三方网关的token。此时要考虑集群和并发的因素。

此外假设这个全局变量是一个集合,那么要小心这个集合内的元素数量只增不减。此时应该使用GuavaCache来代替原生的集合。

1.15、调用网关不再使用万能钥匙

万能钥匙不安全,禁止再使用!

1.16、setInteval要慎用

前端使用setInterval时其实就相当于是一个for循环,假设使用完成之后没有及时清理将会是一个灾难。特别是在循环中还涉及到了接口的调用,这很容易将自己搞死。

1.17、不要错误的应用setTimeout

很多时候我们需要一个操作完成的通知,在这个情况下使用setTimeout是错误的行为。因为延迟n秒之后,这个操作不一定完成了。最好的办法是使用rxjsSubject进行通知。

1.18、加载界面要有loading效果

所有的数据加载(表格)必须要有loading效果。如果是操作按钮,也要有loading效果,而且要防止重复点击。

2、数据库

2.1、多表关联不要超过2个表

多表查询的关联健需要有索引,最好是唯一索引。超过2张表以上的多表关联会很慢。

2.2、如无必要,请不要order by或者在java中进行排序

当我们需要查询一个精确的记录,或者不需要分页时,不要用order by。

1
2
//命名是查一个精确的记录,完全不要使用order by
select * from t_order where order_code ='xxx' and hotel_code='aaa' order by create_time

2.3、确保不要查出过多的数据(不超过3万条)

查出过多的数据会导致java内存溢出,或者频繁的垃圾回收。所以在写sql的时候需要预估影响。如果觉得可能查出的记录数比较多,应该加上时间范围的限制(业务上做取舍)。

2.4、sql一定需要使用上索引

使用explian查看是否sql是否用上了索引

2.5、扫描的行数不能超过10万

使用explian查看扫描的行数

2.6、in的数量不要超过500

in的数量太多可能导致字段不走索引,所以要分批次来in。

2.7、禁止使用左模糊和全模糊查询

会导致无法使用索引

2.8、禁止使用数据库保留关键字作为别名、字段名、表名

防止出现奇怪的问题。

2.9、Mybatis中禁止使用association

如下图所示:假设主记录中查询到了1000条记录,那么相应的getImgList也会执行1000次,这将给mysql带来巨大的压力。

mybatis association

2.10、所有的表必须要有create_timeupdate_time

这两个字段的存在是为了方便排查问题,相当于这条数据的元信息。

2.11、修改和删除数据要根据主键或者唯一索引

如果未使用主键或唯一索引字段更新和删除数据会锁表,严重的情况下会导致数据的死锁。

2.12、当插入行数超过500条时,需要分批插入

频繁执行单条insert效率较低,如果需要大量插入数据,应该使用批量插入语句。

原文链接:https://www.jdkdownload.com/programing_best_practice.html