跳到主要内容

开关规则计算算法

概述

本文档介绍用于开关规则计算的算法。

规则计算

所有服务端SDK 都需提供实现开关的规则计算。

SDK 的规则计算接口设计如下:

private <T> T genericEvaluate(String toggleKey, FPUser user, T defaultValue)

规则计算分为如下步骤:

前置检查

1、如果当前开关 Key 不存在,则返回函数中预设的默认值。

2、如果开关处于未生效状态,则返回开关配置中预设的未生效分组值。

规则检查

在 FeatureProbe 开关规则中,匹配规则是通过多个条件子句来匹配任意用户的。为了匹配规则,必须满足所有条件子句。

SDK 会遍历规则,直到找到与给定用户匹配的第一条规则。如果规则配置的分组索引,则直接返回对应的分组值、规则的索引、开关版本和命中原因。如果规则配置的是按比例推出,则按照推出逻辑计算命中的分组,并返回对应的分组值、规则的索引、开关版本和命中原因。

如果所有规则都未命中,则匹配默认规则。

条件运算符

运算符参数类型命中条件
is one ofstring至少与其中一个值与user属性值完全匹配
ends withstring至少其中一个值是user属性值的尾子句
starts withstring至少是其中一个值是user属性值的首子句
containsstring至少是其中一个值是user属性值的子句
matches regexstring至少与其中一个正则表达式与user属性值匹配
is not any ofstring任何一个值不与user属性值完全匹配
does not end withstring所有值都不是user属性值尾子句
does not start withstring所有值都不是user属性值首子句
does not containstring所有值都不是user属性值子句
does not match regexstring所有正则表达式都与user属性值不匹配
is insegmentuser在其中任意一个人群组中
is not insegmentuser不在所有人群组中
afterdatetimeuser属性值在这个日期之后或等于该日期
beforedatetimeuser属性值在这个日期之前
=number/semver属性值至少等于其中一个值
!=number/semver属性值与所有值不相等
>number/semver属性值至少大其中一个值
>=number/semver属性值至少大于等于其中一个值
<number/semver属性值至少小于其中一个值
<=number/semver属性值至少小于等于其中一个值

当规则条件的值类型为 “segment” 时,值列表是该 segment 的唯一键,通过该键可以获取到相应的 Segment 规则。与开关的规则匹配计算类似,只要当前的 FPUser 命中了该人群组中的任意一个规则,就认为该 FPUser 属于该人群组。如果该键不存在,则认为该 FPUser 未命中该人群组。

百分比推出

开关规则可以配置分组按百分比推出。

百分比规则格式如下:

[
[
[
0,
3333
]
],
[
[
3333,
6666
]
],
[
[
6666,
10000
]
]
]

规则是由 0 到 10000 间的整数组成的 3 维数组。第一维数组的下标与分组的下标对应。第二维数组表示每个分组的权重。

为了进行分组匹配,我们只需要通过 FPUser 的 key 属性使用散列算法计算出一个 0 到 10000 间的整数值,然后查找该整数值在第二维数组中的位置,进而确定该 FPUser 命中的分组下标,即对应第一维数组的下标。

散列算法

1、将 FPUser 的 Key 值与开关的盐值连接,如果开关的盐值为 null,则使用开关的 Key 代替。

2、使用 SHA1 散列算法对上一步得到的字符串进行散列,生成一个散列字符串。

3、从散列字符串中取出后 4 位,并将其转换为十进制整数。

4、对上一步得到的整数进行取模操作,得到一个介于 0 到 10000 之间的整数,作为分组匹配的依据。

参考代码(java):

private int hash(String hashKey, String hashSalt, int bucketSize) {
String value = hashKey + hashSalt;
byte[] hashValue;
MessageDigest messageDigest;
try {
messageDigest = MessageDigest.getInstance("SHA-1");
messageDigest.update(value.getBytes(StandardCharsets.UTF_8));
hashValue = messageDigest.digest();
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("couldn't clone MessageDigest object", e);
}
byte[] bytes = Arrays.copyOfRange(hashValue, hashValue.length - 4, hashValue.length);
return new BigInteger(1, bytes).mod(BigInteger.valueOf(bucketSize)).intValue();
}