首先来说一下业务场景吧。
如果需要去统计某个行为,如请求的发起次数,用redis去模拟这个行为就是如下,使用一个key=request_1的键值对,来保存该请求的处理次数即可,自增模拟访问请求。如果针对多个请求的统计,使用多个键值对去保存即可。
127.0.0.1:6379> set request_1 0
OK
127.0.0.1:6379> INCR request_1
(integer) 1
127.0.0.1:6379> INCR request_1
(integer) 2
127.0.0.1:6379> INCR request_1
(integer) 3
127.0.0.1:6379> INCR request_1
(integer) 4
127.0.0.1:6379> INCR request_1
(integer) 5
127.0.0.1:6379> INCR request_1
(integer) 6
那么问题来了,如果是对某个用户对某个请求的次数去重呢?就是某个用户访问某个请求,历史只算一次,我们按简单的来的话,java里面的数据结构必然会想到set,因为去重。那么redis里面也有set。简单概括一下就是,总共有多少个客户访问了某个请求,如果用set的话,可以实现功能,对于小项目来说,客户数量不多,数据量也不会很庞大。是可行的。
但是对大项目来说呢?这个set只会无限扩张,最后变得很庞大,非常占用内存,变得很臃肿。
那么如何既能够实现该功能,又可以减少内存的浪费呢,可以考虑使用hyperLoglog
先来基本的使用一下(命令为什么是pf,因为作者的姓名简称就是Pf)
模拟对requset_1的浏览用户的添加
127.0.0.1:6379> PFADD request_1_users user1
(integer) 1
127.0.0.1:6379> PFADD request_1_users user2
(integer) 1
127.0.0.1:6379> PFADD request_1_users user3
(integer) 1
统计出requset_1的浏览用户数
127.0.0.1:6379> PFCOUNT request_1_users
(integer) 3
如果某天,需要知道哪些客户,既访问了请求1又访问了请求2怎么办?
这个时候还有一个很强大的功能PFMERGE,我们来模拟一下
127.0.0.1:6379> PFADD request_1_users user1
(integer) 1
127.0.0.1:6379> PFADD request_1_users user2
(integer) 1
127.0.0.1:6379> PFADD request_1_users user3
(integer) 1
127.0.0.1:6379> PFADD request_2_users user1
(integer) 1
127.0.0.1:6379> PFADD request_2_users user2
(integer) 1
127.0.0.1:6379> PFADD request_2_users user4
(integer) 1
127.0.0.1:6379> PFCOUNT request_1_users
(integer) 3
127.0.0.1:6379> PFCOUNT request_2_users
(integer) 3
127.0.0.1:6379> PFMERGE request_1_2_users request_1_users request_2_users
OK
127.0.0.1:6379> PFCOUNT request_1_2_users
(integer) 4
通过PFMERGE指令,可以将多个hyperLoglog合在一块,生成新的集合。request_1_users集合有user1,user2,user3 request_2_users有user1,user2,user4,所以最终去重合并后的集合数应该为4.
概括一下就是hyperLoglog具备了以下功能
1.pfadd 往集合添加成员
2.pfcount 统计集合的长度
3.pfmerge 将多个集合融合成一个新集合
特点:数量特别大的时候内存占用只有12k,远远小于set占用的内存,另外数量大的时候可能会存在较小误差(即实际上pfadd了一亿次,而pfcount的数量少于1亿,但是这俩个的值区别不会太大,待会验证一下)
那么我们通写代码的方式来使用一下
首先看看redisTemplate对hyperLoglog的支持,总共有几个方法,add,size,delete,union,添加,统计,删除,融合,基本的都支持了。
public interface HyperLogLogOperations<K, V> {
/**
* Adds the given {@literal values} to the {@literal key}.
*
* @param key must not be {@literal null}.
* @param values must not be {@literal null}.
* @return 1 of at least one of the values was added to the key; 0 otherwise. {@literal null} when used in pipeline /
* transaction.
*/
Long add(K key, V... values);
/**
* Gets the current number of elements within the {@literal key}.
*
* @param keys must not be {@literal null} or {@literal empty}.
* @return {@literal null} when used in pipeline / transaction.
*/
Long size(K... keys);
/**
* Merges all values of given {@literal sourceKeys} into {@literal destination} key.
*
* @param destination key of HyperLogLog to move source keys into.
* @param sourceKeys must not be {@literal null} or {@literal empty}.
* @return {@literal null} when used in pipeline / transaction.
*/
Long union(K destination, K... sourceKeys);
/**
* Removes the given {@literal key}.
*
* @param key must not be {@literal null}.
*/
void delete(K key);
}
直接上代码
@Component
public class HyperLoglogUtils {
@Autowired
private RedisTemplate<String, String> jedisTemplate;
public Long add(String key, String ... value) {
return jedisTemplate.opsForHyperLogLog().add(key, value);
}
public void del(String key) {
jedisTemplate.opsForHyperLogLog().delete(key);
}
public Long merge(String destination, String ...sourceKeys) {
return jedisTemplate.opsForHyperLogLog().union(destination, sourceKeys);
}
public Long count(String key) {
return jedisTemplate.opsForHyperLogLog().size(key);
}
}
下面校验一下误差
public class TestHyperLoglog extends TestApplication{
@Autowired
private HyperLoglogUtils hyperLoglogUtils;
@Test
public void add() {
for(int i = 0; i < 100000; i ++) {
hyperLoglogUtils.add("my_req_one", "user" + i);
}
System.out.println(hyperLoglogUtils.count("my_req_one"));
}
}
最终输出结果99725 误差还是可以接受的,千分之225
网友评论