MySQL底层原理

MySQL内部组件结构

Server层

主要包括连接器、查询缓存、分析器、优化器、执行器等,涵盖 MySQL 的大多数核心服务功能,以及所有的内置函数(如日期、时间、数学和加密函数等),所有跨存储引擎的功能都在这一层实现,比如存储过程、触发器、视图等。

存储引擎层

存储引擎层负责数据的存储和提取。其架构模式是插件式的,支持 InnoDB、MyISAM、Memory 等多个存储引擎。create table时不指定表的存储引擎类型,默认会给你设置存储引擎为InnoDB。

组件 说明
连接器 连接器负责跟客户端建立连接、获取权限、 维持和管理连接。
查询缓存 MySQL 拿到一个查询请求后,会先查看缓存是否存在,存在直接返回,否则查询结果,并以 key-value 对的形式,被直接缓存在内存中。key 是查询的语句,value 是查询的结果。 MySQL 8已经移除此功能,命中率太低,增加维护成本。
分析器 + 词法分析:识别sql中的字符串 + 语法分析:验证是否符合MySQL语法
优化器 从多个执行方案中选择效果最好的查询方案
执行器 + 验证是否具有执行权限 + 调用存储引擎存储和索引数据

MySQL索引

索引分类

索引底层结构

Hash表

  • 对索引的key进行一次hash计算就可以定位出数据存储的位置
  • 很多时候Hash索引要比B+ 树索引更高效
  • 仅能满足 "=","in",不支持范围查询
  • hash冲突问题,会导致查询效率降低

自适应哈希索引

InnoDB 在发现某些热点数据(被频繁访问的二级索引)时,会在内存中自动创建一个自适应哈希索引,以加速等值查询。 这种哈希索引是动态和自适应的,InnoDB 会根据查询模式和数据访问频率自动构建和调整它,而不需要用户手动干预。

B-Tree

  • 叶节点具有相同的深度,叶节点的指针为空
  • 所有索引元素不重复
  • 节点中的数据索引从左到右递增排列

B+Tree(B-Tree变种)

  • 非叶子节点不存储data,只存储索引(冗余),可以放更多的索引
  • 叶子节点包含所有索引和完整数据
  • 叶子节点用指针连接,提高区间访问的性能

为什么MySQL要使用B+Tree作为索引结构,而不是红黑树、Hash表或者B-Tree?

  1. 红黑树,大数据量的场景下会导致树的高度很深,查询效率不高,更新数据时维护红黑树平衡成本很大
  2. B-Tree 非叶子节点存储数据,会导致每一个数据页存储的索引数量较少,会增加查询IO的次数,对于范围查询还需要回溯到上层进行查找
  3. Hash表 对于=、in查询效率很高,但是不支持范围查询,另外hash冲突会导致查询降为O(n) 的时间复杂度
  4. B+Tree 非叶子节点只存储索引Key,一个数据页能存储更多的索引Key减少IO次数,叶子节点上通过链表指针可以很好的支持范围查询

存储引擎存储实现

MyISAM

MyISAM索引文件和数据文件是分离的(非聚集)

InnoDB

  • 表数据文件本身就是按B+Tree组织的一个索引结构文件
  • 聚集索引-叶节点包含了完整的数据记录
  • 为什么建议InnoDB表必须建主键,并且推荐使用整型的自增主键?
    • 数据文件本身就是主索引,InnoDB将表的数据文件按主键的 B+ 树结构组织,如果没有主键,InnoDB就没有一个结构来组织数据,导致数据无法存储和检索。
    • 使用自增主键提高插入性能,避免页分裂:
      • 按序插入:自增ID的值是递增的,新插入的数据总是能按顺序追加到当前数据页的末尾。
      • 减少碎片:这种连续的插入方式避免了随机插入导致的 B+ 树页分裂和数据移动。
      • 高效页利用:新数据页只需顺序写入,索引和数据页的存储更紧凑,减少磁盘碎片,提升效率。
      • 紧凑索引结构:整型主键占用空间小,可以减小辅助索引的体积,因为所有辅助索引存储的都是主键的值。
  • 为什么非主键索引结构叶子节点存储的是主键值?(一致性和节省存储空间)

单值主键索引和辅助索引

联合索引

  1. 索引是排好序的一份数据
  2. 联合索引,先按照第一个字段排序,然后按照第二个字段排序,后面以此类推依次排序
  3. 字符串会按照字符的ASCII码进行排序

Explain工具

使用EXPLAIN关键字可以模拟优化器执行SQL语句,分析你的查询语句或是结构的性能瓶颈。

