HiSEN

优雅地分桶 - 数据分片 - list 拆分

零、背景

最近在做数据迁移
为了加速迁移速度
其中就需要把查询到的数据( max 100 条)
拆分成 5 份,然后执行 5 个子任务,加速处理

一、代码

1.1 常规做法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static <T> List<List<T>> split2(List<T> lists, int subCount) {
if (CollectionUtils.isEmpty(lists) || subCount < 1) {
return null;
}

List<List<T>> result = new ArrayList<>();

int size = lists.size();
int count = (size + subCount - 1) / subCount;

for (int i = 0; i < count; i++) {
List<T> subList = lists.subList(i * subCount, (Math.min((i + 1) * subCount, size)));
result.add(subList);
}
return result;
}

1.2 新颖做法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public static <T> List<List<T>> split(List<T> lists, int subCount) {
if (CollectionUtils.isEmpty(lists) || subCount < 1) {
return null;
}
// 简约的计算分桶次数,避免判断是否有余数,再+1
int splitTimes = (lists.size() + subCount - 1) / subCount;
return Stream
// 每次递增指定子列表个数
.iterate(0, n -> n + subCount)
// 限制循环次数
.limit(splitTimes)
// 转换成想要的结果
.map(item -> lists.stream()
// 跳过之前的下标
.skip(item)
// 限制每次的个数为子表个数
.limit(subCount)
// 转换为子列表
.collect(Collectors.toList()))
// 转换为主列表
.collect(Collectors.toList());
}

三、性能

3.1 结果

从结果可以看出,一味地追求新颖也不是好事,需要知其然
java 8 的 stream 并不占优势,性能相差好几个数量级…

1
2
3
Benchmark              Mode  Cnt       Score       Error  Units
ListSplit.split2Test thrpt 10 306050.155 ± 24646.689 ops/s
ListSplit.splitTest thrpt 10 353.048 ± 122.423 ops/s

3.3 压测代码

压测教程:JMH 使用参考

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
import com.google.api.client.util.Lists;
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.BenchmarkMode;
import org.openjdk.jmh.annotations.Fork;
import org.openjdk.jmh.annotations.Measurement;
import org.openjdk.jmh.annotations.Mode;
import org.openjdk.jmh.annotations.Scope;
import org.openjdk.jmh.annotations.Setup;
import org.openjdk.jmh.annotations.State;
import org.openjdk.jmh.annotations.Warmup;
import org.springframework.util.CollectionUtils;

import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
* @author hisenyuan
* @date 2022/2/18 20:05
*/
// 默认的 State,每个测试线程分配一个实例
@State(Scope.Thread)
// 如果 fork 数是 2 的话,则 JMH 会 fork 出两个进程来进行测试。
@Fork(1)
// 预热的次数 3 次基准测试(不是执行三次方法)
@Warmup(iterations = 1)
// 基准执行次数 10 次(参数含义同上)
@Measurement(iterations = 10)
@BenchmarkMode(Mode.Throughput)
public class ListSplit {
private static final List<Integer> INTEGERS = Lists.newArrayList();
@Setup
public void init() {
List<Integer> list = Stream.iterate(0, n -> n + 1)
.limit(1000)
.collect(Collectors.toList());
INTEGERS.addAll(list);
}

@Benchmark
public void splitTest(){
List<List<Integer>> listList = split(INTEGERS, 3);
}
@Benchmark
public void split2Test(){
List<List<Integer>> listList = split2(INTEGERS, 3);
}

public static <T> List<List<T>> split(List<T> lists, int subCount) {
// 简约的计算分桶次数,避免判断是否有余数,再+1
int splitTimes = (lists.size() + subCount - 1) / subCount;
return Stream
// 每次递增指定子列表个数
.iterate(0, n -> n + subCount)
// 限制循环次数
.limit(splitTimes)
// 转换成想要的结果
.map(item -> lists.stream()
// 跳过之前的下标
.skip(item)
// 限制每次的个数为子表个数
.limit(subCount)
// 转换为子列表
.collect(Collectors.toList()))
// 转换为主列表
.collect(Collectors.toList());
}

public static <T> List<List<T>> split2(List<T> list, int len) {
if (CollectionUtils.isEmpty(list) || len < 1) {
return null;
}

List<List<T>> result = new ArrayList<>();

int size = list.size();
int count = (size + len - 1) / len;

for (int i = 0; i < count; i++) {
List<T> subList = list.subList(i * len, (Math.min((i + 1) * len, size)));
result.add(subList);
}
return result;
}

四、后话

其中不错的一点我觉得是

1
int splitTimes = (lists.size() + subCount - 1) / subCount;

这样巧妙的计算,避免了先取模,后判断是否有余数的繁杂。
这个不是我想到的,也不知道怎么通过数学证明,但是这样就是对的 ==

ps:是谁说数学没用的 ????