String是redis最基层的一种数据类型
String最大可以存放512M的数据,这个非常关键
因为String可以存放二进制数据,所以有人可能会放图片视频,所以在设计应用时要考虑的一个关键因素,关系到我们能不能用String来存储这样的数据,因为有可能这个资源会大于String的存储容量
list因为可以从一端进一端出(blpush以阻塞的形式推入数据,brpop以阻塞的形式弹出数据),所以可以当做消息队列来用,所以说redis功能不局限在缓存,也可以做消息队列(当然有更专业的数据类型Streams)
set有并集、交集、差集的功能,非常有用
每一个不同的数据类型,底层都有多种实现,每种实现叫一种编码,每一个编码都有一种实现方案
为什么需要多种实现方案呢,因为redis是基于内存的数据库,内存的容量比较有限,所以存数据的时候要尽可能的节约内存,另外还有考虑性能;不同的方案是为了应付不同的场景,所以当数据规模大的时候,要压缩空间;数据量少的时候,性能是第一要务
SDS是简单动态字符串,c语言自带的字符串是定长的,不适合做扩展,所以封装了一个SDS
具体分内存的时候是分两块内存,一个存放对象的元数据,比如对象长度,类型;另一个存放具体字符串的值
当元素少的时候,用压缩列表,是用一块连续的空间存储数据,可以将每个节点的数据压缩;但是一旦中间节点的数据长度发生变化,会引起连锁反应,因为存在一起了,需要连锁的扩展;数据太多的时候,影响范围太大,所以就不适合用连续的空间了,适合用linkedlist,比较散的空间
3.2开始使用quicklist,是把前两者结合在一起,本质上也是一个链表,链表中每个节点是一个ziplist,比较省空间的一个东西。把两者串接起来了
数据量小,用ziplist;数据量大,用字典;字典是一个结构,key-value,底层封装了一个struct(C语言中的东西)
如果数据量大,用的是跳表,跳表时间复杂度和红黑树接近,实现上要简单很多
ziplist底层实际上是字典+跳表,字典是为了取值方便,跳表是为了排序
一般用前两个命令就解决问题了,因为redis提供的是简单的事务,不支持回滚,通常不支持ACID中的D,持久性
redis可以做持久化,但是持久化的时候,会把数据先写到页缓存中,页缓存再刷到磁盘上,这是基于操作系统的机制,可以每个命令都刷盘,也可以若干时间刷盘
我们用redis的本质是我们希望解决性能问题才用它,所以如果每次执行命令都刷盘,那就别用reids了,直接用mysql就好了
所以我们一般能接受的也就1s刷盘一次,间歇性的刷盘就可以了,所以不能完全支持持久性,有可能会丢失1s内的数据,但这个往往可以被接受
在multi以后,是将命令放在一个队列里,并没有传给redis,在redis中是没有的,所以这时候从redis中取数据是取不到的;在exec后才会传给redis执行,执行完以后才会得到结果
redis性能好的本质是线程模型带来的高性能
redis是单线程的,是指redis的主要业务,网络IO和键值对读写是一个线程来完成的
因为是单线程的,所以执行命令时要考虑这个命令(例如keys *)会不会阻塞这个线程,是不是数据量太大,需要分割什么的,想办法避免线程阻塞
redis如何利用IO多路复用的?
基于IO多路复用,可以实现同一个线程并发处理多个请求,处理请求的组件,叫做套接字socket(其实就是调用操作系统的socket)
redis服务器在应对并发,处理客户端请求的时候,是通过套接字去处理的
客户端可以同时处理多个请求
客户端连服务器是通过socket相连的,但是连的过程是一个比较慢的过程,有可能连上了在准备数据,或者说数据发出去了,在网络中传递;这个时候socket阻塞,等待数据到来;这个时候socket是没啥事情的,就是在等待数据;我们只需要在几个关键时刻去理socket:
1.客户端与服务器连接时刻,2.连接关闭时刻,3.socket已经接收到了数据要进行写入,4.或者是客户端想从socket读数据(数据已经准备好了),socket要给客户端返回数据
我们不需要每时每刻关注socket,只需要socket处于这四个状态中的一个的时候,进行处理就行了,所以用一个线程监听四个状态就可以了
IO多路复用底层就是这样一个机制,监听socket的状态,当需要处理的时候就进行处理,不需要处理的时候不管
所以要用IO多路复用程序监听多个socket,监听到这些事件以后,会把这个事情分给别人去处理;而且可能同时有很多socket都触发了这些事件,都监听到了;
因为是单线程的,处理的话需要有序的处理,所以需要引入一个套接字队列;就是说如果监听到哪个socket触发了事件,就把这个socket存入套接字队列中,这个队列使得当前需要被处理的socket变的有序了
谁来处理这些socket呢,有一个文件事件分派器,它来负责具体的处理,它是通过读取队列来做进一步的处理的,这个也是单线程的,读取队列中的每一个socket逐个处理
因为每个socket的状态是不同的,所以底层要用不同的组件处理不同的事件:读数据用命令请求处理器,想给客户端返回结果用命令回复处理器,连接用连接应答处理器
文件时间分派器根据对应的顺序处理socket,针对每个socket当前状态分给不同的处理器依次做处理
总之,一个线程为什么能解决多个问题呢?
服务器基于多个socket与客户端相连,IO多路复用程序这个单线程监听socket的状态,当符合监测的状态,就放入队列中排队,由文件事件分派器一个线程逐个处理
IO多路复用程序底层是基于操作系统的IO多路复用机制来实现的,得看操作系统支持哪个?
操作系统IO多路复用底层多种实现机制,比如有select,epoll,evport,kqueue
redis会选用哪一个呢,redis是都支持的,提供了统一的API去调用,它会结合当前操作系统,选一个性能好的
redis持久化可以做到在保证性能的前提下,丢失1s的数据,
把redis当做持久性的中间件来说,redis常用来存储阅读量,点赞数量这种东西,它们丢失1s的内容影响不大
RDB优点是体积小,缺点是不能实时的,因为每次BGSAVE的时候会产生阻塞,如果每次执行命令都去持久化,就会经常阻塞,就别用redis了
RDB适合备份,每隔多久执行一次
AOF适合实时的保存数据
1.bgsave命令会通知redis的主进程,我们称之为父进程去fork出一个子进程,利用子进程来做持久化的操作,因为如果直接让主进程做持久化的话,就会阻塞对命令的响应
但是有可能刚执行了bgsave,有子进程了,所以返回
2.如果没有,则fork出一个子进程,这里需要注意:父进程在创建出子进程的那一刻,父进程会被阻塞,在这一刻父进程是无法响应客户端的请求的,但是fork这个过程是短暂的,不会很久
3.创建完子进程以后,父进程就可以继续响应别的命令
4.子进程做的事情就是持久化,要把内存中的数据,存入硬盘中,最终要存入一个文件中,那个文件就是RDB文件(这里第三步和第四步其实是并行的),子进程会把父进程中的数据存储到RDB文件里
5.子进程存储完以后,就会向父进程发起通知,告诉做完了,如果有旧的RDB文件,让父进程替换一下
但是子进程在写入文件的时候,父进程同时也在响应命令,似乎是冲突的,因为一个在写,一个在读,怎么解决一致性的问题呢?
这里用的是操作系统底层的一个机制:写时复制技术(CopyOnWrite)
内存最小单元是页,有很多页,子进程要往RDB写东西时,读取的是红色的页;如果刚好父进程也是读取这个红色的页,那么没有问题,读读是共享的;
但是如果父进程是写,如果写的不是红色的页,没有问题;如果写的是红色的页,就冲突了,读写互斥,那么就把红色的页复制一份,副本,父进程往副本里写,子进程继续读取原来页的内容
所以刚刚说RDB持久化是以快照的形式将数据持久化到硬盘里,什么叫快照
就相当于fork出子进程的那一刻,就可以共享父进程的内存空间,在这一刻父进程的内存相当于被锁住了,相当于照了个像,内存在这一段时间不会发生任何改变,如果要改,改的是副本
副本被修改以后,什么时候同步到内存中?
对于父进程来说,有了副本,其实就不需要对应的page了,因为副本是它的完全替代品,所以副本其实也是一个page,复制出来以后,父进程维护的page列表就变了,原来的那个page内存单元就不用了
那么通知父进程替换旧文件?
可以看到redis的配置文件中,有一个持久化的文件的名字 dump.rdb,路劲是下面的
子进程持久化的时候,会产生一个新的文件,比如说dump1.rdb,持久化完成以后告诉父进程原来的dump作废,要引用子进程的新的文件dump1,通知就相当于把路径变量更新成最新的文件
AOF每次执行命令都记录,实时性好,它的过程很简单,但是也会有一个环节产生阻塞(AOF重写机制),需要关注
AOF为什么要重写,因为记命令,命令会有冗余,只需要记录最后一次修改的命令就可以了
如下面这三行,相当于没有执行任何命令
但是并不是说去挨个判断哪些是冗余的,而是去看内存现有的数据,比如说内存有一个name tom,抛弃旧的AOF文件,在新的AOF中加一个命令 set name tom就可以了;而不是说对旧的文件去分析,而是分析内存中的数据,最终给定一个合适的命令来代表这个数据;
把新的AOF替换掉旧的AOF这个逻辑,叫AOF重写,相当于重新生成了一个压缩版的AOF文件,这个很关键,否则文件会很大
AOF持久化流程
(1)客户端的请求写命令会被append追加到AOF缓冲区aof_buf内,查询的命令不会被写入;
(2)AOF缓冲区根据AOF持久化策略[always,everysec,no]将操作sync同步到磁盘的AOF文件中;每次刷盘性能很差,一般不用;按操作系统来,什么时候满了什么时候刷盘,不可控,一般也不会用;一般是1s刷一次
(3)AOF文件大小超过重写策略或手动重写时,会对AOF文件rewrite重写,压缩AOF文件容量;
(4)Redis服务重启时,会重新load加载AOF文件中的写操作达到数据恢复的目的;
重点是AOF重写:
Rewrite压缩
no-appendfsync-on-rewrite:
如果 no-appendfsync-on-rewrite=yes ,不写入aof文件只写入缓存,用户请求不会阻塞,但是在这段时间如果宕机会丢失这段时间的缓存数据。(降低数据安全性,提高性能)
如果 no-appendfsync-on-rewrite=no, 还是会把数据往磁盘里刷,但是遇到重写操作,可能会发生阻塞。(数据安全,但是性能降低)
触发机制,何时重写
Redis会记录上次重写时的AOF大小,默认配置是当AOF文件大小是上次rewrite后大小的一倍且文件大于64M时触发
重写虽然可以节约大量磁盘空间,减少恢复时间。但是每次重写还是有一定的负担的,因此设定Redis要满足一定条件才会进行重写。
auto-aof-rewrite-percentage:设置重写的基准值,文件达到100%时开始重写(文件是原来重写后文件的2倍时触发)
auto-aof-rewrite-min-size:设置重写的基准值,最小文件64MB。达到这个值开始重写。
例如:文件达到70MB开始重写,降到50MB,下次什么时候开始重写?100MB
系统载入时或者上次重写完毕时,Redis会记录此时AOF大小,设为base_size,
如果Redis的AOF当前大小>= base_size +base_size*100% (默认) 且当前大小>=64mb(默认)的情况下,Redis会对AOF进行重写。
3、重写流程
(1)bgrewriteaof触发重写,判断是否当前有bgsave或bgrewriteaof在运行,如果有前者,等待结束,推迟执行;有后者,返回
(2)主进程fork出子进程执行重写操作,保证主进程不会阻塞。fork时父进程阻塞
(3)在子进程创建的那一刻,子进程和父进程是共享内存的,会把此刻的内存做一个快照,子进程读取快照的数据往新的AOF文件里写,并不是分析旧的AOF文件;
(4)在子进程新的AOF文件时,父进程会把客户端的写请求同时写入aof_buf缓冲区和aof_rewrite_buf重写缓冲区,保证原AOF文件完整以及新AOF文件生成期间的新的数据修改动作不会丢失。
(5)子进程写完新的AOF文件后,向主进程发信号,父进程更新统计信息。
(6)主进程把aof_rewrite_buf中的数据写入到新的AOF文件。一般aof_rewrite_buf中的数据很少,同步很快
(7)使用新的AOF文件覆盖旧的AOF文件,完成AOF重写。
bgsave每次都会把内存中的数据存到文件里,不能每一次执行命令都去做这样的事情,频繁做会对父进程阻塞,所以bgsave往往是隔几个小时做一次,起到一个备份的作用
aof不要看重写,而是左边的部分(白色)是和bgsave的作用是等同的,每1s可以写到AOF文件中,是实时的
bgsave是把内存中所有数据同步到文件里,每次都做,会阻塞,受不了;AOF是每次追加一个命令,可以接受
AOF的问题是文件体积大,重写是为了压缩文件,这件事不是必须的,做的话是为了节省磁盘空间
如果重写过程中断点,同样可以恢复1s内的数据,因为新的数据在旧的AOF文件中也有,仅仅是没有得到压缩后的AOF文件,不影响数据完整性
惰性删除有一个问题:就是如果不访问过期的key,就永远不会删除这个key
redis分配的内存空间满了(maxmemory)
volatile的意思是从设置了过期时间的key中,选择一些淘汰,后缀代表选择方式不同
allkeys是从所有的键中,选择一些淘汰
ttl 是选择将要过期的key淘汰,always中因为可能有的没有设置过期时间,所以不适用
redis往往承接的是缓存的任务,缓存很多时候要和数据库同步;
被动是说,如果缓存的数据因为到期被淘汰,那么下一次查询的时候,在缓存中查不到,就会查数据库,然后同步到缓存;被动一般是基于查询而触发的同步
主动是更新数据库以后,缓存中的数据不一样了,就要把缓存删除,下次再查的时候,会走被动方式
当然,也可以先删缓存再更新数据库,或者更新缓存,但是最建议的方式是先更新数据库,再删除缓存
为什么删除缓存比较好?
删除缓存要比更新缓存更合理,因为更新缓存,缓存数据结构可能比较复杂,需要特殊处理;或者说有可能更新缓存比较频繁,而查询比较少,白白浪费操作,缓存更新以后很长一段时间不被使用,这次更新就没什么用,效率低
而删除很简单,下次查询的时候,再去访问数据库回填缓存
基于第二步失败这个前提,看哪个更合理:
如果先删除缓存,再更新数据库的话:如果第一步删除缓存成功了(图上写错了,应该是del),第二步更新数据库没成功,而却有线程查询缓存中的数据,但因为缓存数据已经被删除了,所以需要到数据库中查,而查后又将数据放在了缓存中;而异步重试更新以后的数据库,和缓存中的数据不一致了,不同步了
如果先更新数据库,再删除缓存的话:如果第一步更新成功了,但是删除缓存失败了,如果此时有人查数据,查到的是老数据;异步调试以后缓存删除成功,再查就会是新的数据了。我们认为是同步的
问题是有一些线程会得到旧的数据,但是更容易被人接受
如果两步都成功了:
先删缓存,再更新数据库,数据库是新的数据,但是缓存中仍然后填到旧的数据,仍然会出现不同步的问题
如果先更新数据库,再删除缓存,仍然会读到旧的数据,比较能接受
还需要注意的一点是:
如果在缓存和数据库同步的过程中,如果出错,为了保证两者的一致性,会重试,是异步的
会把这件事提交给一个消息队列,用一个线程消费,去解决这个问题
这种同步缓存的方式适用于允许一定延迟、有一定脏数据存在的场景,例如在查看淘宝时一个手机的信息做了更改,但是刷新以后看到的信息还是原来的信息
但是如果是库存的情况,就要尽快的同步,还要保证同步的成功率,还有回查的机制,所以要用特殊的机制,库存严格来说不是缓存
库存是敏感数据
缓存空对象可能会占用很对缓存的空间,不是很好
布隆过滤器是用哈希算法估算键值是否存在,绝大部分情况下是准确的