列名 说明
id id是 select 的序列号,有几个 select 就有几个id,并且id的顺序是按 select 出现的顺序增长的。 id值越大执行优先级越高,id相同则从上往下执行,id为NULL最后执行。
select_type select_type 表示对应行是简单还是复杂的查询。 + simple:简单查询。查询不包含子查询和union + primary:复杂查询中最外层的 select + subquery:包含在 select 中的子查询(不在 from 子句中) + derived:包含在 from 子句中的子查询。MySQL会将结果存放在一个临时表中,也称为派生表(derived的英文含 义) + union:在 union 中的第二个和随后的 select
table 这一列表示 explain 的一行正在访问哪个表。 + 当 from 子句中有子查询时,table列是 格式,表示当前查询依赖 id=N 的查询,于是先执行 id=N 的查询。 + 当有 union 时,UNION RESULT 的 table 列的值为<union1,2>,1和2表示参与 union 的 select 行id。
patitions 如果查询是基于分区表的话,partitions 字段会显示查询将访问的分区。
type 表示关联类型或访问类型 ,即MySQL决定如何查找表中的行,查找数据行记录的大概范围,依次从最优到最差分别为:system > const > eq_ref > ref > range > index > ALL 一般来说,得保证查询达到range级别,最好达到ref + NULL :mysql能够在优化阶段分解查询语句,在执行阶段用不着再访问表或索引就能得到结果。 + const, system :mysql能对查询的某部分进行优化并将其转化成一个常量。用于 primary key 或 unique key 的所有列与常数比较时,所以表最多有一个匹配行,读取1次,速度比较快。system是 const的特例 ,表里只有一条元组匹配时为system。 + eq_ref :primary key 或 unique key 索引的列被连接使用 ,最多只会返回一条符合条件的记录。这可能是在 const 之外最好的联接类型了,简单的 select 查询不会出现这种 type。 + ref :相比 eq_ref,不使用唯一索引,而是使用普通索引或者唯一性索引的部分前缀,索引要和某个值相比较,可能会找 到多个符合条件的行。 + range :范围扫描通常出现在 in(), between ,> ,<, >= 等操作中。使用一个索引来检索给定范围的行。 + index :扫描全索引就能拿到结果,一般是扫描某个二级索引,这种扫描不会从索引树根节点开始快速查找,而是直接对二级索引的叶子节点遍历和扫描,速度还是比较慢的,这种查询一般为使用覆盖索引,二级索引一般比较小,所以这种通常比ALL快一些。 + ALL:即全表扫描,扫描你的聚簇索引的所有叶子节点。通常情况下这需要增加索引来进行优化了。
possible_key 这一列显示查询可能使用哪些索引来查找。 explain 时可能出现 possible_keys 有列,而 key 显示 NULL 的情况,这种情况是因为表中数据不多,mysql认为索引对 此查询帮助不大,选择了全表查询。
key 这一列显示mysql实际采用哪个索引来优化对该表的访问。 如果没有使用索引,则该列是 NULL。如果想强制mysql使用或忽视possible_keys列中的索引,在查询中使用 force index、ignore index。
key_len 这一列显示了mysql在索引里使用的字节数,通过这个值可以算出具体使用了索引中的哪些列。 key_len计算规则如下: + 字符串,char(n)和varchar(n),5.0.3以后版本中,n均代表字符数,而不是字节数, 如果是utf-8,一个数字或字母占1个字节,一个汉字占3个字节 - char(n):如果存汉字长度就是 3n 字节 - varchar(n):如果存汉字则长度是 3n + 2 字节,加的2字节用来存储字符串长度,因为varchar是变长字符串 + 数值类型 - tinyint:1字节 - smallint:2字节 - int:4字节 - bigint:8字节 + 时间类型 - date:3字节 - timestamp:4字节 - datetime:8字节 + 如果字段允许为 NULL,需要1字节记录是否为 NULL 索引最大长度是768字节,当字符串过长时,mysql会做一个类似左前缀索引的处理,将前半部分的字符提取出来做索 引。
ref 这一列显示了在key列记录的索引中,表查找值所用到的列或常量。
rows 这一列是mysql估计要读取并检测的行数,注意这个不是结果集里的行数。
filtered 该列是一个百分比的值,rows * filtered/100 可以估算出将要和 explain 中前一个表进行连接的行数(前一个表指 explain 中的id值比当前表id值小的表)。
extra Using index :使用覆盖索引 覆盖索引定义:mysql执行计划explain结果里的key有使用索引,如果select后面查询的字段都可以从这个索引的树中获取,这种情况一般可以说是用到了覆盖索引,extra里一般都有using index;覆盖索引一般针对的是辅助索引,整个查询结果只通过辅助索引就能拿到结果,不需要通过辅助索引树找到主键,再通过主键去主键索引树里获取其它字段值。
Using where:使用 where 语句来处理结果,并且查询的列未被索引覆盖
Using index condition:查询的列不完全被索引覆盖,where条件中是一个前导列的范围;
Using temporary:mysql需要创建一张临时表来处理查询。出现这种情况一般是要进行优化的,首先是想到用索引来优化。
Using filesort:将用外部排序而不是索引排序,数据较小时从内存排序,否则需要在磁盘完成排序。这种情况下一般也是要考虑使用索引来优化的。
Select tables optimized away:使用某些聚合函数(比如 max、min)来访问存在索引的某个字段

索引最佳实践

索引使用原则

eg:index(a, b, c)

  1. 全匹配(等值查询)
  2. 最左前缀匹配法则

如果索引了多列,要遵守最左前缀法则。指的是查询从索引的最左前列开始并且不跳过索引中的列。

  1. 不在索引列上做任何操作(计算、函数、(自动or手动)类型转换),会导致索引失效而转向全表扫描
  2. 存储引擎不能使用索引中范围条件右边的列
  3. 尽量使用覆盖索引(只访问索引的查询(索引列包含查询列),不回表),减少 select * 语句
  4. mysql在使用不等于(!=或者<>),not in ,not exists 的时候无法使用索引,会导致全表扫描, < 小于、 > 大于、 <=、>= 这些,mysql内部优化器会根据检索比例、表大小等多个因素整体评估是否使用索引
  5. is null,is not null 一般情况下也无法使用索引
  6. like以通配符开头('$abc...')mysql索引失效会变成全表扫描操作

问题:解决like'%字符串%'索引不被使用的方法?

复制代码
- <font style="color:rgb(0,0,0);">使用覆盖索引,查询字段必须是建立覆盖索引字段</font>
- <font style="color:rgb(0,0,0);">如果不能使用覆盖索引则可能需要借助搜索引擎</font>
  1. 字符串不加单引号索引失效
  2. 少用or或in,用它查询时,mysql不一定使用索引,mysql内部优化器会根据检索比例、表大小等多个因素整体评估 是否使用索引,详见范围查询优化
  3. 范围查询优化

优化方法:可以将大的范围拆分成多个小范围(如果mysql认为使用二级索引查询出大范围数据,进行回表成本太高,会直接扫描主键索引)

  1. 索引下推,在索引遍历过程中,对索引中包含的所有字段先做判断,过滤掉不符合条件的记录之后再回表,可以有效的减少回表次数
    1. 索引下推会减少回表次数,对于innodb引擎的表索引下推只能用于二级索引
    2. 为什么范围查找Mysql没有用索引下推优化?

Mysql认为范围查找过滤的结果集过大,like KK% 在绝大多数情况来看,过滤后的结果集比较小,所以这里Mysql选择给 like KK% 用了索引下推优化,当然这也不是绝对的,有时like KK% 也不一定就会走索引下推。

order by和group by优化

filesort文件排序原理

  • 单路排序:是一次性取出满足条件行的所有字段,然后在sort buffer中进行排序;用trace工具可

以看到sort_mode信息里显示< sort_key,additional_fields>或者< sort_key,packed_additional_fields>

  • 双路排序(又叫回表 排序模式):首先根据相应的条件取出相应的排序字段可以直接定位行

数据的行 ID,然后在 sort buffer 中进行排序,排序完后需要再次取回其它需要的字段;用trace工具

可以看到sort_mode信息里显示< sort_key, rowid >

