当前位置: 首页 > ai >正文

InnoDB存储引擎底层拆解:从页、事务到锁,如何撑起MySQL数据库高效运转(下)

目录

索引**

undoLog*

什么是undo log

如何开启事务

何时分配事务id

事务id是怎么生成的

为什么要加256?

事务id在记录中存储的位置

INSERT对应的undo日志结构

DELETE操作之垃圾链表

删除记录的两个步骤

DELETE操作对应的undoLog结构

UPDATE操作对应的undoLog结构

事务**

事务的状态有哪些?

事务并发执行时数据一致性问题有哪些?

SQL事务隔离级别

MVCC

版本链

ReadView包含的内容

如何通过ReadView来判断记的某版本是否可见?

Read View 生成时机

锁*

并发事务问题

记录锁*

gap锁*

nextkey锁*


接上篇 InnoDB存储引擎底层拆解:从页、事务到锁,如何撑起MySQL数据库高效运转(上)

本篇继续从undo log 讲起

undoLog*

什么是undo log

事务是需要保证原子性的,也就是说,事务中的操作要么全部完成,要么什么也不做。但有如下情况,会造成事务执行不完:①事务执行过程中可能遇到各种错误,比如:服务器宕机,操作系统异常,突然断电……②程序员在事务执行过程中手动输入rollback语句结束当前事务的执行。

遇到上面的情况,为了保证事务的原子性,我们需要把数据还原回原来的样子,这个过程就叫做回滚(rollback)数据库为了回滚而记录的日志,我们就称之为撤销日志(undo log)

注意一点,由于SELECT操作并不会修改任何记录,所以并不需要记录相应的undo日

如何开启事务

开启的事务类型

解释

只读事务

通过START TRANSACTION READ ONLY语句开启一个只读事务。在只读事务中,不可以对普通表进行增删改操作;但可以对临时表进行增删改操作。

读写事务

通过START TRANSACTION READ WRITE语句开启一个读写事务。使用BEGIN、START TRANSACTION语句开启的事务,默认也算是读写事务。在读写事务中可以对表执行增删改查操作。

何时分配事务id

只有在事务对表中的记录进行改动时才会为这个事务分配一个唯一的事务id,否则事务id值默认为0。

只读事务何时分配事务id?只有在它第一次对某个用户创建的临时表(CREATE TEMPORARY TABLE)执行增删改操作时,才会为这个事务分配一个事务id,否则是不分配的。 读写事务何时分配事务id?只有在它第一次对某个表(包括用户创建的临时表)执行增删改操作时,才会为这个事务分配一个事务id,否则是不分配的。

事务id是怎么生成的

事务id本质上就是一个数字,事务id生成策略如下:① 内存中维护一个全局变量,每当需要为某个事务分配事务id时,就会把该变量值当作事务id分配给该事务,并且自增1。② 每当这个变量的值为256的倍数时,就会将值刷新到系统表空间中页号为5的页面中一个名为Max Trx ID的属性中(占用8个字节)。③ 当系统下一次启动时,会将Max Trx ID的值加载到到内存中,并加上256之后赋值给前面提到的全局变量。

为什么要加256?

答:因为上次关机时,该全局变量的值可能大于磁盘页面中的Max Trx ID属性值。

事务id在记录中存储的位置

trx_id表示对该条记录进行改动的语句所对应的事务id。

不同版本下,查询table_id的方式:

【mysql 5.x】select * from information_schema.innodb_sys_tables;

【mysql 8.x】select * from information_schema.innodb_tables;

INSERT对应的undo日志结构

TRX_UNDO_INSERT_REC类型的undo日志结构:

名称

解释

end of record

undoLog结束,下一条开始时在页面中的地址。

undo type

undoLog的类型,也就是TRX_UNDO_INSERT_REC。

undo no

undoLog对应的编号,在一个事务中是从0开始递增的,只要事务没提交,每生成一条undo日志,那么该条日志的undo no就加1。

table id

undoLog对应的记录所在表的table_id。

主键各列信息 <len:value>列表

主键的每个列占用的存储空间大小和真实值,如果记录中的主键包含多个列,那么每个列占用的存储空间大小和对应的真实值都需要记录下来。

start of record

上一条undoLog结束,本条开始时在页面中的地址

roll_pointer本质上就是一个指向记录对应的undo日志的指针。不同的内容放到了不同的页面中,其中:① 聚簇索引记录存放到类型为FIL_PAGE_INDEX的页面;② undo日志存放到类型为FIL_PAGE_UNDO_LOG的页面;

