Redis中基本数据类型及其使用场景

219 阅读8分钟

先谈为什么用Redis

既然已经有了 MySQL 数据库为什么还研发出 Redis 这种 key-value 内存数据库,或者为什么不直接存储在.txt/.log这种文件里?Redis 的出现为我们解决了什么问题?

  • 解决磁盘的 IO 瓶颈

    • 存放在 txt/log、MySQL 的数据最终存放是在磁盘的,磁盘寻址耗时是毫秒(ms)级别的。
    • 磁盘存放的数据,一次查询能查询出来的数据大小受带宽(单位时间内能传递的文件大小)的影响。
  • 而内存的寻址是纳秒(ns)级别的。相比磁盘寻址快了十万倍。

画外音:为什么要MySQL使用索引?

2.String

我们在使用 Redis 时候经常会用到对于某个 key 的 value 值进行自增,但是为什么 Redis 里面却没有 int 类型呢?

Redis 的存放的是字节数据,数据是类型是根据 encoding 编码获取的。 执行set testkey 99时 redis 会先判断这个 testkey 的 encoding 类型,判断出是个 int,下次 incr 的时候,就能感知到这个 testkey 对应的 value 是 int 类型。

# 查看testkey对应的value的编码类型,返回int
OBJECT encoding testkey
  • 当我们存储的是 123 时,实际数据类型为 Int
  • 当我们存储的是 abc 字符串时,实际数据类型是 embstr
  • 当字符串很长(长度大于 44)时,实际数据类型为 raw

那么是否长度超过44为的数字:例如:99.....9,incr是不是就会报错? 其实并不会,redis在incr之前也会判断一次encoding,判断是否能进行数值运算。

2.1 字符类型

  • 常见操作:set、get、append、strlen

  • 使用场景:

    • 存放手机动态验证码

2.2 数值类型

  • 常见操作:incr、decr
  • 使用场景:点赞数,加入购物车。

2.3 bitmap

  • 位图常见操作:setbit、bitcount、bitpos、bitop

  • 位图使用场景:

    • 统计系统上线后随机时间段内的用户登录天数。

      以用户ID+年份为key,value是一个位图(每一位代表第N天是否登录了,登录在位图中则记为1);例如:获取用户ID为1006在2020年在最后三天登录了几次:

      bitcount 10062020 -3 -1
      
    • 某一年的某个时间段的活跃用户统计。 以日期(yyyyMMdd)为key,每一个位代表一个用户。例如:在20210614,用户ID:1001登录系统,在20210615,用户ID:1002登录系统,在20210616,用户ID:1003登录系统;获取这三天内活跃用户数:

      setbit 20210614  1001  1
      setbit 20210615  1002  1
      setbit 20210616  1003  1
      
      # 三天内任何一个用户登录都计入到destkey
      bitop or destkey 20210614 20210615 20210616
      # 获取三天登录了的用户数
      bitcount destkey 0 -1
    

    小的优化点:能否借鉴HashMap的计算key的摆放位置:bitIndex = hash(userID)%(totalUserCount+N),可以缩小位图value的长度,从而提高内存空间利用率?

3.List

List常用的操作:

  • LRANGE : LRANGE key start stop
# 从头到尾取出key为:testlist 的元素
LRANGE testlist 0 -1
  • LPUSH : LPUSH key element [element ...] 从上个元素左侧添加元素,或者说从头部添加。
LPUSH testlist 1 2 3 4
# 这里容易理解成摆放顺序为 1 2 3 4,实则不然。
LRANGE testlist 0 -1
1) "4"
2) "3"
3) "2"
4) "1"
  • LPOP:LPOP key 从集合的最左侧弹出一个元素。

LTRIM:LTRIM key start stop,截取元素,类似 java List 的 subList(),截取的下标包前不包后。

  • LLEN:LLEN key 获取 list 元素个数。
  • LREM:LREM key count element删除 count 个 element 元素。当 count 为负数,从后往前删指定的 |count| 个元素
  • BLPOP/BRPOP:BLPOP key [key ...] timeout,阻塞式地从左右侧弹出一个元素,当key里没有元素能弹出时一直阻塞等待,或者重试等待timeout秒,没有元素弹出则返回nil。

List类型使用场景:

  1. 基于 redis 反向命令实现先进后出的队列结构:like:lpush,lpop,基于Redis的消息队列其实就是这个原理。
  2. 基于 redis 同向命令实现后进先出的栈结构:like:lpush,lpop
  3. 基于下标操作的数组结构
  4. 阻塞队列
  5. 推荐文章列表的分页查询。

3.1 Set

