目录
scheduleUpdateIfAbsent(服务定时更新)
学习目的:通过本篇学习,需要知道本地服务实例是怎么存储的,是怎么保证最新的,这里面涉及到的并发控制是怎么实现。
属性说明
public class HostReactor{
private static final long DEFAULT_DELAY = 1000L;
private static final long UPDATE_HOLD_INTERVAL = 5000L;
private final Map<String, ScheduledFuture<?>> futureMap = new HashMap<String,
ScheduledFuture<?>>();
//存储本地的服务信息
private Map<String, ServiceInfo> serviceInfoMap;
private Map<String, Object> updatingMap;
private PushReceiver pushReceiver;
private EventDispatcher eventDispatcher;
private NamingProxy serverProxy;
private FailoverReactor failoverReactor;
private String cacheDir;
private ScheduledExecutorService executor;
.....
}
public class ServiceInfo {
@JSONField(serialize = false)
private String jsonFromServer = EMPTY;
public static final String SPLITER = "@@";
//服务名
private String name;
//组名
private String groupName;
//集群
private String clusters;
private long cacheMillis = 1000L;
//实列列表
@JSONField(name = "hosts")
private List<Instance> hosts = new ArrayList<Instance>();
private long lastRefTime = 0L;
private String checksum = "";
private volatile boolean allIPs = false;
....
}
serviceInfoMap:记录service信息 key 为 serviceName@@clusterName value: ServiceInfo
map类型: ConcurrentHashMap,
updatingMap: 记录正在更新的service信息,记录service信息 key 为 serviceName@@clusterName 类型为ConcurrentHashMap, 用于并发控制同一时刻同样的service只有一个线程执行请求更新操作
pushReceiver:创建udp监听端口并监听Nacos Server 发送过来的数据
eventDispatcher:服务变更事件监听器
cacheDir:服务持久化目录(在服务启动时如果设置了【namingLoadCacheAtStart】属性自动总该目录加载service列表 减轻服务刚启动时的请求的压力)
executor: 1秒钟执行一次的服务更新任务执行器
futureMap: 并发控制服务更新任务的执行, 保证同一时刻同一个服务只有一个更新任务在执行。
futureMap 和 updatingMap 都是控制并发,但是场景不一样控制的操作不一样,前者是控制定时更新服务任务的执行 后者是控制远程服务的请求
初始化
public HostReactor(EventDispatcher eventDispatcher, NamingProxy serverProxy, String
cacheDir) {
this(eventDispatcher, serverProxy, cacheDir, false,
UtilAndComs.DEFAULT_POLLING_THREAD_COUNT);
}
public HostReactor(EventDispatcher eventDispatcher, NamingProxy serverProxy, String
cacheDir, boolean loadCacheAtStart, int pollingThreadCount) {
executor = new ScheduledThreadPoolExecutor(pollingThreadCount, new
ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
Thread thread = new Thread(r);
thread.setDaemon(true);
thread.setName("com.alibaba.nacos.client.naming.updater");
return thread;
}
});
this.eventDispatcher = eventDispatcher;
this.serverProxy = serverProxy;
this.cacheDir = cacheDir;
if (loadCacheAtStart) {
this.serviceInfoMap = new ConcurrentHashMap<String, ServiceInfo>
(DiskCache.read(this.cacheDir));
} else {
this.serviceInfoMap = new ConcurrentHashMap<String, ServiceInfo>(16);
}
this.updatingMap = new ConcurrentHashMap<String, Object>();
this.failoverReactor = new FailoverReactor(this, cacheDir);
this.pushReceiver = new PushReceiver(this);
}
初始化方法里初始化了实例更新线程池:线程池大小默认为:cpu cores / 2
loadCacheAtStart:控制是否服务启动时候从缓存文件加载服务列表到serviceInfoMap中
这里面的serviceInfoMap是线程安全的是因为更新服务实例有多个场景 1、服务第一次查询的时候会更新 2、定时任务会定时更新任务 3、推送任务也会触发更新
那么updatingMap为啥也要并发安全呢?
因为updatingMap的入口方法是服务实例查询,执行这个方法的线程肯定不只一个。
因此对这个对象的更新是需要线程安全的
服务查询
public ServiceInfo getServiceInfo(final String serviceName, final String clusters) {
NAMING_LOGGER.debug("failover-mode: " + failoverReactor.isFailoverSwitch());
String key = ServiceInfo.getKey(serviceName, clusters);
//failoverReactor 后面的章节讲解,这里先略过
if (failoverReactor.isFailoverSwitch()) {
return failoverReactor.getService(key);
}
//本地serverInfoMap中查询
ServiceInfo serviceObj = getServiceInfo0(serviceName, clusters);
if (null == serviceObj) {
serviceObj = new ServiceInfo(serviceName, clusters);
serviceInfoMap.put(serviceObj.getKey(), serviceObj);
updatingMap.put(serviceName, new Object());
updateServiceNow(serviceName, clusters);
updatingMap.remove(serviceName);
} else if (updatingMap.containsKey(serviceName)) {
if (UPDATE_HOLD_INTERVAL > 0) {
// hold a moment waiting for update finish
synchronized (serviceObj) {
try {
serviceObj.wait(UPDATE_HOLD_INTERVAL);
} catch (InterruptedException e) {
NAMING_LOGGER.error("[getServiceInfo] serviceName:" +
serviceName + ", clusters:" + clusters, e);
}
}
}
}
//启动定时服务更新任务
scheduleUpdateIfAbsent(serviceName, clusters);
return serviceInfoMap.get(serviceObj.getKey());
}
private ServiceInfo getServiceInfo0(String serviceName, String clusters) {
String key = ServiceInfo.getKey(serviceName, clusters);
return serviceInfoMap.get(key);
}
如果本地serviceInfoMap中么有找到该服务,则调用方法 updateServiceNow(serviceName, clusters); 拉取服务并保存到本地serviceInfoMap中。并设置服务更新状态;
如果本地已经存在该服务且有线程在执行更新操作则等待,超时时间是5秒。等待updateServiceNow方法执行完毕后唤醒程序继续执行。
启动该服务的定制更新任务 并返回刚刚查找的serviceInfo.
if (oldService != null) {
synchronized (oldService) {
oldService.notifyAll();
}
}
updateServiceNow(服务更新)
public void updateServiceNow(String serviceName, String clusters) {
//保存老的服务信息 用于后面finally块中幻灯等待该锁的线程
ServiceInfo oldService = getServiceInfo0(serviceName, clusters);
try {
String result = serverProxy.queryList(serviceName, clusters, pushReceiver.getUDPPort(), false);
if (StringUtils.isNotEmpty(result)) {
processServiceJSON(result);
}
} catch (Exception e) {
NAMING_LOGGER.error("[NA] failed to update serviceName: " + serviceName, e);
} finally {
if (oldService != null) {
synchronized (oldService) {
oldService.notifyAll();
}
}
}
}
public ServiceInfo processServiceJSON(String json) {
ServiceInfo serviceInfo = JSON.parseObject(json, ServiceInfo.class);
ServiceInfo oldService = serviceInfoMap.get(serviceInfo.getKey());
if (serviceInfo.getHosts() == null || !serviceInfo.validate()) {
//empty or error push, just ignore
return oldService;
}
boolean changed = false;
...
Set<Instance> modHosts = new HashSet<Instance>();
Set<Instance> newHosts = new HashSet<Instance>();
Set<Instance> remvHosts = new HashSet<Instance>();
...
serviceInfo.setJsonFromServer(json);
if (newHosts.size() > 0 || remvHosts.size() > 0 || modHosts.size() > 0) {
eventDispatcher.serviceChanged(serviceInfo);
DiskCache.write(serviceInfo, cacheDir);
}
} else {
changed = true;
NAMING_LOGGER.info("init new ips(" + serviceInfo.ipCount() + ") service: " +
serviceInfo.getKey() + " -> " + JSON
.toJSONString(serviceInfo.getHosts()));
serviceInfoMap.put(serviceInfo.getKey(), serviceInfo);
eventDispatcher.serviceChanged(serviceInfo);
serviceInfo.setJsonFromServer(json);
DiskCache.write(serviceInfo, cacheDir);
}
MetricsMonitor.getServiceInfoMapSizeMonitor().set(serviceInfoMap.size());
...
return serviceInfo;
}
通过NamingProxy请求远程服务列表信息,然后通过Json反序列化成ServiceInfo对象,校验新对象的有效的host信息是否不为空(ServiceInfo.validate() 方法逻辑应该是有问题的,用于返回true), 否则还是返回老的ServiceInfo。
进一步比较新老对象的差异(统计出新增 修改 删除的host 列表)如果新老对象有变化, 则触发服务变更事件eventDispatcher.serviceChanged(serviceInfo); 同时缓存新服务到本地serviceInfoMap 同时更新本地的文件缓存。
public ServiceInfo processServiceJSON(String json) {
ServiceInfo serviceInfo = JSON.parseObject(json, ServiceInfo.class);
ServiceInfo oldService = serviceInfoMap.get(serviceInfo.getKey());
if (serviceInfo.getHosts() == null || !serviceInfo.validate()) {
return oldService;
}
.....
}
class ServiceInfo {
...
//这个有问题方法 用于返回true
public boolean validate() {
if (isAllIPs()) {
return true;
}
List<Instance> validHosts = new ArrayList<Instance>();
for (Instance host : hosts) {
if (!host.isHealthy()) {
continue;
}
for (int i = 0; i < host.getWeight(); i++) {
validHosts.add(host);
}
}
return true;
}
...
}
scheduleUpdateIfAbsent(服务定时更新)
public void scheduleUpdateIfAbsent(String serviceName, String clusters) {
if (futureMap.get(ServiceInfo.getKey(serviceName, clusters)) != null) {
return;
}
synchronized (futureMap) {
if (futureMap.get(ServiceInfo.getKey(serviceName, clusters)) != null) {
return;
}
ScheduledFuture<?> future = addTask(new UpdateTask(serviceName, clusters));
futureMap.put(ServiceInfo.getKey(serviceName, clusters), future);
}
}
public synchronized ScheduledFuture<?> addTask(UpdateTask task) {
return executor.schedule(task, DEFAULT_DELAY, TimeUnit.MILLISECONDS);
}
public class UpdateTask implements Runnable {
long lastRefTime = Long.MAX_VALUE;
private String clusters;
private String serviceName;
public UpdateTask(String serviceName, String clusters) {
this.serviceName = serviceName;
this.clusters = clusters;
}
@Override
public void run() {
try {
ServiceInfo serviceObj =
serviceInfoMap.get(ServiceInfo.getKey(serviceName, clusters));
if (serviceObj == null) {
updateServiceNow(serviceName, clusters);
executor.schedule(this, DEFAULT_DELAY, TimeUnit.MILLISECONDS);
return;
}
if (serviceObj.getLastRefTime() <= lastRefTime) {
updateServiceNow(serviceName, clusters);
serviceObj = serviceInfoMap.get(ServiceInfo.getKey(serviceName,
clusters));
} else {
// if serviceName already updated by push, we should not override it
// since the push data may be different from pull through force push
refreshOnly(serviceName, clusters);
}
executor.schedule(this, serviceObj.getCacheMillis(),
TimeUnit.MILLISECONDS);
lastRefTime = serviceObj.getLastRefTime();
} catch (Throwable e) {
NAMING_LOGGER.warn("[NA] failed to update serviceName: " + serviceName,
e);
}
}
}
通过 futureMap 控制任务的并发,如果该服务更新任务还在执行就不处理。否则添加更新任务 注意这里用了synchronize 关键字做了二次校验,是典型的synchronize 用法,这里不赘述。新任务以1秒中后执行
执行逻辑:
如果当前缓存中没有那么直接从远程服务拉取服务,1秒钟后再次执行更新任务
如果缓存中服务的lastRefTime小于当前的lastRefTime字段,说明缓存中服务已经过期,需要重新拉取更新。
如果缓存中服务的lastRefTime大于当前的lastRefTime字段 则 执行refreshOnly(serviceName, clusters) 方法 但不做本地服务的任何更新操作
refreshOnly这个方法执行的意义目前没有发现,有同学知道的评论区请留言。
什么情况下缓存的数据会跟任务中的lastRefTime时间不一致呢?
比如服务端主动推送服务列表到客户端更新缓存
服务端主动推动逻辑下一章节解析
总结
服务实例管理的逻辑比较复杂, 除了服务第一次查询时候都并发查询控制之外,还涉及到定时更新任务的并发控制、定时任务的逻辑以及获取到最新服务实例后新老数据的比较与事件触发,另外还涉及到锁的等待和唤醒。