Redis基本数据结构

Redis中有5个基本的数据结构,string(字符串),list(链表),hash(哈希),set(集合)以及zset(有序集合),本文对每个类型进行简单的介绍,并给出一些常用的使用命令。

字符串

Redis支持五种数据类型,string(字符串),list(列表),set(集合),zset(有序集合),hash(哈希)。Redis的存储本质上是key-value键值对,不同的数据类型指的是在value中存的数据类型。

字符串是Redis最简单的数据类型,使用也相当广泛。Redis中的字符串是动态字符串,是可以修改的,不同于Java中的字符串(Java中字符串是不可修改的)。
Redis的内部实现和ArrayList类似,会冗余多分配一些内存,且能够动态扩容但是Redis限制了String的最大大小为512MB。在string小于1M的情况下,每次扩容都是容量翻倍,当string超过1M后,每次扩容只会每次增加1M。

字符串是用的很多的一种类型,可以用来存储用户信息,session等。存之前先将对象序列化成JSON字符串,然后作为value存入Redis,key通常是用户唯一ID。

基本用法

string常用的一些命令:

  • set: 类似于Java中Map的put操作,新建或者修改一个key对应的value
  • get: 类似Map的get操作
  • del: 删除键值对
  • exists: 检测key是否存在,类似Java中的containsKey方法
  • mset: 批量设置键值对
  • mget: 一次获取多个key对应的值
  • expire: 设置key-value的存活时长

简单键值对操作

1
2
3
4
5
6
7
8
9
10
127.0.0.1:6379> set name jianyuan
OK
127.0.0.1:6379> get name
"jianyuan"
127.0.0.1:6379> exists name
(integer) 1
127.0.0.1:6379> del name
(integer) 1
127.0.0.1:6379> exists name
(integer) 0

批量键值对操作

1
2
3
4
5
6
7
8
9
10
127.0.0.1:6379> mset name jianyuan age 28
OK
127.0.0.1:6379> mget name age nonexist
1) "vincent"
2) "18"
3) (nil)
127.0.0.1:6379> exists name age
(integer) 2
127.0.0.1:6379> exists name age nonexist
(integer) 2

设置超时

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
127.0.0.1:6379> set name jianyuan
OK
127.0.0.1:6379> expire name 10
(integer) 1
# 10 seconds later
127.0.0.1:6379> exists name
(integer) 0
# 原子操作,同时设置name和超时
127.0.0.1:6379> set name haha EX 10
OK
# 检查name多久过期
127.0.0.1:6379> ttl name
(integer) 8
# 和set name haha EX 10效果一样
127.0.0.1:6379> setex name 10 haha
OK
127.0.0.1:6379> ttl name
(integer) 8

计数功能
当value时一个int值时,可以对其进行增加和减少操作。由于redis的单线程这个特性,这个操作天然就是原子操作,不存在并发问题,想想Java中的AtomicInteger等工具类。

1
2
3
4
5
6
7
8
127.0.0.1:6379> set age 18
OK
127.0.0.1:6379> incr age
(integer) 19
127.0.0.1:6379> incrby age 10
(integer) 29
127.0.0.1:6379> incrby age -5
(integer) 24

但是,Redis里面的自增操作也是有取值范围的,对应的取值范围是signed long。

1
2
3
4
5
6
7
8
127.0.0.1:6379> set edgeval 9223372036854775807 # Long.MAX
OK
127.0.0.1:6379> incr edgeval
(error) ERR increment or decrement would overflow
127.0.0.1:6379> set edgeval -9223372036854775808 # Long.MIN
OK
127.0.0.1:6379> incrby edgeval -1
(error) ERR increment or decrement would overflow

Redis字符串还有一个高级用法,就是构造位图(bitmap),其原理也很简单,大家知道字符串由字节组成,而一个字节是8位,把字符串展开成bits,也就得到位图了。位图可以用来压缩数据存储空间,比如用来存用户活跃天数。365天对应365个字节,签到了的天用1,未签到则是0.

链表list

Redis中list,对应的数据结构是双向链表,和Java中的LinkedList很相似。作为一个链表,他的插入和删除操作的时间复杂度是O(1),是一个很快的操作,而随机访问性能很差,时间复杂度是O(n),因为他需要从头结点开始进行遍历操作。

由于Redis是一个双向链表,可以从头和尾部进行存取操作,所以很容易模拟先进先出(队列)和先进后出(栈)。

Redis中list的常见操作

1
2
3
4
5
6
7
8
9
10
11
127.0.0.1:6379> rpush redisTypes string list set hash zset
(integer) 5
127.0.0.1:6379> rpop redisTypes
"zset"
127.0.0.1:6379> lpop redisTypes
"string"
127.0.0.1:6379> llen redisTypes
(integer) 3
127.0.0.1:6379> rlen redisTypes
127.0.0.1:6379> lpush redisTypes string list set hash zset
(integer) 8

上面的操作是对list头尾结点进行的操作,这些操作的时间复杂度都是O(1),不需要进行list遍历。下面来看看在list上的复杂操作。Java中,对于LinkedList进行随机index访问get(int index)方法需要对链表进行遍历,时间复杂度达到O(n),耗时随着n的增长而增长。同样,对redis的list的索引操作也很慢。Redis中lindex类似Java中链表的get(int index)操作。

1
2
3
4
127.0.0.1:6379> rpush components redis kafka zookeeper storm tsdb
(integer) 5
127.0.0.1:6379> lindex components 3 # 时间复杂度O(n),不推荐使用
"storm"

除了lindex,redis还提供了lrange,ltrim来基于索引查询list,这些方法的时间复杂度同样是O(n),慎用。

  • lrange: 获取指定范围的元素
  • ltrim: 只保留指定范围内的元素,其他元素全部删掉

