Redis 中的数据结构

1. 预备知识

1.1 全局命令

keys *             查询所有的键,会遍历所有的键
dbsize             键总数,查询Redis内置键数量的变量
exists key         查询key是否存在
expire key seconds 设置键的存活时间
ttl key 查看键的剩余过期时间,返回值:大于0的整数,剩余过期时间;-1时,未设置过期时间;-2时,键不存在
type key           键的数据类型,键若不存在,返回null

1.2 数据结构和内部编码
Redis中每个数据类型都有多种内部编码实现如下图所示,
在这里插入图片描述
这样做有2个好处:

1)可以不断改进内部实现,而对外部的数据命令没有影响;
2)多种内部编码实现可以在不同场景下发挥各自的优势;

1.3单线程架构

Redis使用单线程架构,当多个客户端访问服务端时,将他们依次放入等待队列中,依次执行客户端的命令并返回结果。

Question:为什么单线程还这么快?

1)纯内存访问。数据都放在内存中;
2)非阻塞I/O。Redis使用epoll作为多路复用计数的实现,再加上Redis自身的事件处理
模型将  epoll中的链接,读写,关闭都转为事件,不在网络I/O上浪费太多时间;
3)单线程避免了线程切换和竞态产生的消耗。

2.字符串

字符串是Redis中最基本的数据类型,其他几种数据类型都是在string类型上构建的。字符串类型的值可以是字符串,数字,或者是二进制,但是大小不能超过512M。

2.1 命令

2.1.1 常用命令

1)设置值

set key value [ex seconds] [px milliseconds] [nx|xx] 
例:set hello world ex 10 nx

ex: 为键设置秒级过期时间
px: 为键设置毫秒级过期时间
nx: 键必须不存在,才可以设置成功,用于添加
xx: 键必须存在,才可以设置成功,用于修改

类似的指令有:setnx和setex,用法如下:
setnx hello world  键必须不存在,才可以设置成功
setex hello world  键必须存在,才可以设置成功

对于setnx命令来说,只有一个客户端才能设置成功,所以可以用它实现分布式锁。

2)获取值

get key 获取值
例:get hello 
如果获取的键不存在,返回 nil(空)

3)批量设置值

mset key value [key value ...]
例:mset a 1 b 2 c 3 d 4

4)批量获取值

mget key [...]
例:mget a b c d
如果获取的键不存在,返回 nil(空)

5)值自增

incr key
例:incr a

返回值有3中情况:
不是整数,返回错误;
是整数,返回自增后的结果;
键不存在,按照0自增,返回结果1;

类似的命令还有:decr(自减),incrby(自增指定数字),decrby(自减指定数字),
             incrbyfloat(自增浮点数)

2.1.2 不常用命令

(1)追加值

append key value  向字符串尾部追加值
例:append hello java

(2)字符串长度

strlen key key占用的字节数
注意:每个汉字占3个字节

(3)设置并返回原值

getset key value 

(4)设置指定位置的字符

setrange key offset value
例:setrange hello 0 t   将hello对应的值得第一个字符变为t

(5)获取部分字符串

getrange key start end   获取从start位置到end位置的字符
例:getrange hello 0 3   

2.2 内部编码

字符串的内部编码有3种:

1. int:    8个字节的长整形
2. embstr: 小于等于39个字节的字符串
3. raw:   大于39个字节的字符串

Redis 会根据当前值得类型和长度决定使用哪种内部编码实现。

2.3 使用场景

(1)缓存功能

用户访问Web服务,先从缓存层(Redis)中请求目标数据,如果缓存层(Redis)中有目标数据,则直接返回数据,如果没有,则从存储层(MySQL)中获取数据,然后返回数据并将数据写入缓存层(Redis)在这里插入图片描述

(2)计数。如视频的播放量等。

(3)共享session。

分布式web服务器将用户的信息(登录信息)保存在各自的服务器中, 而用户刷新后,由于负载均衡的考虑,负载均衡服务器有可能将用户请求映射到不同的服务器上,这样可能导致用户重新登录。这个问题是用户无法忍受的。

解决方法:将用户的信息(登录信息)集中保存在Redis中进行管理,用户每次更新或者查询信息直接从Redis中取集中获取。

(4)限速

为了限制短信接口不被频繁访问,比如限制用户一分钟内验证码最多可以输入5次。实际上是执行了下面命令:

set key value ex 60 nx

3.哈希

Redis中,哈希类型键值本身又是一个键值对结构。哈希类型中的映射关系叫做field-value,这里的value是指field对应的值,并不是键对应的值。
在这里插入图片描述
3.1 命令

(1)设置值

hset key field value  
例:hset user:1 name tom
还有类似的 hsetnx命令

(2)获取值

hget key field 
例:hget user:1 name
如果field不存在,返回nil

(3)删除field

hdel key field [field ...]
例:hdel user:1 name

(4)计算field个数

hlen key 
例:hlen user:1

(5)批量设置或获取field-value

hmget key field [field ...]
例子:hmset user:1 name mike age 12 city tianjin
hmset key field value [field value ...]
例子:hmget user:1 name city

(6)判断field是否存在

