├── README.md
├── pom.xml
├── split-algorithm.png
├── split-flow.png
└── src
└── main
├── java
└── com
│ └── flow
│ └── experiment
│ ├── AbstractExperSplitHandler.java
│ ├── DefaultExperSplitHandler.java
│ ├── DefaultExperSplitHandlerBuilder.java
│ ├── DefaultSplitBo.java
│ ├── ISpitBo.java
│ └── model
│ ├── ExperBo.java
│ ├── Experiment.java
│ └── ExperimentMark.java
└── test
├── DemoExperList.java
└── TestRunner.java
/README.md:
--------------------------------------------------------------------------------
1 | # split
2 | 分流工具,针对不同层级对用户进行分流处理,针对试验情况的分流处理。
3 | 可用于AB测试。
4 |
5 | # 设计描述
6 | 采用-分层分流。同一层的分流共享流量,不同层的流量独立分流,支持新增加分流方案,和停止分流方案重新分流,采用责任链模式进行处理,依赖jdk8
7 |
8 | # 层级之间是独立分流的,每个分出去的流量都是一个小管道,每个管道中又存在不同版本
9 | 
10 |
11 | # 默认采用随机数分流算法,图示按照千分制级进行分流处理
12 | 
13 |
14 |
--------------------------------------------------------------------------------
/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
5 | 4.0.0
6 |
7 | com.flow
8 | split
9 | 1.0.0
10 | jar
11 |
12 | split
13 |
14 |
15 | UTF-8
16 | 1.8
17 | 1.8
18 |
19 |
20 |
21 |
22 | junit
23 | junit
24 | 4.12
25 | test
26 |
27 |
28 | org.apache.commons
29 | commons-lang3
30 | 3.7
31 |
32 |
33 |
34 |
35 | split
36 |
37 |
38 |
39 |
40 | org.apache.maven.plugins
41 | maven-source-plugin
42 | 2.2.1
43 |
44 | true
45 |
46 |
47 |
48 | compile
49 |
50 | jar
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
--------------------------------------------------------------------------------
/split-algorithm.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhouwenmo/split/2d82a6ce739daa99985421ccaf5f68b86efec3d4/split-algorithm.png
--------------------------------------------------------------------------------
/split-flow.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zhouwenmo/split/2d82a6ce739daa99985421ccaf5f68b86efec3d4/split-flow.png
--------------------------------------------------------------------------------
/src/main/java/com/flow/experiment/AbstractExperSplitHandler.java:
--------------------------------------------------------------------------------
1 | package com.flow.experiment;
2 |
3 |
4 | import com.flow.experiment.model.ExperimentMark;
5 |
6 | import java.util.HashSet;
7 |
8 | /**
9 | * 试验分流处理者抽象类(责任链)
10 | * 注意责任链是有顺序的,新增的试验,要放在责任链最后
11 | *
12 | * @author zwm
13 | * @date 2019-10-15
14 | */
15 | public abstract class AbstractExperSplitHandler {
16 | /**
17 | * 下一个处理者
18 | */
19 | private AbstractExperSplitHandler nextHandler;
20 |
21 | /**
22 | * 处理试验
23 | *
24 | * @param request 携带参数
25 | * @param response 此参数不能为空,这个参数需要返回用户使用
26 | * @param splitBo 分流业务类,不可为空
27 | */
28 | public final void handler(ExperimentMark request, ExperimentMark response, ISpitBo splitBo) {
29 | if (response == null || splitBo == null) {
30 | //不进行分流处理
31 | return;
32 | }
33 | if (response.getParticipationSet() == null) {
34 | response.setParticipationSet(new HashSet<>());
35 | }
36 | //流过当前管道
37 | boolean through = through(request, response, splitBo);
38 | if (through) {
39 | //成功命中版本,进入上层通道处理
40 | handler(response);
41 | return;
42 | }
43 | if (nextHandler == null) {
44 | //到达最后一个处理者返回
45 | return;
46 | }
47 | //下一个管道处理者处理
48 | nextHandler.handler(request, response, splitBo);
49 | }
50 |
51 | /**
52 | * 上层管道处理,只加入响应标记
53 | *
54 | * @param response 响应内容
55 | */
56 | private void handler(ExperimentMark response) {
57 | //流过上层通道
58 | through(response);
59 | if (nextHandler != null) {
60 | //存在下级流过下级通道
61 | nextHandler.handler(response);
62 | }
63 | }
64 |
65 | public AbstractExperSplitHandler getNextHandler() {
66 | return nextHandler;
67 | }
68 |
69 | public void setNextHandler(AbstractExperSplitHandler nextHandler) {
70 | this.nextHandler = nextHandler;
71 | }
72 |
73 | /**
74 | * 流过试验,处理
75 | *
76 | * @param request 携带请求参数
77 | * @param response 携带响应参数
78 | * @param splitBo 分流处理类
79 | * @return 是否分流结束
80 | */
81 | protected abstract boolean through(ExperimentMark request, ExperimentMark response, ISpitBo splitBo);
82 |
83 | /**
84 | * 上层管道流过,只加入参与标记
85 | *
86 | * @param response 响应内容
87 | */
88 | protected abstract void through(ExperimentMark response);
89 | }
90 |
--------------------------------------------------------------------------------
/src/main/java/com/flow/experiment/DefaultExperSplitHandler.java:
--------------------------------------------------------------------------------
1 | package com.flow.experiment;
2 |
3 |
4 | import com.flow.experiment.model.ExperBo;
5 | import com.flow.experiment.model.ExperimentMark;
6 |
7 | /**
8 | * 试验分流具体类
9 | *
10 | * @author zwm
11 | * @date 2019-10-15
12 | */
13 | public class DefaultExperSplitHandler extends AbstractExperSplitHandler {
14 | private ExperBo experBo;
15 |
16 | public DefaultExperSplitHandler() {
17 | super();
18 | }
19 |
20 | public DefaultExperSplitHandler(ExperBo experBo) {
21 | super();
22 | this.experBo = experBo;
23 | }
24 |
25 | public ExperBo getExperBo() {
26 | return experBo;
27 | }
28 |
29 | public void setExperBo(ExperBo experBo) {
30 | this.experBo = experBo;
31 | }
32 |
33 |
34 | @Override
35 | public boolean through(ExperimentMark request, ExperimentMark response, ISpitBo splitBo) {
36 | through(response);
37 | return splitBo.split(request, response, experBo);
38 | }
39 |
40 | @Override
41 | protected void through(ExperimentMark response) {
42 | if (experBo == null) {
43 | //试验数据空,分流结束
44 | return;
45 | }
46 | //添加参与标记
47 | response.getParticipationSet().add(experBo.getExperimentId());
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/main/java/com/flow/experiment/DefaultExperSplitHandlerBuilder.java:
--------------------------------------------------------------------------------
1 | package com.flow.experiment;
2 |
3 |
4 | import com.flow.experiment.model.ExperBo;
5 |
6 | import java.util.List;
7 |
8 | /**
9 | * 默认分流处理的构造者
10 | *
11 | * @author zwm
12 | * @date 2019-10-23
13 | */
14 | public class DefaultExperSplitHandlerBuilder {
15 | /**
16 | * 构造试验流处理责任链对象,试验集合中试验id唯一
17 | *
18 | * @param experBoList 试验对象集合,装配按照list顺序进行从后向前装配(即第一个队形最后处理)。需要按照开始时间降序传入(最后开始的放在最后一个处理)。
19 | * @return 默认
20 | */
21 | public static AbstractExperSplitHandler builder(List experBoList) {
22 | if (experBoList == null || experBoList.size() == 0) {
23 | return null;
24 | }
25 | AbstractExperSplitHandler result = null;
26 | for (ExperBo experBo : experBoList) {
27 | AbstractExperSplitHandler experSplitHandler = new DefaultExperSplitHandler(experBo);
28 | experSplitHandler.setNextHandler(result);
29 | result = experSplitHandler;
30 | }
31 | return result;
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/main/java/com/flow/experiment/DefaultSplitBo.java:
--------------------------------------------------------------------------------
1 | package com.flow.experiment;
2 |
3 |
4 | import com.flow.experiment.model.ExperBo;
5 | import com.flow.experiment.model.Experiment;
6 | import com.flow.experiment.model.ExperimentMark;
7 | import org.apache.commons.lang3.RandomUtils;
8 |
9 | /**
10 | * 默认分流算法,分层分流算法
11 | *
12 | * @author zwm
13 | * @date 2019-10-22
14 | */
15 | public class DefaultSplitBo implements ISpitBo {
16 | private int limit;
17 | private int random;
18 | private boolean startRandom;
19 |
20 |
21 | /**
22 | * 初始化,上限1000,随机数1000,未开启随机
23 | */
24 | public DefaultSplitBo() {
25 | this.limit = 1000;
26 | this.random = 1000;
27 | this.startRandom = false;
28 | }
29 |
30 | /**
31 | * 构造默认随机算法
32 | *
33 | * @param totalFlow 总流量数
34 | */
35 | public DefaultSplitBo(int totalFlow) {
36 | this.limit = totalFlow;
37 | this.random = totalFlow;
38 | this.startRandom = false;
39 | }
40 |
41 | @Override
42 | public boolean split(ExperimentMark request, ExperimentMark response, ExperBo experBo) {
43 | if (this.startRandom) {
44 | return splitByRandom(response, experBo);
45 | } else {
46 | return splitByLimit(request, response, experBo);
47 | }
48 | }
49 |
50 | /**
51 | * 根据上线阈值进行分流
52 | *
53 | * @param request 携带的标记
54 | * @param response 响应的标记
55 | * @param experBo 试验的业务信息
56 | * @return 分流结束标记
57 | */
58 | private boolean splitByLimit(ExperimentMark request, ExperimentMark response, ExperBo experBo) {
59 | if (request != null && request.getParticipationSet().contains(experBo.getExperimentId())) {
60 | //携带参与标记
61 | if (checkExperimentMark(request.getExperiment(), experBo)) {
62 | //携带此试验的试验标记,放入试验,分流结束
63 | response.setExperiment(request.getExperiment());
64 | return true;
65 | }
66 | //降低上限阈值
67 | limit = limit - experBo.getFlow() * experBo.getNum();
68 | } else {
69 | if (limit < 0) {
70 | //限制小于0直接返回
71 | return true;
72 | }
73 | //未参与过,开启随机算法
74 | startRandom = true;
75 | //生成随机数并分流
76 | int randomCreate = RandomUtils.nextInt(0, limit);
77 | Experiment split = split(randomCreate, experBo);
78 | if (split != null) {
79 | response.setExperiment(split);
80 | return true;
81 | }
82 | //降低随机数
83 | this.random = randomCreate - experBo.getNum() * experBo.getFlow();
84 | }
85 | return false;
86 | }
87 |
88 | /**
89 | * 检查试验参与标记是否符合规范命中
90 | *
91 | * @param experiment 试验标记
92 | * @param experBo 试验对象
93 | * @return 是否真的命中
94 | */
95 | private boolean checkExperimentMark(Experiment experiment, ExperBo experBo) {
96 | return experiment != null && experiment.getExperimentId().equals(experBo.getExperimentId()) && experiment.getLayerId().equals(experBo.getLayerId()) && experiment.getType().equals(experBo.getType()) && experiment.getVersion() <= experBo.getNum();
97 | }
98 |
99 | /**
100 | * 根据随机数分流
101 | *
102 | * @param response 响应标记
103 | * @param experBo 试验信息
104 | * @return 分流结束标记
105 | */
106 | private boolean splitByRandom(ExperimentMark response, ExperBo experBo) {
107 | //已经开始随机算法
108 | if (random < 0) {
109 | //随机数小于0 结束后续处理
110 | return true;
111 | }
112 | Experiment split = split(random, experBo);
113 | if (split != null) {
114 | response.setExperiment(split);
115 | return true;
116 | }
117 | //降低随机数
118 | random = random - experBo.getNum() * experBo.getFlow();
119 | return false;
120 | }
121 |
122 | private Experiment split(int random, ExperBo experBo) {
123 | int version = random / experBo.getFlow() + 1;
124 | if (version > experBo.getNum()) {
125 | return null;
126 | }
127 | return new Experiment(experBo, version);
128 | }
129 | }
130 |
--------------------------------------------------------------------------------
/src/main/java/com/flow/experiment/ISpitBo.java:
--------------------------------------------------------------------------------
1 | package com.flow.experiment;
2 |
3 |
4 | import com.flow.experiment.model.ExperBo;
5 | import com.flow.experiment.model.ExperimentMark;
6 |
7 | /**
8 | * 分流对象接口,继承实现自定义接口
9 | *
10 | * @author zwm
11 | * @date 2019-10-23
12 | */
13 | public interface ISpitBo {
14 | /**
15 | * 进行分流
16 | *
17 | * @param request 携带的标记
18 | * @param response 响应的标记
19 | * @param experBo 试验的业务信息
20 | * @return 分流结束标记
21 | */
22 | public boolean split(ExperimentMark request, ExperimentMark response, ExperBo experBo);
23 | }
24 |
--------------------------------------------------------------------------------
/src/main/java/com/flow/experiment/model/ExperBo.java:
--------------------------------------------------------------------------------
1 | package com.flow.experiment.model;
2 |
3 | /**
4 | * 试验业务对象
5 | * @author zwm
6 | * @date 2019-10-15
7 | */
8 | public class ExperBo {
9 | /**
10 | * 试验Id
11 | */
12 | private Integer experimentId;
13 | /**
14 | * 试验 层级id
15 | */
16 | private Integer layerId;
17 | /**
18 | * 类型 同一层及下的多种类型
19 | */
20 | private Integer type;
21 | /**
22 | * 版本数量
23 | */
24 | private Integer num;
25 | /**
26 | * 每个试验版本的流量
27 | */
28 | private Integer flow;
29 |
30 | public ExperBo() {
31 | }
32 |
33 | public ExperBo(Integer experimentId, Integer layerId, Integer type, Integer num, Integer flow) {
34 | this.experimentId = experimentId;
35 | this.layerId = layerId;
36 | this.type = type;
37 | this.num = num;
38 | this.flow = flow;
39 | }
40 |
41 | public Integer getExperimentId() {
42 | return experimentId;
43 | }
44 |
45 | public void setExperimentId(Integer experimentId) {
46 | this.experimentId = experimentId;
47 | }
48 |
49 | public Integer getLayerId() {
50 | return layerId;
51 | }
52 |
53 | public void setLayerId(Integer layerId) {
54 | this.layerId = layerId;
55 | }
56 |
57 | public Integer getType() {
58 | return type;
59 | }
60 |
61 | public void setType(Integer type) {
62 | this.type = type;
63 | }
64 |
65 | public Integer getNum() {
66 | return num;
67 | }
68 |
69 | public void setNum(Integer num) {
70 | this.num = num;
71 | }
72 |
73 | public Integer getFlow() {
74 | return flow;
75 | }
76 |
77 | public void setFlow(Integer flow) {
78 | this.flow = flow;
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/src/main/java/com/flow/experiment/model/Experiment.java:
--------------------------------------------------------------------------------
1 | package com.flow.experiment.model;
2 |
3 |
4 | /**
5 | * 试验对象
6 | *
7 | * @author zwm
8 | * @date 2019-10-17
9 | */
10 | public class Experiment {
11 | /**
12 | * 试验的id
13 | */
14 | private Integer experimentId;
15 | /**
16 | * 版本
17 | */
18 | private Integer version;
19 | /**
20 | * 试验类型 ,同一层及下的多种类型
21 | */
22 | private Integer type;
23 | /**
24 | * 层级id
25 | */
26 | private Integer layerId;
27 |
28 | public Experiment() {
29 | }
30 |
31 | public Experiment(Integer experimentId, Integer version, Integer type, Integer layerId) {
32 | this.experimentId = experimentId;
33 | this.version = version;
34 | this.type = type;
35 | this.layerId = layerId;
36 | }
37 |
38 | public Integer getExperimentId() {
39 | return experimentId;
40 | }
41 |
42 | public void setExperimentId(Integer experimentId) {
43 | this.experimentId = experimentId;
44 | }
45 |
46 | public Integer getVersion() {
47 | return version;
48 | }
49 |
50 | public void setVersion(Integer version) {
51 | this.version = version;
52 | }
53 |
54 | public Integer getType() {
55 | return type;
56 | }
57 |
58 | public void setType(Integer type) {
59 | this.type = type;
60 | }
61 |
62 | public Integer getLayerId() {
63 | return layerId;
64 | }
65 |
66 | public void setLayerId(Integer layerId) {
67 | this.layerId = layerId;
68 | }
69 |
70 | public Experiment(ExperBo experBo, Integer version) {
71 | this.experimentId = experBo.getExperimentId();
72 | this.type = experBo.getType();
73 | this.layerId = experBo.getLayerId();
74 | this.version = version;
75 | }
76 |
77 | @Override
78 | public String toString() {
79 | return "Experiment{" +
80 | "experimentId=" + experimentId +
81 | ", version=" + version +
82 | ", type=" + type +
83 | ", layerId=" + layerId +
84 | '}';
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/src/main/java/com/flow/experiment/model/ExperimentMark.java:
--------------------------------------------------------------------------------
1 | package com.flow.experiment.model;
2 |
3 | import java.util.Set;
4 |
5 | /**
6 | * 试验标记对象
7 | *
8 | * @author zwm
9 | * @date 2019-10-17
10 | */
11 | public class ExperimentMark {
12 | /**
13 | * 参与标记集合,代表参与了试验,最少一个
14 | */
15 | private Set participationSet;
16 | /**
17 | * 分配的试验,可为空
18 | */
19 | private Experiment experiment;
20 |
21 | public Set getParticipationSet() {
22 | return participationSet;
23 | }
24 |
25 | public void setParticipationSet(Set participationSet) {
26 | this.participationSet = participationSet;
27 | }
28 |
29 | public Experiment getExperiment() {
30 | return experiment;
31 | }
32 |
33 | public void setExperiment(Experiment experiment) {
34 | this.experiment = experiment;
35 | }
36 |
37 | @Override
38 | public String toString() {
39 | return "ExperimentMark{" +
40 | "participationSet=" + participationSet +
41 | ", experiment=" + experiment +
42 | '}';
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/main/test/DemoExperList.java:
--------------------------------------------------------------------------------
1 | import com.flow.experiment.model.ExperBo;
2 |
3 | import java.util.ArrayList;
4 | import java.util.List;
5 |
6 | /**
7 | * Created by admin on 2019-11-18.
8 | */
9 | public class DemoExperList {
10 | public static List getDemoExper1() {
11 | List demoExperList = new ArrayList<>();
12 | //层级1,类型2,版本数量4个,每个版本为23(针对千分制)的流量,第二个开始的试验
13 | demoExperList.add(new ExperBo(2, 1, 2, 4, 23));
14 | //层级1,类型1,版本数量3个,每个版本为25(针对千分制)的流量,第一个开始的试验
15 | demoExperList.add(new ExperBo(1, 1, 1, 3, 25));
16 | return demoExperList;
17 | }
18 | public static List getDemoExper2() {
19 | List demoExperList = new ArrayList<>();
20 | //层级2,类型2,版本数量2个,每个版本为5(针对千分制)的流量,第三个开始的试验
21 | demoExperList.add(new ExperBo(5, 2, 2, 4, 5));
22 | //层级2,类型1,版本数量2个,每个版本为27(针对千分制)的流量,第二个开始的试验
23 | demoExperList.add(new ExperBo(4, 2, 1, 4, 27));
24 | //层级2,类型1,版本数量5个,每个版本为10(针对千分制)的流量,第一个开始的试验
25 | demoExperList.add(new ExperBo(3, 2, 1, 5, 10));
26 | return demoExperList;
27 | }
28 |
29 |
30 | }
31 |
--------------------------------------------------------------------------------
/src/main/test/TestRunner.java:
--------------------------------------------------------------------------------
1 | import com.flow.experiment.AbstractExperSplitHandler;
2 | import com.flow.experiment.DefaultExperSplitHandlerBuilder;
3 | import com.flow.experiment.DefaultSplitBo;
4 | import com.flow.experiment.model.ExperBo;
5 | import com.flow.experiment.model.Experiment;
6 | import com.flow.experiment.model.ExperimentMark;
7 | import org.junit.Test;
8 |
9 | import java.util.HashSet;
10 | import java.util.List;
11 | import java.util.Set;
12 |
13 | /**
14 | * Created by admin on 2019-11-18.
15 | */
16 | public class TestRunner {
17 |
18 | @Test
19 | public void testEmptySplit() {
20 | //未参与试验
21 | ExperimentMark request = null;
22 | //获取试验列表
23 | List demoExper = DemoExperList.getDemoExper1();
24 | AbstractExperSplitHandler builder = DefaultExperSplitHandlerBuilder.builder(demoExper);
25 | ExperimentMark response = new ExperimentMark();
26 | builder.handler(request, response, new DefaultSplitBo());
27 | System.out.println(response);
28 | }
29 |
30 | @Test
31 | public void testAllThroughNullSplit() {
32 | //参与 3,4,5 试验,未分入对应流量
33 | ExperimentMark request = new ExperimentMark();
34 | Set mark = new HashSet<>();
35 | mark.add(3);
36 | mark.add(4);
37 | mark.add(5);
38 | request.setParticipationSet(mark);
39 | //获取试验列表
40 | List demoExper = DemoExperList.getDemoExper2();
41 | AbstractExperSplitHandler builder = DefaultExperSplitHandlerBuilder.builder(demoExper);
42 | ExperimentMark response = new ExperimentMark();
43 | builder.handler(request, response, new DefaultSplitBo());
44 | System.out.println(response);
45 | }
46 |
47 | @Test
48 | public void testAlreadySplit() {
49 | //参与 3,4 试验,分到4试验
50 | ExperimentMark request = new ExperimentMark();
51 | Set mark = new HashSet<>();
52 | mark.add(3);
53 | mark.add(4);
54 | request.setParticipationSet(mark);
55 | request.setExperiment(new Experiment(4, 4, 1, 2));
56 | //获取试验列表
57 | List demoExper = DemoExperList.getDemoExper2();
58 | AbstractExperSplitHandler builder = DefaultExperSplitHandlerBuilder.builder(demoExper);
59 | ExperimentMark response = new ExperimentMark();
60 | builder.handler(request, response, new DefaultSplitBo());
61 | System.out.println(response);
62 | }
63 | }
64 |
--------------------------------------------------------------------------------