UV 统计- HLL算法(JAVA实现)

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);
    }
}
  • 3
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值