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)