【项目实战】加权轮询算法在线索分配上的应用(含实现代码)

1. 背景

公司里面有个客成的项目,里面涉及到一个需求,就是用户在我们的网站上做了哪些操作之后,可以将线索推送到我们的客成系统中,由系统分配给对应的客成人员进行跟进,那这里就有一个线索如何分配的问题。整个分配的流程包含了用户行为的过滤、用户会员生命周期过滤、客成小组过滤、线索分配等流程,流程比较复杂。这里要提到的就是线索分配流程如何实现的。
对于线索分配,这里经过了两次演化,由最开始的随机分配,到中间的加权随机分配,到最后的加权轮询分配。随机分配的问题是在于线索分配不均,我们在后续加上权重之后先采用的加权随机分配,但是这个分配方式在所有人权重都一样的时候,又降级成了随机分配,还是分配不均。所以在最后,选择了加权轮询分配算法。
这里采用的是类似于Nginx中的平滑轮询算法,这种算法在线索量不大的时候,任然可以保证低权重的对象有被选中的机会。

2.加权轮询

2.1.实现思路

实现思路稍微有一点绕,但是在理解了之后也会感觉比较简单。
我们给需要轮询的对象定义两个属性:weight , currentWeight,以及一个全局属性totalWeight

  • weight:对象分配的权重
  • currentWeight:对象当前应该被分配的权重,这个权重会在每次分配的时候变化
  • totalWeight:所有待分配对象的权重之和

每个对象的初始currentWeight为0,进入分配之后会循环所有的对象,将currentWeight 加上它自己的weight权重,循环完一次之后,对比所有对象找到currentWeight最大的那个,这个对象就是当次分配选中的对象。
到这里,如果不做其他处理的话,每次选中的都是同一个对象,所以我们在对象被选中之后,需要处理currentWeight的值,让其他的对象有机会的被选中,这里处理的方法是将currentWeight 减去 totalWeight,这么处理的话,就让对象权重与总权重之间出现了比例关系


下面通过一个表格来体现一下这个思路,我们假设有3个对象A、B、C,权重大小分别为3:1:1
在这里插入图片描述
可以看到,前5次分配中,A分配了3次,B和C各分配了1次,5次之后currentWeight又回到了原点,以此类推。

2.2.实现代码

首先,需要一个分配对象的实体:

import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class RoundRobinModel {
    
    private Long id;
    private Long weight;
    private Long currentWeight;
    
}

这里的id主要是一个标识字段,在我的项目中用来表示的是需要分配的客成人员对象,可以根据需要修改成其他的标识。


后续需要做算法实现和封装,按照上面的列出的表格,写出代码:

/**
 * 加权轮询选择
 *
 * @param roundRobinModels 加权轮询对象
 * @return 被选中的对象
 */
public RoundRobinModel select(List<RoundRobinModel> roundRobinModels) {
    if (CollectionUtils.isEmpty(roundRobinModels)) {
        return null;
    }
    // 总权重
    Long totalWeight = 0L;
    // 最大权重对象
    RoundRobinModel maxWeightModel = null;
    for (RoundRobinModel roundRobinModel : roundRobinModels) {
        totalWeight += roundRobinModel.getWeight();
        // 当前权重累加
        roundRobinModel.setCurrentWeight(roundRobinModel.getCurrentWeight() + roundRobinModel.getWeight());
        // 选出当前权重最大的对象
        if (maxWeightModel == null || maxWeightModel.getCurrentWeight() < roundRobinModel.getCurrentWeight()) {
            maxWeightModel = roundRobinModel;
        }
    }
    // 最大权重对象权重减去总权重
    maxWeightModel.setCurrentWeight(maxWeightModel.getCurrentWeight() - totalWeight);
    return maxWeightModel;
}

从上面的代码可以看到,使用这个算法时传入的是一个列表 List<RoundRobinModel> roundRobinModels,需要需要将每次操作的结果存储起来,这样才能实现轮询的效果,否则的话,每次循环所有对象的currentWeight都是0,也就是每次都会选中同一个对象。

在生产环境中,为了服务的高可用,往往会部署两个或两个以上的节点。要做到每个服务节点的权重对象同步,我们这里需要将权重对象存储到中间件中,可以是数据库中,也可以是缓存中间件中,我这里选择的是Redis。
其次,在分配阶段为了避免多个节点同时处理导致分配不均的情况,还需要对分配的代码加上分布式锁。
最后,需要考虑存储的权重对象对应的权重,有没有人在做配置的时候修改了,如果修改了,需要重新获取缓存。

综上,一个完整的工具类代码如下:

import com.alibaba.excel.util.StringUtils;
import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;

import javax.annotation.Resource;
import java.util.List;

/**
 * 加权轮询算法,采用Nginx中的平滑轮询算法
 * 平滑:数据量小的时候,低权重的对象有更多被选中的机会,而不是由权重高的先占用,在数据量起来后,总体趋于一个按权重比例分配的规律
 */
