Redis的几种数据模型及应用场景

字符串(K-V)

字符串(K-V)是我们在Redis中使用最多的一个类型,其中V的值不能超过512M,甚至很多人用Redis只用这个类型。如果只是单纯的使用K-V其实是并没有把Redis的特性发挥出来,在这种使用情况下,使用Redis和使用Memcache并没有过多的区别。

命令

  • 设置值
    set key value 直接设置值。
    setex key seconds value 设置值并且带有过期时间。
    setnx key value 设置值,并且判断是否存在。如果存在则设置不成功,如果不存在则设置成功。主要用于分布式锁。
    getset key value 设置并返回原值。

  • 获取值
    get key 根据key获取值,如果不存在则返回nil。

  • 批量设置值
    mset key value [key value ...] 批量设置K-V,例如:mset a 1 b 2 c 3 d 4

  • 批量获取值
    mget key [key ...]
    下面操作批量获取了键 a、b、c、d 的值:

    127.0.0.1:6379> mget a b c d
    1) "1"
    2) "2"
    3) "3"
    4) "4"
    

    如果有些键不存在,那么它的值为 nil(空),结果是按照传入键的顺序返回:

    127.0.0.1:6379> mget a b c f
    1) "1"
    2) "2"
    3) "3"
    4) (nil)
    

批量调用命令与单个命令相比的好处就是节约了网络请求时间。get命令执行5次,相当于5次请求&响应,但是如果执行mget5个key,只有1次请求&响应。

  • 计数
    incr key 根据key自增,并且返回执行后的结果。
    值不是整数,返回错误。
    值是整数,返回自增后的结果。
    键不存在,按照值为0自增,返回结果为1。
    decr key 自减。
    incrby key increment 自增指定数字。
    decrby key decrement 自减指定数字。
    incrbyfloat key increment 自增浮点数。

  • 删除
    del key 删除

内部编码

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

  • int:8个字节的长整型。
  • embstr:小于等于39个字节的字符串。
  • raw:大于39个字节的字符串。
    Redis 会根据当前值的类型和长度决定使用哪种内部编码实现。

使用场景

字符串的使用场景比较常见。我们日常用的缓存,session共享等,都是通过字符串的方式去实现。

哈希(hash)

在 Redis 中,哈希类型是指键值本身又是一个键值对结构。相当于 key-{field1-value1},{field2-value2},{field3-value3}……{fieldN-valueN}

命令

  • 设置值
    hset key field value 设置key-field-value。

  • 获取值
    hget key field 如果key或者field不存在,则返回nil。

  • 计算 field 个数
    hlen key

  • 计算 value 的字符串长度
    hstrlen key field

  • 批量设置值&获取值
    hmget key field [field ...] 批量获取值,同一个key,不同的fieled
    hmset key field value [field value ...] 批量设置值,同一个key,不同的field-value。

  • 删除field
    hdel key field [field ...] hdel 会删除一个或多个 field,返回结果为成功删除 field 的个数。

  • 判断 field 是否存在
    hexists key field 包含时返回结果为1,不包含返回0。

  • 获取所有field
    hkeys key 根据key,返回所有的field。

  • 获取所有value
    hvals key 根据key,返回所有的value

  • 获取所有的 field-value
    hgetall key 根据kye,返回所有的filed及value。如果这个key里面的field太多,而每个field的value也太多,不建议使用,很有可能会引起Redis阻塞。

  • 计数(与k-v差不多)
    hincrby key field
    hincrbyfloat key field

内部编码

哈希类型的内部编码有两种:

  • ziplist(压缩列表):当哈希类型元素个数小于 hash-max-ziplist-entries 配置(默认512个)、同时所有值都小于 hash-max-ziplist-value 配置(默认64字节)时,Redis 会使用 ziplist 作为哈希的内部实现,ziplist 使用更加紧凑的结构实现多个元素的连续存储,所以在节省内存方面比 hashtable 更加优秀。
  • hashtable(哈希表):当哈希类型无法满足 ziplist 的条件时,Redis 会使用 hashtable 作为哈希的内部实现,因为此时 ziplist 的读写效率会下降,而 hashtable 的读写时间复杂度为O(1)。