hexists key field 
例子:hexists user:1 name

(7)获取所有field

hkeys key
例子:hkeys user:1

(8)获取所有value

hvals key
例子:hvals user:1 

(9)获取所有的field-value

hgetall key
例: hgetall user:1

(10)hincrby hincrbyfloat

hincrby key field
hincrbyfloat key field

(11)计算value的字符长度

hstrlen key field
例:hstrlen user:1 name

3.2 内部编码

ziplist(压缩列表)
当哈希类型元素个数小于hash-max-ziplist-entries配置(默认512个),同时所有值都小于hash-max-ziplist-value配置(默认64字节)时,Redis会使用ziplist作为哈希的内部实现,ziplist使用更加紧凑的结构实现多个元素的连续存储,所以比hashtable更节省内存。

hashtabe(哈希表)
当哈希类型无法满足ziplist条件时,Redis会使用hashtable作为内部实现。

3.3使用场景
使用hash类型存储用户登录数据。相对于字符串序列化缓存用户数据,hash类型更加直观,修改更加便捷。
在这里插入图片描述

hash类型是稀疏的,而关系型数据库是完全结构化的,例如hash类型每个键可以有不同的field,而关系型数据库添加的列,都要为他添加数据。

缓存用户数据的3种方式:

1)原生字符串类型:每个属性一个键。

set user:1 name Tom
set user:1 age 34
set user:1 sex male

优点:简单直观,每个属性都支持更新操作。
缺点:占用过多的键,占用内存较大,同时用户信息内聚性较差.

2)序列化字符串类型

set user:1 serialize(userInfo)

优点:简化编程,如果合理的使用序列化可以提高内存的使用效率。
缺点:序列化和反序列化有一定的开销;每次更新都要把所有数据取出来进行反 序列化,更新完成后再进行序列化到Redis中。

3)哈希类型:每个用户属性使用一对field-value,但是只用一个键保存。

hset user:1 name tom age 12 sex m

优点:简单直观,使用合理可以减少内存的使用。
缺点:要控制hash在ziplist和hashtable中切换,hashtable会消耗更多内存。

4.列表

列表是用来存储多个有序字符串,一个列表最多可以存储2^32-1个元素,可以在两端进行插入和删除,还可以获取指定范围内的元素,指定位置的元素,可以充当栈和队列的角色。

列表中的元素是有序的,且是可重复的。

4.1 命令

  1. 添加操作
    (1)从右边插入元素

    rpush key value [value ...]
    例: rpush MyList 1 2 3 
    可用 lrange 0 -1 来获取列表中的所有元素
    例:lrange MyList 0 -1 
    

    (2)从左边插入元素 lpush key

    value [value ...] 
    例:lpush MyList 1 2 3 
    可用 lrange 0 -1 来获取列表中的所有元素
    例:lrange MyList 0 -1 (3)向某个元素前或者后插入元素 linsert key before|after pivot value 例:linsert MyList before b 100 在MyList中的b元素之前插入100
    
  2. 查找操作
    (1)获取指定范围内的元素列表

    lrange key start end 
    

    (2)获取列表指定索引下标的元素

    lindex key index 
    例:lindex MyList 2 
    

    (3)获取列表长度

    llen key 
    例:llen MyList 
    
  3. 删除操作

    (1)从列表左侧弹出元素

    lpop key 
    例:lpop MyList 
    

    (2)从列表右侧弹出元素

    rpop key 
    例:rpop MyList
    

    (3)删除指定元素

    lrem key count value
    count > 0  从左到右,删除最多count个元素;
    count < 0  从右到左,删除最多count绝对值个元素;
    count = 0  删除所有元素。
    例:lrem MyList 2  a
    

    (4)按照索引范围修剪列表

    ltrim key start end
    例:ltrim MyList 1 3  只保留列表中第2到第4个元素
    
  4. 修改

    lset key index newvalue
    例:lset MyList 2 q
    
  5. 阻塞操作

    阻塞式弹出操作:
    blpop key [key …] timeout
    brpop key [key …] timeout
    1)列表为空

    例: blpop  MyList: test 3   列表空,3秒后返回
        blpop  MyList: test 0   列表空,阻塞,直到添加了数据后返回
    

    2)列表不为空

    blpop  MyList: test 3  客户端立即返回
    

    注意:
    如果有多个键,那么brpop从左到右遍历键,一旦 有一个键能弹出元素,返回客户端;
    如果多个客户端对同一个键执行brpop,那么最先执行brpop命令的客户端可以获取到弹出的值。

4.2 内部编码

  • ziplist
  • linkedlist

4.3 使用场景

  1. 消息队列
    Redis的 lpush + brpop可以实现阻塞队列,生产者从队列左边添加数据,消费者从右边阻塞式的取出数据。
    在这里插入图片描述

  2. 文章列表

    每篇文章使用哈希结构存储,如下:

    hmset acticle:1 title xx timestamp 1476536196 content xxxx
    ...
    hmset acticle:n title yy timestamp 1476536198 content yyyy
    

    向用户文章列表添加文章,user:{id}:articles作为文章的键:

    lpush user :1:articles article:1 article3
    ...
    lpush user: k:articles article: 5
    

    分页获取文章列表:

    articles = lrange user:1:articles 0 9
    for article in {articles}
    hgetall{article}
    

