目录

[TOC]

1. Java 代码规范

1.1. 模块划分规范

大型项目中的服务可以按照模块划分,模块划分规则为一个大的具体的功能服务单独为一个模块,公共共有工具为一个模块,其他支撑功能为单独的模块(如消息,缓存等)。模块之间的依赖关系应为单层依赖,不应出现循环依赖。建议公共的被所有业务模块依赖,公共模块不依赖业务模块。
业务模块之间的依赖在业务设计时做好主次关系,若确实有主业务模块调用子业务模块的情况,应考虑使用观察者模式(如Spring的ApplicationEvent-ApplicationListener,发布订阅模型等);
在小型项目中可以直接使用单模块项目,各业务的区分在各自的Controller中体现,但服务Service之间的调用需要注意,应保持一定的主从依赖关系,保证业务开发变更时不会相互影响。

1.2. 服务分层规范

为保证服务的复用性,结构层次分明,各层次职责分明,定义Controller层,Service层,持久层的规范

1.2.1. Controller层

Controller层主要用于参数校验,访问权限等服务调用之前的前置操作,也用于获取用户信息等有状态的数据。Controller层同时也用于组装统一返回结果Result。
Controller层不应承担业务逻辑,业务逻辑应该下移至Service层。

正例:

1
2
3
4
5
6
7
8
9
10
@GetMapping("/getVisibleMenus")
@AuthMenu //接口访问权限校验
public Result<List<MenuResp>> getVisibleMenus( @Validated BusinessAuthorityEditReq req ) { //数据校验
//获取登录用户权限
Collection<String> userAuthorities = CurrentUserContext.getCurrentUserAuths();
//根据权限查询有权限的菜单数据
List<MenuResp> menuTree = sysMenuAuthUnify.getCurrentUserMenuTree(userAuthorities);
//组装统一返回结果
return Result.success(menuTree);
}

反例: controller中承担大量逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public BaseResponse<List<MenuResp>> getVisibleMenus1() {
//获取登录用户权限
Collection<String> userAuthorities = CurrentUserContext.getCurrentUserAuths();
//根据权限查询有权限的菜单数据
//得到全部树
List<MenuResp> trees = sysMenuAuthUnify.getMenuTree();
//克隆副本
List<MenuResp> menuTree = BeanUtil.copyList(trees);
//将不属于自己的移除
Iterator<MenuResp> iterator = menuTree.iterator();
while (iterator.hasNext()) {
MenuResp next = iterator.next();
boolean needRemove = needRemove(next, authorities);
if (needRemove){
iterator.remove();
}
}
return menuTree;
//组装统一返回结果
return BaseResponse.success(menuTree);
}

1.2.2. Service层

Service层主要用于实现业务功能,Service返回的数据应为接口需要的数据,即正向业务数据,失败的返回应直接抛出业务异常,在统一异常处理器中处理返回。
Service层不建议加带有用户状态的对象,如从上下文中获取用户信息等数据,因为此举会减小业务服务的复用性。
各服务调用使用Service层注入,但不应出现循环依赖的情况,虽然Spring提供了循环依赖的解决方案,但循环依赖在业务中其实是有问题的,应当考虑是否出现了业务设计上的问题。

正例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public LoginResp login(LoginReq loginReq) {
String loginInfo = loginReq.getLoginInfo();

//查询用户
SysUserInfo userInfo = userInfoService.getOne(
new LambdaQueryWrapper<SysUserInfo>()
.eq(SysUserInfo::getUsername, loginInfo)
.or()
.eq(SysUserInfo::getPhone, loginInfo)
);
if (userInfo == null) {
throw new BaseException("用户不存在");
}

//todo 校验密码,此处应该抽象密码验证器
if (!userInfo.getPassword().equals(loginReq.getPassword())) {
throw new BaseException("密码不匹配");
}

UserDetail userDetail = fetchUserDetail( userInfo.getUserId(),userInfo.getNickName());

LoginResp loginResp = userDetailStore.saveUserDetail(userDetail);

return loginResp;
}

反例1: Service中获取状态信息,直接返回结果对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public Result login(LoginReq loginReq) {
String loginInfo = loginReq.getLoginInfo();

// 不应该在此获取有状态信息
UserDetail currentUser = CurrentUserContext.getCurrentUser();

SysUserInfo userInfo = userInfoService.getOne(
new LambdaQueryWrapper<SysUserInfo>()
.eq(SysUserInfo::getUsername, loginInfo).or()
.eq(SysUserInfo::getPhone, loginInfo)
);
if (userInfo == null) {
// Service中只返回正常的业务逻辑,非正常的应该使用异常外抛的形式结束请求
return BaseResponse.failed("用户不存在");
}
if (!userInfo.getPassword().equals(loginReq.getPassword())) {
return BaseResponse.failed("密码不正确");
}

UserDetail userDetail = fetchUserDetail( userInfo.getUserId(),userInfo.getNickName());
LoginResp loginResp = userDetailStore.saveUserDetail(userDetail);

return BaseResponse.success(loginResp);
}