MySQL 通过比较系统变量 max_length_for_sort_data(默认1024字节) 的大小和需要查询的字段总大小来判断使用哪种排序模式。

  • 如果字段的总长度小于max_length_for_sort_data ,那么使用单路排序模式;
  • 如果字段的总长度大于max_length_for_sort_data ,那么使用 双路排序模式。

注意,如果全部使用sort_buffer内存排序一般情况下效率会高于磁盘文件排序,但不能因为这个就随便增 大sort_buffer(默认1M),mysql很多参数设置都是做过优化的,不要轻易调整。

排序和分组优化总结

  1. MySQL支持两种方式的排序filesort和index,Using index是指MySQL扫描索引本身完成排序,index效率高,filesort效率低。
  2. order by满足两种情况会使用Using index。
    1. order by语句使用索引最左前列。
    2. 使用where子句与order by子句条件列组合满足索引最左前列。
  3. 尽量在索引列上完成排序,遵循索引建立(索引创建的顺序)时的最左前缀法则。
  4. 如果order by的条件不在索引列上,就会产生Using filesort。
  5. 能用覆盖索引尽量用覆盖索引
  6. group by与order by很类似,其实质是先排序后分组,遵照索引创建顺序的最左前缀法则。对于group by的优化如果不需要排序的可以加上order by null禁止排序。注意,where高于having,能写在where中的限定条件就不要去having限定了。

索引设计原则

  1. 代码先行,索引后上(功能开发完之后统一规划索引)
  2. 联合索引尽量覆盖条件

尽量设计联合索引(少建单值索引),让每一个联合索引都尽量去包含sql语句里的 where、order by、group by的字段,还要确保这些联合索引的字段顺序尽量满足sql查询的最左前缀原则。

  1. 不要再小基数字段上建立索引

基数 = count(distinct 字段) / count(字段)

  1. 长字符串可以采用前缀索引
    • 尽量对字段类型较小的列设计索引,因为字段类型较小的话,占用磁盘空间也会 比较小,此时你在搜索的时候性能也会比较好一点。
    • 对于这种varchar(255)的大字段可以针对这个字段的前20个 字符建立索引,就是说,对这个字段里的每个值的前20个字符放在索引树里,类似于 KEY index(name(20),age,position)。
  2. where 与order by 冲突时,优先满足where
  3. 基于慢sql查询做优化,根据监控的慢sql查询做特定的索引优化。

分页优化

  1. 根据自增且连续的主键排序的分页查询
sql 复制代码
select * from employees limit 90000,5;

优化成

sql 复制代码
select * from employees where id > 90000 limit 5;
  1. 根据非主键字段排序的分页查询
sql 复制代码
select * from employees order by name limit 90000,5;

其实关键是让排序时返回的字段尽可能少,所以可以让排序和分页操作先查出主键,然后根据主键查到对应的记录,SQL改写如下

sql 复制代码
mysql> select * from employees e inner join (select id from employees order by name limit 90000,5) ed on e.id = ed.id;

join 关联查询优化

关联算法

  1. 嵌套循环连接 ****Nested-Loop Join(NLJ) 算法

关联字段有索引的情况

一次一行循环地从第一张表(称为驱动表 )中读取行,在这行数据中取到关联字段,根据关联字段在另一张表(被驱动表)里取出满足条件的行,然后取出两张表的结果合集。

  1. **基于块的嵌套循环连接 ****Block Nested-Loop Join(**BNL****)算法

关联字段没有索引的情况

驱动表 的数据读入到 join_buffer 中,然后扫描被驱动表 ,把被驱动表每一行取出来跟 join_buffer 中的数据做对比。

关联sql优化

  • 关联字段加索引,让mysql做join操作时尽量选择NLJ算法
  • 小表驱动大表 ,写多表连接sql时如果明确知道哪张表是小表可以用straight_join写法固定连接驱动方式,省去 mysql优化器自己判断的时间

对于小表定义的明确

在决定哪个表做驱动表的时候,应该是两个表按照各自的条件过滤,过滤完成之后 ,计算参与 join 的各个字段的总数据 量,数据量小的那个表,就是"小表",应该作为驱动表。

in和exsits优化

原则:小表驱动大表,即小的数据集驱动大的数据集

**in:**当B表的数据集小于A表的数据集时,in优于exists

sql 复制代码
select * from A where id in (select id from B)

**exists:**当A表的数据集小于B表的数据集时,exists优于in

sql 复制代码
select * from A where exists (select 1 from B where B.id = A.id)

count(*)查询优化

  • 字段有索引:count(*)≈count(1)>count(字段)>count(主键 id)

字段有索引,count(字段)统计走二级索引,二级索引存储数据比主键索引少,所以count(字段)>count(主键 id)

  • 字段无索引:count(*)≈count(1)>count(主键 id)>count(字段)

字段没有索引count(字段)统计走不了索引,count(主键 id)还可以走主键索引,所以count(主键 id)>count(字段)

  • count(1)跟count(字段)执行过程类似,不过count(1)不需要取出字段统计,就用常量1做统计,count(字段)还需要取出字段,所以理论上count(1)比count(字段)会快一点。
  • count() 是例外,mysql并不会把全部字段取出来,而是专门做了优化,不取值,按行累加,效率很高,所以不需要用count(列名)或count(常量)来替代 count()。

MySQL 事务

事务的ACID属性

  • 原子性(Atomicity) :当前事务的操作要么同时成功,要么同时失败。原子性由undo log日志来实现。
  • 一致性(Consistent) :在事务开始和完成时数据都必须保持一致状态。这是使用事务的最终目的,由其它3个特性以及业务代码正确逻辑来实现。
  • 隔离性(Isolation) :在事务并发执行时,他们内部的操作不能互相干扰,隔离性由MySQL的各种锁以及MVCC机制来实现。
  • 持久性(Durable) :一旦提交了事务,它对数据库的改变就应该是永久性的。持久性由redo log日志来实现。

事务并发问题

