位枚举

也称为 二进制枚举。

什么是位枚举

位枚举也是一个枚举类,只不过其是利用二级制位运算来实现与枚举项的比较、包含、不包含等运算,其主要思想就是利用高性能的位运算操作来替换传统的大小比较,集合是否包含等方法,以此来进一步提高业务代码的性能。

常见的使用场景:

  • 标签字段
  • 类型字段
  • 等…

位枚举并不复杂。下面以标签字段的应用场景分别使用传统的枚举操作和位枚举进行对比说明,加深对位枚举的理解和应用。

传统枚举

枚举类定义

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
/**
* 标签枚举
*
* @author Maple
* @date 2023/4/28
*/
public enum OrderTagEnum {

NORMAL_ORDER(1, "普通订单"),

SECKILL_ORDER(2, "秒杀订单"),

THIRD_SERVICE_ORDER(3, "三方服务订单"),

;

private int code;

private String desc;

OrderTagEnum(int code, String desc) {
this.code = code;
this.desc = desc;
}

public int getCode() {
return code;
}

public static OrderTagEnum getEnumByCode(int code) {

for (OrderTagEnum tagEnum : OrderTagEnum.values()) {
if (tagEnum.getCode() == code) {
return tagEnum;
}
}

return null;
}

}

数据库中的标签字段定义

1
tags	varchar(64)	null comment '标签,List的json串',

如此,当我们以传统形式的枚举去表示标签时,那么对应的数据库表中对应的字段存储的值形式会是下图这样的格式(在代码防腐层,保存前我们会将以List<Integer>集合形式存储的tags标签值转换为Json字符串),常用的序列化组件有:Jackson、Gson、Fastjson等

1
2
Gson gson = new Gson();
String tagsJson = gson.toJson(Lists.newArrayList(OrderTagEnum.SECKILL_ORDER.getCode(), OrderTagEnum.THIRD_SERVICE_ORDER.getCode()));

存储的结果如下图:

与保存时对应,当进行数据库查询时,我们去查询库表,同样在代码防腐层会进行发序列化处理:

1
2
Gson gson = new Gson();
List<Integer> tagList = gson.fromJson(order.getTags(), new TypeToken<List<Integer>>() {}.getType());

传统枚举场景分析

性能侧

在以上使用传统枚举的过程中,保存数据库之前将标签字段序列化为Json串,查询数据库时再将Json串反序列化为List集合,在代码防腐层对标签字段的这两次序列化和反序列化操作,相比于标签字段直接存储,严格意义上来说是会存在一定的性能损耗,但是随着序列化组件性能的不断优化和服务器性能的提升,这两次序列化和反序列化操作对代码性能的影响可忽略不计,当然,这也不是位枚举的核心意义之所在。

数据库侧

增 | 改 都是针对数据库原数据新增和更新操作,剩下就是上述性能侧同样的问题。

查 | 删 就会涉及到数据库原数据字段的业务操作,比如:查询、统计或者删除指定标签的记录,判断记录是否包含指定标签等场景。针对这些逻辑使用传统的枚举实现的步骤为:

  1. 分页批量查询所有记录或者利用字符串模糊查询目标记录;
  2. 将tags字段反序列化为List<Integer>
  3. 利用List集合的API方法,指定标签值;
  4. 进行后续逻辑处理;

其中,步骤1步骤3是整个业务链路中性能影响最明显的环节,同时,此环节也是位枚举的核心意义之所在

位枚举

位枚举是指利用位运算实现的枚举类,枚举的code值必须是2的幂数,一般使用2^0,2^1,2^2,…依次递增。这样的话我们就可以利用位运算去快速的处理和枚举相关的业务。说到位运算大家第一反应应该都是速度嘎嘎快

位枚举的核心思想

  • 枚举项的code值请以2的幂数递增,如: 1,2,4,8,16,32,64,128…
  • 任意两个整数m和n,如果m和n都是2的幂数,那么m和n进行按位或运算的结果就等于m和n的字面值相加 ,什么意思呢?
    • 即:m|n = m+n,如m=2,n=4,那么m|n = 6。此处等价于加法运算
  • 任意两个整数m和n,如果m和n都是2的幂数,p是m和n按位或的结果,那么就有p&n=n,p&m=m,类似于减法运算。

