优质博文:IT-BLOG-CN
一、命令规范
包命名规范
包Package
的作用是将功能相似或相关的类或者接口进行分组管理,便于类的定位和查找,同时也可以使用包来避免类名的冲突和访问控制,使代码更容易维护。通常,包命使用小写英文字母进行命名,并使用“.”进行分割,每个被分割的单元只能包含一个名词。一般地,包命名常采用顶级域名作为前缀,例如com
,net
,org
,edu
,gov
,cn
,io
等,随后紧跟公司/组织/个人名称以及功能模块名称。
下面是一些包命名示例:
package org.springframework.boot.autoconfigure.cloud
package org.springframework.boot.util
package org.hibernate.action
package org.hibernate.cfg
package com.alibaba.druid
package com.alibaba.druid.filter
package com.alibaba.nacos.client.config
package com.ramostear.blog.web
下面是Oracle Java
的一些常见包命名例子:
package java.beans
package java.io
package java.lang
package java.net
package java.util
package javax.annotation
类命名规范
类Class
通常采用名词进行命名,且首字母大写,如果一个类名包含两个以上名词,建议使用驼峰命名Camel-Case
法书写类名,每个名词首字母也应该大写。一般地,类名的书写尽量使其保持简单和描述的完整性,因此在书写类名时不建议使用缩写(一些约定俗成的命名除外,例如Internationalization and Localization
缩写成i18n
,Uniform Resource Identifier
缩写成URI
,Data Access Object
缩写成DAO
,JSON Web Token
缩写成JWT
,HyperText Markup Language
缩写成HTML
等等)。
下列是一些常见的类命名示例:
public class UserDTO{
//TODO...
}
class EmployeeService{
//TODO...
}
class StudentDAO{
//TODO...
}
class OrderItemEntity{
//TODO...
}
public class UserServiceImpl{
//TODO...
}
public class OrderItemController{
//TODO...
}
下面是Oracle Java
中的一些标准命名示例:
public class HTMLEditorKit{
//...
}
public abstract class HttpContext{
//...
}
public interface ImageObserver{
//...
}
public class ArrayIndexOutOfBoundsException{
//...
}
public class enum Thread.State{
//...
}
接口命名规范
首先,接口Interface
是一种表述某一类型对象动作的特殊类;简单来说,接口也是类(不太严谨),所以,接口的名称的书写也应该符合类名书写规范,首字母应该大写,与普通类名不同的是,接口命名时通常采用形容词或动词来描述接口的动作行为。
下列是Oracle Java
中一些标准库的接口使用形容词命名示例:
public interface Closeable{
//...
}
public interface Cloneable{
//...
}
public interface RunnableP{
//...
}
public interface Comparable<T>{
//...
}
public interface CompletionService<V>{
//...
}
public interface Iterable<T>{
//...
}
public interface EventListener{
//...
}
在Spring Framework
标准库中,通常采用名词+动词/形容词的组合方式来命名接口。
下列是Spring Framework
中一些接口命名示例:
public interface AfterAdvice{
//...
}
public interface TargetClassAware{
//...
}
public interface ApplicationContextAware{
//...
}
public interface MessageSourceResolvable{
//...
}
抽象类命名规范
抽象类Abstract Class
是一种特殊的类,其命名与普通类的命名规范相当。一般地,为了将抽象类与普通类和接口做出区别,提高抽象类的可读性,在命名抽象类时,会以“Abstract”/“Base”
作为类命的前缀。
下面是编程中一些常规的命名示例:
public abstract class AbstractRepository<T>{
//...
}
public abstract class AbstractController{
//...
}
public abstract class BaseDao<T,ID>{
//...
}
public abstract class AbstractCommonService<T>{
//...
}
以下是Spring Framework
中常见的抽象类示例:
public abstract class AbstractAspectJAdvice{
//...
}
public abstract class AbstractSingletonProxyFactoryBean{
//...
}
public abstract class AbstractBeanFactoryPointcutAdvisor{
//...
}
public abstract class AbstractCachingConfiguration{
//...
}
public abstract class AbstractContextLoaderInitializer{
//...
}
异常类命名规范
异常类Exception Class
也是类的一种,但与普通类命名不同的是,异常类在命名时需要使用“Exception”
作为其后缀。
下面是常见的异常类命名示例:
public class FileNotFoundException{
//...
}
public class UserAlreadyExistException{
//...
}
public class TransactionException{
//...
}
public class ClassNotFoundException{
//...
}
public class IllegalArgumentException{
//...
}
public class IndexOutOfBoundsException{
//...
}
另外,在Java
中还有另外一类异常类,它们属于系统异常,这一类异常类的命名使用“Error”作为其后缀,以区分Exception
(编码,环境,操作等异常)。下面是系统异常(非检查异常)的命名示例:
public abstract class VirtualMachineError{
//...
}
public class StackOverflowError{
//...
}
public class OutOfMemoryError{
//...
}
public class IllegalAccessError{
//...
}
public class NoClassDefFoundError{
//...
}
public class NoSuchFieldError{
//...
}
public class NoSuchMethodError{
//...
}
方法命名规范
方法Method
命名时,其首字母应该小写,如果方法签名由多个单词组成,则从第二个单词起,使用驼峰命名法进行书写。一般地,在对方法进行命名时,通常采用动词/动词+名词的组合。下面是方法命名的一些常见示例。
表述获取: 如果一个方法用于获取某个值,通常使用get
作为其前缀,例如:
public String getUserName(){
//...
}
public List<Integer> getUserIds(){
//...
}
public User getOne(){
//...
}
表述条件: 如果方法需要通过查询或筛选的方式获取某个数据,通常使用find
/query
作为其前缀,例如:
public List<User> findOne(Integer id){
//...
}
public List<Integer> findAll(){
//...
}
public List<String> queryOrders(){
//...
}
表述条件: 如果一个方法需要一些条件参数,则可以使用by
/with
等字符作为方法名中条件的连接符,例如:
public User findByUsername(String username){
//...
}
public List<Integer> getUserIdsWithState(boolean state){
//...
}
public List<User> findAllByUsernameOrderByIdDesc(String username){
//...
}
表述设置: 如果一个方法是要设置,插入,修改,删除等操作,应该将对应的动词set,insert,update,delete
作为其名词的前缀,例如:
public void setName(String name){
//...
}
public User insert(User user){
//...
}
public void update(User user){
//...
}
public void clearAll(){
//...
}
其他规范: 如果一个方法用于获取某组数据的长度或数量,则该方法应该使用length
或size
命名;如果方法的返回值为布尔类型Boolean
,则该方法应该使用is
或has
作为前缀;如果方法用于将一种类型的数据转换为另一种数据数类型,则可以使用to
作为前缀。下面是综合示例:
public long length(){
//...
}
public int size(){
//...
}
public boolean isOpen(){
//...
}
public boolean isNotEmpty(){
//...
}
public boolean hasLength(){
//...
}
public Set<Integer> mapToSet(Map map){
//...
}
public UserDto convertTo(User user){
//...
}
public String toString(Object obj){
//...
}
变量命名规范
变量Variable
命名包括参数名称,成员变量和局部变量。变量命名通常以小写字母开头,如果变量名由多个单词构成,则从第二个单词起首字母需要大写,在变量命名过程中,不建议使用“_”作为前缀或者单词之间的分割符号。下面是一些常见的变量命名示例:
private String nickName;
private String mobileNumber;
private Long id;
private String username;
private Long orderId;
private Long orderItemId;
常量命名规范
一般地,常量名称采用全部大写的英文单词书写,如果常量名称由多个单词组成,则单词之间统一使用“_”进行分割,下面是常量命名示例:
public static final String LOGIN_USER_SESSION_KEY = "current_login_user";
public static final int MAX_AGE_VALUE = 120;
public static final int DEFAULT_PAGE_NO = 1;
public static final long MAX_PAGE_SIZE = 1000;
public static final boolean HAS_LICENSE = false;
public static final boolean IS_CHECKED = false;
枚举命名规范
枚举Enum
类是一种特殊的类,其命名规范遵循普通类的命名约束条件,首字母大写,采用驼峰命名法;枚举类中定义的值的名称遵循常量的命名规范,且枚举值的名称需要与类名有一定的关联性,下面是枚举的一些示例:
public enum Color{
RED,YELLOW,BLUE,GREEN,WHITE;
}
public enum PhysicalSize{
TINY,SMALL,MEDIUM,LARGE,HUGE,GIGANTIC;
}
下面是Oracle Java
标准库中的一个示例:
public enum ElementType{
TYPE,
FIELD,
METHOD,
PARAMETER,
CONSTRUCTOR,
LOCAL_VARIABLE,
ANNOTATION_TYPE,
PACKAGE,
TYPE_PARAMETER,
TYPE_USE;
}
其他命名规范
数组: 在定义数组时,为了便于阅读,尽量保持以下的书写规范:
int[] array = new int[10];
int[] idArray ={1,2,3,4,5};
String[] nameArray = {"First","Yellow","Big"}
public List<String> getNameById(Integer[] ids){
//...
}
//或者
public List<String> getNameById(Integer...ids){
//...
}
表述复数或者集合: 如果一个变量用于描述多个数据时,尽量使用单词的复数形式进行书写,例如:
Collection<Order> orders;
int[] values;
List<Item> items;
另外,如果表述的是一个Map
数据,则应使用map
作为其后缀,例如:
Map<String,User> userMap;
Map<String,List<Object>> listMap;
泛型类: 在书写泛型类时,通常做以下的约定:
【1】E
表示Element
,通常用在集合中;
【2】ID
用于表示对象的唯一标识符类型;
【3】T
表示Type
(类型),通常指代类;
【4】K
表示Key
(键),通常用于Map
中;
【5】V
表示Value
(值),通常用于Map
中,与K
结对出现;
【6】N
表示Number
,通常用于表示数值类型;
【7】?
表示不确定的Java
类型;
【8】X
用于表示异常;
【9】U
,S
表示任意的类型。
下面时泛型类的书写示例:
public class HashSet<E> extends AbstractSet<E>{
//...
}
public class HashMap<K,V> extends AbstractMap<K,V>{
//...
}
public class ThreadLocal<T>{
//...
}
public interface Functor<T,X extends Throwable>{
T val() throws X;
}
public class Container<K,V>{
private K key;
private V value;
Container(K key,V value){
this.key = key;
this.value = value;
}
//getter and setter ...
}
public interface BaseRepository<T,ID>{
T findById(ID id);
void update(T t);
List<T> findByIds(ID...ids);
}
public static <T> List<T> methodName(Class<T> clz){
List<T> dataList = getByClz(clz);
return dataList;
}
接口实现类: 为了便于阅读,在通常情况下,建议接口实现类使用Impl
作为后缀,不建议使用大写的I
作为接口前缀(PS:当然也有很多代码是用I开头的),下面是接口和接口实现类的书写示例。
推荐写法:
public interface OrderService{
//...
}
public class OrderServiceImpl implements OrderService{
//...
}
不建议的写法:
public interface IOrderService{
//...
}
public class OrderService implements IOrderService{
//...
}
测试类和测试方法: 在项目中,测试类采用被测试业务模块名/被测试接口/被测试类+Test
的方法进行书写,测试类中的测试函数采用test
+用例操作_状态的组合方式进行书写,例如:
public class UserServiceTest{
public void testFindByUsernameAndPassword(){
//...
}
public void testUsernameExist_notExist(){
//...
}
public void testDeleteById_isOk(){
//...
}
}
阿里代码手册中命名规范
【强制】代码中的命名均不能以下划线或美元符号开始,也不能以下划线或美元符号结束。
反例 : _name
/ __name
/ $Object
/ name_
/ name$
/ Object$
【强制】代码中的命名严禁使用拼音与英文混合的方式,更不允许直接使用中文的方式。说明: 正确的英文拼写和语法可以让阅读者易于理解,避免歧义。注意,即使纯拼音命名方式也要避免采用。
正例 : alibaba
/ taobao
/ youku
/ hangzhou
等国际通用的名称,可视同英文。
反例 : DaZhePromotion
[打折] / getPingfenByName()
[评分] / int
某变量 = 3
【强制】类名使用UpperCamelCase
风格,必须遵从驼峰形式,但以下情形例外: DO
/ BO
/ DTO
/ VO
/ AO
正例 : MarcoPolo
/ UserDO
/ XmlService
/ TcpUdpDeal
/ TaPromotion
反例 : macroPolo
/ UserDo
/ XMLService
/ TCPUDPDeal
/ TAPromotion
【强制】方法名、参数名、成员变量、局部变量都统一使用lowerCamelCase
风格,必须遵从驼峰形式。
正例 : localValue
/ getHttpMessage()
/ inputUserId
【强制】常量命名全部大写,单词间用下划线隔开,力求语义表达完整清楚,不要嫌名字长。
正例 : MAX_STOCK_COUNT
反例 : MAX_COUNT
【强制】抽象类命名使用Abstract
或Base
开头;异常类命名使用Exception
结尾;测试类命名以它要测试的类的名称开始,以Test
结尾。
【强制】中括号是数组类型的一部分,数组定义如下: String[] args
。
反例 : 使用String args[]
的方式来定义。
【强制】POJO
类中布尔类型的变量,都不要加is
,否则部分框架解析会引起序列化错误。
反例 : 定义为基本数据类型Boolean isDeleted
;的属性,它的方法也是isDeleted()
,RPC
框架在反向解析的时候,“以为”对应的属性名称是 deleted
,导致属性获取不到,进而抛出异常。
【强制】包名统一使用小写,点分隔符之间有且仅有一个自然语义的英语单词。包名统一使用单数形式,但是类名如果有复数含义,类名可以使用复数形式。
正例 : 应用工具类包名为com.alibaba.open.util
、类名为MessageUtils
(此规则参考spring
的框架结构)
【强制】杜绝完全不规范的缩写,避免望文不知义。
反例 : AbstractClass
“缩写”命名成AbsClass
;condition
“缩写”命名成condi
,此类随意缩写严重降低了代码的可阅读性。
【推荐】如果使用到了设计模式,建议在类名中体现出具体模式。
说明 : 将设计模式体现在名字中,有利于阅读者快速理解架构设计思想。
正例 :
public class OrderFactory; public class LoginProxy; public class ResourceObserver;
【推荐】接口类中的方法和属性不要加任何修饰符号(public
也不要加),保持代码的简洁性,并加上有效的Javadoc
注释。尽量不要在接口里定义变量,如果一定要定义变量,肯定是与接口方法相关,并且是整个应用的基础常量。
正例 : 接口方法签名: void f()
;
接口基础常量表示: String COMPANY = "alibaba"
;
反例 : 接口方法定义: public abstract void f()
;
说明 : JDK8
中接口允许有默认实现,那么这个default
方法,是对所有实现类都有价值的默认实现。
接口和实现类的命名有两套规则:
【强制】对于Service
和DAO
类,基于SOA
的理念,暴露出来的服务一定是接口,内部的实现类用Impl
的后缀与接口区别。
正例 : CacheServiceImpl
实现CacheService
接口。
【推荐】如果是形容能力的接口名称,取对应的形容词做接口名(通常是–able
的形式)。
正例 : AbstractTranslator
实现Translatable
。
【参考】枚举类名建议带上Enum
后缀,枚举成员名称需要全大写,单词间用下划线隔开。
说明 : 枚举其实就是特殊的常量类,且构造方法被默认强制是私有。
正例 : 枚举名字: DealStatusEnum
,成员名称: SUCCESS
/ UNKOWN_REASON
。
【参考】各层命名规约:
Service/DAO
层方法命名规约
☑️ 获取单个对象的方法用get
做前缀。
☑️ 获取多个对象的方法用list
做前缀。
☑️ 获取统计值的方法用count
做前缀。
☑️ 插入的方法用save
(推荐)或insert
做前缀。
☑️ 删除的方法用remove
(推荐)或delete
做前缀。
☑️ 修改的方法用update
做前缀。
领域模型命名规约
☑️ 数据对象: xxxDO
,xxx
即为数据表名。
☑️ 数据传输对象: xxxDTO
,xxx
为业务领域相关的名称。
☑️ 展示对象: xxxVO
,xxx
一般为网页名称。
☑️ POJO
是DO/DTO/BO/VO
的统称,禁止命名成xxxPOJO
。
速记Java
开发中的各种O
名称 | 使用范围 | 解释说明 |
---|---|---|
VO | 通常是视图控制层和模板引擎之间传递的数据对象 | Value Object 值对象,主要用于视图层,视图控制器将视图层所需的属性封装成一个对象,然后用一个VO 对象在视图控制器和视图之间进行数据传输。 |
POJO | POJO 是DO/DTO/BO/VO 的统称 | Plain Ordinary Java Object 简单Java 对象,它是一个简单的普通Java 对象,禁止将类命名为XxxxPOJO |
PO | Bean ,Entity 等类的命名 | Persistant Object 持久化对象,数据库表中的数据在Java 对象中的映射状态,可以简单的理解为一个PO 对象即为数据库表中的一条记录 |
DTO | 经过加工后的PO对象,其内部属性可能增加或减少 | Data Transfer Object 数据传输对象,主要用于远程调用等需要大量传输数据的地方,例如,可以将一个或多个PO 类的部分或全部属性封装为DTO 进行传输 |
DAO | 用于对数据库进行读写操作的类进行命名 | Data Access Object 数据访问对象,主要用来封装对数据库的访问,通过DAO 可以将POJO 持久化为PO ,也可以利用PO 封装出VO 和DTO |
BO | 用于Service ,Manager ,Business 等业务相关类的命名 | Business Object 业务处理对象,主要作用是把业务逻辑封装成一个对象。 |
AO | 应用层对象 | Application Object ,在Web 层与Service 层之间抽象的复用对象模型,很少用。 |
下面将通过一张图来理解上述几种O
之间相互转换的关系:
二、代码规范
【强制】大括号的使用约定。如果是大括号内为空,则简洁地写成{}
即可,不需要换行;如果是非空代码块则:
☑️ 左大括号前不换行。
☑️ 左大括号后换行。
☑️ 右大括号前换行。
☑️ 右大括号后还有else
等代码则不换行;表示终止的右大括号后必须换行。
【强制】左小括号和字符之间不出现空格;同样,右小括号和字符之间也不出现空格。详见 第5
条下方正例提示。
反例 : if (空格 a == b 空格)
【强制】if/for/while/switch/do
等保留字与括号之间都必须加空格。
【强制】任何二目、三目运算符的左右两边都需要加一个空格。
说明: 运算符包括赋值运算符=
、逻辑运算符&&
、加减乘除符号等。
【强制】缩进采用4
个空格,禁止使用tab
字符。
说明: 如果使用tab
缩进,必须设置1
个tab
为4
个空格。IDEA
设置tab
为4
个空格时,请勿勾选Use tab character
;而在eclipse
中,必须勾选insert spaces for tabs
。正例 : (涉及1-5
点)
public static void main(String[] args) {
// 缩进 4 个空格
String say = "hello";
// 运算符的左右必须有一个空格
int flag = 0;
// 关键词 if 与括号之间必须有一个空格,括号内的 f 与左括号,0 与右括号不需要空格
if (flag == 0) {
System.out.println(say);
}
// 左大括号前加空格且不换行;左大括号后换行
if (flag == 1) {
System.out.println("world");
// 右大括号前换行,右大括号后有 else,不用换行
} else {
System.out.println("ok");
// 在右大括号后直接结束,则必须换行
}
}
【强制】单行字符数限制不超过120
个,超出需要换行,换行时遵循如下原则:
☑️ 第二行相对第一行缩进4
个空格,从第三行开始,不再继续缩进,参考示例。
☑️ 运算符与下文一起换行。
☑️ 方法调用的点符号与下文一起换行。
☑️ 在多个参数超长,在逗号后换行。在括号前不要换行,见反例。
正例 :
StringBuffer sb = new StringBuffer();
//超过 120 个字符的情况下,换行缩进 4 个空格,并且方法前的点符号一起换行
sb.append("zi").append("xin")...
.append("huang")...
.append("huang")...
.append("huang");
反例 :
StringBuffer sb = new StringBuffer();
//超过 120 个字符的情况下,不要在括号前换行
sb.append("zi").append("xin")...append
("huang");
//参数很多的方法调用可能超过 120 个字符,不要在逗号前换行
method(args1, args2, args3, ..., argsX);
【强制】方法参数在定义和传入时,多个参数逗号后边必须加空格。
正例 : 下例中实参的"a"
,后边必须要有一个空格。method("a", "b", "c")
;
【强制】IDE
的text file encoding
设置为UTF-8
; IDE
中文件的换行符使用Unix
格式,不要使用windows
格式。
【推荐】没有必要增加若干空格来使某一行的字符与上一行对应位置的字符对齐。
正例 :
int a = 3;
long b = 4L;
float c = 5F;
StringBuffer sb = new StringBuffer();
说明 : 增加sb
这个变量,如果需要对齐,则给a
、b
、c
都要增加几个空格,在变量比较多的情况下,是一种累赘的事情。
【推荐】方法体内的执行语句组、变量的定义语句组、不同的业务逻辑之间或者不同的语义之间插入一个空行。相同业务逻辑和语义之间不需要插入空行。
说明 : 没有必要插入多个空行进行隔开。
阿里代码手册中代码规范
【推荐】同项目代码必须使用统一的格式化规范,推荐配置idea
/eclipse
的fmt
模板。
【推荐】减少不必要的代码,比如样板代码、重复代码等。
【推荐】功能简单、使用方单一的方法,合并到使用方对应类中,比如check
参数的方法。
【推荐】对于类外部几乎不会用到的常量、方法(比如校验参数),推荐直接放到类内部。
【推荐】对于关键的异常,提供必要的metrics
和log
输出。
【推荐】避免使用Object
、Map
等不清晰的类型作为请求参数和响应对象。
三、开发规范
【推荐】使用公司推荐中间件,包括qmq
、qconfig
、qschedule
、credis/qredis
、mysql
、dubbo
、soa
。
【强制】合理的使用各种资源、锁、流等,保证资源一定可以得到释放。
【推荐】使用java8
的stream
进行集合操作,但是需要控制复杂度,保证可读性。
【推荐】try-catch
缩小最小化原则,杜绝一个从头到尾的try-catch
。
【推荐】接口设计遵循职责单一、清晰易懂,参数逻辑,默认值处理需要表达明确
【推荐】为API
设计合理的错误码,避免返回调用方不关注的错误码,或者缺失必要的错误码,同时提供必要的错误信息。
【推荐】使用单元测试保证核心逻辑的准确性,尽量做到脱机可执行。
【强制】保证使用的数据结构和代码段的线程安全。
【强制】避免对表进行单次大规模扫描,必须加上限制条件。
【强制】mysql的query for update
必须指定主键或者唯一键。
【强制】对外部资源的访问必须有超时机制。
【推荐】sql
语句尽可能明确定义,尽可能避免动态sql
拼接,动态sql
容易出现条件失效,导致全表扫描。
【推荐】对于数据规模较少、访问及其频繁的配置类型,如果存放到db
中,那么建议启动、以及定期更新到内存,避免每次都查询。
【推荐】推荐使用java Stream
,但是避免链太长,以及在“.”之前做必要的断行。
【推荐】信息安全的规范可以参考04安全规范。
四、数据库规范
建表规范
【1】表达是与否概率的字段,必须使用is_xxx
的方式命令,数据类型是tinyint
(1
表示是,0
表示否)
正例:表达逻辑删除的字段名is_active
,0
表示删除,1
表示未删除。
【2】表名、字段名必须使用小写字母或数字,禁止数字开头,禁止两个下划线中间只出现数字。数据库字段名的修改代价很大,因为无法进行预发布,所以字段名称需要慎重考虑。
MySQL
在Windows
下不区分大小写,但在Linux
下默认是区分大小写。因此,数据库名、表名、字段名,都不允许出现任何大写字母,避免节外生枝。
正例:aliyun_admin
,rdc_config
,level3_name
;
反例:AliyunAdmin
,rdcConfig
,level_3_name
;
【3】表名不使用复数名词:表名应该仅仅表示表里面的实体内容,不应该表示实体数量,对应于DO
类名也是单数形式,符合表达习惯。
正例:store
;
反例:stores
;
【4】禁用保留字,如desc
、range
、match
、delayed
等,请参考MySQL
官方保留字。
【5】主键索引名为pk_
字段名;唯一索引名为uk_
字段名;普通索引名则为idx_
字段名。
主键索引自动生成,名:PRIMARY
正例:uk_storeIdidx_storeId_vehicldId
;
【6】小数类型为decimal
,禁止使用float
和double
。
【7】如果修改字段含义或对字段表示的状态追加时,需要及时更新字段注释。
【8】varchar
是可变长字符串,不预先分配存储空间,长度不要超过5000
,如果存储长度大于此值,定义字段类型为text
,独立出来一张表,用主键来对应,避免影响其它字段索引效率。
由于公司规范不允许使用text
,推荐超过5000
使用多条记录拼接。
【9】表必备四字段:id
, (BigInt)datachange_createtime
,(Timestamp)catachange_lasttime
, (Timestamp)is_active (TinyInt)
。
【10】表的命名最好是遵循“业务名称_表的作用”:比如订单相关:表全部以订单开头。
【11】库名与应用名称尽量一致。
【12】字段允许适当冗余,以提高查询性能,但必须考虑数据一致。冗余字段应遵循:1.不是频繁修改的字段。2.不是唯一索引的字段。3.不是varchar
超长字段,更不能是text
字段。
【13】单表行数超过500
万行或者单表容量超过2GB
,才推荐进行分库分表。
如果预计三年后的数据量根本达不到这个级别,请不要在创建表时就分库分表
【14】合适的字符存储长度,不但节约数据库表空间、节约索引存储,更重要的是提升检索速度。
【15】如果存储的字符串长度几乎相等,使用char
定长字符串类型。
索引规约
【1】业务上具有唯一特性的字段,即使是组合字段,也必须建成唯一索引。不要因为唯一索引影响了insert
速度,这个速度损耗可以忽略,但提高查找速度是明显的;另外,即使在应用层做了非常完善的校验控制,只要没有唯一索引,根据墨菲定律,必然有脏数据产生。
【2】超过二个表禁止join
。需要join
的字段,数据类型保持绝对一致;多表关联查询时,保证被关联的字段需要有索引。即使双表join
也要注意表索引、SQL
性能。
【3】在varchar
字段上建立索引时,必须指定索引长度(推荐20
),没必要对全字段建立索引,根据实际文本区分度决定索引长度。 索引的长度与区分度是一对矛盾体,一般对字符串类型数据,长度为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】利用覆盖索引来进行查询操作,避免回表。如果一本书需要知道第11
章是什么标题,会翻开第11
章对应的那一页吗?目录浏览一下就好,这个目录就是起到覆盖索引的作用。能够建立索引的种类分为主键索引、唯一索引、普通索引三种,而覆盖索引只是一种查询的一种效果,用explain
的结果,extra
列会出现:using index
。
【7】利用延迟关联或者子查询优化超多分页场景: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
【8】SQL
性能优化的目标:至少要达到range
级别,要求是ref
级别,如果可以是consts
最好。
1)consts
单表中最多只有一个匹配行(主键或者唯一索引),在优化阶段即可读取到数据。
2)ref
指的是使用普通的索引normal index
。
3)range
对索引进行范围检索。
explain
表的结果,type=index
索引物理文件全扫描,速度非常慢,这个index
级别比较range
还低,与全表扫描是小巫见大巫。
【9】建组合索引的时候,区分度最高的在最左边。存在非等号和等号混合判断条件时,在建索引时,请把等号条件的列前置。如:where c>? and d=?
那么即使c
的区分度更高,也必须把d
放在索引的最前列,即建立组合索引idx_d_c
。如果where a=? and b=?
,a
列的几乎接近于唯一值,那么只需要单建idx_a
索引即可。
【10】防止因字段类型不同造成的隐式转换,导致索引失效。当操作符与不同类型的操作数一起使用时,会发生类型转换以使操作数兼容。则会发生转换隐式。
隐式类型转换规则:》如果一个或两个参数都是NULL
,比较的结果是NULL
,除了NULL
安全的<=>
相等比较运算符。对于NULL <=> NULL
,结果为true
。不需要转换》如果比较操作中的两个参数都是字符串,则将它们作为字符串进行比较。》如果两个参数都是整数,则将它们作为整数进行比较。》如果不与数字进行比较,则将十六进制值视为二进制字符串》如果其中一个参数是十进制值,则比较取决于另一个参数。 如果另一个参数是十进制或整数值,则将参数与十进制值进行比较,如果另一个参数是浮点值,则将参数与浮点值进行比较》如果其中一个参数是TIMESTAMP
或DATETIME
列,另一个参数是常量,则在执行比较之前将常量转换为时间戳。》在所有其他情况下,参数都是作为浮点数(实数)比较的。
正例:假如orderId
为varchar
类型:select * from order_main where order_id='1234'
反例:假如orderId
为varchar
类型:select * from order_main where order_id=1234
【11】创建索引时避免有如下极端误解:1)索引宁滥勿缺。认为一个查询就需要建一个索引。2)吝啬索引的创建。认为索引会消耗空间、严重拖慢记录的更新以及行的新增速度。3)抵制惟一索引。认为惟一索引一律需要在应用层通过“先查后插”方式解决。
【12】最左前缀匹配原则,非常重要的原则,mysql
会一直向右匹配直到遇到范围查询(>
、<
、between
、like
)就停止匹配,比如a = 1 and b = 2 and c > 3 and d = 4
如果建立(a,b,c,d)
顺序的索引,d
是用不到索引的,如果建立(a,b,d,c)
的索引则都可以用到,a,b,d
的顺序可以任意调整。
【13】=
和in
可以乱序,比如a = 1 and b = 2 and c = 3
建立(a,b,c)
索引可以任意顺序,mysql
的查询优化器会帮你优化成索引可以识别的形式。
【14】尽量选择区分度高的列作为索引,区分度的公式是count(distinct col)/count(*)
,表示字段不重复的比例,比例越大我们扫描的记录数越少,唯一键的区分度是1
,而一些状态、性别字段可能在大数据面前区分度就是0
,那可能有人会问,这个比例有什么经验值吗?使用场景不同,这个值也很难确定,一般需要join
的字段我们都要求是0.1
以上,即平均1
条扫描10
条记录。
【15】索引列不能参与计算,保持列“干净”,比如from_unixtime(create_time) = ’2014-05-29’
就不能使用到索引,原因很简单,b+
树中存的都是数据表中的字段值,但进行检索时,需要把所有元素都应用函数才能比较,显然成本太大。所以语句应该写成create_time = unix_timestamp(’2014-05-29’)
。
【16】尽量的扩展索引,不要新建索引。比如表中已经有a
的索引,现在要加(a,b)
的索引,那么只需要修改原来的索引即可。
【17】业务上具有唯一特性的字段,即使是组合字段,也必须建成唯一索引.
【18】合理使用索引,同时需要避免过多创建索引,浪费资源 。
sql 语句
【1】不要使用count(列名)
或count(常量)
来替代count(*)
,count(*)
是SQL92
定义的标准统计行数的语法,跟数据库无关,跟NULL
和非NULL
无关。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
问题。
【4】使用ISNULL()
来判断是否为NULL
值。NULL
与任何值的直接比较都为NULL
。1)NULL<>NULL
的返回结果是NULL
,而不是false
。2)NULL=NULL
的返回结果是NULL
,而不是true
。3)NULL<>1
的返回结果是NULL
,而不是true
。
在SQL
语句中,如果在null
前换行,影响可读性。select * from table where column1 is null and column3 is not null
; 而ISNULL(column)
是一个整体,简洁易懂。从性能数据上分析,ISNULL(column)
执行效率更快一些。
【5】代码中写分页查询逻辑时,若count
为0
应直接返回,避免执行后面的分页语句。
【6】不得使用外键与级联,一切外键概念必须在应用层解决。(概念解释)学生表中的student_id
是主键,那么成绩表中的student_id
则为外键。如果更新学生表中的student_id
,同时触发成绩表中的student_id
更新,即为级联更新。外键与级联更新适用于单机低并发,不适合分布式、高并发集群;级联更新是强阻塞,存在数据库更新风暴的风险;外键影响数据库的插入速度。
【7】禁止使用存储过程,存储过程难以调试和扩展,更没有移植性。
【8】数据订正(特别是删除或修改记录操作)时,要先select
,避免出现误删除,确认无误才能执行更新语句。
【9】对于数据库中表记录的查询和变更,只要涉及多个表,都需要在列名前加表的别名(或表名)进行限定。对多表进行查询记录、更新记录、删除记录时,如果对操作列没有限定表的别名(或表名),并且操作列在多个表中存在时,就会抛异常。
正例:select t1.name from table_first as t1, table_second as t2 where t1.id=t2.id;
反例:在某业务中,由于多表关联查询语句没有加表的别名(或表名)的限制,正常运行两年后,最近在某个表中增加一个同名字段,在预发布环境做数据库变更后,线上查询语句出现出1052
异常:Column 'name' in field list is ambiguous
。
【10】in
操作能避免则避免,若实在避免不了,需要仔细评估in
后边的集合元素数量,控制在1000
个之内。
【11】SQL
语句中表的别名前加as
,并且以t1
、t2
、t3
、...
的顺序依次命名。1)别名可以是表的简称,或者是依照表在SQL
语句中出现的顺序,以t1
、t2
、t3
的方式命名。2)别名前加as
使别名更容易识别。
正例:select t1.name from table_first as t1,table_second as t2 where t1.id=t2.id;
【12】因国际化需要,所有的字符存储与表示,均采用utf8mb64
字符集,那么字符计数方法需要注意。SELECT LENGTH("轻松工作")
; 返回为12
。SELECT CHARACTER_LENGTH("轻松工作")
; 返回为4
如果需要存储表情,那么选择utf8mb4
来进行存储,注意它与utf8
编码的区别。
【13】TRUNCATE TABLE
比DELETE
速度快,且使用的系统和事务日志资源少,但TRUNCATE
无事务且不触发trigger
,有可能造成事故,故不建议在开发代码中使用此语句。TRUNCATE TABLE
在功能上与不带WHERE
子句的DELETE
语句相同。
orm映射
【1】在表查询中,一律不要使用*
作为查询的字段列表,需要哪些字段必须明确写明。1)增加查询分析器解析成本。2)增减字段容易与resultMap
配置不一致。3)无用字段增加网络消耗,尤其是text
类型的字段。
【2】更新数据表记录时,必须同时更新记录对应的data_last_time
字段值为当前时间。
【3】不要写一个大而全的数据更新接口。传入为POJO
类,不管是不是自己的目标更新字段,都进行update table set c1=value1,c2=value2,c3=value3;
这是不对的。执行SQL
时,不要更新无改动的字段,一是易出错;二是效率低;三是增加binlog
存储。
【4】@Transactional
事务不要滥用。事务会影响数据库的QPS
,另外使用事务的地方需要考虑各方面的回滚方案,包括缓存回滚、搜索引擎回滚、消息补偿、统计修正等。
事务使用规范
【1】一个事务里面,避免一次处理太多数据。
【2】在一个事务里面,尽量避免不必要的查询。
【3】在一个事务里面,避免耗时太多的操作,造成事务超时。一些非DB
的操作,比如rpc
调用,消息队列的操作尽量放到事务之外操作。
五、QMQ 开发规范
代码规范
【1】消费者必须以Consumer
结尾,生产者必须以Producer
结尾。
【2】选择合适的消费模式:根据业务判断消费模式是集群模式还是广播模式,具体为:MessageConsumerProvider.addListener(String subject, String consumerGroup, MessageListener listener,boolean consumeMostOnce)
//consumerGroup
为空时是广播模式,否则为集群模式。
【3】选择适合的队列类型:根据业务判断消费模式是普通队列还是延迟队列,具体为:Message message = producer.generateMessage("qmq.fx.test");
// 延迟10smessage.setDelayTime(10, TimeUnit.SECONDS);
。
【4】选择适合的消费者数量:若想加快消费速度,可以新增单台机器的消费者数量,具体为:MessageListenerConfig messageListenerConfig = new MessageListenerConfig();messageListenerConfig.setNotifierThreadCount(Runtime.getRuntime().availableProcessors());
。
命令规范
【1】若为监听数据库,命名为top.cache.db.topcoredb.表名
若为其他业务需要,命名为top.模块.业务
。
配置规范
【1】需要新增配置文件xxx.qmq.properties
:若应用使用qmq
,需要新增配置文件xxx.qmq.properties
,否则会导致qmq
初始化失败。
【2】明确使用的场景:解耦减少依赖,使核心业务外的其他业务插件化,例如订单出票后需要发邮件,库存卖空需要触发报警等削峰异步化,前置流量过大,将整体流程拆为两部分,核心逻辑保持干净和足够的性能,较重的逻辑后置处理定时器支持在未来指定时刻消费消息,减少业务侧轮询任务的开发。
【3】慎用Pull
模式:确保所有分支都覆盖ACK
,最好是有finally
,建议Push
模式:采用自动ack
,无须业务线程池。
【4】有限次数的重试。
【5】消息量适度:消息量过大且消费者来不及处理会导致消息堆积。
【6】消息大小适度:业务消息<4KB
。
【7】合并处理:相同处理对象更新频次较高场景,建议滑动窗口聚合.
【8】尽量避免使用有序消息:有时序要求场景,和服务提供方确认是不是同步写入。
【9】消息重复投递,业务要做容错:幂等处理。
【10】消息堆积、延迟、处理能力下降等指标监控。
【11】防止jackson
版本不一致:jackson
版本不一致可能会导致反序列化的结果不一致。
【12】对消息量大的消息队列可以单独拆分:监听binlog
时,对于消息量大又不太重要的表,可以拆分消息队列,将这部分放到单独的消息队列中,可以避免这部分消息积压影响重要数据。
【13】重要数据持久化消息或做补偿动作:正常qmq
客户端不能保证消息不丢失,如果对对消息有可靠性要求,需要持久化消息或做补偿动作。比如数据库监听binlog
,将订单状态通过qmq
进行同步,实际情况可能存在丢失;可以通过定时任务查询数据库进行补偿。
使用规范
【1】QMQ
新主题通过注册模式部署到独立的集群, 避免直接代码中定义与使用。
【2】QMQ
适用范围, 避免滥用。
☑️ 不能将QMQ
用作数据传输, 数据存储等功能。
☑️ 适用于状态通知,事件驱动。
☑️ 适用于异步解耦。
☑️ 契约json
遵循最精简方式, 只传递基本信息, 如订单号, 状态等, 其他业务处理数据依赖接口反查. 同时精简有利于后续的补偿与异常处理。
【3】QMQ
生产者和消费者代码目前存在的问题:
☑️ 规范不统一, 编码风格不统一, 订阅和推送QMQ
消息代码写法各异。
☑️ 消费者与生产者的代码散落在各个地方, 同一个topic
存在多个producer
。
☑️ QMQ
缺乏契约治理, 消费者/生产者都需感知具体的message
, 定义一个或多个类做json
转换或读取具体的message
属性。
☑️ 开发直接依赖的是QMQ
框架Message
, 读取各个property
或字符串再转为对象, 非直接面向对象的编码风格。
【4】对于QMQ
生产者与消费者编码在框架基础上再次进行了统一的封装,实现代码风格与编码规范的统一。
☑️ 消费者模板
a) QMQ
消费者(流量入口)只能在service
层的consumer package
b) 命名后缀为XXXConsumer
。
☑️ 生产者模板
a) QMQ
生产者只能在service
层的producer package
。
b) 命名后缀为XXXProducer
。
c) 同一个Topic
只能有一个Producer
,严格禁止在不同的应用或单个应用不同的package
中出现相同topic
的生产者。
☑️ 生产者与消费者共同依赖契约对象
a) QMQ
契约类为contract
层。
b) QMQ
即使是只有1
,2
个property
, 也需定义一个类,走统一的对象模式。
b) 发布Contract Jar
, 提供maven
依赖给消费者, 包括后续升级同步。
☑️ 放到独立Consumer
集群。消费时,调用业务API
接口进行处理。
☑️ 返回状态要正确。如果消费成功,那么返回成功,否则返回失败。
【5】QMQ
订阅之后的处理逻辑即使非常简单, 调用链也必须是如下的统一模式, 流程或控制代码在service
层,细节代码在process
层.SendMessageService
(service
层) → SendMessageProcess
(process
层)
【6】QMQ
生产,需要注意几点:
☑️ 生产失败如何处理?
a) 失败记录信息。
b) Job
补偿处理。
☑️ 生产Q
是否需要持久化。
a) 核心Q
必须持久化。
【7】QMQ
异常消费重试注意:重试分本地重试和远程重试。如果使用本地重试,一定要设置最大重试次数,防止死循环。
【8】topic
和ConsumerGroup
的长度不允许超过170
。
六、Redis 开发规范
开发规范
【1】弱依赖检查与线下确认:Redis
必须是弱依赖,即Redis
宕机不影响业务。包括超时检查。
【2】是否当存储使用检查:Redis
不能作为存储设备来使用,只能作为缓存或状态等场景来使用。存储优先使用本地缓存。
【3】超时时间检查与线下确认:Redis
使用需要设置超时时间。如果超时,对应的策略和方案是什么。
【4】无状态检查:Redis
同一个Key
不能被不同的应用,不同的场景使用。谁生产,谁消费的原则。
【5】同步锁检查:优先使用集团框架提供的分布式锁。
【6】Key
检查:Key
的唯一性是否存在明显问题,与其他场景和应用的重名的可能。Key
的长度,尽可能的小于128
字节,禁止超过1024
简洁性
保证语义的前提下,控制key
的长度,当key
较多时,内存占用也不容忽视可读性和可管理性 以业务名(或数据库名)为前缀(防止key冲突),用冒号分隔,比如业务名:表名:id
不要包含特殊字符,反例:包含空格、换行、单双引号以及其他转义字符需要规范(car
+应用名+业务名+具体id
)
【7】审批记录检查:是否已经在审批记录conf
完整记录,包括审批人。
场景使用
合理使用数据结构: Redis
支持的数据库结构类型较多:字符串String
,哈希Hash
,列表List
,集合Set
,有序集合Sorted Set
, Bitmap
, HyperLogLog
和地理空间索引geospatial
redis
命令
需要根据业务场景选择合适的类型,常见的如:
【1】String
可以用作普通的K-V
、计数类;
【2】Hash
可以用作对象如商品、经纪人等,包含较多属性的信息;
【3】List
可以用作消息队列(不推荐)、粉丝/关注列表等;
【4】Set
可以用于推荐;
【5】Sorted Set
可以用于排行榜等;
键值设计
key
设计:
【1】可读性和可管理性【建议】
☑️ 以业务名(或数据库名)为前缀(防止key
冲突),用冒号分隔。
☑️ 应用名:表名:id
。
【2】简洁性,key
长度适中【建议】
☑️ 保证语义的前提下,控制key
的长度,当key
较多时,内存占用也不容忽视。
eg:user:{uid}:friends:messages:{mid}
简化为u:{uid}:fr:m:{mid}
。
【3】不要包含特殊字符【强制】
☑️ 禁止包含特殊字符如空格,换行,单双引号,其他转义字符。
【4】Key
个数限制【强制】
☑️ 由于Redis Rehash
机制,实例Key
数量达到一定值rehash
操作时,需要有一定量空闲内存资源,如key
达到134217728
,rehash
需要有2gb
空闲内存资源,达到268435456
时,rehash
需要有4gb
空闲内存资源。如果没有组够的内存资源rehash
时会发生Key
剔除(数据丢失/程序超时/甚至引起切换)。
单实例key
个数达到134217728
已经很大了,实例元素过大对于后续分析rdb
遍历大key
时会非常耗时。
value
设计
【1】拒绝bigkey
(防止网卡流量、慢查询)
☑️ 防止网卡流量、慢查询,string
类型控制在10KB
以内,hash
、list
、set
、zset
元素个数不要超过5000
。
☑️ 非字符串的bigkey
,不要使用del
删除,使用hscan
、sscan
、zscan
方式渐进式删除,同时要注意防止bigkey
过期时间自动删除问题(例如一个200
万的zset
设置1
小时过期,会触发del
操作,造成阻塞,而且该操作不会出现在慢查询中(latency
可查))
☑️ credis
页面,群集所有者可以通过unlink
异步清理或小批量迭代清理(或提事件给DBA
来处理)
【2】一定要设置过期时间,当实例写满,根据volatile-lru
淘汰老的数据
☑️ redis
只是缓存,不能当成数据库来用。不设置过期时间,redis
实例大小会一直无限增长,会出现机器内存耗尽、故障恢复耗时特别长等问题。
☑️ 建议使用expire
设置过期时间(条件允许可以打散过期时间,防止集中过期),不过期的数据重点关注idletime
(该命令返回的是当前键从上一次访问到现在经过的时间(单位,秒))
☑️ DBA
会定期对redis
集群中过期时间超过1
年的数据做告警处理。
命令使用
【1】禁用KEYS
正则匹配,可用SCAN
代替
☑️ 禁止线上使用keys
、flushall
、flushdb
等,通过redis
的rename
机制禁掉命令,或者使用scan
的方式渐进式处理。
【2】O(N)
命令关注N
,控制集合元素尽可能小
☑️ hgetall/lrange/smembers/zrange
等在集合包含元素个数较少的情况下使用。
☑️ 若规模较大,有遍历需求,可用HSCAN/SSCAN/ZSCAN
渐进式遍历。
【3】合理使用select
☑️ redis
的多数据库较弱,使用数字进行区分,很多客户端支持较差,同时多业务用多数据库实际还是单线程处理,会有干扰。
【4】Redis
事务支持较弱,不建议过多使用
☑️ redis
的事务功能较弱(不支持回滚),而且集群版本(自研和官方)要求一次事务操作的key
必须在一个slot
上(可以使用hashtag
功能解决)
【5】线上禁止使用monitor
命令
☑️ 禁止生产环境使用monitor
命令,monitor
命令在高并发条件下,会存在内存暴增和影响Redis
性能的隐患。
【6】使用批量操作提高效率
☑️ 原生命令:例如mget
、mset
。
☑️ 非原生命令:可以使用pipeline
提高效率。
☑️ 但要注意控制一次批量操作的元素个数(例如500
以内,实际也和元素字节数有关)。
注意两者不同:
1、原生是原子操作,pipeline
是非原子操作。
2、pipeline
可以打包不同的命令,原生做不到。
3、pipeline
需要客户端和服务端同时支持。
数据保存
【1】容量合理评估
☑️ 在系统设计阶段,需要考虑当前redis
集群的容量是否足够,设置合理的大小和过期时间
1、内存使用率保持在[50%~85%]
之间。
2、使用率<50%
需要考虑缩容。
3、使用率>85%
需要考虑扩容。
【2】冷热数据分离
☑️ 虽然Redis
支持持久化,但是Redis
的数据存储全部都是在内存中的,成本昂贵。
☑️ 建议根据业务只将高频热数据存储到Redis
中【QPS
大于5000
】,对于低频冷数据可以使用MySQL
/ElasticSearch
/MongoDB
等基于磁盘的存储方式,不仅节省内存成本,而且数据量小在操作时速度更快、效率更高。
【3】不同的业务数据要分开存储
☑️ 不要将不相关的业务数据都放到一个Redis
实例中,建议新业务申请新的单独实例。
☑️ 因为Redis
为单线程处理,独立存储会减少不同业务相互操作的影响,提高请求响应速度;同时也避免单个实例内存数据量膨胀过大,在出现异常情况时可以更快恢复服务。
【4】必须要存储的大文本数据一定要压缩后存储
☑️ 对于大文本【一般超过500
字节】写入到Redis
时,建议要压缩后存储。
☑️ 大文本数据存入Redis
,除了带来极大的内存占用外,在访问量高时,很容易就会将网卡流量占满,进而造成整个服务器上的所有服务不可用,并引发雪崩效应,造成各个系统瘫痪
七、并发编程规范
【1】【强制】获取单例对象需要保证线程安全,其中的方法也要保证线程安全。
说明: 资源驱动类、工具类、单例工厂类都需要注意。
【2】【强制】创建线程或线程池时请指定有意义的线程名称,方便出错时回溯。
正例: 自定义线程工厂,并且根据外部特征进行分组,比如,来自同一机房的调用,把机房编号赋值给whatFeaturOfGroup
public class UserThreadFactory implements ThreadFactory {
private final String namePrefix; private final AtomicInteger nextId = new AtomicInteger(1);
// 定义线程组名称,在 jstack 问题排查时,非常有帮助
UserThreadFactory(String whatFeaturOfGroup) {
namePrefix = "From UserThreadFactory's " + whatFeaturOfGroup + "-Worker-";
}
@Override
public Thread newThread(Runnable task) {
String name = namePrefix + nextId.getAndIncrement();
Thread thread = new Thread(null, task, name, 0, false);
System.out.println(thread.getName()); return thread;
}
}
【3】【强制】线程资源必须通过线程池提供,不允许在应用中自行显式创建线程。
说明: 线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源的开销,解决资源不足的问题。 如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题。
【4】【强制】线程池不允许使用Executors
去创建,而是通过ThreadPoolExecutor
的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
说明:Executors
返回的线程池对象的弊端如下:
1)FixedThreadPool
和SingleThreadPool
:允许的请求队列长度为Integer.MAX_VALUE
,可能会堆积大量的请求,从而导致OOM
。
2)CachedThreadPool
: 允许的创建线程数量为Integer.MAX_VALUE
,可能会创建大量的线程,从而导致OOM
。
【5】【强制】SimpleDateFormat
是线程不安全的类,一般不要定义为static
变量,如果定义为static
, 必须加锁,或者使用DateUtils
工具类。
正例:注意线程安全,使用DateUtils
。亦推荐如下处理:
private static final ThreadLocal df = new ThreadLocal() {
@Override
protected DateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd");
}
};
说明: 如果是JDK8
的应用,可以使用Instant
代替Date
,LocalDateTime
代替Calendar
,DateTimeFormatter
代替SimpleDateFormat
,官方给出的解释:simple beautiful strong immutable thread-safe
。
【6】【强制】必须回收自定义的ThreadLocal
变量,尤其在线程池场景下,线程经常会被复用, 如果不清理自定义的ThreadLocal
变量,可能会影响后续业务逻辑和造成内存泄露等问题。 尽量在代理中使用try-finally
块进行回收。
// 正例:
objectThreadLocal.set(userInfo);
try {
// ...
} finally {
objectThreadLocal.remove();
}
【7】【强制】高并发时,同步调用应该去考量锁的性能损耗。能用无锁数据结构,就不要用锁;能 锁区块,就不要锁整个方法体;能用对象锁,就不要用类锁。
说明: 尽可能使加锁的代码块工作量尽可能的小,避免在锁代码块中调用RPC
方法。
【8】【强制】对多个资源、数据库表、对象同时加锁时,需要保持一致的加锁顺序,否则可能会造 成死锁。
说明: 线程一需要对表A
、B
、C
依次全部加锁后才可以进行更新操作,那么线程二的加锁顺序也必须是A
、B
、C
,否则可能出现死锁。
【9】【强制】在使用阻塞等待获取锁的方式中,必须在try
代码块之外,并且在加锁方法与try
代码块之间没有任何可能抛出异常的方法调用,避免加锁成功后,在finally
中无法解锁。
说明一:如果在lock
方法与try
代码块之间的方法调用抛出异常,那么无法解锁,造成其它线程无法成功 获取锁。
说明二:如果lock
方法在try
代码块之内,可能由于其它方法抛出异常,导致在finally
代码块中,unlock
对未加锁的对象解锁,它会调用AQS
的tryRelease
方法(取决于具体实现类),抛出IllegalMonitorStateException
异常。
说明三:在Lock
对象的lock
方法实现中可能抛出unchecked
异常,产生的后果与说明二相同。
//正例:
Lock lock = new XxxLock(); // ...
lock.lock();
try {
doSomething();
doOthers();
} finally {
lock.unlock();
}
//反例:
Lock lock = new XxxLock();
// ...
try {
// 如果此处抛出异常,则直接执行 finally 代码块
doSomething();
// 无论加锁是否成功,finally 代码块都会执行
lock.lock();
doOthers();
} finally {
lock.unlock();
}
【10】【强制】在使用尝试机制来获取锁的方式中,进入业务代码块之前,必须先判断当前线程是否持有锁。锁的释放规则与锁的阻塞等待方式相同。
说明: Lock
对象的unlock
方法在执行时,它会调用AQS
的tryRelease
方法(取决于具体实现类),如果 当前线程不持有锁,则抛出 IllegalMonitorStateException
异常。
// 正例:
Lock lock = new XxxLock();
// ...
boolean isLocked = lock.tryLock();
if (isLocked) {
try {
doSomething();
doOthers();
} finally {
lock.unlock();
}
}
【11】【强制】并发修改同一记录时,避免更新丢失,需要加锁。要么在应用层加锁,要么在缓存加锁,要么在数据库层使用乐观锁,使用version
作为更新依据。
说明: 如果每次访问冲突概率小于20%
,推荐使用乐观锁,否则使用悲观锁。乐观锁的重试次数不得小于3
次。
【12】【强制】多线程并行处理定时任务时,Timer
运行多个TimeTask
时,只要其中之一没有捕获抛出的异常,其它任务便会自动终止运行,使用 ScheduledExecutorService
则没有这个问题。
【13】【推荐】资金相关的金融敏感信息,使用悲观锁策略。
说明: 乐观锁在获得锁的同时已经完成了更新操作,校验逻辑容易出现漏洞,另外,乐观锁对冲突的解决策 略有较复杂的要求,处理不当容易造成系统压力或数据异常,所以资金相关的金融敏感信息不建议使用乐观锁更新。
正例: 悲观锁遵循一锁二判三更新四释放的原则
【14】【推荐】使用CountDownLatch
进行异步转同步操作,每个线程退出前必须调用countDown
方法,线程执行代码注意catch
异常,确保countDown
方法被执行到,避免主线程无法执行至await
方法,直到超时才返回结果。
说明: 注意,子线程抛出异常堆栈,不能在主线程try-catch
到。
【15】【推荐】避免Random
实例被多线程使用,虽然共享该实例是线程安全的,但会因竞争同一seed
导致的性能下降。
说明: Random
实例包括java.util.Random
的实例或者Math.random()
的方式。
正例: 在JDK7
之后,可以直接使用API ThreadLocalRandom
,而在JDK7
之前,需要编码保证每个线 程持有一个单独的Random
实例。
八、外部代码规范
Google
代码规范文档,Java
代码规范:Java Style Guide
Python
代码规范:Python Style Guide
HTML/CSS
代码规范
HTML/CSS Style Guide
JavaScript
代码规范:JavaScript Style Guide
阿里巴巴阿里巴巴开发手册:泰山版
首先安装如下核心插件Alibaba Java Coding Guidelines该插件是阿里巴巴编码规范的IDEA
插件,通过该插件可以提示不符合规范的代码。
九、代码规范相关书籍
【1】《Clean Code: A Handbook of Agile Software Craftsmanship》
- Robert C. Martin
这本书是关于编写可读、可维护和高质量代码的经典之作。它介绍了一系列的代码规范和最佳实践,帮助开发者提高代码质量和开发效率。
【2】《Effective Java》
- Joshua Bloch
这本书是关于Java编程的最佳实践的指南。它提供了一系列的建议和规范,帮助开发者编写高效、健壮和易于维护的Java
代码。
【3】《Code Complete: A Practical Handbook of Software Construction》
- Steve McConnell
这本书是关于软件构建的实用指南,涵盖了从代码编写到测试和调试的各个方面。它提供了大量的代码规范和最佳实践,帮助开发者提高代码质量和开发效率。
【4】《The Pragmatic Programmer: Your Journey to Mastery》
- Andrew Hunt
, David Thomas
这本书是关于实用编程的指南,介绍了一系列的技巧和原则,帮助开发者成为更加高效和有经验的程序员。它包含了一些关于代码规范和可维护性的实用建议。
【5】《Java Coding Guidelines: 75 Recommendations for Reliable and Secure Programs》
- Fred Long
,Dhruv Mohindra, Robert C. Seacord, Dean F. Sutherland
, David Svoboda
这本书是关于Java
编码准则的指南,提供了75
条关于可靠性和安全性的建议。它涵盖了命名规范、异常处理、并发编程、安全性等方面的代码规范。