Redis核心数据结构应用场景与高性能原理刨析
后续有两个概念比较模糊,后续需要进行处理
1、Redis的数据结构
Redis有5种数据结构,分别为String(字符串)、Hash(哈希)、List(列表),Set(集合)、ZSet(有序集合)。
1.1、String
1、单值缓存
set key value
get key
2、对象缓存
2.1、将对象式的value抽象成一个json格式,然后就可以跟单值缓存一样了
set user:1 value
get user:1
2.2、批量操作,一次存多个键值对
命令 | 含义 |
---|---|
mset user:1:name lele user:1:balance 1800 | 批量操作存储 |
mget user:1:name user:1:balance | 批量操作获取 |
3、分布式锁
命令 | 含义 |
---|---|
setnx product:100 true | 获取当前key的锁,返回1代表取锁成功 |
setnx product:100 true | 获取当前key的锁,返回0代表取锁失败 |
执行相应的业务操作,加积分什么的
命令 | 含义 |
---|---|
del product:100 | 删除当前key |
set product:100 true ex 10 nx | 设置超时时间,默认为秒 |
这几行命令的意思是多个线程同时对同一商品进行操作,只有第一个才能获取锁成功,获取到锁之后,进行一系列的业务操作,减去相应的库存,并将这个锁释放掉,但为了防止意外,可以设置这个锁的超时时间,单位是秒。
4、计数器
命令 | 含义 |
---|---|
incr article:readcount:{文章id} | 给当前key值的积分加1 |
get article:readcount:{文章id} | 获取当前key值的积分 |
这行命令的意思是每执行一次 incr article:readcount:{文章id} 命令,就会加1,当执行get命令时,就会获取到加的总数,比如我们平时看的微信小文章中的阅读次数就可以用这个做。
5、指定增量计数器
incrby key 100
给对应的key加上100
1.2、Hash
1、常用操作
命令 | 含义 |
---|---|
HSET key field value | 存储一个哈希表key的键值 |
HSETNX key field value | 存储一个不存在的哈希表key的键值 |
HMSET key field value [field value …] | 在一个哈希表key中存储多个键值对 |
HGET key field | 获取哈希表key对应的field键值 |
HMGET key field [field …] | 批量获取哈希表key中多个field键值 |
HDEL key field [field …] | 删除哈希表key中的field键值 |
HLEN key | 返回哈希表key中field的数量 |
HGETALL key | 返回哈希表key中所有的键值 |
HINCRBY key field increment | 为哈希表key中field键的值加上增量increment |
2、对象缓存
命令 | 含义 |
---|---|
hmset user {userid}:name lele {userid}:balance 100 | 设置hash对象 |
hmget user {userid}:name lele {userid}:balance 100 | 获取hash对象 |
hmset user 1:name lele 1:balance 100
hmget user 1:name 1:balance
hgetall key
获取全部的值
优缺点:
优点
1、同类数据归类整合储存,方便数据管理。
2、相比string操作消耗内存与cpu更小
3、相比string存储更节省空间
缺点:
1、过期功能不能使用在field上,只能使用在key上
2、redis集群架构下不适合大规模使用
1.3、List
1、常用操作
命令 | 含义 |
---|---|
LPUSH key value [value …] | 将一个或多个值value插入到key列表的表头(最左边) |
RPUSH key value [value …] | 将一个或多个值value插入到key列表的表尾(最右边) |
LPOP key | 移除并返回key列表的头元素 |
RPOP key | 移除并返回key列表的尾元素 |
LRANGE key start stop | 返回列表key中指定区间内的元素,区间以偏移量start和stop指定 |
BLPOP key [key …] timeout | 从key列表表头弹出一个元素,若列表中没有元素,阻塞等待timeout秒,如果timeout=0,一直阻塞等待 |
BRPOP key [key …] timeout | 从key列表表尾弹出一个元素,若列表中没有元素,阻塞等待timeout秒,如果timeout=0,一直阻塞等待 |
2、常用数据结构也可以用redis的list轻松实现
Stack(栈) = LPUSH + LPOP
Queue(队列)= LPUSH + RPOP Blocking
MQ(阻塞队列)= LPUSH + BRPOP
这个阻塞队列相当于,我要去里面拿东西,但是现在里面没有了,所以我就执行 BRPOP 让队列阻塞,知道队列中有了,我再执行,可以设置超时时间。
3、案例
平时我们刷的微博或者朋友圈等 ,每次的消息都是发的时间最晚的在最上面,这种用redis也可以实现
比如小明现在关注了 A 和 B ,当前,A 先发了一条消息,随后,B 也发了一条消息,B 发消息的时间比 A 的晚,那么在小明这里的排序应该是 B的消息在上面,A 的消息在下面,这里其实是当有消息时,我们可以用 LPUSH 命令将消息放到队列中,然后使用 LRANGE 命令将需要展示的消息展示出来。但是这里我们就会发现一个问题,每一个人都会有自己的队列,然后通过 LRANGE 命令去操作它,这里之前解决办法有两种方案,一种是push,一种是pull。
push是每发一条消息,会把自己的消息id全部告诉粉丝,然后粉丝存到自己的队列中,这种在一个人有上亿粉丝时就不太适用了,但是有些业务场景可以做成先发给在线粉丝,非在线的后续可以慢慢发。
pull是每发一条消息,然后把这个消息放到一个公共的地方,粉丝们自己去访问然后放到自己的队列中。
1.4、Set
1、Set常用操作
命令 | 含义 |
---|---|
SADD key member [member …] | 往集合key中存入元素,元素存在则忽略,若key不存在则新建 |
SREM key member [member …] | 从集合key中删除元素 |
SMEMBERS key | 获取集合key中所有元素 |
SCARD key | 获取集合key的元素个数 |
SISMEMBER key member | 判断member元素是否存在于集合key中 |
SRANDMEMBER key [count] | 从集合key中选出count个元素,元素不从key中删除 |
SPOP key [count] | 从集合key中选出count个元素,元素从key中删除 |
2、Set运算操作
命令 | 含义 |
---|---|
SINTER key [key …] | 交集运算 |
SINTERSTORE destination key [key …] | 将交集结果存入新集合destination中 |
SUNION key [key …] | 并集运算 |
SUNIONSTORE destination key [key …] | 将并集结果存入新集合destination中 |
SDIFF key [key …] | 差集运算 |
SDIFFSTORE destination key [key …] | 将差集结果存入新集合destination中 |
3、案例
抽奖,比如抽奖是下面这张图
当有一个人点击抽奖时,我们执行 sadd key {userId} 命令去给set集合里面添加一个用户id。
当点击查看所有参与用户的时候,我们可以使用命令 smembers key 去显示出所有的用户。
抽取中奖用户的时候我们可以使用命令 SRANDMEMBER key [count] 或者 SPOP key [count],
SRANDMEMBER key [count] 命令只适用于抽一次奖的那种,因为筛选完之后,筛选出的用户还在set集合中;
SPOP key [count] 命令适合于多次抽奖,比如一、二、三等奖,因为筛选完之后,筛选到的用户会被移除set集合。
关注模型
me关注的人:{A1,A2,A4,A6,A8}
A1关注的人:{A2,A3,A4}
A2关注的人:{A1,A4,A5,A6}
A3关注的人:{A6,A7,A8}
A4关注的人:{A2,A5}
me和A2的共同关注:{A1,A4,A6}
A1关注的人也关注着A2:{A4} --也就是看A1关注的每一个人的关注列表中有没有A2
命令如下图:
1.5、ZSet
1、Set常用操作
命令 | 含义 |
---|---|
ZADD key score member [[score member]…] | 往有序集合key中加入带分值元素 |
ZREM key member [member …] | 从有序集合key中删除元素 |
ZSCORE key member | 返回有序集合key中元素member的分值 |
ZINCRBY key increment member | 为有序集合key中元素member的分值加上increment |
ZCARD key | 返回有序集合key中元素个数 |
ZRANGE key start stop [WITHSCORES] | 正序获取有序集合key从start下标到stop下标的元素 |
ZREVRANGE key start stop [WITHSCORES] | 倒序获取有序集合key从start下标到stop下标的元素 |
2、Zset集合操作
命令 | 含义 |
---|---|
ZUNIONSTORE destkey numkeys key [key …] | 并集计算 |
ZINTERSTORE destkey numkeys key [key …] | 交集计算 |
3、案例
这个微博热搜相信我们也是经常见到的,他们上面的排行以及阅读量都可以用redis来实现。
1、可以通过ZINCRBY key increment member命令给文章加上分值,也就是打开了多少次,默认是1。
2、获取当天的排行可以用倒叙排序展示的命令
ZREVRANGE key start stop [WITHSCORES]
[WITHSCORES] 这个的意思是加上分值
我这里是乱码,能说明意思就好
3、如果要展示七天的总和,我们可以求并集再排序
zunionstore hotNews:20180813-20180819 7 hotNews:20180813 hotNews:20180814 hotNews:20180815 hotNews:20180816 hotNews:20180817 hotNews:20180818 hotNews:20180819
这行命令的意思是对7个key求并集并存储到 hotNews:20180813-20180819 key中
2、Redis高性能原理
2.1、Redis是单线程的吗?
其实严格意义上来说,Redis不是单线程的,比如Redis的持久化、异步删除、集群数据这些都是由额外的线程来完成的。
我们平时说的Redis单线程其实是是指从其它端发送给Redis的IO操作都是由一个线程来完成的,外部访问Redis时,Redis会将这些命令排好序,然后一个一个执行。
2.2、Redis单线程为什么还能这么快?
因为Redis的操作是在内存上的,所有的操作都是内存级别的,而且单线程也避免了多线程之间的线程切换损耗,但是在操作时一定要注意,尤其是bigKey,一旦操作失误就会造成Redis卡顿。
2.3、Redis 单线程如何处理那么多的并发客户端连接?
Redis的IO多路复用:redis利用epoll来实现IO多路复用,将连接信息和事件放到队列中,依次放到文件事件分派器,事件分派器将事件分发给事件处理器。
这块这个概念还是有点模糊,后续进行深层次的学习可能会有其它博客解释
3、Redis高级命令
3.1、keys *
遍历全库,效率比较低,可支持通配符,如下图所示:
3.2、scan渐进式遍历键
SCAN cursor [MATCH pattern] [COUNT count] scan 参数提供了三个参数,第一个是 cursor 整数值(hash桶的索引值),第二个是 key 的正则模式, 第三个是一次遍历的key的数量(参考值,底层遍历的数量不一定),并不是符合条件的结果数量。第 一次遍历时,cursor 值为0,然后将返回结果中第一个整数值作为下一次遍历的 cursor。一直遍历 到返回的 cursor 值为 0 时结束。
注意:但是scan并非完美无瑕, 如果在scan的过程中如果有键的变化(增加、 删除、 修改) ,那 么遍历效果可能会碰到如下问题:
新增的键可能没有遍历到, 遍历出了重复的键等情况(有可能在遍历期间又插入新数据,产生了rehash()), 也就是说 scan并不能保证完整的遍历出来所有的键, 这些是我们在开发时需要考虑的。
这块这个概念还是有点模糊,后续进行深层次的学习可能会有其它博客解释
代码示例如下:
从图中就可以看出,每一次的结果不一定都是固定的,每一次返回的数字都是下一次扫描的起点游标,直到返回0结束。