redis二:常用数据结构实战和底层分析

常用数据结构实战和底层分析

redis的常用命令不在展示,可以通过help命令进行查看。比如help @数据类型 可以查看对应的api命令
在这里插入图片描述

String

使用场景

单值缓存
例如缓存商品编号为1001的价格 :set goods:1001 28.5
对象缓存

  1. set user:1 value( user的json格式数据) 也可以通过hash进行对象缓存
  2. mset user1:name zz user1:age 18 user1:sex 1 user1:balance 1000

这两种有什么区别,第一种在于简单,但是如果对象的字段很多,每次需要修改部分字段,那么第二种会更好
分布式锁
setnx goods:1001 存在相同的key 会set失败
设置分布式锁并且设置超时时间
set goods:1002 1 ex 60 nx
计数器功能
比如博客文章的阅读量、点赞数、收藏数等等,每次有人阅读 incr auth_aa:article_test:readcount 对对应的key进行+1
web集群的session共享
spring session + redis 实现session共享
全局唯一性id
incr orderid 如果每次获取id就进行一次redis的交互,那么redis的压力非常大。可以通过批量生成id来提升性能。比如incrby orderid 10000 每次一次性的获取一万个id到本机的内存,然后在本机的内存中进行++操作。
亿级日活统计
每天上亿的访问量,如何统计一天的用户登录情况。类似这种只有两种状态的(登录、未登录)都可以用bitmap来表示
bitmap默认为0 只有0和1两种数据 这样一个字节就有8个bit 就能表示8个用户的登录状态 0表示未登录 1表示登录
例如 统计今天的用户登录情况 用今天的日期作为key login_20220426 使用用户的id作为偏移量 这里用户的id就得是整形。
比如用户id为5 今天登录过了 那么SETBIT login_20220426 5 1 其他用户同理。
查询用户id为81的用户是否今天登录过 GETBIT login_20220426 81
统计有多少用户登录:BITCOUNT login_20220426
统计连续登录情况
还是使用bitmap 比如连续登录两天进行统计,把两天的bitmap进行位的与(&)运算 得到新的bitmap ,在统计新的bitmap即可
如果统计周活的 七天的bitmap进行位的或(|)运算 在统计得到的bitmap
bitmap的位运算:BITOP and login_25-26 login_20220425 login_20220426 and:表示运算符 login_25-26:新的bitmap的名称 剩下就是需要运算的key

底层结构

redis 是用C语言来写的,在C语言中表示字符串采用chat数组,redis定义了一种数据类型sds表示字符串
SDS(simple dynamic string),它的数据结构

SDS:
  #可剩余空间
  free:0
  #字符串长度
  len: 5
  #字符串
  chat[]='china'  ->(扩容成) china123

那么就会计算出addlen:3,通过(len+addlen)*2 = 16

SDS:
  free:8
  len:8
  chat[]= 'china123'

后续扩容的时候会判断可用空间够不够,如果足够就不会进行扩容。当字符串定义完后会自动在末尾加上\0,这样可以兼容C语言的函数库
SDS特点:

  1. 二进制安全的数据结构
  2. 提供了内存预分配机制、避免内存频繁分配
  3. 兼容C语言函数库

redis3.2以前结构:

struct sdshdr {
    int len;
    int free;
    char buf[]; //具体数据的存储
};

3.2以后的数据结构
在这里插入图片描述
redis所有的数据类型都封装成了redisobject对象,其中type表示对应的数据类型、encoding表示对应的类型编码 ptr指向具体的数据
在这里插入图片描述
即使使用了string类型,但是底层编码也会有所不同
在这里插入图片描述
使用type命令查看它们的类型都是string类型
在这里插入图片描述
使用encoding查看它们的编码
在这里插入图片描述
String类型的底层编码就是这三种了。raw的编码就是sds的结构,embstr是对raw的优化 在value的长度较小的时候可以直接放到缓存行中 减少读取的IO

hash

使用场景

对象缓存
hmset user 1:name zhangsan 1:age 18 2:name lisi 2:age 19 在user这个map中缓存了用户id 分别为1和2的用户信息。然后可以通过hgetall user 获取所有的用户信息
购物车操作
用户id1001 的购物车 作为key :shopping_cart:1001
想往购物车添加1个商品id为111 的商品 hset shopping_cart:1001 111 1
继续添加商品为122 的商品 hset shopping_cart:1001 122 1
增加商品111的数量 +2 hincrby shopping_cart:1001 111 2
获取购物车商品类型总数:hlen shopping_cart:1001
获取购物车111商品的数量:hget shopping_cart:1001 111
获取购物车所有信息:hgetall shopping_cart:1001
删除购物车111的商品:hdel shopping_cart:1001 111

