likes
comments
collection
share

Redis-数据结构与经典使用场景

作者站长头像
站长
· 阅读数 16

Redis本质上是一个基于内存的存储系统,被我们大量的使用在项目中,那提到为什么用Redis,那么不可避免的一个理由就是,够快,根据官方文档数据,单台机器Redis可以支持10W/S的QPS。淘宝等大型电商公司除开大促活动,一般也不会达到这个级别的QPS。那Redis为什么会那么快呢?

Redis单线程为什么这么快?

因为它所有的数据都在内存中,所有的运算都是内存级别的运算,而且单线程避免了多线程的切换性能损耗问题。正因为 Redis 是单线程,所以要小心使用 Redis 指令,对于那些耗时的指令(比如keys),一定要谨慎使用,一不小心就可能会导致 Redis 卡顿。

Redis6.0即将推出,这个版本会将单线程变为多线程,届时性能将大幅提高。

那既然是单线程,那如何处理并发客户端链接?

Redis的IO多路复用:redis利用epoll来实现IO多路复用,将连接信息和事件放到队列中,依次放到文件事件分派器,事件分派器将事件分发给事件处理器。见如下图: Redis-数据结构与经典使用场景

查看redis支持的最大连接数,在redis.conf文件中的“maxclients”参数可修改。默认10000

Redis基本结构

Redis常用5种数据结构string,hash,list,set,zset,如下图所示: Redis-数据结构与经典使用场景

String

这种结构是最基本常用的的key-value结构 1. 基本操作命令

SET  key  value 			//存入字符串键值对
MSET  key  value [key 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

2. 分布式锁,利用setnx命令

SETNX  product:10001  true 		//返回1代表获取锁成功
SETNX  product:10001  true 		//返回0代表获取锁失败
DEL  product:10001			    //执行完业务释放锁
SET product:10001 true  ex  10  nx	//增加一个10S的超时时间,避免程序挂了,锁一直不释放

3. 计数器 下图是微信公众号里面的文章,利用INCR实现阅读数的增加 Redis-数据结构与经典使用场景

INCR article:readcount:{文章id} //文章阅读数+1
GET article:readcount:{文章id}  //查看文章阅读数

4. 分布式系统全局ID 如果是单机系统,生成ID可以直接利用数据库自增ID来实现,但是涉及到分布式系统的话,数据会涉及到分库分表,所以仅仅利用数据库自增是无法解决问题的。当然目前有很多分布式ID解决方案。目前主流的有推特的“雪花算法”,美团的“Leaf框架”,以及我们今天要将的基于Redis的解决方案。 利用Redis生成全局的自增ID,我们第一个想到的就是利用INCR原子加命令

INCR orderId  //每执行一行,orderId就会+1

但是这种解决方案有一个显著的问题: 如果我一个大型系统,有几百张数据库表需要生成唯一ID,那么每张表生成一条数据之前都要调用INCR命令去生成一个唯一ID,这对于Redis资源是极大的浪费。所以我们稍微改造一下。

INCRBY  orderId  1000   //一次性拿1000个orderId

如下图 : Redis-数据结构与经典使用场景 每台服务一次性拿1000个ID,保存到服务器自己内存中,然后自己服务器内部保障这1000个ID的分配。

hash

hash这种结构,可以理解为一个key关联的value是一个map。它比上面描述单纯的k-v结构更易于管理,消耗的内存和cpu也更少。但是缺点是过期时间无法利用在field上,而且集群架构下不适合大规模使用。 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. 电商的购物车 我们以下图,一个电商购物车为例,看如何使用hash结果来实现购物车的一些功能 Redis-数据结构与经典使用场景 假设我们以用户ID(1001)为hash的KEY,商品ID(100088)作为某个用户的KEY里面的field,商品数量为field里面的value,那么我们利用hash可以做如下操作:

hset cart:1001 10088 1      //给用户1001添加商品10088,数量为1
hincrby cart:1001 10088 1   //用户1001将商品10088购买数量+1
hlen cart:1001              //获得用户1001购物车商品总数
hdel cart:1001 10088        //用户1001将10088商品从购物车删除
hgetall cart:1001           //获得用户1001购物车的所有商品以及购买数量

list

list结构,类似一个双向队列,队两头都可以进队和出队,而且该结构还实现了阻塞队列的功能。如下图: Redis-数据结构与经典使用场景 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. 利用list实现常用数据结构

Stack(栈) = LPUSH + LPOP //遵循先进后出  
Queue(队列)= LPUSH + RPOP  //遵循先进先出
Blocking MQ(阻塞队列)= LPUSH + BRPOP  //遵循先进先出,出队如果没有元素会阻塞

3. 微博或者微信公众号消息流 拿微信公众号举例,假设我关注了MacTalk这个公众号,然后MacTalk今天发布了一篇ID=10080的文档出来了,如下图: Redis-数据结构与经典使用场景 那么redis用如下命令可以实现:

LPUSH  {订阅号消息}:{MacTalk的ID}:{我的ID}  10018  //MacTalk发了一条最新的文章
LRANGE  {订阅号消息}:{MacTalk的ID}:{我的ID}  0  5  //查看最新的5条消息

set

set结构式不可重复集合 1. 常用操作命令

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中删除
SINTER  key  [key ...] 				    //交集运算
SINTERSTORE  destination  key  [key ..]	//将交集结果存入新集合destination中
SUNION  key  [key ..] 				    //并集运算
SUNIONSTORE  destination  key  [key ...] //将并集结果存入新集合destination中
SDIFF  key  [key ...] 				    //差集运算
SDIFFSTORE  destination  key  [key ...]	//将差集结果存入新集合destination中

2. 微信抽奖小程序 假设我要实现如下图这样的微信抽奖功能 Redis-数据结构与经典使用场景 参与抽奖者5人,这5人的ID分别是1001-1005。

1) 5个人点击参与抽奖加入集合
SADD lottery 1001 1002 1003 1004 1005
2) 查看参与抽奖的所有用户ID
SMEMBERS lottery
3) 抽取3名获奖者
SRANDMEMBER lottery 3  //可重复获奖,用户ID不会从集合中删除,可以参与下次抽奖
SPOP lottery 3         //不可重复获奖,用户ID从集合中删除,无法参与下次抽奖

