谈到Redis,就必然离不开分布式系统,这也是为啥第一篇文章要介绍分布式.还记得冷热分离架构吗---将热点数据存储到存取速度更快的缓存中,这个缓存就可以使用Redis实现.
一. Redis的特性
1. 速度快
想必各位同学或多或少都接触到了MySQL,Oracle等关系型数据库,它们有一个共性--存储在硬盘上,提供了基本的ARUD功能.Redis则存储在内存上,是一种基于键值对的非关系型数据库.
下面是Redis速度快的一些原因:
- Redis的所有数据都存放在内存中,存取时间比硬盘快的多.
- Redis使用了单线程处理用户请求,避免了多线程调度的开销.
- Redis的核心功能是对数据结构的操作,逻辑简单,操作快捷
想必童鞋们肯定会问,既然Redis存取速度比MySQL快的多,那我以后都使用Redis做数据库不行么?
----肯定是不可以的,相比于硬盘来讲,内存的存取时间缩短,但是容量也减少了.因此Redi通常作为主数据库的缓存来使用,而不是作为真正存放数据的数据库.
问题二,既然Redis是单线程,是不是就不能支持多用户同时访问?----这里的单线程是指Redis只使用了一个线程处理用户请求,还有其他的线程负责网络连接等等,并非是Redis中间件只有一个线程.为了缩短响应时间,Redis的单线程还使用了IO多路复用技术(打个比方,假设你向字节投了简历,但是字节没鸟你,聪明的童靴肯定会接着向腾讯,某书接着投,而不是苦哈哈地等面试)--你就可以把自己看作一个单线程,各个大厂就相当于用户请求,当它们回复你的时候才去处理,它们不回复就找下一家.
问题三,如果请求量过大或者操作较为复杂,Redis使用单线程是不是没有多线程的效果更好?----这个问题没有正确答案,需要区分不同的使用场景.但是,Redis开发者使用单线程的原因就是对Redis做了精打细磨,让它的功能实现更为简单,因此面对高并发问题还是可以应付地来的.
2. 基于键值对的数据结构服务器
不论是学过哪种语言,童靴们肯定都学过Map结构吧.(没学过的可以回炉重造了~~)
Redis就是一个巨大的Map,它的Key是固定的字符串类型,value可以是多种数据结构(以后会详细介绍~~)
3. 功能丰富
Redis本身还支持键过期,发布订阅,事务功能,流水线批量处理等功能.不仅如此,Redis还提供了功能扩展的API,方便开发者创建合适的轮子~
4. 简单稳定
Redis的源码只有几万行左右,相对于其他的数据库来讲非常少.并且具备相当的稳定性,在面对大量用户使用的过程中,服务器很少因为Redis出现问题而宕掉.
5. 客户端语言多
Redis提供了简单的TCP协议,很多编程语言可以很方便的接入Redis.并且因为Redis的认可度非常高,支持客户端的编程语言也非常多,后续会对客户端做详细介绍~
6. 持久化
前文提到过,Redis是存储在内存上的,数据会随着服务器掉电而丢失.因此Redis提供了两种持久化策略:RDB和AOF,可以将内存的数据保存在硬盘中,这样就保证了数据的持久性.后面也会对这两种策略做详细说明~
7. 主从复制
Redis提供了复制功能,可以创建多个相同数据的Redis副本,后续也会详细介绍~
8. 高可用和分布式
Redis提供了高可用实现的Redis哨兵,能够保证Redis结点的故障发现和故障自动转移,也提供了Redis集群,可以实现分布式.
二. Redis的使用场景
前面部分我们讲述了Redis是什么,下面来了解一下Redis可以干什么.
上图是Redis官网上总结的Redis使用场景,下面由我为大家来详细介绍~~
1. Caching(缓存)
这是Redis现在的主流用法.主数据库使用MySQL,Oracle这样的关系型数据库,虽然容量足够大,但是随着数据的增多,操作花费的时间也会增长(尤其是select *这样的操作).当大量用户同时访问时,主数据库服务器很容易宕机.Redis作为缓存可以存储热点数据,减少数据库集群的访问量.
2. Database(数据库)
Redis本身也可以当做NoSQL数据库来使用,但是它适用于存储数据量小,对存取速度有要求的场景.
3. Session Management(统一会话管理)
先来简单介绍一下Session机制
举个栗子~~你刚入学的时候,学校会专门给你办一张校园卡,需要刷校园卡才能进校.这张校园卡就相当于sessionId,客户端将sessionId提交给服务器后,服务器会根据这个sessionId查找有无之前的会话信息,如果有则放行,否则拒绝处理.
那么问题就来了~如果引入了分布式架构,当用户第一次访问服务器时,这台服务器为用户创建了一个session,如果用户第二次访问时用户请求被分配到第二台服务器,这台服务器不承认用户合法性,该用户就需要重新登陆了~
对于上述情况,可以将session集中存放在Redis上,因为session不需要太多的存储空间.这样用户就不必频繁地进行登陆操作了.
4. Feature Stores(用户存储)
当你结束一天的学习/工作,打开某音,刷到一位小姐姐跳舞的视频并反复观看了好几遍,那你要坠入爱河了~后端的服务器就会给你打上一个标签,接下来你刷到的视频大概率都是小姐姐跳舞~~当然,不同的企业之间为了共赢,会共享用户数据.比如你又点开了某手,发现第一条又是小姐姐跳舞~
那么问题来了,这些APP如何判定屏幕前的是同一位用户呢?这是因为现在的主流登陆方式基本都是手机号,微信等,很容易判定这是同一个user.不同的APP后端共享用户信息时,可以使用一个set做到用户的去重处理.关于set后面会有更详细的介绍~
5. Message Queue(消息队列)
Redis研发的初心就是作为消息队列来使用,不幸的是~现在比较强势的是rabbitMQ,kafka等.
Redis提供了发布订阅和阻塞功能,相比专业的消息队列来讲功能不够强大,但是也能实现削峰填谷和解耦的功能.
三. Redis的客户端
Redis的客户端有三种:
- 命令行客户端(自带),当我们安装好Redis后,可以直接使用redis-cli命令打开Redis客户端
上面是连接本机的redis,也可以使用redis-cli -h IP -p Port连接另一台主机的Redis服务器(博主没有另外的主机,就使用了环回IP)
- 图形化客户端,随着Redis的影响力不断扩大,也有大佬开发了Redis的图形界面,但是不推荐使用(命令行式的客户端是全世界通用的,图形化客户端换个环境就不一定能用了)
- 基于Redis的api自行开发的客户端,前文提到过,Redis为很多主流的编程语言提供了接口,可以通过这些编程语言来操作Redis(类似于MySQL的JDBC编程)
四. Redis的基本全局命令
Redis有五种常见的数据结构(string,hash,list,set,zset),这里指的是Redis的value可以使用的数据结构,Redis本身还是键值对的形式.不同的数据结构对应不同的操作命令,下面介绍的命令是这些数据结构通用的.
Redis的命令不区分大小写~可以使用tab键进行命令补全.
4.1 keys
用来查找所有满足样式的key,可以使用通配符: *匹配0个或多个字符,?匹配任意一个字符
- hell?,可以匹配hella,hellb,hellc...
- h*,可以匹配h,hello...
- h[ab]llo,可以匹配hallo,hbllo
- h[^ab]llo,可以匹配hcllo,hello但不能匹配hallo,hbllo
- h[a-c]llo,可以匹配hallo,hbllo,hcllo
下面来演示一下~
mset指令设置了三个字符串类型的键值对,然后使用keys *查询Redis中所有的key
注意: 你刚刚学到了一个很危险的指令,keys *用来查找所有的key,如果redis中存放了几万条数据,这个操作的速度就会变得非常慢,又因为redis是单线程处理客户端请求,其他用户的请求会被阻塞或者直接打到SQL数据库上,很容易让数据库服务器挂掉~
4.2 exists
判断某个/某些key是否存在,会返回查询的key存在的数量
Redis使用CS结构,客户端和服务器使用网络进行通信,因此一条指令到达服务器需要经过复用和分用.正因为这个原因,Redis提供的很多指令都可以支持一次指令多次操作~在达到相同效果的情况下,我们尽量让指令的个数减少.
4.3 del
用于删除指定的某个或某些key,返回成功删除key的个数
注意: 当Redis作为缓存使用时,存储的热数据是从主数据库复制出来的,可以进行少量的删除操作;如果Redis作为数据库使用,del操作很可能让你丢失升职机会~~
4.4 expire
为指定的key添加秒级的过期时间,如果想要将单位设置成毫秒级,可以使用pexpire.
返回1代表设置成功,返回0代表设置失败(通常是因为设置的key不存在).
nil相当于java中的null
4.5 TTL
用于获取指定key的过期时间(单位是秒),也可以使用pttl获取毫秒级的过期时间.
返回-1表示指定key没有设置过期时间,返回-2表示key不存在
细心的童靴肯定会发现,为啥TTL博主用的大写,是不是有种似曾相识的熟悉感~可以自行百度一下IP协议的跳数限制.
4.6 type
返回key对应的数据类型
4.7 键的过期机制(重点)
关于过期机制有很多应用场景,比如session的过期时间设置,短信验证码的过期设置...
先来讲解一下Redis采用的过期机制~
1.惰性删除
如果某个Key已经过期了,并不会被立即删除,而是被访问到了才会删除.
举个栗子~你到超市去买牛奶,挑选了一桶,然后发现它已经过了保质期,这时候售货员就会告诉你这桶牛奶他们不买了~然后就把这桶牛奶扔掉了.
2. 定期删除
Redis每隔一段时间会遍历一次key,发现有过期的就会删除.
这种方法会出现STW问题,如果要遍历的Key太多,redis又是单线程,就会造成很大的阻塞.因此redis会每次抽取一部分Key进行遍历.
Redis采用的是上述两种方法结合的方式----定期删除部分过期的Key,如果访问到某些过期但还未删除key也会被清理.当然,这种方式仍会留下一些残留的key,Redis又设置了淘汰机制,后面会详细讲解~
3. 定时删除
实现原理很简单,设置一个定时器线程,检索各个Key有没有过期.有两种实现方式:
- 基于优先级队列实现,将key按照过期时间插入到优先级队列中,定时器线程不需要一直检查队首元素,可以等待一定的时长(通常是队首元素的过期时长),时间到或者有新元素插入时自动唤醒.
- 基于时间轮实现,类似"轮询"的方式,将时间片划分成多个小段,每个时间段上都有一个链表,用于记录该时间到期后要执行的任务.
见上图,Timer Thread会循环检查每个时间段,并尝试执行该时间段上的任务(有可能还没到时间,就无需执行)
Redis并没有采用这种方式,可能是因为Redis的开发者本意是想使用单线程,定时器这种方式会额外增加一个线程的开销~
五. 数据结构和内部编码
Redis内部有很多数据结构,之后我们会详细学习string,list,hash,set,zset(有序集合),但它们只是Redis对外表示的数据结构,也就是使用type命令展示的数据结构.
实际上,Redis对每种数据结构都做了一定的优化,会使用不同的编码方式,可以使用object encoding key查看
5.1 String类型
- 字符串类型有三种编码方式:
- raw,底层实现是一个字节数组(byte[])
- int,Redis也会被用来实现计数功能,如果value是一个整数对应的字符串,底层实现就是一个整数
- embstr,这是针对短字符串的优化,会对字符串进行一定的压缩,可以节省空间
5.2 hash类型
哈希类型有两种编码方式:
- hashtable,使用普通的哈希表实现
- ziplist,当value(key对应的哈希表)元素比较少时,使用hashtable存储会浪费很多空间,因此redis使用ziplist进行了压缩,但是降低了查找效率
5.3 list类型
有三种编码方式:
- linkedlist,最开始redis使用list实现消息队列,后来又引入了stream类型实现.因此list需要支持频繁地插入删除操作,基于链表结构实现
- ziplist,节省了空间,但是存取效率比较低
- quicklist,从redis3.2版本开始,使用的是quicklist,是前两种方式的折中,整体是一个链表结构的list,链表上的每个元素是一个ziplist.
5.4 set类型
有两种实现方式:
- hashtable,学过java的童靴都知道,set底层是借助map实现的,无非是将map中的value替换成了一个object对象
- intset,如果集合中存储的都是整数,会使用这种内部编码方式
5.5 zset类型
zset指的是有序集合,会给key赋予不同的权重.有两种实现方式:
- skiplist,"跳表",每个链表元素不仅要存储下一个结点位置,还会有其他的指针域存储另外的结点位置
- ziplist,可以用来压缩空间
需要注意的是,Redis内部编码方式的转换是不会被用户感知到的,因此我们不必过多关注redis什么时候使用哪种编码方式.
六. Redis的单线程模型
再次强调:这里的单线程指的是Redis使用一个线程处理用户的请求,实际上redis还有其他的线程用来处理与网络有关的命令
即使有多个请求到达Redis,Redis也会按照串行执行,因此无须担心线程安全问题.
与MySQL等SQL数据库相比,即使redis使用的是单线程,它的处理速度也是非常快的,具体原因开篇做了详细的分析(提醒一下,与redis的存储位置,处理逻辑,多线程竞争有关)