MyBatis-Plus 入门教程之二

大纲

前言

版本说明

本文的教程内容是基于 MyBatis-Plus 3.5.2 版本编写的,若你使用的是 2.x 或其他版本,可能会有部分知识点、案例代码不兼容,一切以 MyBatis-Plus 官方文档为准。

MyBatis-Plus 常用注解

提示

MyBatis-Plus 3.x 注解类包的源码:👉 mybatis-plus-annotation

@TableName

  • 描述:表名注解,标识实体类对应的表
  • 使用位置:实体类
1
2
3
4
5
6
7
8
9
@TableName("sys_user")
public class User {

private Long id;
private String name;
private Integer age;
private String email;

}
属性类型必须指定默认值描述
valueString""表名
schemaString""Schema
keepGlobalPrefixbooleanfalse是否保持使用全局的 tablePrefix 的值(当全局 tablePrefix 生效时)
resultMapString""SQL 映射文件中 resultMapid(用于满足特定类型的实体类对象绑定)
autoResultMapbooleanfalse是否自动构建 resultMap 并使用(如果设置 resultMap 则不会进行 resultMap 的自动构建与注入)
excludePropertyString[]{}需要排除的属性名(@since 3.3.1 version

关于 autoResultMap 的说明

MyBatis-Plus 会自动构建一个 resultMap 并注入到 MyBatis 里(一般用不上),请注意以下内容:

  • 因为 MyBatis-Plus 的底层是 MyBatis,所以 MyBatis-Plus 只是帮开发者注入了常用 CRUD 到 MyBatis 里,注入之前是动态的(根据开发者的 Entity 字段以及注解变化而变化),但是注入之后是静态的(等同于 XML 配置中的内容)
  • 对于 typeHandler 属性,MyBatis 只支持写在两个地方
    • 第一个地方:定义在 resultMap 里,作用于查询结果的封装
    • 第二个地方:定义在 insertupdate 语句的 #{property} 中的 property 后面(例:#{property, typehandler=xxx.xxx.xxx}),并且只作用于当前 设置值
    • 除了以上两种直接指定 typeHandler 的形式,MyBatis 还支持在全局配置文件中自定义 typeHandler 包的配置,原理是根据开发者的 property 类型去找其对应的 typeHandler 并使用。

@TableId

  • 描述:主键注解
  • 使用位置:实体类主键属性
1
2
3
4
5
6
7
8
9
10
@TableName("sys_user")
public class User {

@TableId
private Long id;
private String name;
private Integer age;
private String email;

}
属性类型必须指定默认值描述
valueString""主键字段名
typeEnumIdType.NONE指定主键类型

IdType

  • 描述:主键类型
描述
AUTO 数据库 ID 自增,该类型必须确保数据库表的主键字段设置了 ID 自增,否则无法生效
NONE 无状态,该类型为未设置主键类型(注解里等于跟随全局,全局里约等于 INPUT)
INPUT 用户自行设置主键值,该类型可以通过自己注册自动填充插件进行填充
ASSIGN_ID 分配 ID,主键类型为 Number [Long 和 Integer] 或 String(@since 3.3.0 version),使用的是 IdentifierGenerator 接口的 nextId 方法(默认实现类为 DefaultIdentifierGenerator,使用的是雪花算法)
ASSIGN_UUID 分配 UUID,主键类型为 String(@since 3.3.0 version),使用的是 IdentifierGenerator 接口的 nextUUID 方法
ID_WORKER 分布式全局唯一 ID 长整型类型(请使用 ASSIGN_ID
UUID32 位 UUID 字符串(请使用 ASSIGN_UUID
ID_WORKER_STR 分布式全局唯一 ID 字符串类型(请使用 ASSIGN_ID

注意

1、AUTO 主键类型必须确保数据库表的主键字段设置了 ID 自增,否则无法生效
2、ASSIGN_ID 主键类型(使用雪花算法生成 ID),与数据库表的主键字段是否设置 ID 自增没有任何关系
3、ASSIGN_IDASSIGN_UUID 这两种主键类型,只有当插入对象的 ID 为空时,才会自动填充主键值

@TableField

  • 描述:字段注解(非主键)
  • 使用位置:实体类非主键属性
1
2
3
4
5
6
7
8
9
10
11
@TableName("sys_user")
public class User {

@TableId
private Long id;
@TableField("nickname")
private String name;
private Integer age;
private String email;

}
属性类型必须指定默认值描述
valueString""数据库字段名
existbooleantrue是否为数据库表字段
conditionString""字段 where 实体查询比较条件,有值设置则按设置的值为准,没有则为默认全局的 %s=#{%s}
updateString""字段 update set 部分注入,例如:当在 version 字段上注解 update="%s+1",表示更新时会 set version=version+1 (该属性优先级高于 el 属性)
insertStrategyEnumFieldStrategy.DEFAULT举例:NOT_NULL
insert into table_a(<if test="columnProperty != null">column</if>) values (<if test="columnProperty != null">#{columnProperty}</if>)
updateStrategyEnumFieldStrategy.DEFAULT举例:IGNORED
update table_a set column=#{columnProperty}
whereStrategyEnumFieldStrategy.DEFAULT举例:NOT_EMPTY
where <if test="columnProperty != null and columnProperty!=''">column=#{columnProperty}</if>
fillEnumFieldFill.DEFAULT字段自动填充策略
selectbooleantrue是否进行 select 查询
keepGlobalFormatbooleanfalse是否保持使用全局的 format 进行处理
jdbcTypeJdbcTypeJdbcType.UNDEFINEDJDBC 类型 (该默认值不代表会按照该值生效)
typeHandlerClass<? extends TypeHandler>UnknownTypeHandler.class类型处理器 (该默认值不代表会按照该值生效)
numericScaleString""指定小数点后保留的位数

关于 jdbcType 和 typeHandler 以及 numericScale 的说明

  • numericScale 只生效于 update 的 SQL 语句
  • jdbcTypetypeHandler 如果不配合 @TableName(autoResultMap = true) 一起使用,也只生效于 update 的 SQL 语句
  • 对于 typeHandler,如果你的字段类型和 set 进去的类型为 equals 关系,则只需要让你的 typeHandler 让 Mybatis 加载到即可,不需要使用注解

FieldStrategy

字段策略 FieldStrategy 的作用主要是在进行新增、更新时,根据配置的策略判断是否对实体对象的值进行空值判断,如果策略为字段不能为空,则不会对为空的字段进行赋值或更新。同样的,在进行 where 条件查询时,根据 whereStrategy 策略判断是否对字段进行空值判断,如果策略为字段不能为空,则为空的字段不会作为查询条件组装到 where 条件中。

描述
IGNORED 忽略判断
NOT_NULL 非 NULL 判断
NOT_EMPTY 非空判断(只对字符串类型字段,其他类型字段依然为非 NULL 判断)
DEFAULT 追随全局配置
NEVER 不加入 SQL

FieldFill

  • 描述:字段自动填充策略
描述
DEFAULT 默认不处理
INSERT 插入时填充字段
UPDATE 更新时填充字段
INSERT_UPDATE 插入和更新时填充字段

@Version

  • 描述:乐观锁注解
  • 使用位置:实体类属性
  • 使用案例:乐观锁插件

@OrderBy

  • 描述:内置 SQL 默认排序规则,优先级低于 wrapper 条件查询
属性类型必须指定默认值描述
ascboolean false 是否升序查询
isDescboolean true 是否倒序查询
sortshort Short.MAX_VALUE 数字越小越靠前

@EnumValue

  • 描述:普通枚举类注解
  • 使用位置:实体类枚举类型的属性
  • 使用案例:通用枚举

@TableLogic

  • 描述:表字段逻辑处理注解(逻辑删除)
  • 使用场景:数据删除后需要进行数据恢复
  • 物理删除:真实删除,将对应数据从数据库中删除,之后查询不到此条被删除的数据
  • 逻辑删除:假删除,将对应数据中代表是否被删除字段的状态修改为 “被删除状态”,之后在数据库中仍旧能看到此条数据记录
  • 使用案例:逻辑删除
属性类型必须指定默认值描述
valueString “” 逻辑未删除时的值
delvalString “” 逻辑删除时的值

@KeySequence

  • 描述:序列主键策略 oracle
  • 属性:valueresultMap
属性类型必须指定默认值描述
valueString “” 序列名
clazzClass Long.classid 的类型,可以指定 String.class,这样返回的 Sequence 值是字符串 1

通用 Mapper CRUD 操作

本节所需的案例代码,可以直接从 GitHub 下载对应章节 mybatis-plus-lesson-03

查询数据操作

  • Mapper 接口
1
2
3
public interface EmployeeMapper extends BaseMapper<Employee> {

}
  • Junit 测试代码
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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
@SpringBootTest
public class MyBatisPlusApplicationTest {

@Autowired
private EmployeeMapper empMapper;

/**
* 根据ID查询
*/
@Test
public void selectById() {
Employee employee = empMapper.selectById(1L);
System.out.println(employee);
}

/**
* 根据多个ID查询
*/
@Test
public void selectBatchIds() {
List<Long> ids = Arrays.asList(1L, 2L, 3L);
List<Employee> list = empMapper.selectBatchIds(ids);
list.forEach(System.out::println);
}

/**
* 根据Map条件查询
*/
@Test
public void selectByMap() {
Map<String, Object> columnMap = new HashMap<String, Object>();
// 查询条件,指定的是表字段的名称,而不是实体类的属性名
columnMap.put("last_name", "Jim");
List<Employee> list = empMapper.selectByMap(columnMap);
list.forEach(System.out::println);
}

/**
* 查询所有数据
*/
@Test
public void selectList() {
List<Employee> list = empMapper.selectList(null);
list.forEach(System.out::println);
}

}

删除数据操作

  • Mapper 接口
1
2
3
public interface EmployeeMapper extends BaseMapper<Employee> {

}
  • Junit 测试代码
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
28
29
30
31
32
33
34
35
36
37
38
@SpringBootTest
public class MyBatisPlusApplicationTest {

@Autowired
private EmployeeMapper empMapper;

/**
* 根据ID删除数据
*/
@Test
public void deleteById() {
Integer deleteResult = empMapper.deleteById(2L);
System.out.println("deleteResult: " + deleteResult);
}

/**
* 根据多个ID删除数据
*/
@Test
public void deleteBatchIds() {
List<Long> ids = Arrays.asList(2L, 3L);
Integer deleteResult = empMapper.deleteBatchIds(ids);
System.out.println("deleteResult: " + deleteResult);
}

/**
* 根据Map条件删除数据
*/
@Test
public void deleteByMap() {
Map<String, Object> columnMap = new HashMap<String, Object>();
// 查询条件,指定的是表字段的名称,而不是实体类的属性名
columnMap.put("gender", "0");
Integer deleteResult = empMapper.deleteByMap(columnMap);
System.out.println("deleteResult: " + deleteResult);
}

}

插入数据操作

插入数据获取主键值

若数据库表使用的是自增主键,MyBatis 支持在插入数据后返回自增的主键值,详细的使用教程可以看 这里。值得一提的是,MyBatis-Plus 在插入数据后会自动将主键值回写到 JavaBean 中,不需要任何配置。示例代码如下:

  • Mapper 接口
1
2
3
public interface EmployeeMapper extends BaseMapper<Employee> {

}
  • Junit 测试代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@SpringBootTest
public class MyBatisPlusTest {

@Autowired
private EmployeeMapper empMapper;

@Test
public void insert() {
Employee employee = new Employee("David", "1", "david@gmail.com", 23);
Integer insertResult = empMapper.insert(employee);
System.out.println("insert result: " + insertResult);
System.out.println(employee);
}

}

上述代码执行后的结果如下:

1
2
insert result: 1
Employee [id=41, lastName=David, gender=1, email=david@gmail.com, age=23]

插入数据进行非空判断

MyBatis-Plus 在插入数据的时候,默认会对实体类的每个属性进行非空判断,只有非空的实体类属性会拼接到 SQL 语句中。

  • 实体类
1
2
3
4
5
6
7
8
9
10
11
public class Employee {

private Long id;
private String email;
private String lastName;
private String gender;
private Integer age;

...

}
  • Mapper 接口
1
2
3
public interface EmployeeMapper extends BaseMapper<Employee> {

}
  • Junit 测试代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@SpringBootTest
public class MyBatisPlusTest {

@Autowired
private EmployeeMapper empMapper;

@Test
public void insert() {
Employee employee = new Employee();
employee.setLastName("David");
employee.setEmail("david@gmail.com");
Integer insertResult = empMapper.insert(employee);
System.out.println("insertResult: " + insertResult);
System.out.println(employee);
}

}

观察下述的代码执行结果,可以发现只有非空的实体类属性会拼接到 SQL 语句中。

1
2
3
4
5
==>  Preparing: INSERT INTO t_employee ( last_name, email ) VALUES ( ?, ? )
==> Parameters: David(String), david@gmail.com(String)
<== Updates: 1
insertResult: 1
Employee [id=44, lastName=David, gender=null, email=david@gmail.com, age=null]

若希望新增操作忽略实体类属性的非空判断,可以在实体类的属性上使用 @TableField 注解,并通过 @TableField 注解的 insertStrategy 参数来指定判断策略。

  • 实体类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Employee {

private Long id;

private String email;

private String lastName;

@TableField(insertStrategy = FieldStrategy.IGNORED)
private String gender;

@TableField(insertStrategy = FieldStrategy.IGNORED)
private Integer age;

...

}

重新执行 Junit 测试代码,观察下述的执行结果,可以发现非空的实体类属性也会拼接到 SQL 语句中。

1
2
3
4
5
==>  Preparing: INSERT INTO t_employee ( last_name, gender, email, age ) VALUES ( ?, ?, ?, ? )
==> Parameters: David(String), null, david@gmail.com(String), null
<== Updates: 1
insertResult: 1
Employee [id=45, lastName=David, gender=null, email=david@gmail.com, age=null]

更新数据操作

更新数据进行非空判断

MyBatis-Plus 在更新数据的时候,默认会对实体类的每个属性进行非空判断,只有非空的属性才会拼接到 SQL 语句中。

  • 实体类
1
2
3
4
5
6
7
8
9
10
11
public class Employee {

private Long id;
private String email;
private String lastName;
private String gender;
private Integer age;

...

}
  • Mapper 接口
1
2
3
public interface EmployeeMapper extends BaseMapper<Employee> {

}
  • Junit 测试代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@SpringBootTest
public class MyBatisPlusTest {

@Autowired
private EmployeeMapper empMapper;

@Test
public void update() {
Employee employee = new Employee();
employee.setId(1L);
employee.setLastName("David");
employee.setEmail("david@gmail.com");
Integer updateReuslt = empMapper.updateById(employee);
System.out.println("updateReuslt: " + updateReuslt);
}

}

观察下述的代码执行结果,可以发现只有非空的实体类属性会拼接到 SQL 语句中。

1
2
3
4
==>  Preparing: UPDATE t_employee SET last_name=?, email=? WHERE id=?
==> Parameters: David(String), david@gmail.com(String), 1(Long)
<== Updates: 1
updateReuslt: 1

若希望更新操作忽略实体类属性的非空判断,可以在实体类的属性上使用 @TableField 注解,并通过 @TableField 注解的 updateStrategy 参数来指定判断策略。

  • 实体类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class Employee {

private Long id;

private String email;

private String lastName;

@TableField(updateStrategy = FieldStrategy.IGNORED)
private String gender;

@TableField(updateStrategy = FieldStrategy.IGNORED)
private Integer age;

...

}

重新执行 Junit 测试代码,观察下述的执行结果,可以发现非空的实体类属性也会拼接到 SQL 语句中。

1
2
3
4
==>  Preparing: UPDATE t_employee SET email=?, last_name=?, gender=?, age=? WHERE id=?
==> Parameters: david@gmail.com(String), David(String), null, null, 1(Long)
<== Updates: 1
updateReuslt: 1

通用 Service CRUD 操作

MyBatis-Plus 为通用的 Service CRUD 操作提供了 IService 接口 和 ServiceImpl 实现类,里面封装了常用的 CRUD 方法。本节所需的案例代码,可以直接从 GitHub 下载对应章节 mybatis-plus-lesson-04

创建 Service 接口和实现类

  • Service 接口
1
2
3
4
5
6
/**
* IEmployeeService 接口继承 IService 接口提供的基础功能
*/
public interface IEmployeeService extends IService<Employee> {

}
  • Service 实现类
1
2
3
4
5
6
7
/**
* EmployeeServiceImpl 类实现了 IEmployeeService 接口,并继承 ServiceImpl 类提供的基础功能
*/
@Service
public class EmployeeServiceImpl extends ServiceImpl<EmployeeMapper, Employee> implements IEmployeeService {

}

调用 Service 接口和实现类

  • Junit 测试代码
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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
@SpringBootTest
public class MyBatisPlusApplicationTest {

@Autowired
private IEmployeeService empService;

/**
* 查询总记录数
*/
@Test
public void count() {
Long count = empService.count();
System.out.println("count: " + count);
}

/**
* 更新数据
*/
@Test
public void update() {
Employee employee = new Employee();
employee.setId(1L);
employee.setGender("0");
boolean updateResult = empService.updateById(employee);
System.out.println("updateResult: " + updateResult);
}

/**
* 批量插入
*/
@Test
public void saveBatch() {
List<Employee> list = new ArrayList<>();
for (int i = 0; i < 5; i++) {
Employee emp = new Employee();
emp.setAge(26);
emp.setGender("1");
emp.setLastName("user-" + i);
list.add(emp);
}
boolean saveResult = this.empService.saveBatch(list);
System.out.println("saveResult: " + saveResult);
}

/**
* 删除数据
*/
@Test
public void delete() {
boolean deleteResult = this.empService.removeById(11L);
System.out.println("deleteResult: " + deleteResult);
}

}

数据库扩展介绍

在企业开发中,往往需要选择合适的方案去应对数据规模的增长,以应对数据库逐渐增长的访问压力和数据量。数据库的扩展方式主要包括:业务分库、主从复制、数据库分表、数据库分片、数据库分区。

数据库分表

将不同业务数据分散存储到不同的数据库服务器,能够支撑百万甚至千万用户规模的业务,但如果业务继续发展,同一业务的单表数据也会达到单台数据库服务器的处理瓶颈。例如,淘宝的几亿用户数据,如果全部存放在一台数据库服务器的一张表中,肯定是无法满足性能要求的,此时就需要对单表数据进行拆分。单表数据拆分有两种方式,包括垂直分表和水平分表。示意图如下:

垂直分表

垂直分表适合将表中某些不常用且占了大量空间的列拆分出去。例如,上面示意图中的 nicknamedescription 字段,假设开发一个婚恋网站,用户在筛选其他用户的时候,主要是用 agesex 两个字段进行查询,而 nicknamedescription 两个字段主要用于展示,一般不会在业务查询中用到。description 字段本身又比较占存储空间,因此可以将这两个字段独立到另外一张表中,这样在查询 agesex 时,就能带来一定的性能提升。

水平分表

水平分表适合表行数特别大的表,有的公司要求单表行数超过 5000万 就必须进行分表,这个数字可以作为参考,但并不是绝对标准,关键还是要看表的访问性能。对于一些比较复杂的表,可能超过 1000万 就要分表了;而对于一些简单的表,即使存储数据超过 1亿 行,也可以不分表。但不管怎样,当看到表的数据量达到千万级别时,作为架构师就要警觉起来,因为这很可能是架构的性能瓶颈或者隐患。值得一提的是,水平分表相比垂直分表,会引入更多的复杂性,例如要处理全局唯一的数据 ID(分布式唯一全局 ID)。

提示

更多关于分布式唯一全局 ID 的生成方案可以看 这里

UUID

UUID 按照 OSF 制定的标准计算,用到了以太网卡地址、纳秒级时间、芯片 ID 码和许多可能的数字,并由以下几部分的组成:当前日期和时间、时钟序列、全局唯一的 IEEE 机器识别号。UUID 的标准形式包含 32 个 16 进制的数字,以连字号分为 5 段,形式为 cb6ce510-74fe-4e18-ac3d-05a5c96d3a0f 的 36 个字符。特别注意,基于 MAC 地址生成 UUID 的算法可能会造成 MAC 地址泄露,这个漏洞曾被用于寻找梅丽莎病毒的制作者位置。

  • 适用场景:只要对存储空间没有苛刻要求的都能够适用,比如各种链路追踪、日志存储等。
  • 优点:性能高,本地生成,没有网络消耗。如果只考虑唯一性,那么可以使用。
  • 缺点
    • 无序:即无法预测 ID 的生成顺序,不能生成递增的有序数字。
    • ID 长度过长:分布式 ID 一般都是作为主键,但是 MySQL 官方推荐主键尽量越短越好,36 个字符长度的 UUID 不是很推荐。
    • 入库性能差:既然分布式 ID 是主键,然后主键是包含索引的,MySQL 的索引是通过 B+ 树来实现的,每一次新的 UUID 数据的插入,为了查询的优化,都会对索引的底层 B+ 树进行修改。因为 UUID 数据是无序的,所以每一次 UUID 的数据插入都会对主键底层的 B+ 树进行很大的修改,这一点非常不好。插入的 ID 完全无序,不但会导致一些中间节点产生分裂(B+ 树索引分裂),也会白白的创造出很多的不饱和节点,这样大大的降低了数据库的插入性能。
主键自增

以最常见的用户 ID 为例,可以按照 1000000 的范围大小进行分段,1 ~ 999999 放到表 1 中,1000000 ~ 1999999 放到表 2 中,以此类推。

  • 复杂点:分段大小的选取。分段太小会导致切分后子表数量过多,增加维护复杂度;分段太大可能会导致单表依然存在性能问题,一般建议分段大小在 100万2000万 之间,具体需要根据业务需求选取合适的分段大小。
  • 优点:可以随着数据的增加平滑地扩充新的表。例如,现在的用户是 100 万,如果增加到 1000 万,只需要增加新的表就可以了,原有的数据不需要动。
  • 缺点:分布不均匀。假如按照 1000 万来进行分表,有可能某个分段实际存储的数据量只有 1 条,而另外一个分段实际存储的数据量有 1000 万条。
取模运算

同样以用户 ID 为例,假如一开始就规划了 10 个数据库表,可以简单地用 user_id % 10 的值来表示数据所属的数据库表编号,ID 为 985 的用户放到编号为 5 的子表中,ID 为 10086 的用户放到编号为 6 的子表中。

  • 复杂点:初始表数量的确定。表数量太多维护比较麻烦,表数量太少又可能导致单表性能存在问题。
  • 优点:表分布比较均匀。
  • 缺点:扩充新的表很麻烦,所有表的数据都要重新迁移。
雪花算法

雪花算法(SnowFlake)是由 Twitter 公布的分布式主键生成算法,它能够保证不同表的主键的不重复性,以及相同表的主键的有序性。雪花算法的特性是有序、全局唯一、高性能、低延迟(响应时间在 2ms 以内),可在分布式环境(多集群,跨机房)下使用,因此使用雪花算法得到的 ID 是分段组成的:

  • 与指定日期的时间差(毫秒级),41 位,够用 69 年
  • 集群 ID + 机器 ID,一共 10 位,包括 5 位 datacenterId 和 5 位 workerId,最多支持 1024 台机器
  • 序列号,12 位,每台机器每毫秒内最多产生 4096 个序列号

snowflake-1

  • 1bit:符号位,固定是 0,表示全部 ID 都是正整数
  • 41bit:时间戳(毫秒数时间差),从指定的日期算起,够用 69 年,用 Long 类型表示的时间戳是从 1970-01-01 00:00:00 开始算起的
  • 10bit:机器 ID,有异地部署,多集群的也可以配置,需要线下规划好各地机房,各集群,各实例 ID 的编号
  • 12bit:序列号,前面都相同的话,最多可以支持到 4096 个

优缺点:

  • 适用场景:分布式应用环境的数据主键
  • 优点:
    • 可以根据自身业务特性分配 bit 位,非常灵活
    • 毫秒数在高位,自增序列在低位,整个 ID 都是趋势递增的
    • 不依赖数据库等三方系统,以服务的方式部署,稳定性更高,生成 ID 的效率也是非常高,低延迟
  • 缺点:
    • 强依赖机器时钟,如果机器的时钟回拨了,会导致生成重复的 ID
    • 若生成环境中使用了容器化技术,实例的个数随时有变化,那么 SnowFlake 算法需要一定的改造才能更好地应用到生产环境中
    • 在单机上是递增的,但是由于涉及到分布式环境,每台机器上的时钟不可能完全同步(如时钟回拨),有时候可能会出现不是全局递增的情况(此缺点可认为无所谓,一般分布式 ID 只是要求趋势递增,并不会严格要求递增,90% 的业务需求都只需要趋势递增)