聚簇索引记录和undo日志的存放位置,如下图所示:

DELETE操作之垃圾链表

被删除的记录其实也会根据记录头信息中的next_record属性组成一个链表,只不过这个链表中的记录所占用的存储空间可以被重新利用,所以也称这个链表为垃圾链表。Page Header部分中有一个名为PAGE_FREE的属性,它指向由被删除记录组成的垃圾链表中的头节点。每删除一条记录,则该记录都会插入到垃圾链表的头节点处。

删除记录的两个步骤

第一步:delete mark阶段仅仅将记录的deleted_flag标识位设置为1,但是这条记录并没有加入到垃圾链表中。也就是说,这条记录即不是正常记录,也不是已删除记录。在删除语句所在的事务提交之前,被删除的记录一直都处于这种中间状态(其实主要是为了实现MVCC的功能才这样处理的)。

第二步:purge阶段当该删除语句所在的事务提交后,会有专门的线程来把该记录从正常记录链表中移除,并加入到垃圾链表中作为头节点

DELETE操作对应的undoLog结构

TRX_UNDO_DEL_MARK_REC类型的undo日志结构:

名称

解释

info bits

记录头信息的前4个比特的值。

trx_id

旧记录的trx_id值。

roll_pointer

旧记录的roll_pointer值。

len of index_col_info

索引列信息及本部分占用存储空间总和(即下方【索引列各列信息】部分的总存储长度)。

索引列各列信息 <pos, len, value>列表

被索引列的各列信息(包含列在记录中的位置、长度、实际值)。

UPDATE操作对应的undoLog结构

TRX_UNDO_UPD_EXIST_REC类型的undo日志结构:

名称

解释

n_updated

本条UPDATE语句执行后将有几个列被更新。

被更新的列更新前信息 <pos, old_len, old_value> 列表

被更新列在记录中的位置、更新前该列占用的存储空间大小、更新前该列的真实值。

事务**

什么是ACID ?

Atomicity 原子性某个操作,要么全都执行完毕,要么全都回滚。

Consistency 一致性事务执行的结果应该符合数据库的约束条件和逻辑。

Isolation 隔离性在不同的业务处理过程中,事务保证了各自业务正在读、写的数据互相独立,不会彼此影响。

Durability 持久性现实中的状态转换映射到数据库中,意味着对数据所做的修改都应该在磁盘中保存。

事务的状态有哪些?

事务并发执行时数据一致性问题有哪些?

1、脏读;

        是指一个事务能读取其他事务未提交的数据。如果一个事务(小明)读取到了另一个未提交事务(小丽)修改过的数据,就意味着发生了脏读现象。

2、不可重复读;

        "同一事务先后读取同一条数据,但前后两次读到的数据是不一致的。(强调update 针对单行)" 一个事务在前后两次读取某个数据时,发现前后两次读到的数据是不一致的(数据发生了修改),这种现象叫做“不可重复读”。如果一个事务(小丽)修改了另一个未提交事务(小明)读取的数据,就意味着发生了不可重复读现象,或者叫模糊读FuzzyRead

3、幻像读;

        "同一事务先后读取一个范围的记录,但两次读取的纪录数不同。(强调insert或delete,范围查询)" 指的是在一个事务执行过程中,读取到了其他事务新插入数据,导致两次读取的结果不一致 。 一个事务按相同的查询条件重新读取以前检索过的数据,却发现其他事务插入了满足其查询条件的新数据,这种现象就称为“幻读” 。如果一个事务(小明)先根据某些搜索条件(select ... where vip='是')查询了一些记录,但是在该事务并未提交时,另一个事务(小丽)写入了一些符合上面搜索条件的记录(这里的写入可以值insert、delete、update操作。例如:insert into ... values('0003',700,'是')),就意味着发生了幻读现象。

4、丢失更新/脏写

        "撤销一个事务时,把其他事务已提交的更新数据覆盖" (事务A和B并发执行事物,A事务执行更新后,提交;B事务在A事务更新后,B事务结束前也做了对该行数据的更新操作,然后回滚,则两次更新操作都丢失了) "不可重复读和幻读的区别:可重复读是读到的是其他事务修改或者删除的数据,而幻读读到的是其它事务新插入的数据" 无论是脏读,不可重复读,还是幻读,它们都属于数据库的读一致性的问题,都是在一个事务里面前后两次读取出现了不一致的情况在多事务并发执行的环境下,一个事务修改了另一个未提交事务已经修改过的数据,从而导致数据不一致的问题。

SQL事务隔离级别

1、读未提交:

        "指一个事务还没提交时,它做的变更就能被其他事务看到",会产生脏读、幻读、不可重复读。 所有事务都可以看到其他未提交事务的执行结果 比如同一时间有两个事物,一个正在修改,一个正在查询,查询的可以查看到正在修改的数据。若修改事物回滚,则查询的结果就是错误的。

2、读已提交:

        "指一个事务提交之后,它做的变更才能被其他事务看到",会产生幻读、不可重复读。 一个事物只能看见其他已经提交的事物所做的改变 两个并发的事务,“事务1:小明消费”、“事务2:小红网上转账”,事务1事先读取了数据,事务2紧接了更新了数据,并提交了事务, 而事务1再次读取该数据时,数据已经发生了改变。

3、可重复读:

        "指一个事务执行过程中看到的数据,一直跟这个事务启动时看到的数据是一致的",会产生幻读。 若同一事物修改数据,会产生两次读取数据结果不一致问题。 当用户读取某一范围的数据行时,另一个事务又在该范围内插入了新行,当用户再读取该范围的数据行时会读取到包括刚插入的数据。 如小红查小明信用卡消费金额为50元,而小明此时正好买东西花了1000元,随后小红将小明当月的信用卡消费明细打印出来,却发现消费总额为1050元, 小红很诧异,以为是出现了幻觉。

4、串行化:

        "会对记录加上读写锁,在多个事务对这条记录进行读写操作时,如果发生了读写冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行" 这是最高的隔离级别,它通过强制事务排序,使之不可能相互冲突,从而解决不读脏,可重复读,不可幻读。 简言之,它是在每个读的数据行上加上共享锁。在这个级别,可能导致大量的超时现象和锁竞争,并发性能最差,在分布式事务中可能会被用到 

隔离级别

脏读

不可重复读

幻象读

READ UNCOMMITED (读未提交)

Y

Y

Y

READ COMMITED(读未提交)

N

Y

Y

REPEATABLE READ(可重复读)

N

N

Y

SERIALIZABLE(串行化)

N

N

N

        由于无论哪种隔离级别,都不允许脏写的情况发生,所以没有列入到表格中。

        数据库的事务隔离越严格,并发副作用就越小,但付出的代价也就越大, 因为事务隔离本质上就是使事务在一定程度上串行化,需要根据具体的业务需求来决定使用哪种隔离级别。

        事务不同隔离级别会产生3种数据不一致情况:

        1、脏读:读到其他事务未提交的数据;

        2、不可重复读:前后读取的数据不一致;

        3、幻读:前后读取的记录数量不一致。

注释:InnoDB在可重复读的级别就已经解决了幻读的问题,这也是InnoDB使用可重复读作为默认隔离级别的原因。 InnoDB 引擎的默认隔离级别是「可重复读」, InnoDB 通过next-key lock 锁(行锁和间隙锁的组合)来锁住记录之间的"间隙"和记录本身, 防止其他事务在这个记录之间插入新的记录,这样就避免了幻读现象。

四种隔离级别具体是如何实现的呢?

1、读未提交:因为可以读到未提交事务修改的数据,所以直接读取最新的数据就好了;

2、读已提交:通过 Read View 来实现的,隔离级别是在读取每个数据前都生成一个 Read View(MVCC);

3、可重复读:通过 Read View 来实现的,隔离级别是启动事务时生成一个 Read View,然后整个事务期间都在用这个 Read View(MVCC)。

4、串行化: 通过加读写锁的方式来避免并行访问

MVCC

        MVCC (Multi-Version Concurrency Control)多版本并发控制,利用记录的版本链和ReadView一致性视图,来控制并发事务访问相同记录时的行为。

ReadView 一致性视图,用来判断版本链中的哪个版本是当前事务可见的

版本链

在每次更新该记录后,都会将旧值放到一条undo日志中。随着更新次数的增多,所有的版本都会被roll_pointer属性连接成一条链表,这个链表就称之为版本链。

ReadView包含的内容

ReadView也叫一致性视图,用来判断版本链中的哪个版本是当前事务可见的。ReadView包含4个比较重要的内容:

名称

解释

m_ids

在生成ReadView时,当前系统中活跃的读写事务的事务id列表。

min_trx_id

在生成ReadView时,当前系统中活跃的读写事务中最小的事务id,也就是m_ids中的最小值。

max_trx_id

在生成ReadView时,系统应该分配给下一个事务的事务id值。

creator_trx_id

生成该ReadView的事务的事务id。

只有在对表中的记录进行改动时(即:insert、delete、update)才会为事务分配唯一的事务id,否则一个事务的事务id值都默认为0。

如何通过ReadView来判断记的某版本是否可

判断规则如下:

规则一:版本的事务 ID 与当前事务 ID 相同

如果RC.trx_id == RV.creator_trx_id,则表明当前事务在访问它自己修改过的记录,所以该版本可以被当前事务访问。

规则二:版本的事务 ID 小于 Read View 中的最小事务 ID

规则二:版本的事务 ID 小于 Read View 中的最小事务 ID

如果RC.trx_id < RV.min_trx_id,则表明生成该版本的事务在当前事务生成ReadView之前已经提交了,所以该版本可以被当前事务访问。

规则三:版本的事务 ID 大于 Read View 中的最大事务 ID

如果RC.trx_id >= RV.max_trx_id,则表明生成该版本的事务在当前事务生成ReadView之后才开启,所以该版本不可以被当前事务访问。

规则四:版本的事务 ID 在 Read View 的活跃事务列表中

如果RC.trx_id in RV.m_ids,说明创建ReadView时生成该版本的事务还是活跃的,该版本不可以被访问如果RC.trx_id not in RV.m_ids,说明创建ReadView时生成该版本的事务已经被提交,该版本可以被访问(m_ids为空集合)。

如果某个版本的数据对当前事务不可见,那就顺着版本链找到下一个版本的数据,并继续执行上面的步骤来判断记录的可见性,以此类推,直到版本链中的最后一个版本。

Read View 生成时机

不同的事务隔离级别下,Read View 生成的时机有所不同。主要是因为不同隔离级别对数据一致性和并发性能的侧重点不同,通过调整 Read View 的生成时机来满足相应的要求

READ COMMITTED和REPEATABLE READ隔离级别之间一个非常大的区别就是——它们生成ReadView的时机不同

READ COMMITTED——在一个事务中,每次读取数据前都生成一个ReadView。

REPEATABLE READ——在一个事务中,只在第一次读取数据时生成一个ReadView。

隔离级别及事务重要参数查询和设置

show variables like 'transaction_isolation';set session transaction isolation level read committed;set session transaction isolation level repeatable read;show variables like 'autocommit'; set autocommit=0;

*

并发事务问题

1.写-写

由于任何一种隔离级别都不允许脏写(写-写)的现象发生,所以,当多个未提交事务相继对一条记录进行改动的时候,就需要让它们排队执行。

这个排队的过程是通过为该记录加锁来实现的。这个锁本质上是一个内存中的结构。

2.读-写/-

为了避免在“读-写”情况下避免脏读、不可重复读、幻读现象,有如下两种可选的解决方案:① 方案1:快照读 | 一致性读 | 一致性无锁读

读操作——MVCC;写操作——对记录进行加锁。

② 方案2:锁定读(S锁 | X锁)

读、写操作都采用加锁方式

如果采用MVCC方式,读-写操作彼此并不冲突,性能更高

如果采用加锁方式,读-写操作彼此需要排队执行,从而影响性能;

所有普通的SELECT语句在READ COMMITTED或REPEATABLE READ隔离级别下都算是一致性读。所以一般来说,我们会选择采用MVCC方式来解决读-写操作并发执行的问题,但是在某些特殊的业务场景(如:银行业务,需要读取最新数据和数据准确性,对执行时间并无苛刻要求),这时我们才会选择读、写都加锁的方式。

下面几种重要的锁会持续更新

记录锁*

并发事务问题

1.写-写

由于任何一种隔离级别都不允许脏写(写-写)的现象发生,所以,当多个未提交事务相继对一条记录进行改动的时候,就需要让它们排队执行。

这个排队的过程是通过为该记录加锁来实现的。这个锁本质上是一个内存中的结构。

2.读-写/-

为了避免在“读-写”情况下避免脏读、不可重复读、幻读现象,有如下两种可选的解决方案:① 方案1:快照读 | 一致性读 | 一致性无锁读

读操作——MVCC;写操作——对记录进行加锁。

