万丈高楼平地起 ——Redis 基础数据结构

2019/04/19 22:42
阅读数 9

Redis 有 5 种基础数据结构,分别为:string (字符串)、list (列表)、set (集合)、hash (哈希) 和 zset (有序集合)。

一、string (字符串)

字符串内部结构

Redis 的字符串是动态字符串,是可以修改的字符串,内部结构实现上类似于 Java 的ArrayList,采用预分配冗余空间的方式来减少内存的频繁分配,如图中所示,内部为当前字符串实际分配的空间 capacity 一般要高于实际字符串长度 len。当字符串长度小于 1M 时,扩容都是加倍现有的空间,如果超过 1M,扩容时一次只会多扩 1M 的空间。需要注意的是字符串最大长度为 512M。

键值对

        jedis.set(STR,STR);
        Assert.assertTrue(jedis.exists(STR));
        Assert.assertEquals(STR,jedis.get(STR));
        jedis.del(STR);

批量键值对

可以批量对多个字符串进行读写,节省网络耗时开销

        jedis.mset(STR+1,STR+1,STR+2,STR+2);
        String[] mKey = new String[]{STR + 1, STR + 2};
        List<String> mStr = jedis.mget(mKey);
        Assert.assertEquals(2,mStr.size());
        Assert.assertEquals(CollectionUtil.newArrayList(mKey),mStr);
        jedis.del(mKey);

过期和 set 命令扩展

可以对 key 设置过期时间,到点自动删除,这个功能常用来控制缓存的失效时间。

        jedis.set(STR,STR);
        jedis.expire(STR,2); //  2s 后过期
        Thread.sleep(2000);
        Assert.assertFalse(jedis.exists(STR));
        jedis.setex(STR,2,STR); //  2s 后过期
        Thread.sleep(2000);
        Assert.assertFalse(jedis.exists(STR));
        Long setnx = jedis.setnx(STR, STR);// 如果 key 不存在就执行 set 创建
        Assert.assertEquals(1,setnx.longValue());
        setnx = jedis.setnx(STR,STR+1); // 因为 key 已经存在,所以 set 创建不成功
        Assert.assertEquals(0,setnx.longValue());
        jedis.del(STR);

计数

如果 value 值是一个整数,还可以对它进行自增操作。自增是有范围的,它的范围是signed long 的最大最小值,超过了这个值,Redis 会报错

        jedis.set(STR,"0");
        jedis.incr(STR);
        Assert.assertEquals("1",jedis.get(STR));
        jedis.incrBy(STR,5);
        Assert.assertEquals("6",jedis.get(STR));
        jedis.set(STR,Long.MAX_VALUE + StrUtil.EMPTY);
        try {
            jedis.incr(STR);
        }catch (Exception e){
            Assert.assertEquals(JedisDataException.class,e.getClass());
        }
        jedis.del(STR);

字符串是由多个字节组成,每个字节又是由 8 个 bit 组成,如此便可以将一个字符串看成很多 bit 的组合,这便是 bitmap「位图」数据结构。

二、list (列表)

Redis 的列表相当于 Java 语言里面的 LinkedList,注意它是链表而不是数组。这意味着list 的插入和删除操作非常快,时间复杂度为 O(1),但是索引定位很慢,时间复杂度为O(n),这点让人非常意外。

当列表弹出了最后一个元素之后,该数据结构自动被删除,内存被回收。

Redis 的列表结构常用来做异步队列使用。将需要延后处理的任务结构体序列化成字符串塞进 Redis 的列表,另一个线程从这个列表中轮询数据进行处理。

右边进左边出:队列

        jedis.rpush(BOOKS,"python","java","golang");
        Assert.assertEquals(3,jedis.llen(BOOKS).longValue());
        Assert.assertEquals("python",jedis.lpop(BOOKS));
        Assert.assertEquals("java",jedis.lpop(BOOKS));
        Assert.assertEquals("golang",jedis.lpop(BOOKS));
        Assert.assertEquals(null,jedis.lpop(BOOKS));

右边进右边出:栈

        Assert.assertEquals(3,jedis.rpush(BOOKS,"python","java","golang").longValue());
        Assert.assertEquals("golang",jedis.rpop(BOOKS));
        Assert.assertEquals("java",jedis.rpop(BOOKS));
        Assert.assertEquals("python",jedis.rpop(BOOKS));
        Assert.assertEquals(null,jedis.rpop(BOOKS));

