本篇文章主要介绍 Redo Log 和 Undo Log:

  1. 利用 Redo Log 和 Undo Log 实现本地事务的原子性、持久性
  2. Redo Log 的写回策略
  3. Redo Log Buffer 的刷盘时机

日志:Redo Log 和 Undo Log · 语雀 (yuque.com)

通过写入日志来保证原子性、持久性是业界的主流做法。

介绍 Redo Log 和 Undo Log

Redo Log 是什么:Redo Log 被称为重做日志。

Undo Log 是什么:Undo Log 被称为撤销日志、回滚日志。

技术是为了解决问题而生的,通过 Redo Log 我们可以实现崩溃恢复,防止数据更新丢失,保证事务的持久性。也就是说,在机器故障恢复后,系统仍然能够通过 Redo Log 中的信息,持久化已经提交的事务的操作结果。

技术是为了解决问题而生的,Undo Log 的作用 / 功能:

  • 事务回滚:可以对提前写入的数据变动进行擦除,实现事务回滚,保证事务的原子性。
  • 实现 MVCC 机制:Undo Log 也用于实现 MVCC 机制,存储记录的多个版本的 Undo Log,形成版本链。

Undo Log 中存储了回滚需要的数据。在事务回滚或者崩溃恢复时,根据 Undo Log 中的信息对提前写入的数据变动进行擦除。


Redo Log 和 Undo Log 都是用于实现事务的特性,并且都是在存储引擎层实现的。由于只有 InnoDB 存储引擎支持事务,因此只有使用 InnoDB 存储引擎的表才会使用 Redo Log 和 Undo Log。

实现本地事务的原子性、持久性

“Write-Ahead Log”日志方案

MySQL 的 InnoDB 存储引擎使用“Write-Ahead Log”日志方案实现本地事务的原子性、持久性。

“提前写入”(Write-Ahead),就是在事务提交之前,允许将变动数据写入磁盘。与“提前写入”相反的就是,在事务提交之前,不允许将变动数据写入磁盘,而是等到事务提交之后再写入。

“提前写入”的好处是:有利于利用空闲 I/O 资源。但“提前写入”同时也引入了新的问题:在事务提交之前就有部分变动数据被写入磁盘,那么如果事务要回滚,或者发生了崩溃,这些提前写入的变动数据就都成了错误。“Write-Ahead Log”日志方案给出的解决办法是:增加了一种被称为 Undo Log 的日志,用于进行事务回滚。

变动数据写入磁盘前,必须先记录 Undo Log,Undo Log 中存储了回滚需要的数据。在事务回滚或者崩溃恢复时,根据 Undo Log 中的信息对提前写入的数据变动进行擦除。


“Write-Ahead Log”在崩溃恢复时,会经历以下三个阶段:

  • 分析阶段(Analysis):该阶段从最后一次检查点(Checkpoint,可理解为在这个点之前所有应该持久化的变动都已安全落盘)开始扫描日志,找出所有没有 End Record 的事务,组成待恢复的事务集合(一般包括 Transaction Table 和 Dirty Page Table)。
  • 重做阶段(Redo):该阶段依据分析阶段中,产生的待恢复的事务集合来重演历史(Repeat History),找出所有包含 Commit Record 的日志,将它们写入磁盘,写入完成后增加一条 End Record,然后移除出待恢复事务集合。
  • 回滚阶段(Undo):该阶段处理经过分析、重做阶段后剩余的待恢复事务集合,此时剩下的都是需要回滚的事务(被称为 Loser),根据 Undo Log 中的信息回滚这些事务。

MySQL 中一条 SQL 更新语句的执行过程

以下的执行过程限定在,使用 InnoDB 存储引擎的表

  1. 事务开始

  2. 申请加锁:表锁、MDL 锁、行锁、索引区间锁(看情况加哪几种锁)

  3. 执行器找存储引擎取数据。

    1. 如果记录所在的数据页本来就在内存(innodb_buffer_cache)中,存储引擎就直接返回给执行器;
    2. 否则,存储引擎需要先将该数据页从磁盘读取到内存,然后再返回给执行器。
  4. 执行器拿到存储引擎给的行数据,进行更新操作后,再调用存储引擎接口写入这行新数据。

  5. 存储引擎将回滚需要的数据记录到 Undo Log,并将这个更新操作记录到 Redo Log,此时 Redo Log 处于 prepare 状态。并将这行新数据更新到内存(innodb_buffer_cache)中。同时,然后告知执行器执行完成了,随时可以提交事务。

  6. 手动事务 commit:执行器生成这个操作的 Binary Log,并把 Binary Log 写入磁盘。

  7. 执行器调用存储引擎的提交事务接口,存储引擎把刚刚写入的 Redo Log 改成 commit 状态。

  8. 事务结束

