ABTest分流算法设计与实现

需求

有这么一个需求,我们需要建立一个实验,实验有一个实验名称.实验下面有多个分组,每个分组也有个分组名称.当我们创建一个实验的时候,需要同时建立一个或多个分组,并且每个分组都有一个百分比的属性,代表当我们进入一个实验的时候,选择某个分组的可能性有多大,所有的分组的百分比之和为100%.

目的

通过实现实验和分组,我们可以把它应用在分流策略中,比如ABTest.考虑这样一个场景,我们要对比三个推荐算法带来的效益,此时我们会给这三个推荐算法分配一定的比例,然后让每次推荐都按这个比例来执行不同的算法,最终再根据一定的换算来统计算法带来的效益.

实现思路

我们可以将分组的百分比看成是几条线段,假如总共有100米长,每条线段有一定的长度,我们根据标识(比如hashCode)来对100取模,最终这个数字一定会落在某一条线段上,也就是某个分组上.在我的实现中,将以1000作为模.在这里,我将使用Java语言实现一个简单的分流算法.

完整代码地址为:github.com/CodeShowZz/...,所有的接口都进行了初步的测试.

步骤一:定义模型

vbnet 复制代码
public class Lab {

    private String key;

    private String name;

    private List<Group> groupList;

}
arduino 复制代码
public class Group {

    private String key;

    private String name;
    
    /**
    *  百分比
    **/
    private BigDecimal ratio;

    /**
     * 分组的开始位置
     */
    private int start;

    /**
     * 分组的结束位置
     */
    private int end;

}

步骤二:实现分流工具类

这里要实现一个工具类,能够将百分比转换成区间.

ini 复制代码
public static void assignRangeByRatio(Lab lab) {
        List<Group> groupList = lab.getGroupList();
        int current = 0;
        for (Group group : groupList) {
            BigDecimal ratio = group.getRatio();
            int count = ratio.multiply(range).setScale(0, RoundingMode.HALF_UP).intValue();
            int start = current;
            int end = current + count - 1;
            group.setStart(start);
            group.setEnd(end);
            current = end + 1;
        }
    }

步骤三:获取标识取Hash,将其分配到某个分组中.

在我的实现中,如果两次传入的标识key是一样的,那么计算出来的分组位置也是一样的.所以使用这个Hash算法时,可能要根据具体的应用场景来取一个具体的key,比如对于一个用户来说,取值如果要和上次相同,那么可以使用用户id来作为key,如果取值要随机,那么可以取时间戳或者其它属性作为key.

这里我的Hash算法借鉴(可以说是照抄)了HashMap中的Hash算法.

arduino 复制代码
public static final int hashCode(String key, String value) {
        return Math.abs(Objects.hashCode(key) ^ Objects.hashCode(value));
    }

接着,使用上面的模型并且结合Hash算法来实现分组.分区函数的返回结果就是某一个分组.

vbnet 复制代码
public static Group partition(String key, Lab lab) {
        int hashCode = hashCode(key, lab.getName());
        int position = hashCode % range.intValue();
        List<Group> groupList = lab.getGroupList();
        for (Group group : groupList) {
            if (group.getStart() <= position && group.getEnd() >= position) {
                return group;
            }
        }
        return null;
    }

步骤四:测试

通过上面的三个步骤,一个简单的分流算法实现完成.现在我们来假设这样一个场景:据统计,50%的人喜欢数学,30%的人喜欢语文,20%的人喜欢英语,那么现在我们随便找一个人,来猜测它喜欢哪个科目,那么我们就可以使用上面的程序,测试程序如下.

ini 复制代码
public static void main(String[] args) {
        Lab subject = new Lab();
        Group math = new Group();
        math.setRatio(new BigDecimal(0.5));
        math.setKey("math");
        math.setName("数学");

        Group chinese = new Group();
        chinese.setRatio(new BigDecimal(0.3));
        chinese.setKey("chinese");
        chinese.setName("语文");

        Group english = new Group();
        english.setRatio(new BigDecimal(0.2));
        english.setKey("english");
        english.setName("英语");

        List<Group> groupList = Arrays.asList(math, chinese, english);
        subject.setGroupList(groupList);
        subject.setKey("subject");
        subject.setName("学科");

        SplitFlowUtil.splitFlow(subject);

        Group res = partition("the boy maybe like math", subject);
        System.out.println(res);

        res = partition("i am a programmer", subject);
        System.out.println(res);
    }

继续思考

很明显,上面的这个程序其实算是一个通用程序,如果设计的算法更加的快捷,API接口更加易用,它完全可以作为一个公司内部的服务来提供给别人调用.所以现在我们要思考如何将它改进成一个公司内部可以使用的程序.

改进一:建立微服务

提供添加、更新、删除、查询、分流五个接口来对实验进行操作,在这里使用Restful接口来提供这项服务.

改进二:将模型数据存储到Mysql

上面的测试程序只是在本地构造程序,我们可以将模型数据映射成表,然后存储到数据库中,然后通过UI界面来进行CRUD,这一点很容易就可以做到,不再赘述.

改进三:引入Zookeeper

