Redis特性:
1:丰富的数据类型
2:进程内与跨进程
3:功能丰富:持久化机制,过期策略
4:支持多种变成语言
5:高可用,集群
Redis的数据类型:
String, Hash , Set, List, Zset, Hyperloglog Geo, Streams
Stirng数据类型:
每个键值对都会有一个dictEntry,里面指向了key和value的指针,next指向下一个dictEntry
key是字符串,存在在自定义的SDS中,value存储在redisObject中,五种常用数据类型都是通过redisObject来存储
字符串内部有三种编码:
1:int 存储8个字节的长整型
2:embstr 代表embstr格式的SDS,存储小于44个字节的字符串
3:raw,存储大于44个字节的字符串,最大存储512M
Redis使用SDS实现字符串原因:
C语言本身没有字符串类型,只能有字符数组char[]实现
1:使用字符数组必须先给目标变量分配足够的空间,否则可能会溢出
2:如果要获取字符长度,必须比那里字符数组,时间复杂度O(n)
3:C字符串长度的变更会对字符数组做内存重分配
4:通过从字符串开始到结尾碰到的第一个\0来标记字符串结束,不能保存图片,视频,压缩文件等二进制内容
SDS特点:
1:不用担心内存溢出问题,如果需要会对SDS扩容
2:获取字符串长度时间复杂度为O(1),有len属性
3:通过空间预分配和惰性空间释放,防止多次重分配内存
4:判断是否结束标志是len属性
embstr和raw的区别:
embstr的使用只分配了一次内存空间,raw分配了两次
embstr实现为只读
int数据不再是整数,或者大小超过了long范围自动转换成embstr,对于embstr的修改都先会转成raw,再进行修改,转换过程不可逆
使用场景:
热点数据缓存,对象缓存,全页缓存,可以提升热点数据的访问速度
数据共享分布式:Redis是分布式的独立服务,可以在多个应用之间共享,如分布式Session
分布式锁:setnx方法,加上过期时间
全局ID:Int类型,Incrby,利用其原子性
计数器:int类型,incr方法,文章的阅读量,微博的点赞数,允许一定的延迟,先写入Redis在定时同步到数据库
限流:Int类型,Incr方法,访问者的IP作为key,访问一次增加一次计数
位统计:Bitcount 非常节省空间,可以用来做大数据量的统计,如在线用户统计,留存用户统计
Hash(哈希):
Hash和String的主要区别
1:Hash将所有相关的值聚集到一个key中,节省内存空间
2:只使用一个key,减少key冲突
3:当需要批量获取值的时候,只需要使用一个命令,减少内存/IO/CPU的消耗
Hash不适合场景:
1:Field不能单独设置过期时间
2:没有bit操作
3:需要考虑数据量分布的问题
底层结构:
应用场景:
String可以做的事情, Hash都可以做
存储对象类型的数据:比如对象或者一张表的数据
购物车:key:用户id field:商品id value:商品数量 +1:hincr -1:hdecr 删除:hdel 全选:hgetall 商品数:hlen
List列表:
底层使用quicklist存储,quicklist存储了一个双向链表,每个节点都是一个ziplist
应用场景:
用户消息时间线timeline:List是有序的,可以用来做用户时间线
消息队列:队列-->先进先出 栈--->先进后出
Set集合:
存储类型:String类型的无序集合,最大存储2^32-1个
存储原理:Redis使用intset或者hashtable存储set,如果元素都是整数类型,使用inset存储,如果不是整数类型使用hashtable(数组+链表存储),元素超过512个也会用hashtable存储
应用场景:
抽奖
点赞,签到,打卡
商品标签
商品筛选
用户关注,推荐模型
Zset有序集合:
存储类型:
存储原理:
元素数量小于128个,所有member的长度都小于64字节使用ziplist编码,在ziplist内部按照score排序递增来存储
超过阈值欧虎使用skiplist+dict存储
Skiplist跳表:
应用场景:
排行榜
数据结构总结:
编码转换总结:
高级特性:
发布订阅模式
subscribe channel-1 channel-2 channel-3 订阅频道
publish channel-1 33333 发布消息
unsubscribe channel-1 取消订阅
订阅频道支持通配符
Redis事务:
特点:
1:按进入队列的顺序执行
2:不会受到其他客户端的请求的影响
四个命令:multi(开启事务) exec(执行事务) discard(取消事务) wathc(监视)
Redis事务不能嵌套,多个multi命令效果一样,开启事务后,客户端可以继续向服务端发送任意多条命令,这些命令不会被立即执行,而是放入一个队列中,当exec命令时候,队列中命令才被执行
Redis中的Watch命令可以提供CAS乐观锁行为,也就是多个线程更新变量时候,会和原值做比较,只有它没有被其他线程修改的情况下才更新成新值,用watch监视一个或者多个值,如果至少有一个key被修改了,则事务取消
事务遇到错误:
1:在执行exec之前发生错误,包括语法错误,编译器错误等待,这种情况下事务会被拒绝执行,队列中所有命令无法执行
2:在执行exec之后发生错误,比如String使用了Hash命令,这时候只有错误的命令没有被执行
Lua脚本:
对IP进行限流
lua缓存:执行script load命令时候计算脚本的SHA1摘要并记录在脚本缓存中
脚本超时:Redis的指令执行本身是单线程的,这个线程还要执行lua脚本,如果lua脚本执行超时或者陷入死循环,Redis提供了
lua-time-limit参数限制脚本最长运行时间,默认5s,当脚本超过这一个限制后,redis将开始接受其他命令但不会执 行(确保脚本的原子性,此时脚本并没有被终止),而是返回一个BUSY错误,Redis可以提供一个script kill命令来中 止脚本,如果当前lua脚本对redis数据进行了修改,那么通过script kill命令是不能终止脚本的,因为要保证脚本运行 的原子性,如果脚本执行了一部分终止,那么就违背了脚本原子性要求,最终要保证脚本要么执行,要么都不执行
遇到这种情况,只能通过shutdown nosave和shutdown来终止
Redis为什么这么快?
1:纯内存结构
2:单线程
3:多路复用
多路复用:
传统的阻塞IO模型
如果当前FD(文件描述符)不可读,系统就不会对其他操作做出响应,阻塞当前线程
I/O多路复用:
多路:多个TCP连接(Sokcet或者Channel)
复用:复用一个或多个线程
不再由应用程序自己监视连接,而是由内核替应用程序监视文件描述符
内存回收:
1:key过期
2:内存使用达到上限
过期策略:
1:定时过期(主动淘汰)
每个设置过期时间的key创建一个定时器,到过期时间就立即清除,对内存友好,但是会使用大量CPU,影响响应事件和吞吐量
2:惰性过期(被动淘汰)
只有当访问一个key时候才会判断key是否过期,过期则清除,可以最大化的节省CPU资源,但是对内存不友好,极端情况下出现大量过期key没有再次被访问从而不会被清除
3:定期过期
每隔一段时间会扫描一定数量的数据库的expires字典中一定数量的key,并清除其中已经过期的key,是前两者的折中方案
Redis中同时使用了惰性过期和定期过期
如果key都不过期,Redis内存满了怎么办?
淘汰策略
当内存使用达到最大极限时候,需要使用淘汰算法来决定清理掉哪些数据,以保证新数据的存入
LRU:最近最少使用,判断最近被使用的时间,目前最远的数据优先被淘汰
LFU:最不常用
Random:随机删除
建议使用volatile-lru,在保证正常服务的情况下,优先删除最近最少使用的key
如果使用传统的LRU算法需要额外的数据结构存储,消耗内存
Redis LRU对传统的LRU算法进行改进,通过随机采样来调整算法的精度,根据配置的采样值(默认5个)随机从数据库中选择m个key,淘汰热度最低的key对应的缓存数据,m值越大,越能精确查找到待淘汰的缓存数据
热度最低的数据?
Redis中所有对象redisObject中都有一个lru字段,使用低24位来记录对象的热度,在被访问时候更新lru的值,采用全局变量的值,该全局变量的值是由Redis中的定时函数每100毫秒生成的,不采用当前时间戳是减少调用系统函数time的次数,提高效率,当lru的值和全局变量差值越大,该对象热度越低,超过24bit能表示的最大时间后它会从头开始计算,这种情况下就是两个相加来判断热度
LFU基于访问频率的淘汰机制:
Redis对象redisObject中的lru_bits中24位用作LFU时候,其被分为两个部分:
1:高16位用来记录访问时间
2:低8位用来记录访问频率
对象被读写时候,lfu的值会被更新,没有被访问时候,通过衰减因子来控制,N分钟没被访问就减少N
持久化机制:
RDB(Redis DataBase)快照和AOF(Append Only File)
RDB:
Redis默认的持久化方案,当满足一定条件时候,会将当前内存中的数据写入磁盘,生成快照文件dump.rdb,重启通过加载dump.rdb文件恢复数据
RDB触发:
1:自动触发
a:配置规则触发
b:shutdown触发,保证服务器正常关闭
c:flushall
2:手动触发
如果我们需要重启服务或者迁移数据,这个时候需要手动触发RDB快照保存
a:save
save在生成快照时候会阻塞当前Redis服务器,Redis不能处理其他命令,数据量大的时候会造成长时间阻塞
b:bgsave
Redis在后台异步进行快照操作,快照同时能响应客户端请求
优势:
1:RDB是一个非常紧凑的文件,保存了redis在某个时间点上的数据集,适合备份和灾难恢复
2:生成RDB文件时候,redis主线程会fork()一个子进程来处理所有保存工作,主进程不需要进行任何磁盘IO操作
3:RDB在恢复大数据集时候的速度比AOF的恢复速度快
劣势:
1:没办法做到秒级持久化/实时持久化,bgsave每次运行都要执行fork操作创建子进程,频繁执行成本过高
2:在一定间隔时间做一次备份,如果redis以外down掉,会丢失最后一次快照后的所有数据
AOF:
Redis默认不开启,AOF采用日志形式记录每个操作,并追加到文件中,Redis重启时候会根据日志文件的内容把写指令从前 到后执行一次以完成数据恢复
由于操作系统缓存,AOF数据并没有真正写入硬盘,而是进入系统的硬盘缓存,Redis默认是everysec,表示每秒执行一次 fsync,可能会导致丢失这1s数据
no:表示不执行fsync,由操作系统保证数据同步到磁盘,速度最开,但是不太安全
always:表示每次都写入执行fsync,以保证数据同步到磁盘,效率很低
当AOF文件大小超过了所设定的阈值时候,Redis会启动AOF文件内容的压缩,值保留可以恢复数据的最小指令集
重写触发机制:
AOF重写过程中,AOF文件被修改了
优点:
AOF持久化方法提供了多种同步频率,即使使用默认的也是丢失1s数据
缺点:
1:对于具有相同数据的Redis,AOF文件通常比RDF文件体积更大(RDB存储数据快照)
2:高并发情况下RDB比AOF具有更好的性能
可以允许一小段内数据丢失,可以使用RDB,一把两者同时使用
Redis集群
需要集群的原因:
1:在某些并发量非常高的情况下,性能还是会受到影响
2:扩展,Redis数据存放在内存中,如果数据量大,很容易受到硬件限制
3:可用性,单个Redis宕机其他机器还能运行
Redis主从复制:
原理
1:连接阶段
a:slave node启动时候,会在自己本地保存master node的信息,包括master node的host和ip
b:slave node 内部有个定时任务,每隔1s检查是否有新的master node需要连接,如果有则和master node建立socket 网络连接,连接成功,从节点建立事件处理器来处理任务,当从从节点变成主节点一个客户端后,会给主节点发送 ping请求
2:数据同步阶段
c:master node第一次执行全量复制,通bgsave在本地生成一份RDB快照,在RDB生成期间,master将接受到的命令 缓存到内存中,将RDB快照文件发送给slave node,slave node 首先清除自己的数据,然后使用RDB文件加载数 据
3:命令传播阶段
d:master node持续将写命令异步复制给slave node,如果从节点有一段时间断开了和主节点的连接,master通过 master_repl_offset记录了偏移量
主从复制不足之处:
1:RDB文件过大情况下,非常耗时
2:在一主一从或者一主多从情况下,如果主服务器宕机,对外提供的服务将不可用,需要手动切换
Sentinel(哨兵):
Sentinel本身没有主从之分,只有Redis服务节点有主从之分
1:服务下线
Sentinel默认以每秒1次的频率向Redis服务节点发送PING命令,如果在donw-after-milliseconds内都没有收到有效回复,Sentinel会将该服务器标记为下线,此时Sentinel节点会继续询问其他的Sentinel节点,确认这个节点是否下线,如果多数Sentinel节点都认为master下线,master才真正确认被下线,此时需要重新选举
2:故障转移
在Sentinel集群选择一个Leader,由Leader完成故障转移流程,通过Raft算法实现Sentinel选举
Raft核心思想:先到先得,少数服从多数 http://thesecretlivesofdata.com/raft/
a:选举slave 节点成为主节点
选出Sentinel Leader之后,由Sentinel Leader向某个节点发送slaveof no one命令,让他成为独立节点
向其他节点发送slave of x.x.x.xxx(本机服务),让它们成为这个节点的子节点,故障转移完成
b:有多个slave node,如何选择一个
有四个因素影响选举的结果,断开连接时长,优先级排序,复制数量,进程id
如果与哨兵连接断开的比较久,超过了某个阈值,直接失去选举权,如果拥有选举权,则判断优先级(replica- priority),数值越小优先级越高,如果优先级相同,则判断谁从master中复制的数据最多(复制偏移量最大),选择最 多的,如果复制数量相同则选择进程id最小的那个
Sentinel功能总结:
1:监控,Sentinel会不断检查主服务器和从服务器是否正常运行
2:通知,如果某一个被监控的实例出现问题,Sentinel可以通过API发出通知
3:自动故障转移,如果主服务器出现故障,Sentinel可以通过故障转移将从服务器升级为主服务器,并发出通知
4:配置管理,客户端连接到Sentinel,获取当前Redis主服务器的地址
不足:
主从切换的过程会丢失数据,因为只有一个master
只能单点写,没有解决水平扩容问题
如果数据量很大,我们需要多个master-slave的group,将数据分布到不同的group中
Redis分布式方案:
1:客户端Sharding ShardedJedis
2:代理proxy Twemproxy , Codis, Redis Cluster
Redis Cluster:用于解决分布式的需求,去中心化
一致性Hash算法:所有哈希值空间组织成一个虚拟的圆环(哈希环),整个空间按顺时针方向组织,
解决了动态增减节点时,所有数据都需要重新分布的问题,它只会影响到下一个相邻的节点,对其他节点没影响
Redis分区既没有使用哈希取模,也没有用一致性哈希,而是使用虚拟槽实现
每个master节点维护一个16384位的位序列,对象分布到Redis节点上时候,对Key用CRC16算法计算再%16348,得到一个slot的值,数据落到负责这个slot的Redis节点上,key和slot的关系永远不会变,变的只是slot和Redis节点的关系
客户端连接到的服务器上没有访问的节点,使用客户端重定向进行操作
Pipeline:
Redis使用的是客户端/服务器(C/S)模型和请求/响应协议的TCP服务器,通常情况下遵循以下请求:
1: 客户端向服务端发送一个查询请求,并监听Socket返回,通常是以阻塞模式,等待服务端响应
2:服务端处理命令,并将结果返回给客户端
Redis客户端和Redis服务器之间使用TCP协议进行连接,一个客户端可以通过一个socket连接发起多个请求命令,每个请求命令发出后client通常会阻塞并等待redis服务器处理,redis处理完请求后会将结果通过响应报文返回给client,因此当执行多条命令时候都需要等待上一条命令执行完毕才能执行
Pipeline通过一个队列将所有的命令缓存起来,然后将多个命令在一次连接中发送给服务器
Lettuce:
SpringBoot 2.X默认的客户端,直接调用Spring的RedisTemplate操作
Redisson
在Redis的基础上实现的java内存数据网格,提供分布式和可扩展的java数据结构
基于Netty实现,采用非阻塞IO,性能高
支持异步请求,支持连接池,pipeline,LUA Scripting等
不支持事务,官方建议使用LUA Scripting代替事务
数据一致性:
一旦被缓存的数据发生变化的时候,我们既要操作数据库的数据也要操作Redis的数据,此时有两种选择
1:先操作Redis的数据,然后操作数据库的数据
2:先操作数据库的数据,然后操作Redis数据
Redis更新数据时候是删除还是更新
更新缓存之前是不是要经过其他表的查询,接口调用计算后才能得到最新的数据,而不是直接从数据库拿到的值,如果是的话建议直接删除缓存,这种方案更简单,而且避免了数据库的数据和缓存不一致情况
a:先更新数据库,在删除缓存
异常情况下:
1:更新数据库失败,程序捕获异常,不会到下一步,所以数据不会出现不一致
2:更新数据库成功,删除缓存失败,数据库是新数据,缓存是旧数据,发生了不一致情况
解决方案:
可以提供重试机制,例如删除缓存失败,捕获异常,将需要删除的key发送到消息队列,然后不断尝试删除这个key,这种方式会对业务代码造成入侵
异步更新缓存,例如更新数据库时候会往binlog写入日志,所以可以通过一个服务来监听binlog(canal),然后在客户端完成删除key操作,如果删除失败的话,再发送到消息队列
b:先删除缓存,再更新数据库
异常情况:
1:删除缓存失败,程序捕获异常,不会到下一步,所以数据不会出现不一致情况
2:删除缓存成功,更新数据库失败,因为以数据库的数据为准,所以不存在数据不一致情况
这种情况在并发操作下有问题
1)线程A需要更新数据,首先删除了Redis缓存
2)线程B查询数据,发现缓存不存在,到数据库查询旧值,写入Redis,返回
3)线程A更新了数据库
这个时候Redis是旧的值,数据库是新的值,发生了数据不一致情况
可以采用延时双删的策略,在写入数据之后,再删除一次缓存
缓存雪崩:
大量Redis热点数据同时过期(失效),因为设置了相同的过期时间,刚好这个时候Redis请求的并发量很大,会导致所有请求落到数据库
解决方案:
1) 加互斥锁或者使用队列,针对同一个key只允许一个线程到数据库查询
2)缓存定时预先更新,避免同时失效
3)增加随机数,使key在不同的时间过期
4)缓存永不过期
缓存穿透:
查询Redis和数据库中不存在额数据
解决方案:
1):缓存空数据
2):缓存特殊字符串
在应用里面拿到这个特殊字符时候就知道数据库里面没有值了,没必要再去查询数据库了
3):布隆过滤器
缓存击穿:
大量用户访问热点数据,当该热点数据失效时候,请求都去查询数据库
解决方案:
1)热点数据不过期
2)增加互斥锁