目录
4.3 如何保证Redis的高并发高可用(尽量少的数据丢失)
Redis
Redis就是一个基于C 语言编写的一个开源的单线程的高性能键值对数据库。可以用来做缓存,处理即时信息。
1,redis的数据类型及应用场景
类型 | 格式 | 应用场景 |
---|---|---|
string | key:value | 验证码 ,分布式session 登录,(计数器-incr-文章阅读数),缓存热点数据 |
hash | 大key 小key:value 小key:value | 购物车 cart:uid 大key用户id 小key是商品id和数量 |
list | key:集合 | 消息队列,保证顺序消费 |
set | key: 集合 (可以进行交集、并集、差集等集合运算) | 微信朋友圈点赞可以展示共同好友,抽奖 |
zset | key:score1-value1 score2-value2 | 每个元素都有一个排序权重,可以根据权重值进行排序)排行榜 |
位图 hyperloglog |
2,redis持久化机制
由于Redis存储在内存中,一旦系统重启、程序崩溃等情况出现,所有数据将会丢失。因此,为了保证数据的可靠性,需要进行持久
化,在我的了解中,redis提供了两种方式进行了持久化,分别是分别是分别是RDB快照和AOF日志,在redis4.0之后,Redis还提供了
RDB和AOF混合持久化方式,以保证数据的实时性和完整性。
-
RDB 会根据配置的规则进行定时快照备份,它的优点是快照数据以二进制存储,文件小,恢复速度快,但是也存在缺点,就是由于定时备份会有一定的时间间隔,会丢失一定的数据(15分钟 1个 5分钟10个 1分钟1w个),
-
AOF是过程命令形式,记录每个操作,数据相对完整(最快可以每秒同步一次), 弥补了RDB不能实时存储的缺点 ,但它也有一定的缺点: 它的数据体量大,效率上没有RDB好,存储格式上也更加复杂 , AOF文件可能会被篡改,AOF文件大、恢复速度比较慢
AOF重写就是将AOF文件进行重写,去除其中的数据冗余和历史操作,保留数据的更新和当前状态。
-
如果对数据非常敏感用AOF,如果追求大数据集的恢复速度选RDB
3,redis淘汰策略
3.1 Redis的过期key删除策略
reids的数据过期策略一共有两种,一种是主动的定期删除,另一种是被动的惰性删除;
惰性删除 客户端来查询一个key,redis会先判断该key是否过期,不过期直接返回。过期就删除,再返回一个null 定期删除
里面有两个非常重要的概念,一个是LRU,另外一个是LFU LRU的意思就是最近最少使用,用当前时间减去最后一次访问时间,这个值越大则淘汰优先级越高。 LFU的意思是最少频率使用。会统计每个key的访问频率,值越小淘汰优先级越高 我们在项目如果设置了大量不过期的key可以使用allkeys-lru,如果设置了大量过期时间的key,则可以使用volatile-lru,挑选最近最少使用的数据淘汰,把一些经常访问的key留在redis中
3.2 Redis的内存用完了会发生什么?
这个要看redis的数据淘汰策略是什么,如果是默认的配置,redis内存用完以后则直接报错。我们当时设置的 allkeys-lru 策略。把最近最常访问的数据留在缓存中。
4,Redis高可用
4.1 主从同步
因为单节点Redis的并发能力是有上限的,要进一步提高Redis的并发能力,可以搭建主从集群,实现读写分离。一般都是一主多从,主节点负责写数据,从节点负责读数据,主节点写入数据之后,需要把数据同步到从节点中 缺点:主从复制模式最大的问题是只有一个主节点,没有实现高可用,所以还需要哨兵机制来实现高可用
4.2 主从同步的原理
全量同步是指从节点第一次与主节点建立连接的时候使用全量同步,流程是这样的:
-
第一:从节点请求主节点同步数据,其中从节点会携带自己的replication id和offset偏移量。
-
第二:主节点判断是否是第一次请求,主要判断的依据就是,主节点与从节点是否是同一个replication id,如果不是,就说明是第一次同步,那主节点就会把自己的replication id和offset发送给从节点,让从节点与主节点的信息保持一致。
-
第三:在同时主节点会执行bgsave,生成rdb文件后,发送给从节点去执行,从节点先把自己的数据清空,然后执行主节点发送过来的rdb文件,这样就保持了一致
当然,如果在rdb生成执行期间,依然有请求到了主节点,而主节点会以命令的方式记录到缓冲区,缓冲区是一个日志文件,最后把这个日志文件发送给从节点,这样就能保证主节点与从节点完全一致了,后期再同步数据的时候,都是依赖于这个日志文件,这个就是全量同步
增量同步指的是,当从节点服务重启之后,数据就不一致了,所以这个时候,从节点会请求主节点同步数据,主节点还是判断不是第一次请求,不是第一次就获取从节点的offset值,然后主节点从命令日志中获取offset值之后的数据,发送给从节点进行数据同步
4.3 如何保证Redis的高并发高可用(尽量少的数据丢失)
首先可以搭建主从集群,再加上使用redis中的哨兵模式,哨兵模式可以实现主从集群的自动故障恢复,里面就包含了对主从服务的监控、自动故障恢复、通知; 如果master故障,Sentinel(哨兵)会将一个slave(从节点)提升为master。当故障实例恢复后也以新的master为主;同时Sentinel也充当Redis客户端的服务发现来源,当集群发生故障转移时,会将最新信息推送给Redis的客户端,所以一般项目都会采用哨兵的模式来保证redis的高并发高可用
4.4 你们使用redis是单点还是集群,哪种集群
嗯!,我们当时使用的是主从(1主1从)加哨兵。一般单节点不超过10G内存,如果Redis内存不足则可以给不同服务分配独立的Redis主从节点。尽量不做分片集群。因为集群维护起来比较麻烦,并且集群之间的心跳检测和数据通信会消耗大量的网络带宽,也没有办法使用lua脚本和事务
4.3 redis哨兵
redis的哨兵主要是为了提升主从架构的可用性和监控主从架构的是否可用;
当哨兵监控主节点宕机,通过其他哨兵,确认主节点宕机了,通知从节点进行选举
选举规则
-
然后判断从节点的slave-priority值(优先级属性),越小优先级越高
-
如果slave-prority一样,则判断slave(从)节点的offset值,越大优先级越高
-
最后是判断slave节点的运行id大小,越小优先级越高。
哨兵的核心知识 哨兵至少需要 3 个实例,保证是奇数个,来保证自己的健壮性。 哨兵 + redis 主从的部署架构,是不保证数据零丢失的,只能保证 redis 集群的高可用性。 对于哨兵 + redis 主从这种复杂的部署架构,尽量在测试环境和生产环境,都进行充足的测试和演练。
4.4 redis集群
主从和哨兵可以解决高可用、高并发读的问题。但是依然有两个问题没有解决:
-
海量数据存储问题
-
高并发写的问题
4.4.1 redis的分片集群有什么作用
-
集群中有多个master,每个master保存不同数据,可以很好的解决海量数据的问题
-
每个master都可以有多个slave节点
-
master之间通过ping监测彼此健康状态
-
客户端请求可以访问集群任意节点,最终都会被转发到正确节点
4.4.2 Redis分片集群中数据是怎么存储和读取的?
-
Redis 分片集群引入了哈希槽的概念,Redis 集群有 16384 个哈希槽
-
将16384个插槽分配到不同的实例
-
读写数据:根据key的有效部分计算哈希值,对16384取余(有效部分,如果key前面有大括号,大括号的内容就是有效部分,如果没有,则以key本身做为有效部分)余数做为插槽,寻找插槽所在的实例
-
一个插槽上的数据不止一个,可以存放多个
5,Redis分布式锁
传统锁通常是在单机或单进程中使用的,可以使用Synchronized和lock进行加锁,但是对于布式锁而言和传统锁的本质相同,都是为了保证在多个线程或多个进程并发访问共享资源时,能够保证数据的一致性和安全性。分布式锁通常可以基于数据库的实现和基于 Zookeeper 的实现,也可以 使用 Redis 实现等
5.1 redis加锁
Redis分布式锁的加锁,可以使用setnx命令,
setnx命令.其实就是给Key键设置一个值,相当于加锁,其他进程执行前会判断Redis中这个Key是否有值,如果发现这个Key有值了,就说明已有其他进程在执行,则循环等待,超时则获取失败。 解锁就是将Key键删除,为了保证解锁的原子性操作,用Redis自带的LUA脚本完成操作。
Redis分布式锁应该注意 ①加锁的时候,需要去考虑到执行一半宕机或故障导致没能执行到解锁的命令,产生死锁,所以需要给定一个过期时间或者把释放锁的代码放到finally中,防止死锁。 ②解锁要保证原子一致性。原因是因为怕误删锁把其他客户端的锁解开. ③redis的解锁为了防止误删锁,这个时候可以设置个随机的value进行标记,可以用uuid来设置随机值,每一个线程都生成一个随机值作为value,删除key的时候先判断随机的value值是否和本线程的一致,一致的才可以删除。
5.2 关于redisson
由于使用setnx命令时锁的有效期和其他线程等待的时间需要去手动设定,这时设置的值是不好控制的,于是我们可以使用redisson来实现分布式锁,因为redisson引入了一个看门狗机制,Redisson是一个基于Redis的分布式对象框架,它提供了丰富的Java API,使用Redisson创建分布式锁具体流程就是
-
线程一尝试去获取锁,拿到锁之后【锁的有效是30S】
-
在后台开启一个子线程【定时(每过10秒)去查询当前线程是否还持有锁,如果有,则给锁续命(延长锁到30S)】,直到主线程执行结束,手动释放锁
-
线程二尝试去获取锁,拿锁失败,会进行自旋【每隔一定时间去拿锁】,直到拿锁成功后去执行2
-
当应用程序释放锁时,Redisson会自动删除相应的key,以释放锁。
redisson的看门狗机制
-
在redisson中引入了一个看门狗机制,就是说每隔一段时间就检查当前业务是否还持有锁,如果持有就增加加锁的持有时间,当业务执行完成之后需要使用释放锁就可以了
-
还有一个好处就是,在高并发下,一个业务有可能会执行很快,先客户1持有锁的时候,客户2来了以后并不会马上拒绝,它会自旋不断尝试获取锁,如果客户1释放之后,客户2就可以马上持有锁,性能也得到了提升。
redisson实现的分布式锁能解决主从一致性的问题吗
这个是不能的,比如,当线程1加锁成功后,master节点数据会异步复制到slave节点,此时当前持有Redis锁的master节点宕机,slave节点被提升为新的master节点,假如现在来了一个线程2,再次加锁,会在新的master节点上加锁成功,这个时候就会出现两个节点同时持有一把锁的问题。
我们可以利用redisson提供的红锁来解决这个问题,它的主要作用是,不能只在一个redis实例上创建锁,应该是在多个redis实例上创建锁,红锁中要求是redis的节点数量要过半。这样就能避免线程1加锁成功后master节点宕机导致线程2成功加锁到新的master节点上的问题了。
但是,如果使用了红锁,因为需要同时在多个节点上都添加锁,性能就变的很低了,并且运维维护成本也非常高,所以,我们一般在项目中也不会直接使用红锁,并且官方也暂时废弃了这个红锁
redis本身就是支持高可用的,做到强一致性,就非常影响性能,所以,如果有强一致性要求高的业务,建议使用zookeeper实现的分布式锁,它是可以保证强一致性的。
6,关于缓存三剑客(穿透,击穿,雪崩)
缓存穿透
缓存穿透是指查询一个一定不存在的数据,如果从存储层查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到 DB 去查询,可能导致 DB 挂掉。这种情况大概率是遭到了攻击。
·对于缓存穿透的解决办法有:
-
第一种是,当查一个不存在的数据时,可以给一个设置一定过期间的key的假数据,存入缓存
-
第二种可以使用布隆过滤器,
布隆过滤器主要是用于检索一个元素是否在一个集合中。我们当时使用的是redisson实现的布隆过滤器。
它的底层主要是先去初始化一个比较大数组,里面存放的二进制0或1。在一开始都是0,当一个key来了之后经过3次hash计算,模于数组长度找到数据的下标然后把数组中原来的0改为1,这样的话,三个数组的位置就能标明一个key的存在。查找的过程也是一样的。 当然是有缺点的,布隆过滤器有可能会产生一定的误判,我们一般可以设置这个误判率,但是一般不会超过5%,这个误判率是可以接受的,不至于高并发下压倒数据库。
缓存击穿
缓存击穿的意思是对于设置了过期时间的key,缓存在某个时间点过期的时候,恰好这时间点对这个Key有大量的并发请求过来,这些请求发现缓存过期一般都会从后端 DB 加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把 DB 压垮。
对于缓存击穿的解决办法有:
1、分布式锁,当多个实例同时查询到缓存为空时,只有一个实例能够去数据库查询数据,其他实例需要等待查询结果,避免了多个实例同时查询同一个key导致的问题。 2、热点数据永不过期,对于一些频繁访问的热点数据,可以通过程序主动更新等方式,让这些热点数据永不过期。 3、限流
缓存雪崩
缓存雪崩就是缓存中在同一个时间点有大量的key同时过期,请求查询缓存中没有,直达数据库
解决办法有:
解决:1.构建多级缓存架构Nginx缓存+Redis缓存+ehcache缓存+页面静态化 2.限流/降级 3.炒热数据使用永久Key(定期维护) 4.设置随机时间
7、Redis单线程?多线程?
Redis的读写是单线程的,为什么那么快?
Redis的读写是单线程的,之所以很快首先是因为它是单线程的,而且它是纯在内存上进行操作的,所以速度很快,还有一个原因就是他可以io多路复用,就是非阻塞io
什么是I/O多路复用模型?
1,I/O多路复用是指利用单个线程来同时监听多个多个客户端,并且会在某个客户端可读、可写时得到通知,从而可以很好的去避免无效的等待,充分利用CPU资源。
目前的I/O多路复用都是采用的epoll模式实现,它会在通知用户进程Socket就绪的同时,把已就绪的Socket写入用户空间,不需要挨个遍历Socket来判断是否就绪,提升了性能。
2,Redis的网络模型就是使用I/O多路复用结合事件的处理器来应对多个客户端请求,比如,提供了连接应答处理器、命令回复处理器,命令请求处理器;
3,在Redis6.0之后,为了提升更好的性能,在命令回复处理器使用了多线程来处理回复事件,在命令请求处理器中,将命令的转换使用了多线程,增加命令转换速度,在命令执行的时候,依然是单线程
select,poll和epoll差异 select和poll只会通知用户进程有Socket就绪,但不确定具体是哪个Socket ,需要用户进程逐个遍历Socket来确认 epoll则会在通知用户进程Socket就绪的同时,把已就绪的Socket写入用户空间
关于reids的发展
-
Redis4.0之前是单线程,所以没有线程安全问题
-
Redis4.0之后提供了多线程,仅针对于后台一些功能【主从复制、持久化.......】
-
Redis6.0之后提供了多线程,仅针对于网络传输 Redis的瓶颈是网络传输和内存,不是CPU
-
Redis的多线程默认是关闭的,除非达到瓶颈,否则不用开
8、缓存与数据库数据一致性?
对于如何保证缓存和数据库数据的一致性,可以分为两个方面,
如果想要保证数据的弱一致,
-
第一种可以使用延时双删,先更新数据库,再删除缓存,虽然这样会有短暂的不一致情况,但是最终会一致的,可以很好的保证数据的最终一致性
-
第二种就是可以基于mysql binlog 日志进行异步更新缓存,这种方法可以使用canal监听bin-log日志,如果监听到数据库的数据有变动,就把变动的数据同步到缓存中
如果想要保证数据的强一致性,可以使用加锁的方式,把更新数据库和修改缓存的操作当成一个原子性操作,给这个操作加锁,这样虽然可以很好的保证数据的强一致性,但是在性能方面较差。
9,redis底层数据结构和通信协议
Redis使用一种基于文本协议的序列化协议——RESP(Redis Serialization Protocol)