- 为什么使用缓存
高性能、高可用
- 什么是缓存穿透 击穿 雪崩? 怎么解决
缓存穿透:缓存中查不多、数据库也查不到。解决:
- 对参数进行合法性校验
- 数据库中没有查到结果的数据也写到缓存,这是要注意为了防止redis被无用的key占满,这一类缓存有效期要设置的更短
- 引入布隆过滤器,在查redis之前判断数据是否存在,注意布隆过滤器存在一定误判率,只支持加数据,不支持减数据。
布隆过滤器:维护一个列表,类似于所有key值hash后的与。判断一个元素不在集合中,那么肯定不在;如果判断在,存在一定误判率
缓存击穿:缓存中没有,数据库中有。一般出现在缓存初始化及key过期的情况,问题在于写入缓存需要一定时间,高并发下会给DB造成压力。解决:
- 设置热点数据永不过期,这时注意在value中包含逻辑上的过期时间,然后另起一个线程定期重建这些数据
- 加载数据到缓存时要防止并发,往缓存写时加个客户端到缓存的锁
缓存雪崩:缓存大面积过期,导致请求都到DB。解决:
- 把缓存的失效时间分散开,例如在原有同一失效时间基础上增加一个随机值
- 对热点数据设置永不过期
- key放到不同集群
- 缓存预热,
- 如何保证redis与数据库一致?
当我们对数据进行修改的时候,到底是先删缓存还是先写数据库?
- 如果先删缓存,再写数据库:在高并发场景下,当第一个线程删除了缓存,还没来得及写数据库,第二个线程来读数据,会发现缓存中的数据为空,那就会去读数据库中的数据(旧值、脏数据),把读到的结果写入缓存(此时,第一个线程已经将新的值写到缓存里面了),这样缓存中的值就会被覆盖外修改前的脏数据。
总结:在这种方式下,通常要求写操作不会太频繁
解决方案:a.先操作缓存,但是不删缓存,将缓存修改为一个特殊值(-999),客户端读缓存时,发现是默认值,就休眠一会,再去查redis。——特殊值对业务有侵入,可能会多次重复,对性能有影响。b.延时双删,先删缓存,再写数据库,休眠一会,再次删缓存。——如果数据写操作很频繁,同样还是会有脏数据问题
- 先写数据库,再删缓存:如果数据库写完了之后,缓存删除失败,数据就会不一致。
总结:始终只能保证一定时间内的最终一致性。
解决方案:a.给缓存设置一个过期时间,问题:过期时间内,缓存数据不会更新。b.引入MQ,保证原子操作。
解决方案:将热点数据缓存设置为永不过期,但是在value当中写入一个逻辑过期时间,另外一个后台线程,扫描这些key,对逻辑上过期的缓存进行删除。
终极方案:
将访问操作串行化:1.先删缓存,将更新数据库的操作放进有序队列中。2.从缓存查不到的查询都进入有序队列。
面临问题:1.读请求积压,大量超时,导致数据库的压力限流、熔断。2.如何避免大量请求积压:将队列水平切分,提高并行度。3.保证相同请求路由正确。
- 如何设计分布式锁?对锁 性能优化?
setnx+setex:存在设置超时时间失败的情况,导致死锁
set(key,value,nx,px):将setnx+setex变成原子操作
问题:
- 任务超时,锁自动释放,导致并发问题,使用redisson解决(看门狗监听,自动续期);
- 以及加锁和释放锁不是同一个线程问题。在value里存入uuid,删除锁时判断标识(LUa保证院子操作);
- 不可重入,使用redisson解决(实现机制类似AQS,计数);
- 异步复制可能造成锁丢失,使用redLock解决
- 顺序向五个节点请求加锁
- 根据一定的超时时间来推算是不是跳过该节点
- 三个节点锁成功并且花费时间小于锁的有效期
- 认定加锁成功
- redis如何配置key过期时间?原理?
- expire指令,为key设置过期时间
- 字符串setex设置有效时长,设置key的value和过期时间的原子操作。
实现原理:1.定期删除:每隔一段时间,执行一次删除过期key;2.懒汉式删除:当使用get getset等指令时判断key是否过期,过期后先删除再进行后面的操作。
Redis是将两种方式结合使用,定期删除要平衡执行频率和执行时长。定期删除时会遍历每个database(默认16个),检查当前库中指定个数的key(默认20个)随机抽查这些key,如果过期就删除,程序中有一个全局变量记录到秒到了哪个数据库
缓存过期策略
- 定时过期:每个设置过期时间的key都需要创建一个定时器,到过期时间就会立即清除。该策略可以立即清除过期的数据,对内存很友好;但是会占用大量的cpu资源去处理过期的数据,从而影响缓存的响应时间和吞吐量。
- 惰性过期:只有当访问一个key时,才会判断该key是否已过期,过期则清除,该策略可以最大化地节省cpu资源,但是很消耗内存,许多的过期数据还存在内存中,极端情况可能出现大量的过期key没有再次被访问,从而不会被清除,占用大量内存。
- 定期清理:每隔一段时间,会扫描一定数量的数据库expires字典中一定数量的key(是随机的),并清除其中已过期的key,该策略是定时过期和惰性过期的折中方案,通过调整定时扫描的时间间隔和每次扫描的限定耗时,可以在不同情况下使得cpu和内存资源达到最优的平衡效果。
- 海量数据下,如何快速查找一条记录?
- 使用布隆过滤器,快速过滤不存在的记录
使用redis的bitMap结构来实现布隆过滤器
- 在redis中建立数据缓存-将我们对redis使用场景的理解尽量表达出来
以普通字符串的形式来存储(userid->user.json),以hash来存储一条记录(userid’key-> username/ userAge field->)。以一整个hash来存储所有数据,userInfo->field就用userid, value就是user.json。一个hash最多能支持2^32-1 个键值对约40亿个
缓存击穿:对不存在的数据也建立key,这些key都是经过布隆过滤器过滤过的,所以一般不会太多
缓存过期:将热点数据设置成永不过期,定期重建缓存;使用分布式锁重建缓存
- 查询优化。按槽位分配数据,自己实现槽位计算,找到记录应该分配到哪台机器上,然后直接去目标机器上去找。
redis主从同步机制
- 从节点执行slaveof masterIp port,保存主节点信息
- 从节点中的定时任务发现主从节点信息,建立和主节点的socket连接
- 从节点发生信号,主节点返回,两边能相互通信
- 连接建立后,主节点将所有数据发送给从节点(数据同步)
- 主节点把当前的数据同步给从节点后,便完成了复制过程。接下来。主节点就会持续的把写命令发送给从节点,保证从数据一致性。
runid:每个redis节点启动都会生成唯一的uuid,每次redis重启后,runid都会发生变化。
offset:主从节点各自维护自己的复制偏移量offset,当主节点有写入命令时,offset=offset+命令的字节长度。从节点在收到主节点发送的命令后,也会增加自己的offset。并把自己的offset发送给主节点,主节点同时保存自己的offset和从节点的offset,通过对比offset来判断主从节点数据是否一致。
repl_backlog_size:保存在主节点上的一个固定长度的先进先出队列,默认大小是1MB
全量复制:
- 从节点发送psync命令,psync runid offset(由于是第一次,runid为?,offset为-1)
- 主节点返回FULLRESYNC runid offset,runid是主节点的runid,offset是主节点目前的offset。从节点保存信息
- 主节点启动bgsave命令fork子进程进行rdb持久化
- 主节点将rdb文件发送给从节点,到从节点加载数据完成之前,写命令写入缓冲区
- 从节点清理本地数据并加载rdb,如果开启了aof会重写aof
部分复制:
- 复制偏移量:psync runid offset
- 复制积压缓冲区:当主从节点offset的差距过大超过缓冲区长度时,将无法执行部分复制,只能执行全量复制。
- 如果从节点保存的runid与主节点现在的runid相同说明主从节点之前同步过,主节点会继续尝试使用部分复制(到底能不能部分复制还要看offset和复制积压缓冲区的情况);
- 如果从节点保存的runnid与主节点现在的runid不同,说明从节点在断线前同步的redis节点不是当前的主节点,只能进行全量复制。
Redis的高可用方案?
- 主从:主挂了需要手动启动从,从只起到分担查询的能力
- 哨兵
sentinel,哨兵是redis集群中非常重要的一个组件,主要有以下功能:
集群监控:复制监控redis master和slave进程是否正常工作;
消息通知:如果某个redis实例有故障,那么哨兵负责发送消息作为报警通知给管理员;
故障转移:如果master node挂掉了,会自动转移到slave node上;
配置中心:如果故障转移发生了,通知client客户端新的master地址。
哨兵用于实现redis集群的高可用,本身也是分布式的,作为一个哨兵集群去运行,互相协同工作。
故障转移时,判断一个master node是否宕机了,需要大部分的哨兵同义才行,涉及到了分布式选举;
即使部分哨兵节点挂掉了,哨兵集群还是能正常工作的;
哨兵通常需要3个实例来保证自己的健壮性;
哨兵+redis主从的部署架构,是不保证数据领丢失的,只能保证redis集群的高可用性 ;
对于哨兵+redis主从这种复杂的部署架构,尽量在测试环境和生产环境,都进行充足的测试和演练。
3、分片
redis Sharding是redis cluster出来之前,业界普遍使用最多redis实例集群方法,其主要思想是采用哈希算法将redis数据的key进行散列,通过hash函数,特定的key会映射到特定的节点上。javaredis客户端驱动jedis,支持Sharding功能,即Shardedjedis以及结合缓冲池的shardedjedisPool
优点:非常简单,客户端的redis实例彼此独立,相互无关联,每个redis实例像单列服务器一样运行,非常容易线性扩展,系统的灵活性强
缺点:由于Sharding处理放到客户端,规模进一步扩大时给运维带来挑战。客户端Sharding不支持动态增删节点,服务端redis实例群拓扑结构有变化时,每个客户端都需要更新调整,连接不能共享,当使用规模增大时,资源浪费制约优化
- redis集群
redis Cluster是一种服务端Sharding技术,3.0版本开始正式提供,采用slot(槽)的概念,一共分成16384个槽,将请求发送到任意节点,接收到请求的节点会将查询请求发送到正确的节点上执行。
方案:
- 通过哈希方式,将数据分片,默认分配了16384个槽位
- 每份数据分片会存储在多个护卫主从的多节点上
- 数据写入先写主节点,再同步到从节点,支持配置为阻塞同步
- 同一个分片多个节点间的数据不保持强一致性
- 读取数据时,当客户端操作的key没有分配在该节点上时,redis会返回转向指令,指向正确的节点
- 扩容时需要把旧节点的数据迁移一部分到新节点
在redis cluster下,每个redis要开放两个端口号,比如一个是6379,另一个就是加1w的16379.
16379端口号是用来进行节点间通信的,也就是cluster bus的通信,用来进行故障检测、配置更新、故障转移授权。cluster bus用了另外一种二进制协议,gossip协议,用于节点间进行高效的数据交换,占用更少的网络带宽和处理时间。
优点:
无中心架构,支持动态扩容,对业余透明
具备sentinel的监控和自动Failover故障转移能力
客户端不需要连接集群所有节点,连接集群中任何一个节点即可
高性能,客户端直连redis服务,免去了proxy代理的消耗
缺点
运维复杂,数据迁移需要人工干预
只能使用0号数据库
不支持批量操作,pipeline管道操作
分布式逻辑和存储模块耦合
Redis事务的实现
- 事务开始
MULTI命令的执行,标识着一个事务的开始,MULTI命令会将客户端状态的flags属性打开REDIS_MULTI标识来完成的
- 命令入队
当一个客户端切换到事务状态后,服务器会根据这个客户端发送来的命令执行不同的操作。如果客户端发送的命令为MULTI、EXEC、EATCH、DISCARD中的一个,立即执行这个命令,否则将命令放入一个事务队列里面,然后向客户端返回QUEUED回复
- 如果客户端发送的命令为EXEC、DISCARD、WATCH、MULTI四个命令的其中一个,name服务器立即执行这个命令
- 如果客户端发送是四个命令以外的其他命令,name服务器并不立即执行这个命令。首先检查这个命令的格式是否正确,如果不正确,服务器会在客户端状态(redisclient)的flags属性关闭REDIS_MULTI标识,并且返回错误信息给客户端,如果正确,将这个命令放入一个事务队列里面,然后向客户端返回QUEUED回复。
事务队列是安装FIFO的方式保存入队的命令。
- 事务执行
客户端发生EXEC命令,服务器执行EXEC命令逻辑。
- 日过客户端状态的flags属性不包括REDIS_MULTI标识,或者包含REDIS_DIRTY_CAS或者REDIS_DIRTY_EXEC标识,name就直接取消事务的执行;
- 否则客户端处于事务状态(flags有REDIS_MULTI标识),服务器会变量客户端的事务队列,然后执行事务队列中的所有命令,最后将返回结果全部返回给客户端。
redis不支持事务回滚机制,但是它会检查每一个事务中的命令是否错误。
redis事务不支持检查程序员自己的逻辑错误,例如对string类型的数据库键执行对hashmap的操作。
WATCH命令是一个乐观锁,可以为redis事务提供check-and-set(CAS)行为,可以监控一个或多个键,一旦其中有一个键被修改或删除,之后的事务就不会执行,监控一致持续到EXEC命令。
MULTI命令用于开启一个事务,它总是返回OK,MULTI执行之后,客户端可以继续向服务器发送任意多条命令,这些命令不会立即被执行,而是被放到一个队列中,当EXEC命令被调用时,所有队列中的命令才会被执行。
EXEC:执行所有事务块内的命令,返回事务块内所有命令的返回值,按命令执行的先后顺序排列,当操作被打断时,返回控制null
通过调用DISCARD客户端可以情况事务队列,并放弃执行事务,并且客户端会从事务状态中退出
UNWATCH命令可以取消WATCH对所有key的监控
Redis的数据结构
string:字符串
List:列表
Hash:哈希表
Set:无序集合
Sorted Set:有序集合
bitmap:布隆过滤器
GeoHash:坐标,借助sorted set 实现,通过zset的score进行排序就可以得到坐标附近的其他元素,通过将score还原成坐标值就可以得到元素的原始坐标
HyperLogLog:统计不重复数据,用于大数据基数统计
Streams:内存版的kafka
分布式系统中常见的缓存方案有哪些
客户端缓存:页面和浏览器缓存,app缓存,H5缓存,LocalStorage和sessionStorage
CDN缓存:内存存储:数据的缓存,内容分发:负载均衡
nginx缓存:静态资源
服务端缓存:本地缓存,外部缓存
数据库缓存:持久层缓存(mybatis,hibernate多级缓存),MySQL查询缓存
操作系统缓存:page cache、buffer cache
常见的缓存淘汰算法
FIFO:first in first out 先进先出,根据缓存被存储的时间,离当前最远的数据优先被淘汰;
LRU:least recently used 最近最少使用,根据最近被使用的时间,离当前最远的数据优先被淘汰;
LFU:least frequently used 最不经常使用,在一段时间内,缓存数据被使用次数最少的会被淘汰。
Redis持久化机制
RDB:redis database 将某一盒时刻的内存快照(Snapshot)以二进制的方式写入磁盘。
手动触发:
- save命令,使redis处于阻塞状态,直到rdb持久化完成,才会响应其他客户端发来的命令,所以在生产环境一定要慎用
- bgsave命令,fork出一个子进程进行持久化,主进程只在fork过程中有短暂阻塞,子进程创建之后,主进程就可以响应客户端请求了
自动触发:
- save m n:在m秒内,如果有n个键发生改变,则自动触发持久化,通过bgsave执行,如果设置多个、只要满足其一就会触发,配置文件有默认配置(可以注释掉)
- flushall:用于清空redis所有的数据库,flushdb清空当前redis所在库数据(默认是0号数据库),会清空rdb文件,同时也会生成dump.rdb,内容为空
- 主从同步:全量同步时会自动触发bgsave命令,生成rdb发送给从节点
优点:
- 整个redis数据库将只包含一个dump.rdb,方便持久化
- 容灾性好,方便备份
- 性能最大化,fork紫禁城来完成写操作,让主线程继续处理命令,所以IO最大化,使用单独子进程来进行持久化,主进程不会进行任何IO操作,保证了redis的高性能
- 相对于数据集大时,比AOF的启动效率更高
缺点:
- 数据安全性低,rdb是间隔一段时间进行持久化,如果持久化之间redis故障,会发生数据丢失,所以这种方式更适合数据要求不严谨的时候
- 由于rdb是通过fork子进程来协助完成数据持久化工作的,因此,如果当数据集较大时,可能会导致整个服务器停止服务几百毫秒,甚至是1s钟,会占用cpu
AOF:append only file 以日志的形式记录服务器所处理的每一个写、删除操作,查询操作不会记录,以文本的方式记录,可以打开文件看到详细的操作记录,调操作系统命令进程刷盘
- 所有的写命令会追加到AOF缓冲中
- AOF缓冲区根据对应的策略向硬盘进行同步操作
- 随着AOF文件越来越大,需要定期对AOF文件进行重写,达到压缩的目的
- 当redis重启时,可以加载aof文件进行数据恢复
同步策略:
- 每秒同步:异步同步,效率非常高,一旦系统出现宕机现象,name这一秒钟之内修改的数据会丢失
- 每修改同步:同步持久化,每次发生的数据变化都会被立即记录到磁盘中,最多丢一条
- 由操作系统控制,可能丢失较多数据
优点:
- 数据安全
- 通过append模式写文件,即使中途服务器宕机也不会破坏已存在的内容,可以通过redis-check-aof工具解决一致性问题
- AOF机制的rewrite模式,定期对AOF文件进行重写,以达到压缩的目的
缺点:
- AOF文件比RDB文件大,且恢复速度慢
- 数据集大的时候,比rdb启动效率低
- 运行效率没有rdb高
AOF文件比RDB更新频率更高,优先使用AOF还原数据;
AOF比RDB更安全也更大;
RDB性能比AOF好
Redis线程模型、单线程快的原因
redis基于reactor模式开发了网络事件处理器,这个处理器叫做文件事件处理器file event handler,这个文件事件处理器是单线程的,所以redis才叫做单线程的模型,它采用IO多路复用机制来同时监听多个socket,根据socket上的事件类型来选择对应的事件处理器来处理这个事件,可以实现高性能的网络通信模型,又可以跟内部其他单线程的模块进行对接,保证了redis内部的线程模型的简单性。
文件事件处理器的结构包含4个部分:多个socket、IO多路复用程序、文件事件分派器以及事件处理器(命令请求处理器、命令回复处理器、连接应答处理器等)。
多个socket会将socket放入一个队列中排队,每次从队列中取出一个socket给事件分派器,事件分派器把socket给对应的事件处理器。
然后一个socket的事件处理完之后,IO多路复用程序才会将队列中的下一个socket给事件分派器,文件事件分派器会根据每个socket当前产生的事件,来选择对应的事件处理器来处理。
单线程快的原因:1、纯内存操作;2、核心是基于非阻塞的iO多路复用机制;3、单线程反而避免了多线程的频繁上下文切换带来的性能问题。
- redis启动初始化时,将连接应答处理器跟AE_READABLE事件关联;
- 若一个客户端发起连接,会产生一个AE_READABLE事件和命令请求处理器关联,使得客户端可以向主服务器发生命令请求。
- 当客户端向dedis发送请求时(不管读还是写请求),客户端socket都会产生一个AE_READABLE事件,触发命令请求处理器。处理器读取客户端的命令内容然后传给相关程序执行。
- 当redis服务器准备好给客户端的响应数据后,会将socket的AE_writable事件和命令回复处理器关联,当客户端准备号读取响应数据时,会在socket产生一个AE_writable事件,由对应命令回复处理器处理,即将准备好的响应数据写入socket,供客户端读取。
- 命令回复处理器全部写完到socket后,就会删除该socket_waitable事件和命令回复处理器的映射。