InnoDB 这块我花的时间最多,因为它讲的是引擎内部怎么运转,以理解为主,不是背参数。我的经验是:不要死记那些大小数值,重要的是能在脑子里建立画面——内存里有什么、磁盘上有什么、数据怎么流动,而且看完能自己讲出来才算过。
按小节来说,逻辑存储结构(表空间→段→区→页→行)理解层次就够了;内存/磁盘架构最重要,Buffer Pool、Log Buffer、redo log、undo log、doublewrite 这些要能说出谁负责干什么,后面监控和调参全靠它们;后台线程知道几个核心线程在干嘛就行;事务原理(redo / undo)是运维的命根子,得能用自己的话解释清楚。
一、逻辑存储结构
磁盘操作 IO 的本质是对表空间文件操作,其最小单元被分为页,所以磁盘操作几乎都是对页的操作。
表空间 - 段 - 区 - 页 - 行(这里的页和索引查询的叶子节点毫无关系,只是把 ibd 文件划分了多个层次)。

1KB=1024B
**1M=1024KB=1024\*1024B**
不需要记住具体大小。
MySQL 的数据文件在 /var/lib/mysql,这台机器上挂载到了 /opt/mysql/data:
root@VM-0-12-ubuntu:/opt/mysql/data# ll
total 107520
drwxr-xr-x 10 lxd root 4096 Aug 22 20:41 ./
drwxr-xr-x 5 root root 4096 Aug 14 18:14 ../
drwxr-x--- 2 lxd docker 4096 Aug 21 14:16 adpart/
-rw-r----- 1 lxd docker 56 Aug 14 18:33 auto.cnf
-rw-r----- 1 lxd docker 2996555 Aug 14 18:33 binlog.000001
-rw-r----- 1 lxd docker 36540 Aug 22 20:41 binlog.000002
-rw-r----- 1 lxd docker 1120 Aug 24 17:47 binlog.000003
-rw-r----- 1 lxd docker 48 Aug 22 20:41 binlog.index
-rw------- 1 lxd docker 1709 Aug 14 18:33 ca-key.pem
-rw-r--r-- 1 lxd docker 1112 Aug 14 18:33 ca.pem
-rw-r--r-- 1 lxd docker 1112 Aug 14 18:33 client-cert.pem
-rw------- 1 lxd docker 1705 Aug 14 18:33 client-key.pem
-rw-r----- 1 lxd docker 4194304 Aug 24 21:11 '#ib_16384_0.dblwr'
-rw-r----- 1 lxd docker 12582912 Aug 21 15:12 '#ib_16384_1.dblwr'
-rw-r----- 1 lxd docker 5457 Aug 22 20:41 ib_buffer_pool
-rw-r----- 1 lxd docker 12582912 Aug 24 21:09 ibdata1
-rw-r----- 1 lxd docker 12582912 Aug 22 20:41 ibtmp1
drwxr-x--- 2 lxd docker 4096 Aug 22 20:41 '#innodb_redo'/
drwxr-x--- 2 lxd docker 4096 Aug 22 20:41 '#innodb_temp'/
drwxr-x--- 2 lxd docker 4096 Aug 18 21:51 itcast/
drwxr-x--- 2 lxd docker 4096 Aug 14 18:33 mysql/
# 表空间文件
-rw-r----- 1 lxd docker 31457280 Aug 24 17:47 mysql.ibd
-rw-r----- 1 lxd docker 466 Aug 24 18:57 mysql-slow.log
lrwxrwxrwx 1 lxd docker 27 Aug 22 20:41 mysql.sock -> /var/run/mysqld/mysqld.sock
-rw-r----- 1 lxd docker 132 Aug 14 18:33 mysql_upgrade_history
drwxr-x--- 2 lxd docker 4096 Aug 14 18:33 performance_schema/
-rw------- 1 lxd docker 1705 Aug 14 18:33 private_key.pem
-rw-r--r-- 1 lxd docker 452 Aug 14 18:33 public_key.pem
-rw-r--r-- 1 lxd docker 1112 Aug 14 18:33 server-cert.pem
-rw------- 1 lxd docker 1705 Aug 14 18:33 server-key.pem
drwxr-x--- 2 lxd docker 4096 Aug 14 18:33 sys/
drwxr-x--- 2 lxd docker 4096 Aug 16 13:46 test/
-rw-r----- 1 lxd docker 16777216 Aug 24 17:49 undo_001
-rw-r----- 1 lxd docker 16777216 Aug 24 21:11 undo_002
# 进入到具体的库,有很多表空间文件
root@VM-0-12-ubuntu:/opt/mysql/data/itcast# ll
total 1448
drwxr-x--- 2 lxd docker 4096 Aug 18 21:51 ./
drwxr-xr-x 10 lxd root 4096 Aug 22 20:41 ../
-rw-r----- 1 lxd docker 114688 Aug 19 13:43 account.ibd
-rw-r----- 1 lxd docker 114688 Aug 17 22:10 course.ibd
-rw-r----- 1 lxd docker 131072 Aug 17 18:58 dept.ibd
-rw-r----- 1 lxd docker 131072 Aug 17 21:54 emp.ibd
-rw-r----- 1 lxd docker 114688 Aug 17 18:27 emp_test.ibd
-rw-r----- 1 lxd docker 114688 Aug 24 21:09 score.ibd
-rw-r----- 1 lxd docker 147456 Aug 17 22:10 student_course.ibd
-rw-r----- 1 lxd docker 114688 Aug 17 22:10 student.ibd
-rw-r----- 1 lxd docker 131072 Aug 17 22:23 tb_user_edu.ibd
-rw-r----- 1 lxd docker 114688 Aug 17 22:23 tb_user.ibd
-rw-r----- 1 lxd docker 114688 Aug 16 17:00 test_emp.ibd
-rw-r----- 1 lxd docker 131072 Aug 17 17:44 user.ibd
二、架构
1. 内存结构
先看架构图,左边是内存结构,右边是磁盘结构:

