1、缓存穿透:
方案一:
方案二:布隆过滤器
bitmap位图:
可能存在误判:
实现:
误判率是可以自己定义的:
2、缓存击穿:
互斥锁:分布式锁
逻辑过期:
3、缓存雪崩
redis的口诀:
4、缓存双写一致
延迟双删:
正常场景:
异常场景:
正常场景:先操作数据库,再删除缓存
异常场景:
为什么要删除两次缓存? 就是为了降低脏数据的出现
为什么要延时删除?因为数据库是主从模式,读写分离的,需要延时一会让主节点把数据同步到从节点,所以需要延时一会,
延时也有问题:这里的延时时间不好确定,所以也存在脏数据的风险
为了保持强一致:
可以用互斥锁方式,也就是分布式锁
也有更好点的方案:
一般放到缓存中的数据是读多写少
这比分布式锁,在性能上要好一点
使用读写锁
读操作:
写操作:
要保持一致:
特点:强一致,性能低
如果只要最终一致:
方案一:异步通知
方案二:基于Canal的异步通知
总结:
5、redis持久化
RDB AOF
RDB:
执行原理:
主进程操作虚拟内存,页表映射到物理内存
这里是进程,不是线程
fork 相当于复制了一个进程,页表是复制的,所以是相同的映射关系
AOF:
对比:
6、数据过期策略
惰性删除:
定期删除:
说明:
Redis 使用的是一种基于内存、持久化的键值存储数据库。它不仅将数据保存在内存中以实现高速访问,同时也提供了多种持久化策略(例如RDB快照和AOF日志)来确保即使在服务器重启后也能恢复数据。
具体来说,Redis 数据库并非基于传统的关系型数据库管理系统(RDBMS),而是一种 NoSQL 存储系统,其内部采用简单且高效的底层数据结构(如哈希表、跳跃表、整数集合等)来组织和管理不同的数据类型(String、Hash、List、Set、Sorted Set)。这意味着 Redis 不依赖于 SQL 语句进行数据操作,而是通过命令行接口发送特定指令完成对数据的各种增删查改操作
总结:
7、数据淘汰策略
使用建议:
面试问题:
总结:
面试题:
8、redis分布式锁使用场景
抢券业务场景:
模拟运行过程:
正常场景:
异常场景:
解决方法:加锁
在Java中,synchronized
关键字是一个用于处理多线程并发控制的关键字,它的主要作用是实现线程同步,确保共享资源在同一时刻只能被一个线程访问,从而避免了因多个线程同时读写数据导致的数据不一致或竞态条件(race condition)
synchronized
关键字有两种常见用法:
- 同步方法: 将
synchronized
修饰符应用于整个方法,这意味着这个方法在同一时间只能被一个线程执行,对象的实例锁将在这段方法执行期间被持有。 - 同步代码块: 使用
synchronized
关键字声明一个代码块,并指定一个对象作为锁。只有获取到该对象锁的线程才能执行该代码块。
通过这两种形式的 synchronized
,Java 程序员可以有效地管理多线程环境中的共享资源访问,保证并发环境下的程序正确性和数据完整性。
服务集群部署:
synchronized 是本地锁,锁是属于jvm的,单不同jvm的锁就锁不住了
在集群下,synchronized就失效了
两台机器去获取锁,都可以获取到
补充:
在Java中,锁的种类主要包括以下这些:
-
悲观锁 (Pessimistic Lock)
- 悲观锁假设数据会被并发修改,所以在访问数据时会先获取锁,确保在整个操作过程中都能独占资源。
-
乐观锁 (Optimistic Lock)
- 乐观锁假定多线程并发环境下数据一般不会造成冲突,只有在更新数据时才会去检查是否产生冲突,如通过版本号或CAS(Compare-And-Swap)操作实现。
-
互斥锁 (Mutex Lock / Exclusive Lock)
- 只允许一个线程持有锁,即一次只有一个线程可以进入临界区。Java中的
synchronized
关键字和ReentrantLock
类都是互斥锁的实现。
- 只允许一个线程持有锁,即一次只有一个线程可以进入临界区。Java中的
-
读写锁 (Read-Write Lock)
- 多个读取者可以同时访问共享资源,但在给定时间内只能有一个写入者。Java中的
java.util.concurrent.locks.ReentrantReadWriteLock
就是读写锁的实现。
- 多个读取者可以同时访问共享资源,但在给定时间内只能有一个写入者。Java中的
-
共享锁 (Shared Lock)
- 允许多个线程同时获取锁并进行读取操作,但不允许任何线程进行写入操作,它是读写锁的一部分特性。
-
独享锁 (Exclusive Lock)
- 同互斥锁,只允许一个线程拥有锁,不允许其他线程获取。
-
可重入锁 (Reentrant Lock)
- 同一线程在已经获得锁的情况下可以再次申请此锁,不会出现死锁。Java中的
synchronized
关键字具有可重入性,并且ReentrantLock
类也支持可重入。
- 同一线程在已经获得锁的情况下可以再次申请此锁,不会出现死锁。Java中的
-
公平锁与非公平锁
- 公平锁按照请求锁的顺序来分配锁,而非公平锁则不保证这种顺序。
ReentrantLock
可以通过构造函数传入参数决定是公平还是非公平。
- 公平锁按照请求锁的顺序来分配锁,而非公平锁则不保证这种顺序。
-
偏向锁 (Biased Locking)
- JVM的一种优化手段,在没有多线程竞争的情况下,让单一线程无须同步操作即可多次进入代码块,减少不必要的同步开销。
-
轻量级锁 (Lightweight Lock)
- 是JVM对 synchronized 锁进行优化后产生的锁状态,适用于没有多线程争用的情况,尝试以CAS的方式快速获取锁。
-
重量级锁 (Heavyweight Lock)
- 当锁升级为重量级锁时,通常涉及操作系统层面的互斥量(mutex)操作,开销较大,但能有效防止多个线程间的并发问题。
-
自旋锁 (Spin Lock)
- 线程在获取锁失败时,不立即阻塞而是循环等待(自旋),直到获取到锁为止。Java中的自旋锁实现并不直接体现在语言层面,而是在JVM内部进行优化时可能使用的技术。
-
分段锁 (Segment Lock)
- 将数据分成多个段,每个段有自己的锁,从而提高高并发场景下的性能,例如
ConcurrentHashMap
中的分段锁实现。
- 将数据分成多个段,每个段有自己的锁,从而提高高并发场景下的性能,例如
分布式锁实现的效果:
9、redis分布式锁实现原理
SET lock value NX EX 10
lock是key value 是某个值 NX是 互斥 EX是设置过期时间 10是具体时间秒
这是一条命令,之所以不用setnx,然后再设置过期时间两条命令,就无法保证原子性
过期时间之所以需要,因为会存在死锁的可能
如何控制锁的有效时长:
基于redis的setnx命令实现的redisson产品
第二个的while循环的好处就是增加高并发场景下的使用性能,也叫重试机制
执行流程:
redis实现的分布式锁可以实现可重入:
线程唯一id标识
可重入的逻辑:去锁的时候判断,重入的线程是否和获取锁的线程是否是一个线程,是就可以获取到锁,不是就获取不到锁
总结:就是判断是否是同一个线程
上图的代码是可以重入的,好处
一、 业务复杂,锁的粒度比较细,就可以用到锁的重入
二、避免多个锁之间产生死锁的问题
redis实现锁的可重入步骤:
redisson实现的分布式锁-主从一致性
在集群环境下,redisson如何保证分布式锁的主从一致性:
场景:
主节点宕机,选择一个从节点变为主节点,一个新的请求来获取分布式锁,之前的数据没有同步到从节点,导致新的应用也可以加锁成功,出现两个线程同时持有一把锁
解决这个问题,redisson有一个Red Lock 红锁
复杂,性能差,运维繁琐,官方也不推荐,解决方法
第一:主节点宕机导致未同步数据的情况是低概率事件
redis思想:AP
采用CP思想的 zookeeper
总结:
10、主从复制,主从同步流程
主从同步流程:
问题一:如何判断是否是第一次请求?
通过发送的replid是否和自己一致,一致不是第一次,不一致是第一次
问题二:后续发送repl_baklog中的数据就正好是从节点 不多不少 正好需要的那部分数据呢?
通过记录的offset来判断要同步多少数据到从节点
增量同步:
面试问题:
11、哨兵模式,集群脑裂
哨兵模式比主从模式:高可用
脑裂问题:
由于网络原因,导致哨兵任务主节点宕机,选了一个新的主节点,但老的主节点仍在接收客户端消息,当网络恢复,哨兵会将老的master强制将为slave,然后从新的主节点去同步数据,首先清空自己的数据然后同步,这样就会导致数据丢失
解决方法
有两个配置,如果达不到这两个要求,就拒绝客户端的请求,从而解决脑裂问题
总结:
12、分片集群,数据读写规则
海量数据存储
补充:
哈希槽:
在Redis集群中,哈希槽(Hash Slot)是一种将键空间进行分割的机制,用于实现数据分片。Redis Cluster通过将所有可能的键映射到一个固定数量的槽位上,以此来决定每个键应该存储在哪个节点上。
具体来说:
- Redis Cluster预定义了16384个哈希槽。
- 当客户端尝试对某个键执行命令时,集群首先会计算这个键的CRC16校验和值,然后通过取模运算得到一个介于0至16383之间的数字,这个数字就是该键所属的哈希槽编号。
- 每个Redis集群节点都会负责处理一部分哈希槽,不同的节点可以拥有不同数量的哈希槽,但同一个哈希槽不会被分配给多个节点。
- 当客户端向集群发送请求时,集群能够根据键对应的哈希槽ID快速找到负责该槽的节点,从而将命令路由到正确的节点上执行。
通过这种方式,Redis集群实现了数据的水平扩展,当需要添加或删除节点时,只需迁移部分哈希槽的数据即可,而无需进行全量数据迁移,这样既保证了集群的可伸缩性,也确保了在集群状态变化时能够尽可能减少服务中断的时间
CRC16检验
CRC16校验是一种基于循环冗余校验(Cyclic Redundancy Check)的错误检测方法,主要用于数据通信和存储领域。它通过特定的数学算法生成一个16位的校验码(也称为帧检验序列,FCS),这个校验码与原始数据一起发送或存储。
具体过程如下:
-
多项式选择:在CRC16中,预先定义了一个固定长度的二进制多项式(例如,常见的CRC-16-CCITT或CRC-16-IBM等标准使用不同的多项式),其阶数为16,这意味着最高有效位(MSB)和最低有效位(LSB)都为1。
-
除法操作:将要校验的数据看作是一个大数(以二进制表示),然后用这个预设的多项式对数据进行“除法”运算,这里的除法实际上是模2除法,也就是异或(XOR)操作而非普通的算术除法。
-
计算余数:经过上述“除法”后得到的余数就是CRC校验码,将其附在原始数据之后一同发送或存储。
-
接收方验证:当接收方收到数据时,它会重新执行相同的CRC计算步骤,并检查得出的余数是否为0。如果余数为0,则认为传输过程中没有出现错误;如果不为0,则表明数据可能在传输过程中发生了改变,需要重传或采取其他错误恢复策略。
由于CRC16能有效地检测出一定范围内的随机错误以及突发错误,因此在确保数据完整性和正确性方面非常实用,被广泛应用于网络通信协议、文件系统、磁盘驱动器等领域。
总结:
13、redis是单线程的,为什么那么快?
多线程上下文切换:
多线程上下文切换是指在多线程并发执行的环境中,操作系统或运行时环境(如Java虚拟机)将CPU从当前正在执行的一个线程切换到另一个线程去执行的过程。这个过程中包含了对当前线程执行状态的保存和新线程执行状态的恢复。
具体来说,当操作系统决定进行上下文切换时:
-
保存当前线程的上下文:这包括当前线程在CPU寄存器中的所有信息(比如程序计数器PC,表示下一条指令的位置;栈指针SP,用于记录函数调用栈的状态等)、内存地址空间、打开的文件描述符等所有与线程执行相关的内容。
-
恢复目标线程的上下文:操作系统加载下一个要执行线程的上下文,即把该线程之前保存的所有相关信息重新载入到CPU的寄存器和其他硬件状态中。
-
转移控制权:完成上述步骤后,CPU开始执行新的线程,同时旧线程进入就绪队列等待被再次调度。
多线程上下文切换是实现多任务并行处理的关键技术之一,它使得单个CPU看上去能够同时处理多个任务。然而,频繁的上下文切换会带来一定的开销,例如CPU时间消耗于保存和恢复上下文、内存操作以及可能的缓存失效等,这些都可能影响系统的整体性能。因此,在设计多线程应用时需要合理地管理线程数量和同步机制,以降低不必要的上下文切换次数。
IO多路复用模型
IO多路复用(I/O Multiplexing)是一种高效的处理多个文件描述符(例如网络套接字)的I/O事件的技术,它允许单个进程或线程同时监听和处理来自多个描述符的输入输出事件,而无需为每个描述符创建一个单独的线程。通过使用IO多路复用技术,程序可以避免在等待数据时阻塞,并且可以在有事件发生时立即进行响应。
主要的IO多路复用模型包括:
-
select:
select
函数允许开发者监控一组文件描述符集合,查看是否有可读、可写或者错误条件准备好。- 参数
nfds
是要检查的文件描述符集合中的最大值加1。 - 通过三个
fd_set
结构分别表示需要监视的读、写和错误条件的文件描述符集合。 - 提供了一个超时参数,如果在指定时间内没有描述符变为可操作状态,则函数会返回。
-
poll:
- 类似于
select
,但不依赖于文件描述符集合大小的限制,并且提供了更灵活的事件类型检测机制。 - 使用
struct pollfd
数组来跟踪感兴趣的文件描述符及其关注的事件类型。
- 类似于
-
epoll(Linux系统):
- epoll比select和poll更高效,尤其是在大量并发连接的情况下。
- 内部采用红黑树存储被监控的文件描述符,查询效率更高。
- 当就绪事件发生时,epoll不会像select那样每次都全部轮询所有文件描述符,而是只通知那些真正有事件发生的描述符。
- 提供了
epoll_create
、epoll_ctl
(添加、修改、删除对文件描述符的关注事件)和epoll_wait
等接口函数。
这些模型的核心思想都是让操作系统内核代为管理多个文件描述符的状态变化,并在有至少一个描述符准备好的时候唤醒相应的进程或线程,从而实现对多个I/O流的高效并行处理。在高并发服务器编程中,IO多路复用是十分关键的技术手段。
非阻塞IO
非阻塞I/O(Non-Blocking IO)是一种编程模型,它允许应用程序在发起I/O操作后立即返回,而不需要等待该操作完成。这意味着当一个线程调用读取或写入操作时,如果数据没有准备好或者无法立即进行传输,操作系统不会让这个线程进入阻塞状态,而是立即返回一个错误提示(如EAGAIN, EWOULDBLOCK等),表示当前无数据可读或不可写。
在非阻塞模式下,程序可以继续执行其他任务,然后通过循环或者其他机制反复检查是否可以进行IO操作。这种方式通常会配合轮询、事件驱动或者异步通知的机制来得知何时数据已经准备好可以进行实际的读写操作。
例如,在Linux系统中,可以通过设置套接字为非阻塞模式,并结合epoll
(或其他IO多路复用技术如select、poll)来高效地管理多个文件描述符,监听它们的状态变化,一旦有描述符就绪(例如,网络套接字上有数据到达),就会得到通知并可以立即进行数据读取。
非阻塞I/O的主要优势在于:
- 提高了并发性能:单个线程可以处理更多的连接,因为无需为每个连接分配单独的阻塞线程。
- 更好地利用CPU资源:避免了由于大量线程因I/O阻塞而导致的上下文切换开销。
然而,非阻塞I/O也存在挑战,比如编写和维护这种代码相对复杂,需要处理好不断轮询与执行实际I/O操作之间的逻辑,并且可能增加系统的CPU使用率,尤其是在数据准备就绪前频繁检查的情况下。
造成效率不高的原因
一:用户空间需要数据要从内核拿,如果内核没有,就要等待
二:都数据要从内核缓冲区拷贝到用户缓冲区,写数据要从用户-> 内核,这种来回拷贝非常耗时
解决方法就是从这两点出发:
一:
1、阻塞IO模型
2、非阻塞IO
在第一个阶段是非阻塞
在第二个阶段还是阻塞
3、IO多路复用
实现模式:
redis网络模型
IO多路复用:仅仅负责已经就绪的连接
世事件派发:将这些不同的时间派发到不同的事件处理器
仅有两处使用了多线程:仅仅是为了解决I/O,影响性能的永远是IO,在redis网络模型中,IO多路复用和事件派发机制和命令的执行并不是真正影响性能的原因
数据库里面最影响性能的永远是磁盘的读写 ,在处理器就是网络的IO
命令的执行还是主线程进行处理
减少了网络IO对性能的影响
总结:
redis实操课程: