01-Redis核心数据结构和高性能原理

2021/7/17 19:06:49

本文主要是介绍01-Redis核心数据结构和高性能原理,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

Redis核心数据结构和高性能原理

  • Redis安装
  • 核心数据结构以及用法
    • String结构
      • 字符串常用操作
      • 原子加减
      • 应用场景
    • Hash结构
      • Hash常用操作
      • Hash应用场景
      • Hash结构优缺点
    • List结构
      • List常用操作
      • List应用场景
    • Set结构
      • Set常用操作
      • Set运算操作
      • Set应用场景
    • ZSet有序集合结构
      • ZSet常用操作
      • Zset集合操作
      • Zset应用场景
  • Redis的单线程和高性能

重要的事说:每一个字都是拾光手敲的,希望你可以一字一字的去阅读和理解拾光在写博客时的思想。希望你有所收获,谢谢!
这是第一篇关于redis的文章,因为redis在我们工作当中可以说是有举足轻重的地位,后续我会写到redis的持久化以及主从架构,哨兵模式,集群模式,redis核心原理,redis的缓存设计与性能优化,redis6的新特性,以及redis的C源码。
今天说redis的五种数据结构与单线程高性能原理

Redis安装

redis中文官网地址

下载地址:http://redis.io/download
安装步骤:
# 安装gcc
yum install gcc

# 把下载好的redis-5.0.3.tar.gz放在/usr/local文件夹下,并解压
wget http://download.redis.io/releases/redis-5.0.3.tar.gz
tar xzf redis-5.0.3.tar.gz
cd redis-5.0.3

# 进入到解压好的redis-5.0.3目录下,进行编译与安装
make

# 修改配置
daemonize yes  #后台启动
protected-mode no  #关闭保护模式,开启的话,只有本机才可以访问redis
# 需要注释掉bind
#bind 127.0.0.1(bind绑定的是自己机器网卡的ip,如果有多块网卡可以配多个ip,代表允许客户端通过机器的哪些网卡ip去访问,内网一般可以不配置bind,注释掉即可)

# 启动服务
src/redis-server redis.conf

# 验证启动是否成功 
ps -ef | grep redis 

# 进入redis客户端 
src/redis-cli 

# 退出客户端
quit

# 退出redis服务: 
(1)pkill redis-server 
(2)kill 进程号                       
(3)src/redis-cli shutdown 

核心数据结构以及用法

String结构

字符串常用操作

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

应用场景

String应该是我们工作当中用的最为普遍的一种数据类型,上面以及罗列了一些常用的命令。接下来让我们看看他们有哪些使用场景。

  • 单值缓存
SET  key  value 	
GET  key 

这种就是最简单的保存一个字符串,然后get获取

  • 对象缓存
SET  user:1  value(json格式数据)
MSET  user:1:name  zhuge   user:1:balance  1888
    MGET  user:1:name   user:1:balance 

如果我们要存储一个对象的数据,我们平时会把一个对象数据变换为json存储到redis,而MSET就是批量存储的命令,如果有很多的数据对象,每次存储都要和redis进行io交互,那么我就就可以用MSET来批量保存。

  • 分布式锁
SETNX  product:10001  true 		//返回1代表获取锁成功
SETNX  product:10001  true 		//返回0代表获取锁失败
。。。执行业务操作
DEL  product:10001			//执行完业务释放锁

SET product:10001 true  ex  10  nx	//防止程序意外终止导致死锁

SETNX 是『SET if Not eXists』(如果不存在,则 SET)的简写。
用来做分布式锁,如果能保存成功,说明就拿到锁了,加锁失败,说明锁已经被别人占用了。
其实分布式锁远远没有这么简单,如果只是保存了数据,没有设置过期时间,那么获得锁的程序如果卡死,后面所有的业务都不会拿到锁,导致系统崩溃。如果设置了过期时间,那么删除锁的时候也会有一个问题,就是第一个线程加锁了,执行代码时间比较长,锁自动过期了,那么第二个线程就可以加锁了,第二个线程加完锁以后第一个线程代码执行完成删除锁的时候把第二个线程的锁删了,因为第一个线程的锁已经自动过期删除了等等一系列的问题,后面针对redis做分布式锁我会单独说一些会出现的问题和一些大厂现在的解决思路。

  • 计数器
INCR article:readcount:{文章id}  	
GET article:readcount:{文章id} 

可以在如下图所示的地方,显示阅读数的时候,每个人只要点击文章一次就执行INCR操作进行加1.
在这里插入图片描述
INCR 将 key 中储存的数字值增一。 如果 key 不存在,那么 key 的值会先被初始化为 0 ,然后再执行 INCR 操作。 如果值包含错误的类型,或字符串类型的值不能表示为数字,那么返回一个错误。 本操作的值限制在 64 位(bit)有符号数字表示之内。 注:这是一个针对字符串的操作,因为 Redis 没有专用的整数类型,所以 key 内储存 的字符串被解释为十进制 64 位有符号整数来执行 INCR 操作。
场景一: 计数器
计数器是 Redis 的原子性自增操作可实现的最直观的模式了,它的想法相当简单:每 当某个操作发生时,向 Redis 发送一个 INCR 命令。 比如在一个 web 应用程序中,如果想知道用户在一年中每天的点击量,那么只要将用 户 ID 以及相关的日期信息作为键,并在每次用户点击页面时,执行一次自增操作即可。 比如用户名是 peter ,点击时间是 2012 年 3 月 22 日,那么执行命令 INCR peter::2012.3.22
场景二:限速器
限速器是特殊化的计算器,它用于限制一个操作可以被执行的速率(rate)。 限速器的典型用法是限制公开 API 的请求次数,以下是一个限速器实现示例,它将 API 的最大请求数限制在每个 IP 地址每秒钟十个之内:

FUNCTION LIMIT_API_CALL(ip)

ts = CURRENT_UNIX_TIME()

keyname = ip+":"+ts

current = GET(keyname)

IF current != NULL AND current > 10 THEN

ERROR "too many requests per second"

END

IF current == NULL THEN

MULTI

INCR(keyname, 1)

EXPIRE(keyname, 1)

EXEC

ELSE

INCR(keyname, 1)
END

PERFORM_API_CALL()
  • Web集群session共享
    spring session + redis实现session共享

  • 分布式系统全局序列号

INCRBY  orderId  1000		//redis批量生成序列号提升性能

这样的场景可以用在生成自增的id,在数据库中使用。
格式:incrby key increment 将 key 所储存的值加上增量 increment 。 如果 key 不存在,那么 key 的值会先被初始化为 0 ,然后再执行 INCRBY 命令。 如果值包含错误的类型,或字符串类型的值不能表示为数字,那么返回一个错误。 本操作的值限制在 64 位(bit)有符号数字表示之内。
示例代码:

# key 存在且是数字值

redis> SET rank 50

OK
redis> INCRBY rank 20

(integer) 70

redis> GET rank

"70"

# key 不存在时

redis> EXISTS counter

(integer) 0

redis> INCRBY counter 30

(integer) 30

redis> GET counter

"30"

# key 不是数字值时

redis> SET book "long long ago..."

OK

redis> INCRBY book 200

(error) ERR value is not an integer or out of range

当然了此处也会有弊端,就是如果是订单数据,一小时如果有上亿的数据id需要生成,那么使用redis生成也是耗费性能耗费内存的,我们可以使用雪花算法和一些比如美团开源的leaf生成器。

Hash结构

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

其实Hash就是维护一个数组,然后在这个数组上可能还有很多的链表去存储数据。

Hash应用场景

  • 对象缓存
HMSET  user  {userId}:name  zhuge  {userId}:balance  1888
HMSET  user  1:name  zhuge  1:balance  1888
HMGET  user  1:name  1:balance 

可以把一个hash操作就看作是一个数据库表,用户表,如果要存储用户信息,只需要根据用户id去存储到user这个槽位,也就是用户表里面。
在这里插入图片描述
这样存储可以节约成本,节约存储空间,但是什么东西都有利弊,如果这样存储数据,就会形成Redis最不能接受的BigKey的问题,会大大的影响性能,有什么解决办法呢,我们可以考虑使用集群模式分片存储,后面会写到16384个槽位分片存储的用法。

  • 电商购物车
    在这里插入图片描述
1)以用户id为key
2)商品id为field
3)商品数量为value
  • 购物车操作
添加商品:hset cart:1001 10088 1
增加数量:hincrby cart:1001 10088 1
商品总数:hlen cart:1001
删除商品:hdel cart:1001 10088
获取购物车所有商品:hgetall cart:1001

我觉得用Hash用来存储购物车数据是比String更好的一个选择。具体他的性能会有什么样的提升,后期我会写到redis他自己内部分装的数据结构的原理。

Hash结构优缺点

  • 优点
1)同类数据归类整合储存,方便数据管理
2)相比string操作消耗内存与cpu更小
3)相比string储存更节省空间
  • 缺点
1)过期功能不能使用在field上,只能用在key上
2)Redis集群架构下不适合大规模使用

List结构

List相信程序员都不陌生,它底层其实就是维护的一个数组。

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	//从key列表表头弹出一个元素,若列表中没有元素,阻塞等待					timeout秒,如果timeout=0,一直阻塞等待
BRPOP  key  [key ...]  timeout 	//从key列表表尾弹出一个元素,若列表中没有元素,阻塞等待					timeout秒,如果timeout=0,一直阻塞等待

List应用场景

  • 做常用数据结构
Stack(栈) = LPUSH + LPOP

我们知道栈的特点就是先进后出,那么我们就可以利用List的特点,使用LPUSH存储数据,这样数据一直是往左递增的,而我们使用 LPOP就是从最左边取数据,这样就是最新存储的数据最早可以取出来,同样的使用RPUSH+RPOP同样可以做栈这种数据结构。学习嘛,肯定要融会贯通

Queue(队列)= LPUSH + RPOP

队列就是先进先出了,和我们日常追女孩子一样,先碰到的先认识的肯定具有优先权。

Blocking MQ(阻塞队列)= LPUSH + BRPOP

从key列表表头弹出一个元素,若列表中没有元素,阻塞等待 timeout秒,如果timeout=0,一直阻塞等。就是一直取数据,没数据就等着,和我们代码的自旋获取数据类似。

  • 微博和微信公号以及微信朋友圈消息流
    在这里插入图片描述
  • 微博消息和微信公号消息
    在这里插入图片描述
甜甜关注了MacTalk,备胎说车等大V
1)MacTalk发微博,消息ID为10018
LPUSH  msg:{甜甜-ID}  10018
2)备胎说车发微博,消息ID为10086
LPUSH  msg:{甜甜-ID} 10086
3)查看最新微博消息
LRANGE  msg:{甜甜-ID}  0  4

意思就是你关注的大V发布了文章,你打开公众号的时候,就会获取到最新的发布的数据,有时候可能默认显示最新的两条,就可以用LRANGE key start stop获取分页数据

Set结构

我觉得Set最大的作用就是取交集,并集等。。。。下面具体说

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中删除

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中

Set应用场景

  • 微信抽奖小程序
    在这里插入图片描述
1)点击参与抽奖加入集合
SADD key {userlD}
2)查看参与抽奖所有用户
SMEMBERS key	  
3)抽取count名中奖者
SRANDMEMBER key [count] / SPOP key [count]

