- 1-概览
- Redis是典型的Key-Value类型数据库,单线程内存型,用于做缓存(redis的计数器生成分布式唯一主键,实现分布式锁,队列和会话缓存)
- Key为字符类型,Value的类型常用的为五种类型:String、Hash 、List 、 Set 、 Ordered Set
- 2-持久化
- RDB持久化是指在指定的时间间隔内将内存中的数据集快照写入磁盘,实际操作过程是调用系统的fork()函数创建一个与当前进程一模一样的一个子进程(所有数据,变量、环境变量、程序计数器等)进行持久化操作,先将数据集写入临时文件,待持久化结束,再用临时文件替换上次的持久化文件,用二进制压缩存储。主进程不需要任何io操作,提高性能(dump文件)
- 什么时候出发fork子进程触发持久化:
- 在shoutdown,如果没有开其aof,会触发
- 配置文件中默认的快照配置
- save 900 1: 900秒内,出现一次增删改操作会触发
- save 300 10:300秒内,出现10次增删改
- save 60 10000:60秒内,10000次增删改
- save“”:关闭
- 手动执行sava:只是保存,主进程进行持久化,其他请求阻塞;bgsave:在后台进行常规fork进行持久化
- 什么时候出发fork子进程触发持久化:
- AOF持久化以日志的形式记录服务器所处理的每一个写、删除操作命令,查询操作不会记录,以文本的方式记录,可以打开文件看到详细的操作记录。在重新启动之后,会照着日志文件重新运行一遍命令完成持久化
- 持久化文件 :apppendonly.aof
- 触发机制:
- no:操作系统数据缓存同步到磁盘(快,持久化没保证)
- always:同步持久化,每次发生数据变更,都会记录日志(慢。安全)
- everysec:每秒同步一次(默认,很快,但可能会丢失1秒钟以内的数据)
- aof重写机制:如果日志过大,Redis可以自动启用rewrite机制。Aof重写不需要对原有的aof文件进行任何操作,而是通过当前数据库的状态,直接替换掉多余或者重复操作。
- 1、fork出一个子进程进行AOF重写,主进程可以继续处理命令请求;子进程带有主进程的数据副本,使用子进程而不是线程,可以避免在锁的情况下,保证数据的安全性。
- 2、Redis增加了一个AOF重写缓存,这个缓存在fork出子进程之后开始启用,Redis服务器主进程在执行完写命令之后,会同时将这个写命令追加到AOF缓冲区和AOF重写缓冲区;AOF缓冲区的内容会定期被写入和同步到AOF文件中,对现有的AOF文件的处理工作会正常进行从创建子进程开始,服务器执行的所有写操作都会被记录到AOF重写缓冲区中;
- 3、将AOF重写缓存中的内容全部写入到新的AOF文件中;这个时候新的AOF文件所保存的数据库状态和服务器当前的数据库状态一致;对新的AOF文件进行改名,原子的覆盖原有的AOF文件;完成新旧两个AOF文件的替换
- 触发机制(配置):最大AOF文件达到xmb就触发重写机制,如果正在率为100%,那么下一次从写就是2xmb
- redis4.0后的混合持久化机制:对重复多余操作进行改写合并,然后存储到dump.rdb中
- RDB和AOF:RDB由于AOF,如果两种机制共存,以Aof为主;一般需要同时开启,优先使用aof持久化机制。
- RDB存在哪些优势呢?
- 1). 一旦采用该方式,那么你的整个Redis数据库将只包含一个文件,这对于文件备份而言是非常完美的。比如,你可能打算每个小时归档一次最近24小时的数据,同时还要每天归档一次最近30天的数据。通过这样的备份策略,一旦系统出现灾难性故障,我们可以非常容易的进行恢复。
- 2). 对于灾难恢复而言,RDB是非常不错的选择。因为我们可以非常轻松的将一个单独的文件压缩后再转移到其它存储介质上。
- 3). 性能最大化。对于Redis的服务进程而言,在开始持久化时,它唯一需要做的只是fork出子进程,之后再由子进程完成这些持久化的工作,这样就可以极大的避免服务进程执行IO操作了。
- 4). 相比于AOF机制,如果数据集很大,RDB的启动效率会更高。
- RDB又存在哪些劣势呢?
- 1). 如果你想保证数据的高可用性,即最大限度的避免数据丢失,那么RDB将不是一个很好的选择。因为系统一旦在定时持久化之前出现宕机现象,此前没有来得及写入磁盘的数据都将丢失。
- 2). 由于RDB是通过fork子进程来协助完成数据持久化工作的,因此,如果当数据集较大时,可能会导致整个服务器停止服务几百毫秒,甚至是1秒钟。
- AOF的优势有哪些呢?
- 1). 该机制可以带来更高的数据安全性,即数据持久性。Redis中提供了3中同步策略,即每秒同步、每修改同步和不同步。事实上,每秒同步也是异步完成的,其效率也是非常高的,所差的是一旦系统出现宕机现象,那么这一秒钟之内修改的数据将会丢失。而每修改同步,我们可以将其视为同步持久化,即每次发生的数据变化都会被立即记录到磁盘中。可以预见,这种方式在效率上是最低的。至于无同步,无需多言,我想大家都能正确的理解它。
- 2). 由于该机制对日志文件的写入操作采用的是append模式,因此在写入过程中即使出现宕机现象,也不会破坏日志文件中已经存在的内容。然而如果我们本次操作只是写入了一半数据就出现了系统崩溃问题,不用担心,在Redis下一次启动之前,我们可以通过redis-check-aof工具来帮助我们解决数据一致性的问题。
- 3). 如果日志过大,Redis可以自动启用rewrite机制。即Redis以append模式不断的将修改数据写入到老的磁盘文件中,同时Redis还会创建一个新的文件用于记录此期间有哪些修改命令被执行。因此在进行rewrite切换时可以更好的保证数据安全性。
- 4). AOF包含一个格式清晰、易于理解的日志文件用于记录所有的修改操作。事实上,我们也可以通过该文件完成数据的重建。
- AOF的劣势有哪些呢?
- 1). 对于相同数量的数据集而言,AOF文件通常要大于RDB文件。RDB 在恢复大数据集时的速度比 AOF 的恢复速度要快。
- 2). 根据同步策略的不同,AOF在运行效率上往往会慢于RDB。总之,每秒同步策略的效率是比较高的,同步禁用策略的效率和RDB一样高效。
- 二者选择的标准,就是看系统是愿意牺牲一些性能,换取更高的缓存一致性(aof),还是愿意写操作频繁的时候,不启用备份来换取更高的性能,待手动运行save的时候,再做备份(rdb)。rdb这个就更有些 eventually consistent的意思了。
- RDB存在哪些优势呢?
- 常用配置
- RDB持久化配置
- Redis会将数据集的快照dump到dump.rdb文件中。此外,我们也可以通过配置文件来修改Redis服务器dump快照的频率,在打开6379.conf文件之后,我们搜索save,可以看到下面的配置信息:
- save 900 1 #在900秒(15分钟)之后,如果至少有1个key发生变化,则dump内存快照。
- save 300 10 #在300秒(5分钟)之后,如果至少有10个key发生变化,则dump内存快照。
- save 60 10000 #在60秒(1分钟)之后,如果至少有10000个key发生变化,则dump内存快照。
- Redis会将数据集的快照dump到dump.rdb文件中。此外,我们也可以通过配置文件来修改Redis服务器dump快照的频率,在打开6379.conf文件之后,我们搜索save,可以看到下面的配置信息:
- AOF持久化配置
- 在Redis的配置文件中存在三种同步方式,它们分别是:
- appendfsync always #每次有数据修改发生时都会写入AOF文件。
- appendfsync everysec #每秒钟同步一次,该策略为AOF的缺省策略。
- appendfsync no #从不同步。高效但是数据不会被持久化。
- 在Redis的配置文件中存在三种同步方式,它们分别是:
- RDB持久化配置
- RDB持久化是指在指定的时间间隔内将内存中的数据集快照写入磁盘,实际操作过程是调用系统的fork()函数创建一个与当前进程一模一样的一个子进程(所有数据,变量、环境变量、程序计数器等)进行持久化操作,先将数据集写入临时文件,待持久化结束,再用临时文件替换上次的持久化文件,用二进制压缩存储。主进程不需要任何io操作,提高性能(dump文件)
- 2- Redis内部内存管理
- 3- Key(键值):
- 过期删除:
- 过期数据的清除从来不容易,为每一条key设置一个timer,到点立刻删除的消耗太大,每秒遍历所有数据消耗也大,Redis使用了一种相对务实的做法: 当client主动访问key会先对key进行超时判断,过时的key会立刻删除。 如果clien永远都不再get那条key呢? 它会在Master的后台,每秒10次的执行如下操作: 随机选取100个key校验是否过期,如果有25个以上的key过期了,立刻额外随机选取下100个key(不计算在10次之内)。可见,如果过期的key不多,它最多每秒回收200条左右,如果有超过25%的key过期了,它就会做得更多,但只要key不被主动get,它占用的内存什么时候最终被清理掉只有天知道。
- 常用操作
- Key的长度限制:Key的最大长度不能超过1024字节,在实际开发时不要超过这个长度,但是Key的命名不能太短,要能唯一标识缓存的对,作者建议按照在关系型数据库中的库表唯一标识字段的方式来命令Key的值,用分号分割不同数据域,用点号作为单词连接。
- Key的查询:Keys,返回匹配的key,支持通配符如 “keys a*” 、 “keys a?c”,但不建议在生产环境大数据量下使用。
- 对Key对应的Value进行的排序:Sort命令对集合按数字或字母顺序排序后返回或另存为list,还可以关联到外部key等。因为复杂度是最高的O(N+Mlog(M))*(N是集合大小,M 为返回元素的数量),有时会安排到slave上执行。官网链接https://redis.io/commands/sort
- Key的超时操作:Expire(指定失效的秒数)/ExpireAt(指定失效的时间戳)/Persist(持久化)/TTL(返回还可存活的秒数),关于Key超时的操作。默认以秒为单位,也有p字头的以毫秒为单位的版本
- 过期删除:
- 4- String(字符串类型的Value):可以是String,也可是是任意的byte[]类型的数组,如图片等。String 在 redis 内部存储默认就是一个字符串,被 redisObject 所引用,当遇到 incr,decr 等操作时会转成数值型进行计算,此时 redisObject 的 encoding 字段为int。
- 大小限制:最大为512Mb,基本可以存储任意图片啦。
- 常用命令的时间复杂度为O(1),读写一样的快。
- 对String代表的数字进行增减操作(没有指定的Key则设置为0值,然后在进行操作):Incr/IncrBy/IncrByFloat/Decr/DecrBy(原子性),** 可以用来做计数器,做自增序列,也可以用于限流,令牌桶计数等**。key不存在时会创建并贴心的设原值为0。IncrByFloat专门针对float。。
- 设置Value的安全性:SetNx命令仅当key不存在时才Set(原子性操作)。可以用来选举Master或做分布式锁:所有Client不断尝试使用SetNx master myName抢注Master,成功的那位不断使用Expire刷新它的过期时间。如果Master倒掉了key就会失效,剩下的节点又会发生新一轮抢夺。SetEx, Set + Expire 的简便写法,p字头版本以毫秒为单位。
- 获取:GetSet(原子性), 设置新值,返回旧值。比如一个按小时计算的计数器,可以用GetSet获取计数并重置为0。这种指令在服务端做起来是举手之劳,客户端便方便很多。MGet/MSet/MSetNx, 一次get/set多个key。
- 其他操作:Append/SetRange/GetRange/StrLen,对文本进行扩展、替换、截取和求长度,只对特定数据格式如字段定长的有用,json就没什么用。
- BitMap的用法:GetBit/SetBit/BitOp,与或非/BitCount, BitMap的玩法,比如统计今天的独立访问用户数时,每个注册用户都有一个offset,他今天进来的话就把他那个位设为1,用BitCount就可以得出今天的总人数。
- 5- Hash(HashMap,哈希映射表):Redis 的 Hash 实际是内部存储的 Value 为一个 HashMap,并提供了直接存取这个 Map 成员的接口。Hash将对象的各个属性存入Map里,可以只读取/更新对象的某些属性。另外不同的模块可以只更新自己关心的属性而不会互相并发覆盖冲突。
- 实现原理
- Redis Hash 对应 Value 内部实际就是一个 HashMap,实际这里会有2种不同实现,** 这个 Hash 的成员比较少时 Redis 为了节省内存会采用类似一维数组的方式来紧凑存储,而不会采用真正的 HashMap 结构,对应的 value redisObject 的 encoding 为 zipmap,当成员数量增大时会自动转成真正的 HashMap,此时 encoding 为 ht**。一般操作复杂度是O(1),要同时操作多个field时就是O(N),N是field的数量。
- 常用操作
- O(1)操作:hget、hset等等
- O(n)操作:hgetallRedis 可以直接取到全部的属性数据,但是如果内部 Map 的成员很多,那么涉及到遍历整个内部 Map 的操作,由于 Redis 单线程模型的缘故,这个遍历操作可能会比较耗时,而另其它客户端的请求完全不响应,这点需要格外注意。
- 实现原理
- 6- List(双向链表):Redis list 的应用场景非常多,也是 Redis 最重要的数据结构之一,比如 twitter 的关注列表,粉丝列表等都可以用 Redis 的 list 结构来实现,还提供了生产者消费者阻塞模式(B开头的命令),常用于任务队列,消息队列等。
- 7- set(HashSet):Set就是HashSet,可以将重复的元素随便放入而Set会自动去重,底层实现也是HashMap,并且 set 提供了判断某个成员是否在一个 set 集合内的重要接口,这个也是 list 所不能提供的。
- 实现原理
- set 的内部实现是一个 value 永远为 null 的 HashMap,实际就是通过计算 hash 的方式来快速排重的,这也是 set 能提供判断一个成员是否在集合内的原因。
- 常用操作
- 增删改查:SAdd/SRem/SIsMember/SCard/SMove/SMembers等等。除了SMembers都是O(1)。
- 集合操作:SInter/SInterStore/SUnion/SUnionStore/SDiff/SDiffStore,各种集合操作。交集运算可以用来显示在线好友(在线用户 交集 好友列表),共同关注(两个用户的关注列表的交集)。O(N),并集和差集的N是集合大小之和,交集的N是小的那个集合的大小的2倍。
- 实现原理
- 8- Sorted Set(插入有序Set集合):set 不是自动有序的,而** sorted set 可以通过用户额外提供一个优先级(score)的参数来为成员排序,并且是插入有序的,即自动排序**。当你需要一个有序的并且不重复的集合列表,那么可以选择 sorted set 数据结构,比如 twitter 的 public timeline 可以以发表时间作为 score 来存储,这样获取时就是自动按时间排好序的。
- 实现方式
- 内部使用 HashMap 和跳跃表(SkipListhttps://www.cnblogs.com/thrillerz/p/4505550.html)来保证数据的存储和有序
- Sorted Set的实现是HashMap(element->score, 用于实现ZScore及判断element是否在集合内),和SkipList(score->element,按score排序)的混合体。SkipList有点像平衡二叉树那样,不同范围的score被分成一层一层,每层是一个按score排序的链表。
- 常用操作
- ZAdd/ZRem是O(log(N));ZRangeByScore/ZRemRangeByScore是O(log(N)+M),N是Set大小,M是结果/操作元素的个数。复杂度的log取对数很关键,可以使,1000万大小的Set,复杂度也只是几十不到。但是,如果一次命中很多元素M很大则复杂度很高。
- ZRange/ZRevRange,按排序结果的范围返回元素列表,可以为正数与倒数。
- ZRangeByScore/ZRevRangeByScore,按score的范围返回元素,可以为正数与倒数。
- ZRemRangeByRank/ZRemRangeByScore,按排序/按score的范围限删除元素。
- ZCount,统计按score的范围的元素个数。
- ZRank/ZRevRank ,显示某个元素的正/倒序的排名。
- ZScore/ZIncrby,显示元素的Score值/增加元素的Score。
- ZAdd(Add)/ZRem(Remove)/ZCard(Count),ZInsertStore(交集)/ZUnionStore(并集),与Set相比,少了IsMember和差集运算。
- 实现方式
- Redis为什么这么快
- 1、完全基于内存,绝大部分请求是纯粹的内存操作,非常快速。数据存在内存中,类似于HashMap,HashMap的优势就是查找和操作的时间复杂度都是O(1);
- 2、数据结构简单,对数据操作也简单,Redis中的数据结构是专门进行设计的;
- 3、采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗;
- 4、使用多路I/O复用模型,非阻塞IO;
- 5、使用底层模型不同,它们之间底层实现方式以及与客户端之间通信的应用协议不一样,Redis直接自己构建了VM 机制 ,因为一般的系统调用系统函数的话,会浪费一定的时间去移动和请求;
- 以上几点都比较好理解,下边我们针对多路 I/O 复用模型进行简单的探讨:多路I/O复用模型是利用 select、poll、epoll 可以同时监察多个流的 I/O 事件的能力,在空闲的时候,会把当前线程阻塞掉,当有一个或多个流有 I/O 事件时,就从阻塞态中唤醒,于是程序就会轮询一遍所有的流(epoll 是只轮询那些真正发出了事件的流),并且只依次顺序的处理就绪的流,这种做法就避免了大量的无用操作。这里“多路”指的是多个网络连接,“复用”指的是复用同一个线程。采用多路 I/O 复用技术可以让单个线程高效的处理多个连接请求(尽量减少网络 IO 的时间消耗),且 Redis 在内存中操作数据的速度非常快,也就是说内存内的操作不会成为影响Redis性能的瓶颈,主要由以上几点造就了 Redis 具有很高的吞吐量。
- 那么为什么Redis是单线程的
- 我们首先要明白,上边的种种分析,都是为了营造一个Redis很快的氛围!官方FAQ表示,因为Redis是基于内存的操作,CPU不是Redis的瓶颈,Redis的瓶颈最有可能是机器内存的大小或者网络带宽。既然单线程容易实现,而且CPU不会成为瓶颈,那就顺理成章地采用单线程的方案了(毕竟采用多线程会有很多麻烦!)。
- 谈谈Redis的过期策略以及内存淘汰机制
- 过期策略:
- 我们set key的时候,都可以给一个expire time,就是过期时间,指定这个key比如说只能存活1个小时,我们自己可以指定缓存到期就失效。如果假设你设置一个一批key只能存活1个小时,那么接下来1小时后,redis是怎么对这批key进行删除的?答案是:定期删除+惰性删除;所谓定期删除,指的是redis默认是每隔100ms就随机抽取一些设置了过期时间的key,检查其是否过期,如果过期就删除。注意,这里可不是每隔100ms就遍历所有的设置过期时间的key,那样就是一场性能上的灾难。实际上redis是每隔100ms随机抽取一些key来检查和删除的。但是,定期删除可能会导致很多过期key到了时间并没有被删除掉,所以就得靠惰性删除了。
- 这就是说,在你get某个key的时候,redis会检查一下 ,这个key如果设置了过期时间那么是否过期了?如果过期了此时就会删除,不会给你返回任何东西。并不是key到时间就被删除掉,而是你查询这个key的时候,redis再懒惰的检查一下
- 如果大量过期key堆积在内存里,导致redis内存块耗尽了,怎么办?答案是:内存淘汰机制。
- 内存淘汰机制
- 如果redis的内存占用过多的时候,此时会进行内存淘汰,有如下一些策略:
- allkeys-lru:当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key(这个是最常用的)
- volatile-random:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,随机移除某个key
- volatile-ttl:当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的key优先移除
- 过期策略:
- 如何解决Redis和和数据库双写一致性的问题?读的时候,先读缓存,缓存没有的话,就读数据库,然后取出数据后放入缓存,同时返回响应。写的时候出现双写怎么办:更新数据库的时候,更新的时候,先更新数据库,然后再删除缓存。等下一次查询的时候再将数据库数据更新至内存;
- 方案一:先更新数据库,再更新缓存
- 同时有请求A和请求B进行更新操作,那么会出现
- (1)线程A更新了数据库
- (2)线程B更新了数据库
- (3)线程B更新了缓存
- (4)线程A更新了缓存
- 这就出现请求A更新缓存应该比请求B更新缓存早才对,但是因为网络等原因,B却比A更早更新了缓存。这就导致了脏数据,因此不考虑。
- 同时有请求A和请求B进行更新操作,那么会出现
- 方案二:先删除缓存,在更新数据库:A有数据发生了变更,先删除了缓存,然后准备要去修改数据库,此时还没修改,这时候一个请求过来B,去读缓存,发现缓存空了,去查询数据库,查到了修改前的旧数据,放到了缓存中 ,A之后数据变更的程序完成了数据库的修改。数据库和缓存中的数据不一样了;
- 解决:采用延时双删策略:但还是有可能再第二次删除缓存失败----方案三
- (1)先淘汰缓存
- (2)再写数据库(这两步和原来一样)
- (3)休眠1秒,再次淘汰缓存
- 解决:采用延时双删策略:但还是有可能再第二次删除缓存失败----方案三
- 方案三:先更新数据库,再删缓存:(1)缓存刚好失效(2)请求A查询数据库,得一个旧值(3)请求B将新值写入数据库(4)请求B删除缓存(5)请求A将查到的旧值写入缓存
- 这个方案发生不一致的前提是,(3)中B的写操作耗时比(2)中的读时间更短,才有可能出现(4)早于(5);
- 方案一:先更新数据库,再更新缓存
- 如何应对缓存穿透(直接对存储层操作,失去了缓存层的意义)与缓存雪崩的问题(缓存同一时间大面积的失效,所以,后面的请求都会落到数据库上,造成数据库短时间内承受大量请求而崩掉。)?
- 缓存穿透:查询一个数据库中不存在的数据,比如商品详情,查询一个不存在的ID,每次都会访问DB,如果有人恶意破坏,很可能直接对DB造成过大地压力。
- 解决方案:
- 1.当通过某一个key去查询数据的时候,如果对应在数据库中的数据都不存在,我们将此key对应的value设置为一个默认的值,比如“NULL”,并设置一个缓存的失效时间,这时在缓存失效之前,所有通过此key的访问都被缓存挡住了。后面如果此key对应的数据在DB中存在时,缓存失效之后,通过此key再去访问数据,就能拿到新的value了。
- 2.常见的则是采用布隆过滤器(可以用很小的内存保留很多的数据),将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被 这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。(布隆过滤器:实际上是一个很长的二进制向量和一系列随机映射函数。布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和删除困难。)
- 解决方案:
- 缓存雪崩:缓存同一时间大面积的失效,所以,后面的请求都会落到数据库上,造成数据库短时间内承受大量请求而崩掉。
- 解决方案:
- 1.将系统中key的缓存失效时间均匀地错开,防止统一时间点有大量的key对应的缓存失效;
- 2.重新设计缓存的使用方式,当我们通过key去查询数据时,首先查询缓存,如果此时缓存中查询不到,就通过分布式锁进行加锁,取得锁的进程查DB并设置缓存,然后解锁;其他进程如果发现有锁就等待,然后等解锁后返回缓存数据或者再次查询DB。
- 3.尽量保证整个 redis 集群的高可用性,发现机器宕机尽快补上
- 4.本地ehcache缓存 + hystrix限流&降级,避免MySQL崩掉
- 解决方案:
- 缓存穿透:查询一个数据库中不存在的数据,比如商品详情,查询一个不存在的ID,每次都会访问DB,如果有人恶意破坏,很可能直接对DB造成过大地压力。
- 如何解决Redis的并发竞争Key问题(同时有多个子系统去set一个key。这个时候要注意什么呢?)
- 第一种方案:分布式锁+时间戳
- 这种情况,主要是准备一个分布式锁,大家去抢锁,抢到锁就做set操作。
- 加锁的目的实际上就是把并行读写改成串行读写的方式,从而来避免资源竞争。
- 第二种方案:利用消息队列
- 在并发量过大的情况下,可以通过消息中间件进行处理,把并行读写进行串行化。
- 把Redis.set操作放在队列中使其串行化,必须的一个一个执行。
- 第一种方案:分布式锁+时间戳