其中第 5 步,将这个更新操作记录到 Redo Log。生成的 Redo Log 是存储在 Redo Log Buffer 后就返回,还是必须写入磁盘后才能返回呢?

这就是 Redo Log 的写入策略,Redo Log 的写入策略由 innodb_flush_log_at_trx_commit 参数控制,该参数不同的值对应不同的写入策略。

还有第 6 步,把 Binary Log 写入磁盘和 Redo Log 一样,也有相应的写回策略,由参数 sync_binlog 控制。

通常我们说 MySQL 的“双 1”配置,指的就是 sync_binlog 和 innodb_flush_log_at_trx_commit 都设置成 1。也就是说,一个事务提交前,需要等待两次刷盘,一次是 Redo Log 刷盘(prepare 阶段),一次是 Binary Log 刷盘。

Redo Log 的两阶段提交 & 崩溃恢复

在上面【MySQL 中一条 SQL 更新语句的执行过程】部分,最后将 Redo Log 的写入拆成了两个步骤:prepare 和 commit,这就是"两阶段提交"。

“两阶段提交”的作用 / 目的:

  • 将事务设置为 prepare,为了在崩溃重启时,能够知道事务的状态
  • 保证两份日志(Binary Log 和 Redo Log)之间的逻辑一致。

如果先写 Redo Log,再写 Binary Log 或者 先写 Binary Log,再写 Redo Log,写入第一个日志后,如果此时发生了崩溃,那么第二个日志没有写入,就造成了两个日志的不一致。数据库的状态就有可能和用 Binary Log 恢复出来的库的状态不一致。备库利用 Binary Log 进行数据同步,就会出现主备库数据不一致的问题。具体的讲解可以看极客时间的专栏《MySQL实战45讲》

而使用“两阶段提交”,遵守“崩溃恢复时,判断事务该提交、还是该回滚的规则”,就可以保证两份日志(Binary Log 和 Redo Log)之间的逻辑一致。

“崩溃恢复时,判断事务该提交、还是该回滚的规则”如下:

  1. 如果 Redo Log 里面的事务是完整的,也就是已经有了 commit 标识,那么利用该 Redo Log 中的信息,持久化事务的操作结果;
  2. 如果 Redo Log 里面的事务只有完整的 prepare,则判断对应事务的 Binary Log 是否存在并完整:

    a. 如果是,利用该 Redo Log 中的信息,持久化事务的操作结果;

    b. 如果否,则回滚事务,根据 Undo Log 中的信息对提前写入的数据变动进行擦除。

如果事务写入 Redo Log 处于 prepare 阶段之后、写 Binary Log 之前,发生了崩溃(也就是时刻 A 发生了崩溃),由于此时 Binary Log 还没写,Redo Log 也还没处于 commit 状态,所以崩溃恢复的时候,这个事务会回滚。这时 Binary Log 还没写,所以也不会传到备库。主库和备库的数据状态一致。

如果事务写入 Binary Log 之后,Redo Log 还没处于 commit 状态之前,发生了崩溃(也就是时刻 B 发生了崩溃),根据崩溃恢复时的判断规则中第 2 条,Redo Log 处于 prepare 阶段,Binary Log 完整,所以崩溃恢复的时候,会利用该 Redo Log 中的信息,持久化事务的操作结果。这时 Binary Log 已经写了,所以会传到备库。主库和备库的数据状态一致。


Binary Log 的写入在崩溃恢复时,判断事务该提交还是该回滚时,起到了至关重要的作用,只有 Binary Log 写入成功才能保证两份日志(Binary Log 和 Redo Log)之间的逻辑一致,才能考虑提交。

Redo Log 配置的选项

  • innodb_log_buffer_size:Redo Log Buffer 的内存大小
  • innodb_log_file_size:单个 Redo Log 文件的空间大小
  • innodb_log_files_in_group:Redo Log 文件的数量(默认值是 2)
  • innodb_log_group_home_dir:Redo Log 文件的存储目录(默认值是 .\ ,即数据目录)
  • innodb_flush_log_at_trx_commit:Redo Log 的写入策略