反例2: 不应该使用Map返回。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public Map<String,Object> login(LoginReq loginReq) {
String loginInfo = loginReq.getLoginInfo();
// 不应该在此获取有状态信息
UserDetail currentUser = CurrentUserContext.getCurrentUser();

SysUserInfo userInfo = userInfoService.getOne(loginInfo);

HashMap<String, Object> map = new HashMap<String, Object>();

if (userInfo == null) {
// Service中只返回正常的业务逻辑,非正常的应该使用异常外抛的形式结束请求
map.put("error","用户不存在");
return map;
}
if (!userInfo.getPassword().equals(loginReq.getPassword())) {
map.put("error","密码不正确");
return map;
}

UserDetail userDetail = fetchUserDetail( userInfo.getUserId(),userInfo.getNickName());

LoginResp loginResp = userDetailStore.saveUserDetail(userDetail);

//不应使用 Map作为返回
map.put("data",loginResp);
return map;
}

1.2.3. 持久层

持久层用于访问数据库数据,主要与数据库的交互提供数据返回。持久层不应承担业务逻辑,只负责数据的查询及组装。

1.3. 前后端交互规范

1.3.1. 统一响应 Code

  • 成功为 200,success 为 true
  • 系统失败为 500,success 为 false
  • 业务异常根据各项目独立维护,success为false

只要是后台成功返回了没有预期之外的结果,返回code均为200(如账号不存在、密码错误等),业务是否成功是在success中体现。 —- todo 可以再讨论是否用code+success

成功:

1
2
3
4
5
6
7
8
{
"success":true,
"code":"200",
"message":"",
"data":{

}
}

失败:

1
2
3
4
5
{
"success":false,
"code":"200",
"message":""
}

其中,data为封装的返回数据,业务中返回给前端的数据均应该封装到其中返回。
后端示例返回体;

1
2
3
4
5
6
public class Result<T> {
private Integer code;
private Boolean success;
private String message;
private T data;
}

1.3.2. 统一分页参数(请求+返回参数)

请求参数:

1
2
3
4
{
"page":1,
"size":10
}

后端统一结构

1
2
3
4
5
6
7
8
9
10
11
12
//这里使用了Lombok,简化了get set
@Data
public class PageReq {
/**
* 页码
*/
protected Integer page;
/**
* 每页条数
*/
protected Integer size;
}

在有其他筛选入参条件时,入参应继承统一分页请求对象实现扩展。

1
2
3
4
5
6
7
8
9
10
11
12
{
"success":false,
"code":"200",
"message":"",
"data":{
"page":1,
"size":10,
"data":[

]
}
}

后端统一分页返回结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Data
public class PageResp {
/**
* 当前页
*/
private Integer page ;
/**
* 每页显示条数
*/
private Integer size;
/**
* 总数
*/
private Integer total ;
/**
* 查询数据列表
*/
private List<T> data;
}

返回的数据结构也应在Result返回体的data数据中。

1.3.3. 统一接口请求协议

获取数据用 GET, 新增/修改/发送数据用 POST;
注意:用户登录等涉密数据必须使用post方式请求,通过body传输数据。
GET:
get方法传参建议使用Param参数,少用path参数。当根据id查询时可以用path参数
正例:getUserById?id=1&username=aa
反例:getUserById/1/aa
POST:
Post方法应使用body传输数据,保证安全。一般使用表单传输或JSON传输,在前后端分离的情况下建议统一使用JSON传输数据。

1.4. 代码规范

整体而言,遵循阿里巴巴开源的《阿里巴巴 Java 开发手册》。除此之外针对公司的应用场景,我们在其基础上进行了补充,以形成公司的规范。

1.【推荐】对于方法的 javadoc 注释,需要对每个请求参数、返回值、类型变量、异常声明进行说明,同时对代码进行格式化,保证对齐。如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 除法操作
*
* @param first 被除数
* @param second 除数
* @return 除法操作之后的结果
* @throws FailedException 除数为0时抛出
*/
private int divide(int first, int second) throws FailedException {
if (second == 0) {
throw new FailedException();
}
return first / second;
}

2.【推荐】所有的类都必须添加创建者和创建日期

如下所示:

1
2
3
4
5
6
7
8
/**
* 用户信息
* @author user
* @since 2023/01/01 10:32
*/
public class UserInfo {

}

3.【强制】当请求进来之后首先对请求数据进行校验

校验使用统一的校验模式 @Validated ,禁止使用if-else对每个方法进行逐步校验。