② 方案2:锁定读(S锁 | X锁)

读、写操作都采用加锁方式

如果采用MVCC方式,读-写操作彼此并不冲突,性能更高

如果采用加锁方式,读-写操作彼此需要排队执行,从而影响性能;

所有普通的SELECT语句在READ COMMITTED或REPEATABLE READ隔离级别下都算是一致性读。所以一般来说,我们会选择采用MVCC方式来解决读-写操作并发执行的问题,但是在某些特殊的业务场景(如:银行业务,需要读取最新数据和数据准确性,对执行时间并无苛刻要求),这时我们才会选择读、写都加锁的方式。

行级锁

1.锁定操作

共享锁 | S锁(Shared Lock在事务要读取一条记录时,需要先获取该记录的S锁。 SELECT....LOCK IN SHARE MODE;

独占锁 | 排它锁 | X锁(Exclusive Lock)在事务要修改一条记录时,需要先获取该记录的X锁。SELECT ...FOR UPDATE;

S锁与X锁的兼容关系如下所示:

2.锁定操作

针对DELETE操作先在B+树中定位到这条记录的位置,获取这条记录的X锁,最后再执行delete mark操作。

针对INSERT操作一般情况下,新插入的一条记录受隐式锁保护,不需要在内存中为其生成对应的锁结构。

针对UPDATE操作,分为如下3种情况:① 未修改主键并且被更新的列在修改前后所占用的存储空间未发生变化先在B+树中定位到这条记录的位置,然后获取这条记录的X锁,最后在原记录的位置进行修改操作。② 未修改主键并且被更新的列在修改前后所占用的存储空间发生变化先在B+树中定位到这条记录的位置,然后获取这条记录的X锁,之后将原记录彻底删除掉(即:把记录彻底移入垃圾链表),最后再插入一条新记录。③ 修改主键相当于在原记录上执行DELETE操作之后再来一次INSERT操作。加锁操作就需要按照DELETE和INSERT的规则进行了。

级锁

  • 表级共享锁(S锁)其他事务可以继续获得该表/该表中的某些记录的S锁。其他事务不可以继续获得该表/该表中的某些记录的X锁。
  • 表级独占锁(X锁)其他事务不可以继续获得该表/该表中的某些记录的X锁或S锁。
  • 意向共享锁(IS锁)当事务准备在某条记录上加S锁时,首先需要在表级别加一个IS锁。
  • 意向独占锁(IX锁)当事务准备在某条记录上加X锁时,首先需在表级别加一个IX锁。

表级锁之间的关系

IS锁和IX锁是表级锁,它们的提出仅仅为了在之后加表级别的S锁和X锁时可以快速判断表中的记录是否被上锁,以避免用遍历的方式来查看表中有没有上锁的记录;也就是说,IS锁和IX锁之间或者与自身都是彼此兼容的。

兼容性关系如下所示:

InnoDB表级锁

InnoDB存储引擎提供的表级S锁或者X锁相当“鸡肋”,只会在一些特殊情况下(比如:系统崩溃恢复时)用到。

在对某个表执行DDL语句时,其他事务在对这个表并发执行DML语句时,会发生阻塞;反之亦然。这个过程其实是通过在server层使用一种称为元数据锁(MetadataLock,MDL)的东西来实现的,也不会使用表级S锁和X锁。

当我们使用了auto_increment修饰列的时候,就会涉及到表级AUTO-INC锁和轻量级锁。其中:innodb_autoinc_lock_mode系统变量,用来控制到底使用上述两种方式中的哪一种。① 0:一律采用AUTO_INC锁。② 1:混合使用。插入记录的数量确定时采用轻量级锁,不确定时采用AUTO-INC锁。③ 2:一律采用轻量级锁。

Inno DB行级锁

记录锁*

LOCK_REC_NOT_GAP被称为记录锁,也就是仅仅负责把1条记录锁上的锁。

gap锁*

LOCK_GAP被称为gap锁,锁住了指定记录前面的间隙,防止其间插入新记录。gap锁的提出仅仅是为了防止插入幻象记录(即:幻读现象)而提出的。

nextkey锁*

LOCK_ORDINARY被称为next-key锁,本质就是一个 记录锁 + gap锁 的合体。它既能保护该条记录,又能阻止别的事务将新纪录插入到被保护记录前面的间隙中。

插入意向锁

LOCK_INSERT_INTENTION也被称为插入意向锁,事务在等待时也需要在内存中生成一个锁结构,表明有事务想在某个间隙中插入新记录,但是现在处于等待状态

隐式锁

一般情况下,执行INSERT语句是不需要在内存中生成锁结构的。

但是也会有例外,比方说:一个事务(T1)首先插入了一条记录(此时并没有与该记录关联的锁结构),然后另一个事务(T2)执行如下操作:① 立即使用 SESELELELECECTCT.T... LOLOCOCK CK IN IN SHSHAHARARE RE MOMODODE DE 语句读取这条记录 (也就是要获取这条记录的S锁),或者使用 SESELELELECECT CT ... FOFOR OR UPUPDPDADATATE TE 语句读取这条记录(也就是要获取这条记录的X锁),该咋办?如果允许这种情况的发生,那么可能出现脏读现象。② 立即修改这条记录(也就是要获取这条记录的X锁)该咋办?如果允许这种情况的发生,那么可能出现脏写现象。

解决办法——使用事务id,我们把聚簇索引和二级索引中的记录分开看一下:

场景1:对于聚簇索引有一个trx_id隐藏列,该隐藏列记录着最后改动该记录的事务id。在当前事务中新插入一条聚簇索引记录后,该记录的trx_id隐藏列代表的就是当前事务的事务id。如果其他事务此时想对该记录添加S锁或者X锁,首先会看一下该记录的trx_id隐藏列代表的事务是否是当前的活跃事务。如果不是的话就可以正常读取:如果是的话,那么就帮助当前事务创建一个X锁的锁结构,该锁结构的is_waiting属性为false:然后为自己也创建一个锁结构,该锁结构的is_waiting属性为true,之后自己进入等待状态。

场景2:对于二级索引本身并没有trx_id隐藏列,但是在二级索引页面的Page Header 部分有一个PAGE_MAX_TRX_ID属性,该属性代表对该页面做改动的最大的事务id。如果PAGE_MAX_TRX_ID属性值小于当前最小的活跃事务id,那就说明对该页面做修改的事务都己经提交了,否则就需要在页面中定位到对应的二级索引记录,然后通过回表操作找到它对应的聚集索引记录,然后再重复情景1的做法

InnoDB锁的内存结构

前文已经介绍过,对一条记录加锁的本质就是在内存中创建一个锁结构跟这条记录相关联。那么,我们说了这么多次的锁结构,它到底是怎么组成的呢?它其实主要是由6部分组成的。分别为:锁所在的事务信息、索引信息、表锁或行锁信息、type_mode、其他信息、与heap_no对应的比特位。

下一篇将更新一些关于MySQL的高频面试题总结

MySQL高频问题:事务及慢SQL优化全解析-CSDN博客

http://www.xdnf.cn/news/19044.html

相关文章:

  • MySQL 中如何解决深度分页的问题?
  • 嵌入式接口通识知识之RGB接口
  • 基于机器学习的多个模型的预测Backtrader自动化交易系统设计
  • 关于shell命令的扩展
  • AlexNet:点燃深度学习革命的「卷积神经网络之王」
  • 接口测试工具:Postman详解
  • 计算机专业考研备考建议
  • idea2025.2中maven编译中文乱码
  • 编译esp32报错解决办法
  • 机器学习复习
  • 【go】三端实时反馈系统的设计,websocket实现
  • 12.压缩和打包
  • 创建第一个 Electron 应用:Hello World 示例
  • 【算法】15. 三数之和
  • 阻塞,非阻塞,同步,异步的理解
  • Linux -- 进程间通信【命名管道】
  • 【golang长途旅行第34站】网络编程
  • GPT-5原理
  • mybatis.xml直接读取配置文件(application.yml)中的数据
  • 图扑 HT 农林牧数据可视化监控平台
  • 计算机视觉----opencv(图像轮毂绘制(大小选择,排序,外接图形绘制),轮廓的近似,模板的匹配)
  • 10迁移TiDB数据库数据到GaussDB
  • 前端vue3入门学习
  • OSS Nginx 反代提示 SignatureDoesNotMatch
  • 【面试系列】谈谈你对数据库ACID的理解
  • 2023年12月GESP5级C++真题解析,包括选择判断和编程
  • 【MFC教程】C++基础:01 小黑框跑起来
  • 嵌入式学习 day61 DHT11、I2C
  • 数据分析编程第六步:大数据运算
  • MySQL-索引(下)