Netty开发小工具,响应统计器-阻塞响应模型

81 阅读3分钟

Netty开发小工具,响应统计器-阻塞响应模型

起因

在开发一个基于Raft共识算法的分布式系统过程中发现了两个可以复用的过程,于是想把这个功能做成一个静态工具类来进行代码复用。

功能:

这是一个响应统计工具,主线程发起请求后阻塞并统计响应的数量,当响应满足一定数量后主程序继续执行。在我的项目中共有两处相似的功能,一个是集群选举时候选节点需要统计其他节点的投票数量,另一个是Leader节点在更新配置时需要等待其他Follower节点先应用后才能提交该配置

代码

/**
 * 结果数量统计,用于响应结果数量统计
 * 1. Raft选举投票数量统计
 * 2. Raft日志同步返回Ack数量统计
 */
@Slf4j
public class ResultStatisticsUtil {
    // 统计结果池,key为统计id,value为统计结果
    private static final ConcurrentHashMap<Long, AtomicInteger> statisticsPool = new ConcurrentHashMap<>();
​
    // 锁池,key为统计id,value为锁
    private static final ConcurrentHashMap<Long, ReentrantLock> lockPool = new ConcurrentHashMap<>();
​
    // 条件池,用于阻塞以及唤醒
    private static final ConcurrentHashMap<ReentrantLock,Condition> conditionPool = new ConcurrentHashMap<>();
​
    public static void createStatistics(Long id) {
        // 初始化所需资源
        statisticsPool.put(id, new AtomicInteger(0));
        ReentrantLock reentrantLock = new ReentrantLock();
        lockPool.put(id, reentrantLock);
        conditionPool.put(reentrantLock,reentrantLock.newCondition());
    }
​
    public static Boolean syncWait(Long id,Long timeout, Integer count) {
        ReentrantLock reentrantLock = lockPool.get(id);
        if(reentrantLock == null){
            log.error("结果数量统计工具当前id:{} 未被创建,请确保该id已执行createStatistics方法",id);
            return false;
        }
        Condition condition = conditionPool.get(reentrantLock);
        long start = System.currentTimeMillis();
        while (statisticsPool.get(id).get() < count) {
            try {
                reentrantLock.lock();
                log.info("同步等待投票中,当前已经投票数量: {}, 需要投票数量: {}", statisticsPool.get(id).get(), count);
                long costTime = System.currentTimeMillis() - start;
                // 这里的超时与下面的await方法超时逻辑不同。这里是整个同步等待的超时
                if (costTime > timeout) {
                    log.info("同步等待投票结束,耗时超时:" + costTime);
                    removeStatistics(id);
                    return false;
                }
                // 这里的是判断单次等待的超时
                if (!condition.await(timeout, TimeUnit.MILLISECONDS)) {
                    log.info("同步等待投票结束,耗时超时:" + (System.currentTimeMillis() - start));
                    removeStatistics(id);
                    return false;
                }
​
            } catch (InterruptedException e) {
                log.error("同步等待被中断,错误信息: {}", e.getMessage());
            } finally {
                reentrantLock.unlock();
            }
        }
        log.info("同步等待投票结束,耗时:{},获得投票:{}",(System.currentTimeMillis() - start),statisticsPool.get(id).get());
        removeStatistics(id);
        return true;
    }
​
    public static void incrStatistics(Long id) {
        // 安全检查
        if(!statisticsPool.containsKey(id)){
            return;
        }
        // 统计池中增加一次结果数
        statisticsPool.get(id).incrementAndGet();
        ReentrantLock reentrantLock = lockPool.get(id);
        Condition condition = conditionPool.get(reentrantLock);
        reentrantLock.lock();
        try {
            // 唤醒等待线程
            condition.signalAll();
        } finally {
            reentrantLock.unlock();
        }
    }
​
    private static void removeStatistics(Long id) {
        ReentrantLock reentrantLock = lockPool.get(id);
        conditionPool.remove(reentrantLock);
        statisticsPool.remove(id);
        lockPool.remove(id);
    }
​
​
}

工具使用

 public void preCommitRemoveService(Channel channel) {
        String ip = NetUtil.getIpByChannel(channel);
        // 获取离线机器上的所有服务列表
        List<ServiceMeta> serviceMetasByChannel = providerServiceCache.getServiceMetasByIp(ip);
        // 构造一个删除服务的日志,并带上termId,logIndex,便于其他节点进行安全检查
        ClusterRaftLog clusterRaftLog = new ClusterRaftLog(serviceMetasByChannel,RequestType.DELETE_SERVICES_CLUSTER, System.currentTimeMillis(),nodeContent.getTermId(),nodeContent.getLogIndex().incrementAndGet());
        RequestHeader requestHeader = new RequestHeader(RequestType.DELETE_SERVICES_CLUSTER,SnowFlakeUtil.getNextId());
        RpcRequestHolder rpcRequestHolder = new RpcRequestHolder(requestHeader,clusterRaftLog);
        // 获取其他节点的地址然后发送数据
        List<String> clusterFollowerAddress = rpcRegistry.getClusterAddress().stream().filter(e -> !e.equals(nodeContent.getAddress())).collect(Collectors.toList());
        ConnectUtil.sendDataToNodes(clusterFollowerAddress,rpcRequestHolder, clusterBootStrap,clusterConnectCache);
        Long waitId = requestHeader.getRequestId();
        // 创建所需要的资源
        ResultStatisticsUtil.createStatistics(waitId);
        // 如果超过半数的节点响应则应用
        if (ResultStatisticsUtil.syncWait(waitId,1000L,nodeContent.getClusterNodeAmount().get()/2)) {
            registryService.handleInActive(channel);
            log.info("当前配置成功,删除了服务");
        }else{
            log.warn("未超过半数节点应用配置,本次变更无效");
        }
    }
    
    // 下面是接收的代码
     public void handleAck(Channel channel, Long requestId) {
        // 结果收集器增加一次结果
        ResultStatisticsUtil.incrStatistics(requestId);
    }

使用讲解:

在需要阻塞的方法中调用 ResultStatisticsUtil.createStatistics(id)

来进行创建所需要的资源,比如锁同时需要有一个Id 用于资源定位

然后就是调用 ResultStatisticsUtil.syncWait(waitId,1000L,nodeContent.getClusterNodeAmount().get()/2)

配置之前存入的id,超时时间,以及期待的响应数量,这里是当集群一半的节点响应了那么就放行,否则阻塞,超时后拦截。

使用时需要让请求,响应携带的id值一样,这样才能定位到同一个锁和计数器。

总的来说 只需要三行代码即可完成响应收集功能

1:ResultStatisticsUtil.createStatistics(id)
​
2:ResultStatisticsUtil.syncWait(id,outTime,expNum)
​
3:ResultStatisticsUtil.incrStatistics(id)