标签和版本设计

背景

很多时候,同样的接口,针对不同国家地区,要返回不同的配置。有时,即使是同样的国家地区,随着版本的迭代,也需要返回不同的配置,例如新版本支持了新的 UI 组件或者新的 SDK,而老版本不支持,这时就免不了针对不同版本返回不同配置

每当这个时候,就需要修改服务器程序,在代码里增加新的分支判断代码,为新老版本返回不同的配置。有没有一种通用的解决方案,一劳永逸的解决这样的问题呢?

版本和标签就是为了解决这种基于不同的客户端条件来返回针对性的配置的一种解决方案,其优点在于

  1. 业务无关性:任何业务都可以很简单的套用版本和标签来返回不同的配置

  2. 最大化配置:客户端条件的变化只需要后台配置,而不需要开发人员修改代码

  3. 适用性广:无论是 A/B test 还是灰度发布,都可以看作是下发不同的配置,这样就可以利用版本和标签来实现,不需要额外的设计和开发

业务模型

版本 edition

版本是一个抽象的概念,一个版本就是一套配置。

例如,你想给英语用户和俄语用户返回不同的配置,那就有一个英语版的配置和一个俄语版的配置,如果还要给更多不同语言的用户返回不同的配置,那就增加相应的版本即可

所以,版本有 2 个属性

  • 配置,即最终返回给客户端的业务数据

  • 标签,版本的标签和客户端提交参数进行匹配,以决定客户端是否能得到这个版本的配置

另外,如果有多个版本都符合客户端参数,那该怎么决定哪个版本被命中呢?所以我们还需要版本的第 3 个属性

  • 优先级

标签 tag

标签就是用来决定如何返回配置的条件,例如要根据不同的语言返回不同的配置,那么语言就是一个标签,随着版本的迭代,标签是由运营同学自行维护的

这里实际上有 2 个概念,即标签元素和标签

  • 标签元素:实际上就是一个条件的值,例如国家=US 或者国家=CN,那么 US 或者 CN 就是一个标签元素

  • 标签:标签元素的组合,单个标签元素也可以算是一种组合,例如国家=US且语言=en,那么 US&en 就是一个标签

这里我们统称标签,根据语境来区分是指标签元素还是标签

标签的分类

根据经验,可以预先定义如下的 5 类标签

  • 国家,例如:中国=CN,美国=US

  • 语言,例如:中文=zh,英文=en

  • 客户端版本号,例如:6.5.1,7.0.0,......

  • 染色码,这个取决于染色接口的返回

  • 能力,一般来说表示客户端随着版本迭代导致不同版本之间的差异,例如:有的版本支持下拉刷新,有的版本不支持;有的版本支持 ActionView,有的版本不支持

随着业务的发展,也许会需要更多类的标签

标签的组合

通常情况下,标签元素是组合使用的,例如

  • 在俄罗斯做一次 A/B test,标签就可能是

    • RU&染色码A

    • RU&染色码B

    • ......

  • 在俄语用户中推出支持 ActionView 的新版本,标签可能是

    • ru&ActionViewSupport

    • ru&ActionViewNotSupport

    • ......

标签的组合是不同类的标签可以组合,同一类的标签组合没有意义,例如 中国&英国&美国 这样的标签是没有意义的(能力类标签除外)

标签的属性

标签自身也有属性,即

  • 可选:客户端提交的参数里可以有这个标签,也可以没有

  • 必选:客户端提交的参数里必须有这个标签

如何命中版本

版本的命中,本质是标签的匹配,这是整个设计思路的重点,有必要先定义 2 个概念

客户端标签

客户端标签就是由客户端提交的、用于服务端来匹配版本的参数,例如 国家、语言、染色码、客户端版本号、能力等

版本标签

前面介绍版本时已经提到,版本有 3 个属性,其中一个就是标签。版本标签描述了这个版本可以用于哪些条件

标签匹配逻辑

那么,决定哪个版本被命中返回给客户端的,就是标签的匹配逻辑了:有如下的规则

  • 版本标签中的必选标签,全部都要在客户端标签里存在

  • 版本标签中的可选标签,至少有一个,在客户端标签里存在

举个例子:版本的标签定义为

  • 必选标签

    • 俄语(语言)

    • 7.1.x(客户端版本号)

  • 可选标签

    • 俄国

    • 乌克兰

    • 哈萨克斯坦

这就表示语言是俄语,版本号为 7.1.x,国家是俄国,乌克兰,哈萨克斯坦的客户端都能拿到该版本的配置

版本命中逻辑

如果在多个版本里仅有一个版本的标签和客户端标签匹配,版本命中逻辑就很简单了。但是也要考虑到多个版本都命中或者没有任何版本命中的情况。

