为什么要用Redis
主要从“高性能”和“高并发”这两点来看待这个问题。
高性能:
假如用户第一次访问数据库中的某些数据。这个过程会比较慢,因为是从硬盘上读取的。将该用户访问的数据存在数缓存中,这样下一次再访问这些数据的时候就可以直接从缓存中获取了。操作缓存就是直接操作内存,所以速度相当快。如果数据库中的对应数据改变的之后,同步改变缓存中相应的数据即可。
高并发:
直接操作缓存能够承受的请求是远远大于直接访问数据库的,所以我们可以考虑把数据库中的部分数据转移到缓存中去,这样用户的一部分请求会直接到缓存这里而不用经过数据库。
redis的事务和mysql的事务有什么区别
MySQL事务的四大特性:原子性,一致性,隔离性,持久性
mysql:
Begin:显式的开启一个事务
Commit:提交事务,将对数据库进行的所有的修改变成永久性
Rollback:结束用户的事务,并撤销现在正在进行的未提交的修改
redis:
Multi:标记事务的开始
Exec:执行事务的commands队列
Discard:结束事务,并清除commands队列
mysql会默认开启一个事务,且缺省设置是自动提交,即每成功执行sql,一个事务就会马上commit,所以不能rollback
redis默认不会开启事务,即command会立即执行,而不会排队,并不支持rollback
mysql(包含两种方式):
用Begin、Rollback、commit显式开启并控制一个 新的 Transaction
执行命令 set autocommit=0,用来禁止当前会话自动commit,控制 默认开启的事务
redis:
用multi、exec、discard,显式开启并控制一个Transaction。
mysql:
mysql实现事务,是基于undo/redo日志
undo记录修改前状态,rollback基于undo日志实现
redo记录修改后的状态,commit基于redo日志实现
redis:
redis实现事务,是基于commands队列
如果没有开启事务,command将会被立即执行并返回执行结果,并且直接写入磁盘
如果事务开启,command不会被立即执行,而是排入队列,并返回排队状态(具体依赖于客户端(例如:spring-data-redis)自身实现)。
调用exec才会执行commands队列
Redis事务不支持Rollback(重点)
Redis命令可能会执行失败,仅仅是由于错误的语法被调用(命令排队时检测不出来的错误),或者使用错误的数据类 型操作某个Key:COMMAND排队失败)
这意味着,实际上失败的命令都是编程错误造成的,都是开发中能够被检测出来的,生产环境中不应该存在。
需要注意的是,即使命令失败,队列中的所有其他命令也会被处理——Redis不会停止命令的处理
如果排队命令时发生错误,大多数客户端将中止该事务并清除命令队列
如果key在Exec命令执行前有改变,那么整个事务被取消,Exec返回null表示事务没有成功。证明有锁冲突了,全部事务取消造成命令浪费,从而解决了分布式事务问题,这种锁处理也称为“乐观锁”,它提供有效昂是保障竞争条件。容易造成资源浪费
watch指令类似于乐观锁事务提交时,如果key的值已被别的客户端改变,整个事务队列都不会被执行
无原子性
Redis 开始事务 multi 命令后,Redis 会为这个事务生成一个队列,每次操作的命令都会按照顺序插入到这个队列中。
但这并不保证原子性,redis的错误分为两种,一种是比较明显的语法错误,整个事务都不会被执行,但是另一种编译未错,运行错误时,其他语句仍然会执行
但是其处理网络IO和执行客户端请求的只有一个线程,对于客户端而言是个单线程服务器。
所以redis一致性和隔离性都天然的好。
Redis为什么需要事务
redis服务器本身而言是没有竞态的,将活跃的客户端一个一个取出,将客户端中的请求一条一条执行,所有的处理都是one by one的。
但是一个redis服务器会有多个客户端进行连接,他们之间可能会出现竞态的。
超卖问题
总结
- Redis 具备了一定的原子性,但不支持回滚;
- Redis 并不能用传统的一致性概念来看待。(或者说 Redis 在设计时就无视这点);
- Redis 具备隔离性;
- Redis 通过一定策略可以保证持久性。
Redis的线程模型
redis 和 memcached 的区别
总结四点区别:
redis支持更丰富的数据类型(支持更复杂的应用场景):Redis不仅仅支持简单的k/v类型的数据,同时还提供list,set,zset,hash等数据结构的存储。memcache支持简单的数据类型,String。
Redis支持数据的持久化,可以将内存中的数据保持在磁盘中,重启的时候可以再次加载进行使用,而Memecache把数据全部存在内存之中。
集群模式:memcached没有原生的集群模式,需要依靠客户端来实现往集群中分片写入数据;但是 redis 目前是原生支持 cluster 模式的.
Memcached是多线程,非阻塞IO复用的网络模型;Redis使用单线程的多路 IO 复用模型。
多路I/O复用(epoll)
这里“多路”指的是多个网络连接,“复用”指的是复用同一个线程
1.网络IO都是通过Socket实现,Server在某一个端口持续监听,客户端通过Socket(IP+Port)与服务器建立连接(ServerSocket.accept),成功建立连接之后,就可以使用Socket中封装的InputStream和OutputStream进行IO交互了。针对每个客户端,Server都会创建一个新线程专门用于处理;
2.默认情况下,网络IO是阻塞模式,即服务器线程在数据到来之前处于阻塞状态,等到数据到达,会自动唤醒服务器线程,着手进行处理。阻塞模式下,一个线程只能处理一个流的IO事件;
3.为了提升服务器线程处理效率,有以下三种思路:
非阻塞(忙轮询):采用死循环方式轮询每一个流,如果有IO事件就处理,这样可以使得一个线程可以处理多个流,但是效率不高,容易导致CPU空转;
select代理(无差别轮询):可以观察多个流的IO事件,如果所有流都没有IO事件,则将线程进入阻塞状态,如果有一个或多个发生了IO事件,则唤醒线程去处理。但是还是得遍历所有的流,才能找出哪些流需要处理。如果流个数为N,则时间复杂度为O(N);
epoll代理:select代理有一个缺点,线程在被唤醒后轮询所有的Stream,还是存在无效操作。 epoll会将哪个流发生了怎样的I/O事件通知处理线程,因此对这些流的操作都是有意义的,复杂度降低到了O(1)。
单线程模型
Redis客户端对服务端的每次调用都经历了发送命令,执行命令,返回结果三个过程。其中执行命令阶段,由于Redis是单线程来处理命令的,所以每一条到达服务端的命令不会立刻执行,所有的命令都会进入一个队列中,然后逐个被执行。并且多个客户端发送的命令的执行顺序是不确定的。但是可以确定的是不会有两条命令被同时执行,不会产生并发问题,这就是Redis的单线程基本模型。
单线程高效原因
1.绝大部分请求是纯粹的内存操作,非常快速。数据存在内存中,类似于HashMap,HashMap的优势就是查找和操作的时间复杂度都是O(1);
2.采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU,不用去考虑各种锁的问题,不存在加锁释放锁操作;
3.核心是基于非阻塞的IO多路复用机制。
AOF是先执行后写日志
=所以,如果先记日志再执行命令的话,日志中就有可能记录了错误的命令,Redis 在使用日志恢复数据时,就可能会出错。
而写后日志这种方式,就是先让系统执行命令,只有命令能执行成功,才会被记录到日志中,否则,系统就会直接向客户端报错。
Redis 使用写后日志这一方式的一大好处是,
可以避免出现记录错误命令的情况。
除此之外,AOF 还有一个好处:它是在命令执行后才记录日志,所以不会阻塞当前的写操 作。
Redis数据结构详解
1)String
常用命令:set/get/decr/incr/mget等;
应用场景:String是最常用的一种数据类型,普通的key/value存储都可以归为此类;
实现方式:String在redis内部存储默认就是一个字符串,被redisObject所引用,当遇到incr、decr等操作时会转成数值型进行计算,此时redisObject的encoding字段为int。
2)Hash
常用命令:hget/hset/hgetall等
应用场景:我们要存储一个用户信息对象数据,其中包括用户ID、用户姓名、年龄和生日,通过用户ID我们希望获取该用户的姓名或者年龄或者生日;
实现方式:Redis的Hash实际是内部存储的Value为一个HashMap,并提供了直接存取这个Map成员的接口。如图所示,Key是用户ID, value是一个Map。这个Map的key是成员的属性名,value是属性值。这样对数据的修改和存取都可以直接通过其内部Map的Key(Redis里称内部Map的key为field), 也就是通过 key(用户ID) + field(属性标签) 就可以操作对应属性数据。当前HashMap的实现有两种方式:当HashMap的成员比较少时Redis为了节省内存会采用类似一维数组的方式来紧凑存储,而不会采用真正的HashMap结构,这时对应的value的redisObject的encoding为zipmap,当成员数量增大时会自动转成真正的HashMap,此时encoding为ht。
3)List
常用命令:lpush/rpush/lpop/rpop/lrange等;
应用场景:Redis list的应用场景非常多,也是Redis最重要的数据结构之一,比如twitter的关注列表,粉丝列表等都可以用Redis的list结构来实现;
实现方式:Redis list的实现为一个双向链表,即可以支持反向查找和遍历,更方便操作,不过带来了部分额外的内存开销,Redis内部的很多实现,包括发送缓冲队列等也都是用的这个数据结构。
4)Set
常用命令:sadd/spop/smembers/sunion等;
应用场景:Redis set对外提供的功能与list类似是一个列表的功能,特殊之处在于set是可以自动排重的,当你需要存储一个列表数据,又不希望出现重复数据时,set是一个很好的选择,并且set提供了判断某个成员是否在一个set集合内的重要接口,这个也是list所不能提供的;
实现方式:set 的内部实现是一个 value永远为null的HashMap,实际就是通过计算hash的方式来快速排重的,这也是set能提供判断一个成员是否在集合内的原因。
(4)set
set 是无序集合,自动去重。直接基于 set 将系统里需要去重的数据扔进去,自动就给去重了,如果需要对一些数据进行快速的全局去重,你当然也可以基于 jvm 内存里的 HashSet 进行去重,但是如果你的某个系统部署在多台机器上呢?得基于 redis 进行全局的 set 去重。
5)Sorted Set
常用命令:zadd/zrange/zrem/zcard等;
应用场景:Redis sorted set的使用场景与set类似,区别是set不是自动有序的,而sorted set可以通过用户额外提供一个优先级(score)的参数来为成员排序,并且是插入有序的,即自动排序。当你需要一个有序的并且不重复的集合列表,那么可以选择sorted set数据结构,比如twitter 的public timeline可以以发表时间作为score来存储,这样获取时就是自动按时间排好序的。
实现方式:Redis sorted set的内部使用HashMap和跳跃表(SkipList)来保证数据的存储和有序,HashMap里放的是成员到score的映射,而跳跃表里存放的是所有的成员,排序依据是HashMap里存的score,使用跳跃表的结构可以获得比较高的查找效率,并且在实现上比较简单。
三 Redis哨兵模式
1.背景
哨兵(Sentinel)是Redis的主从架构模式下,实现高可用性(high availability)的一种机制。
由一个或多个Sentinel实例(instance)组成的Sentinel系统(system)可以监视任意多个主服务器,以及这些主服务器属下的所有从服务器,并在被监视的主服务器进入下线状态时,自动将下线主服务器属下的某个从服务器升级为新的主服务器,然后由新的主服务器代替已下线的主服务器继续处理命令请求。
Redis支持主从同步机制,redis2作为redis1的slave从机,同步复制master的内容,当其中一个数据库宕机,应用服务器是很难直接通过找地址来切换成redis2,这时就用到了redis sentinal 哨兵机制。sentinal与redis1和redis2建立长连接,与主机连接是心跳机制,miaosha.jar无需知道redis1,redis2主从关系,只需ask redis sentinal,之后sentinal就response回应redis1为master,redis2为slave
Redis缓存问题
缓存穿透
指的是对某个一定不存在的数据进行请求,该请求将会穿透缓存到达数据库
解决方案:
1接口层增加校验,如用户鉴权校验,id做基础校验,id<=0的直接拦截;
2.**布隆过滤器**
布隆过滤器
。它实际上是一个很长的二进制向量和一系列随机映射函数。布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和删除困难。
如果想判断一个元素是不是在一个集合里,一般想到的是将集合中所有元素保存起来,然后通过比较确定。布隆过滤器的原理是,当一个元素被加入集合时,通过K个Hash函数将这个元素映射成一个位数组中的K个点,把它们置为1。检索时,我们只要看看这些点是不是都是1就(大约)知道集合中有没有它了:如果这些点有任何一个0,则被检元素一定不在;如果都是1,则被检元素很可能在。这就是布隆过滤器的基本思想。
比如我们的数据库用户id是111,112,113,114依次递增,但是别人要攻击你,故意拿-100,-936,-545这种乱七八糟的key来查询,这时候redis和数据库这种值都是不存在的,人家每次拿的key也不一样,你就算缓存了也没用,这时候数据库的压力是相当大,比上面这种情况可怕的多,怎么办呢,这时候我们今天的主角布隆过滤器就登场了。
这里引入一种节省空间的数据结构,位图,他是一个有序的数组,只有两个值,0 和 1。0代表不存在,1代表存在。
用哈希函数有两个好处,第一是哈希函数无论输入值的长度是多少,得到的输出值长度是固定的,第二是他的分布是均匀的,如果全挤的一块去那还怎么区分,
第一种扩容,但是占内存
第二种方式就是经过多几个哈希函数的计算,你想啊,24和147现在经过一次计算就碰撞了,那我经过5次,10次,100次计算还能碰撞的话那真的是缘分了,你们可以在一起了,但也不是越多次哈希函数计算越好,因为这样很快就会填满位图,而且计算也是需要消耗时间,所以我们需要在时间和空间上寻求一个平衡。
从元素的角度来说:
如果元素实际存在,布隆过滤器一定判断存在
如果元素实际不存在,布隆过滤器可能判断存在
从容器的角度来说:
如果布隆过滤器判断元素在集合中存在,不一定存在
如果布隆过滤器判断不存在,一定不存在
原理过程图!!!!
缓存击穿
缓存击穿,是指一个key非常热点,在不停的扛着大并发,大并发集中对这一个点进行访问,当这个key在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库,就像在一个屏障上凿开了一个洞。
解决方案:
1.设置热点数据永远不过期。
2.加互斥锁,
缓存雪崩
缓存雪崩是指缓存中数据大批量到过期时间,而查询数据量巨大,引起数据库压力过大甚至down机。和缓存击穿不同的是,缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。
解决方案:
1缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生。
2如果缓存数据库是分布式部署,将热点数据均匀分布在不同搞得缓存数据库中。
3设置热点数据永远不过期。
Redis数据结构
Redis(1)——5种基本数据结构 - 我没有三颗心脏的博客 (wmyskxz.com)
string(字符串)、list(列表)、hash(字典)、set(集合) 和 zset(有序集合)。
Redis跳表
那么会先从第顶层开始找,方式就是循环比较,如过顶层节点的下一个节点为空说明到达末尾,会跳到第二层,继续遍历,直到找到对应节点。
继续找顶层的下一个节点,发现 66 也是大于五的,继续遍历。由于下一节点为空,则会跳到 level 2。
记录下当前处在 5 这个节点,那接下来遍历是 5 往后走,发现 100 大于目标 66,所以还是继续下沉。
平均时间复杂度为O(log n)。
String类型
SDS 与 C 字符串的区别
获取字符串长度为 O(N) 级别的操作 → 因为 C 不保存数组的长度,每次都需要遍历一遍整个数组;
不能很好的杜绝 缓冲区溢出/内存泄漏 的问题 → 跟上述问题原因一样,如果执行拼接 or 缩短字符串的操作,如果操作不当就很容易造成上述问题;
C 字符串 只能保存文本数据 中间出现的 ‘\0’ 可能会被判定为提前结束的字符串而识别不了
SDS称为「简单动态字符串」
- len保存了字符串的长度,
- free表示buf数组中未使用的字节数量
- buf数组则是保存字符串的每一个字符元素。
1.Redis中获取字符串只要读取len的值就可,时间复杂度变为O(1)。
2.SDS是二进制安全的,除了可以储存字符串以外还可以储存二进制文件(如图片、音频,视频等文件的二进制数据
3.SDS还提供「空间预分配」和「惰性空间释放」两种策略。在为字符串分配空间时,分配的空间比实际要多,这样就能「减少连续的执行字符串增长带来内存重新分配的次数」。
4.而SDS会先根据len属性判断空间是否满足要求,若是空间不够,就会进行相应的空间扩展,所以「不会出现缓冲区溢出的情况」。
SDS 的自动扩容机制完全杜绝了发生缓冲区溢出的可能性:当SDS API需要对SDS进行修改时,API会先检查 SDS 的空间是否满足修改所需的要求,如果不满足,API会自动将SDS的空间扩展至执行修改所需的大小,然后才执行实际的修改操作,所以使用 SDS 既不需要手动修改SDS的空间大小,也不会出现缓冲区溢出问题
惰性空间释放机制:则用于优化 SDS 字符串缩短时并不立即使用内存重分配来回收缩短后多出来的空间,而仅仅更新 SDS 的len属性,多出来的空间供将来使用。
buf的空间并未真正被清空,新的数据可以复写,而不用重新申请内存。
Redis持久化机制(默认端口6379)
RDB持久化(默认支持,无需配置)
(原理是将Reids在内存中的数据库记录定时dump到磁盘上的RDB持久化)
实际操作过程是fork一个子进程,先将数据集写入临时文件,写入成功后,再替换之前的文件,用二进制压缩存储。
优点
- 启动快
- 效率高
缺点
RDB弊端:
存储数据量较大,效率较低
基于快照思想,每次读写都是全部数据,当数据量巨大时,效率非常低
大数据量下的IO性能较低
基于fork创建子进程,内存产生额外消耗
宕机带来的数据丢失风险
AOF持久化
(原理是将Reids的操作日志以追加的方式写入文件)
以日志的形式记录服务器所处理的每一个写、删除操作,查询操作不会记录,以文本的方式记录。
当客户端发出一条指令给服务器时,服务器收到并没有马上记录,而是放到临时区域:刷新缓存区,缓存区是最终存成文件时用的
优点
- 高可用! 即每秒同步
- 可读性强!AOF文件的内容非常容易被人读懂,对文件进行分析也很轻松
缺点
1). 对于相同数量的数据集而言,AOF文件通常要大于RDB文件。RDB 在恢复大数据集时的速度比 AOF 的恢复速度要快。
- . 根据同步策略的不同,AOF在运行效率上往往会慢于RDB。
官方建议 两种持久化机制同时开启,如果两个同时开启 优先使用aof
不建议单独用 AOF,因为可能会出现Bug。
真出什么时候第一时间用RDB恢复,然后AOF做数据补全
Redis哨兵模式
哨兵+主从并不能保证数据不丢失,但是可以保证集群的高可用。
集群监控:负责监控 Redis master 和 slave 进程是否正常工作。
消息通知:如果某个 Redis 实例有故障,那么哨兵负责发送消息作为报警通知给管理员。
故障转移:如果 master node 挂掉了,会自动转移到 slave node 上。
配置中心:如果故障转移发生了,通知 client 客户端新的 master 地址。
但是你让这个master机器去写,数据同步给别的slave机器,他们都拿去读,分发掉大量的请求那是不是好很多,而且扩容的时候还可以轻松实现水平扩容。
数据同步加持久化详解RDB
分别是「 AOF 日志和 RDB 快照」。
这两种技术都会用各用一个日志文件来记录信息,但是记录的内容是不同的。
AOF 文件的内容是操作命令;
RDB 文件的内容是二进制数据。
关于 AOF 持久化的原理我在上一篇已经介绍了,今天主要讲下 RDB 快照。
所谓的快照,就是记录某一个瞬间东西,比如当我们给风景拍照时,那一个瞬间的画面和信息就记录到了一张照片。
所以,RDB 快照就是记录某一个瞬间的内存数据,记录的是实际数据,而 AOF 文件记录的是命令操作的日志,而不是实际的数据。
因此在 Redis 恢复数据时, RDB 恢复数据的效率会比 AOF 高些,因为直接将 RDB 文件读入内存就可以,不需要像 AOF 那样还需要额外执行操作命令的步骤才能恢复数据。
快照怎么用?
要熟悉一个东西,先看看怎么用是比较好的方式。
Redis 提供了两个命令来生成 RDB 文件,分别是 save 和 bgsave,他们的区别就在于是否在「主线程」里执行:
执行了 save 命令,就会在主线程生成 RDB 文件,由于和执行操作命令在同一个线程,所以如果写入 RDB 文件的时间太长,会阻塞主线程;
执行了 bgsava 命令,会创建一个子进程来生成 RDB 文件,这样可以避免主线程的阻塞;
RDB 文件的加载工作是在服务器启动时自动执行的,Redis 并没有提供专门用于加载 RDB 文件的命令。
Redis 还可以通过配置文件的选项来实现每隔一段时间自动执行一次 bgsava 命令,默认会提供以下配置:
save 900 1
save 300 10
save 60 10000
别看选项名叫 sava,实际上执行的是 bgsava 命令,也就是会创建子进程来生成 RDB 快照文件。
只要满足上面条件的任意一个,就会执行 bgsava,它们的意思分别是:
900 秒之内,对数据库进行了至少 1 次修改;
300 秒之内,对数据库进行了至少 10 次修改;
60 秒之内,对数据库进行了至少 10000 次修改。
这里提一点,Redis 的快照是全量快照,也就是说每次执行快照,都是把内存中的「所有数据」都记录到磁盘中。
所以可以认为,执行快照是一个比较重的操作,如果频率太频繁,可能会对 Redis 性能产生影响。如果频率太低,服务器故障时,丢失的数据会更多。
通常可能设置至少 5 分钟才保存一次快照,这时如果 Redis 出现宕机等情况,则意味着最多可能丢失 5 分钟数据。
这就是 RDB 快照的缺点,在服务器发生故障时,丢失的数据会比 AOF 持久化的方式更多,因为 RDB 快照是全量快照的方式,因此执行的频率不能太频繁,否则会影响 Redis 性能,而 AOF 日志可以以秒级的方式记录操作命令,所以丢失的数据就相对更少。
执行快照时,数据能被修改吗?
直接说结论吧,执行 bgsava 过程中,Redis 依然可以继续处理操作命令的,也就是数据是能被修改的。
那具体如何做到到呢?关键的技术就在于写时复制技术(Copy-On-Write, COW)。
执行 bgsava 命令的时候,会通过 fork() 创建子进程,此时子进程和父进程是共享同一片内存数据的,因为创建子进程的时候,会复制父进程的页表,但是页表指向的物理内存还是一个。
这样的目的是为了减少创建子进程时的性能损耗,从而加快创建子进程的速度,毕竟创建子进程的过程中,是会阻塞主线程的。
所以,创建 bgsave 子进程后,由于共享父进程的所有内存数据,于是就可以直接读取主线程里的内存数据,并将数据写入到 RDB 文件。
当主线程对这些共享的内存数据也都是只读操作,那么,主线程和 bgsave 子进程相互不影响。
但是,如果主线程要修改共享数据里的某一块数据(比如键值对 A)时,就会发生写时复制,于是这块数据的物理内存就会被复制一份(键值对 A’),然后主线程在这个数据副本(键值对 A’)进行修改操作。与此同时,bgsave 子进程可以继续把原来的数据(键值对 A)写入到 RDB 文件。
就是这样,Redis 使用 bgsave 对当前内存中的所有数据做快照,这个操作是由 bgsave 子进程在后台完成的,执行时不会阻塞主线程,这就使得主线程同时可以修改数据。
细心的同学,肯定发现了,bgsave 快照过程中,如果主线程修改了共享数据,发生了写时复制后,RDB 快照保存的是原本的内存数据,而主线程刚修改的数据,是被办法在这一时间写入 RDB 文件的,只能交由下一次的 bgsave 快照。
所以 Redis 在使用 bgsave 快照过程中,如果主线程修改了内存数据,不管是否是共享的内存数据,RDB 快照都无法写入主线程刚修改的数据,因为此时主线程的内存数据和子线程的内存数据已经分离了,子线程写入到 RDB 文件的内存数据只能是原本的内存数据。
如果系统恰好在 RDB 快照文件创建完毕后崩溃了,那么 Redis 将会丢失主线程在快照期间修改的数据。
另外,写时复制的时候会出现这么个极端的情况。
在 Redis 执行 RDB 持久化期间,刚 fork 时,主进程和子进程共享同一物理内存,但是途中主进程处理了写操作,修改了共享内存,于是当前被修改的数据的物理内存就会被复制一份。
那么极端情况下,如果所有的共享内存都被修改,则此时的内存占用是原先的 2 倍。
所以,针对写操作多的场景,我们要留意下快照过程中内存的变化,防止内存被占满了。
RDB 和 AOF 合体
尽管 RDB 比 AOF 的数据恢复速度快,但是快照的频率不好把握:
如果频率太低,两次快照间一旦服务器发生宕机,就可能会比较多的数据丢失;
如果频率太高,频繁写入磁盘和创建子进程会带来额外的性能开销。
那有没有什么方法不仅有 RDB 恢复速度快的优点和,又有 AOF 丢失数据少的优点呢?
当然有,那就是将 RDB 和 AOF 合体使用,这个方法是在 Redis 4.0 提出的,该方法叫混合使用 AOF 日志和内存快照,也叫混合持久化。
如果想要开启混合持久化功能,可以在 Redis 配置文件将下面这个配置项设置成 yes:
aof-use-rdb-preamble yes
1
混合持久化工作在 AOF 日志重写过程。
当开启了混合持久化时,在 AOF 重写日志时,fork 出来的重写子进程会先将与主线程共享的内存数据以 RDB 方式写入到 AOF 文件,然后主线程处理的操作命令会被记录在重写缓冲区里,重写缓冲区里的增量命令会以 AOF 方式写入到 AOF 文件,写入完成后通知主进程将新的含有 RDB 格式和 AOF 格式的 AOF 文件替换旧的的 AOF 文件。
也就是说,使用了混合持久化,AOF 文件的前半部分是 RDB 格式的全量数据,后半部分是 AOF 格式的增量数据
这样的好处在于,重启 Redis 加载数据的时候,由于前半部分是 RDB 内容,这样加载的时候速度会很快。
加载完 RDB 的内容后,才会加载后半部分的 AOF 内容,这里的内容是 Redis 后台子进程重写 AOF 期间,主线程处理的操作命令,可以使得数据更少的丢失。
回归正题,他们数据怎么同步的呢?
你启动一台slave 的时候,他会发送一个psync命令给master ,如果是这个slave第一次连接到master,他会触发一个全量复制。master就会启动一个线程,生成RDB快照,还会把新的写请求都缓存在内存中,RDB文件生成后,master会将这个RDB发送给slave的,slave拿到之后做的第一件事情就是写进本地的磁盘,然后加载进内存,然后master会把内存里面缓存的那些新命名都发给slave。
数据传输的时候断网了或者服务器挂了怎么办啊?
传输过程中有什么网络问题啥的,会自动重连的,并且连接之后会把缺少的数据补上的。
大家需要记得的就是,RDB快照的数据生成的时候,缓存区也必须同时开始接受新请求,不然你旧的数据过去了,你在同步期间的增量数据咋办?是吧?
Redis的过期策略,是有定期删除+惰性删除*两种。
Redis主从复制原理
主从服务器间的第一次同步的过程可分为三个阶段:
- 第一阶段是建立链接、协商同步;
- 第二阶段是主服务器同步数据给从服务器;
- 第三阶段是主服务器发送新写操作命令给从服务器
那么为了保证主从服务器的数据一致性,主服务器会将在 RDB 文件生成后收到的写操作命令,写入到 replication buffer 缓冲区里。
第三阶段:主服务器发送新写操作命令给从服务器
在主服务器生成的 RDB 文件发送后,然后将 replication buffer 缓冲区里所记录的写操作命令发送给从服务器,然后从服务器重新执行这些操作。
至此,主从服务器的第一次同步的工作就完成了
命令传播
主从服务器在完成第一次同步后,双方之间就会维护一个 TCP 连接。
而且这个连接是长连接的,目的是避免频繁的 TCP 连接和断开带来的性能开销。
上面的这个过程被称为基于长连接的命令传播,通过这种方式来保证第一次同步后的主从服务器的数据一致性
所以,从 Redis 2.8 开始,网络断开又恢复后,从主从服务器会采用增量复制的方式继续同步,也就是只会把网络断开期间主服务器接收到的写操作命令,同步给从服务器。
增量复制
那么关键的问题来了,主服务器怎么知道要将哪些增量数据发送给从服务器呢?
答案藏在这两个东西里:
repl_backlog_buffer,是一个「环形」缓冲区,用于主从服务器断连后,从中找到差异的数据;
replication offset,标记上面那个缓冲区的同步进度,主从服务器都有各自的偏移量,主服务器使用 master_repl_offset 来记录自己「写」到的位置,从服务器使用 slave_repl_offset 来记录自己「读」到的位置。
那repl_backlog_buffer 缓冲区是什么时候写入的呢?
在主服务器进行命令传播时,不仅会将写命令发送给从服务器,还会将写命令写入到 repl_backlog_buffer 缓冲区里,因此 这个缓冲区里会保存着最近传播的写命令。
网络断开后,当从服务器重新连上主服务器时,从服务器会通过 psync 命令将自己的复制偏移量 slave_repl_offset 发送给主服务器,主服务器根据自己的 master_repl_offset 和 slave_repl_offset 之间的差距,然后来决定对从服务器执行哪种同步操作:
如果判断出从服务器要读取的数据还在 repl_backlog_buffer 缓冲区里,那么主服务器将采用增量同步的方式;
相反,如果判断出从服务器要读取的数据已经不存在
repl_backlog_buffer 缓冲区里,那么主服务器将采用全量同步的方式。
当主服务器在 repl_backlog_buffer 中找到主从服务器差异(增量)的数据后,就会将增量的数据写入到 replication buffer 缓冲区,这个缓冲区我们前面也提到过,它是缓存将要传播给从服务器的命令。
11.Redis 主从同步原理
Slave 初始化中是全量同步,
- 从服务器连接主服务器,发送SYNC命令;
- 主服务器接收到SYNC命名后,开始执行BGSAVE命令生成RDB文件并使用缓冲区记 录此后执行的所有写命令;
- 主服务器BGSAVE执行完后,向所有从服务器发送快照文件,并在发送期间继续记录 被执行的写命令;
- 从服务器收到快照文件后丢弃所有旧数据,载入收到的快照;
- 主服务器快照发送完毕后开始向从服务器发送缓冲区中的写命令;
- 从服务器完成对快照的载入,开始接收命令请求,并执行来自主服务器缓冲区的写命 令;
全量之后是增量同步:指Slave初始化后开始正常工作时主服务器发生的写操作同 步到从服务器的过程。
- 一个master可以有多个slave,slave也可以有多个slave,组成树状结构2)主从同步不会阻塞master,但是会阻塞slave。也就是说当一个或多个slave与master进行初次同步数据时,master可以继续处理client发来的请求。相反slave在初次同步数据时则会阻塞不能处理client的请求;3)主从同步可以用来提高系统的可伸缩性,我们可以用多个slave专门处理client的读请求,也可以用来做简单的数据冗余或者只在slave上进行持久化从 而提升集群的整体性能。
非关系型数据库与关系型数据库区别
1 关系数据库:
是建立在关系模型基础上的数据库,借助于集合代数等数学概念和方法来处理数据库中的数据。简单说来就是关系型数据库用了选择、投影、连接、并、交、差、除、增删查改等数学方法来实现对数据的存储和查询。可以用SQL语句方便的在一个表及其多个表之间做非常复杂的数据查询。安全性高。
其中server层包括连接池、查询缓存、分析器、优化器等部分,
重点:表与表之间有关系,1对多(靠主外键),多对多(靠中间表)2.非关系型数据库:
简称NOSQL,是基于键值对的对应关系,并且不需要经过SQL层的解析,所以性能非常高。但是不适合用在多表联合查询和一些较复杂的查询中。NoSQL用于超大规模数据的存储。
重点:表与表之间无关系,比如Redis,key,valueRedis的过期策略
过期删除策略
策略为:定期删除+惰性删除
定期删除:redis默认是每隔100ms就随机抽取一些设置了过期时间的key,检查其是否过期,如果过期就删除。如果不是随机抽取,那么每隔100ms就遍历所有的设置过期时间的key的话,就会给CPU带来很大的负载。需要创建定时器,而且消耗CPU,一般不推荐使用。
惰性删除(默认) :定期删除可能会导致很多过期 key 到了时间并没有被删除掉。所以就有了惰性删除。假如一个过期的key,靠定期删除没有被删除掉,还停留在内存里,除非去查一下那个key,才会被redis给删除掉。
3、主动删除
在redis.conf文件中可以配置主动删除策略,默认是no-enviction(不删除)
内存淘汰机制 内存满了淘汰策略
可以设置内存最大使用量,当内存使用量超出时,会施行数据淘汰策略。
Redis 具体有6种淘汰策略:
策略 | 描述 |
---|---|
volatile-lru | 从已设置过期时间的数据集中挑选最近最少使用的数据淘汰 |
volatile-ttl | 从已设置过期时间的数据集中挑选将要过期的数据淘汰 |
LRU策略
2.LRU:最近最少使用,淘汰最近不使用的页面
实现步骤原理如下:
1. 新数据插入到链表头部;
2. 每当缓存命中(即缓存数据被访问),则将数据移到链表头部;
3. 当链表满的时候,将链表尾部的数据丢弃。
双链表 + hashtable实现原理:
将Cache的所有位置都用双连表连接起来,当一个位置被命中之后,就将通过调整链表的指向,将该位置调整到链表头的位置,新加入的Cache直接加到链表头中。这样,在多次进行Cache操作后,最近被命中的,就会被向链表头方向移动,而没有命中的,而想链表后面移动,链表尾则表示最近最少使用的Cache。当需要替换内容时候,链表的最后位置就是最少被命中的位置,我们只需要淘汰链表最后的部分即可。
具体实现
具体实现Map里边是key是itemId,value是库存,每次去Map里查是否有key,如果有就直接返回value,每次命中插入链表头,如果队列满了,就抛弃队尾节点。。
ttl策略
volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的key优先移除。
2. 过期策略
redis中对过期的数据的处理,通常有三种方式:
定时过期:对每个设置过期时间的key都需要建一个定时器,到达过期时间会立即清除。该方法对内存友好,对CPU不友好,会占用大量的CPU资源去处理过期数据;
惰性过期:只有当访问一个key时,才会判断key是否过期,该方案最大化的节省CPU资源,但会占用内存资源(对CPU友好,对内存不友好);
定期过期:每隔一定的时间,会扫描一定数据的expires字典中的一些数据,并清除掉其中已过期的数据
持久化详解AOF
AOF
说到日志,我们比较熟悉的是数据库的写前日志(Write Ahead Log, WAL),也就是 说,在实际写数据前,先把修改的数据记到日志文件中,以便故障时进行恢复。
不过,AOF 日志正好相反,它是写后日志,“写后”的意思是 Redis 是先执行命令,把数据写入 内存,然后才记录日志,如下图所示:
那 AOF 为什么要先执行命令再记日志呢?
传统数据库的日志,例如 redo log(重做日志),记录的是修改后的数据,而 AOF 里记 录的是 Redis 收到的每一条命令,这些命令是以文本形式保存的。
==为了避免额外的检查开销,Redis 在向 AOF 里面记录日志的时候,并不会先去对这些命令进行语法检查。==所以,如果先记日志再执行命令的话,日志中就有可能记录了错误的命令,Redis 在使用日志恢复数据时,就可能会出错。
而写后日志这种方式,就是先让系统执行命令,只有命令能执行成功,才会被记录到日志中,否则,系统就会直接向客户端报错。
Redis 使用写后日志这一方式的一大好处是,
可以避免出现记录错误命令的情况。
除此之外,AOF 还有一个好处:它是在命令执行后才记录日志,所以不会阻塞当前的写操 作。
问题:
记录日志就宕机,丢失数据
下一条命令执行受到影响,因为需要记录执行完上一条命令AOF日志到磁盘
三种回写策略
这里的“性能问题”,主要在于以下三个方面:一是,文件系统本身对文件大小有限制, 无法保存过大的文件;二是,如果文件太大,之后再往里面追加命令记录的话,效率也会 变低;三是,如果发生宕机,AOF 中记录的命令要一个个被重新执行,用于故障恢复,如 果日志文件太大,整个恢复过程就会非常缓慢,这就会影响到 Redis 的正常使用。
AOF重写
AOF 文件是以追加的方式,逐一记录接收到的写命令的。当一个键值对被多条写命令反复修改时,AOF 文件会记录相应的多条命令。但是,在重写的时候,是根据这个键值对当前的最新状态,为它生成对应的写入命令。这样一来,一个键值对在重写日志中只用一条命令就行了,而且,在日志恢复时,只用执行这条命令,就可以直接完成这个键值对的写入了。
5.2.3.RedisTemplate基本操作
Spring Data Redis 提供了一个工具类:RedisTemplate。里面封装了对于Redis的五种数据结构的各种操作,包括:
- redisTemplate.opsForValue() :操作字符串
- redisTemplate.opsForHash() :操作hash
- redisTemplate.opsForList():操作list
- redisTemplate.opsForSet():操作set
- redisTemplate.opsForZSet():操作zset
其它一些通用命令,如expire,可以通过redisTemplate.xx()来直接调用
5种结构:
- String:等同于java中的,
Map<String,String>
- list:等同于java中的Map<String,List<String>>
- set:等同于java中的Map<String,Set<String>>
- sort_set:可排序的set
- hash:等同于java中的:`Map>