index可以为负数,-1表示倒数第一个元素。

1
2
3
4
5
6
7
8
9
10
11
127.0.0.1:6379> lrange components 0 -1
1) "redis"
2) "kafka"
3) "zookeeper"
4) "storm"
5) "tsdb"
127.0.0.1:6379> ltrim components 3 -1
OK
127.0.0.1:6379> lrange components 0 -1
1) "storm"
2) "tsdb"

实现原理

其实,Redis中的list并不是一个简单的双向链表,底层的存储数据结构是quicklist(快速链表)。由于双向链表需要保存Prev和Next指正,内存浪费比较严重,且容易家中内存碎片化。Redis里面的quicklist是基于ziplist(压缩列表)构成的。压缩列表的元素是精密存放的,所以分配的内存是连续的,通过将ziplist使用双向指针串联起来,及满足了插入删除的性能,也避免了太多的内存冗余。

哈希Hash

对Java程序猿,理解Hash可以参考Java HashMap的实现。Redis的Hash是基于数组和链表实现的,和JDK8之前的HashMap实现原理一样,首先hash确定数组的索引,如果发生碰撞,则通过链表的方式串联起来。

image

Java中,当HashMap很大时,rehash是一个很耗时的操作,因为需要一次性rehash完成。Redis中的hash字典在rehash的则不要求一次性完成rehash,而是渐进式的rehash。在渐进式rehash的过程中,会保留两个table,一个是老的,一个是rehash后的,如果在rehash的过程中查询,会同时查询两个hash表,在后续的定时任务或者hash操作中,慢慢的完成rehash,将旧的hash表中的内容迁移到新的hash结构上。迁移完成后,老的hash就不存在了,后续的查询只会查新的hash结构。

Hash也可以用来存储用户信息。如果我们用string来存储的用户信息,我们需要将用户信息通过hash我们可以对用户信息的各个字段单独存储。

使用string存用户信息,每次存取都必须操作完整的用户信息,而用hash则可以单独处理字段,所以使用hash比较节省网络带宽。但是,使用hash的缺点是比使用string占用更多的内存。实际项目中按照具体需求来选择合理的数据结构,一个建议是:

  • 使用string: 如果你每次操作需要用到大部分的字段,或者key有可能改变
  • 使用hash:大部分情况只需要访问和处理单个字段,你明确知道哪些字段是可用的,哪些是在redis中没有的。

常见的操作

  • hset: hset key field value,设置key某个字段的值
  • hgetall: hgetall key,获取key的所有字段
  • hlen: hlen key,查询key的字段数
  • hget: hget key field, 获取key指定字段的值
  • hmset: hmset key field value [field value…],设置key多个字段的值
  • hincrby: hincrby key field increment, 增加key的某个字段的值,增量是increment
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
127.0.0.1:6379> hset userinfo name "Jianyuan"
(integer) 1
127.0.0.1:6379> hset userinfo age 18
(integer) 1
127.0.0.1:6379> hset userinfo addresse "China"
(integer) 1
127.0.0.1:6379> hgetall userinfo
1) "name"
2) "Jianyuan"
3) "age"
4) "18"
5) "addresse"
6) "China"
127.0.0.1:6379> hlen userinfo
(integer) 3
127.0.0.1:6379> hincrby userinfo age 10
(integer) 28
127.0.0.1:6379> hmset jianyuan name jianyuan age 18 address "Chengdu, Sichuan, China"
OK

集合Set

Redis的set和Java的HashSet相当,表示唯一集合。Java中,HashSet是基于HashMap实现的,同样,Redis中的Set是基于Hash实现的,实际上是value都为null的hash。

Set常见的操作:

  • sadd : sadd key member [member …]。为指定key添加成员
  • smembers: smembers key,列出key的成员
  • sismember: sismember key member, 判断集合中是否包含member
  • scard: scard key, 获取key的元素数量
  • spop: spop key [count],弹出

有序集合zset

zset是redis中最有特色的一个数据类型,即有序集合。类似于java中的SortedSet和HashMap的合体。zset一方面是一个集合,内部元素唯一不重复,同时又可以带有一个值(score),代表排序的权重。zset是基于跳表实现的。

zset常用操作:

  • zadd: zadd key score member
  • zrange: zrange key start stop [WITHSCORES]
  • zreverange: zrevrange key start stop [WITHSCORES]
  • zcard: zcard key, key的成员数量
  • zrangebyscore: zrangebyscore key min max
  • zscore: zscore key member, 指定key的成员的score
  • zrem:zrem key member, 删除成员。

Redis数据结构的一些规则

list/set/hash/zset这四个数据结构是容器型的数据结构,他们遵从下面的规则:

  1. create if not exists: 如果容器不存,先创建再做操作。比如rpush刚开始时没有列表,Redis会先创建一个列表,然后再应用rpush的操作。
  2. drop if no elements: 如果容器中没有其他元素,就会释放列表清理内存。所以,如果lpop操作到最后一个元素,列表就没释放了。

过期策略
Redis所有的数据结构都可以设置过期时间,时间到了,redis会自动删除相应的对象。Redis过期是以对象为单位的,比如hash结构的过期是对hash对象的过期,而不是对其中的某个子key。

如果一个字符串已经设置了过期时间,然后调用了set方法进行了修改,过期时间会被清除。

127.0.0.1:6379> set hello world
OK
127.0.0.1:6379> expire hello 10000
(integer) 1
127.0.0.1:6379> ttl hello
(integer) 9998
127.0.0.1:6379> set hello "nice world"
OK
127.0.0.1:6379> ttl hello
(integer) -1
skiplist介绍 ParNew长时间停顿

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×