@Slf4j
@Component
public class WeightedRoundRobinHelper {

    @Resource
    private StringRedisTemplate stringRedisTemplate;
    @Resource
    private RedissonClient redissonClient;

    /**
     * 获取加权轮询算法返回的权重对象,包含处理数据库和缓存数据,以及调用加权轮询算法
     *
     * @param lockKey            加权轮询分布式锁key
     * @param roundRobinModelKey 加权轮询对象存储key
     * @param roundRobinModels   加权轮询算法参数
     * @return 加权轮询算法返回的权重对象
     */
    public RoundRobinModel getRoundRobinModel(String lockKey, String roundRobinModelKey, List<RoundRobinModel> roundRobinModels) {
        RLock lock = redissonClient.getLock(lockKey);
        lock.lock();
        try {
            // 从缓存中获取轮询对象
            String redisValue = stringRedisTemplate.opsForValue().get(roundRobinModelKey);

            if (StringUtils.isNotBlank(redisValue)) {
                // 从缓存中获取分配人列表
                List<RoundRobinModel> roundRobinModelsByCache = JSON.parseArray(redisValue, RoundRobinModel.class);
                // 对比缓存中的分配人列表和参数中的分配人列表是否一致,包括weight是否一致,一致则使用缓存中的分配人列表(用于人为修改了分配权重的情况)
                if (roundRobinModelsByCache.size() == roundRobinModels.size()) {
                    boolean isSame = true;
                    for (int i = 0; i < roundRobinModels.size(); i++) {
                        if (!roundRobinModels.get(i).getId().equals(roundRobinModelsByCache.get(i).getId())
                                || !roundRobinModels.get(i).getWeight().equals(roundRobinModelsByCache.get(i).getWeight())) {
                            isSame = false;
                            break;
                        }
                    }
                    if (isSame) {
                        roundRobinModels = roundRobinModelsByCache;
                    }
                }
            }

            // 调用加权轮询,获取分配人
            RoundRobinModel roundRobinModel = new WeightedRoundRobinHelper().select(roundRobinModels);
            // 更新缓存
            stringRedisTemplate.opsForValue().set(roundRobinModelKey, JSON.toJSONString(roundRobinModels));
            return roundRobinModel;
        } catch (Exception e) {
            log.error("加权轮询算法获取分配人列表异常", e);
            return null;
        } finally {
            lock.unlock();
        }
    }

    /**
     * 加权轮询选择
     *
     * @param roundRobinModels 加权轮询对象
     * @return 被选中的对象
     */
    public RoundRobinModel select(List<RoundRobinModel> roundRobinModels) {
        if (CollectionUtils.isEmpty(roundRobinModels)) {
            return null;
        }
        // 总权重
        Long totalWeight = 0L;
        // 最大权重对象
        RoundRobinModel maxWeightModel = null;
        for (RoundRobinModel roundRobinModel : roundRobinModels) {
            totalWeight += roundRobinModel.getWeight();
            // 当前权重累加
            roundRobinModel.setCurrentWeight(roundRobinModel.getCurrentWeight() + roundRobinModel.getWeight());
            // 选出当前权重最大的对象
            if (maxWeightModel == null || maxWeightModel.getCurrentWeight() < roundRobinModel.getCurrentWeight()) {
                maxWeightModel = roundRobinModel;
            }
        }
        // 最大权重对象权重减去总权重
        maxWeightModel.setCurrentWeight(maxWeightModel.getCurrentWeight() - totalWeight);
        return maxWeightModel;
    }

}

2.3. 测试

@RestController
@RequestMapping("/roundRobin")
public class RoundRobinController {
    
    @Resource
    private WeightedRoundRobinHelper weightedRoundRobinHelper;

    @GetMapping("/test")
    public void testRoundRobin() {
        RoundRobinModel A = new RoundRobinModel(1L, 3L, 0L);
        RoundRobinModel B = new RoundRobinModel(2L, 1L, 0L);
        RoundRobinModel C = new RoundRobinModel(3L, 1L, 0L);
        List<RoundRobinModel> roundRobinModels = Arrays.asList(A, B, C);
        String lockKey = "myRoundRobinLock";
        String roundRobinModelKey = "myRoundRobinModelKey";

        for (int i = 0; i < 5; i++) {
            RoundRobinModel chosenModel = weightedRoundRobinHelper.getRoundRobinModel(lockKey, roundRobinModelKey, roundRobinModels);
            System.out.println(chosenModel.getId());
        }
    }
}

运行测试代码,得到的结果与预期相符:

1
2
1
3
1

在这里插入图片描述

3.总结

本章主要是在说加权轮询算法在业务上的应用,有类似的业务都可以参考这个代码进行编写。代码算法参考的事Nginx的平滑轮询算法,这也是学习框架原理带来的一些好处。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

挥之以墨

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值