(一)数据类型
我们说redis是key value键值对的方式存储数据,key是字符串,而value是一些数据结构,那今天就来说一下value存储的数据。
我们数据结构包含,String,hash,list,set和zest但是在redis内部真的就是按这些数据结构存储的嘛?
很明显上面的图告诉我们不是,我们在内部是由不同的内存编码来保存数据的,但是时间复杂度是跟我们的数据结构一样的,那我们先来看一下第一个
1.String
String数据结构内部编码由raw,int,embstr
raw:存储一些比较短的字符串
int:我们redis里没有存储整形数据的方法,所以我们一般会用字符串来保存整型数据,所以我们String里有一个内存编码int,如果我们存入整型数据的话,内存编码就是int类型来保存数据。
embstr:存储一些比较长的字符串
2.hash
hashtable:内部就是一个哈希表
ziplist:是一个压缩列表
3.list
内部包含
linkedlist:链表(不是很节省空间)但是效率比较高
ziplist:压缩列表,比较节省空间,但是效率不高
我们redis3.2开始引入了新的实现方式quicklist,同时兼顾了linkedlist和ziplist,其实quicklist就是个链表,每个元素又是一个ziplist,同时节省了空间和效率
4.set
hashtable:就是一个哈希表
intset:集合中存的元素都是整数
5.zset
skiplist:跳表,也是链表,每个节点上有多个指针域,使用这些指针域的指向可以做到从跳表上查询的时间复杂度是o(logn)
查询内部编码方式
使用object encoding key,用来查看key对应的value的实际编码方式(redis会自动根据实际情况,自动适应的)
redis单线程模式
我们之前说redis只使用一个线程来处理所有的命令请求,但是redis又很快,这看起来是相互矛盾的,因为多个线程一起执行怎么会比单线程还慢呢?
实际上redis确实只有多个线程,但是是用来处理网络IO
当我们两个客户端同时发起上述的请求,此时我们会产生疑问是否会产生线程安全问题
答案是不会,因为我们多个请求同时到达redis服务器,需要在队列中排队,再等待redis服务器一个一个取出里面的命令再执行,类似于实现了一个阻塞队列,微观上讲,redis服务器是串行执行这个多个命令的
而他快的原因就是因为他的业务逻辑都是些很简单的操作,不是很消耗cpu资源,但是redis由于是单线程模型就必须要小心,某一些操作占用时间太长了阻塞其他命令的执行,而且redis是访问内存数据库访问硬盘,并且正因为他是单线程模型,所以省去了一些线程竞争的开销
处理网络IO的时候是使用了类似epoll这样的IO多路复用机制
这里我们说一下什么是IO多路复用,这里我们在网络原理那几篇博客中说过,简单来说就是一个线程管理多个socket
我们来回顾一下,TCP客户端需要有一个sever socket,然后会给我们客户端分配一个socket,如果我们多个客户端同时访问,同时就有多个socket,但这些socket大多数情况下并不是一直都在传输数据,很多情况下socket都是静默的,因为没有数据要进行传输(可能在等待用户的输入),但也有一些socket是活跃的,我们之前实现的时候因为每个客户端都要分配一个线程,客户端多了,线程就多了,系统开销就会变大,所以我们当时有两个方案,一个就是引入协程(更小的线程),一个就是使用IO多路复用
那我们说IO多路复用就是一个线程管理多个socket,那对于这些socket也是有要求的,我们需要这几个socket交互不频繁,大部分时间都在等待,如果这几个socket的交互都特别频繁,那我们还是要使用多个线程的
(二)String类型
我们redis中的string是直接按照二进制数据的方式存储的不做任何的编码转换,存什么取什么(mysql中的默认字符集是拉丁文,所以我们插入中文会失败),因为是二进制方式存储的,所以我们也可以存一些音频和图片等(但不可以存太大的)
1.set和get
我们看后面的nx和xx,nx标识如果key不存在才设置value存在就不设置(返回nil),xx标识如果key存在那就设置不存在就不设置(返回nil),ex和px是设置过期时间单位是秒和毫秒。
redis文档给出的语法格式说明【】是独立的单元,可以同时存在多个【】,但是|是表示或,一个单元只能出现一个
我们现在库上有很多操作,我们可以有一个操作可以删除所有数据,就是flushall(我们一般不使用)
get的使用就很简单,但是get只是支持字符串类型的value,如果value是其他的类型,get就会出错
2.MSET和MGET
这两条命令一次可以操作多组键值对,我们之前也说过一次在exists查询多个key时说,我们redis是通过网络传输数据的,那如果一次传一个数据传多次就会导致我们大部分时间浪费在网络通信中,就会导致我们效率变低,但是如果我们一次传多个数据,就可以很好的节省我们的网络开销
但是还是我们说的那个问题,redis是单线程模型,如果我们一次性传太多的数据,就会导致我们的redis会花很长的时间进行处理,就会导致其他的请求等待,可能会引发一系列的问题
我们这里的时间复杂度都是O(1)
3.incr和incrby
incr就是针对value的值+1,我们这里的value必须要是整数,如果是小数,需要我们是以哦那个incrfloat可以针对小数进行加减操作,我们这里的incr操作的key如果不存在,我们就会把这个key的value当作0来使用
incrby就是把+1操作换成任意的一个整数,当然也可以是小数,但是这种减法我们会更适用于下一条指令
我们的返回值就是修改后的值
4.decr和decrby
与上条指令类似,就是把+变成了-,我们要注意value中的值必须要是整数,在64位范围内,如果这个key对应value不存在就会当0处理
decrby就是-n的操作
上述的操作时间复杂度都是o(1)
5.append字符串拼接
append是用来拼接字符串的,返回值是拼接后的长度,单位是字节,在redis中也是按照utf8进行编码所以一个汉字通常是三个字节
6.setrange设置一定范围的字符
默认从1开始设置,但是我们可以设置偏移量
但是如果是一个中文字符串就可能会出问题,如果我们凭空生成了一个字节,那么就会自动生成一个0x00
返回值是修改后的长度
7.strlen计算value中字符串长度
返回值就是value的长度单位是字节
8.getrange
我们可以设置获取的头和尾通过设置start和end(都是闭区间)
这里的start和end就类似于下标,但是redis下标可以支持负数,-1就表示倒数第一个元素,如果我们保存的是汉字,就会对字符串进行切分,到时候就不一定是一个完整的汉字了
9.String的编码方式
我们之前说String内部有三种编码方式,并且可以通过object encoding+key来查询内部编码方式,那么我们现在就来通过指令来看一下
1)int
首先内部编码为int:8个字节的长整型
如果我们存的使一个整数,那么内部就会变为int,但是我们强调必须是整数,如果是小数会怎么样?
可以看到,如果我们的存一个小数内部编码会变为emstr(用来存较短的字符串),所以整数用int存是方便我们进行算术运算的,小数用字符串来存,那么我们想要进行运算,就需要讲字符串转为小数,运算后再保存
2)embstr
如果字符串的长度小于某个数,内部就会自动编码为emstr,具体的数字默认为39
3)raw
如果字符串长度比较大,内部就会自动编码为raw
10.String的一些应用场景
1)缓存功能
这是我们redis作为缓存的模型,因为mysql从硬盘读取数据会比较慢,但是如果同时有很多请求,就会导致mysql出错,所以我们引入了redis,之前的博客也有详细说到过
那我们String具体在这里有什么用呢?我们说key中存的一定是String类型的数据,我们先来看一段伪代码
我们让用户传一个id,然后我们在内部拼接一个类似报头一样的字符串,这样我们就能清楚的知道,在redis中,这是什么数据,如果我们不加,我们很可能存在redis中存储了用户的id(key)商品的id(key)如果统一传来一个id,我们也无法知道具体是什么类,当然我们也不建议这个名称过长,会影响我们redis的一个效率
2)计数
我们上面说到内部编码的时候说String可以存整型数据,所以我们就可以把redis当作一个计数的基础工具,因为redis比较快,所以可以用来实现快速计数,查询缓存等功能,就比如我们播放一个视频,那我们视频的播放量就需要立刻+1
但是也有一些问题,就是redis不擅长统计数据,如果我们要通过value里存的整数来统计一些数据,就会比较麻烦,就比如我们统计播放量前10的数据,如果我们使用mysql,直接一个orderby,一个limite就可以直接查询到
所以我们需要一个统计数据的仓库,可以是mysql也可以是别的,来将播放量同步到其他数据源,由这个仓库来统计数据,这里我们写入统计数据的仓库的方式是异步的,也就是说,我们不需要获取数据就立刻给这个统计数据的仓库,等双方都不是很忙的时候,再进行同步就可以
3)session
我们在之前的博客里也说到了好几次session,他通常跟cookie一起出现
session用来存储服务器数据,cookie用来存储客户端数据(都是通过键值对的方式存储)
我们客户端向服务器发起了请求之后服务器要存储我们的数据,就存到了session中,然后给我们返回一个token(内部可能是sessionid也可以是别的,但是一定能通过这个找到对应的session),我们客户端通常就会把这个token中的数据存储到cookie中,然后我们下次请求服务器的时候,就会携带着cookie,服务器拿到cookie拿到sessionid,然后再获取对应的数据,那这个作为标识放到token中的数据,就是一个字符串
那这种通过负载均衡分配到不同服务器上的模型,如果只是我们上述说的过程,就不是很够用了,因为我们无法保证每一次访问的都是那一个服务器,所以我们需要再引入一个redis
我们通过引入个redis服务器来存储所有session,这样我们每个web服务器的session都会放到redis中,客户端拿着cookie来查询时,我们就会再访问redis寻找对应的session
4)手机验证码
我们现在每次登录一个东西,会让用户输入手机号并且发送一个验证码,通过验证码进行验证,确定是不是用户本人,但是验证码每多久才可以获取一次,并且有过期时间,这时我们就可以很好的利用我们的setex key +时间+value的指令
我们也可以验证短信,把收到的验证码提交,系统来进行验证
这是伪代码的实现