Redis基础redis是什么？一个用c语言实现的非关系型的键值对数据库，数据存储在内存上。redis和MEmcached区别共同点：都是基于内存区别：MEmcached只有key-value的数据结构redis有持久化机制，MEmcached没有，重启或者挂掉数据就没了redis原生就支持集群redis有发布订阅、lua脚本、事务，MEmcached没有redis的优点本质上就三点：高性能、高灵活、高可用高性能内存读写速度快高灵活多种数据结构：Hash存储用户信息，键值对形式，可以单个字段的更新Zset实现排行榜，支持范围查询和排序List还能充当简单的MQ高可用一、完整的持久化机制1.RDB快照：定期生成内存数据的全量快照2.AOF：记录每一条写操作的指令，支持每秒每次操作同步详细见持久化二、集群、主从复制、哨兵读写策略旁路缓存读多写少以DB的数据为准写先更新DB，然后删除cache读先从cache中读，有就返回，没有就DB中返回，然后数据放到cache中缺陷首次请求肯定不在cache里->热点数据提前放写频繁会导致cache频繁删除，命中率变很低强一致场景：更新db的时候同时更新cache，必须加（分布式）锁保证cache里面没有线程不安全问题短期可以不一致：更新db的时候同时更新cache，给缓存一个短的过期时间读写穿透意思在于：应用程序只和缓存交互，不再和数据库交互，而是由缓存和数据库交互侧重cache写先查cachecache中不存在，直接更新DBcache中存在，先更新cache，然后cache自己更新DB读查cachecache中存在，直接返回cache中不存在，先通过缓存组件从DB加载，写到cache里再返回思考：特点是由缓存组件和数据库打交道，而不是应用程序和缓存打交道。异步缓存写入Write Behind Pattern 和 Read/Write Through Pattern 很相似，两者都是由 cache 服务来负责 cache 和 db 的读写。但是，两个又有很大的不同：Read/Write Through 是同步更新 cache 和 db，而 Write Behind 则是只更新缓存，不直接更新 db，而是改为异步批量的方式来更新 db。一致性策略线程1来写，线程2来读先删缓存再更新数据库问题：线程1删除缓存然后更新Mysql中间，网络延迟，此时线程2查询发现缓存没有去Mysql查，查到旧数据，更新到redis中，最后结果是redis一直是旧数据，Mysql一直是新数据写的过程的一半出现的问题解决：延迟双删，也就是说线程1删除缓存更新数据库再删除缓存，实现了最终一致性注意延迟时间，根据业务找到线程2查询+更新redis这个过程的耗时，在这之后延迟删除先更新数据库再删缓存问题：线程2查询数据，redis的数据过期了，去Mysql查，查数据之后更新redis之前，线程1做完了更新Mysql+删除缓存，线程2才更新到redis数据，最后结果是redis一直是旧数据，Mysql一直是新数据概率很小很小，因为 更新Mysql+删除redis<读Mysql+写入redis 不太可能读的过程的一半出现的问题解决：延时双删，更数据库-->删除缓存-->延迟删除缓存删除失败情况增加重试机制：发送异步消息到息队列中，让系统监听消息队列Canal解耦，Mysql数据变动，立马通知到Canal，删除缓存和重试操作都Canal来做数据类型结构List、String、hash、Set、ZsetList：信息流---最新文章String：Hash：对象存储---文章信息Set：无序唯一集合---网站数据统计，比如点赞、转发；共同好友（交集）Zset：同set，加了一个score权重----排行榜Stringkey-value形底层实现：通过int和SDS（简单动态字符串），下面详细的SDS的结构和C的对比struct sdshdr { int len; // 已用长度（比如"abc"的len是3） int free; // 剩余空间（比如buf能存5字节，已用3，free=2） char buf[]; // 存数据的数组（末尾有`\0`兼容C字符串） };SDS保存文本数据，还可以保存二进制数据（\0兼容）；C只存文本SDS获取字符串长度的时间复杂度是O（1）（有len）；C语言字符串不能记录长度复杂度O（N）SDS可以自动扩容，C不可以HashHash 在字段较少且值较小时，底层使用 ziplist (压缩列表) 。这种结构在内存中是连续的，没有指针开销，非常紧凑。当字段增多时，会自动转为 hashtable。S和H区别特性StringHash 数据结构简单的 Key-Value。Value 可以是字符串、整数或序列化的对象。一个 Key 对应一个 field-value 映射表，类似 Java 的 HashMap。内存占用较高。每个 Key 都有额外的元数据开销（如过期时间、LRU 信息等）。较低。多个字段共用一个 Key 的元数据，且小对象会使用 ziplist 压缩存储。操作粒度粗粒度。修改对象需取出整个字符串，反序列化修改后再存回。细粒度。可以独立操作（HSET/HGET）对象中的某个属性。String 的适用场景String 是 Redis 最基础的类型，适合“一存一取”的简单场景。缓存简单对象：如果对象结构固定且总是整体读写（例如：Session 信息、配置信息），直接将对象 JSON 序列化存为 String 最快。大文本/二进制存储：如存储一段 HTML 页面片段或一张图片的 Base64。Hash 的适用场景Hash 适合存储对象，尤其是对象属性经常需要部分更新的情况。存储用户信息/商品详情：例如 user:1001 作为 Key，内部 field 包括 name, age, email。场景：用户只修改了 age，你只需 HSET user:1001 age 25，无需读写整个对象，节省网络带宽。购物车：cart:user_id 作为 Key，sku_id 作为 field，count 作为 value。ZsetZset 类型的底层数据结构是由压缩列表或跳表实现的：如果有序集合的元素个数小于 128 个，并且每个元素的值小于 64 字节时，Redis 会使用压缩列表作为 Zset 类型的底层数据结构；如果有序集合的元素不满足上面的条件，Redis 会使用跳表作为 Zset 类型的底层数据结构；压缩列表场景：我们要找车厢里第一个包裹。定位车厢头部：Redis 找到这块连续的内存区域（火车车厢）。车厢最前面有三个小标签：zlbytes：整节车厢有多长（方便快速跳过它）。zltail：最后一个包裹在哪里（方便快速找尾部）。zllen：车厢里一共有几个包裹。找到第一个包裹：第一个包裹紧跟在这些小标签后面。包裹结构： 每个包裹（Entry）不是裸数据，它也自带两个标签：前一个包裹有多长（prevlen）：这是最关键的！它可以让我们知道从当前包裹往回退多少距离，就能找到前一个包裹，实现了“双向”功能。包裹的内容和长度（encoding + content）：实际的数据。取出数据：Redis 读出第一个包裹的内容（例如："Java"），任务完成！缺点：连锁更新想象一下：包裹 A 比较小，它后面的 包裹 B 用一个 1 厘米的小标签记录了 A 的长度。现在你把 包裹 A 换成一个巨大的包裹。包裹 B 发现它那个 1 厘米的小标签不够写 A 的新长度了，它必须换成一个 5 厘米的大标签。换标签（从 1 变 5）会挤占它后面 包裹 C 的位置。如果 包裹 C 的位置变了，它后面 包裹 D 记录 C 位置的标签可能也要更新...结果就是，一个包裹变大，可能导致后面所有包裹的标签都要更新和移动，浪费大量计算资源。这就是 “连锁更新” 问题。跳表结构：结构查询：查询SDS大小：也就是字典序权重是Zset的Score字段层数增加的方式：跳表的相邻两层的节点数量最理想的比例是 2:1，查找复杂度可以降低到 O(logN)。Redis具体的做法是，跳表在创建节点时候，会生成范围为[0-1]的一个随机数，如果这个随机数小于 0.25（相当于概率 25%），那么层数就增加 1 层，然后继续生成下一个随机数，直到随机数的结果大于 0.25 结束，最终确定该节点的层数。这样的做法，相当于每增加一层的概率不超过 25%，层数越高，概率越低，层高最大限制是 64。image-20251213214155246应用redis 还能干啥？分布式锁：见项目记录限流：Redis+lua实现，消息队列：List可做（加了订阅广播），还有Stream类型延时队列：Redisson内置延迟队列（zset实现）分布式登录的sessionRedis线程模型单线程部分单线程是说Redis的主线程是单线程负责部分：6.0之前是执行所有客户端发来的命令（set之类的），从连接到命令执行都是 6.0之后是把网络IO读写扔给了IO线程池，其他不变 为什么？保证原子性，什么命令都是串行，不需要加锁，避免了线程上下文开销多线程部分多线程是说的IO读写（6.0之后有的）和后台任务的BIO负责部分：1、IO线程池：让多个IO线程并行从多个客户端中读取数据，并行把响应数据返回给客户端 2、异步线程BIO：执行不影响核心的耗时任务---AOP、RDB 为什么？IO的目的是并行处理数据读取实现快速到执行步骤，提高吞吐量，异步BIO线程执行持久化等耗时的操作，防止阻塞主线程执行单线程怎么监听的大量客户端连接的？IO多路复用机制！内核代替应用程序去监控多个 I/O 流主线程向内核注册它感兴趣的所有Socket（包括监听 Socket 和已连接 Socket）当Socket要进行操作的时候， 内核会将这些已就绪的套接字列表返回给 Redis 主线程，并把主线程休眠中唤醒。Redis 6.0 之前为什么使用单线程性能瓶颈不在CPU上，在内存和网络上单线程编程更容易容易维护多线程就存在死锁，线程上下文切换问题Redis 6.0 之后为什么引入了多线程随着网络硬件的提升，Redis的性能瓶颈有时候出现在网络IO上-->万兆网卡之类的所以对于网络IO来说采取多线程客户端读写更快但是！对于命令还是单线程执行的Redis内存淘汰为什么要有内存淘汰就是因为避免 Redis 因为内存耗尽而崩溃触发时机：当触发新内存申请命令的时候，并且当前内存的使用量超过了配置的maxmemory参数。策略：触发时机发生的时候，根据maxmemory-policy这个参数设置的策略进行淘汰，举例几个：1、LRU:最近最少使用算法2、LFU：淘汰最不经常使用的3、随机淘汰4、淘汰过期时间剩的最短的Redis事务Redis是有事务的，通过一些关键词就能开启。用Lua脚本而不是用redis原生事务，原因：特性Redis 事务Lua 脚本逻辑控制不能用中间结果进行判断可以在服务端执行ifelse等原子性实现依赖 WATCH 实现乐观锁，需客户端重试。脚本整体原子执行，无需 WATCH。AOF持久化机制是什么把写操作保存到日志，注意只记录写操作顺序是先执行写操作命令后，才将该命令记录到 AOF 日志。好处：不需要额外的检查的开销---先记录日志万一语法错了还得检查风险：写操作和记录日志是两个操作有前后之分，还没存到硬盘，间隙间宕机，就丢失数据阻塞，如果磁盘 IO 压力很大，写入磁盘的速度变慢，它会阻塞主线程，导致 Redis 无法处理后续的所有请求。这不仅仅是“下一个”操作的问题，而是整个服务的响应延迟增加。能看出来两个风险都跟日志写回磁盘的时候有关，所以详解写回的策略写回策略区别image-20251009175236953过程图image-20251009182959367实际Always策略就是每次写入AOF文件后，就执行fsync ()Everysec策略就是创建一个异步任务执行fsync ()No策略就是永不执行fsync ()重写机制什么时候：自动的话有两个参数调控，手动的话是执行BGREWRITEAOFAOF日志越写越大总得清除，避免越写越大，重写举个例子就是说多个set语句，只保留最新状态对应的set语句到日志里重写完成之后就把这个新的日志文件覆盖掉旧的，实现压缩效果为什么不复写旧的而是写新的覆盖？因为如果重写失败了，就会污染现有的AOF文件重写的过程是由后台子进程完成的，不影响主进程接着用AOF恢复是很慢的，因为Redis命令是单线程的，一条一条执行恢复。RDB持久化机制是什么通过创建快照来存储内存的数据在某一个时间上的副本。怎么用save命令：在主进程生成RDB文件，所以写入RDB时候会堵塞主线程bgsave命令：创建一个子进程来生成 RDB 文件，不堵塞主线程注意：RDB的快照是全量快照，很重，如果频繁干，影响性能，所以一般分钟级设置混合持久化与异同混合持久化4.0之后，可以开启混合持久化，就是把RDB中的内容写到AOP文件开头。缺点就是AOF里面的RDB部分是压缩格式而不是AOF格式，可读性低。异同区别AOFRDB内容存储的每一次写命令，大存储压缩后的二进制数据，小恢复速度一条一条执行，慢解析还原就行，快恢复程度秒级记录，丢的少分钟级记录，丢的多选择丢一点没关系，就用单独RDB丢一些有点伤，建议全开或者混合持久化不单独开AOF，开启RDB有助于快速重启，数据库备份。机制描述默认状态开启方式RDB时间点快照（Snapshot）默认启用自动执行（基于配置）或手动命令（SAVE/BGSAVE）AOF写命令日志（Log）默认关闭主动修改配置文件，设置 appendonly yes主从复制原理见canal同步mysql的原理部分作用1、实现读写分离，从库主要负责读操作，主库主要负责写操作2、数据备份，通过Sentinel实时监控主从节点哨兵监控
基本redis 就是一个用c语言实现的键值对形式的数据库，只不过特殊的地方是数据存储在内存上。redis作用：整体上来说有很多，比如分布式锁，缓存，计数限流等，主要我用过的redis在视频项目中是缓存的作用，在面试项目中是做一个简易的消息队列的作用。redis的数据类型：String，Hash，Zset，Stream 详细见概括内容。redis的读写方式有三种，这个比较核心。第一种：旁路缓存，这个的读写的核心都在于DB，缓存就跟计算机组成的缓存一样作为辅助。读：先从缓存中读，有的话就直接返回，没有就从DB中返回然后放回DB中写：先更新DB，然后删除缓存。第二种：读写穿透，这个读写的核心在于缓存本身，整体的体系是都跟缓存接触，不和DB直接接触。读：先从缓存中读，有的话直接返回，没有的话先从缓存组件从DB中加载到缓存中然后再返回写：!!!怎么保证一致性模拟情况：两个线程一个读一个写先删除缓存再更新数据库在删除缓存和更新数据库之间，读的线程来了，看到了旧数据，然后更新数据到缓存中，这样缓存一直都是旧数据，数据库一直是新数据。解决方法：延迟双删，写线程删除缓存然后更新数据库再删除缓存，就可以了，第二次删除得选在读线程操作完之后删除。先更新数据库再删除缓存读的线程查询数据，但是缓存中的数据过期了，然后就得去数据库查，查完数据库和写回缓存之间，写线程干完了更新数据库然后删除缓存的步骤，这样导致了缓存一直是旧数据，数据库一直是新数据。这种相较于上一种的发生概率很低，因为：更新数据库的操作耗时是这里面最长的，很小概率是在这个间隙就做完了。解决方法：也是延迟双删持久化机制AOF机制AOF就是记录写操作的一个日志。是先进行写操作，然后进行日志记录。有两个风险一个就是因为写操作和写日志是两个操作并不是原子性的，所以可能宕机然后就导致丢失数据。第二个就是阻塞，因为是都在主线程上进行的，流量高的时候就容易阻塞重写的话是写完新的然后覆盖，是重复写不是全量写。RDB机制RDB就是创建一个某一个时间副本上的快照，存储的内存的数据。只有一个问题就是因为是全量写，所以如果时间设定的太频繁了容易卡顿，所以一般是分钟级设置。
IOCIOC就是控制反转。也就是把创建对象管理对象等事情交给spring容器来做。举例：传统的开发就是A依赖于B，那就是A手动new一个对象出来；springIOC就是spring来帮我们实例化对象，我们需要什么对象直接在spring容器里取出来就是了。用一个技术就要看解决了什么问题，优点。而IOC的优点是解决了对象之间的耦合性，不会牵一发而动全身。举例：service依赖dao层无IOC：每次使用dao层实现类的增删改查就需要new这个实现类，如果有新的实现代码还要改业务代码。有IOC：spring的配置时代就改配置的对应实现类就行，springboot的注解时代就是Qualifier注解注明就行。AOPAOP就是面向切面编程。就是把横切的地方从核心业务中分离出来，也是一种解耦。通知就是在哪里触发，切面就是对关注点封装的类。解决什么问题，优点。可以解耦日志通知，接口限流，事务管理，如果一直重复实现这些行为，代码会特别冗余，难以维护。Bean的注入依赖注入常见三种方式：构造函数注入Setter注入Field（字段） 注入：在类的字段上使用注解（如 @Autowired 或 @Resource）来注入依赖项。Bean的实现xml：标签和工厂标签Java配置类：@Configuration+@Bean手动创建bean逻辑注解：@Component、@Repository、@Service等xml 和 java配置类 使用场景在于是第三方的SDK、第三方Jar包，不能自己写注解修改的这种场景注解自然就是自己写的业务代码的逻辑可以用的，同时也是自动装配机制的一环。自动装配原理自动装配就是现在的注解时代的不用写xml文件的核心机制。那这个机制是通过 @SpringBootApplication启动的，而这个注解是三个注解集合：@Configuration、@EnableAutoConfiguration 和 @ComponentScan@Configuration: 就是代表配置类，允许在上下文中注册额外的 Bean 或导入其他配置类。@EnableAutoConfiguration: 启用 Spring Boot 的自动配置机制。它是自动配置的核心，允许 Spring Boot 根据项目的依赖和配置自动配置 Spring 应用的各个部分。@ComponentScan:启用组件扫描。描被 @Component（以及 @Service、@Controller 等）注解的类，并将这些类注册为 Spring 容器中的 Bean。默认情况下，它会扫描该类所在包及其子包下的所有类。EnableAutoConfiguration是核心，下面有两个核心注解：@AutoConfigurationPackage，该注解上有一个注解，其中 Registrar 的作用是将启动类所在包下的所有子包的组件扫描注入到spring容器中。这也是为什么在spring boot中，把项目的controller、pojo、service、dao等包放在启动类同级目录下的原因。@Import(AutoConfigurationImportSelector.class)：这个注解的有一个方法的作用是去找配置文件，该方法去找META-INF/spring.factories文件中的所有自动配置类，并加载这些类。过程两条路，第一条路（手动配置的部分，也就是业务代码，有Component）启动：@SpringBootApplication，标记启动类，开始激活装配扫描：@ComponentScan+@AutoConfigurationPackage，扫描组件（范围）@AutoConfigurationPackage：通过注解导入的Registrar类，注册启动类所在的包是基础包，也就是设置了一个默认的扫描最大范围（执剑人）@ComponentScan：启动组件扫描器，扫描被@Component等注解修饰的类。第二条路（自动配置的部分，去找springboot默认自动配置的配置文件配置，）发现：@Import(AutoConfigurationImportSelector.class)，发现配置清单（位于META-INF/spring.factories）判断：配置清单上的每一个自动配置的类都会有一个 条件注解，用来判断需不需要装配，比如拿数据库举例子： 判断两个事情： 第一是判断有没有数据库的jar包，就是项目用没用数据库； 第二是判断现在项目里有没有DataSource这个配置类，有说明不用自动装配了，已经在第一条路装配了。没有就需要自动装配。注册：这步是把配置类内部的@Bean方法转化成BeanDefiniton注册到Spring容器里面 ----- 这些BeanDefiniton在生命周期里面实例化成Bean实例。循环依赖问题出现的原因：一个类初始化的时候需要另一个类的实例，反过来也是。怎么解决：提前暴露→在A实例化完但是还没依赖注入（属性填充）的时候，直接把A暴露出来给B用IOC容器里面维护了三个Map存储不同阶段的Bean实例 Map名称存储内容作用一级缓存singletonObjects存储完整、初始化完毕的单例Bean。（bean）正常的Bean容器。二级缓存earlySingletonObjects存储提前暴露的Bean实例，可能是代理对象或原始对象。（Bean）存放已暴露但尚未完全初始化的Bean。三级缓存singletonFactories存储一个工厂对象 （逻辑）负责判断要不要代理并且返回给二级缓存实例最开始都在三级。进到a和b开始循环依赖的时候，a会判断需不需要AOP代理，需不需要a都会去二级，然后b就能拿到这个半成品的a，完成初始化。死锁就解开了。不能解决的情况：构造器互相注入的；原型模式的bean。前者不能的原因是通过构造函数注入就意味着这时候**实例化和注入是一个步骤**，因为在java中创建对象唯一的起点就是调用构造函数，调用之后就是实例化，而不能整出来一个空壳。 解决方法是**setter注入、注解注入** 后者不能的原因是三级缓存这个机制只给**单例模式**用。 解决方法是**改成单例 或 手动模拟三级机制**
Spring谈谈Spring框架他是一个开源的轻量级java框架。一般说spring框架都指的是spring framework，是很多个模块的集合，比如AOP，IOC。最核心的思想实际上就是不重新造轮子，开箱即用。列举一些重要的模块？Core Container ：IOC AOP：aspect，aopTest：对单元测试JUnit、Mockito等常用测试框架支持Web：有对MVC的实现，对WebSocket的支持。Spring中Bean的作用域作用域描述singleton容器中唯一的 Bean 实例（默认）。prototype每次获取都会创建新实例。 也就是说，连续 getBean() 两次，得到的是不同的 Bean 实例。request每次 HTTP 请求创建新实例（仅 Web 应用）。session每次 HTTP Session 创建新实例（仅 Web 应用）。application/global-session每个 Web 应用启动时创建一个实例（仅 Web 应用）。websocket每次 WebSocket 会话创建新实例（仅 Web 应用）。注入Bean的方式依赖注入 (Dependency Injection, DI) 的常见方式：构造函数注入：通过类的构造函数来注入依赖项。Setter 注入：通过类的 Setter 方法来注入依赖项。Field（字段） 注入：直接在类的字段上使用注解（如 @Autowired 或 @Resource）来注入依赖项。实现Bean的方式1、xml：标签和工厂标签2、注解：@Component、@Repository、@Service等3、Java配置类：@Configuration+@Bean手动创建bean逻辑Spring事务及失效场景Spring框架事务管理 (@Transactional)作用保证业务层面的原子性功能简化开发 ：开发者无需编写 connection.commit()、connection.rollback()、connection.close() 等手动事务管理代码。只需通过 @Transactional 即可声明事务边界。应用场景commentMapper.insert(comment); (插入评论)videoStatsService.updateStats(...) (更新视频统计)如果第一步成功，但第二步因某种原因失败（如网络、死锁、异常），事务管理器会自动回滚第一步的插入操作，确保数据库中不会出现“评论已插入但统计数未更新”的不一致状态。失效情况​ 1、Spring事务是根据AOP实现的，就是拿到代理对象加入事务逻辑，所以如果调用方式不是通过注入调用的，也就是没有被Spring容器管理，就会失效。​ 比如一个类的一个非事务方法调用事务方法，事务失效，没走容器​ 1、解决：把被调用的事务方法写到一个新service通过注入调用​ 2、方法不是public修饰符​ 3、抛出的异常类型是检查型异常，或者根本没写抛异常语句@Component和@Bean区别特性@Component@Bean作用目标类方法控制权Spring 自动管理开发者手动控制使用条件配合 组件扫描 (@ComponentScan) 使用。必须在带有 @Configuration 或 @Component 注解的类中使用。创建过程spring 通过反射自动创建对象并注入依赖。开发者在方法体内手动实例化对象，并将对象返回给 spring容器。依赖注入@Autowired 或 @Resource 注解。在方法参数中声明依赖，spring 会自动传入。适用场景绝大多数自定义的类。1. 集成第三方库（无法修改源码）。 2. 需要复杂初始化逻辑。 3. 需要基于条件创建 Bean。衍生注解@Service, @Controller, @Repository无SpringBoot三者关系Spring：依赖注入、解耦 (AOP/IOC)MVC：Spring的一个模块，注解开发的核心，比如Controller，requestmapping等SpringBoot：有什么优点？1、提升开发效率：自动配置，起步依赖，开箱即用的功能2、和Spring生态集成3、Spring Boot 能够根据项目依赖自动配置大量的常见组件（如数据源、Web 容器、消息队列等），提供合理的默认设置。同时也允许开发者根据需要轻松覆盖或定制配置，极大减少了繁琐的手动配置工作。4、内嵌web服务器5、适合微服务架构6、Spring Boot 为常用的构建工具（如 Maven 和 Gradle）提供了专门的插件自动装配原理自动装配机制是通过 @SpringBootApplication 启动的，这个注解本质上是三个注解的集合。@Configuration、@EnableAutoConfiguration 和 @ComponentScan@EnableAutoConfiguration: 启用 Spring Boot 的自动配置机制。它是自动配置的核心，允许 Spring Boot 根据项目的依赖和配置自动配置 Spring 应用的各个部分。@ComponentScan:启用组件扫描。描被 @Component（以及 @Service、@Controller 等）注解的类，并将这些类注册为 Spring 容器中的 Bean。默认情况下，它会扫描该类所在包及其子包下的所有类。@Configuration: 允许在上下文中注册额外的 Bean 或导入其他配置类。它相当于一个具有 @Bean 方法的 Spring 配置类。@EnableAutoConfiguration是启动自动配置的关键，先详解一下这个注解：@Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited @AutoConfigurationPackage //作用：将main包下的所有组件注册到容器 @Import({AutoConfigurationImportSelector.class}) //加载自动装配类xxxAutoconfiguration public @interface EnableAutoConfiguration { String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration"; }他又有两个注解：@AutoConfigurationPackage，该注解上有一个注解@Import(AutoConfigurationPackages.Registrar.class)，其中 Registrar 类的作用是将启动类所在包下的所有子包的组件扫描注入到spring容器中。这也是为什么在spring boot中，把项目的controller、pojo、service、dao等包放在启动类同级目录下的原因。@Import(AutoConfigurationImportSelector.class)：其中AutoConfigurationImportSelector类中有一个getCandidateConfigurations()方法，他的作用是去找配置文件，该方法通过SpringFactoriesLoader.loadFactoryNames()方法查找位于META-INF/spring.factories文件中的所有自动配置类，并加载这些类。image-20251017182542430过程启动：@SpringBootApplication，标记启动类，开始激活装配扫描：@ComponentScan+@AutoConfigurationPackage，扫描组件（范围）@AutoConfigurationPackage：通过注解导入的Registrar类，注册启动类所在的包是基础包，也就是设置了一个默认的扫描最大范围（执剑人）@ComponentScan：启动组件扫描器，扫描被@Component等注解修饰的类，默认根据上面的Registrar类发现：@Import(AutoConfigurationImportSelector.class)，发现配置清单（位于META-INF/spring.factories）判断：配置清单上的每一个自动配置的类都会有一个@ConditionalXXXX这样的注解，用来判断需不需要装配，比如拿数据库举例子：@ConditionalOnClass(DataSource.class)：判断有没有DataSource这个类，没有就不用加载数据库相关的自动配置的逻辑@ConditionalOnMissingBean(DataSource.class)：判断是不是自己手动配置（@Bean），不是再帮你自动配置两个一般一起用注册：ConfigurationClassPostProcessor类（继承于BeanFactoryPostProcessor类），解析通过上面的判断的自动配置类，然后把配置类内部的@Bean方法转化成BeanDefiniton注册到Spring容器里面 ----- 这些BeanDefiniton在生命周期里面实例化成Bean实例关系自动装配是通过@SpringBootApplication启动的，而这个注解有三个注解@Configuration、@EnableAutoConfiguration 和 @ComponentScan@SpringBootApplication@Configuration：标明启动类本身是一个配置类@ComponentScan：启动组件扫描@EnableAutoConfiguration：启动自动配置，进入逻辑@AutoConfigurationPackage，该注解上有一个注解@Import(AutoConfigurationPackages.Registrar.class)@Import(AutoConfigurationImportSelector.class)Spring的循环依赖问题出现原因一个类初始化的时候需要另一个类的实例，反过来也是框架怎么解决的spring默认是支持自动解决单例的循环依赖问题的关键在于提前暴露：在A实例化完但是还没依赖注入（属性填充）的时候，直接把A暴露出来给B用三级缓存 IOC容器里面维护了三个Map存储不同阶段的Bean实例 Map名称存储内容作用一级缓存singletonObjects存储完整、初始化完毕的单例Bean。（bean）正常的Bean容器。二级缓存earlySingletonObjects存储提前暴露的Bean实例，可能是代理对象或原始对象。（Bean）存放已暴露但尚未完全初始化的Bean。三级缓存singletonFactories存储一个工厂对象 （逻辑）负责判断要不要代理并且返回给二级缓存实例A开始创建（A到3级）暴露A的工厂：实例化A但是在DI之前，把ObjectFactory放到三级缓存里，来负责A的早期引用A的属性注入： A开始进行属性注入，发现依赖B，于是开始查找B。B开始创建B的属性注入： B开始进行属性注入，发现依赖A，于是开始查找A。查找A（三级缓存触发）： B尝试从一级缓存、二级缓存查找A，都未找到。它会访问三级缓存，发现A对应的 ObjectFactory。（A到2级）工厂执行： B执行这个 ObjectFactory。Factory会判断：A是否需要AOP代理？如果需要代理，Factory会生成A的代理对象。如果不需要代理，Factory直接返回A的原始对象。无论返回什么， 这个对象都会被放入二级缓存 ，并从三级缓存中移除。（B从2级拿的A）B获取A并初始化： B拿到A的半成品，完成自己的属性注入，并完成初始化。此时B是一个完整的Bean。A完成初始化： B完成后，Spring回到A的初始化流程。A拿到完整的B，完成自己的属性注入。（A到1级）A最终入库： A完成所有初始化步骤（包括 init 方法、AOP代理创建等），最终将完整的A放入一级缓存 (singletonObjects)，并从二级缓存中移除。总结： 第三级缓存 (singletonFactories) 是一个缓冲层，它将生成早期Bean实例的决定权延迟到了真正需要解决循环依赖的那一刻，从而确保了AOP代理的正确性和完整性。情况总结场景分类AOP 需求注入方式循环依赖最少所需的缓存级别原因正常 Bean 创建有/无-无问题一级缓存仅需最终存储完整的 Bean 实例。循环依赖 (简单)均无 AOPSetter/Field能解决二级缓存仅需提前暴露原始实例，无需 AOP 决策逻辑。循环依赖 (复杂)至少一方有 AOPSetter/Field能解决三级缓存需要 ObjectFactory 延迟 AOP 代理的生成和决策，确保早期引用是 AOP 后的对象。循环依赖 (无效)有/无Constructor不能解--无法在实例化前提前暴露实例。框架不能解决的构造器互相注入的为什么？因为构造器创建bean的时候DI和实例化的是一个步骤（调用构造器方法）过程：尝试实例化 A 时发现需要 B，转而实例化 B；实例化 B 时发现需要 A，又转而实例化 A。死锁解决：a.改为Setter 注入或者@Resource注解 b.在其中一个Bean对象的构造函数参数上使用@LazyPrototype作用域的为什么？三级缓存是为单例模式设计的过程：同上解决：a.改为单例 b.手动模拟三级缓存注入 ObjectProvider 或 Provider，而非 B 实例。在需要 B 时，手动调用 getObject() 或 get() 获取新的原型实例。Bean的生命周期承接自动装配的第 5 步，Spring 的 BeanFactory 中已经拥有了所有需要的 BeanDefinition：用户通过 @Service, @Repository 等注解定义的 BeanDefinition。Spring Boot 自动配置通过 @Bean 方法生成的 BeanDefinition。1. 实例化（Instantiation）容器根据 BeanDefinition 使用反射创建 Bean 对象的原始实例。下面的两个情况被实例化。对于 XML 配置，Spring 从 <bean> 标签中读取 class 属性对于注解配置，Spring 通过 @Component 等注解找到对应的类2. 属性赋值（Populate Properties）在实例化后，Spring 容器将为 Bean 注入配置的属性值和依赖的其他 Bean。XML 配置中的 <property> 或 <constructor-arg> 标签注解配置中的 @Autowired、@Resource 等注解setter 方法注入或构造函数注入3. 初始化（Initialization）初始化阶段是 Bean 准备就绪前的最后一步，包含多个回调方法的执行。按照执行顺序：3.1 Aware 接口回调Spring 提供了一系列的 Aware 接口，用于让 Bean 获取 Spring 容器的特定资源：BeanNameAware：通过 setBeanName(String name) 方法获取 Bean 在容器中的名称BeanClassLoaderAware：通过 setBeanClassLoader(ClassLoader classLoader) 方法获取加载 Bean 的类加载器BeanFactoryAware：通过 setBeanFactory(BeanFactory beanFactory) 方法获取 BeanFactory 容器实例ApplicationContextAware：通过 setApplicationContext(ApplicationContext applicationContext) 方法获取 ApplicationContext 容器实例3.2 BeanPostProcessor 前置处理BeanPostProcessor 接口的 postProcessBeforeInitialization(Object bean, String beanName) 方法在初始化前被调用，可以对 Bean 进行加工处理。3.3 初始化回调InitializingBean 接口：实现该接口的 Bean 会调用 afterPropertiesSet() 方法自定义 init-method：通过 XML 的 init-method 属性或 @Bean 注解的 initMethod 属性指定的初始化方法3.4 BeanPostProcessor 后置处理BeanPostProcessor 接口的 postProcessAfterInitialization(Object bean, String beanName) 方法在初始化后被调用，可以对初始化完成的 Bean 进行最后的加工处理。4. 使用（In Use）初始化完成后，Bean 就可以被应用程序使用了。单例（Singleton）Bean：容器启动时就完成实例化和初始化，一直存在直到容器关闭原型（Prototype）Bean：每次请求时创建新实例，使用完后由客户端负责销毁5. 销毁（Destruction）当容器关闭时，对于单例 Bean，会执行销毁操作：DisposableBean 接口：实现该接口的 Bean 会调用 destroy() 方法自定义 destroy-method：通过 XML 的 destroy-method 属性或 @Bean 注解的 destroyMethod 属性指定的销毁方法谈谈对IOC的理解（是什么）IOC也就是控制反转，要谈谈IOC，就要先说一下控制反转是什么。传统的开发方式就是A依赖于B，那就是A 手动new一个B的对象出来；而IOC的方式是不需要new对象，而是通过IOC容器也就是Spring来帮助我们实例化对象，需要什么对象，就去IOC容器里取出来就行。所以说控制反转的意思就是“把对象创建管理什么的 权利交给外部环境IOC”。（为什么）（好处）那为什么要使用IOC？解决了什么问题？对象之间的耦合度变低，也可以说依赖程度；资源容易管理，比如使用spring容器很容易实现一个单例，而传统模式得自己写。没有IOC是这样的：Service 层想要使用 Dao 层的具体实现的话，需要通过 new 关键字在UserServiceImpl 中手动 new 出 IUserDao 的具体实现类 UserDaoImpl（不能直接 new 接口类）。image-20251123204339925有IOC是这样的：image-20251123204523971IOC的初始化流程IOC 初始化 = 准备工作 + 自动装配(找图纸) + 注册拦截器 + Bean 生命周期(造实物)。准备工作开启工厂 BeanFactory。自动装配自动装配的流程，产生BeanDefinition。注册拦截器AOP 拦截器：发现你的类上有 @Transactional（事务）或 @Aspect，它就在生命周期最后一步帮你生成动态代理自动注入拦截器：专门负责识别 @Autowired 注解，并在“属性填充”阶段把你要的 Bean 塞进去。Bean 生命周期最后走一遍生命周期生成Bean实例bean的生命周期里的是执行拦截器谈谈对AOP的理解（是什么）aop就是面向切面编程，主要的思想就是把横切关注的地方从核心业务逻辑分离出来，切面Aspect：对关注点封装的类。通知类型有哪些？Before前置通知：在方法调用之前触发After后置通知：在方法调用之后触发AfterReturning返回通知：在方法调用完成，返回返回值之后触发AfterThrowing异常通知：在方法调用中抛出异常后触发Around环绕通知：可以在任意前后触发，还可以直接拿到目标对象，要执行的方法（为什么）为什么要用AOP？解决了什么问题？比如日志记录、事务管理、权限控制、接口限流等，如果一直重复实现这些行为，代码会特别冗余，难以维护。// 日志注解 @Target({ElementType.PARAMETER,ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface Log { /** * 描述 */ String description() default ""; /** * 方法类型 INSERT DELETE UPDATE OTHER */ MethodType methodType() default MethodType.OTHER; } // 日志切面 @Component @Aspect public class LogAspect { // 切入点，所有被 Log 注解标注的方法 @Pointcut("@annotation(cn.annotation.Log)") public void webLog() { } /** * 环绕通知 */ @Around("webLog()") public Object doAround(ProceedingJoinPoint joinPoint) throws Throwable { // 省略具体的处理逻辑 } // 省略其他代码 }@Log(description = "method1",methodType = MethodType.INSERT) public CommonResponse<Object> method1() { // 业务逻辑 xxService.method1(); // 省略具体的业务处理逻辑 return CommonResponse.success(); }（怎么实现的？）spring AOP就是基于动态代理实现的，我详细聊聊Spring AOP 的实现机制Spring AOP 的实现机制A.如何实现剥离？一共有三个对象，真实对象（Target）、代理对象（Proxy）、增强逻辑（Advice）工作流程：接收到方法调用。执行增强逻辑（如 @Before 的通知）。将调用转发给真实对象。执行增强逻辑（如 @After 的通知）。返回结果。B.机制一：JDK动态代理（基于接口）条件：至少实现了一个接口为什么至少实现了一个接口？因为JDK动态代理生成的类已经继承了Proxy类，由于单继承，所以它只能通过实现接口的方式实现代理原理：利用了Java原生的API Proxy InvocationHandler运行的时候动态生成一个代理类，这个类和真实对象是兄弟关系，实现同一个接口通过实现InvocationHandler插入增强的逻辑调用代理对象，走的增强逻辑C.机制二：CGLIB动态代理（基于继承）条件：无（没实现接口只能用它）原理：利用字节码运行的时候操作字节码，生成一个代理类，这个类和真实对象是父子关系子类会重写目标中所有非final的方法重写的方法中加入增强逻辑为什么不能重写final方法？因为final方法不能重写public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {// 1. 执行前置增强 (如 @Before)beforeAdvice();try {// 2. 调用真实目标对象的方法result = method.invoke(targetObject, args);// 3. 执行后置增强 (如 @AfterReturning)afterReturningAdvice(result);} catch (Exception e) {// 4. 执行异常增强 (如 @AfterThrowing)afterThrowingAdvice(e);throw e;} finally {// 5. 执行最终增强 (如 @After)afterFinallyAdvice();}D.总结JDK动态代理限制在于只能代理接口定义的方法，CGLIB动态代理限制在于不能代理final类、方法。Spring的默认：智能选择，有接口JDK动态代理，没有接口就CGLIBSpringBoot2.x以上版本：默认CGLIB-->为了避免不必要的报错(比如注入实现类)什么场景我们会强制使用CGLIB？一、希望代理没在接口定义的方法 二、希望代理类就是目标类的类型@Autowired直接按照实现类类型注入所以一般都用@resource按类型如果使用JDK代理，生成的代理对象只实现了接口，并不是实现类类型，会导致注入失败。而CGLIB代理生成的代理对象是目标类的子类，可以同时匹配接口类型和具体类类型。
存储引擎InnoDB和MyISAM区别InnodBmyisam事务支持不支持索引聚集索引非聚集索引（见B+树部分）锁最小是行锁最小是表锁count不保存行数保存，查行数很快恢复支持redo不支持索引索引是什么？就是数据库的目录索引的优缺点优点：快速检索，降低磁盘IO缺点：索引可能失效、占用额外的存储空间索引的分类聚集索引：数据的物理顺序和列值的逻辑顺序相同（主键）查询的时候，因为聚集索引 B+ 树的叶子节点存储了完整数据，直接查就行非聚集索引：就是顺序没关系（除了聚集就全是非聚集）查询的时候，因为非聚集索引 B+ 树的叶子节点存储的是 索引值 和 对应主键值1、需要先在这个树找到该行的主键值2、使用主键值，去聚集索引 B+ 树二次查找这个过程就叫做「回表」唯一索引：用UNIQUE INDEX变成唯一索引，唯一索引是非聚集索引的一种，作用在于主要用于保证这个索引数据的业务唯一性。覆盖索引：只要你查询的字段（SELECT 后面的列），全部被包含在你使用的索引中，就叫覆盖索引。特点在于不需要回表了直接在非聚集索引树上就能返回。索引的数据结构是树，因为树天生有二分查找的特性，查询速度高索引失效索引失效就是违反了数据库使用索引的原则，转变成了全表扫描查询条件类1、Like通配符前置LIKE '%关键字这个没什 是左侧顺序匹配的，相当于词典，只给右边的字母显然不能找到索引。！！！！2、最左前缀原则！！！！！在使用联合索引的时候，只有带着最左边的索引成分的时候，索引才能正常使用。索引失效的时候，查询的性能就变成了全表扫描，效率慢。CREATE INDEX idx_col ON table (a, b, c);a是最左边WHERE a = 1 AND b = 2可以，因为有a，索引成立WHERE b = 2 AND c = 3不可以，因为没有a，索引失效为什么：比如说三级索引，firstname、lastname、age索引字段对应书架分类作用first_name (第一级)大区域标签决定了所有书籍的宏观顺序。last_name (第二级)区域内小隔断只有在大区域（first_name）相同的书堆中，才按照 last_name 排序。age (第三级)最小的书签只有在前两级都相同的情况下，才按照 age 排序。 如果没有第一级的索引firstname，那通过lastname或age查询，是用不上索引的，因为第二级的索引只有在第一级索引的这个大区域下才有序，全局是无序的，所以失效了范围查询的时候：如果abc联合索引，在这个查询中，a 索引通过 IN 语句生效，用于多点定位。b 索引虽然生效，但它是一个范围查询。在联合索引中，一旦遇到范围查询，B+树的加速查找能力就会中断。因为 b 已经是范围，后续字段c在这个范围内是无序的，无法继续利用索引的排序特性来加速定位，因此c索引失效。3、否定条件比如 ！= not in，范围太大，使用索引成本高于直接进行全表扫描。操作索引类1、对索引进行运算where DATE(time) = '2025-1-1'因为这相当于对索引列的所有数据都计算一遍，等同于全表扫描。2、索引隐形转换比如id是int，结果和string类型比较，内部转换，同1。联合索引的存储方式(first\_name, last\_name, age, id)，查然后回表联合索引的 B+树最关键的一点在于，它的所有叶子节点（也就是数据块）是严格按照从左到右的字段顺序进行物理排序的。MYSQL索引为什么使用 B+树？平衡二叉树AVL：AVL实现平衡主要是靠旋转，旋转是非常耗时的，所以不能用（增加和删除）红黑树：和AVL相比，红黑树不追求完美的平衡，只是大致的平衡。对比AVL，查询略降，删除大提高因为红黑树同时引入了颜色，当插入或删除数据时，只需要进行 O(1)次数的旋转以及变色就能保证基本的平衡，不需要像 AVL 树进行 O(lgn)次数的旋转。总的来说， 红黑树的统计性能高于 AVL。红黑树应用：HashMap链表+红黑树解决哈希冲突 ，TreeMap 红黑树存储排序键值对在“高且瘦”的树中，你每次比较都可能需要访问一个新的节点。如果每个节点都位于磁盘的不同页（Page）上，那么查找一个数据就可能需要很多次磁盘I/O。比如树高20，就需要最多20次I/O，这无法接受。B树：矮胖树，一个节点可以有多个子节点，每个节点都会存储数据本身，节点内的键值是排好序的，节点之间也满足排序关系。B+树直接看和B树的区别数据存储：B+树的Data只存在于叶子节点中，非叶子节点只有索引。（B树什么节点都存数据）由于B+树非叶子节点不存Data，所以每个节点能存的索引更多，树更矮。B 树的非叶子节点结构大致是：[P_1, (K_1, D_1), P_2, (K_2, D_2), ]它在每个节点中，不仅要存储 Key（索引），还要存储 Data（数据）。B+ 树的非叶子节点结构大致是：[P_1, K_1, P_2, K_2, ]它只存储 Key（索引）和指向子节点的指针。索引（Key）： 通常是固定长度或较短的数值类型（例如，一个 BIGINT 类型的主键大约是 8 字节）。数据（Data）：在聚簇索引中，数据是一整行记录，可能包含几十个甚至上百个字段，大小通常从几百字节到几 KB 不等。Data，可能是行的全部数据（如 Innodb 的聚簇索引），也可能只是行的主键（如 Innodb 的辅助索引），或者是行所在的地址（如 MyIsam 的非聚簇索引）。叶子结点：B+树中有叶子节点有指针，叶子节点之间通过指针链接起来形成一个有序链表（B树叶子节点是相互独立的）因此，B+树优势：数据存储→1、因为树的结构更矮胖，磁盘IO次数少（读写）叶子节点→2、因为叶子节点是通过双向链表连接的，查询到一条数据就可以沿着链表的上下直接顺序查询，IO路径规律，范围查询效率更高SELECT * FROM users WHERE age BETWEEN 20 AND 30IO路径规律——>磁头不需要大动叶子节点→3、因为双向链表和Data存于叶子，全表遍历效率高，只需要遍历链表就行叶子节点→4、因为Data都存在于叶子节点里面，最差的情况就是一般情况，查询性能稳定。SQL注入允许攻击者将恶意的 SQL 代码插入到输入字段中，以操纵数据库。 核心原因：程序将用户输入的数据未经充分检查或处理，直接拼接到SQL语句中。预编译（参数化查询），用户输入始终视为 字符串/数字，而非可执行代码。 MyBatis：务必使用 #{} 语法（参数化查询），禁止使用 ${}（字符串替换，等同于拼接）。锁表级锁和行级锁的区别qubie表级锁行级锁范围锁整张表锁当前字段实现简单，加锁快困难，加锁慢与引擎无关有关，因引擎谁有MYISAM、InnoDBInnoDB（默认行）InnoDB的几种表级锁表锁表级读锁：阻止其他的对表的写表级写锁：阻止其他对表的读写元数据锁MDL作用：防止执行CRUD的时候表结构变更数据库本身会加MDL，不用我们来操作写锁获取优先级高于读锁对一张表进行 CRUD 操作时，加的是 MDL 读锁；对一张表做结构变更操作的时候，加的是 MDL 写锁；那它在什么时候释放？-->在事务提交之后意向锁作用：为了快速判断表里是否有行锁为什么要判断？因为读锁和行锁是读写互斥，写写互斥的在加上行共享锁之前，先在表级别加上一个“意向共享锁”在加上行独占锁之前，先在表级别加上一个“意向独占锁”这里面谈到普通的select语句是不算里面的，这类是通过MVCC实现的一致性，无锁InnoDB的几种行级锁锁的兼容性分类共享锁S：读锁，事务在读取记录的时候获取共享锁，允许多个事务同时获取锁排它锁X：写锁，事务在修改记录的时候获取排它锁，不允许多个事务.....。锁的范围分类记录锁：锁定单个记录。作用在于：UPDATE，DELETE等操作的写锁实现-->解决不可重复读的。间隙锁：锁定一个id数字集合（范围），不包括记录本身。作用在于：INSERT操作的写锁实现-->解决幻读。前开后开临键锁：记录锁+间隙锁，锁定一个范围，包含记录本身。作用在于：引入这个锁是因为为了在 RR 级别下，通过一套组合拳同时解决不可重复读（靠记录锁）和幻读（靠间隙锁）。前开后闭什么时候加行级锁？普通的 select 语句是不会对记录加锁的，MVCC实现一致性首先记住，加锁的对象都是索引！！！，基本单位就是默认的next-key lock唯一索引等值查询情况查询的记录 存在，定位到记录之后，退化成 X记录锁查询的记录 不存在，定位到第一个大于查询记录的记录之后，退化成 间隙锁，锁住这个记录和上一条记录的范围思考：不沿用next-key lock的原因是因为这个场景下幻读只需要间隙锁就能解决了为什么 id = 5 记录上的主键索引的锁不可以是 next-key lock？如果是 next-keylock，就意味着其他事务无法删除 id = 5 这条记录，但是这次的案例是查询 id = 2 的记录，只要保证前后两次查询 id = 2 的结果集相同，就能避免幻读的问题了。唯一索引范围查询情况先说明范围查询的整体机制：会对每一个扫描到的索引加next-key lock，下面细分情况实际在 Innodb 存储引擎中，会用一个特殊的记录来标识最后一条记录，该特殊的记录的名字叫supremum pseudo-record大于情况：往后推，前加一个包含自己的，后面以此递归最后扫到supremum加 (xxxx, +∞]大于等于情况：等值的地方变成记录锁小于或小于等于情况：条件值不在表里，终止扫描记录的Next-Key锁退化成间隙锁小于情况：条件值在表里，终止扫描记录的Next-Key锁退化成间隙锁小于等于情况：条件值在表里，终止扫描记录的Next-Key锁不退化成间隙锁记忆！查询类型关键逻辑加锁结果 (以 id=30 为界)1. 大于 (> 20)正常向后扫，直到 Supremum(20, 30], (30, 40], (40, +∞]2. 大于等于 (>= 20)起点等值优化：20 变记录锁[20, 20], (20, 30], (30, 40]...3. 小于不在表内 (< 25)扫到 30 发现不满足，终点优化(Previous, 10], (10, 20], (20, 30) (退化)4. 小于在表内 (< 30)扫到 30 发现不满足，终点优化(Previous, 10], (10, 20], (20, 30) (退化)5. 小于等于在表内 (<= 30)扫到 30 刚好满足，无优化(Previous, 10], (10, 20], (20, 30] (标准锁)什么时候加表级锁？当执行UPDATE、DELETE的语句的时候，WHERE没有命中索引或者索引失效的时候，就会对全表扫描加锁。事务四大特性原子性：事务是最小的执行单位，不能分割 --- undo log保证的隔离性：并发访问数据库的时候，事务彼此之间是不可见的 -- 事务隔离级别来调控比如先后扣款，先扣款事务还没做完，后扣款事务来了看到的也是原数量持久性：一个事务被提交之后，他对数据库的改变是永久的，即使数据库发生故障。 --- redo log保证的一致性：执行事务前后，数据保持一致（指变化总和）并发事务带来的问题（问题）脏读一个事务读取数据并且修改了这个数据，这个时候第二个事务读取了这个未提交的数据，但是第一个事务突然回滚，导致改后的数据没有到数据库里面，最后第二个事务读到了脏数据丢失修改一个事务在读取一个数据的时候，另一个事务读取这个数据，第一个事务修改这个数据，第二个数据也修改这个数据，但是数据最后相当于只修改了一次，丢失了一次修改性质同脏读一样。不可重复读指一个事务在事务内多次读一个数据，但是在中间间隙中来了第二个事务，修改了数据，导致第一个事务在事务内前后两次读到的数据不一样幻读意思就是一个事务读了几条数据，在他第二次读之前中间来了第二个事务插入了几条数据，导致第一个事务再读的时候像出现了幻觉一样多了几条数据不可重复读、幻读的区别实际上幻读就是不可重复读特殊情况，不可重复读强调某一条数据被修改了，幻读强调对数据行数的修改上（变多或者变少）之所以区分是因为解决他们的方案不一样标准事务隔离级别（解决）是用来平衡隔离性和并发性能的东西级别越高，数据一致性越好，但并发性能可能越低。读取未提交：允许读取未提交的数据变更，啥都防不住读取已提交：允许读取并发事务已经提交的数据，可以阻止脏读可重复读：对同一个字段的多次读取都是一样的，除非是自身修改，阻止了脏读和不可重复读，至于幻读，仍有可能（InnoDB大幅减少）一旦事务开始，它就仿佛进入了一个“时间凝固”的快照中*可串行化：最高隔离，依次执行，全都防住READ-UNCOMMITTEDREAD-COMMITTEDREPEATABLE-READSERIALIZABLEMysql的默认隔离级别Mysql InnoDB引擎 的默认隔离级别是可重复读。怎么在RR级别解决大部分幻读的快照读：普通的SELECT的语句，通过MVCC实现，事务启动时创建一个数据快照，后续的快照读都读取这个版本的数据，这样避免了看到其他事务插进来的行（幻读）和修改的行（不可重复读）当前读：除了普通查询之外的操作读取到的都是最新数据而不是快照数据，InnoDB用临键锁next-key lock防止其他事物在这个范围内插入新的记录（幻读）（像 SELECT ... LOCK IN SHARE MODE（加S锁），SELECT ... FOR UPDATE，INSERT， UPDATE， DELETE（加X锁） 这些操作）正因为这样的机制，InnoDB在可重复读（RR）级别的性能，在只读或者读多写少的情况下，和读取已提交（RC）的性能差不多。在写密集的时候，RR的间隙锁可能会导致它比RC出现更多的锁等待小部分未解决的特殊的转换操作： 如果事务 A 先快照读，随后事务 B 插入并提交了一条记录。接着事务 A 更新了这条本不存在（在快照里看不见）的记录，这会导致该记录的 trx_id 变成事务 A 的 ID，从而使其在 A 的下一次快照读中变得可见。（因为ReadView对于自己的修改是可见的）时刻事务 A (RR)事务 BT1BEGIN; T2SELECT * FROM t WHERE id=5; (查不到) T3 INSERT INTO t(id) VALUES(5); COMMIT;T4UPDATE t SET name='new' WHERE id=5; T5SELECT * FROM t WHERE id=5; (幻读出现了！查到了 ID=5 的行) Mysql的隔离级别怎么实现的由锁和 MVCC 共同实现的读取未提交：啥也不用干（实际上InnoDB根本不支持，设置也会自动变成读取已提交）读取已提交：通过Read View实现的，实际上是高级的版本号可重复读：通过Read View实现的，实际上是高级的版本号可串行化：完全依赖于锁Read View是如何工作的？是什么东西？他有四个字段：m_ids是一个活跃事务id列表min_trx_id活跃事务中id最小的事务max_trx_id是活跃事务最大id的下一个事务的id值，也就是最大事务id+1creator_trx_id是创建这个Read View的事务id活跃事务：运行中且未提交的事务怎么解决并发的RR可重复读实现一个事务进行快照读的时候，会先创建一个Read View来判断在做后续的操作的时候，其他事务自己能不能可见，这里有个先后顺序，也就是说先创建快照，再执行查询。有几种情况：（trx _id是其他事务的id）如果记录的trx_id小于Read View中的min_trx_id，说明这个事务是在这view版本创建的时候就已经结束了。可见如果记录的trx_id大于等于Read View中的max_trx_id，说明了这个事务是在View 快照之后才开始的事务。不可见如果记录的trx_id在min_trx_id和max_trx_id之间，先判断trx_id在不在m_ids中在：表示生成该版本的记录的活跃事务还没提交，所以不可见（还在忙活，可能回滚）不在：表示生成该版本的记录的活跃事务已经提交，所以可见（忙活完了，间隙进来的） 在创建快照和执行查询的间隙进来的注：RR下快照读的所有事务问题都解决了image-20251008212707053RC读以提交实现创建快照是同一个事务但是每次执行SELECT都创建一个read view快照，有不可重复读的情况，因为每次都是新快照。MVCC是什么上面这个版本链条控制并发事务同时访问一个记录的解决方案就是MVCC，也就是：版本链（Version Chain）+读视图（Read View）。MVCC回答概述版本链：首先，在InnoDB中，每一行数据都有两个隐藏列：trx_id（最后修改该行的事务ID）和roll_ptr（回滚指针）。通过回滚指针，InnoDB将一行数据的多个历史版本在undo log中串联起来，形成版本链。读视图：其次，当一个事务发起快照读时，MVCC会根据该事务的隔离级别（读已提交或可重复读）生成一个Read View。这个Read View定义了一套可见性规则，事务会用这个Read View去匹配版本链，从而找到对当前事务可见的那个特定版本的数据。”有锁为什么还要MVCC单纯依赖锁，性能实在太差了。互斥互斥等待等待。“单纯使用锁机制，并发性能会很差。即使是读写锁，读和写操作之间仍然是互斥的。数据库作为高性能中间件，如果一个写操作就导致所有读操作被阻塞，这种性能损失是无法接受的。因此，InnoDB引擎引入了MVCC，其核心目的就是通过空间换时间的方式，实现读写操作的并发执行，极大地提升了数据库的并发处理能力。InnoDB 对 MVCC 的实现undolog和readview的配合：当 Read View 判断当前数据不可见时，它必须通过 Undo Log 提供的版本链，去找到一个它认为可见的历史版本。可重复读：启动事务时生成一个 Read View，整个事务期间都用这个，记录相应的undo log用链表连起来，读只能读到启动之前的记录读提交：事务读取的时候创建Read View三大日志binlog二进制日志Server层的日志，逻辑日志，用来全量数据备份和主从复制。写入时机在每完成一次更新操作之后，生成一条binlog刷盘时机什么时候刷盘由sync_binlog参数决定0：不去强制要求，由系统自行判断何时写入磁盘；1：每次事务提交后，都要将binlog写入磁盘；N：每N个事务，才会将binlog写入磁盘。方案STATEMENT：记录数据变更的SQL语句本身好：日志文件体积小，一SQL记一次坏：可能存在数据不一致，对于动态函数---NOW()UUID()LIMIT等依赖上下文，会导致主从数据不一致ROW：记录数据最终被修改成什么样好：数据一致性最高，绝对一致主从坏：日志文件体积大，更新多少行就记多少MIXED：折中判断特点追加写，写满一个文件，就创建一个新的文件继续写，不覆盖，存全量日志--->恢复，主从复制注意的是只记录库、表结构改变和数据修改的日志redolog重做日志存储引擎层面的，物理日志，用来保障事务的持久性和崩溃恢复Mysql有Buffer Pool缓冲池，跟redis一样。如果在Buffer Pool中的页的修改还没持久化到磁盘中，这个时候宕机了，事务更改就消失了引入WAL原则（预写日志），就是先写到日志，再写到磁盘上image-20251010110214968写入时机当执行一条更新语句（如 UPDATE）时，先在 Buffer Pool 中查找对应的数据页。如果不在，就从磁盘加载到 Buffer Pool。接着，修改 Buffer Pool 中的这个数据页。此时，这个被修改了但还没刷回磁盘的数据页，就是所谓的 “脏页”。标记脏页的同时，InnoDB把这个页面做了什么物理修改记录记录到Redo Log Buffer中每一次数据变更都会产生对应的redo log例如：在表空间xx文件、第xx页、偏移量xx的位置写入了xxx数据刷盘时机事务提交时，先把 Redo Log Buffer 中对应的所有日志记录，强制刷盘，写到redolog中， 此时，现在已经算是被持久化了。（由参数调控）trx_commit = 1：每次事务提交时，都必须立刻将 redo log 从内存刷入磁盘。（默认）trx_commit = 0：不保证事务提交时刷盘，而是大约每秒钟由后台线程将 redo log 从内存刷入磁盘一次。不安全trx_commit = 2：每次事务提交时，只把 redo log 从内存写入到操作系统的页缓存（Page Cache），不直接写入磁盘文件。然后大约每秒钟，操作系统会把它的缓存统一刷到磁盘。 折中因为即使此时Buffer Pool 中的脏页还没有写回数据库，崩溃的时候，也可以依靠Redo log来进行重做，恢复到事务最后一次提交的状态这就是WAL思想，先写日志，后写数据两阶段提交不一致问题保障binlog和redolog的数据一致性，不一致的问题：先写 Redo Log，后写 Binlog，但在写 Binlog 前宕机：Redo Log 已经持久化，数据可以恢复。Binlog 没有记录。后果： 主库数据是完整的，但从库（通过 Binlog 复制）会缺失这条数据，导致主从数据不一致。先写 Binlog，后写 Redo Log，但在写 Redo Log 前宕机：Binlog 已经记录，从库会复制这条数据。Redo Log 没有完全持久化，崩溃恢复时主库会回滚这条数据（WAL 思想失败）。后果： 从库多出一条数据，主从数据不一致。阶段一：Prepare 准备阶段数据变更： 事务执行所有 SQL 语句，修改 Buffer Pool 中的数据页，并生成相应的 Redo Log 记录。Redo Log 刷盘：存储引擎将 Redo Log Buffer 中的数据刷入磁盘。标记 XID： 在这条 Redo Log 记录上，会标记一个事务 ID (XID)，并将其状态设置为 "Prepare"（准备状态）。状态： Redo Log 已持久化到磁盘，但事务在 InnoDB 内部仍未最终提交。如果此时宕机，恢复时 Redo Log 会发现自己处于 "Prepare" 状态，等待 Binlog 的最终判断。阶段二：Commit 提交阶段Step 2.1：写入 Binlog（Server 层操作）写入 Binlog：Server 层开始处理，将这个事务对应的 Binlog 事件写入到 Binlog 文件中。binlog刷盘： 此时，Binlog 也被强制刷盘。记录 XID： 在 Binlog 事件中，同样会记录这个事务的 XID。状态： Redo Log（Prepare 状态）和 Binlog 都已持久化到磁盘。Binlog 成为崩溃恢复的最终裁决者。Step 2.2：写入 Redo Log Commit 标记（InnoDB 层操作）Redo Log 提交： 事务执行层通知 InnoDB 存储引擎，事务已完成 Binlog 写入。最终标记： InnoDB 只需要在 Redo Log 上添加一个 "Commit" 标记（最终完成状态），表明该事务已彻底提交。这个操作通常只在内存中修改状态，并不强制刷盘。状态： 事务彻底完成。数据修改对外部可见。为什么 Step 2.2 不强制刷盘？因为即使在 Step 2.2 标记完成前宕机，我们回到 Step 2.1 的状态：Redo Log (Prepare) + Binlog (已记录)。崩溃恢复时，MySQL 依然会根据 Binlog 来完成提交。所以，Step 2.2 的标记只是为了事务的正常结束流程，在崩溃恢复中不再是决定性的步骤。崩溃恢复时恢复2PC 的价值体现在崩溃恢复时。MySQL 重启时，会根据 Redo Log 和 Binlog 的状态，执行以下判断逻辑：Redo Log 状态Binlog 状态恢复操作未 Prepare无记录回滚 ：事务视为失败，Redo Log 没有任何记录，直接清理。Prepare 状态有对应 XID 的记录提交 ：事务视为成功，强制完成 Redo Log 的 Commit 标记（以 Binlog 为准）。Prepare 状态无对应 XID 的记录回滚 ：事务视为失败，因为 Binlog 缺失，所以从库无法复制，主库需要回滚。Committed 状态有记录跳过：事务已完成，正常运行。undolog回滚日志逻辑日志，顾名思义，用来回滚的，也就是保障事务的原子性的作用。记录修改之前的状态Undo Log 的作用作用一：保障事务的原子性，回滚实现机制：当一个事务执行 ROLLBACK 命令，或者事务执行过程中遭遇异常中断（如死锁被终止、应用程序崩溃）时，系统会利用 Undo Log 记录的逻辑信息，将数据恢复到事务开始前的状态。回滚流程： 沿着当前数据行上的 Roll_Pointer 指针，找到最新的 Undo Log 记录，执行其中的反向操作，从而撤销本次事务的所有修改。作用二：实现多版本并发控制 (MVCC)见事务篇写入时机在事务没提交之前，先把更新前的数据放到undo log 中，详细就是：插入：记录插入的主键值就行--------回滚的时候只删除这个主键值对应的记录就行记录新插入行的主键，回滚的时候根据主键定位并删除删除：记录这个数据项的所有字段---回滚的时候把内容组成的记录重新插入到表中这里删除是逻辑删除，删除只是标记为已删除，回滚的时候再置为false更新：记录主键值和被修改的旧值---回滚的时候把列更新为旧值就行**1、更新的不是主键：那undolog记录的是主键以及被修改的原始值，回滚的时候根据主键找到这行，再用旧值覆盖新值** **2、更新的是主键：InnoDB的操作是“删除旧记录+插入新纪录”，所以undolog的记录是针对旧的主键的DELECT型+针对新的主键的INSERT型**刷盘时机随 Redo Log 刷盘而持久化。因为undolog本身就算是一个修改操作，修改了。异同redo log 记录了此次事务「完成后」的数据状态，记录的是更新之后的值（持久性）undo log记录了此次事务「开始前」的数据状态，记录的是更新之前的值（原子性）场景思考总结在事务执行的各个阶段的回滚以redo log是否落盘为界：在redo log的prepare阶段完成并刷盘之前宕机，事务必然回滚。结合binlog判断最终状态：如果redo log已prepare，但binlog未写入成功前宕机，事务回滚。如果binlog已写入成功后宕机，无论redo log的commit标记是否写入，事务都将被视为成功，重启后会完成提交。回滚的本质：利用undo log中记录的数据前镜像来恢复数据。必要性如果没有undolog，事务不能回滚原子性消失，MVCC也没有了如果没有redolog，事务持久性消失，宕机就炸了为什么不直接修改磁盘要引入redolog：因为直接修改磁盘是随机IO，性能极差redolog转换成了顺序IO。（因为各种表在物理存储中的位置可能相隔非常远，而redolog在日志文件中是严格按照时间排序的）也就是说一是减少了IO次数，二是把IO异步化了→事务提交的时候是同步 顺序IO写到日志上，数据写盘的时候是异步 写到磁盘上。没有 redo log：事务提交 $\rightarrow$ 必须立即进行一次随机 I/O，将修改写入物理数据文件。这样每一次提交都会卡在随机 I/O 上。有了 redo log：事务提交 $\rightarrow$ 只需立即进行一次顺序 I/O，将修改记录到 redo log 文件。物理数据文件的随机 I/O 操作可以延迟到数据库的后台线程去做（称为 flush 或 checkpoint），并且可以合并很多次事务的修改一次性完成。假设有 1000 个事务在 1 秒内提交，每个事务都修改了不同表中的不同行。没有 redo log：需要进行 1000 次耗时的随机 I/O 来修改数据文件。有了 redo log：只需要进行 1000 次极快的顺序 I/O 来记录日志。后台线程可以在后续 10 秒内，只执行少数几次甚至 1 次大的随机 I/O（flush 操作），把这 1000 个修改后的数据页写回磁盘。结论：redo log 的作用不是消除随机 I/O，而是将随机 I/O从事务提交的“关键路径”中剥离出来，使其从同步操作（必须马上完成）变成了异步操作（可以延迟、合并、批量完成）。
索引分类：索引这一块按照本质分是两种索引，一种是聚集索引一种是非聚集索引。聚集索引就是实际顺序与存储的时候逻辑顺序相同的索引，也就是主键，如果没有主键，那MySQL会把第一个不包含NULL的唯一索引当做聚集索引，都没有，会生成一个隐式聚集索引。非聚集索引可以粗暴的理解为后加的人为的索引，比如name，age之类的。区别：这两种索引有区别，而这种区别是因为索引的底层结构导致的，索引的底层结构是B+树，B+树的特性就是非叶子节点不存储数据只存储索引，叶子节点存储data也就是数据。那聚集索引的B+树的样子就是：非叶子节点都是索引，叶子节点直接就是行数据。那非聚集索引的B+树的样子是：非叶子节点是索引，叶子节点是索引对应的主键值，所以需要再跳一次主键的B+树，这个过程就叫做回表。其他：至于其他派生的索引都是具有一部分功能性，比如唯一索引就是非聚集索引的一种，是用来表示这个数据的唯一性。覆盖索引的意思是在非聚集索引的这个B+树在查找的时候，索引已经囊括了所有要查的数据，也就是Select后面的要查的数据。特殊：比较特殊的是非聚集索引的联合索引，有最左前缀原则，这个也跟索引的底层的B+树不谋而合。多次谈到B+树结构，下文写一下B+树的优点以及结构结构：B+树之前说一下B树B树：矮胖树，一个节点可以有多个子节点，每个节点都会存储数据本身，节点内的键值是排好序的，节点之间也满足排序关系。B+树：1.数据存储：B+树的Data只存在于叶子节点中，非叶子节点只有索引。（B树什么节点都存数据）由于B+树非叶子节点不存Data，所以每个节点能存的索引更多，树更矮。2.叶子结点：B+树中有叶子节点有指针，叶子节点之间通过指针链接起来形成一个有序链表（B树叶子节点是相互独立的）那根据这俩区别，能总结出四个优点：数据存储的模式导致树更矮胖，磁盘IO次数少。因为Data都存在于叶子节点里面，最差的情况就是一般情况，查询性能稳定。叶子节点之间的双向链表，全表遍历效率高。叶子节点之间的双向链表，范围查询效率高。最左前缀原则：看详细的概括索引失效情况：看详细的概括锁锁的出现是因为要处理多并发多线程场景下的并发问题，也就是下面要说的事务。分类：锁的分类不像索引那样清晰，因为锁既可以锁整个表，也可以锁单行数据，所以大方向分为：表级锁和行级锁。因为MySQL的存储引擎是InnoDB，所以本文只涉及InnoDB的锁表级锁有：表锁：表级读锁阻止其他操作对表的写，表级写锁其他操作对表的读和写都阻止。元数据锁MDL：防止执行CRUD的时候表结构变更（数据库本身会加MDL，不用我们来操作）意向锁：意向锁是是为了解决行锁与表锁的冲突设计的，可以快速判断这个表里有没有行锁行级锁有：按照锁的兼容性分为：排他锁和共享锁，也就是X和S锁。按照锁的范围分为：记录锁：锁单条记录间隙锁：锁一个id数字结合，不包括记录本身（前开后开）临键锁：记录锁+间隙锁，锁一个范围，包含记录本身（前开后闭）什么时候加表级锁？当执行UPDATE、DELETE的语句的时候，WHERE没有命中索引或者索引失效的时候，就会对全表扫描加锁。什么时候加行级锁？看详细的概括三大日志binlog逻辑日志，作用是主从复制，全量数据的备份。追加写，写满一个文件接着创建接着写写入时机在每完成一次更新操作之后，生成一条binlog刷盘时机什么时候刷盘由sync_binlog参数决定0：不去强制要求，由系统自行判断何时写入磁盘；1：每次事务提交后，都要将binlog写入磁盘；N：每N个事务，才会将binlog写入磁盘。redolog缓存机制，以及WAL，看详细的概括。两阶段提交看详细的概括。undolog逻辑日志，用来回滚的，作用是保障事务的原子性和实现多版本并发控制MVCC。原理插入：记录插入的主键值就行--------回滚的时候只删除这个主键值对应的记录就行记录新插入行的主键，回滚的时候根据主键定位并删除删除：记录这个数据项的所有字段---回滚的时候把内容组成的记录重新插入到表中这里删除是逻辑删除，删除只是标记为已删除，回滚的时候再置为false更新：记录主键值和被修改的旧值---回滚的时候把列更新为旧值就行**1、更新的不是主键：那undolog记录的是主键以及被修改的原始值，回滚的时候根据主键找到这行，再用旧值覆盖新值** **2、更新的是主键：InnoDB的操作是“删除旧记录+插入新纪录”，所以undolog的记录是针对旧的主键的DELECT型+针对新的主键的INSERT型** 写入时机在事务没提交之前，先把更新前的数据放到undo log 中刷盘时机随 Redo Log 刷盘而持久化。因为undolog本身就算是一个修改操作，修改了。事务事务概念本身就是为了保障数据的一致性。事务的四大特性：目标实现手段说明原子性 (A)undo log记录回滚日志，失败了能改回去。一致性 (C)以上所有 + 业务逻辑最终目标。隔离性 (I)MVCC (ReadView) + 锁MVCC 负责快照读，锁负责当前读和写。持久性 (D)redo log保证断电后数据不丢失。但是并发事务会导致问题，也就是脏读，不可重复读，幻读，解决方式是通过：标准事务隔离级别级别越高，数据一致性越好，但并发性能可能越低。读取未提交：允许读取未提交的数据变更，啥都防不住读取已提交：允许读取并发事务已经提交的数据，可以阻止脏读可重复读：对同一个字段的多次读取都是一样的，除非是自身修改，阻止了脏读和不可重复读，至于幻读，仍有可能（InnoDB大幅减少）一旦事务开始，它就仿佛进入了一个“时间凝固”的快照中*可串行化：最高隔离，依次执行，全都防住MVCC以及快照读，当前读的区别，以及各种细微的问题见 详细的概括。
如果您看到这篇文章,表示您的 blog 已经安装成功.