慢操作

lindex 相当于 Java 链表的 get(int index)方法,它需要对链表进行遍历,性能随着参数index 增大而变差ltrim 跟的两个参数 start_index 和 end_index 定义了一个区间,在这个区间内的值要保留,区间之外统统砍掉。我们可以通过 ltrim 来实现一个定长的链表,这一点非常有用。index 可以为负数,index=-1 表示倒数第一个元素,同样 index=-2 表示倒数第二个元素

        Assert.assertEquals(3,jedis.rpush(BOOKS, "python", "java", "golang").longValue());
        Assert.assertEquals("java",jedis.lindex(BOOKS,1)); //  O(n) 慎用
        Assert.assertEquals(CollectionUtil.newArrayList("python", "java", "golang"),
                jedis.lrange(BOOKS,0,-1)); //  O(n) 慎用
        jedis.ltrim(BOOKS,1,-1);
        Assert.assertEquals(CollectionUtil.newArrayList( "java", "golang"),
                jedis.lrange(BOOKS,0,-1)); //  O(n) 慎用
        jedis.ltrim(BOOKS,1,0); // 是清空了整个列表,因为区间范围长度为负
        Assert.assertEquals(0,jedis.llen(BOOKS).longValue());

三、hash (字典)

Redis 的字典相当于 Java 语言里面的 HashMap,它是无序字典。内部实现结构上同Java 的 HashMap 也是一致的,同样的数组 + 链表二维结构。第一维 hash 的数组位置碰撞时,就会将碰撞的元素使用链表串接起来。

hash字典数据结构

不同的是,Redis 的字典的值只能是字符串,另外它们 rehash 的方式不一样,因为Java 的 HashMap 在字典很大时,rehash 是个耗时的操作,需要一次性全部 rehash。Redis 为了高性能,不能堵塞服务,所以采用了渐进式 rehash 策略。

渐进式rehash

渐进式 rehash 会在 rehash 的同时,保留新旧两个 hash 结构,查询时会同时查询两个hash 结构,然后在后续的定时任务中以及 hash 的子指令中,循序渐进地将旧 hash 的内容一点点迁移到新的 hash 结构中。当 hash 移除了最后一个元素之后,该数据结构自动被删除,内存被回收。

hash 结构也可以用来存储用户信息,不同于字符串一次性需要全部序列化整个对象,hash 可以对用户结构中的每个字段单独存储。这样当我们需要获取用户信息时可以进行部分获取。而以整个字符串的形式去保存用户信息的话就只能一次性全部读取,这样就会比较浪费网络流量。

hash 也有缺点,hash 结构的存储消耗要高于单个字符串,到底该使用 hash 还是字符串,需要根据实际情况再三权衡。

        jedis.hset(BOOKS,"java","think in java");
        jedis.hset(BOOKS,"golang","concurrency in go");
        jedis.hset(BOOKS,"python","python cookbook");
        Assert.assertEquals(3,jedis.hlen(BOOKS).longValue());
        Map<String, String> books = jedis.hgetAll(BOOKS);
        Assert.assertEquals(3,books.size());
        Assert.assertEquals(jedis.hget(BOOKS,"java"),books.get("java"));
        jedis.hset(BOOKS,"golang","learning go programming" );
        Assert.assertEquals("learning go programming",jedis.hget(BOOKS,"golang"));
        jedis.hdel(BOOKS,"java","golang","python");
        jedis.hmset( BOOKS,books); // 批量 set
        Assert.assertEquals("concurrency in go",jedis.hget(BOOKS,"golang"));
        jedis.hdel(BOOKS,ArrayUtil.toArray(books.keySet(),String.class));

同字符串一样,hash 结构中的单个子 key 也可以进行计数,它对应的指令是 hincrby,和 incr 使用基本一样。

        jedis.hincrBy(USER_LAOQIAN, "age", 1 );
        jedis.hincrBy(USER_LAOQIAN, "age", 1 ); // 老钱又老了一岁
        Assert.assertEquals("2",jedis.hget(USER_LAOQIAN,"age"));
        jedis.hdel(USER_LAOQIAN,"age");

三、set (集合)

Redis 的集合相当于 Java 语言里面的 HashSet,它内部的键值对是无序的唯一的。它的内部实现相当于一个特殊的字典,字典中所有的 value 都是一个值 NULL。

