目录
一、Redis:
-
缓存穿透
缓存未命中会查询数据库,并将查询结果写入redis,但是当查询不存在的数据 时,例如api/getById/-1用户用不存在的用户id来发送请求,mysql查询失败就不会写到缓存,缓存失效,导致每次查询都会访问数据库。
解决方法:1.缓存空对象:尽管某个请求获取的数据在数据库和缓存中都不存在,那么也会在缓存中缓存一个null值,但大量null值会消耗额外的内存,且有短期的数据不一致问题(缓存更新)。2.布隆过滤:bitMap数据结构(bit为单位的数组,string类型,最大512M),对于数据库中的数据,基于某种哈希算法计算出3次哈希值,将哈希值转换成二进制位存储到bitMap 对应的位。当判断数据库中的数据是否存在时,通过判断bit数组中对应位置是0还是1以此判断请求的数据是否存在,空间占用小。当布隆过滤器返回"不存在"时,那么请求的数据100%不存在。但是当布隆过滤器返回"存在"时,由于bit数组空间有限,不同数据会有hash值冲突,请求的数据也不一定存在。3.缓存空对象和布隆过滤都是出现缓存穿透后被动的进行处理。完全可以通过主动的方式来避免缓存穿透,增强id的复杂度,在此基础上做好数据的基础格式校验,在格式校验阶段就能拦截,接触不到数据库。
-
缓存击穿
高并发访问且缓存重建较为复杂的key过期时,由于MySQL写入缓存耗时很长,当线程1重建缓存的过程中,其他多个线程此时要高并发的查询相同的信息,查询缓存还是会未命中,所以都会访问数据库。
解决方法:1.SETNX互斥锁:本质是SETNX是一个单命令原子操作,重建缓存过程前上锁,重建完成释放锁,保证访问相同信息的所有线程中只能有一个请求重建缓存,其他请求由于获取锁失败暂时阻塞。2.逻辑过期: 不给热key设置TTL,而是给key添加一个expire的value,当expire时间减为0,那么第一个访问该key的线程会获取锁并开辟异步线程访问数据库重建缓存,主线程直接返回过期数据,异步线程重建缓存后释放锁。重建缓存过程中如果有其他线程访问该key那么直接返回缓存中过期的数据。
-
缓存雪崩
在同一时间大量的缓存key同时过期或者Redis服务宕机,导致大量请求到达数据库。
解决方法:1.给不同key的TTL添加随机值,或添加多级缓存,防止大量缓存key同时失效。2.利用redis集群,确保一直有redis可用,防止因某一redis宕机引起的缓存雪崩。3.当redis宕机时,牺牲部分服务,不允许这些请求到达数据库,实现限流策略。
-
缓存更新(双写一致性)
读操作:缓存命中则直接返回,缓存未命中则查询数据库,写入缓存。如果是热key那么使用缓存击穿方案来限制查询数据库的线程量,如果不是热key那么允许多个线程同时查数据库并写入缓存。
写操作:1.spring事务确保数据库与缓存操作要么全执行要么全不执行 (原子性);2.先写数据库后删除缓存 (隔离性),写操作后不会有一致性问题,但是在写操作期间会有数据不一致问题,如果要求数据的强一致性,那么对于写操作完成前的数据不一致问题可以使用读写锁 (共享锁+排他锁):1. 共享锁 ,读操作会上锁,期间允许其他读操作,但是不允许任何写操作,但这种方法写操作就饥饿了吧,虽然解决了数据不一致问题但写操作执行晚了,数据没法及时更新还是有脏数据吧。2. 排他锁,写操作上锁,期间不允许其他的读写操作,感觉这个好,既能解决写操作完成前的数据不一致问题,又能保证数据及时更新。延时双删在写操作期间依然会有数据不一致问题,且延时双删多访问了数据库,唯一的优势仅在该情境下"写请求在写数据期间没有读操作,那么写数据库后删除缓存前的读操作不会读到脏数据"。
-
redis持久化
RDB:将内存中的所有数据记录到磁盘的rdb文件中,redis在停机前会自动保存rdb数据到磁盘当前项目工作目录下,然后才会执行停机指令。当启动redis时会自动读取当前工作目录下的rdb文件并加载到内存中。save命令由redis主进程来执行,会阻塞所有命令,适合正常redis手动停机时使用;bgsave命令会fork redis主进程得到一个异步子进程执行持久化,与主进程共享内存空间,fork过程中主进程阻塞,子进程创建后主进程正常工作,子进程创建后会读取内存数据写入一个新的rdb文件,并将新的rdb文件覆盖旧的rdb文件,适合redis运行过程中持久化内存数据,可以防止redis宕机导致的数据丢失。触发频率:900s内如果至少有1个key被修改,10 300s内如果至少有10个key被修改,60s内如果至少有10000个key被修改,则执行bgsave命令,触发频率高那么频繁创建子进程和写rdb文件非常影响性能;时间设置太长会出现向内存写入数据后触发还未执行redis就宕机,导致数据丢失。
AOF:与RDB将内存数据存储到磁盘中不同,AOF是将redis执行的每一条写命令以追加的方式记录在磁盘的aof日志文件中,当服务宕机重启后通过重新执行这些写命令来完成数据恢复,默认触发频率是将写命令暂存到OS为redis分配的缓冲区,每隔1s将缓冲区的所有命令写入aof文件。bgrewriteaof可以对aof文件执行重写功能,减少无效命令的记录,也是使用fork开辟异步线程来执行覆写功能
-
redis数据过期策略
过期策略和淘汰策略用来避免内存存储达到上限。redisDB维护两个Dict,dict记录所有redisObject-内存首址键值对、expires记录设置了过期时间的redisObject-过期时间键值对。1.惰性删除在访问key时执行,首先根据key从dict中找到对应的redisObject,然后检查key记录在expires中的TTL,如果过期那么释放key的内存空间,但是如果key过期了但是永远不会被访问,那么惰性删除策略下该key的内存空间永远不会被释放。2.周期删除为所有key设置同一个定时任务,周期性的抽样部分key,检查是否过期,如果过期执行删除操作。SLOW模式:redis单线程初始化时(初始化epoll阶段)每100ms定期检查并清理过期key,不在主线程main函数中:执行周期为100ms,执行清理耗时不能超过25%即25ms,首先逐个遍历db,每次取20个key检查并清理过期key,如果时间未达到25ms且刚才检查的过期key比例超过10%,再取20个key检查并清理。FAST模式:每次redis单线程调用epoll_wait阻塞前都会先检查并清理部分过期key,因为位于主线程的main代码中,所以执行周期为两次调用epoll_wait的间隔,FAST间隔不能低于2ms,每次清理耗时不能超过1ms。
-
redis数据淘汰策略
淘汰策略就是Redis内存使用达到阈值时,主动挑选部分key删除以释放内存,在主线程解析命令后处理命令前执行。默认不淘汰任何key,内存满时不允许写入新数据。其他方案:1.对设置了TTL的key淘汰,TTL越小越先被淘汰。2.对全体/设置了TTL的key,当前时间减最近一次访问时间,值越大优先淘汰。3.对全体/设置了TTL的key,访问次数越少优先淘汰,使用8bit记录逻辑访问次数,范围0~255,基于随机数,逻辑访问次数越大,计数器越难+1。key的访问次数和访问时间都会封装在redisObject对象中。
-
分布式锁
synchronized悲观互斥锁只能保证单个JVM内部的多个线程之间的互斥,因为synchronized本质是竞争JVM中目标对象关联的Monitor的Owner身份,无法保证集群下多个JVM之间的进程互斥。而分布式锁是集群模式下所有线程都可见的锁。本质还是利用了SETNX原子操作的互斥性,使用SET key thread NX原子命令来对某个key加锁,并设置EX超时时间防止死锁,使用DEL userlock释放锁。这么做有三个问题:不可重入、不可重试、超时释放。不可重入问题是持有锁的线程无法再次获取锁,可以通过给key增加state字段,重复获取锁state++,释放锁state--,如果state减为0那么执行DEL userlock,但问题是if判断是否持有锁和state++是两条指令,具有线程安全问题,所以需要使用Lua脚本来确保原子操作,使用hincrby来进行自增自减。超时释放问题是锁提前释放会导致其他线程提前获取锁,有线程安全问题,可以为每个锁设置定时任务,当上锁成功时启动watchdog,每TTL/3重置锁的TTL,释放锁时停止watchDog。不可重试问题是获取锁只尝试一次,失败就返回false,没有重试机制,可以通过发布订阅模式,在获取锁失败后在等待的最长时间内subscribe其他线程释放锁的信号,等待获得释放锁的信号,而不是盲目的重试占用CPU资源,持有锁的线程在释放锁时通过publish发出信号,等待线程等到信号才会重新尝试获取锁,感觉这里有竞争,设置成等待队列先到先得比较好。 然后Redisson中其他的亮点就是对每个锁用了单例模式,每个锁对应map中的一个锁对象,锁对象中维护定时任务,因此不会频繁的创建锁对象和定时任务,线程切换只要修改锁对象的value就行了。
主从一致性用红锁。
-
消息队列MQ
阻塞队列位于JVM,受到内存上限的限制,且没有持久化机制,队列中的信息有丢失的风险。消息队列位于JVM之外,存入的信息具有持久化机制,包含消息队列、生产者、消费者。1.List结构:双向链表lpush和brpop模拟阻塞队列,缺点是出队后写数据库之前服务器宕机还是会有信息丢失风险。2.PubSub基于发布订阅模式,消费者可以订阅一个或多个channel,生产者向channel存入信息后,订阅该channel的消费者都能收到相关信息,消息可以指定发送给1个消费者也可以同时发送给多个消费者,缺点是redis不支持对该模型的数据持久化。3.Stream数据类型读消息后消息并不会出队,而是依然保存在Stream中,不仅支持持久化,也没有信息丢失风险,其中消费者组维护一个id指针,每次读取消息后指针后移防止漏读,消费者组还维护一个pending-list,记录每个已消费但未处理的信息,当信息被处理后通过XACK标记信息为已处理,并从pending-list中移除,防止信息丢失。
-
Feed流
为用户持续推送消息的一种方式,推送内容按照内容发布时间排序或利用推荐算法推送用户感兴趣的内容。1.拉模式:每个用户只需维护发件箱,用户A发消息时服务器都会将该消息保存到用户A的发件箱中。用户B想要查看消息时,会从用户A的发件箱中获取消息。2.推模式:每个用户只需维护收件箱,用户A发消息时服务器都会将该消息推送到用户B的收件箱中。用户B想要查看消息时,只需要从自己的收件箱中获取消息。3.读写混合:每个用户需维护发件箱和收件箱,若用户A的粉丝太多那么采用拉模式,若用户B是用户A的特别关注,那么将用户A的数据采用推模式到用户b的收件箱,僵尸粉用拉模式。若用户A的粉丝很少,采用推模式到所有粉丝的收件箱。活跃用户用推模式。
-
主从集群
主从集群主要是解决redis高并发读问题,多个从节点用来处理读请求,主节点完成写请求,并将数据同步到从节点。1.从节点首先发送replid和offset给主节点,主节点判断replid是否是自己,如果不是那么还没有确定主从关系,需要全量同步,并返回自己的replid和offset给从节点;如果是自己的那么根据offset做增量同步。2.如果是全量同步,从节点保存返回的replid和offset确定主从关系,主节点将RDB文件发送给从节点,从节点清空本地数据后读取主节点的RDB文件。3.主节点将从节点读取RDB文件期间执行的其他数据操作命令发送给从节点,确保数据一致。4.确定主从关系后,从节点每次重启后都需要做一次增量同步,主节点从repl_baklog文件(线性队列(环形数组))中获取offset之后的数据发送给从节点。特别的,当从节点断连太久,可能会出现环形数组中尚未同步的数据被覆盖,此时可能需要重新做全量同步。
-
哨兵:
从节点断连后可以使用全量同步或增量同步找主节点更新数据,哨兵用来解决主节点断连的数据一致性问题。为了防止单哨兵故障所以哨兵也是集群,1.哨兵每隔1s向集群的每个节点发送ping命令,若哨兵集群中超过指定数量的哨兵都未收到该节点的响应,则认定该节点客观下线。2.排除断连的节点后,根据优先级筛选出优先级最高的一批节点,选出其中offset最大的slave作为master节点。3.哨兵向选中的slave节点发送slaveof no one命令让该节点成为新的master,向其他所有节点发送slaveof newIp newPort命令修改这些slave节点的master,并强制修改故障节点为slave节点。
-
分片集群
主从集群解决了redis高并发读问题,Redis分片集群为了解决redis高并发写问题和海量数据存储。分片集群中有多个master,每个master独立保存数据,每个master都可以搭建主从集群分配多个slave节点,master之间通过ping监测彼此状态,在主节点出现故障时能够自动主从切换,不需要哨兵。
分片集群下多个master节点共同分配16384个hash插槽,每个master节点只能操作自己的插槽。数据key不与master节点绑定,而是与插槽绑定。当操作某个key时,先对key使用CRC16计算hash值,再取余16384得到slot值,然后判断该插槽属于哪个master节点,redirect到该节点来操作key,避免了手动选择master,同时某一master故障时直接由从节点继承这部分数据保证数据不会丢失。
主从切换:从节点通知主节点拒绝任何客户端请求,主节点将offset后的数据给从节点做同步,从节点广播自己是主节点的消息给其他节点。
-
redis是单线程,为什么这么快
redis是纯内存操作,内存执行速度很快。单线程避免线程切换开销。使用非阻塞I/O多路复用模型。
-
redis I/O多路复用模型
redis执行过程为接收网络请求->执行命令->返回响应结果,由于执行命令是内存操作,所以限制redis速度的就是接受网络请求和返回响应结果。
单线程阻塞I/O模型服务器只有一个线程用来监听所有客户端,线程每次调用recvfrom系统调用只能监听一个客户端的数据,如果该客户端没有数据会一直阻塞直到该客户端的数据到达内核缓冲区,无法处理其他客户端早已写入内核缓冲区的数据,性能差。
非阻塞I/O虽然不需要阻塞等待某一客户端的请求,并且可以同时while轮询监听多个客户端的请求,但是while轮询会查询所有客户端的数据是否到达,线程一直占用CPU,导致CPU利用率低,且轮询所有客户端所以性能也较差。
I/O多路复用是利用单个线程同时监听多个socket的网络I/O请求。与阻塞I/O不同的是,I/O多路复用使用的是select、poll、epoll系统调用函数,阻塞状态下可以同时监听多个socket,当某一个socket的数据到达时,就会唤醒阻塞线程回到用户态,线程调用recvfrom来读取已经到达的数据到用户空间并处理。与非阻塞I/O不同的是,当没有数据可到达时线程会阻塞并让出CPU。