Redo Log 的写入策略

我们在【MySQL 中一条 SQL 更新语句的执行过程】部分的第 5 步中说:存储引擎将这行新数据更新到内存(innodb_buffer_cache)中。同时,将这个更新操作记录到 Redo Log,此时 Redo Log 处于 prepare 状态。然后告知执行器执行完成了,随时可以提交事务。

生成的 Redo Log 是存储在 Redo Log Buffer 后就返回,还是必须写入磁盘后才能返回呢?这就是 Redo Log 的写入策略。Redo Log 的写入策略由 innodb_flush_log_at_trx_commit 参数控制。

我们可以通过修改该参数的值,设置 Redo Log 的写入策略,该参数可选的值有 3 个:

  • 值为 0 :表示每次事务提交时,只是把 Redo Log 存储到内存(Redo Log Buffer)就返回,不关心写入文件
  • 值为 1 :表示每次事务提交时,将 Redo Log Buffer 中的内容写入并同步到文件后才能返回(write + fsync 才能返回,这是参数的默认值)
  • 值为 2 :表示每次事务提交时,只是把 Redo Log Buffer 中的内容写入内核缓冲区,但不对文件进行同步,何时同步由操作系统来决定(write,fsync 的时机由操作系统决定)

Redo Log 文件组

MySQL 的数据目录(使用 show variables like 'datadir' 查看)下默认有两个名为 ib_logfile0 和

ib_logfile1 的文件,Redo Log Buffer 中的 Redo Log 默认情况下就是刷新到这两个磁盘文件中。

数据目录的位置也可以通过以下命令查看:select @@datadir;




如果我们对默认的 Redo Log 文件组不满意,可以通过下边几个启动参数来调节:

  • innodb_log_buffer_size:每个 Redo Log 文件的空间大小
  • innodb_log_file_size:每个 Redo Log 文件的最大空间大小
  • innodb_log_files_in_group:Redo Log 文件组中所有 Redo Log 文件的数量(默认值是 2,最大值是 100)
  • innodb_log_group_home_dir:Redo Log 文件的存储目录(默认值是 .\ ,即 MySQL 的数据目录)

从上边的描述中可以看到,磁盘上的 Redo Log 文件不只一个,而是以一个 日志文件组 的形式出现的。这些文件

以 ib_logfile[数字] ( 数字 可以是 0 、 1 、 2 ...)的形式进行命名。

在将 Redo Log 写入 日志文件组 时,是从 ib_logfile0 开始写,如果 ib_logfile0 写满了,就接着 ib_logfile1 写,同理, ib_logfile1 写满了就去写 ib_logfile2 ,依此类推。

如果写到最后一个文件该怎么办呢?那就重新转到 ib_logfile0 继续写,所以整个过程如下图所示:

总共的 Redo Log 文件空间大小其实就是:innodb_log_file_size × innodb_log_files_in_group 。(单个文件的空间大小 * 文件组中的文件个数)

如果采用循环使用的方式向 Redo Log 文件组里写数据的话,那就会造成追尾,也就是后写入的 Redo Log 覆盖掉前边写的 Redo Log。为了解决 Redo Log 的覆盖写入问题,InnoDB 的设计者提出了 checkpoint 的概念。

Redo Log 写入 Redo Log Buffer

Redo Log 的格式

InnoDB 的设计者为 Redo Log 定义了多种类型,以应对事务对数据库的不同修改场景,但是绝大部分类型的 Redo Log 都有下边这种通用的结构:

各个部分的详细释义如下:

  • type:该条 Redo Log 的类型
  • space ID :表空间ID
  • page number :页号
  • data :该条 Redo Log 的具体内容

在 MySQL 5.7.21 这个版本中,InnoDB 的设计者一共为 Redo Log 设计了 53 种不同的类型。各种类型的 Redo Log 的不同之处在于 data 的具体结构不同。

Redo Log 的具体格式可以看掘金小册《MySQL 是怎样运行的:从根儿上理解 MySQL》

这些类型的 Redo Log 既包含 物理 层面的意思,也包含 逻辑 层面的意思,具体指:

  • 物理层面看,这些日志都指明了对哪个表空间的哪个页进行了什么修改。
  • 逻辑层面看,在系统奔溃重启时,并不能直接根据 Redo Log 中的信息,将页面内的某个偏移量处恢复成某个数据,而是需要调用一些事先准备好的函数,执行完这些函数后才可以将页面恢复成系统奔溃前的样子。

