从零开始学Redis系列(二)| Redis常用命令如何在企业中重拳出击

这是我参与更文挑战的第2天,活动详情查看:更文挑战

前言

相信大家在日常工作中多多少少有用过Redis,得益于Redis丰富的数据结构,使它在企业的项目中应用越来越广泛,充当的角色越来越重要。

随着redis在企业中的应用被越挖越深,但是也有越来越多的开发人员由于没有经历过复杂/特定的业务,无法对redis的命令进行深入了解,甚至可能只了解到对象缓存为止

所以这篇文章的目的就是带着大家走完以下两个里程碑:

  • 了解redis的常用命令
  • 如何在企业级项目中重拳出击

常用命令

String

常用操作

  • set key value:存入字符串键值对
  • mset key value [value ...]:批量存入字符串键值对
  • setnx key value:存入一个不存在的字符串键值对
  • get key:获取一个字符串键值
  • mget key [key ...]:批量获取字符串键值
  • del key [key ...]:删除一个键
  • expire key seconds:设置一个键的过期时间(秒)

原子加减

  • incr key:将key中存储的数字值+1
  • decr key:将key中存储的数字值-1
  • incrby key increment:将key中所存储的值加上increment
  • decrby key decrement:将key中所存储的值减去decrement

Hash

常用操作

  • 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

List

  • 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:假如在指定时间内没有任何元素被弹出,则返回一个 nil 和等待时长,如果timeout=0,就一直阻塞等待。反之,返回一个含有两个元素的列表,第一个元素是被弹出元素所属的key,第二个元素是从key列表表头弹出一个元素
  • brpop key [key ...] timeout:假如在指定时间内没有任何元素被弹出,则返回一个 nil 和等待时长,如果timeout=0,就一直阻塞等待。反之,返回一个含有两个元素的列表,第一个元素是被弹出元素所属的key,第二个元素是从key列表表尾弹出一个元素

Set

常用操作

  • sadd key value:往集合key中存入元素,元素存在则忽略
  • srem key value:往集合key中删除元素
  • smembers key:获取集合key中所有元素
  • srandmember key [count]:从集合key中选出count个元素,元素不从key中删除
  • spop key [count]:从集合key中选出count个元素,元素从key中删除

运算操作

  • sinter key [key ...]:交集运算
  • sinterstore destination key [key ...]:交集运算,将结果存入新集合destination中
  • sunion key [key ...]:并集运算
  • sunionstore destination key [key ...]:并集运算,将结果存入新集合destination中
  • sdiff key [key ...]:差集运算
  • sdiffstore destination key [key ...]:差集运算,将结果存入新集合destination中

ZSet-有序集合

常用操作

  • zadd key score member [[score member]...]:往有序集合中加入带分值的元素
  • zrem key member [member ...]:从有序集合中删除元素
  • zscore key member:返回有序集合key中元素member的分值
  • zincrby key increment member:将有序集合中元素member的分值加上increment
  • zcard key:返回有序集合key中元素的个数
  • zrange key start stop [withscores]:正序获取有序集合key从start下标到stop下标的元素
  • zrevrange key start stop [withscores]:倒序获取有序集合key从start下标到stop下标的元素

集合操作

  • zunionstore destkey numkeys key [key ...] :并集计算
  • zinterstore destkey numkeys key [key ...] :交集计算

其他命令

  • keys:全量遍历,用来列出所有满足特定正则字符串规则的key,当redis数据量比较大时,性能比较差,要避免使用

image.png

  • scan:渐进式遍历,scan cursor [MATCH pattern] [COUNT count]

scan 参数提供了三个参数:

  • 第一个:cursor:游标,如果要遍历全量数据,一开始的游标一定要是0
  • 第二个:match:正则匹配
  • 第三个:count:一次遍历的key的数量参考值,底层遍历的数量不一定,结果数量有可能不符合

应用场景

String

单值缓存

  • set key value:设置单值缓存
  • get key:获取单值缓存

image.png

对象缓存

  • set student value(json格式数据):设置完整对象缓存
  • MSET student:name aoteman student:age 18:设置对象部分数据缓存,其实就是批量设置key,一个key就是一个对象中的一个字段
  • MGET student:name student:age:获取对象部分数据缓存,就是批量获取key

image.png

  • 第一种方式:set student value,比较简单,但是更新相对来说复杂
  • 第二种方式:MSET student:name aoteman student:age 18,比较灵活,如果遇到频繁修改某个对象的部分数据,可以使用这个命令,比如:只需要修改商品活动价格,没必要把整个商品对象取出来转换成对象更新后,再设置回去,但是这个命令会创建大量的key,也是一个弊端

简单版分布式锁

  • setnx lock true:设置分布式锁,返回0代表设置失败,返回1代表设置成功

