HLL是什么
HyperLogLog(HLL)算法经常在数据库中被用来统计某一字段的Distinct Value,比如Redis的HyperLogLog结构。目前在我们项目中用于UV统计。
网上有一篇大佬博文十分深入:
https://www.jianshu.com/p/55defda6dcd2
注意:
1.此算法是近似估算值,该算法能在一定误差内近似统计去重数,可以输出空间内去重数大小,若要求UV详细数据则该方法不适用。
2.需要在空间与准确性中取舍。若要求准确性较高,则HLL生成的空间就会较大。
基本流程
统计基本流程:
说明:
- hll空间处理时一般使用的是对象字节,而存储时一般不会将对象字节存入,所以这里我们可以将空间对象转换为base64再存储。
- 这里涉及到对放入对象的hash处理,这里的hash方法值十分重要,直接影响到基数准确与否,目前我们使用的Guava工具包: com.google.common.hash.HashFunction#hashString
具体实现(Java)
HLL我们使用的工具包为java-hll
地址:java-hll
/**
* 根据value向指定key空间进行基数整合
* @param ea
* @param moduleKey 模块key
* @param key 模块中物料 key
* @param value 新增value
*/
public void incrementUV(String ea, String moduleKey, String key, String value){
Preconditions.checkArgument(!Strings.isNullOrEmpty(ea));
Preconditions.checkArgument(!Strings.isNullOrEmpty(moduleKey));
Preconditions.checkArgument(!Strings.isNullOrEmpty(key));
Preconditions.checkArgument(!Strings.isNullOrEmpty(value));
SerializableHLL existedHLL;
// 从库中获取是否有对应hll_value
String existedHllBytesBase64 = uvHllStatisticDao.getHllValue(ea, moduleKey, key);
if(existedHllBytesBase64 == null){
// 创建一个新的空间
existedHLL = HLLUtil.newHLL();
}else{
// 将已有的数据转换为空间
existedHLL = HLLUtil.newHLLFromBytesBase64(existedHllBytesBase64);
}
// 在空间中添加数据
existedHLL.addRaw(HLLUtil.hash(value));
// 计算空间内数据总数
long newCount = existedHLL.cardinality();
uvHllStatisticDao.upsertHllValuesAndCount(ea, moduleKey, key, existedHLL.toBytesBase64(), (int)newCount);
}
上面方法是其中一个使用案例,在这里封装了一个HLLUtil的工具包
public class HLLUtil {
/**
* log2m 桶数的log-base-2 比如桶数为64,则log2m=6,该参数应在4-30之间
*/
private static final int LOG2M = 8;
/**
* 每个桶的位数,在1-8之间(最大为一个字节)
*/
private static final int REG_WIDTH = 8;
/**
* 当EXPLICIT提升为SPARSE的阈值,取值为1-18
*/
private static final int EXP_THRESH = 5;
private static final HashFunction HASH_FUNCTION = Hashing.murmur3_128(1853);
public static SerializableHLL newHLL(){
/**
* 第三个参数:标识SPARSE是否被使用
* 第四个参数:计算的类型
* EXPLICIT 进准存储,基于Long的Hashset
* SPARSE 稀疏计算,数据量不大时为节省空间,只保存有值的桶
* FULL 大部分桶有值时,用位向量存储桶内内容
*/
HLL hll = new HLL(LOG2M, REG_WIDTH, EXP_THRESH, false, HLLType.EXPLICIT);
return new SerializableHLL(hll);
}
public static SerializableHLL newHLL(byte[] hllSpace){
HLL hll = HLL.fromBytes(hllSpace);
return new SerializableHLL(hll);
}
public static SerializableHLL newHLLFromBytesBase64(String base64){
return new SerializableHLL(HLL.fromBytes(Base64.getDecoder().decode(base64)));
}
/**
* 使用的是EXPLICIT计算方式所以需要计算Hash这里使用的是Guava 工具包
* @return hash值
*/
public static long hash(CharSequence charSequence){
return HASH_FUNCTION.hashString(charSequence, Charset.forName("UTF-8")).asLong();
}
}
HLL 序列化对象类
public class SerializableHLL implements Serializable {
private static final long serialVersionUID = 1L;
private transient HLL targetHLL;
public SerializableHLL(HLL targetHLL) {
this.targetHLL = targetHLL;
}
public synchronized void addRaw(final long rawValue) {
targetHLL.addRaw(rawValue);
}
public synchronized long cardinality() {
return targetHLL.cardinality();
}
public synchronized byte[] toBytes(){
return targetHLL.toBytes();
}
public synchronized String toBytesBase64(){
return Base64.getEncoder().encodeToString(targetHLL.toBytes());
}
public synchronized void merge(SerializableHLL hll){
this.targetHLL.union(hll.targetHLL);
}
public synchronized void merge(byte[] hllSpace){
this.targetHLL.union(HLL.fromBytes(hllSpace));
}
private void writeObject(ObjectOutputStream oos) throws IOException{
oos.defaultWriteObject();
byte[] bs = targetHLL.toBytes();
oos.writeInt(bs.length);
oos.write(bs);
}
private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException{
ois.defaultReadObject();
int byteLength = ois.readInt();
byte[] bs = new byte[byteLength];
ois.read(bs);
targetHLL = HLL.fromBytes(bs);
}
}