使用场景

  • 一般情况我们存储一个对象,可以用K-V,key为一个对象的主键,value这个对象(或者对象的json)。这样的话,我们每次get key获取到的就是整个对象,如果这个对象里面我们可能只需要用到一两个字段值,这样的操作其实是非常浪费性能的。而且存储对象的时候,我们需要用到序列化与反序列化,这些也是会消耗一定的性能的。我们可以把整个对象用hash的方式存到Redis中,需要哪个对象的哪些值就获取哪个。

列表(list)

Redis中的列表可以存储多个有序字符串,最多可以存储2^32-1个元素。而且它支持双向操作,可以从列表两端插入(push)和弹出(pop)。因此,它可以用在很多种数据结构上。

命令

列表主要是四种操作类型

操作类型操作
rpush lpush linsert
lpop rpop lrem ltrim
lset
lrange lindex llen
阻塞blpop brpop
  • 增(插入)
    rpush key value [value ...] 从右边插入value。
    lpush key value [value ...] 从左边插入value。
    linsert key before|after VAL value linsert 命令会从列表中找到等于 VAL 的元素,在其前(before)或者后(after)插入一个新的元素 value。

  • 删除
    lpop key 从列表左侧弹出元素。
    rpop key 从列表右侧弹出元素。
    ltrim key start end 按照索引范围修剪列表。
    lrem key count value 删除指定元素。
    rem 命令会从列表中找到等于 value 的元素进行删除,根据 count 的不同分为三种情况:count>0,从左到右,删除最多 count 个元素;count<0,从右到左,删除最多 count 绝对值个元素;
    count=0,删除所有。


  • lset key index newValue 根据index修改列表中的元素。


  • lrange key start end lrange 操作会获取列表指定索引范围所有的元素。索引下标有两个特点:第一,索引下标从左到右分别是0到 N-1,但是从右到左分别是-1到-N,全部分别是0到-1。第二,lrange 中的 end 选项包含了自身,这个和很多编程语言不包含 end 不太相同。
    lindex key index 获取列表指定索引下标的元素。索引下标从左到右分别是0到 N-1,但是从右到左分别是-1到-N。
    llen key 获取列表长度。

  • 阻塞

blpop key \[key ...\] timeout
brpop key \[key ...\] timeout

blpop 和 brpop 是 lpop 和 rpop 的阻塞版本,它们除了弹出方向不同,使用方法基本相同,所以下面以 brpop 命令进行说明,brpop 命令包含两个参数:
key[key…]:多个列表的键。
timeout:阻塞时间(单位:秒)。

列表为空:如果 timeout=3,那么客户端要等到3秒后返回,如果 timeout=0,那么客户端一直阻塞等下去。
列表不为空:客户端会立即返回。

在使用 brpop 时,有两点需要注意。

第一点,如果是多个键,那么 brpop 会从左至右遍历键,一旦有一个键能弹出元素,客户端立即返回:

127.0.0.1:6379> brpop list:1 list:2 list:3 0
..阻塞..

此时另一个客户端分别向 list:2和 list:3插入元素:
client-lpush> lpush list:2 element2
(integer) 1
client-lpush> lpush list:3 element3
(integer) 1

客户端会立即返回 list:2中的 element2,因为 list:2最先有可以弹出的元素:
127.0.0.1:6379> brpop list:1 list:2 list:3 0
1) "list:2"
2) "element2_1"



第二点,如果多个客户端对同一个键执行 brpop,那么最先执行 brpop 命令的客户端可以获取到弹出的值。

客户端1:
client-1> brpop list:test 0
...阻塞...