3. 社交软件的点赞,收藏模型 点赞收藏在社交软件中是最常见的功能,用Redis可以轻松实现。 Redis-数据结构与经典使用场景

1) 用户1001给你这条消息点了个赞
SADD  thumbsUp:{消息ID}  1001
2) 用户1001取消点赞
SREM  thumbsUp:{消息ID}  1001
3) 检查用户1001是否点过赞
SISMEMBER  thumbsUp:{消息ID}  1001
4) 获取点赞的用户列表
SMEMBERS thumbsUp:{消息ID}
5) 获取点赞用户数 
SCARD thumbsUp:{消息ID}

4. 社交软件的关注模型 这也是社交软件最常见的功能,例如我关注的人,他关注的人,我和他共同关注的人,我可能认识的人等。如下图 Redis-数据结构与经典使用场景 在了解实现之前,先要理一下Redis集合操作。见如下图: Redis-数据结构与经典使用场景 图上有三个几个,set1,set2,set3,那么我们执行如下命令:

1) 这个很简单就是求交集,结果集合只有c
SINTER set1 set2 set3 -> {c}
2) 第二个求并集,也很好理解,结果集合有a,b,c,d,e
SUNION set1 set2 set3 -> { a,b,c,d,e }
3) 第三个求差集,这个有点难理解,是以set1作为基准,然后减去set2,set3中出现过的元素,最终得到set1剩下的元素
SDIFF set1 set2 set3 -> { a } // PS:set1->{a,b,c} 减去 set2+set3->{b,c,d,e} 所以set1剩下{a}