并发问题 核心定义 生活化示例
丢失更新 (Lost Update)或脏写 两个事务同时读取并修改同一行数据,后提交的事务覆盖了先提交事务的修改结果,导致先提交的修改"丢失"。 事务A和事务B同时读取库存为100。A减10至90后提交,B减5至95后提交,最终库存为95,A的减10操作丢失了。
脏读 (Dirty Read) 一个事务读到了另一个事务未提交的数据修改。如果那个未提交的事务后来回滚了,那么读到的就是无效的"脏"数据。 事务A将账户余额从300元改为500元(未提交),事务B看到余额变成了500元。随后事务A回滚,余额恢复为300元,但事务B却基于500元这个"脏数据"进行了操作。
不可重复读 (Non-repeatable Read) 在同一个事务内,两次读取同一行数据,得到了不同的结果。这是因为在两次读取之间,另一个事务修改并提交了该数据。 事务A第一次查询账户余额为300元。此时事务B将余额更新为500元并提交。事务A再次查询,发现余额变成了500元,两次读取结果不一致。
幻读 (Phantom Read) 在同一个事务内,两次执行相同的范围查询,返回的记录行数不一致。这是因为在两次查询之间,另一个事务插入或删除了符合查询条件的记录。 事务A查询余额大于100元的用户,得到2条记录。此时事务B插入一条余额为200元的新用户记录并提交。事务A再次查询,得到了3条记录,就像出现了"幻影行"。

事务隔离级别

隔离级别 可解决的并发问题 仍存在的并发问题 实现原理与解决方案 适用场景
读未提交 (Read Uncommitted) 脏读、不可重复读、幻读、丢失更新 几乎不加锁,直接读取数据页的最新版本(包括未提交的)。性能最高,但一致性最差。 对数据一致性要求极低,追求极致性能的统计场景,如近似实时数据查询。
读已提交 (Read Committed) 脏读 不可重复读、幻读、丢失更新 使用 MVCC。每次执行SELECT查询时都会生成一个新的快照(Read View),从而只能读到已提交的数据。 允许不可重复读的普通业务查询,如电商订单查询。是Oracle等数据库的默认级别。
可重复读 (Repeatable Read) - MySQL默认 脏读、不可重复读 幻读(InnoDB通过间隙锁已基本解决) 使用 MVCC。事务开始时生成一个快照(Read View),整个事务期间都读取这个快照,保证可重复读。同时,使用间隙锁(Gap Lock) 和 临键锁(Next-Key Lock) 锁定查询范围,防止其他事务插入,从而大幅减少幻读。 对一致性要求较高的核心业务,如金融转账、库存管理。在性能和数据一致性间取得良好平衡。
串行化 (Serializable) 脏读、不可重复读、幻读(全部解决) 无(但并发性能最低) 最严格的隔离级别。通过强制事务串行执行(通常使用读操作加共享锁,写操作加排他锁)来实现。 数据一致性要求极高且并发量极低的场景,如银行核心结算、审计统计。

Mysql默认的事务隔离级别是可重复读,用Spring开发程序时,如果不设置隔离级别默认用Mysql设置的隔离级别,如果Spring设置了就用已经设置的隔离级别

大事务的影响

  • 并发情况下,数据库连接池容易被撑爆
  • 锁定太多的数据,造成大量的阻塞和锁超时
  • 执行时间长,容易造成主从延迟
  • 回滚所需要的时间比较长
  • undo log膨胀
  • 容易导致死锁

事务优化实践原则

  • 将查询等数据准备操作放到事务外
  • 事务中避免远程调用,远程调用要设置超时,防止事务等待时间太久
  • 事务中避免一次性处理太多数据,可以拆分成多个事务分次处理
  • 更新等涉及加锁的操作尽可能放在事务靠后的位置
  • 能异步处理的尽量异步处理
  • 应用侧(业务代码)保证数据一致性,非事务执行

MySQL锁

MySQL锁分类

锁粒度维度

粒度 说明 典型命令/场景 特点
全局锁 锁定整个 MySQL 实例的所有数据库 <font style="color:rgb(17, 17, 51);background-color:rgba(175, 184, 193, 0.2);">FLUSH TABLES WITH READ LOCK</font> 用于全库逻辑备份;阻塞所有更新操作;粒度最粗
表级锁 锁定整张表 <font style="color:rgb(17, 17, 51);background-color:rgba(175, 184, 193, 0.2);">LOCK TABLES ... READ/WRITE</font> MDL(元数据锁) 自动加锁 开销小,加锁快;并发低;MDL 保证表结构一致性
行级锁 锁定表中的单行或多行数据 <font style="color:rgb(17, 17, 51);background-color:rgba(175, 184, 193, 0.2);">SELECT ... FOR UPDATE</font> <font style="color:rgb(17, 17, 51);background-color:rgba(175, 184, 193, 0.2);">UPDATE</font> /<font style="color:rgb(17, 17, 51);background-color:rgba(175, 184, 193, 0.2);">DELETE</font> 自动加锁 并发度高;开销大;可能死锁;InnoDB 核心机制 行锁是加在索引上的,如果没有索引行锁会升级为表锁(RR级别会升级为表锁,RC级别不会升级为表锁)
页级锁 锁定数据页(如 16KB) BDB 引擎使用 InnoDB 不使用页级锁;行锁基于索引实现
  1. InnoDB 主要使用行级锁 + 表级 MDL,通过索引实现高效并发控制。
  2. RR级别行锁升级为表锁的原因分析

因为在RR隔离级别下,需要解决不可重复读和幻读问题,所以在遍历扫描聚集索引记录时,为了防止扫描过的索引被其它事务修改(不可重复读问题) 或 间隙被其它事务插入记录(幻读问题),从而导致数据不一致,所以MySQL的解决方案就是把所有扫描过的索引记录和间隙都锁上,这里要注意,并不是直接将整张表加表锁,因为不一定能加上表锁,可能会有其它事务锁住了表里的其它行记录。

锁的模式维度

模式 名称 加锁方式 特点
S 锁 共享锁(读锁) <font style="color:rgb(17, 17, 51);background-color:rgba(175, 184, 193, 0.2);">SELECT ... LOCK IN SHARE MODE</font> 多事务可共享读;阻止写操作(X 锁)
X 锁 排他锁(写锁) <font style="color:rgb(17, 17, 51);background-color:rgba(175, 184, 193, 0.2);">SELECT ... FOR UPDATE</font> <font style="color:rgb(17, 17, 51);background-color:rgba(175, 184, 193, 0.2);">UPDATE</font> /<font style="color:rgb(17, 17, 51);background-color:rgba(175, 184, 193, 0.2);">DELETE</font> /<font style="color:rgb(17, 17, 51);background-color:rgba(175, 184, 193, 0.2);">INSERT</font> 独占访问;阻止其他任何锁(S/X)
IS 锁 意向共享锁 自动在表上加锁 表明事务将对某些行加 S 锁
IX 锁 意向排他锁 自动在表上加锁 表明事务将对某些行加 X 锁

