【redis】笔记

统计网页UV

如果你的页面访问量非常大,比如一个爆款页面几千万的 UV,你需要一个很大的 set 集合来统计,这就非常浪费空间。如果这样的页面很多,那所需要的存储空间是惊人的。为这样一个去重功能就耗费这样多的存储空间,不值得。可以使用HyperLogLog

HyperLogLog 提供了两个指令 pfaddpfcount,根据字面意义很好理解,一个是增加计数,一个是获取计数。pfadd 用法和 set 集合的 sadd 是一样的,来一个用户 ID,就将用户 ID 塞进去就是。pfcountscard 用法是一样的,直接获取计数值。

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


Redis事务

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

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

使用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

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 禁止在 multiexec 之间执行 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);
    }

你可能感兴趣的:(Redis)