缺点:
每次分页获取的文章个数较多,需要多次执行hgetall操作,可以考虑使用Pipeline批量获取;
分页获取文章列表时,lrange命令在列表两端性能较好,但是如果列表较大,获取列表中间范围的元素性能较差,可以考虑将表做二级拆分或者使用quicklist内部编码实现。
注意:

lpush + lpop = Stack(栈)
lpush + rpop = Queue(队列)
lpush + brpop = Message Queue(消息队列) 

5.集合

集合也是用来保存多个字符串元素,他不允许元素重复,且元素无序,Redis支持集合的增删改查,还支持集合的交集,并集,差集。
5.1 命令

集合内操作

(1)添加元素

sadd key element [element ...]
返回结果为添加成功的元素个数

(2)删除 元素

srem key element[element ...]

(3)计算元素个数

scard key  直接访问内部元素个数变量,复杂度为O(1)

(4)判断元素是否在集合中

sismember key element  在集合中返回1,否则返回0

(5)随机从集合中返回指定个元素

srandmember key [count] 不写count默认为1,执行后元素还存储在集合

(6)从集合随机弹出元素

spop key 执行后元素从集合删除

(7)获取所有集合元素

smembers key

集合间操作

先设置两个集合:

sadd user:1:follow it music his sports
sadd user:2:follow it news ent sports

(1)集合的交集

sinter user:1:follow user:2:follow

(2)集合的并集

sunion user:1:follow user:2:follow

(3)集合的差集

sdiff user:1:follow user:2:follow

(4)保存并交差的结果

sinterstore user:1_2:inter user:1:follow user:2:follow
sunionstore user:1_2:union user:1:follow user:2:follow

5.2 内部编码

intset:元素个数较少且为整数时,用intset编码;
hashtable:元素个数超过512个,或者某个元素不是整数时,内部编码变为hashtable。

5.3 使用场景

典型的使用场景是标签。比如用户可能对音乐,运动等感兴趣,系统可以根据这些给用户推荐相关信息。

  1. 给用户添加标签
sadd user:1:tags tag1 tag2 tag3
sadd user:2:tags tag2 tag3 tag5
  1. 给标签添加用户
sadd tag1:users user:1
sadd tag2:users user:1 user:2
  1. 删除用户下的标签
srem user:1:tags tag1 tag2
  1. 删除标签下的用户
srem tag1:users:user:1

3和4尽量放在一个事务中进行

  1. 计算用户感兴趣的标签
sinter user:1:tags user:2:tags

6.有序集合

有序集合和集合类似,它可以根据元素的分数为元素排序,分数可以重复。

6.1 命令

集合内命令

(1)添加成员

zadd user:ranking 251 tom
zadd user:ranking 1 kris 91 mike 200 frank 220 tim 250 martin

(2)计算成员个数

zcard user:ranking

(3)计算某个成员的分数

zscore user:ranking tom

(4)计算某个成员的排名

zrank user:ranking tom      由低到高排名
zrevrank user:ranking tom   由高到低排名

(5)删除成员

zrem user:ranking tom

(6)增加成员的分数

zincrby user:ranking 9 tom 

(7)返回指定排名范围的成员

zrange user:ranking 0 2 withscores  从低到高排名,并返回分数
zrevrange user:ranking 0 2 withscores 从高到低排名,并返回分数

(8)返回指定分数范围的成员

zrangebyscore user:ranking 200 thif withscores 
zrevrangescore user:ranking 221 200 withscores
zrangebyscore user:ranking (200 +inf withscores
+inf和-inf 分别表示无限小和无限大

(9)返回指定分数范围的成员个数

zcount user:ranking 200 221

(10)删除指定排序内的升序元素

zremrangebyrank user:ranking 0 2

(11)删除指定分数范围的成员

zremrangebyrank user:ranking 0 2

集合间命令

(1)交集

zinterstore user:ranking:1_inter_2 2 user:ranking:1 user:ranking:2
zinterstore user:ranking: 1_inter_2 2 user:ranking:1 user:ranking:2 weights 1 0.5 aggregate max

(2)并集

zunionstore user:ranking:1_union_2 2 user:ranking:1 user:ranking:2
6.2 内部编码

ziplist(压缩列表):元素个数较少且每个元素较小时,内部编码为skiplist;
skiplist(跳跃表):元素个数超过128个,内部编码变为skiplist;
hashtable:当某个元素大于64字节时,内部编码变为hashtable。

6.3 使用场景

排行榜系统。如对用户上传的视频做排行榜。

添加用户的赞数

zadd user:ranking:2019_04_12 mike 3
zincrby user:ranking:2019_05_04 mike 2

取消用户的赞数

zrem user:ranking:2019_04_12 mike

展示获取赞数最多的10个用户

zrevrangebyrank  user:ranking:2019_05_04 0 9

展示用户信息以及用户分数

hgetall user:info tom
zscore user:ranking:2020_03_21 tom
zrank user:ranking:2020_03_24 tom
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值