数据类型与底层数据结构
Redis 数据类型和应用场景
Redis 是一个 Key-Value 的存储系统,使用 ANSI C 语言编写。
key 的类型是字符串。
value 的数据类型有:
- 常用的:string 字符串类型、list 列表类型、set 集合类型、sortedset(zset)有序集合类型、hash 类型。
- 不常见的:bitmap 位图类型、geo 地理位置类型、HyperLogLog 基数统计
- Redis 5.0 新增一种:stream 类型
注意:Redis 中命令忽略大小写,(set SET),key 是不忽略大小写的 (NAME name)
Redis 的 Key 的设计
- 用
:
分割 - 把表名转换为 key 前缀,例如:
user:
- 第二段放置主键值
- 第三段放置列名
比如:用户表 user , 转换为 Redis 的 key-value 存储
userid | username | password | |
---|---|---|---|
9 | zhangf | 111111 | zhangf@lagou.com |
username 的 key: user:9:username
{userid:9, username:zhangf}
email 的 key:user:9:email
-
表意明确:看 key 知道意思
-
不易被覆盖
string 字符串类型
Redis 的 String
能表达 3 种值的类型:字符串、整数、浮点数,100.01
是个六位的字符串
常见操作命令如下表:
命令 | 示例 | 描述 |
---|---|---|
set |
set key value |
赋值 |
get |
get key |
取值 |
getset |
getset key value |
取值并赋值 |
setnx |
setnx key value |
当 value 不存在时赋值, set key value NX PX 3000 原子操作,px 设置毫秒数 |
append |
append key value |
向尾部追加值 |
strlen |
strlen key |
获取字符串长度 |
incr |
incr key |
递增数字 |
incrby |
incrby key increment |
增加指定的整数 |
decr |
decr key |
递减数字 |
decrby |
decrby key decrement |
减少指定的整数 |
应用场景:
-
key 和命令是字符串
-
普通的赋值
-
incr
用于乐观锁incr
:递增数字,可用于实现乐观锁watch
(事务) -
setnx
用于分布式锁当 value 不存在时采用赋值,可用于实现分布式锁
举例:
# setnx
127.0.0.1:6379> setnx name zhangf #如果name不存在赋值
(integer) 1
127.0.0.1:6379> setnx name zhaoyun #再次赋值失败
(integer) 0
127.0.0.1:6379> get name
"zhangf"
# set
127.0.0.1:6379> set age 18 NX PX 10000 #如果不存在赋值 有效期10秒
OK
127.0.0.1:6379> set age 20 NX #赋值失败
(nil)
127.0.0.1:6379> get age #age失效
(nil)
127.0.0.1:6379> set age 30 NX PX 10000 #赋值成功
OK
127.0.0.1:6379> get age
"30"
list 列表类型
- list 列表类型可以存储有序、可重复的元素
- 获取头部或尾部附近的记录是极快的
- list 的元素个数最多为
2^32-1
个(40亿) - 应用场景:
- 因为其有序性,可以作为栈或队列使用
- 可用于各种列表,比如用户列表、商品列表、评论列表等
常见操作命令如下表:
命令名称 | 命令格式 | 描述 |
---|---|---|
lpush |
lpush key v1 v2 v3 ... |
从左侧插入列表 |
lpop |
lpop key |
从列表左侧取出 |
rpush |
rpush key v1 v2 v3 ... |
从右侧插入列表 |
rpop |
rpop key |
从列表右侧取出 |
lpushx |
lpushx key value |
将值插入到列表头部 |
rpushx |
rpushx key value |
将值插入到列表尾部 |
blpop |
blpop key timeout |
从列表左侧取出,当列表为空时阻塞,可以设置最大阻塞时 间,单位为秒 |
brpop |
blpop key timeout |
从列表右侧取出,当列表为空时阻塞,可以设置最大阻塞时 间,单位为秒 |
llen |
llen key |
获得列表中元素个数 |
lindex |
lindex key index |
获得列表中下标为 index 的元素,index 从 0 开始 |
lrange |
lrange key start end |
返回列表中指定区间的元素,区间通过 start 和 end 指定 |
lrem |
lrem key count value |
删除列表中与 value 相等的元素,当 count > 0 时, lrem 会从列表左边开始删除;当 count < 0 时, lrem 会从列表后边开始删除;当 count = 0 时, lrem 删除所有值为 value 的元素 |
lset |
lset key index value |
将列表 index 位置的元素设置成 value 的值 |
ltrim |
ltrim key start end |
对列表进行修剪,只保留 start 到 end 区间 |
rpoplpush |
rpoplpush key1 key2 |
从 key1 列表右侧弹出并插入到 key2 列表左侧 |
brpoplpush |
brpoplpush key1 key2 |
从 key1 列表右侧弹出并插入到 key2 列表左侧,会阻塞 |
linsert |
linsert key BEFORE/AFTER pivot value |
将 value 插入到列表,且位于值 pivot 之前或之后 |
127.0.0.1:6379> lpush list:1 1 2 3 4 5 3
(integer) 5
127.0.0.1:6379> lrange list:1 0 -1
1) "5"
2) "4"
3) "3"
4) "2"
5) "1"
127.0.0.1:6379> lpop list:1 # 从0开始
"5"
127.0.0.1:6379> rpop list:1
"1"
127.0.0.1:6379> lindex list:1 1
"3"
127.0.0.1:6379> lrange list:1 0 -1
1) "4"
2) "3"
3) "2"
127.0.0.1:6379> lindex list:1 1
"3"
127.0.0.1:6379> rpoplpush list:1 list:2
"2"
127.0.0.1:6379> lrange list:2 0 -1
1) "2"
127.0.0.1:6379> lrange list:1 0 -1
1) "4"
2) "3"
set 集合类型
- Set :无序、唯一元素
- 集合中最大的成员数为 2^32 - 1
- 应用场景:
- 适用于不能重复的且不需要顺序的数据结构,比如:关注的用户,还可以通过
spop
进行随机抽奖
- 适用于不能重复的且不需要顺序的数据结构,比如:关注的用户,还可以通过
常见操作命令如下表:
命令名称 | 命令格式 | 描述 |
---|---|---|
sadd |
sadd key mem1 mem2 .... |
为集合添加新成员 |
srem |
srem key mem1 mem2 .... |
删除集合中指定成员 |
smembers |
smembers key |
获得集合中所有元素 |
spop |
spop key |
返回集合中一个随机元素,并将该元素删除 |
srandmember |
srandmember key |
返回集合中一个随机元素,不会删除该元素 |
scard |
scard key |
获得集合中元素的数量 |
sismember |
sismember key member |
判断元素是否在集合内 |
sinter |
sinter key1 key2 key3 |
求多集合的交集 |
sdiff |
sdiff key1 key2 key3 |
求多集合的差集 |
sunion |
sunion key1 key2 key3 |
求多集合的并集 |
127.0.0.1:6379> sadd set:1 a b c d
(integer) 4
127.0.0.1:6379> smembers set:1
1) "d"
2) "b"
3) "a"
4) "c"
127.0.0.1:6379> srandmember set:1
"c"
127.0.0.1:6379> srandmember set:1
"b"
127.0.0.1:6379> sadd set:2 b c r f
(integer) 4
127.0.0.1:6379> sinter set:1 set:2
1) "b"
2) "c"
127.0.0.1:6379> spop set:1
"d"
127.0.0.1:6379> smembers set:1
1) "b"
2) "a"
3) "c
sortedset 有序集合类型
- SortedSet ( ZSet ) 有序集合: 元素本身是无序不重复的
- 每个元素关联一个分数( score )
- 可按分数排序,分数可重复
- 应用场景:
- 由于可以按照分值排序,所以适用于各种 排行榜。比如:点击排行榜、销量排行榜、关注排行榜等
常见操作命令如下表:
命令名称 | 命令格式 | 描述 |
---|---|---|
zadd |
zadd key score1 member1 score2 member2 ... |
为有序集合添加新成员 |
zrem |
zrem key mem1 mem2 .... |
删除有序集合中指定成员 |
zcard |
zcard key |
获得有序集合中的元素数量 |
zcount |
zcount key min max |
返回集合中 score 值在 [min, max] 区间 的元素数量 |
zincrby |
zincrby key increment member |
在集合的 member 分值上加 increment |
zscore |
zscore key member |
获得集合中 member 的分值 |
zrank |
zrank key member |
获得集合中 member 的排名(按分值 从小到大) |
zrevrank |
zrevrank key member |
获得集合中 member 的排名(按分值从大到小) |
zrange |
zrange key start end |
获得集合中指定区间成员,按分数递增排序 |
zrevrange |
zrevrange key start end |
获得集合中指定区间成员,按分数递减排序 |
127.0.0.1:6379> zadd hit:1 100 item1 20 item2 45 item3
(integer) 3
127.0.0.1:6379> zcard hit:1
(integer) 3
127.0.0.1:6379> zscore hit:1 item3
"45"
127.0.0.1:6379> zrevrange hit:1 0 -1
1) "item1"
2) "item3"
3) "item2"
hash 类型(散列表)
- Redis hash 是一个 string 类型的 field 和 value 的映射表,它提供了字段和字段值的映射
- 每个 hash 可以存储 2^32 - 1 键值对(40多亿)
- 应用场景:对象的存储,表数据的映射
常见操作命令如下表:
命令名称 | 命令格式 | 描述 |
---|---|---|
hset |
hset key field value |
赋值,不区别新增或修改 |
hmset |
hmset key field1 value1 field2 value2 |
批量赋值 |
hsetnx |
hsetnx key field value |
赋值,如果 filed 存在则不操作 |
hexists |
hexists key filed |
查看某个 field 是否存在 |
hget |
hget key field |
获取一个字段值 |
hmget |
hmget key field1 field2 ... |
获取多个字段值 |
hgetall |
hgetall key |
获取所有字段值 |
hdel |
hdel key field1 field2... |
删除指定字段 |
hincrby |
hincrby key field increment |
指定字段自增 increment |
hlen |
hlen key |
获得字段数量 |
127.0.0.1:6379> hmset user:001 username zhangfei password 111 age 23 sex M
OK
127.0.0.1:6379> hgetall user:001
1) "username"
2) "zhangfei"
3) "password"
4) "111"
5) "age"
6) "23"
7) "sex"
8) "M"
127.0.0.1:6379> hget user:001 username
"zhangfei"
127.0.0.1:6379> hincrby user:001 age 1
(integer) 24
127.0.0.1:6379> hlen user:001
(integer) 4
bitmap 位图类型
- bitmap 是进行位操作的
- 通过一个 bit 位来表示某个元素对应的值或者状态,其中的 key 就是对应元素本身
- bitmap 本身会极大的节省储存空间
- 应用场景:
- 用户每月签到,用户 id 为 key, 日期作为偏移量 1 表示签到
- 统计活跃用户, 日期为 key ,用户 id 为偏移量 1 表示活跃
- 查询用户在线状态, 日期为 key ,用户 id 为偏移量 1 表示在线
常见操作命令如下表:
命令名 称 | 命令格式 | 描述 |
---|---|---|
setbit |
setbit key offset value |
设置 key 在 offset 处的 bit 值(只能是 0 或者 1 ) |
getbit |
getbit key offset |
获得 key 在 offset 处的 bit 值 |
bitcount |
bitcount key |
获得 key 的 bit 位为 1 的个数 |
bitpos |
bitpos key value |
返回第一个被设置为 bit 值的索引值 |
bitop |
bitop and[or/xor/not] destkey key [key …] |
对多个 key 进行逻辑运算后存入 destkey 中 |
127.0.0.1:6379> setbit user:sign:1000 20200101 1 #id为1000的用户20200101签到
(integer) 0
127.0.0.1:6379> setbit user:sign:1000 20200103 1 #id为1000的用户20200103签到
(integer) 0
127.0.0.1:6379> getbit user:sign:1000 20200101 #获得id为1000的用户20200101签到状态,1 表示签到
(integer) 1
127.0.0.1:6379> getbit user:sign:1000 20200102 #获得id为1000的用户20200102签到状态,0 表示未签到
(integer) 0
127.0.0.1:6379> bitcount user:sign:1000 # 获得id为1000的用户签到次数
(integer) 2
127.0.0.1:6379> bitpos user:sign:1000 1 #id为1000的用户第一次签到的日期
(integer) 20200101
127.0.0.1:6379> setbit 20200201 1000 1 #20200201的1000号用户上线
(integer) 0
127.0.0.1:6379> setbit 20200202 1001 1 #20200202的1000号用户上线
(integer) 0
127.0.0.1:6379> setbit 20200201 1002 1 #20200201的1002号用户上线
(integer) 0
127.0.0.1:6379> bitcount 20200201 #20200201的上线用户有2个
(integer) 2
127.0.0.1:6379> bitop or desk1 20200201 20200202 #合并20200201的用户和20200202上线了的用户
(integer) 126
127.0.0.1:6379> bitcount desk1 #统计20200201和20200202都上线的用户个数
(integer) 3
geo 地理位置类型
- geo 是 Redis 用来处理位置信息的。
- 在 Redis3.2 中正式使用。
- 主要是利用了 Z 阶曲线、 Base32 编码和 geohash 算法
- 应用场景:
- 记录地理位置
- 计算距离
- 查找 "附近的人"
常见操作命令如下表:
命令名称 | 命令格式 | 描述 |
---|---|---|
geoadd |
geoadd key 经度 纬度 成员名称1 经度1 纬度1 成 员名称2 经度2 纬度 2 ... |
添加地理坐标 |
geohash |
geohash key 成员名称1 成员名称2... |
返回标准的 geohash 串 |
geopos |
geopos key 成员名称1 成员名称2... |
返回成员经纬度 |
geodist |
geodist key 成员1 成员2 单位 |
计算成员间距离 |
georadiusbymember |
georadiusbymember key 成员 值单位 count 数 asc[desc] |
根据成员查找附近的成员 |
127.0.0.1:6379> geoadd user:addr 116.31 40.05 zhangf 116.38 39.88 zhaoyun 116.47 40.00 diaochan #添加用户地址 zhangf、zhaoyun、diaochan的经纬度
(integer) 3
127.0.0.1:6379> geohash user:addr zhangf diaochan #获得zhangf和diaochan的geohash码
1) "wx4eydyk5m0"
2) "wx4gd3fbgs0"
127.0.0.1:6379> geopos user:addr zhaoyun #获得zhaoyun的经纬度
1) 1) "116.38000041246414185"
2) "39.88000114172373145"
127.0.0.1:6379> geodist user:addr zhangf diaochan #计算zhangf到diaochan的距离,单位是m
"14718.6972"
127.0.0.1:6379> geodist user:addr zhangf diaochan km #计算zhangf到diaochan的距离,单位是km
"14.7187"
127.0.0.1:6379> geodist user:addr zhangf zhaoyun km
"19.8276"
127.0.0.1:6379> georadiusbymember user:addr zhangf 20 km withcoord withdist count 3 asc
# 获得距离zhangf20km以内的按由近到远的顺序排出前三名的成员名称、距离及经纬度
#withcoord : 获得经纬度 withdist:获得距离 withhash:获得geohash码
1) 1) "zhangf"
2) "0.0000"
3) 1) "116.31000012159347534"
2) "40.04999982043828055"
2) 1) "diaochan"
2) "14.7187"
3) 1) "116.46999925374984741"
2) "39.99999991084916218"
3) 1) "zhaoyun"
2) "19.8276"
3) 1) "116.38000041246414185"
2) "39.88000114172373145"
Z 阶曲线
在 x 轴和 y 轴上将十进制数转化为二进制数,采用 x 轴和 y 轴对应的二进制数依次交叉后得到一个六位数编码。把数字从小到大依次连起来的曲线称为 Z 阶曲线, Z 阶曲线是把多维转换成一维的一种方法。
Base32 编码
- Base32 这种数据编码机制,主要 用来把二进制数据编码成可见的字符串,其编码规则是:任意给定一个二进制数据,以 5 个位( bit )为一组进行切分( base64 以 6 个位( bit )为一组),对切分而成的每个组进行编码得到 1 个可见字符。
- Base32 编码表字符集中的字符总数为 32 个( 0-9 、 b-z 去掉 a 、 i 、 l 、 o ),这也是 Base32 名字的由来。
geohash 算法
Gustavo 在 2008 年 2 月上线了 geohash.org 网站。 Geohash 是一种地理位置信息编码方法。 经过 geohash 映射后,地球上任意位置的经纬度坐标可以表示成一个较短的字符串。可以方便的存储在数据库中,附在邮件上,以及方便的使用在其他服务中。以北京的坐标举例,[39.928167, 116.389550]
可以转换成 wx4g0s8q3jf9
。
Redis 中经纬度使用 52 位的整数进行编码,放进 zset 中, zset 的 value 元素是 key , score 是 GeoHash 的 52 位整数值。在使用 Redis 进行 Geo 查询时,其内部对应的操作其实只是 zset ( skiplist )的操作。通过 zset 的 score 进行排序就可以得到坐标附近的其它元素,通过将 score 还原成坐标值就可以得到元素的原始坐标。
stream 数据流类型
stream 是 Redis5.0 后新增的数据结构,用于可持久化的消息队列。
几乎满足了消息队列具备的全部内容,包括:
- 消息 ID 的序列化生成
- 消息遍历
- 消息的阻塞和非阻塞读取
- 消息的分组消费
- 未完成消息的处理
- 消息队列监控
每个 Stream 都有唯一的名称,它就是 Redis 的 key ,首次使用 xadd 指令追加消息时自动创建。
常见操作命令如下表:
命令名称 | 命令格式 | 描述 |
---|---|---|
xadd |
xadd key id <*> field1 value1.... |
将指定消息数据追加到指定队列( key )中,* 表示最新生成的 id (当前时间 + 序列号) |
xread |
xread [COUNT count] [BLOCK milliseconds] STREAMS key [key ...] ID [ID ...] |
从消息队列中读取消息 COUNT:读取条数 BLOCK:阻塞读(默认不阻塞) key:队列 名称 ID:消息id |
xrange |
xrange key start end [COUNT] |
读取队列中给定 ID 范围的消息 COUNT:返 回消息条数(消息 id 从小到大) |
xrevrange |
xrevrange key start end [COUNT] |
读取队列中给定 ID 范围的消息 COUNT:返 回消息条数(消息 id 从大到小) |
xdel |
xdel key id |
删除队列的消息 |
xgroup |
xgroup create key groupname id |
创建一个新的消费组 |
xgroup |
xgroup destory key groupname |
删除指定消费组 |
xgroup |
xgroup delconsumer key groupname cname |
删除指定消费组中的某个消费者 |
xgroup |
xgroup setid key id |
修改指定消息的最大 id |
xreadgroup |
xreadgroup group groupname consumer COUNT streams key |
从队列中的消费组中创建消费者并消费数据 (consumer 不存在则创建) |
127.0.0.1:6379> xadd topic:001 * name zhangfei age 23
"1591151905088-0"
127.0.0.1:6379> xadd topic:001 * name zhaoyun age 24 name diaochan age 16
"1591151912113-0"
127.0.0.1:6379> xrange topic:001 - +
1) 1) "1591151905088-0"
2) 1) "name"
2) "zhangfei"
3) "age"
4) "23"
2) 1) "1591151912113-0"
2) 1) "name"
2) "zhaoyun"
3) "age"
4) "24"
5) "name"
6) "diaochan"
7) "age"
8) "16"
127.0.0.1:6379> xread COUNT 1 streams topic:001 0
1) 1) "topic:001"
2) 1) 1) "1591151905088-0"
2) 1) "name"
2) "zhangfei"
3) "age"
4) "23"
#创建的group1
127.0.0.1:6379> xgroup create topic:001 group1 0
OK
# 创建cus1加入到group1 消费 没有被消费过的消息 消费第一条
127.0.0.1:6379> xreadgroup group group1 cus1 count 1 streams topic:001 >
1) 1) "topic:001"
2) 1) 1) "1591151905088-0"
2) 1) "name"
2) "zhangfei"
3) "age"
4) "23"
#继续消费 第二条
127.0.0.1:6379> xreadgroup group group1 cus1 count 1 streams topic:001 >
1) 1) "topic:001"
2) 1) 1) "1591151912113-0"
2) 1) "name"
2) "zhaoyun"
3) "age"
4) "24"
5) "name"
6) "diaochan"
7) "age"
8) "16"
#没有可消费
127.0.0.1:6379> xreadgroup group group1 cus1 count 1 streams topic:001 >
(nil)
底层数据结构
Redis 作为 Key-Value 存储系统,数据结构如下:
- Redis 没有表的概念, Redis 实例所对应的 db 以编号区分, db 本身就是 key 的命名空间。
- 比如:
user:1000
作为 key 值,表示在 user 这个命名空间下 id 为 1000 的元素,类似于 user 表的 id = 1000 的行
RedisDB 结构
- Redis 中存在 数据库 的概念,该结构由
redis.h
中的redisDb
定义。 - 当 Redis 服务器初始化时,会预先分配
16
个数据库 - 所有数据库保存到结构
redisServer
的一个成员redisServer.db
数组中 redisClient
中存在一个名叫db
的指针指向当前使用的数据库
RedisDB 结构体源码:
typedef struct redisDb {
int id; //id是数据库序号,为0-15(默认Redis有16个数据库)
long avg_ttl; //存储的数据库对象的平均ttl(time to live),用于统计
dict *dict; //存储数据库所有的key-value
dict *expires; //存储key的过期时间
dict *blocking_keys; //blpop ,存储阻塞key和客户端对象
dict *ready_keys; //阻塞后 push 响应阻塞客户端,存储阻塞后push的key和客户端对象
dict *watched_keys; //存储watch监控的的key和客户端对象
} redisDb;
RedisObject 结构
- 其 Value 是一个对象,包含字符串对象,列表对象,哈希对象,集合对象和有序集合对象
typedef struct redisObject {
unsigned type:4; //类型,五种对象类型:REDIS_STRING (字符串)、 REDIS_LIST (列表)、 REDIS_HASH (哈希)、 REDIS_SET (集合)、 REDIS_ZSET (有序集合)。
unsigned encoding:4; //对象的内部编码
void *ptr; //指向底层实现数据结构的指针,指向具体的数据
//...
int refcount; //引用计数,主要在于对象的引用计数和内存回收
//...
unsigned lru:LRU_BITS; //LRU_BITS为24bit,记录对象最后一次被命令程序访问的时间,高16位存储一个分钟数级别的时间戳,低8位存储访问计数(lfu : 最近访问次数)
//...
}robj;
# 执行 type 命令时,便是通过读取 RedisObject 的 type 字段获得对象的类型
127.0.0.1:6379> type a1
string
# 通过 object encoding 命令,可以查看对象采用的编码方式
127.0.0.1:6379> object encoding a1
"int"
7 种 type
字符串对象
- C语言: 字符数组 " "
- Redis 使用 SDS ( Simple Dynamic String ) 存储字符串和整型数据。
struct sdshdr{
//记录buf数组中已使用字节的数量
int len;
//记录 buf 数组中未使用字节的数量
int free;
//字符数组,用于保存字符串,buf[] 的长度=len+free+1
char buf[];
}
SDS 的优势:
- SDS 在 C 字符串的基础上加入了 free 和 len 字段,获取字符串长度:SDS 是 O(1),C 字符串是
O(n)。buf 数组的长度=free+len+1 - SDS 由于记录了长度,在可能造成缓冲区溢出时会自动重新分配内存,杜绝了缓冲区溢出。
- 可以存取二进制数据,以字符串长度 len 来作为结束标识
- C: