图解+源码讲解 Nacos 服务端处理配置获取请求
在人生中第一要紧的是发现自己。为了这个目的,各位时常需要孤独和深思 —— 南森
Nacos 源码分析系列相关文章
- 从零开始看 Nacos 源码环境搭建
 - 图解+源码讲解 Nacos 客户端发起注册流程
 - 图解+源码讲解 Nacos 服务端处理注册请求逻辑
 - 图解+源码讲解 Nacos 客户端下线流程
 - 图解+源码讲解 Nacos 服务端处理下线请求
 - 图解+源码讲解 Nacos 客户端发起心跳请求
 - 图解+源码讲解 Nacos 服务端处理心跳请求
 - 图解+源码讲解 Nacos 服务端处理配置获取请求
 
从客户端的请求地址开始分析
    客户端获取配置的请求地址是:/v1/cs/configs, 这样一来我们就可以去服务端找响应的请求地址,看看在哪个项目模块中,应该是在nacos-config 这个项目模块中,从下图中可以看出就是这个模块中的 getConfig方法
响应请求方法 getConfig
(action = ActionTypes.READ, parser = ConfigResourceParser.class)
public void getConfig(HttpServletRequest request, HttpServletResponse response,
        ("dataId") String dataId, ("group") String group,
        (value = "tenant", required = false,
                      defaultValue = StringUtils.EMPTY) String tenant,
        (value = "tag", required = false) String tag)
    throws IOException, ServletException, NacosException {
    // 校验参数
    // check tenant
    ParamUtils.checkTenant(tenant);
    tenant = NamespaceUtil.processNamespaceParameter(tenant);
    // check params
    ParamUtils.checkParam(dataId, group, "datumId", "content");
    ParamUtils.checkParam(tag);
    // 获取客户端请求IP地址
    final String clientIp = RequestUtil.getRemoteIp(request);
    String isNotify = request.getHeader("notify");
    // 获取请求方法
    inner.doGetConfig(request, response, dataId, group, tenant, tag, isNotify,clientIp);
}获取配置方法 doGetConfig
获取读锁 tryConfigReadLock
尝试添加读锁
private static int tryConfigReadLock(String groupKey) {
    // 默认是 -1 ,
    int lockResult = -1;
    // 尝试获取锁的次数,最大次数是10,TRY_GET_LOCK_TIMES = 9
    for (int i = TRY_GET_LOCK_TIMES; i >= 0; --i) {
        lockResult = ConfigCacheService.tryReadLock(groupKey);
        // 数据不存在
        if (0 == lockResult) {
            break;
        }
        // 读写加成功
        if (lockResult > 0) {
            break;
        }
        // 睡一毫秒进行加锁
        if (i > 0) {
            try {
                Thread.sleep(1);
            } catch (Exception e) {
                LogUtil.PULL_CHECK_LOG.error("An Exception" +
                                             "occurred while thread sleep", e);
            }
        }
    }
    return lockResult;
}尝试加锁
public static int tryReadLock(String groupKey) {
    // 从缓存中获取值
    CacheItem groupItem = CACHE.get(groupKey);
    // 如果不为空的话那么尝试加读锁,如果为空那么就返回 0 意味着配置不存在
    int result = (null == groupItem) ? 0 : (groupItem.rwLock.tryReadLock() ? 1 : -1);
    if (result < 0) {
        DEFAULT_LOG.warn("[read-lock] failed, {}, {}", result, groupKey);
    }
    return result;
}Nacos 实现简单锁
这里面 Nacos 自己实现了一个锁,通过 status 状态进行判断的,如果是负值那么加的是写锁,如果是正值那么就是加的读锁,如果等于 0 那么是没有被加锁
public class SimpleReadWriteLock {
   // 尝试加读锁,读锁是可以进行并行加锁的,就是读写锁互斥、写写互斥,但是读读不互斥
    public synchronized boolean tryReadLock() {
        // 是否添加了写锁
        if (isWriteLocked()) {
            return false;
        } else {
            status++;
            return true;
        }
    }
    // 释放读锁
    public synchronized void releaseReadLock() {
        status--;
    }
    // 写锁的时候 status = -1 尝试加写锁
    public synchronized boolean tryWriteLock() {
        if (!isFree()) {
            return false;
        } else {
            status = -1;
            return true;
        }
    }
   // 释放写锁
    public synchronized void releaseWriteLock() {
        status = 0;
    }
   // 是否是写锁
    private boolean isWriteLocked() {
        return status < 0;
    }
  // 是否是没有锁
    private boolean isFree() {
        return status == 0;
    }
  // 0 代表没有锁 -1 代表写锁
    private int status = 0;
}如果获取读锁成功 lockResult > 0
这里面只看获取锁成功的逻辑
if (lockResult > 0) {
        FileInputStream fis = null;
        try {
            String md5 = Constants.NULL;
            long lastModified = 0L;
            // 根据 key 获取缓存中的配置信息
            CacheItem cacheItem = ConfigCacheService.getContentCache(groupKey);
            if (cacheItem.isBeta() && cacheItem.getIps4Beta().contains(clientIp)) {
                isBeta = true;
            }
            ......
            if (StringUtils.isBlank(tag)) {
                    ......
               md5 = cacheItem.getMd5();
               lastModified = cacheItem.getLastModifiedTs();
                if (PropertyUtil.isDirectRead()) {
                    // 根据 dataId ,group, tenant 从 Derby 嵌入式数据库中查询配置信息
                    configInfoBase = persistService.
                        findConfigInfo(dataId, group, tenant); // 查找配置信息
                } else {
                    file = DiskUtil.targetFile(dataId, group, tenant);
                }
                if (configInfoBase == null && fileNotExist(file)) {
                    ......
                    return get404Result(response);
                }
                    isSli = true;
                }
            ......
            }
            ......
            if (PropertyUtil.isDirectRead()) {
                out = response.getWriter();
                out.print(configInfoBase.getContent());
                out.flush();
                out.close();
            }
            ......
        } finally {
            // 释放配置读锁,就是 status--
            releaseConfigReadLock(groupKey);
            IoUtils.closeQuietly(fis);
        }从 Derby 嵌入式数据库中查询配置信息
    ConfigInfo{id=491540548146958336, dataId='service-consumer.properties', group='DEFAULT_GROUP', tenant='', appName='', content='useLocalCache=true', md5='25a822367cd73c79acc56da3c73fa07e'}
