了解Redis
先看一下官方给的解释:
The open source, in-memory data store used by millions of developers as a database, cache, streaming engine, and message broker.
从上面的英格力士可以了解到Redis是将数据存储到内存中,但如果是在内存中存储数据,直接定义一个变量不是更好吗?当然,在单机环境下,定义一个变量确实好使,但如果是在分布式的情况下,Redis就发挥神威了。我们都知道进程是有隔离性的,想要进行进程之间的通信,我们常常 使用网络来实现,Redis就是基于网络来实现的,可以把自己内存中的变量给别的进程,甚至别的主机进程也能进行使用。
上面还提到作为一款数据库,那Redis和MySql有啥区别,换句话说有啥优劣呢?MySql作为常用的数据库,最大的缺点就是慢,Redis由于是将数据存储到内存中的,获取数据肯定要优于MySql的。当然缺点也是显而易见:存储空间小,内存肯定是不如硬盘能存的。常见的作法就是将MySql和Redis结合起来用,热点数据存到Redis中。同样这样做的缺点就是系统的复杂度变高了。
Streaming engine:Redis的初心就是做一个消息中间件(消息队列),就是在分布式情况下的生产者消费者模型。
分布式
了解分布式之前先了解一下单机架构,通俗点讲,一台服务器干了所有的活的这个模式,就叫单机架构。
但随着用户量和数据量的水涨船高,单机架构已不满足我们的需求了,分布式就应运而生。
分布式(Distributed)是指将系统或应用程序的不同组件分布在多个计算机节点上,这些节点通过网络进行通信和协作,共同完成系统功能。在分布式系统中,每个节点都有自己的处理能力和资源,并且可以独立地运行和管理。
通俗点讲,那就是多台服务器来运行一个程序,那就是分布式了,但是分布式的缺点也就是系统复杂度过高。
那么常见的分布有:将应用服务器和存储服务器分开,
俩台服务器还不够用的时候,那就涉及到负载均衡了。
负载均衡(Load Balancing)
是指将系统或应用程序的请求流量分散到多个服务器或计算机节点上,以实现更好的性能、可扩展性和高可用性。负载均衡器(Load Balancer)作为中间层,根据预定的策略将请求分发到不同的服务器上,使得各个服务器能够均衡地处理请求,并避免单个服务器的过载。
负载均衡可以带来以下一些优势:
-
提高性能和响应速度:通过将请求分发到多个服务器上,负载均衡可以减轻单个服务器的压力,并提供更快的响应时间。这样可以更好地处理大量请求和并发访问,提高系统的整体性能。
-
增加可扩展性:负载均衡器可以根据需求动态地添加或移除服务器,从而实现系统的横向扩展。这使得系统能够适应不断增长的流量和用户数量,保持良好的性能。
-
提供高可用性和容错性:通过将请求分发到多个服务器上,即使其中一个服务器发生故障,负载均衡器仍然可以将请求转发到其他正常工作的服务器上。这提高了系统的可靠性和可用性,减少因单点故障而导致的服务中断。
-
优化资源利用:负载均衡可以根据服务器的性能和负载情况进行智能调度,使得每个服务器都能充分利用其处理能力。这样可以最大程度地提高资源的利用率,并避免出现某些服务器过度闲置或过载的情况。
用户请求先到达负载均衡服务器,这个服务器就相当于Boss 安排各个应用服务器该干多少活。
看起来负载均衡器承担了所有,那能顶的住吗?负载均衡器的性能远远大于应用服务器的,但是如果真的还是出现了抗不了的情况,那就引入多个机房来解决问题。
数据库的读写分离
随着上述的增加应用服务器,提高了处理请求量的能力,但是对数据库服务器来说,承担的请求也就越来越多了。
从俩方面进行入手: 开源 + 节流,节流对于大多数公司来说,门槛高,更复杂。开源那就是引入更多的机器。
最常见的方案就是 读写分离。
然后就可以根据具体的业务来看,是给主数据库还是给从数据库更多的资源。
引入缓存
我们都知道,数据库效率低下,那么为了提高其效率,我们可以将热点数据引入到缓存中,当然这是一小部分数据。
回归主题,这里的缓存服务器就是Redis;Redis在内存中存数据 快但小。那么具体的就是,从数据库存放的还是完整数据,来保证数据的完整性,将常用的热点数据放到缓存服务器中,供应用服务器来使用。
数据库的分库分表
引入分布式系统,不光能够应付更高的请求量,同时也要应对更大的数据量,因此可能会出现一台服务器存不下数据这种情况的发生,为了应对以上情况的发生,我们继续对数据库进行水平方向的拆分(分库分表)。
当然如果表足够大,也是需要对表进行拆分的。
微服务
一个服务器里面做了很多的业务,这就可能导致这一个服务器的代码越来越复杂,为了更方便的对其进行维护,我们可以把这个复杂的服务器,拆分成多个功能单一的、更小的 服务器
按照功能对其进行拆分成多个微服务,也有利于对人员结构的划分,因此微服务本质上是为了解决“人”的问题。
引入微服务带来的代价:1.系统性能下降 在没有微服务的时候,这些业务的互相调用,都是在同一台服务器上,而引入微服务之后,进行的调用是跨主机的,会用到网络进行通信,所以性能是一定下降的。 2.系统复杂度提高,可用性受到影响。 服务器更多了,服务器出现问题的概率也就越大了,就需要更多的监控预警手段。
微服务的优势:1.解决了人的问题 2.使用微服务,可以更方便于功能的复用 3.可以给不同的服务进行不同的部署。
再对一些常见的概念进行解释:
应用/系统
一个应用,就是一个/组服务器程序
模块/组件
一个应用,里面有多个功能,每个独立的功能就叫模块
分布式
引入多个主机/服务器来协同配合一系列的工作
集群
和分布式概念差不多,只不过的是分布式是物理上的,集群是逻辑上的多个主机(实际上还是一个主机)。
主/从
分布式系统比较典型的结构:多个服务器节点,其中一个是主,另外的就是从,从节点服务器需要从主服务器进行同步。
中间件
和业务无关的服务(功能更通用的服务)比如说:数据库,缓存,消息队列这样的组件。
可用性
顾名思义就是系统正常使用的时间,那么其公式就是 系统整体可用的时间 / 总时间
响应时长
就是系统响应请求所需要的时间,该指标衡量服务器的性能,与业务相关无需盲目追求。
吞吐 并发
衡量系统处理请求的能力,是衡量性能的一种。
Redis的优点
1.与MySQL相比:
MySQL是关系型数据库,操作表的,而Redis类似键值对的形式来存储数据,因此Redis更快
2.交互命令
Redis可以通过简单的交互命令进行操作,也可以通过一些脚本的方式,批量执行一些操作
3.拓展能力强
Redis拓展能力强,Redis提供了一组API,通过redis支持更多的数据结构以及支持更多的命令。
4.持久化
Redis数据是存储在内存中的,内存中的数据易失的,Redis也会把数据存储在硬盘上,相当于在内存中的数据备份了一部分,当Redis重启了,会在重启的时候加载硬盘中的数据,使Redis中的内存恢复到重启前的状态。
5.支持集群
Redis是支持集群操作的,类似分库分表操作。一个Redis能够存储的数据是有限的,引入多个主机,部署多个Redis节点,每个Redis可以存储数据的一部分。
6.支持主从
Redis支持主从结构,从节点相当于主节点的备份,一旦主节点挂了,从结点会变成主节点
7.快
1.Redis数据在内存中,要比存储在硬盘中,要快很多
2.Redis核心功能都是比较简单的操作内存的数据结构
3.Redis使用了IO多路复用的方式
4.Redis使用的是单线程方式,减少了不必要的竞争的开销
另外谈到redis的快,只是和mysql相比,如果和内存中的操作变量相比,就没有优势了,甚至更慢
比如应用程序要存储购物的一些信息,那么用hashmap来存,要比用redis来存,要更加快速,但是用hashmap来存,如果服务器一旦重启,数据就会丢失。因此到底用那种场景来存,要看具体的应用场景。
Redis的应用场景
1.实时的数据存储
把redis当作了数据库
全部的数据都存储在Redis中了,因此Redis中的数据不能随意的丢弃。
2.作为缓存 & 会话存储
热点数据存到Redis中。全量数据依旧存储在mysql,速度会大大提升。
Redis只是辅助MySQL,Redis中的数据丢就丢了,反正全量数据依旧存储在MySQL中。
cookie => 在客户端实现用户信息的存储,还需要session在服务器端实现对用户信息的存储,cookie只是在浏览器存储了类似用户信息卡的数据(只是存储了一个用户的身份标识),session则存储的是用户真正的信息
我们可以看到由于负载均衡器的存在,每次登陆时候在应用服务器上找不到对应的session,这就导致我们每次进行页面访问的时候都有可能需要重新登陆,为了避免这样的发生有俩种解决方式,一个就是进行除余操作,还有就是将session(会话)存储到redis中,
3.消息队列
基于这个可以实现一个网络版本的生产者消费者模型。Redis初心是为了实现消息队列功能的,但是无心插柳柳成荫,Redis的内存存储功能更是人们使用它的一大原因,而其消息队列的功能也被其他的工具所替代。
Redis常用命令
1.定义key-value
set key value
2.获取value
如果key不存在返回nil
3.模糊匹配
?:匹配任意字符(仅匹配一个)
*:匹配0或多个字符
[ae]:只能匹配a或者e(一个)
[^a]:只不匹配a(反着来)
[a-b]:匹配a到b的范围包含俩侧的边界
另外要注意keys的命令时间复杂度是o(n),所以,在生产环境上,一般都会禁止使用keys命令,尤其是keys*,生产环境上的key可能会非常多,redis是一个单线程服务器,如果执行keys*时间非常长,就使redis服务器被阻塞了,无法给其他客户端提供服务!!!
4.判定key是否存在
exits key1,key2 ...
返回key存在的个数,针对每个key来说的。
5.设置过期时间
expire key seconds
单位是秒, 还有一个pexpire ,单位是ms。
6.查询过期时间 & 删除key & 查询key对应的类型
ttl 和 del 还有 type + 对应key
Redis中key的过期策略
有俩种策略,一是定期删除还有就是惰性删除,redis采取的是俩者相结合。先讲一下什么是惰性删除:假设key到时间了,但是不会立刻删除key,等什么时候用到这个key 的时候才触发删除机制。
定期删除:每次抽取一部分key来进行验证过没过期 因为redis是单线程的,防止处理任务的时间被扫描删除占用,每次要保证抽取速度快。
Redis主要的五种数据类型
同一个数据类型背后的机制不一样,具体内容请看:
Redis单线程模型
这里说的单线程不是Redis整个就是单线程了,只是在处理业务的时候使用的模型是单线程的,而在处理io方面的时候,是执行多线程模型的如下图
那么问题来了redis虽然是单线程模型,为啥效率这么高呢?速度芥末快?
先明确一点,这里的参照物是类似MySQL这样的关系数据库进行比较的。
1.redis访问内存,数据库是访问硬盘
2.redis核心功能相比较MySQL来说更加简单
数据库对于数据的插入查询删除用有更复杂的功能支持,比如针对插入删除、数据库中的各种约束,都会浪费更多的时间。
3.单线程模型天然优势
redis每个基本操作,都是短平快的,简单操作一下内存数据,不是特别消耗cpu操作的,不需要更多的线程。
4.处理网络IO的时候,使用了epoll这样的IO多路复用机制
很多情况下,客户端和服务器交互不是很频繁,我们可以让一个线程处理多个客户端的请求。
IO多路复用的重大前提就是每个socket不是每时每刻都要传输数据的,大部分的时间都是静默状态,没有数据需要传输的,同一个时刻,只有少数的socket是活跃的。
String类型
Redis中的字符串,直接就是按照二进制数据进行存储的,存的是啥,取出来还是啥。不仅仅可以存文本,音频视频(二进制数据)也可以存。
set
expiration = ex 超时时间。px单位毫秒 ex是秒
nx:如果key不存在,才设置。 xx:如果key存在则不设置(返回nil)
xx:相反 如果key存在,才设置(更新value的内容),不存在就不设置。
具体实例如下:
要注意的是get只能使用在value是String类型情况的时候,否则get就会出错!!!
mset/mget :一次操作多组键值对
具体操作如下:
setex / psetex | setnx
setex和psetex就是设置过期时间了(psetex单位是毫秒)
setnx就是存在key的话就不设置,不存当前key才设置。xx就反过来。
其它命令
append
如果append的key不存在会set一个新的key~~
getrange
GETRANGE KEY START END
要注意的几点是:
1、范围的区间都是闭区间的
2、正常的下标都是从0开始,但redis的下标支持负数。例如:-1 = 倒数第一个
setrange:替换目标内容
SETRANGE KEY OFFSET VALUE
offset:指偏移量 就是从第几个下标开始替换,替换的长度就看后面的value内容了。
strlen : 获取字符串长度 *单位是字节
在redis中默认使用的编码方式是utf8 那么一个汉字的字节大小是三个字节,而在java char中,一个汉字是俩个字节,原因是java中char基于unicode这样的编码方式,就能够表示中文等符号。但是java中的string 一个汉字是3个字节(因为编码方式是utf8)
STRLEN KEY
key不是String就会报错
string类型内部编码方式
常见的三种int(64位/8字节的整数),embstr(小),raw(大)
string典型的使用场景
1.作为cache使用
redis存储的数据就是热点数据:频繁被使用的数据。但上述的策略是有一个明显的问题的,肯定有越来越多的key在redis上访问不到,从而从mysql上读取到redis导致redis中的key越来越多。
有几种常见的解决方案:
1、在给redis写数据的同时,给key设置过期时间
2、实现一个淘汰策略
2.作为计数功能
3.共享会话
hash类型
redis本身就是一个键值对的结构,这里主要讲的是value这一层,也可以是hash类型的(套娃)
接下来介绍hash类型的主要几个命令:
hset / hget
HSET KEY FIELD VALUE [FIELD VALUE ... ]
返回值:设置成功的个数
HGET KEY FIELD [FIELD ... ]
同理 看该filed存不存在 命令是:
hexists
返回值是1代表存在~~
hdel
del删除的是key hdel删除的是filed
hkeys
获取hash中所有的字段
HKEYS KEY
hvals
和hkeys相对,hkeys获取filed的名称,而 hvals 获取的filed对应的value值
hgetall
hgetall就是将hkeys和 hvals融合在一起了 具体看:
hmget
查询多个filed
上述的遍历都存在危险的(堵塞redis)hscan遍历redis的hash,但是是渐进式的遍历,敲一下遍历一小部分,再敲一下,再遍历一小部分。
hlen
获取hash中所有字段的个数
hsetnx
HSETNEX key field value
类似于setnx 不存在才能设置成功,如果存在设置会失败。
hincrby / hincrbyfloat
hash内部编码:
啥叫压缩,就是将文件进行重新编码,举个粗糙的例子:如果一个文件的编码文件是 abcd00000000000ef的话 ,我们如果将其压缩的话,它可以变成abcd0[10]ef,而像rar,7z这些压缩算法有着更精妙的设计的。ziplist也是同理,内部数据结构是精心设计过的,节省内存空间。ziplist付出的代价就是进行读写数据会比较慢,但如果元素个数少,慢的不明显;如果元素太多了,慢的雪上加霜~
综上:
1、如果元素比较少的情况下,使用ziplist表示,元素个数比较多的情况下,使用hashtable表示。
2、每个value值长度都比较短可以使用ziplist,如果长度太长也会转变成hashtable。
hash常见应用场景:
1.作为缓存
string虽然也可以作为缓存,但是存储结构化的数据hash更适合
list列表
可以把他当作数组或者顺序表
再来探讨list的特点:
1、 列表中的元素是有序的,但是这里指的不是升序降序,这里说的有序是 顺序很关键:如果把当前元素位置颠倒一下/换一下,此时得到的新list和之前的list是不等价的。
2、 区分获取与删除的区别;lindex返回的指定下标的元素的值,lrem的话虽然也能返回下标的值,但是本意是删除了当前下标的元素。
3、 列表中的元素是允许重复的。
因为当前的list可以头插尾插,头删尾删,我们就可以把list当作一个栈 / 队列 使用了。
常用的命令:
LPUSH
头插 一次可以插入一个或多个元素
LPUSH key element [element...]
LPUSHX
也是头插与LPUSH区别在于,要求当前的key必须存在!
RPUSH
L:代表left R:代表right 那么RPUSH就代表尾插
LPOP
从列表左侧取出元素(头删)
LPOP key
LRANGE
LRANGE key start stop
此处描述的区间都是是闭区间,下标也是支持负数。
还有要注意的是这个范围是可以超出的:
redis会尽可能的获取到给定区间内的元素,如果给定区间非法,比如超出下标的情况下,就会尽可能的获取对应的内容。
LINDEX
给定下标,获取下标对应的元素。
LINDEX KEY INDEX
如果下标非法 返回nil
LINSET
在特定的位置上插入元素
LINSERT key <BEFOR | AFTER> pivot element
如果基准值有多个,从左往右找,找到第一个符合基准值的位置,进行插入即可。
LLEN
直接获取list的长度
LLEN key
LREM
LREM key count element
count:要删除的个数
element:要输出的值
如果count > 0 那么从左往右数删count个
如果count = 0 那么全删
如果count < 0 从右到左 倒着删 |count| 个
LTRIM
也是删除list中的元素,但是这种删除指定范围保留,其他全删~~
LTRIM key start stop
保留start和stop内的元素,区间外的俩边的元素直接删除了。
LSET
根据下标修改元素
LSET key index element
阻塞版本的命令
阻塞:当前的线程不运行了,代码不走了;会在满足一定条件之后被唤醒。
redis的list也像阻塞队列一样,线程安全是通过单线程支持的。
阻塞,则只支持队列为空的情况,不考虑队列满~~
那么blpop brpop的作用在队列不为空的时候,效果和lpop没任何区别,如果list为空的情况下,blpop和brpop就会产生阻塞,一直阻塞到队列不空为止。
解释第二点:
BLPOP
BLPOP key [key....] timeout
按照顺序看哪个key(list)有数据,就先返回哪一个 ,如果都没有数据的情况下,就得先看哪个key先有数据,就返回,如果超出时间都没有数据写入,就拉倒~~
1、针对一个非空的list操作
1的key代表操作的哪个list,2代表取出的数值是啥。
2、针对一个空的list操作:
其他客户端一push,blpop立刻返回结果。
小结
list内部编码
主要有俩种:一个是ziplist,把数据按照更紧凑的压缩形式进行表示的,节省看见,如果元素多起来操作效率就会变低了。
另一个是linkedlist,当然还有更好的选择那就是quicklist,quicklist相当于把二者结合起来,整体是一个链表,每个节点是一个压缩列表,每个节点之间通过链式结构连起来~~
list应用场景
根据键值对实现一个类似mysql中查询功能,list可以实现一个类似数组的功能
使用redis作为消息队列:
brpop:当列表为空的时候,就阻塞等待其他客户端向列表中填入数据。而且谁先执行的brpop谁就优先获得数据,像这样的设定,就构成了一个“轮询”的效果。
多个列表 多频道场景
set
set可能有多种含义:集合、设置(和get对立),这里指的就是集合了。集合就是把一些关联的数据放在一起;
1、集合中的元素是无序的
2、集合中的元素不能重复(唯一的)
3、和list类似,集合中的每个元素都是string类型
sadd
SADD key member [member...]
咱们把集合中的元素,叫做member,就像hash中的field类似。
smembers
获取一个key的所有元素,元素间的顺序是无序的。
SMEMBERS key
sismember
判断一个元素在没在一个集合中。
SISMEMBERS key member
spop
集合中元素是无序的,pop就不是尾删了,在集合中spop的意思是随机删除。
SPOP key count
count的意思是删几个~~
smove
SMOVE key1 key2 member
srem
SREM key member [member...]
可以一次删除一个member,也可以一次删除多个member
交集、并集、差集
sinter
求交集,O(m*n) n是最小集合元素的个数,m是最大集合元素的个数
SINTER key [key ... ]
还有一个求交集的:
SINTERSTORE des key...
结果放在了key3这个新集合中。
sunion
获取给定set的并集中的元素
SUNION key [key...]
同理如果想要把结果放到一个集合中也有sunionstore的语法~~
sdiff
求差集
SDIFF key [key...]
set内部编码
set内部编码有俩种方式:intset、hashtable,元素均为整数且不多的情况下,为了节省内存空间,set内部编码会自动优化成intset。
set应用场景
1.使用set保存用户的“标签”
设计用户画像,分析出用户的特征,投其所好定向推送~~标签就是简短的字符串,把这个字符串存到set中,(存数据,天然去重)set方便计算交集,很容易找到俩个用户的共同的标签:衍生出用户关系(社交属性)
2. 计算共同好友
利用集合求交集,基于上述实现好友推荐。
3. 利用set统计UV
利用set天然去重,UV是啥呢?一个互联网产品如何衡量用户量:俩个指标 PV(page view)用户每次访问该服务器,就会产生一个pv(浏览量),UV(user view)每个用户访问服务器,用set天然去重,同一个用户访问一个服务器是不会重复计数的。
zset
zset有序集合,但是这里说的有序是升序或者降序,而不是顺序~~
那么这里排序的规则是啥,给每个member引入一个新的属性:score(分数),进行排序的时候,依照分数的大小进行升序/降序。
zadd
使用zadd往有序集合中添加元素
记住score喝member是绑在一起的 ~~
分数一样再按照member字典序排,分数不同,按照分数排。zset内部按照升序的方式进行排序的。
修改一下:
nx仅是添加新元素,不会更新已存在的 上图2,nx是添加元素而不是更新已存在的,那么上述第二部操作不会产生任何效果。
还要注意的是,zadd返回的是新增成功的元素,有些时候修改已存在的元素或者是用了xx选项返回值也是0,是对集合进行操作了的。
zrange
返回指定范围区间的元素,分数按照升序进行排列。带上withscores可以一起将分数进行返回。
ZRANGE KEY START STOP [WITHSCORES]
查看有序集合中的元素情况,有序集合中的元素是有前后顺序的,因此下标的存在是可以的。
出来的结果是升序的(依次往下score不断升高)
添加score的版本:
复杂度就是 logN + M 咯,先遍历查找范围的位置,在取得范围之间进行遍历
zcard
ZCARD key
获取到元素key的个数
zcount
ZCOUNT key min max
默认是闭区间
使用( 表示开区间
这里只查看score99的member
zcount的复杂度是 O(log N),正常来说我们查找到区间min值和max值,查到之后遍历这个范围计算个数就能获得结果,但这样做的结果复杂度是log N + M ,所以zset这里真正的做法是,在插入元素的时候,会有对应的排名属性添加(也就是下标),因此在zcount的时候,就可以将min值和max值查询到之后,将排名相减得到结果~~
当然这个区间是可以有浮点数,而且可以使用inf(无穷大)和 -inf(负无穷大)来进行查询操作的。
zrevrange
按照分数降序进行遍历并打印
zpopmax
删除并返回分数最高的count个元素
ZPOPMAX KEY [COUNT]
bzpopmax
就是zpopmax的阻塞版本
BZPOPMAX KEY [KEY...] TIMEOUT
bzpopmax支持读取多个有序集合,因此返回的结果包含了有序集合的标识。
zpopmin / bzpopmin
删除有序集合中最小的元素,和zpopmax用法类似
zrank
这个返回的排名按升序进行排序的。
相反的有一个zrevrank
zscore
ZSCORE KEY MEMBER
根据member查分数。时间复杂度是 O(1) ,
zrem
ZREM KEY MEMBER[MEMBER...]
时间复杂度是 O(logN * M)
zremrangebyrank / zremrangebyscore
ZRANGEBYRANK KEY START STOP
根据指定的下标范围进行删除
时间复杂度是 O(logN + M)
N就是有序集合的个数,M就是start和stop之间元素的个数
同理zremrangebyscore是通过描述分数范围进行删除的
zincrby
ZINCRBY KEY INCREMENT MEMBER
不仅是增加,还需要重排序
zinterstore
ZINTERSTORE destination numkeys key [key...] [WEIGHTS weight weight ... ] [AGGREGATE <SUM | MAX | MIN> ]
求交集结果保存到另一个key
weights是权重,进行求交集的是有序集合,带有分数的。
aggregate作用在于 将得到交集结果member的score是进行sum还是min 还是max
这里aggregate默认是sum
分数乘完权重再进行交集运算。
zunionstore
zset内部编码方式
zset应用场景
最常见的场景就是排行榜系统~~ 其排行榜关键在于这个分数是实时变化的,只需要把用户信息和用户排名分数存储到zset中,就是可以自动生成一个排行榜,还可以根据下标进行范围查询操作。
更复杂的排行榜就是热度,热度考虑很多因素,因此zinterstore和zunionstore的权重就发挥用处了。