意向锁作用:实现多粒度锁兼容。例如,若事务想对某行加 X 锁,必须先获取表的 IX 锁。若另一事务持有表的 S 锁,则 IX 锁无法获取,避免冲突。

锁的算法 / 范围(Algorithm & Range)

InnoDB 为防止 幻读(Phantom Read) 而设计的复合锁机制,结合行锁与间隙控制。

算法 说明 作用范围 隔离级别
记录锁(Record Lock) 锁定索引中的具体记录 单个索引项 所有级别
间隙锁(Gap Lock) 锁定索引记录之间的"间隙" <font style="color:rgb(17, 17, 51);background-color:rgba(175, 184, 193, 0.2);">(a, b)</font> 开区间 <font style="color:rgb(17, 17, 51);background-color:rgba(175, 184, 193, 0.2);">REPEATABLE READ</font>
临键锁(Next-Key Lock) 记录锁 + 间隙锁 = <font style="color:rgb(17, 17, 51);background-color:rgba(175, 184, 193, 0.2);">[a, b)</font> 左开右闭区间 <font style="color:rgb(17, 17, 51);background-color:rgba(175, 184, 193, 0.2);">REPEATABLE READ</font> 默认

🌟 Next-Key Lock 是 InnoDB 的核心技术:它通过锁定"记录+前一个间隙",防止其他事务在范围内插入新记录,从而解决幻读问题。

⚠️ 在 <font style="color:#DF2A3F;">READ COMMITTED</font> 隔离级别下,InnoDB 通常只使用记录锁,不使用间隙锁,因此可能出现幻读。

加锁方式(Explicit vs Implicit)

类型 说明 示例
显式锁 用户通过 SQL 主动声明加锁 <font style="color:rgb(17, 17, 51);background-color:rgba(175, 184, 193, 0.2);">SELECT ... FOR UPDATE</font> <font style="color:rgb(17, 17, 51);background-color:rgba(175, 184, 193, 0.2);">SELECT ... LOCK IN SHARE MODE</font>
隐式锁 InnoDB 自动为 DML 操作加锁 <font style="color:rgb(17, 17, 51);background-color:rgba(175, 184, 193, 0.2);">UPDATE t SET a=1 WHERE id=1;</font> → 自动对 <font style="color:rgb(17, 17, 51);background-color:rgba(175, 184, 193, 0.2);">id=1</font> 加 X 锁

并发控制思想:悲观锁 vs 乐观锁

这是更高层次的并发控制策略,不属于 MySQL 内置锁类型,而是应用层或数据库使用方式的哲学差异。

维度 悲观锁(Pessimistic Locking) 乐观锁(Optimistic Locking)
核心思想 假设并发冲突很可能发生,因此提前加锁保护数据 假设并发冲突较少发生,不加锁,提交时检查是否冲突
实现方式(MySQL) 使用 <font style="color:rgb(17, 17, 51);background-color:rgba(175, 184, 193, 0.2);">SELECT ... FOR UPDATE</font><font style="color:rgb(17, 17, 51);background-color:rgba(175, 184, 193, 0.2);">LOCK IN SHARE MODE</font> 通常通过版本号时间戳 字段实现: <font style="color:rgb(17, 17, 51);background-color:rgba(175, 184, 193, 0.2);">UPDATE t SET val=10, version=version+1 WHERE id=1 AND version=1</font>
加锁时机 读取数据时立即加锁 读取时不加锁,更新时检查
适用场景 写操作频繁、冲突多的场景(如库存扣减) 读多写少、冲突少的场景(如文章点赞)
优点 数据一致性强,避免冲突 并发性能高,减少锁等待
缺点 降低并发,可能死锁 更新失败需重试,增加应用复杂度
隔离级别影响 依赖数据库锁机制,受隔离级别影响 不依赖数据库锁,但需应用层处理冲突

💡 关键区别

  • 悲观锁数据库层面的强制阻塞机制,依赖 InnoDB 的 X/S 锁。
  • 乐观锁应用层面的冲突检测机制 ,通常通过 <font style="color:rgba(17, 17, 51, 0.5);background-color:rgba(175, 184, 193, 0.2);">WHERE version = old_version</font> 实现"条件更新"。

MySQL锁分析

通过检查InnoDB_row_lock状态变量来分析系统上的行锁的争夺情况

sql 复制代码
show status like 'innodb_row_lock%';

对各个状态量的说明如下:

  • Innodb_row_lock_current_waits: 当前正在等待锁定的数量
  • Innodb_row_lock_time: 从系统启动到现在锁定总时间长度
  • Innodb_row_lock_time_avg: 每次等待所花平均时间
  • Innodb_row_lock_time_max:从系统启动到现在等待最长的一次所花时间
  • Innodb_row_lock_waits: 系统启动后到现在总共等待的次数

尤其是当等待次数很高,而且每次等待时长也不小的时候,我们就需要分析系统中为什么会有如此多的等待, 然后根据分析结果着手制定优化计划。

查看INFORMATION_SCHEMA系统库锁相关数据表

sql 复制代码
-- 查看事务
select * from INFORMATION_SCHEMA.INNODB_TRX;

-- 查看锁
select * from INFORMATION_SCHEMA.INNODB_LOCKS;

-- 查看锁等待
select * from INFORMATION_SCHEMA.INNODB_LOCK_WAITS;

-- 释放锁,trx_mysql_thread_id可以从INNODB_TRX表里查看到
kill trx_mysql_thread_id

-- 查看锁等待详细信息
show engine innodb status\G;

查看死锁

查看近期死锁日志信息:show engine innodb status\G;

大多数情况mysql可以自动检测死锁并回滚产生死锁的那个事务,但是有些情况mysql没法自动检测死锁

锁优化建议

  • 尽可能让所有数据检索都通过索引来完成,避免无索引行锁升级为表锁
  • 合理设计索引,尽量缩小锁的范围
  • 尽可能减少检索条件范围,避免间隙锁
  • 尽量控制事务大小,减少锁定资源量和时间长度,涉及事务加锁的sql尽量放在事务最后执行
  • 尽可能低级别事务隔离

MVCC多版本并发控制机制

Mysql在读已提交和可重复读隔离级别下都实现了MVCC机制,对一行数据的读和写两个操作,RC和RR默认是不会通过加锁互斥来保证隔离性,避免了频繁加锁互斥。

