一.引言

大量 id 场景下经常需要通过 id 进行 AB Test,最常见的就是使用尾号 hash 进行分组,但是由于 id 生成规则以及其他因素,按照尾号分组往往会造成 id 不匀,从而导致 AB Test 效果受影响,所以下文采用 md5 加盐 Hash 的方式,得到更均匀的分组与 AB Test 效果。

二.实现原理

1. id 加盐

id 即为用户 uid 或商品 pid,加盐中盐代表盐值,可以指定为任一质数,id 加盐可以理解:

saltNum: Int + id: String => String

2.MD5 编码

通过 MD5 编码将上述加盐的 id 进行编码处理,获取加密后的字节形式,md5 包采用 java 自带的:

import java.security.MessageDigest

val md5 = MessageDigest.getInstance("MD5")
val encoded = md5.digest(saltNum + id)

3.字节转16进制

将加密后的每个字节转16进制,转换16进制采用 org.apache.commons 自带工具包:

import org.apache.commons.codec.binary.Hex

val encodeStr = Hex.encodeHexString(encoded)

也可以直接使用 String 自带的 format 方法实现转换16进制:

val encodeStr = encoded.map("%02x".format(_)).mkString("")

4.16进制转10进制

将16进制数字截取 TopN,然后将16进制转换为10进制

val num = java.lang.Long.parseLong(encodeStr.slice(0, N), 16).toString

直接取 TopN 并通过 parseLong 得到新的 10 进制数字。

5.Hash 获取新分组

通过新的十进制数字取尾号 hash,获取新的分组,上面得到 10 进制数字 num,可以再使用尾号划分,例如对倒数两位取 mod,即可得到 100 个分组,对倒数三位取 mod,即可得到 1000 个分组,依次类推。

6.完整实现

A.MD5 Hash

def md5Encode(id: String, saltNum: Int, N: Int): String = {
    val input = saltNum + id // 加盐
    val md5 = MessageDigest.getInstance("md5")
    val encoded = md5.digest(input.getBytes) // md5 编码
    val encodeStr = Hex.encodeHexString(encoded) // 转16进制
    val num = java.lang.Long.parseLong(encodeStr.slice(0, N), 16).toString // 转10进制
    val group = num.slice(num.length - 2, num.length).toInt % 100 // hash
    group.toString
  }

B.Common Hash

def commonHash(num: String): String = {
    val group = num.slice(num.length - 2, num.length).toInt % 100 // hash
    group.toString
  }

三.效果评估

对 uid、pid 重新分组主要是为了提高 AB Test 的置信度,而且涉及到工程实现即每个 id 都需要获取对应的 group,所以下面从:

-> id 分组均匀程度

-> id 分组AB效果程度

-> 分组速度

三个方面进行评估。

1.分组均匀程度

由于 uid、pid 为系统生成,一定程度上不能做到完美的 hash 均分,所以需要重 hash 解决,下面分别使用 MD5 Hash 与 Common Hash 做 id 数的分析,指标: [分组 id 数 - 分组 id 平均数]

Java中通过盐解密的方法叫什么 java 加盐_Java中通过盐解密的方法叫什么

绿线为 MD5,红线为 CommonHash,可以看到 MD5 得到的 100 个分组 id 数相对 CommonHash 分组均匀很多,前者 Std 为 1100+,后者 Std 达到 4000+。

2.分组效果均匀程度

分组均分后,还要验证下效果是否一致,如果 id 数相同但是同组的 id 表现差异很大,对 AB Test 也会造成很大影响,这里采用 Pid 的销售额作图,指标: [pid 销售额 - pid 销售额均值]

Java中通过盐解密的方法叫什么 java 加盐_Java中通过盐解密的方法叫什么_02

绿线为 MD5,红线为 CommonHash,可以看到 pid 在 MD5 hash 后整体表现均匀,而原始的 CommonHash 则存在个别组出现极端坏数据的情况,影响 AB Test。

3.分组速度

构造 10000 个 id 模拟 Pid,打印执行时间比较:

val random = scala.util.Random
    val testId = (0 to 10000).map(x => random.nextLong()).toArray
    val st = System.currentTimeMillis()
    testId.foreach(num => {
//      md5Encode(num.toString, saltNum, N)
      commonHash(num.toString)
    })
    println(s"cost: ${System.currentTimeMillis() - st}")

MD5 耗时 220ms / 10000,CommonHash 耗时 45ms / 10000,前者大约是后者的 5 倍,但是均匀到 id 上 0.022 ms  / id 的耗时也是可以接受的,所以耗时虽然比 CommonHash 慢5倍,但是工业场景下也基本不受影响。

四.总结

经过上面的分析,该使用什么分组 AB Test 不用我说了吧。