├── 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 | ![flow](https://github.com/zhouwenmo/split/blob/master/split-flow.png) 10 | 11 | # 默认采用随机数分流算法,图示按照千分制级进行分流处理 12 | ![algorithm-mew](https://github.com/zhouwenmo/split/blob/master/split-algorithm.png) 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 | --------------------------------------------------------------------------------