当集合中最后一个元素移除之后,数据结构自动删除,内存被回收。 set 结构可以用来存储活动中奖的用户 ID,因为有去重功能,可以保证同一个用户不会中奖两次。

        Assert.assertEquals(1,jedis.sadd(BOOKS,"python").longValue());
        Assert.assertEquals(0,jedis.sadd(BOOKS,"python").longValue()); // 重复
        Assert.assertEquals(2,jedis.sadd(BOOKS,"java","golang").longValue());
        Set<String> smembers = jedis.smembers(BOOKS);
        Console.log(JSONUtil.toJsonPrettyStr(smembers)); // 注意顺序,和插入的并不一致,因为 set 是无序的
        Assert.assertTrue(jedis.sismember(BOOKS,"java")); // 查询某个 value 是否存在,相当于 contains(o)
        Assert.assertFalse(jedis.sismember(BOOKS,"ruby"));
        Assert.assertEquals(3,jedis.scard(BOOKS).longValue()); // 获取长度相当于 count()
        Console.log(jedis.spop(BOOKS)); // 弹出一个
        Assert.assertEquals(2,jedis.scard(BOOKS).longValue());
        jedis.srem(BOOKS,ArrayUtil.toArray(smembers,String.class));
        Assert.assertEquals(0,jedis.scard(BOOKS).longValue());

四、zset (有序列表)

zset 可能是 Redis 提供的最为特色的数据结构,它类似于 Java 的 SortedSetHashMap 的结合体,一方面它是一个 set,保证了内部value 的唯一性,另一方面它可以给每个 value 赋予一个 score,代表这个 value 的排序权重。它的内部实现用的是一种叫着「跳跃列表」的数据结构。zset 中最后一个 value 被移除后,数据结构自动删除,内存被回收。

zset 可以用来存粉丝列表,value 值是粉丝的用户 ID,score 是关注时间。我们可以对粉丝列表按关注时间进行排序。

zset 还可以用来存储学生的成绩,value 值是学生的 ID,score 是他的考试成绩。我们可以对成绩按分数进行排序就可以得到他的名次。

        String book1 = "think in java";
        String book2 = "java concurrency";
        String book3 = "java cookbook";
        jedis.zadd(BOOKS,9.0,book1);
        jedis.zadd(BOOKS,8.9,book2);
        jedis.zadd(BOOKS,8.6,book3);
        Assert.assertEquals(CollectionUtil.newArrayList(book3,book2,book1),
                CollectionUtil.newArrayList(jedis.zrange(BOOKS,0,-1))); // 按 score 排序列出,参数区间为排名范围
        Assert.assertEquals(CollectionUtil.newArrayList(book1,book2,book3),
                CollectionUtil.newArrayList(jedis.zrevrange(BOOKS,0,-1))); // 按 score 逆序列出,参数区间为排名范围
        Assert.assertEquals(3,jedis.zcard(BOOKS).longValue()); // 相当于 count()
        Assert.assertEquals("8.9",jedis.zscore(BOOKS, "java concurrency").toString()); // 获取指定 value 的 score
        Assert.assertEquals(1,jedis.zrank(BOOKS,"java concurrency").longValue() ); // 排名
        Assert.assertEquals(0,jedis.zrank(BOOKS,"java cookbook").longValue() ); // 排名,从0开始
        Assert.assertEquals(CollectionUtil.newArrayList(book3,book2),
                CollectionUtil.newArrayList(jedis.zrangeByScore(BOOKS, 0, 8.91))); // 根据分值区间遍历 zset
        // 根据分值区间 (-∞, 8.91] 遍历 zset,同时返回分值。inf 代表 infinite,无穷大的意思。
        Assert.assertEquals(CollectionUtil.newArrayList(new Tuple(book3,8.6),new Tuple(book2,8.9)),
                CollectionUtil.newArrayList(jedis.zrangeByScoreWithScores(BOOKS, "-inf","8.91")));
        jedis.zrem(BOOKS,book1,book2,book3);
        Assert.assertEquals(0,jedis.zcard(BOOKS).longValue());

本文基于《Redis深度历险:核心原理和应用实践》一文的JAVA实践。更多文章请参考:高性能缓存中间件Redis应用实战(JAVA)

展开阅读全文
打赏
0
0 收藏
分享
加载中
更多评论
打赏
0 评论
0 收藏
0
分享
返回顶部
顶部