在这里强调一下使用SRANDMEMBER key [count]SPOP key [count]都是可以从中取出count个数据的,你抽奖抽一等奖和二等奖,只需要修改count值就可以了,是不是使用redis都特别方便特别简单?SPOP key [count] 会将得奖者删除,不能再参与抽奖,SRANDMEMBER则是得奖者也可以继续抽奖

  • 微信微博点赞,收藏,标签
    在这里插入图片描述
1) 点赞
SADD  like:{消息ID}  {用户ID}
2) 取消点赞
SREM like:{消息ID}  {用户ID}
3) 检查用户是否点过赞
SISMEMBER  like:{消息ID}  {用户ID}
4) 获取点赞的用户列表
SMEMBERS like:{消息ID}
5) 获取点赞用户数 
SCARD like:{消息ID}

这个地方没什么好解释的,代码应该都可以看懂。下面玩高级的

  • 集合操作
    在这里插入图片描述
SINTER set1 set2 set3 --> { c }  取交集
SUNION set1 set2 set3 --> { a,b,c,d,e }   去重取并集
SDIFF set1 set2 set3 --> { a } 取第一个集合与其他集合之间的差异,也可以认为说第一个集合中独有的元素。不存在的集合 key 将视为空集。
  • 集合操作实现微博微信关注模型
    这个就是我们常见的可能认识的人,共同好友等
    在这里插入图片描述
    在这里插入图片描述
1) 拾光关注的人: 
shiguangSet-> {bingan, xushu}
2) 甜甜关注的人:
 tiantianSet--> {shiguang, baiqi, bingan, xushu}
3) 饼干关注的人: 
 binganSet-> {shiguang, tiantian, baiqi, xushu, xunyu)
4) 拾光和甜甜共同关注: 
SINTER shiguangSet tiantianSet--> {bingan, xushu}
5) 我关注的人也关注他(甜甜): 
SISMEMBER binganSet tiantian 
SISMEMBER xushuSet tiantian
6) 我可能认识的人: 
SDIFF tiantianSet shiguangSet->(shiguang, baiqi}

ZSet有序集合结构

ZSet常用操作

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

Zset集合操作

ZUNIONSTORE destkey numkeys key [key ...] 	//并集计算
ZINTERSTORE destkey numkeys key [key …]	//交集计算

在这里插入图片描述

Zset应用场景

  • Zset集合操作实现排行榜
    在这里插入图片描述
1)点击新闻
ZINCRBY  hotNews:20190819  1  守护香港
2)展示当日排行前十
ZREVRANGE  hotNews:20190819  0  9  WITHSCORES 
3)七日搜索榜单计算
ZUNIONSTORE  hotNews:20190813-20190819  7 
hotNews:20190813  hotNews:20190814... hotNews:20190819
4)展示七日排行前十
ZREVRANGE hotNews:20190813-20190819  0  9  WITHSCORES

Redis的单线程和高性能

  • Redis是单线程吗?
    Redis 的单线程主要是指 Redis 的网络 IO 和键值对读写是由一个线程来完成的,这也是 Redis 对外 提供键值存储服务的主要流程。但 Redis 的其他功能,比如持久化、异步删除、集群数据同步等,其实是由额外的线程执行的。
  • Redis 单线程为什么还能这么快?
    因为它所有的数据都在内存中,所有的运算都是内存级别的运算,而且单线程避免了多线程的切换性 能损耗问题。正因为 Redis 是单线程,所以要小心使用 Redis 指令,对于那些耗时的指令(比如 keys),一定要谨慎使用,一不小心就可能会导致 Redis 卡顿。
  • Redis 单线程如何处理那么多的并发客户端连接?
    Redis的IO多路复用:redis利用epoll来实现IO多路复用,将连接信息和事件放到队列中,依次放到 文件事件分派器,事件分派器将事件分发给事件处理器。
    在这里插入图片描述
    后面如果有机会我会单独写Netty的多路复用和eopll模型

欢迎阅读,如有疑问请及时提出,我们一起进步一起成长!



这篇关于01-Redis核心数据结构和高性能原理的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程