底层结构

Hash 数据结构底层实现为一个字典( dict ),也是RedisBb用来存储K-V的数据结构,当数据量比较小,或者单个元素比较小时,底层用ziplist存储,数据大小和元素数量阈值可以通过如下参数设置。

hash-max-ziplist-entries  512    //  ziplist 元素个数超过 512 ,将改为hashtable编码 
hash-max-ziplist-value    64      //  单个元素大小超过 64 byte时,将改为hashtable编码

当数据量小的时候
在这里插入图片描述
在这里插入图片描述
当数据量大或者单个数据大的时候
在这里插入图片描述
ziplist的结构在list中会讲到。hashtable 就是dict
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
dictEntry存放的就是具体的数据
在这里插入图片描述
从底层结构来说,如果对象的字段很多 数据选型选择string的话 ,因为set key value key value ,这样每个字段就对应了一个key。如果key很多会导致数据扩容,频繁进行rehash(渐进式扩容)。
如果选择hash 那么hset key file value 这个形式 key 就只有一个。但是key只有一个的话,如果增加过期时间只能整体数据增加过期时间。就是选择hash还是string的权衡。

list

使用场景

实现常用的数据结构
Stack(栈) = LPUSH + LPOP
Queue(队列)= LPUSH + RPOP
Blocking MQ(阻塞队列)= LPUSH + BRPOP
微博、微信公众号信息流
比如微信订阅了一些公众号,每次公众号有新的消息都会推送到订阅列表中,而且按时间排序,新的消息都会排序到顶端。那么这个功能可以通过list来实现。
比如我订阅了 A 、B 、C三个公众号,那么这三个公众号要是有新的消息要怎么推送到我的微信上呢
首先redis中定义一个 自己的微信公众号信息的list key为: msg:1001 然后ABC也都有自己的list msg:A等等
A发布了一个消息id为333的消息 通过lpush msg:1001 333
B发布了一个消息id为2525的消息 通过lpush msg:1001 2525
c发布了一个消息id为2678的消息 通过lpush msg:1001 2678
把信息展示到微信上就是lrange msg:1001 0 3 等到的信息id就是
在这里插入图片描述
再把对应的信息查出来展示即可
当然了如果公众号的订阅人数比较多的话,就需要优化了 。比如先给在线的人发送,但是如果订阅的人数很多,在线的人数也很多,那么可以通过pull方式 去订阅的公众号中获取消息,然后在排序等等。对于如何优化不展开讨论

底层结构

List是一个有序(按加入的时序排序)的数据结构,Redis采用quicklist(双端链表) 和 ziplist 作为List的底层实现
ziplist的数据结构:
在这里插入图片描述
zlbytes 占4个字节 表示ziplist占用的字节总数
zltail占4个字节 表示ziplist表中存放数据最后一项,用于找到最后一个数据进行往前面遍历
zlen占2个字节 表示ziplist中数据项(entry)的个数
颜色不同的那块是数据项 entry。

  • prerawlen: 前一个entry的数据长度。
  • len: entry中数据的长度
  • data: 真实数据存储

zlend:ziplist最后1个字节,是一个结束标记,值固定等于255
list并不是所有数据都存放在entry中,而是拆分成多个ziplist ,通过quicklist连接起来
在这里插入图片描述
quicklist的head 表示头节点 tail表示尾节点 每一个节点有着双向指针prev、next。zl存放的就是ziplist。如果想要优化这list
减少ziplist存储的数据,如果ziplist的数据过大,操作会影响性能。通过redis.conf 中修改配置 list-max-ziplist-size -2 一般不需要修改。
还有就是可以把 quicklist 的节点进行压缩。通过redis.conf 中修改配置list-compress-depth 1

list-max-ziplist-size  -2        //  单个ziplist节点最大能存储  8kb  ,超过则进行分裂,将数据存储在新的ziplist节点中
list-compress-depth  1        //  0 代表所有节点,都不进行压缩,1, 代表从头节点往后走一个,尾节点往前走一个不用压缩,其他的全部压缩,2,3,4 ... 以此类推

set

使用场景

