Redis
设计
单线程
-
为什么能用单线程
基于reactor模式的I/O多路复用技术,通过socket对接多个客户端,使用IO多路复用程序,将请求全部放到一个队列里,使用文件时间分派器,交给不同的处理器进行处理。
-
多线程的好处
避免线程切换带来的开销,可维护性高。
-
多线程的弊端
IO操作占用大部分CPU时间,举例:大键值对删除会阻塞
多线程
-
处理方式
-
多线程只用来处理网络数据的读写和协议解析,命令执行仍然使用单线程。
-
默认不开启。
-
新版本6.0特性
-
多线程更完善
-
支持ACL权限控制列表
-
支持客户端缓存
基本用法
一、redis的基本概念及使用
基本数据类型
-
String
-
描述:最常用的类型,一般存数据就是用这个,为key-value格式
-
用途:
-
分布式锁
一般是使用setnx,如果设置成功,返回1,否则返回0
-
共享session
-
计数器
通过incr命令加1减1,比如微博评论数,点赞数,分享数,销售量等等
-
-
命令:get/set/del/incr/decr/incrby/decrby/exist/mget/mset/expire
-
-
hash
-
描述:里边有两个键,一个是redis的键,一个是hash的键,格式类似于 key1 key2 value
-
用途:
- 管理bean信息,存一个对象,对象名叫做key1,key2就存属性名,value存值
-
命令:hset/hget/hgetall/hdel,注,这种hset,hget,hdel需要指定两个key的名字,如果想要删除整个哈希,直接调用del即可
-
-
list
-
描述:类似于LinkList,可以了理解为String类型的双向链表,有序,可重复,插入极快,获取速度慢。
-
用途:
-
分布式栈:先进后出这种,
-
分布式队列/阻塞队列(消息队列,通过命令brpop这种实现,brpop是阻塞式弹出数据,可以设置个等待时间,但最好使用mq)
-
可用于存储一些热点数据列表,例如粉丝列表,文章评论列表,消息排行等
-
有限集合,可以用于定时计算的排行榜,如果是实时计算,最好使用zset这种类型
-
-
命令:通过 lpop,lpush,rpop,rpush 这几种命令的组合,可以实现栈,队列这种功能。 lrange,ltrim,lindex,brpop这种阻塞的
-
-
set
-
描述:元素不重复,底层通过hashtable实现,添加删除查找的时间复杂度都是o(1),hashtable会随着元素的增多减少,动态调整hashtable的大小,但在调整的时候,会出现进程阻塞的情况。可以进行交集,并集,差集操作。
-
用途:
-
计算两个集合中的共同好友,共同喜好,全部喜好,独有的喜好等等,适合集合操作。
-
黑名单,白名单,判断是否在其中
-
-
命令:sadd, smember,sismember,scard,spop,srandmember(随机展示)
-
注:如果是集群,操作的多个key,必须在同一个slot中
-
-
zset
-
描述:key score value 这种结构的
-
用途:
实时排行榜
-
命令:zadd/zrange/zscore/zrem
-
key的优化:
-
避免使用大key
-
设置合理过期时间
-
选择合适的类型
-
避免使用del命令,除非String这个类型,否则,最好使用渐进命令删除
key的过期删除策略
-
定时删除:每隔多长时间删除一次
-
惰性删除:遇到这个键再判断是否到期
-
定期删除:每秒扫10次,一次扫20个Key,如果国企比例超过25%,继续扫描别的key
Redis满了,内存淘汰机制
如果redis达到上限(maxmemory参数配置的上限,并非真正满了),会有几种算法进行key的删除。
-
volatile-lru/lfu/ttl/random:在即将过期的key中选择
-
allkeys-lru/lfu/random no-enviction:在所有的Key中进行选择
二、redis用作缓存
只读缓存策略
如果要使用缓存,先读redis,之后再读数据库,如果redis中有,就从redis中拿,如果redis中没有,查数据库,同时将数据保存到redis中。数据发生了更新,就把redis中的数据删掉。
读写策略策略
读写策略有两种,一种是同步写回,一种是异步写回,同步写回需要往redis中写过之后,将数据也同步到数据库中,这样效率比较慢。异步写回的话,在redis中删改之后,使用异步线程或者消息队列对数据库再进行修改。但是有数据不同步的风险。
热key问题
-
描述:如果突然有几十万以上的请求访问redis上的某个key,这个key就叫做热key,容易造成网卡崩了,redis服务器崩了,继而引起Mysql也崩了,俗称缓存击穿。
-
发现热key:
-
凭借开发经验判断
-
使用aop等方式进行统计
-
在代理层进行统计
-
使用redis本身的命令进行统计
-
使用第三方工具进行统计
-
使用大数据的方式,利用flink搭建流式系统,抓包监听redis端口数据,将数据放到kafka中,消费数据,拿到热key
-
-
解决方案
-
使用JVM维护的二级缓存,提前准备个ehcache或者hashmap,将热key放到里边,如果请求过来,命中了热key,就将当前请求转发到不同的redis服务器上。
-
集群备份热Key,先将热key加上随机数,放到redis集群中,之后请求过来,热key使用相同的算法,也加上随机数,这样就能指定访问某个redis节点了。
-
保持缓存一致性
-
缓存弊端:
-
缓存不可信:存的数据不见得是最新的,数据不一定是同步的;
-
缓存不可靠:缓存服务不一定随时保持可用的状态。
-
-
缓存一致性的处理策略
-
cacheAside策略,即只读模式。读:redis有就从redis取,没有的话就从数据库中取,然后再加载到缓存中。更新:先更新数据库,然后删除缓存。核心就是,一切以后端数据库为准。
注:cacheAside只读策略,在更新的时候会有三种方案:
- 双写模式:先更新缓存再更新数据库,或者先更新数据库再更新缓存。
- 失效模式:先淘汰缓存再更新数据库,或者先更新数据库再淘汰缓存。
- 使用第三方框架canal操作数据库日志。
双写模式,在更新缓存和数据库的时候,数据处理起来比较复杂,所以一般采用失效模式。
比较推荐的是,先更新数据库,再删除缓存,但是这种方案有一定的风险性,即,如果更新了数据库,但是该删除缓存的时候没有删掉,还是会造成数据不一致。
为了解决这种问题,引入了一个方案,延迟双删,先删除缓存,再读数据库,等待一段时间,再删一次缓存。主要是考虑到,第一次删除缓存,数据库写入操作还没有完成的时候,另一个线程读取了数据库,同时把数据拉到缓存中,现在缓存中又有旧数据了。
上边说的延迟双删其实还是有点问题,第二次删除缓存的时候,如果没有删掉,那么redis中的数据还是旧的,所以就引入了binlog操作,直接操作数据库。使用canal框架,订阅数据库的Binlog文件,将订阅后的数据,压入消息队列,当数据库数据发生变动之后,读取消息,对redis执行删除操作。
-
read/write through策略,应用层只需要操作缓存,剩下的交给缓存层去实现。应用层读写只需要从缓存读,写数据只需要写到缓存中。
-
write back策略,
-
读:从redis中读,redis没有就从数据库中读,然后给到redis一份。
-
写:写到redis中,当redis满了,一次将所有数据写到后台数据库中。这种设计方式存在问题,当redis还没满,系统挂了,那么redis中的数据就会丢失。
-
-
实现缓存一致性可能会遇到的问题
-
缓存穿透
-
描述:非正常的url查询数据,先查redis查不到,再查数据库也查不到
-
可能造成的问题:少了还好说,如果这种非正常请求多了,会造成计算资源的极大浪费,如果再有黑客弄清楚你的序列号生成规律,能把redis干崩掉。
-
解决方案:
-
将null值也放到redis中。遇到查询redis和数据库都查不到的数据,直接返回空,存到redis里边,下次就直接从缓存中拿到null;
-
白名单方式:利用布隆过滤器,或者手动维护一个bitmaps,只允许查在范围内的,超过这个范围直接拒掉。
-
运维方式:和运维进行配合,超过访问次数的ip或者什么直接进黑名单
-
用户鉴权,防止身份不明的请求
-
热点数据进行限流
-
-
-
缓存击穿
-
描述:针对某个热点key进行查询,该key承受着高并发的请求,当key失效的瞬间,所有流量全部进到数据库,数据库崩了。
-
解决方案:
-
提前延时:热点数据进行预热,提前存到redis中,同时对这些key的过期时间进行延迟
-
动态延时:通过业务设计,将这些热点key的过期时间动态延长
-
禁止高并发查询:设置分布式锁,查询热key的时候,先看数据库中有没有,如果没有,给查询数据库的语句上锁,此时不允许别的线程读取数据库,等到从数据库提取到redis之后,解锁,保证redis中重新有了这个key的缓存。
-
-
-
缓存雪崩
-
描述:大量数据的key同时到期,查不同的key,导致数据库流量也会激增,击垮。
-
解决方案:
-
多级缓存架构,nginx缓存+redis缓存+jvm中的二级缓存(ehcache实现)
-
使用锁或者队列,防止高并发同时查数据库
-
设置提前量去检查,如果即将过期10分钟内进行了访问,就延长时间
-
将缓存过期的时间分开,避免添加key的时候设置同一个过期时间
-
-
三、redis用作分布式锁
分布式锁简介
目前主流的分布式锁,有几种解决方案
-
基于数据库乐观锁的实现
-
基于缓存redis等的实现
-
基于Zookeeper的实现
Redis分布式锁需要满足的条件
-
互斥性:同一时刻,只有一个redis客户端可以持有锁
-
安全性:当竞争者在持有锁的时候崩溃,未能主动解锁,需要保证其持有的锁能够被正常释放,后续的竞争者也能加锁成功。
-
可靠性:只要大部分redis节点正常运行,那么就必须保证redis客户端能够加锁解锁成功。
-
对称性:加锁解锁必须是同一个客户端,A客户端不能把B客户端的锁给释放了。
单机redis模式下,分布式锁的具体实现
-
描述:能够实现分布式锁的最主要原因就是setnx命令,删除锁使用的是del命令。
-
可能会遇到的问题:
-
手动释放锁时del命令执行失败(非原子性)
-
解决方案:
-
使用LUA脚本进行控制
if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end
即,如果程序业务执行完毕,需要手动释放锁时,使用redisTemplate,excute这个script就行了,在方法中传入键,和值。注:键作为业务名,值用来区分到底是业务中的哪个锁,也可以只用业务名。
-
-
-
异常原因导致锁未手动释放:
-
解决方案:超时机制,设置超时时间
由于上述分布式锁需要满足的条件,安全性的限制,需要设置一个超时时间,
-
setnx + expire
一般是使用expire设置超时时间,但是setnx与expire是两个不同的命令,如果expire命令没执行成功,那么会出现被锁定的key永不失效,有可能会导致redis的内存空间不足。
-
set
命令
set [key] [value] nx ex [seconds]
,这个second一般设置为平均执行时间的35倍,如果被锁住的程序执行时间为200ms,一般设置为3*2005*200ms,超过这个时间,自动释放锁。
-
-
-
超时释放,释放了别人的锁:
-
描述:如果不设置身份标记,假如有这么一个A程序,执行完主逻辑之后会执行解锁的步骤,A卡死了,锁自动超时释放,此时竞争者B进来,执行B的逻辑,A又恢复了,接着往下执行,执行完A的主逻辑,A释放了锁,此时释放的就是B的锁,而B还在执行任务。
-
解决方案:给锁添加身份标记
-
可以在某个业务里进行对比,key设置为一个定值,比如说“lock”,value设置为一个具体的内容,如果通过key拿到了存在redis中的value,跟之前设置的值进行对比,如果相同说明是同一个锁。如果不同,就不进行解锁。
-
直接通过业务进行区分,直接将key设置为某个业务id,加锁就直接在redis中设置这个key,这个业务的别的进程进来,先判断,判断不通过了就等待。
注:两者也可以都用,在同一个业务里确认是不是同一把锁。
-
-
-
线程业务未执行完,锁已经到期自动解锁了
-
解决方案:
- 使用redission框架启动看门狗线程,看门狗每隔十秒检查当前线程是否持有锁,如果仍然持有锁,就一直续期,当然这个续期是有时间限制的,防止卡死的线程一直占用这个锁,可以进行设置锁的续期机制。而且这个redission实现了可重入锁,每一个使用这种锁的,会在锁的次数上加1,直到锁降为0才会解锁。使用的是hash这个数据结构,次数是写到了value中,如果hash不存在,或者hash中的key也不存在,则返回nil,否则就hincrby1或者hdecrby1。
-
-
集群redis,分布式锁的实现方案
-
问题描述:线程1在redis的master节点拿到了锁,master还没有同步给slave,master就挂了,slave升级为master,线程2此时就能获取到锁了
-
解决方案:RedLock机制
- 方案描述:启动多台不适用任何架构的redis实例,不采用主从,不采用哨兵集群等机制,完全是为了容错。当有超过一半的节点都能获得锁,且在锁的自动失效时间范围内,才算获取锁成功。redission这个框架已经实现了分布式redis的锁的redLock,就不用考虑这个东西了。
四、Redis实现session共享
高可用
高可用实现方式速览
为了维护redis的高可用,有几种不同的实现方式
-
持久化
-
主从架构
-
哨兵机制
-
集群架构
Redis 持久化
持久化分为两种:分别是
-
rdb
-
aof
RDB
rdb是保存所有的key-value键值对,保存的是数据。
RDB快照有两种方式,
-
SAVE
-
BGSAVE
-
BGSAVE是redis自动触发的
原理:redis会fock出来一个线程(避免主进程被阻塞),异步生成rdb文件,拷贝原来的rdb文件到新的rdb文件里边,之后将原来的rdb文件删掉。
注意点:子进程和主进程共享同一片内存区域,如果,要写入到新的rdb文件的数据集,正在被主进程正在修改,会使用一种叫“写时复制”的方案进行处理。即:对那一块物理内存进行复制,主进程对复制出的内容进行修改,之前的内容仍然由子进程写入到rdb文件中。
注:rdb方案可以使用lzf算法进行压缩,所以rdb持久化方式的占用空间比aof更小。
-
AOF
aof记录的是所有的命令,一般来说根据记录的快慢,分为三种方式,即下面的刷盘策略:
刷盘策略
-
同步写入,redis接收到什么内容,就往硬盘中刷入什么内容
-
每秒写入,一秒写一次
-
依赖操作系统写入,完全交给操作系统了
子进程操作
因为AOF这种追加方案,没有压缩机制,会导致文件越来越大,所以会重启一个进程,对AOF文件整理,将之前的命令全部删掉,把数据集整理成AOF文件命令,命令是bgrewriteaof,
具体的操作步骤是,子进程读取数据库中的所有的键值对,将每个键值对都用一条命令写入到新的AOF文件中,全部记录完成之后,用新的AOF文件替换旧的。
注:这个进程用的内存跟上边RDB子进程一样,都是使用的主进程中的物理内存。同样,如果两个进程同时对共享内存发起写操作,也会进行缺页中断,写时复制,将那一块内存进行复制。
主从架构
为了解决单机redis容易发生单点故障的问题,及redis内存容量不够的问题,推出主从架构。
-
主从架构的实现原理
-
建立连接阶段:从机连接主机,从机从主机中拉取数据
-
从机从redis的配置文件中获取主机的ip及端口,执行replicaof命令发起连接请求
-
master主机返回相应信息
-
slave保存主机的ip及端口号,根据地址建立socket连接
-
从机依据ping-pong机制,持续向主机发送ping命令,master返回pong回应,若返回不正常则说明超时
-
建立连接后,进行权限校验,如果master中设置了密码,需要对slave进行权限校验
-
验证之后,从机向主机发送自己的监听端口,主机进行保存
-
-
数据同步阶段
-
全量复制
-
建立连接
-
主从服务器建立连接,slave执行replicaof命令,向主机发送psync命令,携带?和-1,表示此时还不知道主机的runId,且为第一次连接,
-
主库收到psync请求,返回fullresync相应,携带两个参数,主库的runId和主库目前的复制进度,offset.
-
从库保存主库的核心信息,同时保存runId和offset
-
-
将全部数据同步给slave
-
主从建立连接的时候,主机可能会同时进行bgsave操作,将文件持久化的rdb操作,同时将数据存到repl_backlog_buf复制积压缓冲区
-
将数据全量发送给slave
-
slave收到rdb文件后,清空自己的数据库,加载rdb文件
-
-
将发送rdb文件期间的命令从复制积压缓冲区拿出来,发送给从库,从库再进行执行。
-
-
增量复制(闪断情况用的多)
-
网络抖动,主从断开连接,恢复之后重新连接,从机发送psync,将自己的runId和slave_repl_offset传送给master主机
-
主机判断,如果runId与之前匹配,且slave_repl_offset偏移量在积压缓冲区范围内,但从机的偏移量与主机记录的master_repl_offset偏移量不一致,就返回continue命令,表示可以增量复制。如果runid不匹配,或者偏移量不在积压缓冲区(说明积压缓冲区已经被覆盖了),那么就进行全量复制。
-
-
-
命令传播阶段
第一次同步之后,主从之间维护一个长连接,不断将新增的写命令传递给从服务器。
-
哨兵机制
流程步骤
-
确认下线状态
- 每个sentinel会以1hz的频率,向其本身知道的master,slave节点,发送ping命令,探测是否存活;
- 如果master实例超过了配置选项指定的时间,就默认该实例已经主观下线了;
- 如果master被标记主观下线,所有监视这个master的sentinel,都要进行确认是否对于自己的sentinel,也是主观下线,频率也是1赫兹,如果Ping通了,那取消主观下线状态
- 如果足够数量的sentinel都确认这个master主观下线了,就确认客观下线,如果没有足够数量的确认,就取消确认客观下线,仍然保持主观下线状态
-
确认拓扑结构
- 每个sentinel会以10s一次的频率,向所有的redis节点发送info命令,获取当前的redis拓扑状态,这个是无论下没下线,都要进行发送的
- 如果确认了master客观下线了,sentinel就会向该master的所有slave发送info命令,改成1s一次,确认这个拓扑结构
- 哨兵根据一定的规则,重新推选出一个哨兵leader,负责故障转移
- 这个哨兵leader,根据规则确定Master,构建全新的主从结构
哨兵机制的架构分析
- 基础就是“一主二从三哨兵”:一个主节点,两个slave,这三个节点都有自己的哨兵进行监控
哨兵机制的宕机情况及对应解决方案
-
从机宕机:恢复后自动加入到之前的主从架构中了,差的那部分数据采用增量复制即可。
-
主机宕机:主机宕机了,根据选举模式,选出来三个哨兵哪个哨兵进行善后工作,选出来之后,这个哨兵进行主导,确认新的master节点,确认新的节点后,将之前的主从的拓扑结构断开,重新生成新的主从拓扑,将之前的主节点设置成从节点,等这个节点上线之后,执行slaveof命令,将其设置成slave。
哨兵的异常通知方式
当某个哨兵发现主节点下线了,会发送一个告警,但这个告警并不是给别的哨兵发的,而是有一个sentinel_hello_master频道,哨兵用的是发布/订阅模式接受别的哨兵的信息。这个哨兵发送的内容,也会被自己接到,有个run ID进行判断,如果是自己发的,就忽略。
哨兵选举Leader的方案
选举这个哨兵Leader的时候,一般是根据redis-sentinel的配置文件制定的哨兵优先级确定的,哨兵进行投票,但是这个投票是基于内部通信机制的,大多数情况下只要有候选者,都会超过半数的sentinel同意这个候选者作为Leader,只有在某种异常情况下,网络故障,哨兵配置出错,数据不一致的时候才会出现大多数哨兵不同意该候选人作为leader。
Redis的集群架构
redis5之后,可直接使用redis-cli命令行进行配置
-
引入原因:为了保证redis的高可用,在持久化机制,主从架构,哨兵机制仍然无法保证高可用的情况下,推出集群架构。
-
原理:
-
划分槽位:采用集群架构,会划分槽位,无论多少台redis服务器,一共只会划分16384个槽位,有多少个master节点,这些槽位就会被平均分成多少份。利用一致性哈希算法,将数据均分成不同的份数。
-
数据入槽:客户端存储key的时候,利用crc16算法,同时对16384进行取余操作,将key-value放到对应的hash槽中。也就是放到不同的redis服务器中。
-
-
弊端:
-
集群架构不允许针对在不同槽中的多个key进行批量操作,因为太影响性能,所以禁掉了。
-
不支持lua脚本
-
硬件投入高。
-
-
架构:多主多从。
-
消息通知算法:主节点之间通过gossip相互串通,当主节点挂了,slave节点顶上。gossip算法:某些节点随机将数据传递给周围节点,周围节点同样操作,最终达到一致。
-
客户端使用:客户端进行访问的时候,不指定连接的哪个master节点。根据缓存数据,如果key不在某个slot上,那么集群就会计算一份新的缓存进行重定位。
-
扩容:
-
可以无限扩展,但是最好服务器不要超过1000个
-
添加服务器,只需要将之前的节点的部分slot,转到新节点就行了,不需要停机操作。
-
-
集群容错:
-
节点失效:半数以上的master与该节点进行通信,超过配置设置的时间,那么就称为节点失效。
-
集群失效:如果某个节点的master失效,又没有slave,那么整个集群就失效,或者超过半数的master失效,也算集群失效。
-
-
集群脑裂
- 定义:某个节点的主节点因为网络或者别的什么原因导致失效,该redis主从架构的从节点向集群中的别的Master发送选举广播且被选为主节点之后,该redis出现两个主节点,网络恢复后,需要将其中的一个主节点变为从,所以会损失一部分数据