接下来我们模拟一个三国人物的关注模型

  1) 刘备关注了诸葛亮,关羽,张飞,孙权
    刘备set-> {诸葛亮,关羽,张飞,孙权}
  2) 曹操关注了刘备,关羽,诸葛亮,孙权,郭嘉
    曹操set-> {刘备,关羽,诸葛亮,孙权,郭嘉}
  3) 孙权关注了刘备,曹操,周瑜
    孙权set-> {刘备,曹操,周瑜}
  4) 刘备点开了曹操的新浪微博页面,查看“共同关注的人”
    SINTER 刘备set 曹操set--> {诸葛亮,关羽,孙权}
  5)刘备点开了曹操的新浪微博页面,查看“我关注的人也关注了他”,这个实际上就要判断刘备关注的人里面,有哪些人关注了曹操
    SISMEMBER 诸葛亮set 曹操
    SISMEMBER 关羽set 曹操
    SISMEMBER 张飞set 曹操
    SISMEMBER 孙权set 曹操 
  6)刘备在曹操的新浪微博页面,查看“我可能认识的人”,实际上就是把曹操关注的而刘备没有关注的人列出来
    SDIFF 曹操set 刘备set  -> {郭嘉}
  1. 电商商品筛选模型 在电商购物中,我们一般都会通过条件来筛选商品,例如购买手机如下图: Redis-数据结构与经典使用场景 这里面筛选条件有,品牌,操作系统,CPU品牌,内存等。那么这些商品在入库之前就会按照商品属性填充到各种set里面去。例如:
SADD  brand:huawei  P30   //P30放到华为品牌筛选条件下
SADD  brand:xiaomi  mi-6X  //mi-6x放到小米品牌筛选条件下
SADD  brand:iPhone iphone8  //iphone8 放到iphone品牌筛选条件下
SADD  os:android  P30  mi-6X  //P30,mi-6X放到安卓操作系统筛选条件下
SADD  cpu:brand:intel  P30  mi-6X  //P30,mi-6X放到CPU使用intel筛选条件下
SADD  ram:8G  P30  mi-6X  iphone8  //P30,mi-6X,iphone8都是8G手机,就放到8G这个筛选条件下

那么我要找安卓系统,CPU使用intel然后内存8G的手机,只要执行下面的命令:

SINTER  os:android  cpu:brand:intel  ram:8G   //最终找到这两个手机{P30,mi-6X}

zset

zset与set结构的不同在于,zset增加了一个score的属性,这个score属性主要用来排名的,下面我们来看看有哪些场景使用。 1. 常用命令操作

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下标的元素

//并集计算,举例(ZUNIONSTORE zset3 2 zset1 zset2)这句的意思是,zset1和zset2求并集得到的结果放入新的zset3里面
ZUNIONSTORE destkey numkeys key [key ...] 	
//交集计算,举例(ZINTERSTORE zset3 2 zset1 zset2)这句的意思是,zset1和zset2求交集得到的结果放入新的zset3里面
ZINTERSTORE destkey numkeys key [key ...]	

2. 热搜榜或者新闻排行榜 下图是微博热搜的展示图: Redis-数据结构与经典使用场景 假设今天日期是20190819,我们zset来实现新闻热搜排行榜:

1) 点击一次”守护香港“这个新闻,那么此新闻的score属性+1
  ZINCRBY  hotNews:20190819  1  守护香港
2) 展示当前热搜新闻的前10名(最后的WITHSCORES代表查询出的结果包含具体的分数)
  ZREVRANGE  hotNews:20190819  0  10  WITHSCORES

那么我们要做一个7日的新闻排名呢?如下图: Redis-数据结构与经典使用场景

 1) 首先我们需要把7天的新闻合并到"hotNews:20190813-20190819"这个zset中去
   ZUNIONSTORE  hotNews:20190813-20190819  7 hotNews:20190813  hotNews:20190814... hotNews:20190819
 2) 然后再展示7日排名前十的新闻
   ZREVRANGE hotNews:20190813-20190819  0  10  WITHSCORES