常用命令操作:

  • SADD 添加元素:SADD key member [member ...]
  • SISMEMBER 是否是这个集合的子集:SISMEMBER key member
  • SMEMBERS 获取集合的所有元素:SMEMBERS key
  • SMOVE 从source移动元素到destination:SMOVE source destination member
  • SPO 从集合中取出count个元素并移除:SPOP key [count]
  • SRANDMEMBER 从集合中查询(并不移除)一个随机元素出来。
  • SUNION 求N个集合的并集:SUNION key [key ...]
  • SINTER 求N个集合的交集:SINTER key [key ...]
  • SDIFF 求N个集合的差集:SDIFF key [key ...]

Set使用场景:

  • 取集合的交并差运算:共同好友

  • 用户随机抽奖

    准备key为prizevalue为:存放每位参与用户ID的Set集合。每次抽count个名额。

    • a:奖品多人少:srandmember prize count,count为负数时单人可以重复中奖。
    • b:奖品少人多:spop prize count,
    • c:抽取之后,奖品仍在奖池,下次还能重复中。srandmember prize count
    • d: 抽取之后,奖品直接从奖池拿走,不能重复中同一个奖,公司年会场景。spop prize count
  • 用户系统:用户打标签。某两位用户有相同的关注内容,那么后期用来做内容精准推荐。

3.2 ZSet

ZSet常用操作命令:

  • ZADD 添加元素,并设置分值 ZADD key [NX|XX] [CH] [INCR] score member [score member ...]元素添加进去之后是升序排放的。score用于标识元素的摆放位置,越小越靠左,score相同则按照字符顺序排序。
  • ZRANGE:升序列举元素;ZRANGE key start stop [WITHSCORES]
ZADD testsort 4 zhangsan 1 lisi 2 zhaoliu
(integer) 3
# 列举元素(不显示分数)
ZRANGE testsort 0 -1
1) "lisi"
2) "zhaoliu"
3) "zhangsan"
# 列举元素,并附带分值一起查询出来
ZRANGE testsort 0 -1 withscores
1) "lisi"
2) "1"
3) "zhaoliu"
4) "2"
5) "zhangsan"
6) "4"
# 测试相同的分值
zadd num 1  1  1 2  1 0 1 3 1 9 1 4
ZRANGE num 0 -1
1) "0"
2) "1"
3) "2"
4) "3"
5) "4"
6) "9"
  • ZREVRANGE :降序列举元素 ZREVRANGE key start stop [WITHSCORES]
# 降序列举
ZREVRANGE testsort 0 -1 withscores
  • ZCOUNT:获取满足范围的数据个数 ZCOUNT key min max
  • BZPOPMAX:阻塞式去除一个分数最高的。BZPOPMAX key [key ...] timeout

ZSet使用场景:

  • 歌曲排行榜:歌曲的热的选取播放量维度排序,一首歌曲播放之后进行incrc操作。
  • 阻塞队列:高校录取填报了志愿的学生,分数从高往低取出,然后做一系列的资质分析【手动狗头】。

ZSet排序是怎么实现的?

  • Skip List(跳表)

假如就按照单链表存放属性有序的元素,最坏的结果得遍历一次整个链表,复杂度为O(N)。 那么怎么优化?MySQL是通过索引加速,这么一想是不是会有异曲同工的解决方案?

通俗地研究一把跳表是个什么东西: 为了加快链表索引速度,那么最先能想到的办法就是二分。怎么个二分法?跳跃性地有序维护!

此时,我们假设要查找节点8,我们可以先在索引层遍历,当遍历到索引层中值为 7 的结点时,发现下一个节点是9,那么要查找的节点8肯定就在这两个节点之间。我们下降到链表层继续遍历就找到了8这个节点。原先我们在单链表中找到8这个节点要遍历8个节点,而现在有了一级索引后只需要遍历五个节点。

4.Hash

Hash常用命令操作:

  • HSET设置key的field值:HSET key field value [field value ...]
  • HGET获取key的field值:HGET key field
  • HINCRBY 自增一个field 的int/folat,增幅为:increment HINCRBY/HINCRBYFLOAT key field increment

Hash结构的使用场景

  • 存放商品明细信息,商品的浏览量,收藏量,下单量,可以通过field的自增来累计。

  • 用户的购物车信息。

    • 以客户id作为key,每位用户创建一个hash存储结构存储对应的购物车信息。
    • 将商品编号作为field,购买数量作为value进行存储
    • 添加商品:追加全新的field与value
    • 浏览:遍历hash
    • 更改数量:自增/自减,设置value值
    • 删除商品:删除field
    • 清空:删除key
    • 全选:hgetall
    • 购物车总数量:hlen
    • 增加某件商品的数量:hincrby
userId1:{
  “productId1”:2
  “productId2”:3
}

但是购物车商品明细数据并没有得到加速,商品信息还要二次查询数据库,面向对象再优化一把。把商品信息和购买数量再包装一层呗。

userId1:
{
  {
  “count_productId1”:2,
  “info_productId1”:"{}"
},
  {
  “count_productId2”:3,
  “info_productId2”:"{}"
},
}