释放读锁 releaseConfigReadLock
无论在什么情况下,在获取一把锁并处理完业务逻辑的情况下,一定要释放这把锁,否则会造成后续请求阻塞
private static void releaseConfigReadLock(String groupKey) {
    ConfigCacheService.releaseReadLock(groupKey);
}
public static void releaseReadLock(String groupKey) {
    // 获取缓存值
    CacheItem item = CACHE.get(groupKey);
    if (null != item) {
        // 释放锁
        item.rwLock.releaseReadLock();
    }
}
// 释放锁
public synchronized void releaseReadLock() {
    status--;
}返回成功 200
如果获取读锁返回 lockResult = 0
返回 404 找不到配置信息
// lockResult = 0 就是没有数据,获取的配置不存在
if (lockResult == 0) {
    .....
  return get404Result(response);
}如果获取读锁失败 lockResult < 0
加锁失败,返回状态码 409 ,并提示请求的文件被修改中,请稍后进行获取查询
// 加读锁失败了
response.setStatus(HttpServletResponse.SC_CONFLICT);
return HttpServletResponse.SC_CONFLICT + "";小结
- 加读锁操作
 - 如果加锁成功那么从 Derby 数据库中查询配置信息
 - 返回查询结果信息
 
其他系列源码分析
feign 源码分析系列相关文章
- 图解+源码讲解 Feign 如何将客户端注入到容器中
 - 图解+源码讲解动态代理获取 FeignClient 代理对象
 - 图解+源码讲解代理对象 ReflectiveFeign 分析
 - 图解+源码讲解 Feign 如何选取指定服务
 - 图解+源码讲解 Feign 请求的流程
 
ribbon 源码分析系列相关文章
- Ribbon 原理初探
 - 图解+源码讲解 Ribbon 如何获取注册中心的实例
 - 图解+源码讲解 Ribbon 服务列表更新
 - 图解+源码讲解 Ribbon 服务选择原理
 - 图解+源码讲解 Ribbon 如何发起网络请求
 
eureka 源码分析系列相关文章
- eureka-server 项目结构分析
 - 图解+源码讲解 Eureka Server 启动流程分析
 - 图解+源码讲解 Eureka Client 启动流程分析
 - 图解+源码讲解 Eureka Server 注册表缓存逻辑
 - 图解+源码讲解 Eureka Client 拉取注册表流程
 - 图解+源码讲解 Eureka Client 服务注册流程
 - 图解+源码讲解 Eureka Client 心跳机制流程
 - 图解+源码讲解 Eureka Client 下线流程分析
 - 图解+源码讲解 Eureka Server 服务剔除逻辑
 - 图解+源码讲解 Eureka Server 集群注册表同步机制
 