基于上述的考虑,版本命中逻辑如下

// 版本
public class Edition {
  // id
  private int id;
  // 优先级
  private int priority;
  // 标签
  private List<Tag> tags;
}

// 标签
public class Tag {
  // id
  private int id;
  // 是否必选
  private boolean required;
  // 值
  private String value;
  // 标签值分解成多个标签元素的值
  private List<String> elements;
}


    Edition getEdition(List<String> clientTags, int targetType, int targetId) {

        // 获得某个业务所有的版本,按优先级从高到低排序
        List<Edition> editions = listAllEditionAndOrderByPriority(targetType, targetId);

        if (editions == null || editions.size() == 0) {
            return null;
        }

        // 对版本进行遍历
        for (Edition edition : editions) {

            // 获取该版本的所有标签
            List<Tag> editionTags = listAllTagsByEditionId(edition.getId());

            // 拆分成出必选和可选标签
            List<Tag> requiredTags = split(editionTags, true);
            List<Tag> optionalTags = split(editionTags, false);

            boolean match = true;

            // 对必选标签进行遍历,所有标签都必须在客户端标签中存在
            for (Tag tag : requiredTags) {
                if (!clientTags.containsAll(tag.getElements())) {
                    match = false;
                    break;
                }
            }

            if (match) {
                // 对可选标签进行遍历,需要有至少一个可选标签匹配
                for (Tag tag : optionalTags) {
                    if (clientTags.containsAll(tag.getElements())) {
                        return edition;
                    }
                }
            }
        }

        // 若没有匹配的版本返回优先级最高的版本
        return editions.get(0);
    }

说明

  1. 拿到某个业务的所有版本,按优先级从高到低排序

  2. 遍历所有版本

    1. 拿到该版本的所有标签

    2. 将标签拆分为可选标签和必选标签

    3. 进行匹配

      1. 客户端标签覆盖所有必选标签?

      2. 有任意一个可选标签是客户端标签的子集?

    4. 若上面 2 个都满足,该版本就被命中了;否则进入下一轮循环

  3. 找不到匹配的版本,返回优先级最高的版本

客户端参数

一个通用的设计方案,应该做到接口协议定义以后基本不需要修改,对于版本和标签这个设计来说,如果客户端提交新的标签需要定义新的参数,那必然导致服务端程序为了解析新的标签参数而修改程序——这肯定不是一个通用的设计——那么要如何定义接口协议,来接收新的标签呢?

使用一个参数接收所有标签

例如,定义一个专用于接收标签的参数为 tag,客户端提交多个标签时,用逗号分隔,例如

// 客户端参数为 [语言=中文, 国家=中国, 版本号=6.8.1, 染色码=A0, 是否支持 yandex SDK=true]
tag=zh,CN,6.8.1,A0,yandex_sdk

这样,随着业务的发展,客户端提交新的标签时,服务端程序可以不用做任何修改

部分标签特殊处理

由于 语言,国家,版本号 等参数基本是所有接口都必传的,如果再通过 tag 参数进行提交就重复了,因此服务端在解析客户端参数时,直接把这几个参数也视同标签进行处理,而客户端则不需要在 tag 参数里重复提交这几个参数

对客户端版本号和区域的特殊处理

特殊处理是在将客户端输入参数转换成标签时的一些处理

客户端版本号

客户端版本号格式一般是 v1.v2.v3,例如 6.3.0;为了方便配置,可以用通配符 x 来表示 v3,即 6.3.0 可以匹配 6.3.x,6.3.2 也可以匹配 6.3.x

也就是说,客户端版本号将被转换为 2 个标签:原始的版本号,用通配符替换后的版本号

区域

客户端提交的参数里,有时候会有区域信息,其格式一般是 语言_国家,例如 zh_CN

生成客户端标签时,会把区域拆分成 语言 和 国家,上例中的 zh_CN 会被拆分成 zhCN 2 个标签,这样加上 zh_CN 实际上得到了 3 个标签

例子

客户端提交参数为

ver=6.2.20&language=zh&color=A10&locale=zh_CN&tag=tag1,tag2,tag3

对该参数进行解析得到的客户端标签为

List<String> clientTags = Arrays.asList(
  "6.2.20",
  "6.2.x",
  "zh",
  "A10",
  "zh_CN",
  "CN",
  "tag1",
  "tag2",
  "tag3"
);

注意其中 3,7 行,并非客户端提交的原始标签,而是服务器解析后增加的标签

后续优化

默认版本

之前的逻辑是如果没有一个匹配的版本,会返回优先级最高的版本,可以优化一下:由运营同学定义一个默认版本,当没有匹配的版本时返回这个默认版本

Last updated