修改 SQL 操作的是缓冲池,不是磁盘:没有数据就从磁盘加载并缓存,在缓冲池里改,变成脏页再写回磁盘。

缓冲池和更改缓冲区都是降低 IO 的,只不过非唯一二级索引随机 IO 代价太高了,才需要单独设立一个更改缓冲区。

针对缓冲池的查询,如果系统判断哈希查询效率高于索引查询,就用哈希。

日志缓冲区,后面会详细介绍。

show variables like 'innodb_log_buffer_size';

show variables like 'innodb_flush_log_at_trx_commit';

2. 磁盘结构
磁盘操作 IO 的本质是对表空间文件操作,其最小单元被分为页,所以磁盘操作几乎都是对页的操作。
root@VM-0-12-ubuntu:/opt/mysql/data# ll
total 107520
drwxr-xr-x 10 lxd root 4096 Aug 22 20:41 ./
drwxr-xr-x 5 root root 4096 Aug 14 18:14 ../
drwxr-x--- 2 lxd docker 4096 Aug 21 14:16 adpart/
-rw-r----- 1 lxd docker 56 Aug 14 18:33 auto.cnf
-rw-r----- 1 lxd docker 2996555 Aug 14 18:33 binlog.000001
-rw-r----- 1 lxd docker 36540 Aug 22 20:41 binlog.000002
-rw-r----- 1 lxd docker 1120 Aug 24 17:47 binlog.000003
-rw-r----- 1 lxd docker 48 Aug 22 20:41 binlog.index
-rw------- 1 lxd docker 1709 Aug 14 18:33 ca-key.pem
-rw-r--r-- 1 lxd docker 1112 Aug 14 18:33 ca.pem
-rw-r--r-- 1 lxd docker 1112 Aug 14 18:33 client-cert.pem
-rw------- 1 lxd docker 1705 Aug 14 18:33 client-key.pem
-rw-r----- 1 lxd docker 4194304 Aug 24 21:11 '#ib_16384_0.dblwr'
-rw-r----- 1 lxd docker 12582912 Aug 21 15:12 '#ib_16384_1.dblwr'
-rw-r----- 1 lxd docker 5457 Aug 22 20:41 ib_buffer_pool
-rw-r----- 1 lxd docker 12582912 Aug 24 21:09 ibdata1
-rw-r----- 1 lxd docker 12582912 Aug 22 20:41 ibtmp1
drwxr-x--- 2 lxd docker 4096 Aug 22 20:41 '#innodb_redo'/
drwxr-x--- 2 lxd docker 4096 Aug 22 20:41 '#innodb_temp'/
drwxr-x--- 2 lxd docker 4096 Aug 18 21:51 itcast/
drwxr-x--- 2 lxd docker 4096 Aug 14 18:33 mysql/
-rw-r----- 1 lxd docker 31457280 Aug 24 17:47 mysql.ibd
-rw-r----- 1 lxd docker 466 Aug 24 18:57 mysql-slow.log
lrwxrwxrwx 1 lxd docker 27 Aug 22 20:41 mysql.sock -> /var/run/mysqld/mysqld.sock
-rw-r----- 1 lxd docker 132 Aug 14 18:33 mysql_upgrade_history
drwxr-x--- 2 lxd docker 4096 Aug 14 18:33 performance_schema/
-rw------- 1 lxd docker 1705 Aug 14 18:33 private_key.pem
-rw-r--r-- 1 lxd docker 452 Aug 14 18:33 public_key.pem
-rw-r--r-- 1 lxd docker 1112 Aug 14 18:33 server-cert.pem
-rw------- 1 lxd docker 1705 Aug 14 18:33 server-key.pem
drwxr-x--- 2 lxd docker 4096 Aug 14 18:33 sys/
drwxr-x--- 2 lxd docker 4096 Aug 16 13:46 test/
-rw-r----- 1 lxd docker 16777216 Aug 24 17:49 undo_001
-rw-r----- 1 lxd docker 16777216 Aug 24 21:11 undo_002

# 系统表空间 /opt/mysql/data 下,即 /var/lib/mysql 下
-rw-r----- 1 lxd docker 12582912 Aug 24 21:09 ibdata1
# 独立表空间文件(每个库内的ibd和mysql.ibd)
drwxr-x--- 2 lxd docker 4096 Aug 21 14:16 adpart/
drwxr-x--- 2 lxd docker 4096 Aug 18 21:51 itcast/
-rw-r----- 1 lxd docker 31457280 Aug 24 17:47 mysql.ibd

# undo 撤销表空间
-rw-r----- 1 lxd docker 16777216 Aug 24 17:49 undo_001
-rw-r----- 1 lxd docker 16777216 Aug 24 21:11 undo_002
# 临时表空间
-rw-r----- 1 lxd docker 12582912 Aug 22 20:41 ibtmp1

# redo log 重做日志
drwxr-x--- 2 lxd docker 4096 Aug 22 20:41 '#innodb_redo'/
# 双写缓冲区:存放脏页的位置
-rw-r----- 1 lxd docker 4194304 Aug 24 21:11 '#ib_16384_0.dblwr'
-rw-r----- 1 lxd docker 12582912 Aug 21 15:12 '#ib_16384_1.dblwr'
3. 后台线程
内存数据刷新到磁盘,中间要经过操作系统的这一层:

涉及到的线程有这么几个:

总结一下:InnoDB 引擎的体系结构就是,业务操作(增删改查)操作的是缓冲区,缓冲区没有数据就把磁盘数据加载到这里再操作。缓冲区的数据会按一定频率通过后台线程刷新到磁盘,在磁盘上做数据持久化(数据、索引等)。
三、事务原理
1. 介绍

2. redolog 和 undo log

redo log
主要用于脏页刷新到磁盘发生错误时进行数据恢复,保证事务持久性。

- 进行操作
- 缓冲区如果没东西,会从磁盘读,然后进行增删改查变为脏页
- 记录数据页变化到 redo log
- 变化日志写入磁盘中(顺序磁盘 IO,性能高)
- 变更数据写入磁盘(随机磁盘 IO,性能低)
每隔一段时间就会清理不需要的 redo log 日志:

redo log 文件大小固定,自动删除最久的数据。
磁盘操作 IO 的本质是对表空间文件操作,其最小单元被分为页,所以磁盘操作几乎都是对页的操作。
重做缓冲文件在缓冲区,没办法演示,了解即可。
# redo log日志位置
drwxr-x--- 2 lxd docker 4096 Aug 22 20:41 '#innodb_redo'/
root@VM-0-12-ubuntu:/opt/mysql/data/#innodb_redo# ll
total 102408
drwxr-x--- 2 lxd docker 4096 Aug 22 20:41 ./
drwxr-xr-x 10 lxd root 4096 Aug 22 20:41 ../
-rw-r----- 1 lxd docker 3276800 Aug 22 20:41 '#ib_redo10_tmp'
-rw-r----- 1 lxd docker 3276800 Aug 22 20:41 '#ib_redo11_tmp'
-rw-r----- 1 lxd docker 3276800 Aug 22 20:41 '#ib_redo12_tmp'
-rw-r----- 1 lxd docker 3276800 Aug 22 20:41 '#ib_redo13_tmp'
-rw-r----- 1 lxd docker 3276800 Aug 22 20:41 '#ib_redo14_tmp'
-rw-r----- 1 lxd docker 3276800 Aug 22 20:41 '#ib_redo15_tmp'
-rw-r----- 1 lxd docker 3276800 Aug 22 20:41 '#ib_redo16_tmp'
-rw-r----- 1 lxd docker 3276800 Aug 22 20:41 '#ib_redo17_tmp'
-rw-r----- 1 lxd docker 3276800 Aug 22 20:41 '#ib_redo18_tmp'
-rw-r----- 1 lxd docker 3276800 Aug 22 20:41 '#ib_redo19_tmp'
-rw-r----- 1 lxd docker 3276800 Aug 22 20:41 '#ib_redo20_tmp'
-rw-r----- 1 lxd docker 3276800 Aug 22 20:41 '#ib_redo21_tmp'
-rw-r----- 1 lxd docker 3276800 Aug 22 20:41 '#ib_redo22_tmp'
-rw-r----- 1 lxd docker 3276800 Aug 22 20:41 '#ib_redo23_tmp'
-rw-r----- 1 lxd docker 3276800 Aug 22 20:41 '#ib_redo24_tmp'
-rw-r----- 1 lxd docker 3276800 Aug 22 20:41 '#ib_redo25_tmp'
-rw-r----- 1 lxd docker 3276800 Aug 22 20:41 '#ib_redo26_tmp'
-rw-r----- 1 lxd docker 3276800 Aug 22 20:41 '#ib_redo27_tmp'
-rw-r----- 1 lxd docker 3276800 Aug 22 20:41 '#ib_redo28_tmp'
-rw-r----- 1 lxd docker 3276800 Aug 22 20:41 '#ib_redo29_tmp'
-rw-r----- 1 lxd docker 3276800 Aug 22 20:41 '#ib_redo30_tmp'
-rw-r----- 1 lxd docker 3276800 Aug 22 20:41 '#ib_redo31_tmp'
-rw-r----- 1 lxd docker 3276800 Aug 22 20:41 '#ib_redo32_tmp'
-rw-r----- 1 lxd docker 3276800 Aug 22 20:41 '#ib_redo33_tmp'
-rw-r----- 1 lxd docker 3276800 Aug 22 20:41 '#ib_redo34_tmp'
-rw-r----- 1 lxd docker 3276800 Aug 22 20:41 '#ib_redo35_tmp'
-rw-r----- 1 lxd docker 3276800 Aug 22 20:41 '#ib_redo36_tmp'
-rw-r----- 1 lxd docker 3276800 Aug 22 20:41 '#ib_redo37_tmp'
-rw-r----- 1 lxd docker 3276800 Aug 22 20:41 '#ib_redo38_tmp'
-rw-r----- 1 lxd docker 3276800 Aug 22 20:41 '#ib_redo39_tmp'
-rw-r----- 1 lxd docker 3276800 Aug 22 20:41 '#ib_redo40_tmp'
# 正式的redo log文件,大小完全固定:重做日志文件
-rw-r----- 1 lxd docker 3276800 Aug 24 21:11 '#ib_redo9'
undo log
保证事务的原子性,还参与了 MVCC,是事务隔离性的一部分(MVCC + 锁)。

四、MVCC
MVCC 讲的就是读(快照读)数据的原理。
1. 基本概念
先复习事务隔离级别:
| 隔离级别 | 脏读 | 不可重复读 | 幻读 |
|---|---|---|---|
| read uncommitted 读未提交 | √ | √ | √ |
| read committed读已提交(Redis 默认) | × | √ | √ |
| repeatable read可重复读(MySQL 默认) | × | × | √ |
| serializable串行化 | × | × | × |
MySQL 默认是可重复读,只能读取到开启事务时的快照。
当前读读的是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,所以会对读取的记录加锁:

操作演示:
# 开启事务 1
begin;
select * from stu;
# 开启事务 2
begin;
update stu set name = 'jsp' where id = 1;
commit;
# 在事务 1
select * from stu;
# 此时读取到和第三行一样的数据,因为mysql默认隔离界别是可重复读
# 此时加上锁进行当前读
# 在事务 1
select * from stu lock in share mode;
# 此时读取了最新数据
快照读就是简单的 select(不加锁),读的是记录数据的可见版本,有可能是历史数据,不加锁、非阻塞读:

- Read Committed:每次 select 都生成一个快照读
- Repeatable Read:开启事务后第一个 select 语句才是快照读的地方
- Serializable:快照读会退化为当前读
快照读就是上面没加锁读到的情况,可重复读。日常查询用快照读(MVCC),不加锁、高性能;写入和显式加锁查询用当前读(锁机制),保证数据一致性。

2. 隐藏字段

查看表空间文件的详细信息:
ibd2sdi [表空间文件名]
# 需要一个软件
sudo apt update && sudo apt install -y mysql-server-core-8.0
# 截取部分信息
root@VM-0-12-ubuntu:/opt/mysql/data/itcast# ibd2sdi score.ibd
"dd_object_type": "Table",
"dd_object": {
"name": "score",
"mysql_version_id": 260700,
"created": 20260824094307,
"last_altered": 20260824094307,
"hidden": 1,
"options": "avg_row_length=0;encrypt_type=N;key_block_size=0;keys_disabled=0;pack_record=1;stats_auto_recalc=0;stats_sample_pages=0;",
"columns": [
# 这里存放了各字段的详细信息
# 最后一次操作事务的ID
{
"name": "DB_TRX_ID",
"type": 10,
"is_nullable": false,
"is_zerofill": false,
"is_unsigned": false,
"is_auto_increment": false,
"is_virtual": false,
"hidden": 2,
"ordinal_position": 8,
"char_length": 6,
"numeric_precision": 0,
"numeric_scale": 0,
"numeric_scale_null": true,
"datetime_precision": 0,
"datetime_precision_null": 1,
"has_no_default": false,
"default_value_null": true,
"srs_id_null": true,
"srs_id": 0,
"default_value": "",
"default_value_utf8_null": true,
"default_value_utf8": "",
"default_option": "",
"update_option": "",
"comment": "",
"generation_expression": "",
"generation_expression_utf8": "",
"options": "",
"se_private_data": "physical_pos=1;table_id=1077;",
"engine_attribute": "",
"secondary_engine_attribute": "",
"column_key": 1,
"column_type_utf8": "",
"elements": [],
"collation_id": 63,
"is_explicit_collation": false
},
# 回滚指针,指向数据的上个版本
{
"name": "DB_ROLL_PTR",
"type": 9,
"is_nullable": false,
"is_zerofill": false,
"is_unsigned": false,
"is_auto_increment": false,
"is_virtual": false,
"hidden": 2,
"ordinal_position": 9,
"char_length": 7,
"numeric_precision": 0,
"numeric_scale": 0,
"numeric_scale_null": true,
"datetime_precision": 0,
"datetime_precision_null": 1,
"has_no_default": false,
"default_value_null": true,
"srs_id_null": true,
"srs_id": 0,
"default_value": "",
"default_value_utf8_null": true,
"default_value_utf8": "",
"default_option": "",
"update_option": "",
"comment": "",
"generation_expression": "",
"generation_expression_utf8": "",
"options": "",
"se_private_data": "physical_pos=2;table_id=1077;",
"engine_attribute": "",
"secondary_engine_attribute": "",
"column_key": 1,
"column_type_utf8": "",
"elements": [],
"collation_id": 63,
"is_explicit_collation": false
}
# 没有主键,自动生成ROW-ID
{
"name": "DB_ROW_ID",
"type": 10,
"is_nullable": false,
"is_zerofill": false,
"is_unsigned": false,
"is_auto_increment": false,
"is_virtual": false,
"hidden": 2,
"ordinal_position": 7,
"char_length": 6,
"numeric_precision": 0,
"numeric_scale": 0,
"numeric_scale_null": true,
"datetime_precision": 0,
"datetime_precision_null": 1,
"has_no_default": false,
"default_value_null": true,
"srs_id_null": true,
"srs_id": 0,
"default_value": "",
"default_value_utf8_null": true,
"default_value_utf8": "",
"default_option": "",
"update_option": "",
"comment": "",
"generation_expression": "",
"generation_expression_utf8": "",
"options": "",
"se_private_data": "physical_pos=0;table_id=1077;",
"engine_attribute": "",
"secondary_engine_attribute": "",
"column_key": 1,
"column_type_utf8": "",
"elements": [],
"collation_id": 63,
"is_explicit_collation": false
},
3. undo log 版本链和 readview 介绍
undo log 版本链
回滚日志在 insert、update、delete 的时候产生,是便于数据回滚的日志:

insert的时候产生的 undo log 日志只在回滚时需要,事务提交后可被立即删除update、delete的时候产生的 undo log 日志不仅在回滚时需要,在快照读时也需要,不会立即被删除

不同事务或相同事务对同一条记录进行修改,会导致该记录的 undolog 生成一条记录版本链表,链表的头部是最新的旧记录,链表尾部是最早的旧记录。
readview 读视图
ReadView(读视图)是快照读 SQL 执行时 MVCC 提取数据的依据,记录并维护系统当前活跃的事务(未提交的)id。它包含四个核心字段:

| 字段 | 含义 |
|---|---|
| m_ids | 当前活跃的事务ID集合 |
| min_trx_id | 最小活跃事务ID |
| max_trx_id | 预分配事务ID,当前最大事务ID+1(因为事务ID是自增的) |
| creator_trx_id | ReadView创建者的事务ID |
版本链数据访问规则:

不同的隔离级别,生成 ReadView 的时机不同:

- READ COMMITTED:在事务中每一次执行快照读时生成 ReadView
- REPEATABLE READ:仅在事务中第一次执行快照读时生成 ReadView,后续复用该 ReadView
4. 原理分析(RC 隔离级别——读已提交)
读已提交:每次快照读都生成 readview。


这几个键分别代表:
- 活跃事务 id(m_ids)
- 最小活跃事务 id(min_trx_id)
- 预分配事务 id(max_trx_id)
- 当前事务 id(creator_trx_id)
拿这套规则去比对版本链,只有版本 2 是满足的,快照读就返回这条记录:

5. 原理分析(RR 隔离级别——可重复读)
还是按生成的 readview 匹配规则来,只不过 RR 只返回一次 readview;既然 readview 都一样了,那返回的数据肯定是一样的。

6. 总结

MVCC 只负责隔离性,而 redo log 和 undo log 负责一致性。
| ACID 特性 | 含义 | InnoDB 实现机制 | MVCC 在其中扮演的角色 |
|---|---|---|---|
| 原子性(Atomicity) | 事务要么全部成功,要么全部回滚 | Undo Log(回滚时逆向恢复) | 无关(MVCC 不负责回滚) |
| 一致性(Consistency) | 事务前后数据状态合法(如约束、触发器) | 由应用层 + 数据库约束(主键/外键/唯一)共同保证 | 无关(MVCC 不负责数据合法性) |
| 隔离性(Isolation) | 并发事务之间互不干扰 | MVCC + 锁机制(行锁/间隙锁) | 核心实现 |
| 持久性(Durability) | 事务提交后数据永久保存 | Redo Log(崩溃恢复重做) | 无关(MVCC 不负责持久化) |
MVCC 和锁如何实现事务隔离性:
| 操作类型 | 使用的技术 | 是否加锁 | 目的 |
|---|---|---|---|
| 普通 SELECT(快照读) | 仅 MVCC | 不加锁 | 读取历史版本,不阻塞写操作 |
| 写操作(INSERT/UPDATE/DELETE) 和 SELECT ... FOR UPDATE | 锁机制 | 加锁(行锁/间隙锁) | 阻止并发修改同一行,保证写-写隔离 |
| 读操作遇到写冲突(如可重复读) | MVCC + 锁 | 不加锁,但通过 MVCC 读到旧版本 | 读不阻塞写,也不被写阻塞 |
关键点:
- 读-写冲突 → 由 MVCC 解决(读旧版本,无需等待写锁)
- 写-写冲突 → 由 锁 解决(后写者等待前写者释放锁)
- 幻读(写操作阻止插入) → 由 间隙锁 解决(锁住范围,不让插入新行)
