如果你的页面访问量非常大,比如一个爆款页面几千万的 UV,你需要一个很大的 set
集合来统计,这就非常浪费空间。如果这样的页面很多,那所需要的存储空间是惊人的。为这样一个去重功能就耗费这样多的存储空间,不值得。可以使用HyperLogLog
。
HyperLogLog
提供了两个指令 pfadd
和 pfcount
,根据字面意义很好理解,一个是增加计数,一个是获取计数。pfadd
用法和 set
集合的 sadd
是一样的,来一个用户 ID,就将用户 ID 塞进去就是。pfcount
和 scard
用法是一样的,直接获取计数值。
127.0.0.1:6379> pfadd webpage user1
(integer) 1
127.0.0.1:6379> pfcount webpage
(integer) 1
127.0.0.1:6379> pfadd webpage user2
(integer) 1
127.0.0.1:6379> pfcount webpage
(integer) 2
127.0.0.1:6379> pfadd webpage user3 user4 user5 user6 user7 user8 user9 user10
(integer) 1
127.0.0.1:6379> pfcount webpage
(integer) 10
@Autowired
private RedisTemplate redisTemplate;
@Test
public void pfTest() {
long start=System.currentTimeMillis();
for (int i = 0; i < 100000; i++) {
pfadd("webpage", "user"+i);
}
Long total = pfcount("webpage");
System.out.println("\r\n");
System.out.printf("%d %d\n",100000, total);
System.out.println("\r\n"+(System.currentTimeMillis()-start)+"\r\n");
}
private List<Long> pfadd(final String key, Object ...value){
HyperLogLogOperations<Serializable, Object> operations=redisTemplate.opsForHyperLogLog();
List<Long> longs=new ArrayList<>();
for (int i = 0; i < value.length; i++) {
Long add = operations.add(key, value[i]);
longs.add(add);
}
return longs;
}
private Long pfcount(final String key){
HyperLogLogOperations<Serializable, Object> operations=redisTemplate.opsForHyperLogLog();
Long size = operations.size(key);
return size;
}
结果如下,差205个,0.205%,误差率不是算太高,再跑一次代码,结果还是一样,说明HLL
有去重的功能
100000 99795
8963
如果是要汇总多个页面的UV,则可使用pfmerge all webpage webpage2
127.0.0.1:6379> pfadd webpage2 user11
(integer) 1
127.0.0.1:6379> pfmerge all webpage webpage2
OK
127.0.0.1:6379> pfcount all
(integer) 99795
比如一些新闻推送的去重,或者是过滤垃圾邮件。
当布隆过滤器说某个值存在时,这个值可能不存在;当它说不存在时,那就肯定不存在
由于是插件,需要额外安装
rebloom
[root@localhost local]# git clone git://github.com/RedisLabsModules/rebloom
正克隆到 'rebloom'...
remote: Enumerating objects: 1032, done.
remote: Total 1032 (delta 0), reused 0 (delta 0), pack-reused 1032
接收对象中: 100% (1032/1032), 377.09 KiB | 3.00 KiB/s, done.
处理 delta 中: 100% (567/567), done.
[root@localhost local]# cd rebloom/
[root@localhost rebloom]# ls
contrib Dockerfile docs LICENSE Makefile mkdocs.yml ramp.yml README.md src tests
[root@localhost rebloom]# make
……编译过程省略
rebloom.so
[root@localhost rebloom]# ls
contrib Dockerfile docs LICENSE Makefile mkdocs.yml ramp.yml README.md rebloom.so src tests
redis.conf
在redis.conf
中加入该行:loadmodule /usr/local/rebloom/rebloom.so
,路径就是rebloom.so
的位置,再重启redis即可。
直接上,bf.exists
返回1
表示可能存在可能不存在,返回0
表示一定不存在
127.0.0.1:6379> bf.add email user1
(integer) 1
127.0.0.1:6379> bf.exists email user1
(integer) 1
127.0.0.1:6379> bf.exists email user2
(integer) 0
127.0.0.1:6379> bf.madd email user2 user3
1) (integer) 1
2) (integer) 1
127.0.0.1:6379> bf.mexists email user1 user2 user3 user4
1) (integer) 1
2) (integer) 1
3) (integer) 1
4) (integer) 0
Q:为什么 Redis 的事务不能支持回滚?
A:redis是先执行指令,然后记录日志,如果执行失败,日志也不会记录,也就不能回滚了。
Q:redis支持事务的原子性吗?
A:multi
表示事务开始的标志,exec
表示事务的执行。所有的指令在 exec
之前不执行,而是缓存在服务器的一个事务队列中,服务器一旦收到 exec
指令,才开执行整个事务队列,执行完毕后一次性返回所有指令的运行结果。
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set name zhaoyoung
QUEUED
127.0.0.1:6379> incr name
QUEUED
127.0.0.1:6379> set age 23
QUEUED
127.0.0.1:6379> exec
1) OK
2) (error) ERR value is not an integer or out of range
3) OK
127.0.0.1:6379> get name
"zhaoyoung"
127.0.0.1:6379> get age
"23"
上面的例子是事务执行到中间遇到失败了,因为我们不能对一个字符串进行数学运算,事务在遇到指令执行失败后,后面的指令还继续执行,所以 age
的值能继续得到设置。
到这里,你应该明白 Redis 的事务根本不能算「原子性」,而仅仅是满足了事务的「隔离性」,隔离性中的串行化——当前执行的事务有着不被其它事务打断的权利。
discard
在事务中的使用
127.0.0.1:6379> get age
"23"
127.0.0.1:6379> multi
OK
127.0.0.1:6379> incr age
QUEUED
127.0.0.1:6379> incr age
QUEUED
127.0.0.1:6379> discard
OK
127.0.0.1:6379> exec
(error) ERR EXEC without MULTI
discard
命令用于取消一个事务, 它清空客户端的整个事务队列, 然后将客户端从事务状态调整回非事务状态,最后返回字符串 OK 给客户端, 说明事务已被取消。
使用pipeline
优化上述使用redis事务的例子,将多次IO操作改为一次IO操作
@Test
public void transactionTest(){
Jedis jedis=new Jedis();
Pipeline pipelined = jedis.pipelined();
pipelined.multi();
pipelined.incr("age");
pipelined.incr("age");
Response<List<Object>> values = pipelined.exec();
List<Object> objects = values .get();
}
watch
的使用
比如redis存储了一个整数的账户余额,现有两个客户端并发修改余额,比如余额乘以2,我们需要先从redis中拿出余额,再在内存里计算余额,最后将结果写回redis。
redis提供了乐观锁watch
27.0.0.1:6379> get age 23
"23"
127.0.0.1:6379> watch age
OK
127.0.0.1:6379> incr age # 被修改了
(integer) 24
127.0.0.1:6379> multi
OK
127.0.0.1:6379> incr age
QUEUED
127.0.0.1:6379> exec # 事务执行失败
(nil)
watch
会在事务开始之前盯住 1 个或多个关键变量,当事务执行时,也就是服务器收到了 exec
指令要顺序执行缓存的事务队列时,Redis 会检查关键变量自 watch 之后,是否被修改了 (包括当前事务所在的客户端)。如果关键变量被人动过了,exec
指令就会返回 null
回复告知客户端事务执行失败,这个时候客户端一般会选择重试。
Redis 禁止在 multi
和 exec
之间执行 watch
指令,而必须在 multi
之前做好盯住关键变量,否则会出错。
@Test
public void transactionTest_2(){
Jedis jedis=jedisPool.getResource();
String userId="user1";
String key=keyFor(userId);
jedis.setnx(key,"5");
int account = doubleAccount(jedis, userId);
System.out.println("\r\n"+"======"+account);
jedis.close();
}
private static int doubleAccount(Jedis jedis, String userId) {
String key = keyFor(userId);
while (true) {
jedis.watch(key);
int value = Integer.parseInt(jedis.get(key));
// 加倍
value *= 2;
Transaction tx = jedis.multi();
tx.set(key, String.valueOf(value));
List<Object> res = tx.exec();
if (null !=res ) {
// 成功了
break;
}
}
// 重新获取余额
return Integer.parseInt(jedis.get(key));
}
private static String keyFor(String userId) {
return String.format("account_%s", userId);
}