总结来说,Redo Log 中记录的是该操作对哪个表空间的哪个页的哪个偏移量进行了什么修改。

Mini-Transaction

一个事务可能包含多条 SQL 语句,每一条 SQL 语句可能包含多个「对底层页面的操作」,每个「对底层页面的操作」可能包含多个 Redo Log。这样的一个「对底层页面的操作」的过程被称为 Mini-Transaction,简称 mtr。

「对底层页面的操作」比如说:

  • 向聚簇索引对应的 B+ 树的某个页面中插入一条记录,插入一条记录这个操作可能包含多个 Redo Log

我们需要保证一个「对底层页面的操作」对应的多个 Redo Log 不可分割,即一个「对底层页面的操作」是原子的,这个操作对应的 Redo Log 要么都写入磁盘,要么都不写入磁盘。所以 InnoDB 的设计者规定在执行这些需要保证原子性的操作时必须以 组 的形式来记录 Redo Log,在进行奔溃恢复时,针对某个组中的 Redo Log,要么把全部的 Redo Log 都恢复掉,要么一个 Redo Log 也不恢复。

那么 InnoDB 的设计者是怎么做到分组的呢?InnoDB 的设计者在一个「对底层页面的操作」的最后一个 Redo Log 后面加上一个特殊类型的 Redo Log。相当于某个需要保证原子性的操作产生的一系列 Redo Log 必须要以一个特殊类型的 Redo Log 结尾,这样在奔溃恢复时:

  • 只有当解析到特殊类型的 Redo Log 时,才认为解析到了一组完整的 Redo Log,才会进行恢复。
  • 否则的话直接放弃前边解析到的 Redo Log。

Redo Log Buffer

Redo Log Buffer 就是在服务器启动时,向操作系统申请的大一片连续的内存空间。

这片连续的内存空间被划分为若干个连续的用来存储 Redo Log 的数据页。

用来存储 Redo Log 的数据页被称为 Redo Log Block。

我们可以通过启动参数 innodb_log_buffer_size 来指定 Redo Log Buffer 的大小。

  • LOG_BLOCK_CHECKPOINT_NO :表示 checkpoint 的序号

Redo Log 写入 Redo Log Buffer

我们前边说过一个 mtr 执行过程中可能产生若干个 Redo Log ,这些 Redo Log 是一个不可分割的组,所以其实并不是每生成一个 Redo Log,就将其插入到 Redo Log Buffer 中,而是每个 mtr 运行过程中产生的日志先暂时存到一个地方,当该 mtr 结束的时候,将过程中产生的一组 Redo Log 再全部复制到 Redo Log Buffer 中。

不同的事务可能是并发执行的,所以不同事务的 mtr 可能是交替执行的。每当一个 mtr 执行完成时,伴随

该 mtr 生成的一组 Redo Log 就需要被复制到 Redo Log Buffer 中,也就是说不同事务的 mtr 可能是交替写入 Redo Log Buffer 的。

Redo Log Buffer 中 Redo Log 的刷盘时机