去重
去掉集合或者数组的重复的值 ,遍历数组或者集合然后把值通过sadd,然后通过smembers获取去重后的值
抽奖活动
假设抽奖活动id为act:100
点击抽奖添加到抽奖名单中,比如用户id为100点击加入抽奖 sadd act:100 100 其他用户同理
查看抽奖用户有哪些 SMEMBERS act:100
在这里插入图片描述
开始抽奖,比如抽三等 3 个然后 把这三个剔除掉:spop act:100 3
在抽二等奖 然后把两个去掉:spop act:100 2
最后抽一等奖:spop act:100 1
消息点赞、收藏、标签等
点赞:SADD msg:{消息ID} {用户ID} sadd msg:1001 1
取消点赞:srem msg:1001 1
检查用户是否点过赞:SISMEMBER msg:1001 5
获取点赞的用户列表:SMEMBERS msg:1001
统计点赞用户数:scard msg:1001
集合的操作
并集:SUNION set1 set2 set3
交集:SINTER set1 set2 set3
差集:SDIFF set1 set2 set3 用第一个集合减去 后面所有集合的交集 得到的不重复的值
通过set集合操作实现关注模型
我关注的人 : a b c d sadd me a b c d
张三 关注的人: b c e f g t sadd zhangsan b c e f g t
c关注的人 : g f 张三 sadd c g f zhangsan
我和张三共同关注的人:SINTER me zhangsan
我关注的人也关注他:通过判断 我关注的人中是否有人关注了张三
SISMEMBER a zhangsan SISMEMBER b zhangsan SISMEMBER c zhangsan 。。。
我可能认识的人:通过张三查看我可能认识的人 SDIFF zhangsan me

底层结构

Set 为无序的,自动去重的集合数据类型,Set 数据结构底层实现为一个value 为 null 的 字典( dict ),当数据可以用整形表示时,Set集合将被编码为intset数据结构。
两个条件任意满足时Set将用hashtable存储数据。

  1. 元素个数大于 set-max-intset-entries ,
  2. 元素无法用整形表示

set-max-intset-entries 512 // intset 能存储的最大元素个数,超过则用hashtable编码

在这里插入图片描述

typedef struct intset {
    uint32_t  encoding;
    uint32_t  length;
    int8_t      contents[];
} intset; 
# 元素个数超出后首先进行升级 超过阀值后变成hashtable
#define INTSET_ENC_INT16 (sizeof(int16_t))
#define INTSET_ENC_INT32 (sizeof(int32_t))
#define INTSET_ENC_INT64 (sizeof(int64_t))

整数集合是一个有序的,存储整型数据的结构。整型集合在Redis中可以保存int16_t,int32_t,int64_t类型的整型数据,并且可以保证集合中不会出现重复数据。
encoding: 编码类型
length: 元素个数
contents[]: 元素存储

在这里插入图片描述

zset

help @sorted_set

使用场景

zset集合操作
分值正序:ZRANGE news:20220401 0 4 withscores
分值倒序:ZREVRANGE news:20220401 0 4 withscores
定义三个zset集合:zadd test1 10 v1 20 v2 30 v3 zadd test 5 v1 5 v2 5 v3 10 v4 zadd test2 10 v5
聚合函数sum的交集:ZUNION 2 test1 test AGGREGATE SUM WITHSCORES 直接返回结果
ZUNIONSTORE test:sum 2 test1 test AGGREGATE SUM 返回一个新的集合test:sum 结果都是一样的
在这里插入图片描述
聚合函数sum的并集:ZINTER 2 test1 test AGGREGATE SUM WITHSCORES
ZINTERSTORE test:sum1 2 test1 test AGGREGATE SUM
在这里插入图片描述
其他的max 、min同理

排行榜
设置一条新闻id为20220401的新闻 ZADD news:20220401 1 20220401
获取该新闻的热度:ZSCORE news:20220401 20220401
每次点击该新闻热度+1 ZINCRBY news:20220401 1 20220401
删除新闻:ZREM news:20220401 20220401
获取新闻集合中的新闻的个数 :ZCARD news:20220401
七日搜索榜单计算:ZUNIONSTORE news:20220401-20220407 7 key1 key2 ...key7 AGGREGATE SUM
展示七日排行前十:ZREVRANGE news:20220401-20220407 0 9 WITHSCORES
延迟队列
使用sorted_set,使用 【当前时间戳 + 需要延迟的时长】做score, 消息内容作为元素,调用zadd来生产消息,消费者使用zrangbyscore获取当前时间之前的数据做轮询处理。消费完再删除任务 rem key member

底层结构

ZSet 为有序的,自动去重的集合数据类型,ZSet 数据结构底层实现为 字典(dict) + 跳表(skiplist) ,当数据比较少时,用ziplist编码结构存储。
在这里插入图片描述
在这里插入图片描述
如上图使用ziplist存储,会把value和score分开存储
跳表:
在这里插入图片描述
说白了 跳表底层存储是用链表存储,把链表相隔一定距离的节点提取出来作为索引层,也即是层高。往上一层的层高,又从下面的层高根据一定的距离提取节点作为索引层,以此类推。查询数据的时候从最高层的进行查找。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值