位枚举定义

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
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
import com.google.common.collect.Lists;
import org.springframework.util.CollectionUtils;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;

/**
* 标签位枚举.
* ps: 枚举项的code值请以2的幂数递增,如: 1,2,4,8,16,32,64,128....
*
* @author Maple
* @date 2023/4/28
*/
public enum OrderTagEnum {

NORMAL_ORDER(1, "普通订单"),

SECKILL_ORDER(2, "秒杀订单"),

THIRD_SERVICE_ORDER(4, "三方服务订单"),

;

private int code;

private String desc;

OrderTagEnum(int code, String desc) {
this.code = code;
this.desc = desc;
}

public int getCode() {
return code;
}

/**
* 通过枚举项值获取对应的枚举项
*
* @param code
* @return
*/
public static OrderTagEnum getEnumByCode(Integer code) {

if (Objects.isNull(code)) {
return null;
}

for (OrderTagEnum tagEnum : OrderTagEnum.values()) {
if (tagEnum.getCode() == code) {
return tagEnum;
}
}

return null;

}

/**
* 判断入参tag是否包含tagEnum枚举项.
* 一个整数 & 枚举项code = 枚举项code,表示该整数包含枚举项.
*
* @param tag 多个标签的code累加和.
* @param tagEnum
* @return
*/
public static boolean isHasTag(Integer tag, OrderTagEnum tagEnum) {

if (Objects.isNull(tag) || Objects.isNull(tag)) {
return false;
}

return (tagEnum.getCode() & tag) == tagEnum.getCode();

}

/**
* 获取tag包含的所有标签枚举集合.
* 传入给定整数和任意枚举项进行按位与运算,结果等于枚举项本身,说明给定整数包含此枚举项.
*
* @param tag 多个标签的code累加和.
* @return
*/
public static List<OrderTagEnum> getTagEnumList(Integer tag) {

if (Objects.isNull(tag)) {
return Collections.emptyList();
}

ArrayList<OrderTagEnum> tagList = Lists.newArrayList();
for (OrderTagEnum tagEnum : OrderTagEnum.values()) {
if ((tagEnum.getCode() & tag) == tagEnum.getCode()) {
tagList.add(tagEnum);
}
}

return tagList;

}

/**
* 枚举项集合按位或运算 得到所有爱好的整数结果.
*
* @param tagEnumList
* @return
*/
public static Long calculate(List<OrderTagEnum> tagEnumList) {

if (CollectionUtils.isEmpty(tagEnumList)) {
return null;
}

Long result = 0L;

for (OrderTagEnum tagEnum : tagEnumList) {
result |= tagEnum.getCode();
}

return result;

}

}

对应的数据库标签字段也修改为Long类型,而不再是Json字符串:

1
tags	bigint	null comment '标签',

数据库标签字段存储变成了:

位枚举场景分析

增 | 改 都是针对数据库原数据增加或者更新操作,代码防腐层不再需要序列化和反序列化操作。

查 | 删  同样是查询、统计或者删除指定标签的记录,判断记录是否包含指定标签等场景。步骤就变成了业务代码的位运算和SQL脚本的位运算。

如:查询三方服务标签和秒杀标签的记录。

秒杀标签code | 三方服务标签code : 2 | 4 = 6,调用方法:

1
Long tagValue = OrderTagEnum.calculate(Arrays.asList(OrderTagEnum.NORMAL_ORDER,OrderTagEnum.THIRD_SERVICE_ORDER));

到SQL层面,入参#{tagValue}就为6,SQL层使用位与运算,就可以查询到所有包含三方服务标签和秒杀标签的记录:

1
2
select * from cloud_order.tb_order
where #{tagValue} = tags & #{tagValue};

tagValue为6时的结果:

查询到结果之后,在代码层调用:

1
List<OrderTagEnum> tagList = OrderTagEnum.getTagEnumList(tagValue);

得到目标值tagValue包含的所有标签枚举项列表。

一般情况下,位枚举相关业务涉及到数据库的操作时,通常也需要结合SQL的位运算来支撑位枚举。


参考文档:

位枚举应用