image.png

  • del lock:删除/释放锁

image.png

  • set lock true ex 10 nx:设置分布式锁的超时时间,防止程序意外终止导致死锁

image.png

计数器

  • incr key:计数器+1
  • get key:获取计数器值

image.png

应用场景:像文章的阅读量、点赞、赞赏量等等需要计数的显示等等,设置key为article:{read/like/...}:count:{id}这种格式即可,只要能确定key的唯一性,如果把这三个属性放到DB,数据库的压力就太大了,显然不合适

image.png

分布式系统全局序列号

  • incrby key valueincrby student 1000:直接往key为student的值往上加1000

应用场景:分布式系统全局序列号,每次key值上加value,拿到返回值后,在本机中去分配,在本机的内存中去把id+1,如果用incr命令的话,每次生成都去请求redis,redis的压力会非常大

用这种方式生成的全局序列号是不连续的,如果有多台机器的话,如果要连续就需要其他解决方案

分布式session

spring + redis实现session共享

image.png

Hash

对象缓存

  • hmset user {userId}:name aoteman {userId}:age 18:设置对象缓存
  • hmget user {userId}:name {userId}:age:获取对象缓存

image.png

image.png

这个应用场景有一个弊端,就是数据量的问题,假设user表有几千万条记录,外面就通过一个user的key来获取,特别是在检索时会非常耗时,而且redis是单线程模型,会非常影响吞吐量,这种key也被称之为big key

电商中的购物车模型

image.png

  • 添加商品:hset cart:{用户id} {商品id} {商品个数}

image.png

  • 购物车商品数量增加:hincrby cart:{用户id} {商品id} {商品个数}

image.png

  • 查询购物车商品数量:hlen cart:{用户id}

image.png

  • 删除购物车某一个商品:hdel cart:{用户id} {商品id}

image.png

  • 获取购物车所有商品:hgetall cart:{用户id}

image.png

Hash的优缺点

优点

  1. 同类数据归类整合存储,方便数据管理,比如上面一个user的key可以管理所有的用户
  2. 同样大小的一个对象,使用Hash比String操作消耗内存更小
  3. 同样大小的一个对象,使用Hash比String更节省空间

缺点

  1. Hash的过期功能不能使用在内层的field上,只能使用在外层的key上,也就是一旦key过期,key底下的数据全都生效

  2. Redis集群架构下不适合大规模使用,假设user的key底下有几千万的数据量,那么根据hash路由下来,这一个key只可能分配给一台机器,也就是几千万的数据都在一台redis上,访问也都是访问在这台redis上,发生了数据倾斜的场景,可以通过分段存储解决这种case

image.png

List

数据结构

在分布式/多台机器环境下,想用统一的数据结构,那么redis会是一种很好的解决方案

  • Stack(栈):lpush + lpop = 先进后出(FILO)

image.png

  • Queue(队列):lpush + rpop = 先进先出(FIFO)

因为dijia已经被移除并返回了,所以现在最右端是aisi

image.png

  • Blocking MQ(阻塞队列):lpush + brpop,和消息队列中的消费者类似

image.png

image.png

推送微博和微信公众号等消息流

image.png

推送消息流案例

微博ABAB关注了A、B这两个微博/公众号消息同理:

  • A发微博消息,消息ID为100:lpush msg:{微博ABAB的ID} {文章ID}
  • B发微博消息,文章ID为101:lpush msg:{微博ABAB的ID} {文章ID}
  • 微博ABAB登录查看最新的微博消息:lrange msg:{微博ABAB的ID} 0 4:获取最新的5条消息/最左边就是最新的消息

image.png

微博的消息流推送方式就是给所有关注它的粉丝的消息列表里面push文章,粉丝按照顺序拿消息

但这种方式有一个致命的缺点:微博粉丝数量过大,假设一个公众号粉丝过千万,那么每一次发布文章,都需要推送几千万个列表,势必会造成性能瓶颈,所以就需要另外的一种方式:pull的方式

A发微博把消息放到一个专门属于A的消息队列,粉丝账号登录的时候主动去拉取这个队列,pull之后存放在本地,如下图所示:

image.png

Set

各种抽奖场景

image.png

  • sadd key {userId}:点击参与抽奖加入集合
  • smembers key:查看参与抽奖的所有用户
  • srandmember key [count]/spop key [count]:抽取count名中奖者

image.png

中奖数量只需要改变一下count大小即可,比如:一等奖1名,二等奖2名,三等奖5名,使用spop key [count]命令让中奖的人从集合中删除

微信、微博、抖音点赞(收藏、标签同理)