正例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@PostMapping("/editMenuAuthorities")
public Result<Void> editMenuAuthorities(@RequestBody @Validated MenuAuthEditReq req){
sysMenuAuthUnify.editMenuAuthorities(req.getMenuId(),req.getAuthority());
return Result.success();
}

@Data
public class MenuAuthorityEditReq {

@NotNull
private Integer menuId;
@NotEmpty(message = "菜单名不能为空")
private String name;
@NotNull
private AuthorityObject authority;
}

反例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@PostMapping("/editMenuAuthorities")
public Result<Void> editMenuAuthorities(@RequestBody MenuAuthEditReq req){
if (req ==null){
throw new BizException("入参为空");
}
if (req.getMenuId()==null){
throw new BizException("菜单id为空");
}
if (req.getAuthority()==null){
throw new BizException("菜单权限为空");
}
sysMenuAuthUnify.editMenuAuthorities(req.getMenuId(),req.getAuthority());
return Result.success();
}

4.【推荐】当 GET 请求参数超过 3 个,或者参数超过 1 个校验规则的时候,需要封装成独立的类。

如果请求参数包含数组类型,那么使用 POST。如果是 controller 层的接口,request 的注释写成请求参数,response 的注释写成返回值。如下:

1
2
3
4
5
6
7
8
9
10
/**
* 修改密码
*
* @param request 请求参数
* @return 返回值
*/
@PostMapping("/updatePassword")
public Result<Void> updatePassword(@RequestBody UpdatePasswordMicReq request){

}

5.【推荐】Controller 层方法的返回类必须为统一返回Result,service 层方法的返回类必须为业务数据对象(原因:Service 方法可以达到复用的目的)。
6.【推荐】 Controller的@RequestParam建议写上,否则框架会做更多的低性能处理匹配字段。
7.【强制】 禁止使用Map作为接口的返回及入参,Map导致接口语义不清晰,需要跟踪代码才能清楚含义;
8.【推荐】暴露的服务返回数据字段一定是要都有意义的;
9.【强制】程序里使用到的常量信息应集中在常量类中,不要在每个单独的业务类中定义。
10.【推荐】请求参数尽量封装成请求类,避免在Controller中写入过多的请求参数以及校验规则。

反例

1
2
3
4
5
6
7
8
9
10
11
@GetMapping("idAuth")
public Response<CreditFacedRes> idAuth(
@NotBlank(message = "身份证号码不能为空")
@Pattern(regexp = ID_CARD_REGEXP, message = "身份证号码格式错误")
@RequestParam("idCard") String idCard,
@NotBlank(message = "姓名不能为空")
@Size(min = 2, max = 30, message = "姓名字数必须位于2到30之间")
@RequestParam("realName") String realName
) {

}

正例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

