常用数据结构实战和底层分析
redis的常用命令不在展示,可以通过help命令进行查看。比如help @数据类型
可以查看对应的api命令
String
使用场景
单值缓存
例如缓存商品编号为1001的价格 :set goods:1001 28.5
对象缓存
set user:1 value( user的json格式数据)
也可以通过hash进行对象缓存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特点:
- 二进制安全的数据结构
- 提供了内存预分配机制、避免内存频繁分配
- 兼容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存储数据。
- 元素个数大于 set-max-intset-entries ,
- 元素无法用整形表示
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分开存储
跳表:
说白了 跳表底层存储是用链表存储,把链表相隔一定距离的节点提取出来作为索引层,也即是层高。往上一层的层高,又从下面的层高根据一定的距离提取节点作为索引层,以此类推。查询数据的时候从最高层的进行查找。