Redis常用数据类型及应用场景
String(字符串)
一个 key 对应一个 value
redis 中的 string 可以包含任何数据。比如jpg图片或者序列化的对象
string 也是 redis最基本的数据结构,string类型的值最大能存储512MB
实例:
> set ha "测试字符串"
OK
> get ha
"测试字符串"
常用指令
命令 | 注释 | |
---|---|---|
1 | set key value | 设置指定key的值 |
2 | get key | 获取指定 key 的值 |
3 | getrange key start end | 返回 key 中字符串值的子字符 |
4 | getset key value | 将给定 key 的值设为 value ,并返回 key 的旧值(old value)。 |
5 | getbit key offset | 对 key 所储存的字符串值,获取指定偏移量上的位(bit) |
6 | mget key1 [key2…] | 获取所有(一个或多个)给定 key 的值 |
7 | setbit key offset value | 对 key 所储存的字符串值,设置或清除指定偏移量上的位(bit) |
8 | setex key seconds value | 将值 value 关联到 key ,并将 key 的过期时间设为 seconds (以秒为单位) |
9 | setnx key value | 只有在 key 不存在时设置 key 的值 |
10 | setrange key offset value | 用 value 参数覆写给定 key 所储存的字符串值,从偏移量 offset 开始 |
11 | strlen key | 返回 key 所储存的字符串值的长度 |
12 | mset key value [key value …] | 同时设置一个或多个 key-value 对 |
13 | msetnx key value [key value …] | 同时设置一个或多个 key-value 对,当且仅当所有给定 key 都不存在 |
14 | psetex key milliseconds value | 以毫秒为单位设置 key 的生存时间 |
15 | incr key | 将 key 中储存的数字值增一 |
16 | incrby key increment | 将 key 所储存的值加上给定的增量值(increment) |
17 | incrbyfloat key increment | 将 key 所储存的值加上给定的浮点增量值(increment) |
18 | decr key | 将 key 中储存的数字值减一 |
19 | decrby key decrement | key 所储存的值减去给定的减量值(decrement) |
20 | append key value | 如果 key 已经存在并且是一个字符串, APPEND 命令将指定的 value 追加到该 key 原来值(value)的末尾 |
应用场景
常规计数
因为 Redis 处理命令是单线程,所以执行命令的过程是原子的。因此 String 数据类型适合计数场景,比如计算访问次数、点赞、转发、库存数量等等。
分布式锁
SET 命令有个 NX 参数可以实现「key不存在才插入」,可以用它来实现分布式锁:
- 如果 key 不存在,则显示插入成功,可以用来表示加锁成功;
- 如果 key 存在,则会显示插入失败,可以用来表示加锁失败。
一般而言,还会对分布式锁加上过期时间,分布式锁的命令如下:
SET lock_key unique_value NX PX 10000
- lock_key 就是 key 键;
- unique_value 是客户端生成的唯一的标识;
- NX 代表只在 lock_key 不存在时,才对 lock_key 进行设置操作;
- PX 10000 表示设置 lock_key 的过期时间为 10s,这是为了避免客户端发生异常而无法释放锁。
而解锁的过程就是将 lock_key 键删除,但不能乱删,要保证执行操作的客户端就是加锁的客户端。所以,解锁的时候,我们要先判断锁的 unique_value 是否为加锁客户端,是的话,才将 lock_key 键删除。
可以看到,解锁是有两个操作,这时就需要 Lua 脚本来保证解锁的原子性,因为 Redis 在执行 Lua 脚本时,可以以原子性的方式执行,保证了锁释放操作的原子性。
// 释放锁时,先比较 unique_value 是否相等,避免锁的误释放
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
这样一来,就通过使用 SET 命令和 Lua 脚本在 Redis 单节点上完成了分布式锁的加锁和解锁。
共享session信息
通常我们在开发后台管理系统时,会使用 Session 来保存用户的会话(登录)状态,这些 Session 信息会被保存在服务器端,但这只适用于单系统应用,如果是分布式系统此模式将不再适用。
例如用户一的 Session 信息被存储在服务器一,但第二次访问时用户一被分配到服务器二,这个时候服务器并没有用户一的 Session 信息,就会出现需要重复登录的问题,问题在于分布式系统每次会把请求随机分配到不同的服务器。
分布式系统单独存储 Session 流程图:
因此,我们需要借助 Redis 对这些 Session 信息进行统一的存储和管理,这样无论请求发送到那台服务器,服务器都会去同一个 Redis 获取相关的 Session 信息,这样就解决了分布式系统下 Session 存储的问题。
分布式系统使用同一个 Redis 存储 Session 流程图:
Hash(哈希)
Redis 中的 hash 是一个键值(key => value)对的集合。适合用于存储对象
Redis 中每个 hash 可以存储 232 - 1 键值对(40多亿)。
实例: 设置一个 名字为 hash 的 hash集合,第一个 字段为 pai 对应的值:排,第二个字段为 hang 对应的值:行
> hmset hash pai "排" hang "行"
OK
> hgetall hash
1) "pai"
2) "排"
3) "hang"
4) "行"
常用指令
命令 | 注释 | |
---|---|---|
1 | hdel key field1 [field2] | 删除一个或多个哈希表字段 |
2 | hexists key field | 查看哈希表 key 中,指定的字段是否存在 |
3 | hget key field | 获取存储在哈希表中指定字段的值 |
4 | hgetall key | 获取在哈希表中指定 key 的所有字段和值 |
5 | hincrby key field increment | 为哈希表 key 中的指定字段的整数值加上增量 increment |
6 | hincrbyfloat key field increment | 为哈希表 key 中的指定字段的浮点数值加上增量 increment |
7 | hkeys key | 获取哈希表中的所有字段 |
8 | hlen key | 获取哈希表中字段的数量 |
9 | hmget key field1 [field2] | 获取所有给定字段的值 |
10 | hmset key field1 value1 [field2 value2 ] | 同时将多个 field-value (域-值)对设置到哈希表 key 中 |
11 | hset key field value | 将哈希表 key 中的字段 field 的值设为 value |
12 | hsetnx key field value | 只有在字段 field 不存在时,设置哈希表字段的值 |
13 | havls key | 获取哈希表中所有值 |
14 | hscan key cursor [MATCH pattern] [COUNT count] | 迭代哈希表中的键值对 |
应用场景
缓存对象
Hash 类型的 (key,field, value) 的结构与对象的(对象id, 属性, 值)的结构相似,也可以用来存储对象。
我们以用户信息为例,它在关系型数据库中的结构是这样的:
我们可以使用如下命令,将用户对象的信息存储到 Hash 类型:
# 存储一个哈希表uid:1的键值
> HMSET uid:1 name Tom age 15
2
# 存储一个哈希表uid:2的键值
> HMSET uid:2 name Jerry age 13
2
# 获取哈希表用户id为1中所有的键值
> HGETALL uid:1
1) "name"
2) "Tom"
3) "age"
4) "15"
Redis Hash 存储其结构如下图:
在介绍 String 类型的应用场景时有所介绍,String + Json也是存储对象的一种方式,那么存储对象时,到底用 String + json 还是用 Hash 呢?
一般对象用 String + Json 存储,对象中某些频繁变化的属性可以考虑抽出来用 Hash 类型存储。
购物车
以用户 id 为 key,商品 id 为 field,商品数量为 value,恰好构成了购物车的3个要素,如下图所示。
涉及的命令如下:
-
添加商品:
HSET cart:{用户id} {商品id} 1
-
添加数量:
HINCRBY cart:{用户id} {商品id} 1
-
商品总数:
HLEN cart:{用户id}
-
删除商品:
HDEL cart:{用户id} {商品id}
-
获取购物车所有商品:
HGETALL cart:{用户id}
当前仅仅是将商品ID存储到了Redis 中,在回显商品具体信息的时候,还需要拿着商品 id 查询一次数据库,获取完整的商品的信息。
List(列表)
Redis列表是简单的字符串列表,按照插入顺序排序。可以添加一个元素到列表的头部(左边) 或者 尾部(右边)。
一个列表最多可以包含 232 - 1 个元素 (4294967295, 每个列表超过40亿个元素)。
Lpush 将 元素插入到列表的左侧 ; Rpush 将元素 插入到列表的右侧。
List集合不需要单独 创建,直接使用插入 语句,如果此时不存在该集合,则会自动创建。
示例:
> lpush heiha redis
(integer) 1
> lpush heiha java
(integer) 2
> rpush heiha golang
(integer) 3
> lrange heiha 0 3
1) "java"
2) "redis"
3) "golang"
常用指令
命令 | 注释 | |
---|---|---|
1 | blpop key1 [key2 ] timeout | 移出并获取列表的第一个元素, 如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止 |
2 | brpop key1 [key2 ] timeout | 移出并获取列表的最后一个元素, 如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止 |
3 | brpoplpush source destination timeout | 从列表中弹出一个值,将弹出的元素插入到另外一个列表中并返回它; 如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止 |
4 | lindex key index | 通过索引获取列表中的元素 |
5 | linsert key BEFORE或AFTER pivot value | 在列表的元素前或者后插入元素 |
6 | llen key | 获取列表长度 |
7 | lpop key | 移出并获取列表的第一个元素 |
8 | lpush key value1 [value2] | 将一个或多个值插入到列表头部 |
9 | lpushx key value | 将一个值插入到已存在的列表头部 |
10 | lrange key start stop | 获取列表指定范围内的元素 |
11 | lrem key count value | 移除列表元素 |
12 | lset key index value | 通过索引设置列表元素的值 |
13 | ltrim key start stop | 对一个列表进行修剪(trim),就是说,让列表只保留指定区间内的元素,不在指定区间之内的元素都将被删除 |
14 | rpop key | 移除列表的最后一个元素,返回值为移除的元素 |
15 | rpoplpush source destination | 移除列表的最后一个元素,并将该元素添加到另一个列表并返回 |
16 | rpush key value1 [value2] | 在列表中添加一个或多个值到列表尾部 |
17 | rpushx key value | 为已存在的列表添加值 |
应用场景
消息队列
消息队列在存取消息时,必须要满足三个需求,分别是消息保序、处理重复的消息和保证消息可靠性。
1、如何满足消息保序需求?
List 本身就是按先进先出的顺序对数据进行存取的,所以,如果使用 List 作为消息队列保存消息的话,就已经能满足消息保序的需求了。
List 可以使用 LPUSH + RPOP (或者反过来,RPUSH+LPOP)命令实现消息队列。
- 生产者使用
LPUSH key value[value...]
将消息插入到队列的头部,如果 key 不存在则会创建一个空的队列再插入消息。 - 消费者使用
RPOP key
依次读取队列的消息,先进先出。
不过,在消费者读取数据时,有一个潜在的性能风险点。
在生产者往 List 中写入数据时,List 并不会主动地通知消费者有新消息写入,如果消费者想要及时处理消息,就需要在程序中不停地调用 RPOP
命令(比如使用一个while(1)循环)。如果有新消息写入,RPOP命令就会返回结果,否则,RPOP命令返回空值,再继续循环。
所以,即使没有新消息写入List,消费者也要不停地调用 RPOP 命令,这就会导致消费者程序的 CPU 一直消耗在执行 RPOP 命令上,带来不必要的性能损失。
为了解决这个问题,Redis提供了 BRPOP 命令。BRPOP命令也称为阻塞式读取,客户端在没有读到队列数据时,自动阻塞,直到有新的数据写入队列,再开始读取新数据。和消费者程序自己不停地调用RPOP命令相比,这种方式能节省CPU开销。
2、如何处理重复的消息?
消费者要实现重复消息的判断,需要 2 个方面的要求:
- 每个消息都有一个全局的 ID。
- 消费者要记录已经处理过的消息的 ID。当收到一条消息后,消费者程序就可以对比收到的消息 ID 和记录的已处理过的消息 ID,来判断当前收到的消息有没有经过处理。如果已经处理过,那么,消费者程序就不再进行处理了。
但是 List 并不会为每个消息生成 ID 号,所以我们需要自行为每个消息生成一个全局唯一ID,生成之后,我们在用 LPUSH 命令把消息插入 List 时,需要在消息中包含这个全局唯一 ID。
例如,我们执行以下命令,就把一条全局 ID 为 111000102、库存量为 99 的消息插入了消息队列:
> LPUSH mq "111000102:stock:99"
(integer) 1
3、如何保证消息可靠性?
当消费者程序从 List 中读取一条消息后,List 就不会再留存这条消息了。所以,如果消费者程序在处理消息的过程出现了故障或宕机,就会导致消息没有处理完成,那么,消费者程序再次启动后,就没法再次从 List 中读取消息了。
为了留存消息,List 类型提供了 BRPOPLPUSH
命令,这个命令的作用是让消费者程序从一个 List 中读取消息,同时,Redis 会把这个消息再插入到另一个 List(可以叫作备份 List)留存。
这样一来,如果消费者程序读了消息但没能正常处理,等它重启后,就可以从备份 List 中重新读取消息并进行处理了。
好了,到这里可以知道基于 List 类型的消息队列,满足消息队列的三大需求(消息保序、处理重复的消息和保证消息可靠性)。
- 消息保序:使用 LPUSH + RPOP;
- 阻塞读取:使用 BRPOP;
- 重复消息处理:生产者自行实现全局唯一 ID;
- 消息的可靠性:使用 BRPOPLPUSH
List 作为消息队列有什么缺陷?
List 不支持多个消费者消费同一条消息,因为一旦消费者拉取一条消息后,这条消息就从 List 中删除了,无法被其它消费者再次消费。
要实现一条消息可以被多个消费者消费,那么就要将多个消费者组成一个消费组,使得多个消费者可以消费同一条消息,但是 List 类型并不支持消费组的实现。
这就要说起 Redis 从 5.0 版本开始提供的 Stream 数据类型了,Stream 同样能够满足消息队列的三大需求,而且它还支持「消费组」形式的消息读取。
Set(集合)
Redis 的 Set 是 String 类型的无序集合。集合成员是唯一的,这就意味着集合中不能出现重复的数据。
Redis 中集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是 O(1)。
集合中最大的成员数为 232 - 1 (4294967295, 每个集合可存储40多亿个成员)。
示例:
> sadd heiyo redis
(integer) 1
> sadd heiyo mysql
(integer) 1
> sadd heiyo golang
(integer) 1
> sadd heiyo golang
(integer) 0
> smembers heiyo
1) "mysql"
2) "golang"
3) "redis"
常用命令
命令 | 注释 | |
---|---|---|
1 | SADD key member1 [member2] | 向集合添加一个或多个成员 |
2 | SCARD key | 获取集合的成员数 |
3 | SDIFF key1 [key2] | 返回第一个集合与其他集合之间的差异 |
4 | SDIFFSTORE destination key1 [key2] | 返回给定所有集合的差集并存储在 destination 中 |
5 | SINTER key1 [key2] | 返回给定所有集合的交集 |
6 | SINTERSTORE destination key1 [key2] | 返回给定所有集合的交集并存储在 destination 中 |
7 | SISMEMBER key member | 判断 member 元素是否是集合 key 的成员 |
8 | SMEMBERS key | 返回集合中的所有成员 |
9 | SMOVE source destination member | 将 member 元素从 source 集合移动到 destination 集合 |
10 | SPOP key | 移除并返回集合中的一个随机元素 |
11 | SRANDMEMBER key [count] | 返回集合中一个或多个随机数 |
12 | SREM key member1 [member2] | 移除集合中一个或多个成员 |
13 | SUNION key1 [key2] | 返回所有给定集合的并集 |
14 | SUNIONSTORE destination key1 [key2] | 所有给定集合的并集存储在 destination 集合中 |
15 | SSCAN key cursor [MATCH pattern] [COUNT count] | 迭代集合中的元素 |
应用场景
点赞
Set 类型可以保证一个用户只能点一个赞,这里举例子一个场景,key 是文章id,value 是用户id。
uid:1
、uid:2
、uid:3
三个用户分别对 article:1 文章点赞了。
# uid:1 用户对文章 article:1 点赞
> SADD article:1 uid:1
(integer) 1
# uid:2 用户对文章 article:1 点赞
> SADD article:1 uid:2
(integer) 1
# uid:3 用户对文章 article:1 点赞
> SADD article:1 uid:3
(integer) 1
uid:1
取消了对 article:1 文章点赞。
> SREM article:1 uid:1
(integer) 1
获取 article:1 文章所有点赞用户 :
> SMEMBERS article:1
1) "uid:3"
2) "uid:2"
获取 article:1 文章的点赞用户数量:
> SCARD article:1
(integer) 2
判断用户 uid:1
是否对文章 article:1 点赞了:
> SISMEMBER article:1 uid:1
(integer) 0 # 返回0说明没点赞,返回1则说明点赞了
共同关注
Set 类型支持交集运算,所以可以用来计算共同关注的好友、公众号等。
key 可以是用户id,value 则是已关注的公众号的id。
uid:1
用户关注公众号 id 为 5、6、7、8、9,uid:2
用户关注公众号 id 为 7、8、9、10、11。
# uid:1 用户关注公众号 id 为 5、6、7、8、9
> SADD uid:1 5 6 7 8 9
(integer) 5
# uid:2 用户关注公众号 id 为 7、8、9、10、11
> SADD uid:2 7 8 9 10 11
(integer) 5
uid:1
和 uid:2
共同关注的公众号:
# 获取共同关注
> SINTER uid:1 uid:2
1) "7"
2) "8"
3) "9"
给 uid:2
推荐 uid:1
关注的公众号:
> SDIFF uid:1 uid:2
1) "5"
2) "6"
验证某个公众号是否同时被 uid:1
或 uid:2
关注:
> SISMEMBER uid:1 5
(integer) 1 # 返回0,说明关注了
> SISMEMBER uid:2 5
(integer) 0 # 返回0,说明没关注
抽奖活动
存储某活动中中奖的用户名 ,Set 类型因为有去重功能,可以保证同一个用户不会中奖两次。
key为抽奖活动名,value为员工名称,把所有员工名称放入抽奖箱 :
>SADD lucky Tom Jerry John Sean Marry Lindy Sary Mark
(integer) 5
如果允许重复中奖,可以使用 SRANDMEMBER 命令。
# 抽取 1 个一等奖:
> SRANDMEMBER lucky 1
1) "Tom"
# 抽取 2 个二等奖:
> SRANDMEMBER lucky 2
1) "Mark"
2) "Jerry"
# 抽取 3 个三等奖:
> SRANDMEMBER lucky 3
1) "Sary"
2) "Tom"
3) "Jerry"
如果不允许重复中奖,可以使用 SPOP 命令。
# 抽取一等奖1个
> SPOP lucky 1
1) "Sary"
# 抽取二等奖2个
> SPOP lucky 2
1) "Jerry"
2) "Mark"
# 抽取三等奖3个
> SPOP lucky 3
1) "John"
2) "Sean"
3) "Lindy"
Sorted set(有序集合)
Redis 有序集合和集合一样也是 string 类型元素的集合,且不允许重复的成员。
不同的是每个元素都会关联一个 double 类型的分数。redis 正是通过分数来为集合中的成员进行从小到大的排序。
有序集合的成员是唯一的,但分数(score)却可以重复。
集合是通过哈希表实现的,所以添加,删除,查找的复杂度都是 O(1)。 集合中最大的成员数为 232 - 1 (4294967295, 每个集合可存储40多亿个成员)。
示例: C++ 输出 5 号是因为:在最后一次操作,执行了更新操作!
> zadd heihei 1 redis
(integer) 1
> zadd heihei 3 java
(integer) 1
> zadd heihei 2 golang
(integer) 1
> zadd heihei 4 c++
(integer) 1
> zadd heihei 4 c++
(integer) 0
> zadd heihei 5 c++
(integer) 0
> zrange heihei 0 10 withscores
1) "redis"
2) 1.0
3) "golang"
4) 2.0
5) "java"
6) 3.0
7) "c++"
8) 5.0
常用命令
命令 | 注释 | |
---|---|---|
1 | zadd key score1 member1 [score2 member2] | 向有序集合添加一个或多个成员,或者更新已存在成员的分数 |
2 | zcard key | 获取有序集合的成员数 |
3 | zcount key min max | 计算在有序集合中指定区间分数的成员数 |
4 | zincrby key increment member | 有序集合中对指定成员的分数加上增量 increment |
5 | zinterstore destination numkeys key [key …] | 计算给定的一个或多个有序集的交集并将结果集存储在新的有序集合 destination 中 |
6 | zlexcount key min max | 在有序集合中计算指定字典区间内成员数量 |
7 | zrange key start stop [WITHSCORES] | 通过索引区间返回有序集合指定区间内的成员 |
8 | zrangebylex key min max [LIMIT offset count] | 通过字典区间返回有序集合的成员 |
9 | zrangebyscore key min max [WITHSCORES] [LIMIT] | 通过分数返回有序集合指定区间内的成员 |
10 | zrank key member | 返回有序集合中指定成员的索引 |
11 | zrem key member [member …] | 移除有序集合中的一个或多个成员 |
12 | zremrangebylex key min max | 移除有序集合中给定的字典区间的所有成员 |
13 | zremrangebyrank key start stop | 移除有序集合中给定的排名区间的所有成员 |
14 | zremrangebyscore key min max | 移除有序集合中给定的分数区间的所有成员 |
15 | zrevrange key start stop [WITHSCORES] | 返回有序集中指定区间内的成员,通过索引,分数从高到低 |
16 | zrevrangebyscore key max min [WITHSCORES] | 返回有序集中指定分数区间内的成员,分数从高到低排序 |
17 | zrevrank key member | 返回有序集合中指定成员的排名,有序集成员按分数值递减(从大到小)排序 |
18 | zscore key member | 返回有序集中,成员的分数值 |
19 | zunionstore destination numkeys key [key …] | 计算给定的一个或多个有序集的并集,并存储在新的 key 中 |
20 | zscan key cursor [MATCH pattern] [COUNT count] | 迭代有序集合中的元素(包括元素成员和元素分值) |
应用场景
排行榜
Sorted Set常被用于实现排行榜功能。例如,可以将用户的分数作为Sorted Set中的分数,将用户ID作为Sorted Set中的成员,然后根据分数对用户进行排名。
时间轴
Sorted Set可以用来构建时间轴,记录和排序事件。例如,可以将事件的发生时间作为Sorted Set中的分数,事件内容作为Sorted Set中的成员,然后可以按照时间顺序获取事件。
带权重的任务队列
Sorted Set可以用来构建带有权重的任务队列。例如,可以将任务的优先级作为Sorted Set中的分数,任务的标识作为Sorted Set中的成员,然后可以按照优先级获取任务。
范围查询
Sorted Set支持按照分数范围进行查询。这使得它在需要按照分数进行范围查询的场景中非常有效。例如,可以使用Sorted Set存储商品的价格,然后根据价格范围查询商品。
去重
Sorted Set中的成员是唯一的,不允许重复。这使得它可以用来进行去重操作。例如,可以使用Sorted Set对一批数据进行去重,只保留唯一的数据。