image.png

  • 点赞:sadd like:{文章ID} {用户ID}
  • 取消点赞:srem like:{文章ID} {用户ID}
  • 检查用户是否点过赞:sismember like:{文章ID} {用户ID}抖音的点赞爱心高亮就可以通过这个命令实现
  • 获取点赞的用户列表:smembers like:{消息ID}
  • 获取点赞的用户数:scard like:{消息ID}

集合操作

image.png

  • sinter setA setB setC -> {C}:交集
  • sunion setA setB setC -> {A,B,C,D,E}:并集
  • sdiff setA setB setC -> {A}:以setA集合为基准,和setB、setC集合的并集做差集

集合操作实现社交关注模型

image.png

社交关注模型案例

  • 泰罗奥特曼关注的其他奥特曼:tailuo:Set -> {aisi, dijia}

  • 艾斯奥特曼关注的其他奥特曼:aisi:Set -> {tailuo, dijia, leiou, gaiya}

  • 迪迦高特曼关注的其他奥特曼:dijia:Set -> {aisi, saiwen, zuofei}

第一种场景:泰罗和艾斯共同关注的奥特曼sinter tailuo:Set aisi:Set -> {dijia}

第二种场景:泰罗可能认识的奥特曼/泰罗关注的奥特曼也关注的奥特曼:sdiff aisi:Set tailuo:Set -> {leiou, gaiya}

集合操作实现筛选

把商品的特点做成筛选条件,一个筛选条件是一个Set集合,最后取交集,可以实现筛选

image.png

  • sadd brand:huawei 商品ID
  • sadd brand:xiaomi 商品ID
  • sadd phoneType:picture 商品ID
  • sadd rom:512 商品ID
  • ……

sinter brand:huawei phoneType:picture rom:512 …. -> {商品ID集合}

用redis做这样的商品搜索显然不太合适,这个案例希望告诉大家,Set数据结构,针对操作集合的场景有天然的优势

ZSet

排行榜:热搜排行、音乐排行、新闻排行等等都可以用ZSET实现

  • 点击新闻:zincrby new:{日期} 1 {消息ID}
  • 展示当日排行前十:zrevrange new:{日期} 0 9 withscores
  • 七日搜索榜单计算:zunionstore new:{日期开始-结束} 7 new:{日期} new:{日期+1} new:{日期+2}...
  • 展示七日榜单前十:zrevrange new:{日期开始-结束} 0 9 withscores

image.png

SCAN

Redis分页

假设现在有这么多的key,如下图所示:

image.png

我们把count设置成3,发现没有匹配到,因为count是一次遍历的key的数量,也就是从上面总的key中选3个key来遍历匹配,如果没有匹配到,就返回empty array,返回的2就是

image.png

第一次遍历时,cursor为0,然后将返回结果中第一个整数作为下一次遍历的cursor。一直遍历到cursor为0为止,算是所有的key扫描完了

image.png

这个就是redis的分页,但是这个分页不像mysql那样能做到精确匹配

高能预警

scan虽然能做到分页,但是并非完美无瑕,如果在scan的过程中如果有键的变化(增加、删除、修改),那么遍历效果可能会碰到如下问题,这些问题都是需要我们在开发的时候需要考虑的

  1. 新增的键可能没有遍历到
  2. 遍历出了重复的键

因为Redis的存储结构是以k-v的形式存储的,类似于HashMap,key通过Hash函数定位到一个下标或者说是桶位,如果发生hash冲突/碰撞,又会以类似链表的方式存储,这个下标就是scan的游标

image.png

  • 如果在scan的过程中增加键,同时键通过hash算出的桶位已经被扫描过,那么是不会再次扫描的,就会发生新增的键可能没有遍历到

  • 如果在scan的过程中增加键,导致发生扩容的场景,扩容之后会有rehash机制,那么原先下标就会发生变化,就会发生遍历出了重复的键


总结

通过本篇文章对不同数据结构中的Redis常见命令企业级的常见场景以及各自注意事项的介绍,应该已经了解到下面几块内容:

  • 不同的数据结构对应有哪些常见的命令
  • 常见的Redis命令有哪些企业级的应用场景

Redis的场景肯定不仅仅如此,这里只是将我能想到的场景写在上面了,最终还是需要大家自己去琢磨,透过现象看本质,想想在自己负责的领域里,有哪些业务可以用Redis丰富的数据结构优化的,欢迎底下评论区探讨,顺便让我也开开眼界


往期推荐

从零开始学Redis系列(一)| Redis环境搭建


最后,如果感到文章有哪里困惑的,请第一时间留下评论,如果各位看官觉得小沙弥我有点东西的话 求点赞? 求关注❤️ 求分享? ,因为这将是我输出更多优质文章的动力,感谢!!!

如果想获取Redis相关书籍,可以关注微信公众号Java百科全书输入Redis,即可获得

最后感谢各位看官的支持,我们下期再见!

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享