MVCC机制的实现就是通过read-view机制与undo版本链比对机制,使得不同的事务会根据数据版本链对比规则读取同一条数据在版本链上的不同版本数据。

undo日志版本链与read view机制

undo日志版本链是指一行数据被多个事务依次修改过后,在每个事务修改完后,Mysql会保留修改前的数据undo回滚日志,并且用两个隐藏字段trx_id和roll_pointer把这些undo日志串联起来形成一个历史记录版本链。

可重复读隔离级别 ,当事务开启,执行任何查询sql时会生成当前事务的一致性视图read-view, 该视图在事务结束之前永远都不会变化(如果是读已提交隔离级别在每次执行查询sql时都会重新生成read-view),这个视图由执行查询时所有未提交事务id数组(数组里最小的id为min_id)和已创建的最大事务id(max_id)组成,事务里的任何sql查询结果需要从对应版本链里的最新数据开始逐条跟read-view做比对从而得到最终的快照结果。

版本链比对规则

  1. 如果 row 的 trx_id 落在绿色部分( trx_id<min_id ),表示这个版本是已提交的事务生成的,这个数据是可见的;
  2. 如果 row 的 trx_id 落在红色部分( trx_id>max_id ),表示这个版本是由将来启动的事务生成的,是不可见的(若 row 的 trx_id 就是当前自己的事务是可见的);
  3. 如果 row 的 trx_id 落在黄色部分(min_id <=trx_id<= max_id),那就包括两种情况
    1. 若 row 的 trx_id 在视图数组 中,表示这个版本是由还没提交的事务生成的,不可见(若 row 的 trx_id 就是当前自己的 事务是可见的);
    2. 若 row 的 trx_id 不在视图数组 中,表示这个版本是已经提交了的事务生成的,可见

对于删除的情况可以认为是update的特殊情况,会将版本链上最新的数据复制一份,然后将trx_id修改成删除操作的 trx_id,同时在该条记录的头信息(record header)里的(deleted_flag)标记位写上true,来表示当前记录已经被删除, 在查询时按照上面的规则查到对应的记录如果delete_flag标记位为true,意味着记录已被删除,则不返回数据。

关于readview和可见性算法的原理解释

  • readview和可见性算法其实就是记录了sql查询那个时刻数据库里提交和未提交所有事务的状态。
  • 要实现RR隔离级别,事务里每次执行查询操作readview都是使用第一次查询时生成的readview,也就是都是以第一次查询时当时数据库里所有事务提交状态来比对数据是否可见,当然可以实现每次查询的可重复读的效果了。
  • 要实现RC隔离级别,事务里每次执行查询操作readview都会按照数据库当前状态重新生成readview,也就是每次查询都是跟数据库里当前所有事务提交状态来比对数据是否可见,当然实现的就是每次都能查到已提交的最新数据效果了。

**注意:**begin/start transaction 命令并不是一个事务的起点,在执行到它们之后的第一个修改操作或加排它锁操作(比如select...for update)的语句,事务才真正启动,才会向mysql申请真正的事务id,mysql内部是严格按照事务的启动顺序来分配事务id的。

InnoDB 底层原理

redo log 重做日志

redo log 从头开始写,写完一个文件继续写另一个文件,写到最后一个文件末尾就又回到第一个文件开头循环写。write pos 是当前记录的位置,checkpoint是当前要擦除的位置,write pos 和 checkpoint 之间的部分就是空着的可写部分,可以用来记录新的操作。

innodb_flush_log_at_trx_commit:这个参数控制 redo log 的写入策略,它有三种可能取值:

  • 设置为0:表示每次事务提交时都只是把 redo log 留在 redo log buffer 中,数据库宕机可能会丢失数据。
  • 设置为1(默认值):表示每次事务提交时都将 redo log 直接持久化到磁盘,数据最安全,不会因为数据库宕机丢失数据,但是效率稍微差一点,线上系统推荐这个设置。
  • 设置为2:表示每次事务提交时都只是把 redo log 写到操作系统的缓存page cache里,这种情况如果数据库宕机是不会丢失数据的,但是操作系统如果宕机了,page cache里的数据还没来得及写入磁盘文件的话就会丢失数据。

InnoDB 有一个后台线程,每隔 1 秒,就会把 redo log buffer 中的日志,调用操作系统函数 write 写到文件系统的 page cache,然后调用操作系统函数 fsync 持久化到磁盘文件。

binlog二进制归档日志

binlog二进制日志记录保存了所有执行过的修改操作语句,不保存查询操作,主要用来恢复数据或主从复制功能。

binlog 的日志格式

用参数 binlog_format 可以设置binlog日志的记录格式,mysql支持三种格式类型:

  • STATEMENT:基于SQL语句的复制,每一条会修改数据的sql都会记录到master机器的bin-log中,这种方式日志量小,节约IO开销,提高性能,但是对于一些执行过程中才能确定结果的函数,比如UUID()、 SYSDATE()等函数如果随sql同步到slave机器去执行,则结果跟master机器执行的不一样。
  • ROW:基于行的复制,日志中会记录成每一行数据被修改的形式,然后在slave端再对相同的数据进行修改记录下每一行数据修改的细节,可以解决函数、存储过程等在slave机器的复制问题,但这种方式日志量较 大,性能不如Statement。
  • MIXED:混合模式复制,实际就是前两种模式的结合,在Mixed模式下,MySQL会根据执行的每一条具体的sql语句来区分对待记录的日志形式,也就是在Statement和Row之间选择一种,如果sql里有函数或一些在执行时才知道结果的情况,会选择Row,其它情况选择Statement,推荐使用这一种。

binlog写入磁盘机制

binlog写入磁盘机制主要通过 sync_binlog 参数控制,默认值是 0。

  • 0 表示每次提交事务都只 write 到page cache,由系统自行判断什么时候执行 fsync 写入磁盘。虽然性能得到提升,但是机器宕机,page cache里面的 binlog 会丢失。
  • 1 表示每次提交事务都会执行 fsync 写入磁盘,这种方式最安全。
  • 为N(N>1),表示每次提交事务都write 到page cache,但累积N个事务后才 fsync 写入磁盘,这种如果机器宕机会丢失N个事务的binlog。
  1. 数据恢复推荐做法

推荐的是每天(在凌晨后)需要做一次全量数据库备份,那么恢复数据库可以用最近的一次全量备份再加上备份时间点之后的binlog来恢复数据。

undo log回滚日志

