Redis 是一种基于键值对的NoSQL数据库,它的值主要由string(字符串),hash(哈希),list(列表),set(集合),zset(有序集合)五种基本数据结构构成,除此之外还支持一些其他的数据结构和算法。其中key都是由字符串构成的.
这些数据类型的使用方式可以查看官方文档, 下面重点说一说数据类型的选择.
Redis五大数据类型的使用场景
1. 字符串(string)
字符串类型是Redis最基础的数据类型,字符串类型可以是JSON、XML甚至是二进制的图片等数据,但是最大值不能超过512MB。
底层数据结构
Redis会根据当前值的类型和长度决定使用哪种底层数据结构来实现。
字符串类型的底层数据结构有3种:
-
int
:8个字节的长整型。 -
embstr
:小于等于39个字节的字符串。 -
raw
:大于39个字节的字符串。
字符串类型的添加和查询
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
> set mykey somevalue OK > get mykey "somevalue" > set mykey newval nx # when not exists #> SETNX mykey newval # 和上面命令等效 (nil) > set mykey newval xx # only when exists OK > set counter 100 # string type includes number also OK > incr counter # atomic increcement by 1 (integer) 101 > incr counter (integer) 102 > incrby counter 50 # atomic increcement by 50 (integer) 152 # Multple Set/Get 批量查询和获取 > mset a 10 b 20 c 30 OK > mget a b c 10 20 30 |
字符串类型的Key空间操作
这里只列出key空间(key space)比较常用的三个方法:
-
del
删除key空间 -
type
返回key空间中所保存数据的数据类型 -
exists
查询key空间是否存在, 返回1为存在, 返回0则不存在
1 2 3 4 5 6 7 8 9 10 11 12 |
> set mykey hello OK > exists mykey 1 > type mykey string > del mykey 1 > exists mykey 0 > type mykey none |
设置key的过期时间-方法 1
1 2 3 4 5 6 7 8 |
> set key some-value OK > expire key 5 (integer) 1 > get key (immediately) "some-value" > get key (after some time) (nil) |
设置key的过期时间-方法 2
1 2 3 4 |
> set key 100 ex 10 # set key to 100 with expire time of 10 seconds OK > ttl key # check the remaining time to live for the key (integer) 9 |
设置key的过期时间-方法 3
1 2 3 4 5 6 7 8 9 |
# SETEX key seconds value # 给key设置value, 同时设置过期(EXpireTime)时间, 单位秒 # 如果过期则查询结果为null > SETEX exkey 120 value > get exkey valuie # 120秒后 > get exkey null |
1.2 使用场景
1.2.1 缓存
在web服务中,使用MySQL作为数据库,Redis 作为缓存。由于Redis具有支撑高并发的特性,通常能起到加速读写和降低后端压力的作用。web端的大多数请求都是从Redis中获取的数据,如果Redis中没有需要的数据,则会从MySQL中去获取,并将获取到的数据写入redis。
1.2.2 计数
Redis中有一个字符串相关的命令incr key
,incr命令对值做自增操作,返回结果分为以下三种情况:
-
值不是整数,返回错误
-
值是整数,返回自增后的结果
-
key不存在,返回1
比如文章的阅读量,视频的播放量等等都会使用redis来计数,每播放一次,对应的播放量就会加1,同时将这些数据异步存储到数据库中达到持久化的目的。
1.2.3 共享Session
在分布式系统中,用户的每次请求会访问到不同的服务器,这就会导致session不同步的问题,假如一个用来获取用户信息的请求落在A服务器上,获取到用户信息后存入session。下一个请求落在B服务器上,想要从session中获取用户信息就不能正常获取了,因为用户信息的session在服务器A上,为了解决这个问题,使用redis集中管理这些session,将session存入redis,使用的时候直接从redis中获取就可以了。
1.2.4 限速
为了安全考虑,有些网站会对IP进行限制,限制同一IP在给定的一段时间内访问次数不能超过n次。
在redis中保存一个count值,key为user:$ip,value为该ip访问的次数,第一次设置key的时候,设置过期时间为给定时间。
count加1之前,判断是否key是否存在,不存在的话,有两种情况:1、该ip未访问过;2、该ip访问过,但是key已经过期了。那么此时需要再次设置一次expires。
如果用户访问的时候,判断count的值是否大于上限,如果低于上限,就处理请求,否则就拒绝处理请求。
2. 哈希
Redis中哈希类型是指一个键值对的存储结构,其中键不能重复,值对应一个map。
可以用来表示一个表中的一列数据, 其中键为主键,值也就是map的key可以作为列名, map的value可以作为列的值.
比如要表示这样的一条结构化记录:
记录的主键: user:1000
username | birthyear | verified |
---|---|---|
antirez | 1977 | 1 |
使用Redis中的哈希类型存储方式如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
> hmset user:1000 username antirez birthyear 1977 verified 1 OK > hget user:1000 username "antirez" > hget user:1000 birthyear "1977" > hgetall user:1000 1) "username" 2) "antirez" 3) "birthyear" 4) "1977" 5) "verified" 6) "1" > hmget user:1000 username birthyear no-such-field 1) "antirez" 2) "1977" 3) (nil) # 当key不存在时返回 nil |
hincrby 可以对哈希表中的数据进行修改
1 2 3 4 |
> hincrby user:1000 birthyear 10 (integer) 1987 > hincrby user:1000 birthyear -10 (integer) 1977 |
You can find the full list of hash commands in the documentation.
2.1 底层数据结构
哈希类型的底层数据结构有两种:
ziplist(压缩列表):当哈希类型元素个数小于hash-max-ziplist-entries配置(默认512个)同时所有值都小于hash-max-ziplist-value配置(默认64字节)时使用。ziplist使用更加紧凑的结构实现多个元素的连续存储,所以比hashtable更加节省内存。hashtable(哈希表):当ziplist不能满足要求时,会使用hashtable。
2.2 使用场景
由于hash类型存储的是一个键值对,比如数据库有以下一个用户表结构:
表名为user
,其中一个记录如下:
id | username | birthyear | verified |
---|---|---|---|
1000 | antirez | 1977 | 1 |
将以上信息存入redis,用
1 |
> hmset user:1000 username antirez birthyear 1977 verified 1 |
哈希可以让一个Map结构(一系列的键值对)和一个表的主键关联拿起来, 对这样的具有多个字段的记录使用哈希存储会比字符串更加方便直观.
3. 列表
列表类型用来存储多个有序的字符串,一个列表最多可以存储2^32-1
个元素,列表的两端都可以插入和弹出元素。
列表的用法
可以列表看成一个数组构成的双端队列, 那么首先我们约定索引小的元素在左边, 索引大的元素在右边.rpush命令表示向数组的最右侧插入元素, lpush表示想数组的最左侧插入元素.
1 2 3 4 5 6 7 8 9 10 |
> rpush mylist A (integer) 1 > rpush mylist B (integer) 2 > lpush mylist first (integer) 3 > lrange mylist 0 -1 1) "first" 2) "A" 3) "B" |
即:
1 2 3 |
> rpush mylist A: 1) "A" - > rpush mylist B 1) "A" -> 2) "B" > lpush mylist first 1) "first" -> 2) "A" -> 3) "B" |
lrange 可以查询一段范围内的元素, 元素索引从0开始, 负数表示倒序的第几个
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
> rpush mylist 1 2 3 4 5 "foo bar" 9 # 返回总元素数量 > lrange mylist 0 -1 # 列出所有元素 first A B 1 2 3 4 5 foo bar > lrange mylist 1 8 # 效果一样 first A B 1 2 3 4 5 foo bar |
list也具有栈数据结构类似的pop命令, 不同的是可以选择从左边或者右边弹出元素并返回.
1 2 3 4 5 6 7 8 9 10 |
> rpush mylist a b c 3 > rpop mylist c > rpop mylist b > rpop mylist a > rpop mylist null |
同样, 如果要从左边弹出则可以使用LPOP
命令.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
> rpush mylist a b c 6 > del mylist 1 > rpush mylist a b c 3 > lpop mylist a > lpop mylist b > lpop mylist c > lpop mylist null |
Capped lists – 带上限的列表
ltrim
的使用类似lrange,只不过它的作用是将指定区间的元素作为新的列表返回.
1 2 3 4 5 6 7 8 |
> rpush mylist 1 2 3 4 5 (integer) 5 > ltrim mylist 0 2 OK > lrange mylist 0 -1 1) "1" 2) "2" 3) "3" |
上面的LTRIM
命令告诉Redis仅从索引0到2列出列表元素,其他所有内容都将被丢弃。将PUSH + TRIM
命令搭配可以很容易维护一个有限容量的队列, 以便添加新元素并丢弃超出限制的元素.
1 2 |
LPUSH mylist <some element> LTRIM mylist 0 999 |
上面的组合添加了一个新元素,并且仅将1000个最新元素纳入列表。使用LRANGE
可以访问最重要的1000个元素,而无需记住旧的数据。
注意:LRANGE
从技术上讲是O(N)命令,但朝列表的开头或结尾访问较小范围的元素可以近似为O(1)操作。
3.1 底层数据结构
列表的底层数据结构有两种:
ziplist(压缩列表):当哈希类型元素个数小于list-max-ziplist-entries配置(默认512个)同时所有值都小于list-max-ziplist-value配置(默认64字节)时使用。ziplist使用更加紧凑的结构实现多个元素的连续存储,所以比hashtable更加节省内存。linkedlist(链表):当ziplist不能满足要求时,会使用linkedlist。
3.2 使用场景
3.2.1 消息队列
列表用来存储多个有序的字符串,既然是有序的,那么就满足消息队列的特点。使用lpush + rpop
或者rpush + lpop
实现消息队列。
除此之外,redis支持阻塞操作,在弹出元素的时候使用阻塞命令来实现阻塞队列。
列表具有一项特殊功能,使其适合于实现队列,并且通常用作进程间通信系统的构建块:阻止操作。
想象一下,您想通过一个流程将项目推入列表,然后使用不同的流程来对这些项目进行某种处理工作。这是通常的生产者/使用者设置,可以通过以下简单方式实现:
- 为了将项目推送到列表中,生产者调用
LPUSH
。 - 为了从列表中提取/处理项目,消费者调用
RPOP
。
但是,有时列表可能为空,没有任何要处理的内容,因此RPOP
仅返回NULL
。在这种情况下,消费者被迫等待一段时间,然后使用RPOP
重试。这称为轮询,这种情况下这不是一个好主意,因为它有几个缺点:
- 强制Redis和客户端处理无用的命令(列表为空时的所有请求将无法完成任何实际工作,它们只会返回NULL)。
- 由于消费者在收到NULL之后会等待一段时间,因此会增加项目处理的延迟。
所以,命令BRPOP
和BLPOP
就是带有堵塞功能的RPOP
和LPOP
,作用是能够如果列表是空的就堵塞,如果在指定的超时时间到达前列表有了新的数据则返回新数据,否则超过超时时间则直接返回.
这是BRPOP调用的示例:
1 2 3 |
> brpop tasks 5 1) "tasks" 2) "do_something" |
这意味着:“等待列表中的元素tasks
的最后一个元素,但如果5秒钟后没有可用元素,则返回”。
请注意,您可以将0用作超时来永远等待元素,还可以指定多个列表,而不仅仅是一个列表,以便同时等待多个列表,并在第一个列表收到一个元素时得到通知。
注意:
- 客户端以有序方式提供服务:第一个阻塞等待列表的客户端,在某个元素被其他客户端推送时首先提供服务,依此类推。
- 返回值与
RPOP
相比有所不同:它是一个包含两个元素的数组,因为它还包含键的名称,因为BRPOP
和BLPOP
能够阻止等待来自多个列表的元素。- 如果达到超时,则返回NULL。
关于列表和阻止操作,建议了解以下内容:
3.2.2 栈
由于列表存储的是有序字符串,满足队列的特点,也就能满足栈先进后出的特点,使用lpush + lpop
或者rpush + rpop
实现栈。
3.2.3 文章列表
因为列表的元素不但是有序的,而且还支持按照索引范围获取元素。因此我们可以使用lrange
命令分页获取文章列表
1 |
> lrange key 0 9 |
4. 集合
集合类型也可以保存多个字符串元素,与列表不同的是,集合中不允许有重复元素并且集合中的元素是无序的。一个集合最多可以存储2^32-1
个元素。可以对集合使用SADD
命令添加新的元素。还可以对集合进行许多其他操作,例如测试给定元素是否已存在,执行多个集合之间的交集,并集或求差集等等。
1 2 3 4 5 6 7 8 9 10 11 |
> sadd myset 1 2 3 # set::add (integer) 3 > smembers myset 1. 3 # not ordered 2. 1 3. 2 > sismember myset 3 # set::is_member (integer) 1 # 3 is member > sismember myset 30 (integer) 0 # 30 is not member |
提取元素的命令称为SPOP,对于建模某些问题非常方便。例如,为了实现基于Web的扑克游戏,您可能需要用一组字符来代表您的套牌。扑克牌有四种花色, Clubs(梅花),Diamond(方块),Heart(红桃),Spade(黑桃), 使用首字母作为前缀表示花色,则一副扑克牌我们可以使用集合表示:
1 2 3 4 5 |
> sadd deck C1 C2 C3 C4 C5 C6 C7 C8 C9 C10 CJ CQ CK D1 D2 D3 D4 D5 D6 D7 D8 D9 D10 DJ DQ DK H1 H2 H3 H4 H5 H6 H7 H8 H9 H10 HJ HQ HK S1 S2 S3 S4 S5 S6 S7 S8 S9 S10 SJ SQ SK (integer) 52 |
现在我们要为每个玩家提供5张卡片。该SPOP命令删除一个随机元素,将其返回到客户端,所以在这种情况下完美运行。
但是,如果我们直接操作deck集合,那么在游戏的下一场比赛中,我们将需要再次填充纸牌。因此,首先我们可以将deck
集合复制到新集合game:1:deck
中,用于开始第一局游戏。
这可以使用SUNIONSTORE
来完成,SUNIONSTORE通常执行多个集合之间的联合,并将结果存储到第一个集合中。语法:
1 |
SUNIONSTORE destination key [key ...] |
但是,由于单个集合的并集就是它本身,我可以使用以下命令复制我的卡组:
1 2 |
> sunionstore game:1:deck deck (integer) 52 |
现在,我准备为第一位玩家提供五张牌:
1 2 3 4 5 6 7 8 9 10 |
> spop game:1:deck "C6" > spop game:1:deck "CQ" > spop game:1:deck "D1" > spop game:1:deck "CJ" > spop game:1:deck "SJ" |
Redis 的一个命令可以返回集合中元素的数量。在集合理论中元素数量也称为_集合_的基数(cardinality
),因此Redis的这个命令叫做SCARD
。
1 2 |
> scard game:1:deck (integer) 47 |
数学原理:52-5 = 47。
当您只需要获取随机元素而不将其从集合中删除时,可以使用SRANDMEMBER
命令。它还具有返回重复元素和非重复元素的功能。
4.1 底层数据结构
集合类型的底层数据结构有两种:
intset(整数集合):当集合中的元素都是整数且元素个数小于set-max-intset-entries
配置(默认512个)时,redis会选用intset来作为集合的内部实现,从而减少内存的使用。hashtable(哈希表):当intset不能满足要求时,会使用hashtable。
4.2 使用场景
4.2.1 用户标签
集合非常适合表示对象之间的关系。例如,我们可以轻松地使用集合来实现标签功能。
对这个问题进行建模的一种简单方法是为我们要标记的每个对象设置一个集合。该集合包含与对象关联的标签的ID。
以新闻文章来举例。如果ID为1000的新闻带有标签1、2、5和77,实现标签功能需要实现:
- 则使用集合可以将这些标签ID与新闻项相关联:
1 2 |
> sadd news:1000:tags 1 2 5 77 (integer) 4 |
- 反过来我们还需要将给定标签关联到所有新闻的列表:
1 2 3 4 5 6 7 8 |
> sadd tag:1:news 1000 (integer) 1 > sadd tag:2:news 1000 (integer) 1 > sadd tag:5:news 1000 (integer) 1 > sadd tag:77:news 1000 (integer) 1 |
以上2个步骤应放在一个事务中.
有了以上关系后, 要获取给定新闻ID的所有标签很简单:
1 2 3 4 5 |
> smembers news:1000:tags 1. 5 2. 1 3. 77 4. 2 |
也可以给定标签查询所有包含该标签的新闻
1 2 |
> smembers tag:id:news ... results here ... |
注意:如果是完整的工程,还需要有完整的标签表结构和完整的新闻表结构,例如Redis的哈希数据类型,可以将标签ID映射到标签名称, 将新闻ID映射到新闻名称上。
还有其他一些非常简单的操作,使用正确的Redis命令仍然很容易实现。例如,我们可能需要包含标签1、2、10和27的所有对象的列表。我们可以使用SINTER
命令执行此操作,该命令执行不同集合之间的交集。我们可以用:
1 2 |
> sinter tag:1:news tag:2:news tag:10:news tag:27:news ... results here ... |
除了交集之外,您还可以执行并集,求差,提取随机元素等等。
4.2.2 抽奖功能
集合有两个命令支持获取随机数,分别是:
随机获取count个元素,集合元素个数不变
1 |
> srandmember key [count] |
随机弹出count个元素,元素从集合弹出,集合元素个数改变
1 |
> spop key [count] |
用户点击抽奖按钮,参数抽奖,将用户编号放入集合,然后抽奖,分别抽一等奖、二等奖,如果已经抽中一等奖的用户不能参数抽二等奖则使用spop
,反之使用srandmember
。
5. 有序集合
有序集合(sorted-set, zset)是集合和哈希表的结合,这是因为在有序集合也属于集合,和集合一样不能有重复元素。但是有序集合可以排序,它给每个元素设置一个score的浮点值作为排序的依据。最多可以存储2^32-1个元素。
排序规则:
- 如果A和B是两个 score 不同的元素,如果
A.score > B.score
,则有A>B
。 - 如果A和B的score完全相同,那么如果A字符串在字典序上大于B字符串,则有
A>B
。A和B字符串不能相等,因为有序集合不能保存重复的key。
下面使用zadd将黑客信息添加到有序集合里, 其中出生日期作为排序的score值:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
> zadd hackers 1940 "Alan Kay" (integer) 1 > zadd hackers 1957 "Sophie Wilson" (integer) 1 > zadd hackers 1953 "Richard Stallman" (integer) 1 > zadd hackers 1949 "Anita Borg" (integer) 1 > zadd hackers 1965 "Yukihiro Matsumoto" (integer) 1 > zadd hackers 1914 "Hedy Lamarr" (integer) 1 > zadd hackers 1916 "Claude Shannon" (integer) 1 > zadd hackers 1969 "Linus Torvalds" (integer) 1 > zadd hackers 1912 "Alan Turing" (integer) 1 |
根据生日先后排序
1 2 3 4 5 6 7 8 9 10 |
> zrange hackers 0 -1 1) "Alan Turing" 2) "Hedy Lamarr" 3) "Claude Shannon" 4) "Alan Kay" 5) "Anita Borg" 6) "Richard Stallman" 7) "Sophie Wilson" 8) "Yukihiro Matsumoto" 9) "Linus Torvalds" |
根据生日先后倒序排序
1 2 3 4 5 6 7 8 9 10 |
> zrevrange hackers 0 -1 1) "Linus Torvalds" 2) "Yukihiro Matsumoto" 3) "Sophie Wilson" 4) "Richard Stallman" 5) "Anita Borg" 6) "Alan Kay" 7) "Claude Shannon" 8) "Hedy Lamarr" 9) "Alan Turing" |
zrange
或者zrevrange
后面加上WITHSCORES
可以同时返回score数值,如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
> zrange hackers 0 -1 withscores 1) "Alan Turing" 2) "1912" 3) "Hedy Lamarr" 4) "1914" 5) "Claude Shannon" 6) "1916" 7) "Alan Kay" 8) "1940" 9) "Anita Borg" 10) "1949" 11) "Richard Stallman" 12) "1953" 13) "Sophie Wilson" 14) "1957" 15) "Yukihiro Matsumoto" 16) "1965" 17) "Linus Torvalds" 18) "1969" |
对排序的区间进行操作,比如找出出生日期早于等于1950年龄的记录
1 2 3 4 5 6 |
> zrangebyscore hackers -inf 1950 # range by score [-infinite,1950] 1) "Alan Turing" 2) "Hedy Lamarr" 3) "Claude Shannon" 4) "Alan Kay" 5) "Anita Borg" |
再比如删除生日区间在1940到1060之间的记录,并返回删除的记录个数
1 2 |
> zremrangebyscore hackers 1940 1960 # remove range by score (integer) 4 |
获取"Anita Borg"的生日排名:
1 2 |
> zrank hackers "Anita Borg" (integer) 4 # 正数第5 |
如果要获取"Anita Borg"的生日倒序排名可以使用zrevrank
命令。
5.1 底层数据结构
有序集合类型的底层数据结构有两种:
ziplist
(压缩列表):当有序集合的元素个数小于list-max-ziplist-entries
配置(默认128个)同时所有值都小于list-max-ziplist-value
配置(默认64字节)时使用。ziplist
使用更加紧凑的结构实现多个元素的连续存储,更加节省内存。skiplist
(跳跃表):当不满足ziplist
的要求时,会使用skiplist
。
5.2 使用场景
5.2.1 排行榜
用户发布了n篇文章,其他人看到文章后给喜欢的文章点赞,使用score来记录点赞数,有序集合会根据score排序。流程如下
用户发布一篇文章,初始点赞数为0,即score为0
1 2 3 4 5 6 |
zadd user:article 0 url://a (integer) 0 zadd user:article 0 url://b (integer) 0 zadd user:article 0 url://c (integer) 0 |
有人给文章点赞,递增1
1 2 3 4 5 6 7 8 9 10 11 12 |
zincrby user:article 1 url://a (integer) 1 zincrby user:article 1 url://a (integer) 2 zincrby user:article 1 url://a (integer) 3 zincrby user:article 1 url://b (integer) 1 zincrby user:article 1 url://c (integer) 1 zincrby user:article 1 url://c (integer) 2 |
查询点赞前三篇文章(倒序排序)
1 2 3 4 |
> zrevrange user:article 0 -1 1) "url://a" # score=3 2) "url://c" # score=2 3) "url://b" # score=1 |
查询点赞后三篇文章(正序排序)
1 2 3 4 |
> zrange user:article 0 -1 1) "url://b" # score=1 2) "url://c" # score=2 3) "url://a" # score=3 |
5.2.2 延迟消息队列
下单系统,下单后需要在15分钟内进行支付,如果15分钟未支付则自动取消订单。将下单后的十五分钟后的时间作为score,订单作为value存入redis,消费者轮询去消费,如果消费订单的时间大于等于这笔记录的score,则将这笔记录移除队列,即取消订单。
总结
在开发中,字符串类型是用的最多的数据类型,导致我们忽视了redis的其他四种数据类型,在具体场景下选择具体的数据类型对提升redis性能有非常大的帮助。redis虽然支持消息队列的实现,但是并不支持ack。所以redis实现的消息队列不能保证消息的可靠性,除非自己实现消息确认机制,不过这非常麻烦,所以如果是重要的消息还是推荐使用专门的消息队列去做。
注:另外可以利用Redis的Java客户端Jedis实现分布式锁。加锁实际上就是在redis中,给指定客户端的Key键设置一个值,为避免死锁,并给定一个过期时间。解锁的过程就是将指定客户端的Key键删除。但是实际上要实现真正好用且健壮的分布式锁是比较复杂的,一般会使用Redisson、zookeeper或者DataBase来实现。
Views: 161