客户端2:
client-2> brpop list:test 0
...阻塞...

客户端3:
client-3> brpop list:test 0
...阻塞...

此时另一个客户端 lpush 一个元素到 list:test 列表中:
client-lpush> lpush list:test element
(integer) 1

那么客户端1最会获取到元素,因为客户端1最先执行 brpop,而客户端2和客户端3继续阻塞:
client> brpop list:test 0
1) "list:test"
2) "element"

内部编码

列表类型的内部编码有两种。

  • ziplist(压缩列表):当列表的元素个数小于 list-max-ziplist-entries 配置(默认512个),同时列表中每个元素的值都小于 list-max-ziplist-value 配置时(默认64字节),Redis 会选用 ziplist 来作为列表的内部实现来减少内存的使用。
  • linkedlist(链表):当列表类型无法满足 ziplist 的条件时,Redis 会使用 linkedlist 作为列表的内部实现。

使用场景

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

集合(set)

集合(set)类型也是用来保存多个的字符串元素,它和Java中的Set是差不多的东西,不允许重复。

命令

  • 添加元素
    sadd key element [element ...] 返回结果为添加成功的元素个数(如果已有这个元素,则不计入成功元素)。

  • 删除元素
    srem key element [element ...] 返回结果为成功删除元素个数。

  • 计算元素个数
    scard key

  • 判断元素是否在集合中
    sismember key element 如果给定元素在集合内返回1,反之返回0。

  • 随机从集合返回指定个数元素
    srandmember key [count] [count]是可选参数,如果不写默认为1。

  • 从集合随机弹出元素
    spop key [count]

  • 获取所有元素
    smembers key

  • 求多个集合的交集
    sinter key [key ...] 集合1与集合2的共有部分。

  • 求多个集合的并集
    suinon key [key ...] 集合1+集合2。

  • 求多个集合的差集
    sdiff key [key ...] 集合1-集合2。

  • 将交集、并集、差集的结果保存
    sinterstore destination newKey [key1 key2...]
    suionstore destination newKey [key1 key2...]
    sdiffstore destination newKey [key1 key2...]
    将结果保存到newkey中。

内部编码

集合类型的内部编码有两种:

  • intset(整数集合):当集合中的元素都是整数且元素个数小于 set-max-intset-entries 配置(默认512个)时,Redis 会选用 intset 来作为集合的内部实现,从而减少内存的使用。
  • hashtable(哈希表):当集合类型无法满足 intset 的条件时,Redis 会使用 hashtable 作为集合的内部实现。

使用场景

其实set和Java中的set非常像,一般情况如果不需要与其他服务共享数据,Java中的set足够使用了,在某些情况需要共享数据或者存储的情况才需要用到Redis中的set。
比如,使用抽奖的情况下,就可以先将所有人放到Redis中的set,然后利用spop/srandmember可以实现抽奖。

有序集合(zset)

有序集合和集合很相似,set是key中包含多个不重复的字符串,zset也是包含了多个不重复的字符串,但是每个字符串带了一个“分数”,分数允许重复。可以根据分数进行排序操作。

命令

  • 添加成员
    zadd key score member [score member ...]

  • 计算元素个数
    zcard key

  • 计算某个成员的分数
    zscore key member

  • 计算成员的排名
    zrank key member
    zrevrank key member
    zrank是从分数从低到高返回排名,zrevrank反之(排名从0开始计算)。

  • 删除成员
    zrem key member [member ...] 返回结果为成功删除元素个数。

  • 增加成员的分数
    zincrby key increment member

  • 返回指定排名范围的成员
    zrange key start end [withscores]
    zrevrange key start end [withscores]
    有序集合是按照分值排名的,zrange是从低到高返回,zrevrange反之。如果加上withscores选项,同时会返回成员的分数。

  • 返回指定分数范围的成员
    zrangebyscore key min max [withscores] [limit offset count]
    zrevrangebyscore key max min [withscores] [limit offset count]
    其中zrangebyscore按照分数从低到高返回,zrevrangebyscore反之。[limit offset count]选项可以限制输出的起始位置和个数。同时min和max还支持开区间(小括号)和闭区间(中括号),-inf和+inf分别代表无限小和无限大。例如:

127.0.0.1:6379> zrangebyscore user:ranking (200 +inf withscores
1) "tim"
2) "220"
3) "martin"
4) "250"
5) "tom"
6) "260"
  • 返回指定分数范围成员个数
    zcount key min max

  • 删除指定排名内的升序元素
    zremrangebyrank key start end

  • 删除指定分数范围的成员
    zremrangebyscore key min max

内部编码

有序集合类型的内部编码有两种:

  • ziplist(压缩列表):当有序集合的元素个数小于zset-max-ziplist-entries配置(默认128个),同时每个元素的值都小于zset-max-ziplist-value配置(默认64字节)时,Redis会用ziplist来作为有序集合的内部实现,ziplist可以有效减少内存的使用。
  • skiplist(跳跃表):当ziplist条件不满足时,有序集合会使用skiplist作为内部实现,因为此时ziplist的读写效率会下降。

使用场景

有序集合比较典型的使用场景就是排行榜系统。例如视频网站需要对用户上传的视频做排行榜,榜单的维度可能是多个方面的:按照时间、按照播放数量、按照获得的赞数。本节使用赞数这个维度,记录每天用户上传视频的排行榜。

位图(Bitmaps)

Bitmaps 本身不是一种数据结构,实际上它就是字符串,但是它可以对字符串的位进行操作,它是一个最大长度为512MB(2^32)的位数组。一般情况Bitmaps在普通的业务上是用不了太多的,这里之所以拿出来讲解,是因为它能够实现一个非常强大的功能:布隆过滤器。
Bitmaps 单独提供了一套命令,所以在 Redis 中使用 Bitmaps 和使用字符串的方法不太相同。可以把 Bitmaps 想象成一个以位为单位的数组,数组的每个单元只能存储0和1,数组的下标在 Bitmaps 中叫做偏移量。但是它的每个存储单元都是bit,8bit=1Byt,所以它的存在主要就是占用内存非常少。

命令

  • 设置值
    setbit key offset value 设置键的第 offset 个位的值(从0算起,value只能是0或者是1)。

  • 获取值
    getbit key offset 获取键的第 offset 位的值(从0开始算)。

  • 获取 Bitmaps 指定范围值为1的个数
    bitcount key [start] [end]

  • Bitmaps 间的运算
    bitop 运算符 destkey key [key....] bitop 是一个复合操作,它可以做多个 Bitmaps 的 and(交集)、or(并集)、not(非)、xor(异或)操作并将结果保存在 destkey 中

  • 计算 Bitmaps 中第一个值为 targetBit 的偏移量
    bitpos key targetBit [start] [end]

使用场景

以下这个业务场景有点牵强,但是还是简单的描述一下吧。如果统计一个网站某一天的用户访问量,我们通常的做法是添加一个用户登录记录流水表,然后根据日期去count(distinct userId)这张表。但是这样的做法会导致这张表里的数据非常多,而且占用大量的空间。
如果用户的ID是自动增长的,那么就可以使用位图。比如ID为125的用户在2019-01-01这一天登录的时候,我们可以使用setbit user:login:2019-01-01 125 1在Bitmaps的偏移量为125的位置设置值为1;如果还有其他的用户登录,同样根据用户ID将对应偏移量的值设置为1。然后调用bitcount user:login:2019-01-01计算出2019-01-01这一天的访问量。
使用位图最方便的是占用资源比较小,而且执行速度会比较快。但是如果活跃用户比较少,而且用户ID又非常大的情况,用位图就有点得不偿失了。比如:2019-01-02这一天只有一个用户访问这个网站,而且这个用户的ID为100000,那么就需要占用1bit*100000的空间。

布隆过滤器 (Bloom Filter)

布隆过滤器(Bloom Filter)的核心实现是一个超大的位数组和几个哈希函数。假设位数组的长度为m,哈希函数的个数为k,以下图为例,具体的操作流程:首先将位数组进行初始化,将里面每个位都设置为0。这时候我们有一个集合{x, y, z},对于集合里面的每一个元素,将元素依次通过3个哈希函数进行映射,每次映射都会产生一个哈希值,这个值对应位数组上面的一个点,然后将位数组对应的位置标记为1。查询W元素是否存在集合中的时候,同样的方法将W通过哈希映射到位数组上的3个点。如果3个点的其中有一个点不为1,则可以判断该元素一定不存在集合中。反之,如果3个点都为1,则该元素可能存在集合中。注意:此处不能判断该元素是否一定存在集合中,可能存在一定的误判率。可以从图中可以看到:假设某个元素通过映射对应下标为4,5,6这3个点。虽然这3个点都为1,但是很明显这3个点是不同元素经过哈希得到的位置,因此这种情况说明元素虽然不在集合中,也可能对应的都是1,这是误判率存在的原因。
综合来说:如果这个元素存在,那么可能会存在误判,其实它有可能是不存在的。如果这个元素不存在,那么它一定不存在!

《Redis的几种数据模型及应用场景》 image

语法

其实Redis4.0已经帮我们实现了对应的布隆过滤器,我们简单的看下它的语法。

  • 创建布隆过滤器
    bf.reserve key error_rate size key为redis存储键值,error_rate 为错误率(大于0,小于1),size为预计存储的数量(size是比较关键的,需要根据自己的需求情况合理估计,设置太小的话会增大错误率,设置太大会占用过多不必要的空间)

  • 添加元素
    bf.add key value 添加值到布隆过滤器中(当过滤器不存在的时候会,会以默认值自动创建一个,建议最好提前创建好)
    bf.madd key value [value ...] 批量插入元素到布隆过滤器

  • 判断是否存在
    bf.exists key value 判断值是否存在过滤器中 1(表示很可能存在) 0 (表示绝对不存在)
    bf.mexists key value [value ...] 批量判断判断值是否存在过滤器中

应用场景

如果我们结合BitMaps,就可以实现一个布隆过滤器。布隆过滤器的应用场景也非常广泛,举一个比较典型的例子:
通常,我们对缓存的操作是:

Object obj = 查询缓存(key);
if (obj == null) {
    obj = 查询数据库(key);
    
    if (obj != null) {
        设置缓存数据(key, obj);
    }
}
return obj;

但是,这里产生了一个问题:如果这个key本身在我们的数据库就是一个不存在的值,那么这里会直接造成缓存穿透。如果我们的数据库这个表中的数据特别多,而且有人知道这个漏洞,一直调用这个接口,查询这个根本不存在的key,会对数据库造成非常大的压力,甚至压垮数据库。
解决方案:

  • 我们把数据库中,这个表全部的key同步到redis中。这个方案应该是一般人第一想法,但是如果这个表中的数据特别多,那么直接会造成Redis内存爆了。
  • 利用布隆过滤器,对每个key进行多次hash,将每次hash出来的值当作offset修改Bitmaps的值为1。如果需要查询这个key,先查缓存,再查布隆过滤器对应的Bitmaps,最后确定有这个值就再查数据库。
Object obj = 查询缓存(key);
if (obj == null) {
    boolean exists = 查询Bitmaps(key);
    
    if (exists)
    {
        obj = 查询数据库(key);
        
        if (obj != null) {
            设置缓存数据并修改Bitmaps(key, obj);
        }
    }
}
return obj;
    原文作者:迦若莹
    原文地址: https://www.jianshu.com/p/bf3f985d28ed
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