undo log日志什么时候删除

  • 新增类型的,在事务提交之后就可以清除掉了。
  • 修改类型的,事务提交之后不能立即清除掉,这些日志会用于mvcc。只有当没有事务用到该版本信息时才可以清除。

为什么Mysql不能直接更新磁盘上的数据而且设置这么一套复杂的机制来执行SQL了?

因为来一个请求就直接对磁盘文件进行随机读写,然后更新磁盘文件里的数据性能可能相当差。

通过更新内存BufferPool ,然后顺序写日志文件 redo log, 通过undo log 保证数据原子性,同时还能保证各种异常情况下的数据一致性。

MySQL8.0新特性

特性 描述
降序索引 创建索引时指定为降序
group by 不再隐式排序 mysql8 以前group by的结果默认是排序的,8之后如果需要排序需要显式加上order by
增加隐藏索引 使用 invisible 关键字在创建表或者进行表变更中设置索引为隐藏索引。索引隐藏只是不可见,但是数 据库后台还是会维护隐藏索引的,在查询时优化器不使用该索引,即使force index,优化器也不会 使用该索引
函数索引 列会根据你的函数来进行计算 结果,使用函数索引的时候就会用这个计算后的列作为索引
innodb 的select 。。。for update 调过锁等待 select ... for update, 在语句后面添加 NOWAIT、SKIP LOCKED语法可以跳过锁等待,或者跳过锁定
新增innodb_dedicated_server自适应参数 检测到的内存大小自动配置innodb_buffer_pool_size, innodb_log_file_size等参数,不建议开启
死锁检查控制 新增变量 innodb_deadlock_detect,用于控制系统是否 执行 InnoDB 死锁检查,默认是打开的。会耗费性能
undo文件不再使用系统表空间
binlog日志过期时间精确到秒
窗口函数(Window Functions):也称分析函数 聚合函数后面加上over()就变成窗口函数,不实用group by 也能用窗口函数
......
DDL 原子化 InnoDB表的DDL支持事务完整性,要么成功要么回滚。

MySQL复制和高可用

Replication(复制)使来自一个 MySQL数据库服务器(称为源(Source))的数据能够复制到一个或多个 MySQL 服务器(称为副本 (Replica))。

复制的特性

优势:

  • 高可用:通过配置一定的复制机制,MySQL 实现了跨主机的数据复制,从而获得一定的高可用能力,如果需要获得更高的可用性,只需要配置多个副本,或者进行级联复制就可以达到目的。
  • 性能扩展:由于复制机制提供了多个数据备份,可以通过配置一个或多个副本,将读请求分发至副本节点,从而获得整体上读写性能的提升。
  • 异地灾备:只需要将副本节点部署到异地机房,就可以轻松获得一定的异地灾备能力。实际当中,需要考虑网络延迟等可能影响整体表现的因素。

缺点:

  • 没有故障自动转移,容易造成单点故障
  • 主库从库之间有主从复制延迟问题,容易造成最终数据的不一致
  • 从库过多对主库的负载以及网络带宽都会带来很大的负担

复制的数据同步方式

  • 异步复制

默认情况下,MySQL 采用异步复制的方式,执行事务操作的线程不会等复制 Binlog 的线程。

MySQL 主库在收到客户端提交事务的请求之后,会先写入 Binlog,然后再提交事务,更新存储引擎中的数据,事务提交完成后,给客户端返回操作成功的响应。

从库会有一个专门的复制线程,从主库接收 Binlog,然后把 Binlog 写到一个中继日志里面,再给主库返回复制成功的响应。从库还有另外一个回放 Binlog 的线程,去读中继日志,然后回放 Binlog 更新存储引擎中的数据。

提交事务和复制这两个流程在不同的线程中执行,互相不会等待,这是异步复制。异步复制的劣势

是,可能存在主从延迟,如果主节点宕机,可能会丢数据。

  • 半同步复制
复制代码
- <font style="color:#000000;">主节点在收到客户端的请求后,必须在完成本节点日志写入的同时,还需要等待至少一个从节点完成数据同步的 响应之后(或超时),才会响应请求。 </font>
- <font style="color:#000000;">从节点只有在写入 relay-log 并完成刷盘之后,才会向主节点响应。 </font>
- <font style="color:#000000;">当从节点响应超时时,主节点会将同步机制退化为异步复制。在至少一个从节点恢复,并完成数据追赶后,主节点会将同步机制恢复为半同步复制。</font>

半同步复制有两个重要的参数:

  • rpl_semi_sync_master_wait_slave_count(8.0.26之后改为 rpl_semi_sync_source_wait_for_replica_count):至少等待数据复制到几个从节点再返回。这个数量配置的越大,丢数据的风险越小,但是集群的性能和可用性就越差。
  • rpl_semi_sync_master_wait_point(8.0.26之后改为rpl_semi_sync_source_wait_point):这个参数控制主库执行事务的线程,是在提交事务之前(AFTER_SYNC)等待复制,还是在提交事务之后(AFTER_COMMIT)等待复制。默认是 AFTER_SYNC,也就是先等待复制,再提交事务,这样就不会丢数据。

复制的实现方式

基于binlog位点同步的主从复制原理

1、主库会生成多个 binlog 日志文件。

2、从库的 I/O 线程请求指定文件和指定位置的 binlog 日志文件(位点)。

3、主库 dump 线程获取指定位点的 binlog 日志。

4、主库按照从库发送给来的位点信息读取 binlog,然后推送 binlog 给从库。

5、从库将得到的 binlog 写到本地的 relay log (中继日志) 文件中。

6、从库的 SQL 线程读取和解析 relay log 文件。

7、从库的 SQL 线程重放 relay log 中的命令。

基于binlog位点主从复制痛点分析

痛点 1:首次开启主从复制的步骤复杂

  • 第一次开启主从同步时,要求从库和主库是一致的。
  • 找到主库的 binlog 位点。
  • 设置从库的 binlog 位点。
  • 开启从库的复制线程。

痛点 2:****恢复主从复制的步骤复杂

  • 找到从库复制线程停止时的位点。
  • 解决复制异常的事务。无法解决时就需要手动跳过指定类型的错误,比如通过设置 slave_skip_errors=1032,1062。当然这个前提条件是跳过这类错误是无损的。

不论是首次开启同步时需要找位点和设置位点,还是恢复主从复制时,设置位点和忽略错误,这些步骤都显得过于复杂,而且容易出错。所以 MySQL 5.6 版本引入了 GTID,彻底解决了这个困难。

