nacos 是阿里开源的配置中心/注册中心组件
在spring-boot项目中, 可以使用下面的方法启动nacos config
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | nacos: config: # 使用namespace=public时, 这里不能显示指定. 否则会拿不到数据 # namespace: public username: admin password: 12345678 server-addr: 10.0.18.92:8848 group: ${spring.profiles.active} # common 放通用配置, 可以被${spring.application.name}覆盖 data-ids: ${spring.application.name},common type: YAML auto-refresh: true remote-first: true enable-remote-sync-config: true config-long-poll-timeout: 45000 max-retry: 10 config-retry-time: 3500 bootstrap: # 开启log enable: true log-enable: true |
最坑的就是这个
1 | namespace: public |
public是系统默认的namespace
但是不能在这里制定, 否则会造成nacos客户端向nacos server拿不到config的情况.
这是nacos的一个bug(可以说是命名不规范吧), 具体可以参考其源码
com.alibaba.nacos.client.config.impl.ClientWorker#getServerConfig
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | public String[] getServerConfig(String dataId, String group, String tenant, long readTimeout) throws NacosException { String[] ct = new String[2]; if (StringUtils.isBlank(group)) { group = Constants.DEFAULT_GROUP; } HttpRestResult<String> result = null; try { Map<String, String> params = new HashMap<String, String>(3); if (StringUtils.isBlank(tenant)) { params.put("dataId", dataId); params.put("group", group); } else { params.put("dataId", dataId); params.put("group", group); params.put("tenant", tenant); } result = agent.httpGet(Constants.CONFIG_CONTROLLER_PATH, null, params, agent.getEncode(), readTimeout); } catch (Exception ex) { String message = String .format("[%s] [sub-server] get server config exception, dataId=%s, group=%s, tenant=%s", agent.getName(), dataId, group, tenant); LOGGER.error(message, ex); throw new NacosException(NacosException.SERVER_ERROR, ex); } |
代码中 tenant 就是namespace, 当不指定时, params 里面没有 tenant 项, 只有在指定了namespace时才会有这个请求参数
下面看server端实现
com.alibaba.nacos.config.server.service.repository.extrnal.ExternalStoragePersistServiceImpl#findConfigInfo(java.lang.String, java.lang.String, java.lang.String)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | @Override public ConfigInfo findConfigInfo(final String dataId, final String group, final String tenant) { final String tenantTmp = StringUtils.isBlank(tenant) ? StringUtils.EMPTY : tenant; try { return this.jt.queryForObject( "SELECT ID,data_id,group_id,tenant_id,app_name,content,md5,type FROM config_info WHERE data_id=? AND group_id=? AND tenant_id=?", new Object[] {dataId, group, tenantTmp}, CONFIG_INFO_ROW_MAPPER); } catch (EmptyResultDataAccessException e) { // Indicates that the data does not exist, returns null. return null; } catch (CannotGetJdbcConnectionException e) { LogUtil.FATAL_LOG.error("[db-error] " + e.toString(), e); throw e; } } |
到这里就成了 tenant_id
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | /** * Execute clone config operation. * * @param request http servlet request . * @param srcUser src user string value. * @param namespace namespace string value. * @param configBeansList config beans list. * @param policy config policy model. * @return RestResult for map. * @throws NacosException NacosException. */ @PostMapping(params = "clone=true") @Secured(action = ActionTypes.WRITE, parser = ConfigResourceParser.class) public RestResult<Map<String, Object>> cloneConfig(HttpServletRequest request, @RequestParam(value = "src_user", required = false) String srcUser, @RequestParam(value = "tenant", required = true) String namespace, @RequestBody(required = true) List<SameNamespaceCloneConfigBean> configBeansList, @RequestParam(value = "policy", defaultValue = "ABORT") SameConfigPolicy policy) throws NacosException { Map<String, Object> failedData = new HashMap<>(4); if (CollectionUtils.isEmpty(configBeansList)) { failedData.put("succCount", 0); return ResultBuilder.buildResult(ResultCodeEnum.NO_SELECTED_CONFIG, failedData); } configBeansList.removeAll(Collections.singleton(null)); if (NAMESPACE_PUBLIC_KEY.equalsIgnoreCase(namespace)) { namespace = ""; } else if (persistService.tenantInfoCountByTenantId(namespace) <= 0) { failedData.put("succCount", 0); return ResultBuilder.buildResult(ResultCodeEnum.NAMESPACE_NOT_EXIST, failedData); } List<Long> idList = new ArrayList<>(configBeansList.size()); Map<Long, SameNamespaceCloneConfigBean> configBeansMap = configBeansList.stream() .collect(Collectors.toMap(SameNamespaceCloneConfigBean::getCfgId, cfg -> { idList.add(cfg.getCfgId()); return cfg; }, (k1, k2) -> k1)); |
诡异的代码出现了
1 2 3 4 5 6 | if (NAMESPACE_PUBLIC_KEY.equalsIgnoreCase(namespace)) { namespace = ""; } else if (persistService.tenantInfoCountByTenantId(namespace) <= 0) { failedData.put("succCount", 0); return ResultBuilder.buildResult(ResultCodeEnum.NAMESPACE_NOT_EXIST, failedData); } |
添加配置时, 如果namespace为public时, 最终存入数据的 tenant_id 为 StringUtils.EMPTY
获取的时候是没有这个潜规则的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | @Override public ConfigInfo findConfigInfo(final String dataId, final String group, final String tenant) { final String tenantTmp = StringUtils.isBlank(tenant) ? StringUtils.EMPTY : tenant; try { return this.jt.queryForObject( "SELECT ID,data_id,group_id,tenant_id,app_name,content,md5,type FROM config_info WHERE data_id=? AND group_id=? AND tenant_id=?", new Object[] {dataId, group, tenantTmp}, CONFIG_INFO_ROW_MAPPER); } catch (EmptyResultDataAccessException e) { // Indicates that the data does not exist, returns null. return null; } catch (CannotGetJdbcConnectionException e) { LogUtil.FATAL_LOG.error("[db-error] " + e.toString(), e); throw e; } } |
所以, 当我们在客户端指定 namespace = public 时, 就死活拿不到数据了.
其中 namespace -> tenant -> tenant_id -> namespaceId 其实一直扮演着一个角色(即: 命名空间)
在这里提两点意见
1. 既然自己制定了规则, 那就要把规则落地. 不然就必定会有人为此躺坑.
2. 所谓名不正言不顺, 同一个东西在项目中尽量不要起多个名字.