很明显,既然要在公司内部使用,那么要保证每个实验都是隶属于某个项目的,首先要保证实验的唯一性,而这又能看出有很明显的层级结构,所以可以引入Zookeeper来存储这些模型数据,而上面的Mysql则用于冗余模型数据.

开始第一次改进

先列一下实现上述改进所要引入的一些技术,其中改进二不在本次实现考虑范围.另外在下文中可能不会提供所有的代码,完整的代码将在最后给出Github仓库地址.

  • 序列化框架:Kryo,用于序列化模型数据并将其存储到Zookeeper上
  • 微服务框架:Spring Boot,用于实现一个微服务并提供Restful接口
  • 分布式协调框架:Zookeeper及其API,用于实现模型数据的保存,并形成目录结构.

Zookeeper

在工程中实现对zookeeper api的调用,主要考虑的操作有4种

  • 节点增加
  • 节点更新
  • 节点删除
  • 节点数据查询

出于更新的复杂性,调用方可能修改实验名称、分组名称以及分组的属性,所以在真正实现中,将使用节点删除加上节点增加来实现节点更新.在这里将不会讨论Zookeeper API的细节,假设读者已经对此有一定的了解和经验.

Spring Boot

构建一个Spring Boot服务是非常简单的,和上述的Zookeeper类似,我们将对外提供几个api供外部接口调用.

  • 实验创建
  • 实验更新
  • 实验删除
  • 实验查询
  • 根据实验名称进行分流

API如下所示

less 复制代码
/**
     * 创建实验
     * @param lab
     * @return
     */
    @RequestMapping(value = "/create", method = RequestMethod.POST)
    public boolean create(@RequestBody Lab lab) {
        return labService.create(lab);
    }

    /**
     * 根据projectKey和labKey删除实验
     * @param lab
     * @return
     */
    @RequestMapping(value = "/delete", method = RequestMethod.POST)
    public boolean delete(@RequestBody Lab lab) {
        return labService.delete(lab);
    }

    /**
     * 根据projectKey和labKey查询实验下的分组
     * @param projectKey
     * @param labKey
     * @return
     */
    @RequestMapping(value = "/query", method = RequestMethod.GET)
    public List<Group> query(@RequestParam String projectKey, @RequestParam String labKey) {
        return labService.query(projectKey,labKey);
    }

    /**
     * 根据projectKey和labKey还有identify来进行分流 得到某个分组
     * @param projectKey
     * @param labKey
     * @param identify
     * @return
     */
    @RequestMapping(value = "/partition", method = RequestMethod.GET)
    public Group partition(@RequestParam String projectKey, @RequestParam String labKey,@RequestParam String identify) {
        return labService.partition(projectKey,labKey,identify);
    }

    /**
     * 更新实验
     * @param lab
     * @return
     */
    @RequestMapping(value = "update",method = RequestMethod.POST)
    public boolean update(@RequestBody Lab lab) {
        return labService.update(lab);
    }

我在模型数据中又引入了几个参数:

arduino 复制代码
/**
     * 分流需要的参数,由调用方传入,调用方决定分流所使用的标识
     */
    private String identify;

    /**
     * 某个项目的标识,在ZK中是第一级目录,以来区分各个项目的实验
     */
    private String projectKey;

    /**
     * 在进行更新时,需要传入变更前的实验分组key,以便于删除原来的实验
     */
    private String oldKey;

这几个参数的作用已经通过注释来表达,这样可以使得服务更加通用和简单.

测试

实现完上述的两个改进之后,我们就可以打开PostMan或者其它Http请求工具来对我们提供的接口进行测试了,这里对如何测试不进行展开.

总结

在这篇文章中,介绍了一个简单的分流算法的设计以及实现,当然程序还存在很多不足之处,比如异常处理,参数校验,又或者是无法实现多层的分流,这都是值得改进的地方,希望以后有机会再进行改进(程序员经常说的一句话就是下次一定😄).

相关推荐
zopple8 小时前
常见的 Spring 项目目录结构
java·后端·spring
cjy0001119 小时前
springboot的 nacos 配置获取不到导致启动失败及日志不输出问题
java·spring boot·后端
小江的记录本10 小时前
【事务】Spring Framework核心——事务管理:ACID特性、隔离级别、传播行为、@Transactional底层原理、失效场景
java·数据库·分布式·后端·sql·spring·面试
sheji341610 小时前
【开题答辩全过程】以 基于springboot的校园失物招领系统为例,包含答辩的问题和答案
java·spring boot·后端
程序员cxuan11 小时前
人麻了,谁把我 ssh 干没了
人工智能·后端·程序员
wuyikeer12 小时前
Spring Framework 中文官方文档
java·后端·spring
Victor35612 小时前
MongoDB(61)如何避免大文档带来的性能问题?
后端
Victor35612 小时前
MongoDB(62)如何避免锁定问题?
后端
wuyikeer13 小时前
Spring BOOT 启动参数
java·spring boot·后端
子木HAPPY阳VIP13 小时前
Ubuntu 22.04 VMware 设置固定IP配置
人工智能·后端·目标检测·机器学习·目标跟踪