基于全局事务标识符(GTID)复制

GTID是一个基于原始mysql服务器生成的一个已经被成功执行的全局事务ID,它由服务器ID以及事务

ID组合而成。

  • 一个GTID在一个服务器上只执行一次,避免重复执行导致数据混乱或者主从不一致。
  • GTID用来代替传统复制方法,不再使用MASTER_LOG_FILE+MASTER_LOG_POS开启复制。而是使用 MASTER_AUTO_POSTION=1的方式开始复制。
  • 在传统的replica端,binlog是不用开启的,但是在GTID中replica端的binlog是必须开启的,目的是记录执行过的 GTID(强制)。

GTID 的优势

  • 更简单的实现 failover,不用以前那样在需要找位点(log_file 和 log_pos)。
  • 更简单的搭建主从复制。
  • 比传统的复制更加安全。
  • GTID 是连续的没有空洞的,保证数据的一致性,零丢失。

GTID结构

GTID表示为一对坐标,由冒号(:)分隔,如下所示:

<font style="color:rgb(65,70,75);">GTID = source_id:transaction_id</font>

  • source_id标识source服务器,即源服务器唯一的server_uuid,由于GTID会传递到replica,所以也可以理解为源 ID。
  • transaction_id是一个序列号,由事务在源上提交的顺序决定。序列号的上限是有符号64位整数(2^63-1)

如:3E11FA47-71CA-11E1-9E33-C80AA9429562:23

GTID工作原理

主库计算主库 GTID 集合和从库 GTID 的集合的差集,主库推送差集 binlog 给从库。

当从库设置完同步参数后,主库 A 的 GTID 集合记为集合 x,从库 B 的 GTID 集合记为 y。从库同步

的逻辑如下:

  1. 从库 B 指定主库 A,基于主备协议建立连接。
  2. 从库 B 把集合 y 发给主库 A。
  3. 主库 A 计算出集合 x 和集合 y 的差集,也就是集合 x 中存在,集合 y 中不存在的 GTID 集合。比如集合 x 是 1~100,集合 y 是 1~90,那么这个差集就是 91~100。这里会判断集合 x 是不是包含有集合 y 的所有 GTID,如果不是则说明主库 A 删除了从库 B 需要的 binlog,主库 A 直接返回错误。
  4. 主库 A 从自己的 binlog 文件里面,找到第一个不在集合 y 中的事务 GTID,也就是找到了 91。
  5. 主库 A 从 GTID = 91 的事务开始,往后读 binlog 文件,按顺序取 binlog,然后发给 B。
  6. 从库 B 的 I/O 线程读取 binlog 文件生成 relay log,SQL 线程解析 relay log,然后执行 SQL 语句。

GTID 同步方案和位点同步的

  • 位点同步方案是通过人工在从库上指定哪个位点,主库就发哪个位点,不做日志的完整性判断。
  • 而 GTID 方案是通过主库来自动计算位点的,不需要人工去设置位点,对运维人员友好。

组复制(Group Replication)

Group Replication(简称MGR),是一个高可用与高扩展的解决方案,将原有的gtid复制功能进行了增强,支持单主模式和多主模式。

Group Replication有以下大幅改进:

  • 传统复制的主从复制方式有一个主和不等数量的从。主节点执行的事务会异步发送给从节点,在从节点重新执行。而Group Replication采用整组写入的方式,避免了单点争用。
  • Group Replication在传输数据时使用了Paxos协议。Paxos协议保证了数据传输的一致性和原子性。基于Paxos协议,Group Replication构建了一个分布式的状态复制机制,这是实现多主复制的核心技术。
  • Group Replication提供了多写方案,为多活方案带来了实现的可能。

缺点:

当一个组成员变为不可用时,连接到它的客户端必须被重定向或故障转移到其他组成员。此时需要使用连接器、负载均衡器、路由器或某种形 式的中间件,例如 MySQL Router 8.0 。

单主模式

组中的每个MySQL服务器实例都可以在独立的物理主机上运行,这是部署组复制的推荐方式。

在单主模式下(group_replication_single_primary_mode=ON),组中只有一个主服务器,该主服务器被设置为读写模 式。组中的所有其他成员都被设置为只读模式(super_read_only=ON)。

多主模式

在多主模式下(group_replication_single_primary_mode=OFF),没有成员具有特殊的角色。任何与其他组成员兼容的成员在加入组时都被设置为读写模式,并且可以处理写事务,即使它们是并发发布的。

MySQL InnoDB Cluster

基本概述

InnoDB Cluster是MySQL官方实现高可用+读写分离的架构方案,其中包含以下组件

  • MySQL Group Replication,简称MGR,是MySQL的主从同步高可用方案,包括数据同步及角色选举
  • Mysql Shell是InnoDB Cluster的管理工具,用来创建和管理集群
  • Mysql Router是业务流量入口,支持对MGR的主从角色判断,可以配置不同的端口分别对外提供读写服务,实现读写分离

MySQL Router与组复制和MySQL Shell高度整合,只有将其与组复制和MySQL Shell共同使用,才能

够称为InnoDB Cluster。

集群架构

InnoDB Cluster将三个MySQL数据库实例构成一个高可用集群。其中一个实例是具有读/写能力的主要 成员,其他两个实例是具有只读能力的次要成员。组复制将数据从主要成员复制到次要成员。MySQL Router将客户端应用程序连接到集群的主要成员。

相关推荐
算法与双吉汉堡2 小时前
【短链接项目笔记】Day3 用户模块剩余部分
java·redis·后端
Chengbei112 小时前
fastjson 原生反序列化配合动态代理绕过限制
java·安全·网络安全·系统安全·安全架构
qq_377112372 小时前
JAVA的平凡之路——此峰乃是最高峰JVM-GC垃圾回收器(1)-06
java·开发语言·jvm
学编程就要猛2 小时前
算法:2.复写零
java·数据结构·算法
熊猫吃竹子2 小时前
JVM G1GC参数调优实战
jvm·后端
我认不到你2 小时前
paxos一致性算法(大白话+图解)
分布式·后端
文心快码BaiduComate2 小时前
插件开发实录:我用Comate在VS Code里造了一场“能被代码融化”的初雪
前端·后端·前端框架
韩立学长2 小时前
【开题答辩实录分享】以《植物园信息管理系统》为例进行选题答辩实录分享
java·数据库·spring
嘻哈baby2 小时前
记一次线上OOM排查,JVM调优全过程
java