mtr 运行过程中产生的一组 Redo Log 在 mtr 结束时会被复制到 Redo Log Buffer 中,在一些情况下 Redo Log Buffer 中的 Redo Log 会被写回磁盘,Redo Log 的刷盘时机如下:

  • Redo Log Buffer 的空间不足时,执行刷盘操作
  • 一个事务提交时,执行刷盘操作(需要设置指定参数)
  • 将某个脏页刷新到磁盘前,会先保证该脏页对应的 Redo Log 刷新到磁盘中(Redo Log 是顺序写入的,因此在将某个脏页对应的 Redo Log 从 Redo Log Buffer 刷新到磁盘时,也会保证将在其之前产生的 Redo Log 也刷新到磁盘中。
  • 后台线程不停的执行刷盘操作
  • 正常关闭服务器时,执行刷盘操作
  • 做 checkpoint 时,执行刷盘操作

Redo Log Buffer 的空间不足时,执行刷盘操作

Redo Log Buffer 的空间是有限的,空间大小由 innodb_log_buffer_size 来指定。

InnoDB 的设计者认为:如果 Redo Log Buffer 的内存被占用 1 / 2,就需要把 Redo Log Buffer 中的 Redo Log 刷新到磁盘。


一个事务提交时,执行刷盘操作

在前面【Redo Log 的写入策略】部分,讲到我们可以通过设置 innodb_flush_log_at_trx_commit 参数的值,在事务提交时执行刷盘操作后才能返回。

事务提交时执行刷盘操作后才能返回是 Redo Log 的默认写入策略。


后台线程不停的执行刷盘操作

后台有一个线程,每秒都会执行一次刷盘操作。后台线程执行刷盘操作的频率可以通过参数设置。

具体通过哪个参数设置,我也不清楚。


正常关闭服务器时,执行刷盘操作


做 checkpoint 时,执行刷盘操作


等等

Undo Log 的写回策略

MySQL中的 Undo Log 严格的讲不是 Log,而是数据,因此它的管理和落盘都跟数据是一样的:

  • Undo 的磁盘结构并不是顺序的,而是像数据一样按 Page 管理
  • Undo 写入时,也像数据一样产生对应的 Redo Log
  • Undo 的 Page 也像数据一样缓存在 Buffer Pool 中,跟数据 Page 一起做 LRU 换入换出,以及刷脏。Undo Page 的刷脏也像数据一样要等到对应的 Redo Log 落盘之后

之所以这样实现,首要的原因是 MySQL 中的 Undo Log 不只是承担 Crash Recovery 时保证 Atomic 的作用,更需要承担 MVCC 对历史版本的管理的作用,设计目标是高事务并发,方便的管理和维护。因此当做数据更合适。

但既然还叫 Log,就还是需要有 Undo Log 的责任,那就是保证 Crash Recovery 时,如果看到数据的修改,一定要能看到其对应 Undo 的修改,这样才有机会通过事务的回滚保证 Crash Atomic。标准的 Undo Log 这一步是靠 WAL 实现的,也就是要求 Undo 写入先于数据落盘。而 InnoDB 中 Undo Log 作为一种特殊的数据,这一步是通过 redo 的 min-transaction 保证的,简单的说就是数据的修改和对应的 Undo 修改,他们所对应的 Redo Log 被放到同一个 min-transaction 中,同一个 min-transaction 中的所有 Redo Log 在 Crash Recovery 时以一个整体进行重放,要么全部重放,要么全部丢弃。

作者:CatKang

链接:https://www.zhihu.com/question/267595935/answer/2204949497

来源:知乎

Undo Log 配置的选项

  • innodb_max_undo_log_size:值为,单个 Undo Log 最大可占用的字节存储空间;单位为,字节(默认值是 1 G)。
  • innodb_undo_directory:Undo Log 文件的存储目录(默认值是 .\ ,即数据目录)表示回滚日志的存储目录是数据目录,数据目录的位置可以通过查询变量“datadir”来查看。
  • innodb_undo_log_encrypt:Undo Log 是否加密(默认值是 off,即不加密)。
  • innodb_undo_log_truncate:Undo Log 是否自动截断回收(默认值是 on,即自动截断回收)。这个变量有效的前提是设置了使用独立表空间。
  • innodb_undo_tablespaces:值为 Undo Log 的独立表空间的数量(默认值是 0,即 Undo Log 没有独立的表空间,默认记录到共享表空间 ibdata 文件中)

参考资料

20 | 日志(下):系统故障,如何恢复数据? (geekbang.org)

MySQL 是怎样运行的:从根儿上理解 MySQL - 小孩子4919 - 掘金课程 (juejin.cn)

02 | 日志系统:一条SQL更新语句是如何执行的?-极客时间 (geekbang.org)

关于Innodb undo log的刷新时机? - 知乎 (zhihu.com)

日志:Redo Log 和 Undo Log的更多相关文章

  1. 详细分析MySQL事务日志(redo log和undo log)

    innodb事务日志包括redo log和undo log.redo log是重做日志,提供前滚操作,undo log是回滚日志,提供回滚操作. undo log不是redo log的逆向过程,其实它 ...

  2. 详细分析MySQL事务日志(redo log和undo log) 表明了为何mysql不会丢数据

    innodb事务日志包括redo log和undo log.redo log是重做日志,提供前滚操作,undo log是回滚日志,提供回滚操作. undo log不是redo log的逆向过程,其实它 ...

  3. InnoDB事务日志(redo log 和 undo log)详解

    数据库通常借助日志来实现事务,常见的有undo log.redo log,undo/redo log都能保证事务特性,undolog实现事务原子性,redolog实现事务的持久性. 为了最大程度避免数 ...

  4. 必须了解的mysql三大日志-binlog、redo log和undo log

    日志是 mysql 数据库的重要组成部分,记录着数据库运行期间各种状态信息.mysql日志主要包括错误日志.查询日志.慢查询日志.事务日志.二进制日志几大类.作为开发,我们重点需要关注的是二进制日志( ...

  5. MySQL日志系统bin log、redo log和undo log

    MySQL日志系统bin log.redo log和undo log   今人不见古时月,今月曾经照古人. 简介:日志是MySQL数据库的重要组成部分,记录着数据库运行期间各种状态信息,主要包括错误日 ...

  6. 【Mysql】三大日志 redo log、bin log、undo log

    @ 目录 redo log(物理日志\重做日志) binlog(逻辑日志/归档日志) update语句执行流程 Uodolog(回滚日志/重做日志) undo log+redo log保证持久性 re ...

  7. mysql事务(一)——redo log与undo log

    数据事务 即支持ACID四大特性. A:atomicity          原子性——事务中所有操作要么全部执行成功,要么全部执行失败,回滚到初始状态 C:consistency     一致性—— ...

  8. bin log、redo log、undo log和MVVC

    logs innodb事务日志包括redo log和undo log.redo log是重做日志,提供前滚操作,undo log是回滚日志,提供回滚操作. undo log不是redo log的逆向过 ...

  9. 深入理解MySQL系列之redo log、undo log和binlog

    事务的实现 redo log保证事务的持久性,undo log用来帮助事务回滚及MVCC的功能. InnoDB存储引擎体系结构 redo log Write Ahead Log策略 事务提交时,先写重 ...

随机推荐

  1. React技巧之检查元素是否可见

    原文链接:https://bobbyhadz.com/blog/react-check-if-element-in-viewport 作者:Borislav Hadzhiev 正文从这开始~ 总览 在 ...

  2. python小题目练习(七)

    题目:实现如下图所示结果 代码实现: """Author:mllContent:模拟火车订票系统Date:2020-11-16"""# 定义 ...

  3. c# 国内外ORM 框架 dapper efcore sqlsugar freesql hisql sqlserver数据常规插入测试性能对比

    c# 国内外ORM 框架 dapper efcore sqlsugar freesql hisql sqlserver数据常规插入测试性能对比对比 在6.22 号发布了 c# sqlsugar,his ...

  4. [ 1 x 1 ] Convolution-1*1卷积的作用

    一.卷积神经网络中的卷积(Convolution in a convoluted neural network) 具体内容亲参考<深度学习>. 二.1*1卷积(one by one con ...

  5. dubbox 入门demo

    1.Dubbox简介 Dubbox 是一个分布式服务架构,其前身是阿里巴巴开源项目 Dubbo,被国内电商及互联网项目使用,后期阿里巴巴停止了该项目的维护,当当网便在 Dubbo 基础上进行优化,并继 ...

  6. pop!_OS换国内源

    今天给电脑换源了,虽然本来的源大部分好像也都连的上(不知道是不是错觉)换的这个:阿里云镜像开源站 先进入存放源的目录:` cd /etc/apt 里面有这些文件: sources.list是要修改的, ...

  7. Three.js系列: 在元宇宙看电影,享受 VR 视觉盛宴

    本文 gihtub 地址: https://github.com/hua1995116/Fly-Three.js 最近元宇宙的概念很火,并且受到疫情的影响,我们的出行总是受限,电影院也总是关门,但是在 ...

  8. 从入门到爱上Git

    时间不在于你拥有多少,而在于你怎样使用------时之沙 · 艾克 一.Git设置 1.1 Git全局设置 当我们安装好Git以后,我们需要对Git进行账号.邮箱的设置 设置用户信息 git conf ...

  9. java中AOP的环绕通知

    pom.xml <dependencies> <dependency> <groupId>org.springframework</groupId> & ...

  10. IDEA快捷键之晨讲篇

    IDEA之html快捷键 快捷键 释义 ! 生成HTML的初始格式 ---- ---- 标签名*n 生成n个相同的标签 ---- ---- 标签>标签 生成父子级标签(包含) ---- ---- ...