@GetMapping("idAuth")
public Response<CreditFacedMicRes> idAuth(
@RequestHeader(@Validated IdAuthReq request) {
// codes is here
}

@Data
public class IdAuthReq {

@NotBlank(message = "身份证号码不能为空")
@Pattern(regexp = ID_CARD_CONSTRAINT_REGEXP, message = "身份证号码格式错误")
private String idCard;

@NotBlank(message = "姓名不能为空")
@Size(min = 2, max = 30, message = "姓名字数必须位于2到30之间")
private String realName;
}

11.【强制】前端返回的具有特定意义的枚举类型应当使用枚举类型,不应该使用数字类型作为返回。
12.【强制】枚举类型不应用数字表示,更不应在代码中用魔法值匹配,需统一位置定义枚举类;
13.【推荐】List 的遍历操作,应使用 Stream流式操作或者foreach 代替 fori。
14.【推荐】对象注入推荐使用构造器注入,其次考虑使用使用@Autowired 注入。当注入的 bean 存在多个实例时,配合@Qualifier(value = “”)。不宜使用@Resource,@Resource是按照里面的 name 属性来注入的。
15.【推荐】多属性注入尽量使用 ConfigurationProperties 来进行属性的配置,尽量少使用@Value 注入属性,@Value在Spring启动阶段为空会导致异常结束。

1
2
3
4
5
6
7
8
// 正例
@Component
@ConfigurationProperties(prefix = "biz.xxx")
public class ConfigBean {

private String defaultAvatar;
private String enable;
}

反例

1
2
3
// 反例
@Value("biz.xxx.defaultAvatar")
private String defaultAvatar;

16.【推荐】尽量使用 Lombok 的注解,以简化代码。
@Data @Accessors @Getter @Setter @ToString @EqualsAndHashCode @Builder @Slf4j
17.【强制】null 为空的判断,应尽量遵循自然语义,且使用双等号进行比较。

1
2
3
4
5
6
// 反例
if (null == obj) {}
// 反例
if (obj = null) {}
// 正例
if (obj == null) {}

18.【推荐】循环计数器,通常采用字母 i、j、k 或 counter,都是没问题的,可以不用根据场景而调整变量名。例如:i、j、k、counter。
19.【强制】package 行要在 import 行之前,import 中不应包含*,避免出现过多的、不被使用的依赖类。除此之外,import 的顺序应如下所示:

1
2
3
4
5
6
7
8
// 正例
package com.a.sample;

import java.io.InputStream;
import java.io.File;
import java.util.Arrays;
import java.util.ArrayList;
import java.util.HashMap;
1
2
3
4
5
// 反例
package com.a.sample;

import java.io.*;
import java.util.*;

20.【推荐】异步处理建议定义好线程池并由Spring自带 @Async注解处理,统一并简洁的管理异步;
21.【推荐】尽量使用 hutool、spring-core、apache common 等工具类,避免重复造轮子。
22.【推荐】尽量使用 mybatis plus,避免使用 pagehelper 和 tkmybatis,避免采坑。
23.【强制】减少对变量的直接访问
不要把一个类的属性暴露给其它类,尽可能通过访问方法去保护他们(静态常量可以直接暴露)。
24.【推荐】具有相同意义都需要实现相同逻辑的不同实现可以考虑抽象类或接口,采用模板方法模式;
25.【推荐】使用策略模式+模板方法模式处理同一业务不同类型,代替if-else,减小对其他服务的影响;
26.【推荐】具有相同处理逻辑的代码考虑合理抽取公共方法或工具;
27.【推荐】建议不要使用循环依赖,若出现循环依赖,大概率是服务设计划分出现了问题;
28.【推荐】模型之间建议有关联,不能都只在业务逻辑中体现;
29.【推荐】对象转换不应该在业务逻辑中大量的copy,会导致业务变更时无法快速定位哪些需要变动。
30.【推荐】减少方法的长度
通常,我们的方法应该只有尽量少的几行,太长的方法会难以理解。如果方法太长,则应该重新设计。对此,可以总结为以下原则:
- 三十秒原则:如果另一个程序员无法在三十秒之内了解你的函数做了什么(What),如何做(How),以及为什么要这样做(Why),那就说明你的代码是难以维护的,必须得到提高;
- 一屏原则:如果一个函数的代码长度超过一个屏幕,那么或许这个函数太长了,应该拆分成更小的子函数; 一行代码尽量简短,并且保证一行代码只做一件事,那种看似技巧性的冗长代码只会增加代码维护的难度。

1.5. 统一依赖版本

项目一般使用的打包工具为maven,对于gradle使用较少,但两者的理念应当相似。
统一依赖管理的原因在于各模块开发时会出现引用的依赖版本不统一的问题,当相互引用时会出现jar包冲突的问题。而且不同框架间的依赖相互之间会出现兼容性问题,导致项目出现问题。

1.5.1. 单模块项目

现在项目均承载于springboot开发,提供简洁的快速搭建环境。对于单模块项目,可以在pom中继承spring-boot-starter-parent即可,对于常见的Spring家族的依赖均有相应的合适的版本管理,只要是其中的版本就不会出现问题。

1
2
3
4
5
6
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.7.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>

1.5.2. 多模块项目

对于多模块的项目,所有的依赖版本必须在父工程pom中管理,才能保证各自模块使用的共有依赖的版本一致。
对于Spring家族的依赖,除了可以继承spring-boot-starter-parent之外,同时也可以在dependencyManagement节点中引入spring-boot-dependencies依赖,此依赖专门用于依赖管理,需要将scope设置为import表明依赖引入。
如下:

1
2
3
4
5
6
7
8
9
10
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.5.14</version>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

对于非Spring家族的依赖,也需要在依赖管理节点dependencyManagement中管理,内部引入的依赖只定义版本,不做真正的引入,真正的引入是在各子模块中执行。同一般的引入区别在于:子模块只需要引入依赖即可,不再定义版本。

1
2
3
4
5
6
7
8
9
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.83</version>
</dependency>
</dependencies>
</dependencyManagement>

子模块pom中

1
2
3
4
5
6
<dependencies>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
</dependency>
</dependencies>

1.6. 建立公共的基础架构

除了文字做以上规范外,项目组还可以搭建一个公共的基础框架平台用以集成以上部分约定,各应用可直接在框架中开发业务。
公共框架目的如下
1.统一定义外部框架的版本依赖
2.统一实现通用工具,避免重复造轮子
3.统一实现通用的功能。
4.统一管理框架的部分功能配置,根据公司特点配置应当公用的配置,减小业务可自定义范围。
5.开发公共的功能模块,业务系统以最小的代价引入即可使用,不必再花时间研究框架的特性等,同时也避免了各业务系统的实现不一致,导致了相同的功能增加了更多的差异化。

1.6.1. 定制common模块

将基础的共有规范定义在common模块中,包括统一返回结构体Result、统一分页入参PageReq、统一分页返回PageResp、等
定义统一的web异常处理,将业务异常统一封装后以返回体的形式返回给前端,业务中的非正向流程只需要统一抛异常。
定义好jackson的入参返回序列化格式等,在基础中做好统一配置。
定义好是否使用swager等文档工具。
定义好公共的通用工具模块。定义好应用中使用到的的工具如hutool、commons-lang3、commons-collection、json序列化工具等,自己写的工具一定是非常通用的,否则放在业务中自行实现。
统一定义好全局日志流水跟踪号,之后的项目中均可以根据跟踪号跟踪请求的路径以便快速的发现问题。

1.6.2. 定制权限模块

统一制定一套权限体系,实现权限通用逻辑,并预留好扩展空间可以实现业务增加自定义权限方案。同时定制好默认的登录逻辑并保留扩展。

1.6.3. 定制持久层功能模块

选择好数据库的驱动依赖,使用的数据库连接池(druid/c3p0/hikari等)、持久层使用的框架(mybatis/hibernate)等。还可以定义开发环境使用sql打印框架p6spy,生产不使用此框架以提高性能等类似的配置。
开发者只需在中央仓库引入此模块,同时指定数据库连接地址和账号密码等信息即可使用持久层开发,不用再关注使用什么框架技术等,同时也统一了持久层规范。

1.6.4. 定制开发工具模块

代码生成器模块统一生成逻辑,统一基础模板(已有实现)

1.6.5. 定制基础支撑模块

添加日志功能,在统一的地方记录错误日志或全部打印的日志至数据库。

1.6.6. 定制定时任务功能模块

选择好定时任务使用的框架,统一做好定时任务的逻辑,开发若有需要只需引入即可(已有引入quartz框架,通过数据库配置定时任务的功能)

1.6.7. 说明

其他类似的有独立功能也可以以此类似的思想加入到基础框架中来,使得项目开发中使用到的技术都趋近于一致,同时方便技术交流和共同探讨解决问题,在工作交接中也可以增加效率。

2. 数据库规范

2.1. 库设计规范

  • 所有表必须使用Innodb存储引擎:没有特殊要求(即Innodb无法满足的功能如:列存储,存储空间数据等)的情况下,所有表必须使用Innodb存储引擎(mysql5.5之前默认使用Myisam,5.6以后默认的为Innodb)Innodb 支持事务,支持行级锁,更好的恢复性,高并发下性能更好
  • 数据库和表的字符集统一使用utf8mb4:兼容性更好,统一字符集可以避免由于字符集转换产生的乱码,不同的字符集进行比较前需要进行转换会造成索引失效。
  • 所有表和字段都需要添加注释:使用comment从句添加表和列的备注从一开始就进行数据字典的维护。
  • 尽量控制单表数据量的大小,建议控制在500万以内:500万并不是MySQL数据库的限制,过大会造成修改表结构,备份,恢复都会有很大的问题;可以用历史数据归档(应用于日志数据),分库分表(应用于业务数据)等手段来控制数据量大小。
  • 谨慎使用MySQL分区表:分区表在物理上表现为多个文件,在逻辑上表现为一个表。谨慎选择分区键,跨分区查询效率可能更低,建议采用物理分表的方式管理大数据。
  • 尽量做到冷热数据分离,减小表的宽度:MySQL限制每个表最多存储4096列,并且每一行数据的大小不能超过65535字节,减少磁盘IO,保证热数据的内存缓存命中率(表越宽,把表装载进内存缓冲池时所占用的内存也就越大,也会消耗更多的IO)。更有效的利用缓存,避免读入无用的冷数据 经常一起使用的列放到一个表中(避免更多的关联操作)
  • 禁止在表中建立预留字段:预留字段的命名很难做到见名识义。预留字段无法确认存储的数据类型,所以无法选择合适的类型。对预留字段类型的修改,会对表进行锁定。
  • 禁止在数据库中存储图片,文件等大的二进制数据:通常文件很大,会短时间内造成数据量快速增长,数据库进行数据库读取时,通常会进行大量的随机IO操作,文件很大时,IO操作很耗时 通常存储于文件服务器,数据库只存储文件地址信息
  • 禁止在线上做数据库压力测试。
  • 禁止从开发环境,测试环境直接连接生成环境数据库。

2.2. 表设计规范

  • 所有数据库对象名称必须使用小写字母并用下划线分割
  • 所有数据库对象名称禁止使用mysql保留关键字(如果表名中包含关键字查询时,需要将其用单引号括起来)
  • 数据库对象的命名要能做到见名识意,并且最后不要超过32个字符
  • 临时库表必须以tmp_为前缀并以日期为后缀,备份表必须以bak_为前缀并以日期(时间戳)为后缀。
  • 所有存储相同数据的列名和列类型必须一致(一般作为关联列,如果查询时关联列类型不一致会自动进行数据类型隐式转换,会造成列上的索引失效,导致查询效率降低)
  • 优先选择符合存储需要的最小的数据类型
  • 避免使用TEXT、BLOB数据类型,最常见的TEXT类型可以存储64k的数据
  • 避免使用ENUM类型
  • 尽可能把所有列定义为NOT NULL
  • 使用DATETIME类型(8个字节)存储时间,timestamp即将在2038年用完。
  • 同财务相关的金额类数据必须使用decimal类型。
  • 必须建立显示的主键。

常用表字段命名:

常用字段定义命名如下:

字段名称 字段类型 字段含义
deleted tinyint(1) 逻辑删除字段
sort / xxx_sort int(11) 排序号
${xxx}_time datetime 用于表示时间戳(精确到秒)
${xxx}_id bigint(20) 关联的业务 id
${xxx}_type tinyint 某某类型,字段注释应明确枚举值和描述
${xxx}_state tinyint 某某状态(比如:订单状态),字段注释应明确枚举值和描述

表必备三字段:id, create_time, update_time,其他字段不做规定。但小组可约定组内的公用字段,比如:create_id,update_id。

2.3. 索引设计规范

  • 限制每张表上的索引数量,建议单张表索引不超过5个
  • 禁止给表中的每一列都建立单独的索引
  • 常见索引建议
    • 出现在SELECT、UPDATE、DELETE语句的WHERE从句中的列
    • 包含在ORDER BY、GROUP BY、DISTINCT中的字段
    • 不要将符合两个查询条件的字段都建立索引,通常建立联合索引。
    • 多表join的关联列
  • 如何选择索引列的顺序
    • 建立索引的目的是:希望通过索引进行数据查找,减少随机IO,增加查询性能 ,索引能过滤出越少的数据,则从磁盘中读入的数据也就越少。
    • 区分度最高的放在联合索引的最左侧(区分度=列中不同值的数量/列的总行数);
    • 尽量把字段长度小的列放在联合索引的最左侧(因为字段长度越小,一页能存储的数据量越大,IO性能也就越好);
    • 使用最频繁的列放到联合索引的左侧(这样可以比较少的建立一些索引)。
  • 避免建立冗余索引和重复索引
    • 重复索引会增加查询优化器生成执行计划的时间。
    • 重复索引示例:primary key(id)、index(id)、unique index(id)
    • 冗余索引示例:index(a,b,c)、index(a,b)、index(a)
  • 优先考虑覆盖索引
    • 对于频繁的查询优先考虑使用覆盖索引。覆盖索引包含了所有查询字段(where,select,ordery by,group by包含的字段)的索引。
    • 可以避免Innodb表进行索引的回表二次查询。
  • 尽量避免使用外键约束
    • 不建议使用外键约束(foreign key),但一定要在表与表之间的关联键上建立索引;
    • 外键可用于保证数据的参照完整性,但会影响父表和子表的写操作从而降低性能,建议在业务端实现。

2.4. 数据库SQL开发规范

  • 使用预编译语句进行数据库操作
    预编译语句可以重复使用这些计划,减少SQL编译所需要的时间,还可以解决动态SQL所带来的SQL注入的问题 只传参数,比传递SQL语句更高效 相同语句可以一次解析,多次使用,提高处理效率。
  • 避免数据类型的隐式转换
    隐式转换会导致索引失效。如:select name,phone from customer where id = ‘111’;
  • 避免索引失效
    避免使用双%号的like查询条件,会导致索引失效。
    一个SQL只能利用到复合索引中的一列进行范围查询。
    使用left join或 not exists来优化not in操作。not in 也通常会使用索引失效。
  • 程序连接不同的数据库使用不同的账号,禁止跨库查询
    为数据库迁移和分库分表留出余地
    降低业务耦合度
    避免权限过大而产生的安全风险
  • 禁止使用SELECT * 必须使用SELECT <字段列表> 查询
    消耗更多的CPU和IO以网络带宽资源
    无法使用覆盖索引
    可减少表结构变更带来的影响
  • 禁止使用不含字段列表的INSERT语句
    如:insert into values (‘a’,’b’,’c’);
    应使用insert into t(c1,c2,c3) values (‘a’,’b’,’c’);
  • 尽量避免使用子查询,把子查询优化为join操作
    通常子查询在in子句中,且子查询中为简单SQL(不包含union、group by、order by、limit从句)时,才可以把子查询转化为关联查询进行优化。
    子查询性能差的原因:
    子查询的结果集无法使用索引,通常子查询的结果集会被存储到临时表中,不论是内存临时表还是磁盘临时表都不会存在索引,所以查询性能 会受到一定的影响;
    特别是对于返回结果集比较大的子查询,其对查询性能的影响也就越大;
    由于子查询会产生大量的临时表也没有索引,所以会消耗过多的CPU和IO资源,产生大量的慢查询。
  • 避免使用JOIN关联太多的表
    对于Mysql来说,是存在关联缓存的,缓存的大小可以由join_buffer_size参数进行设置。
    在Mysql中,对于同一个SQL多关联(join)一个表,就会多分配一个关联缓存,如果在一个SQL中关联的表越多,所占用的内存也就越大。
    如果程序中大量的使用了多表关联的操作,同时join_buffer_size设置的也不合理的情况下,就容易造成服务器内存溢出的情况,就会影响到服务器数据库性能的稳定性。
    同时对于关联操作来说,会产生临时表操作,影响查询效率,建议不超过3个。
  • 减少同数据库的交互次数
    数据库更适合处理批量操作 合并多个相同的操作到一起,可以提高处理效率
  • 对应同一列进行or判断时,使用in代替or
    in的值不要超过500个,in操作可以更有效的利用索引,or大多数情况下不使用索引。
  • 禁止使用order by rand() 进行随机排序
    会把表中所有符合条件的数据装载到内存中,然后在内存中对所有数据根据随机生成的值进行排序,并且可能会对每一行都生成一个随机值,如果满足条件的数据集非常大,就会消耗大量的CPU和IO及内存资源。
    推荐在程序中获取一个随机值,然后从数据库中获取数据的方式
  • WHERE从句中禁止对列进行函数转换和计算
    对列进行函数转换或计算时会导致无法使用索引。
  • 使用UNION ALL代替UNION
    UNION会把两个结果集的所有数据放到临时表中后再进行去重操作
    UNION ALL不会再对结果集进行去重操作
  • 拆分复杂的大SQL为多个小SQL
    大SQL:逻辑上比较复杂,需要占用大量CPU进行计算的SQL
    MySQL:一个SQL只能使用一个CPU进行计算
    SQL拆分后可以通过并行执行来提高处理效率

2.5. 数据库操作行为规范

  • 超100万行的批量写(UPDATE、DELETE、INSERT)操作,要分批多次进行操作
    • 大批量操作可能会造成严重的主从延迟
      主从环境中,大批量操作可能会造成严重的主从延迟,大批量的写操作一般都需要执行一定长的时间,而只有当主库上执行完成后,才会在其他从库上执行,所以会造成主库与从库长时间的延迟情况
    • binlog日志为row格式时会产生大量的日志
      大批量写操作会产生大量日志,特别是对于row格式二进制数据而言,由于在row格式中会记录每一行数据的修改,我们一次修改的数据越多,产生的日志量也就会越多,日志的传输和恢复所需要的时间也就越长,这也是造成主从延迟的一个原因。
    • 避免产生大事务操作
      大批量修改数据,一定是在一个事务中进行的,这就会造成表中大批量数据进行锁定,从而导致大量的阻塞,阻塞会对MySQL的性能产生非常大的影响。
      特别是长时间的阻塞会占满所有数据库的可用连接,这会使生产环境中的其他应用无法连接到数据库,因此一定要注意大批量写操作要进行分批。
  • 对于大表使用pt-online-schema-change修改表结构
    避免大表修改产生的主从延迟
    避免在对表字段进行修改时进行锁表
    对大表数据结构的修改一定要谨慎,会造成严重的锁表操作,尤其是生产环境,是不能容忍的。
    pt-online-schema-change它会首先建立一个与原表结构相同的新表,并且在新表上进行表结构的修改,然后再把原表中的数据复制到新表中,并在原表中增加一些触发器。
    把原表中新增的数据也复制到新表中,在行所有数据复制完成之后,把新表命名成原表,并把原来的表删除掉。
    把原来一个DDL操作,分解成多个小的批次进行。

2.6. 权限控制规范

  • 禁止为程序使用的账号赋予super权限
    当达到最大连接数限制时,还运行1个有super权限的用户连接super权限只能留给DBA处理问题的账号使用。
  • 对于程序连接数据库账号,遵循权限最小原则
    程序使用数据库账号只能在一个DB下使用,不准跨库。程序使用的账号原则上不准有drop权限。
  • 开发数据库和生产数据库必须分开
    开发数据库和生产数据库必须分开,严禁在开发数据库上连接生产数据库,避免数据意外修改或丢失。
    有条件的建议开发、测试、生产分别配置一个数据库,没条件的可以开发和测试共用数据库。
  • 上线sql发布
    开发时sql的变动应当记录在对应开发任务版本中,sql脚本的命名与版本分支的命名保持一致。
    上线发布版本时由专人负责审核并提交sql脚本,避免出现意外、错误修改的情况。
    上线的账号由专人负责维护,开发人员不应持有生产数据库的账号。
    Sql上线应在应用停机之后、启动之前执行,避免因为改动导致的程序报错。
    对于重要的表应考虑是否需要做临时备份。
    对于确定不需要的备份表在上线时给予删除

3. 版本控制规范

3.1. 版本分支定义

版本分支定义有:master release dev/test 分支
Master分支:为最终稳定代码版本。
Release分支:release分支为版本上线分支,每次确定好上线版本之后从master上创建release分支,定义格式为REL_{YYYYMMdd},如REL_20230301,每次上线都会生成一个分支,均以此定义格式。
DEV/TEST分支: dev分支为开发分支,每次确定好上线之后从master创建dev分支,定义格式为DEV_{YYYYMMdd},如DEV_20230301,一般与release对应分支保持一致。开发过程中完成的功能代码需要合并至此分支,并保证无异常并可测试。此分支的代码应可以保证开发完成的功能,可以提供测试,允许有部分bug存在,但必须为不影响他人合并使用的分支。
DEV_USER分支:此分支为个人开发者使用分支,每个人均可创建一个自己的分支如DEV_kewen,自己的代码可以随意合并至自己分支上,同时也建议每天下班提交一次代码至自己开发分支上,以免意外丢失。此外,若有多个版本同时开发,可以再追加命名时间后缀,格式为DEV_{user}_{YYYYMMdd},如DEV_kewen_20230301。

3.2. 版本分支合并流程

版本合并一般自下而上合并。
1.当个人开发者完成部分功能可以提测后,首先拉取DEV_{YYYYMMdd}版本的代码合并至自己分支并merge。
2.然后将自己代码push到DEV_{YYYYMMdd}分支。
3.在所有人都开发完成并测试完成后将DEV_{YYYYMMdd}分支代码提交到REL_{YYYYMMdd}分支。同时进行回归测试,由于为上下代码,此次测试不应再有bug出现。
4.上线完成后将REL_{YYYYMMdd}分支代码合并至master上,合并完成后打上tag标记。

3.3. Git提交规范

Git上提交代码需要编写注释,注释需要表明此次开发所完成的工作,以便追溯。
注释内容大致上分为三个部分:Header、Body、Footer,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
 - Header(必须),使用type(scope):subject格式。
• type:用于说明提交信息的类别,规定包括以下几种
o feat:新增功能
o fix:修复bug
o docs:修改文档
o refactor:代码重构,未新增任何功能和修复任何bug
o build:改变构建流程,新增依赖库、工具等
o tyle:仅仅修改了空格、缩进等,不改变代码逻辑
o perf:改善性能的修改
o chore:非src和test的修改
o test:测试用例的修改
o ci:自动化流程配置修改
o revert:回滚到上一个版本
• scope:【可选】用于说明commit的影响范围
• subject:提交信息的简要说明,尽量简短

- Body(可选),用于对当前的提交信息进行详细描述,可写多行

- Footer(可选),用于描述以下信息,通常不需要说明。
• 不兼容的变动:需要描述相关信息
• 关闭指定Issue:输入Issue信息

3.4. 权限控制

Git分支应该区分权限控制,开发人员只能操作自己的dev分支和DEV_{YYYYMMdd}分支,此开发分支可以合并代码等。
除了dev分支,其他分支的权限应由后台项目负责人操作,如合并dev至release、打包上线、合并release至master、master分支打tag标签等。

4. 上线规范

4.1. 上线流程

上线主要分为打包和部署两个阶段;

  • 打包流程:
    git合并代码至对应的REL分支 -> 再次检查配置、代码等 -> 用对应分支代码打包 -> 生成的代码、变更的sql放至上线文件夹中。
  • 部署流程:
    停机 -> 替换代码 -> 处理差异化配置 ->执行数据库变更 -> 启动服务 -> 验证

4.2. 应用上线规范

代码上线规范主要是git代码版本的管理,git的管理详见 版本控制规范 –> git提交规范。
Git代码合并完成后需要打包,打包的分支以REL_{YYYYMMdd}分支为准,通过maven或gradle工具打包完成。
部署服务时服务为不可用状态,单机应用部署时如有必要应提前告知客户停机部署时间。
集群部署若为非停机部署需要在网关处做好集群可用性控制。

4.3. 数据库上线规范

数据库上线时由指定的人员执行上线脚本,执行完成后检查是否成功,并由开发测试人员再次确认。
数据变更应在应用停机后,若是集群应用不停机更新则需要分析业务影响并安排变更时间。