InnoDB 事务隔离级别

mac2022-06-30  88

InnoDB是一种平衡高可靠性和高性能的通用存储引擎。在MySQL数据库5.5.8版本开始,InnoDB是默认的MySQL存储引擎。

InnoDB的主要优势 - 其DML操作遵循ACID,具有提交,回滚和崩溃恢复功能的事务,以保护用户数据

行锁设定类似于Oracle风格的一致性读,可提高多用户并发性和性能。

InnoDB表将数据以聚集(clustered)方式进行存储,因此每张表的存储都是按主键的顺序进行存放,如果没有显示地在表定义时指定主键,InnoDB存储引擎会为每一行生成一个6字节的ROWID,并以此作为主键。

支持外键约束。使用外键时,将检查插入,更新和删除,以确保他们不会导致不同表之间的不一致。

InnoDB通过使用多版本并发控制(MVCC)来获得高并发性,并且实现了SQL标准的4种隔离级别,more是REPEATABLE-READ级别

特性支持B-tree indexesYes基于时间点的备份恢复(在服务器中实现,而不是在存储引擎中实现。)YESCluster database supportNoClustered indexesNoCompressed dataYesData cachesYes数据加密(加密功能在服务器中实现。在MySQL 5.7及更高版本中可以使用静态数据表空间加密。)YesForeign key supportYesFull-text search indexesYes(MySQL 5.6及更高版本中提供了InnoDB对FULLTEXT索引的支持。)地理空间数据类型支持Yes地理空间索引支持Yes(在MySQL 5.7及更高版本中可以使用InnoDB对地理空间索引的支持。)Hash 索引No(InnoDB在内部利用其自适应哈希索引来实现哈希索引功能。)Index cachesYesLocking 粒度RowMVCCYesReplication support(在服务器中实现,而不是在存储引擎中实现。)Yes存储限制64TBT-tree indexesNoTransactionsYes更新数据字典的统计信息Yes

1. 事务的基本要素

原子性(Atomicity) 事务开始后所有操作,要么全部做完,要么全部不做,不可能停滞在中间环节。事务执行过程中出错,会回滚到事务开始前的状态,所有的操作就像没有发生一样。也就是说事务是一个不可分割的整体,就像化学中学过的原子,是物质构成的基本单位。

一致性(Consistency) 一致性指事务将数据库从一种状态转变为下一种一致的状态。从事务开始之前和事务结束之后,数据库的晚自习约束没有被破坏。

隔离性(Isolation) 事务的隔离性要求每个读写事务的对象对其他事务的操作对象能相互分离,即该事务提交前对其他事务都不可见,通常这使用锁来实现,当前数据库系统中都提供了一种粒度锁(granular lock)的策略,允许事务仅锁住一个实体对象的子集,以此来提高事务之间的并发度。

持久性(Durability) 事务一旦提交,其结果就永久性的。即使发生宕机等故障,数据库也能将数据恢复。

2. 事务的隔离级别

事务隔离级别脏读不可重复读幻读读未提交(read-uncommitted)是是是读已提交(read-committed)否是是可重复读(repeatable-read)否否是串行化(serializable)否否否

脏读 A事务,会读取到B事务还未提交的数据。因为B事务可能会因为各种原因数据回滚,所以如果A事务读取了B未提交的数据,然后基于此进行一些业务操作,但是B事务发生错误回滚了,那A事务的业务操作就错了。

不可重复读 在同一个事务生命周期内,也就是这个事务还未提交之前。如果另外一个事务,对数据进行了编辑(update)或者删除(delete)操作。那么A事务就会读取到。简单理解,就是在一个事务生命周期内,多次查询数据,每次都可能查出来的不一样。

幻读 幻读的结果其实和不可重复读是一样的表现,差异就在于,不可重复读,主要是针对其他事务进行了编辑(update)和删除(delete)操作。而幻读主要是针对插入(insert)操作。也就是在一个事务生命周期内,会查询到另外一个事务新插入的数据。

如果想在MySQL数据库启动时设置事务的默认隔离级别,那就需要修改MySQL的配置文件,

在[mysqld]中添加如下:

[mysqld] transaction-isolation = READ-COMMITTED

查看当前的会话事务隔离级别,可以使用:

root@localhost [(none)] 14:53:10>select @@tx_isolation; +-----------------------+ | @@global.tx_isolation | +-----------------------+ | REPEATABLE-READ | +-----------------------+ 1 row in set, 1 warning (0.00 sec) root@localhost [(none)] 14:54:49>SHOW VARIABLES LIKE 'tx_isolation'; +---------------+-----------------+ | Variable_name | Value | +---------------+-----------------+ | tx_isolation | REPEATABLE-READ | +---------------+-----------------+ 1 row in set (0.00 sec)

查看全局的事务隔离级别,可以使用:

root@localhost [(none)] 14:53:10>select @@global.tx_isolation; +-----------------------+ | @@global.tx_isolation | +-----------------------+ | REPEATABLE-READ | +-----------------------+ 1 row in set, 1 warning (0.00 sec) root@localhost [(none)] 14:54:49>SHOW global variables like 'tx_isolation'; +---------------+-----------------+ | Variable_name | Value | +---------------+-----------------+ | tx_isolation | REPEATABLE-READ | +---------------+-----------------+ 1 row in set (0.00 sec)

在InnoDB存储引擎中,可以使用以下命令来设置当前会话或全局的事务隔离级别:

SET [GLOBAL | SESSION] TRANSACTION ISOLATION LEVEL { READ-UNCOMMITED | READ-COMMITTED | REPEATABLE-READ | SERIALIZABLE }

3. 各隔离级别示例

首先创建示例表,并插入数据

create table account(id int,name char(30),balance int); insert into account values(1,'wb',18000); insert into account values(2,'wb2',28000); insert into account values(3,'wb3',38000); commit;

关闭自动提交

set autocommit = 0; set global autocommit = 0;

3.1 读未提交(read-uncommitted)

实验步骤 1)分别在session1,与session2 终端设置隔离级别为set session transaction isolation level read uncommitted; 2)分别在session1,session2上开始事务,执行begin; 3)分别在session1,session2上开始事务,执行查询语句select * from account; 4)在session1上,执行update语句,不提交update account set balance=balance - 50 where id= 1; 5)在session2上,执行查询语句select * from account;,session2上能返回session1未提交的数据,这种读叫做脏读。 ##############session 1############## root@localhost [test] 10:20:11>select @@autocommit; #操作1 +--------------+ | @@autocommit | +--------------+ | 0 | +--------------+ 1 row in set (0.00 sec) root@localhost [test] 10:21:58>set session transaction isolation level read uncommitted; #操作3 Query OK, 0 rows affected (0.00 sec) root@localhost [test] 10:22:54>select @@tx_isolation; #操作5 +------------------+ | @@tx_isolation | +------------------+ | READ-UNCOMMITTED | +------------------+ 1 row in set, 1 warning (0.00 sec) root@localhost [test] 10:23:21>begin; #操作6 Query OK, 0 rows affected (0.00 sec) root@localhost [test] 10:23:37>select * from account; #操作7 +------+------+---------+ | id | name | balance | +------+------+---------+ | 1 | wb | 18000 | | 2 | wb2 | 28000 | | 3 | wb3 | 38000 | +------+------+---------+ 3 rows in set (0.00 sec) root@localhost [test] 10:23:46>update account set balance=balance - 50 where id= 1; #操作10 Query OK, 1 row affected (0.01 sec) Rows matched: 1 Changed: 1 Warnings: 0 ##############session 2############## root@localhost [test] 10:20:38>select @@autocommit; #操作2 +--------------+ | @@autocommit | +--------------+ | 0 | +--------------+ 1 row in set (0.00 sec) root@localhost [test] 10:20:46>set session transaction isolation level read uncommitted; #操作4 Query OK, 0 rows affected (0.00 sec) root@localhost [test] 10:23:00>begin #操作8 -> ; Query OK, 0 rows affected (0.00 sec) root@localhost [test] 10:24:08>select * from account; #操作9 +------+------+---------+ | id | name | balance | +------+------+---------+ | 1 | wb | 18000 | | 2 | wb2 | 28000 | | 3 | wb3 | 38000 | +------+------+---------+ 3 rows in set (0.00 sec) root@localhost [test] 10:24:18>select * from account; #操作11 +------+------+---------+ | id | name | balance | +------+------+---------+ | 1 | wb | 17950 | | 2 | wb2 | 28000 | | 3 | wb3 | 38000 | +------+------+---------+ 3 rows in set (0.00 sec)

【结论】:可以看到,我们在session1, insert一条数据,但是未进行提交操作(commit),但是在session2事务中,却查询到了。我们称这种现象叫做脏读,在实际开发过程中,我们一般较少使用Read uncommitted隔离级别,这种隔离级别对任何的数据操作都不会进行加锁。

3.2 读已提交(read-committed)

3.2.1 read committed 脏读与不可重复读示例

实验步骤 1)分别在session1与session2上设置隔离级别set session transaction isolation level read committed; 2)分别在session1,session2上开始事务,执行begin; 3)分别在session1,session2上开始事务,执行查询语句select * from account where id=1; 4)在session1上执行update语句update account set balance=20000 where id=1; 5)在session2上执行select语句select * from account where id = 1;,返回的还是旧数据,说明不能脏读。 6)在session2上执行select + for update语句,for update读取数据,被阻塞,需等待session1,结束事务 7)在session1上执行commit; 8)session2立马返回最新数据,加不加for update都能返回最新数据。 ##############session 1############## root@localhost [test] 11:10:21>select @@autocommit; #操作1 +--------------+ | @@autocommit | +--------------+ | 0 | +--------------+ 1 row in set (0.00 sec) root@localhost [test] 11:12:32>set session transaction isolation level read committed; #操作3 Query OK, 0 rows affected (0.01 sec) root@localhost [test] 11:12:44>select @@tx_isolation; #操作5 +----------------+ | @@tx_isolation | +----------------+ | READ-COMMITTED | +----------------+ 1 row in set, 1 warning (0.00 sec) root@localhost [test] 11:12:55>begin; #操作7 Query OK, 0 rows affected (0.00 sec) root@localhost [test] 11:13:23>select * from account where id=1; #操作9 +------+------+---------+ | id | name | balance | +------+------+---------+ | 1 | wb | 18000 | +------+------+---------+ 1 row in set (0.00 sec) root@localhost [test] 11:13:56>update account set balance=20000 where id=1;#操作11 Query OK, 1 row affected (0.01 sec) Rows matched: 1 Changed: 1 Warnings: 0 root@localhost [test] 11:14:59>commit; #操作14 Query OK, 0 rows affected (0.01 sec) ##############session 2############## root@localhost [test] 11:10:11>select @@autocommit; #操作2 +--------------+ | @@autocommit | +--------------+ | 0 | +--------------+ 1 row in set (0.00 sec) root@localhost [test] 11:12:05>set session transaction isolation level read committed; #操作4 Query OK, 0 rows affected (0.00 sec) root@localhost [test] 11:13:02>select @@tx_isolation; #操作6 +----------------+ | @@tx_isolation | +----------------+ | READ-COMMITTED | +----------------+ 1 row in set, 1 warning (0.00 sec) root@localhost [test] 11:13:11>begin; #操作8 Query OK, 0 rows affected (0.00 sec) root@localhost [test] 11:13:36>select * from account where id = 1; #操作10 +------+------+---------+ | id | name | balance | +------+------+---------+ | 1 | wb | 18000 | +------+------+---------+ 1 row in set (0.00 sec) root@localhost [test] 11:14:16>select * from account where id = 1; #操作12 session 1:未提交时,还是读取到旧数据 +------+------+---------+ | id | name | balance | +------+------+---------+ | 1 | wb | 18000 | +------+------+---------+ 1 row in set (0.00 sec) root@localhost [test] 11:15:05>select * from account where id = 1 for update; #操作13:for update读取数据,被阻塞,需等待session1,结束事务 #操作15: session1执行commit后,加不加for update能读取到最新数据 +------+------+---------+ | id | name | balance | +------+------+---------+ | 1 | wb | 20000 | +------+------+---------+ 1 row in set (5.62 sec)

【总结】:在read committed隔离级别下,可以避免脏读,但是避免不了不可重复读,在session2 这一事务周期间,我们并没有结束事务,但多次查询的结果不一致,说明是进行了不可重复读。

3.2.2 read-committed下的幻读示例

给account表增加索引

alter table account add index idx_id (id); 实验步骤 1)分别在session1与session2上设置隔离级别set session transaction isolation level read committed; 2)分别在session1,session2上开始事务,执行begin; 3)在session1上执行查询语句select * from account where id=2; 4)在session2上执行for update查询语句select * from account where id=2 for update; 5)在session1上insert语句insert into account select 2,'wb4',58000;,这里为什么不产生锁等待?因为id上有索引,session2的查询语句只锁定索引记录 6)在session2上执行查询语句select * from account where id=2;读取的是旧数据,然后再执行for update语句,有行锁等待 7)在session1上 执行commit; 8)在session2上,立马返回最新数据,产生了幻读。加不加for update都可以读到新数据。 ###############session1############### root@localhost [test] 09:55:58>begin; Query OK, 0 rows affected (0.00 sec) root@localhost [test] 09:56:22>select * from account where id=2; +------+------+---------+ | id | name | balance | +------+------+---------+ | 2 | wb2 | 28000 | +------+------+---------+ 1 row in set (0.00 sec) root@localhost [test] 09:56:34>insert into account select 2,'wb4',58000; Query OK, 1 row affected (0.00 sec) Records: 1 Duplicates: 0 Warnings: 0 root@localhost [test] 09:58:18>commit; Query OK, 0 rows affected (0.01 sec) ###############session2############### root@localhost [test] 09:57:11>begin; Query OK, 0 rows affected (0.00 sec) root@localhost [test] 09:57:32>select * from account where id=2 for update; +------+------+---------+ | id | name | balance | +------+------+---------+ | 2 | wb2 | 28000 | +------+------+---------+ 1 row in set (0.00 sec) root@localhost [test] 09:57:43>select * from account where id=2; +------+------+---------+ | id | name | balance | +------+------+---------+ | 2 | wb2 | 28000 | +------+------+---------+ 1 row in set (0.00 sec) root@localhost [test] 09:58:35>select * from account where id=2 for update; +------+------+---------+ | id | name | balance | +------+------+---------+ | 2 | wb2 | 28000 | | 2 | wb4 | 58000 | +------+------+---------+ 2 rows in set (4.17 sec) root@localhost [test] 09:58:45>select * from account where id=2; +------+------+---------+ | id | name | balance | +------+------+---------+ | 2 | wb2 | 28000 | | 2 | wb4 | 58000 | +------+------+---------+ 2 rows in set (0.00 sec)

【总结】在Read committed隔离级别中,可以有效解决脏读问题,但是有不可重复读、幻读问题,而不可重复读和幻读的差异主要是,不可重复读主要是针对修改和删除操作、幻读针对插入数据操作。

3.3 可重复读(repeatable-read)

实验步骤 1)在session1与session2上查询当前默认隔离级别:select @@tx_isolation,返回为REPEATABLE-READ隔离级别 2)session1与session2分别执行begin,表明开始一个事务 3)session1与session2分别查询语句select * from account where id=3; 4)在session1上执行insert 语句insert into account select 3,'wb4',68000; 5)在session2上执行查询语句select * from account where id=3;,没有返回最新数据,说明没有脏读现象. 6)在session1上执行commit操作 7)在session2上多次执行查询语句,select * from account where id=3;,没有返回最新数据,说明没有幻读现象。 8)在session2上执行commit操作,然后执行查询语句能返回最新结果l 9)分别在session1 与session2上执行begin;,表明开始一个事务 10)session1上执行update语句update account set balance=balance-50 where id=1; 然后执行commit; 11)session2上多次执行select语句select * from account where id=1;,结果都是一样,没有最新数据,说明没有不可重复读现象。 12)session2上执行commit,能返回最新数据。 ###############session1############### root@localhost [test] 10:58:39>select @@tx_isolation; +-----------------+ | @@tx_isolation | +-----------------+ | REPEATABLE-READ | +-----------------+ 1 row in set, 1 warning (0.00 sec) root@localhost [test] 10:58:47>begin; Query OK, 0 rows affected (0.00 sec) root@localhost [test] 10:58:51>select * from account; +------+------+---------+ | id | name | balance | +------+------+---------+ | 1 | wb | 18000 | | 2 | wb2 | 28000 | | 3 | wb3 | 38000 | | 2 | wb4 | 58000 | +------+------+---------+ 4 rows in set (0.00 sec) root@localhost [test] 10:59:11>select * from account where id=3; +------+------+---------+ | id | name | balance | +------+------+---------+ | 3 | wb3 | 38000 | +------+------+---------+ 1 row in set (0.00 sec) root@localhost [test] 11:00:31>insert into account select 3,'wb4',68000; ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction root@localhost [test] 11:02:01>select * from account where id =3; +------+------+---------+ | id | name | balance | +------+------+---------+ | 3 | wb3 | 38000 | +------+------+---------+ 1 row in set (0.00 sec) root@localhost [test] 11:02:49>insert into account select 3,'wb4',68000; Query OK, 1 row affected (0.01 sec) Records: 1 Duplicates: 0 Warnings: 0 root@localhost [test] 11:02:57>commit; Query OK, 0 rows affected (0.02 sec) root@localhost [test] 11:03:06>update account set balance=balance-50 where id=1; Query OK, 1 row affected (0.00 sec) Rows matched: 1 Changed: 1 Warnings: 0 root@localhost [test] 11:05:04>commit; Query OK, 0 rows affected (0.01 sec) ###############session1############### root@localhost [test] 10:57:11>select @@tx_isolation; +-----------------+ | @@tx_isolation | +-----------------+ | REPEATABLE-READ | +-----------------+ 1 row in set, 1 warning (0.00 sec) root@localhost [test] 10:57:28>begin; Query OK, 0 rows affected (0.00 sec) root@localhost [test] 10:58:55>select * from account; +------+------+---------+ | id | name | balance | +------+------+---------+ | 1 | wb | 18000 | | 2 | wb2 | 28000 | | 3 | wb3 | 38000 | | 2 | wb4 | 58000 | +------+------+---------+ 4 rows in set (0.00 sec) root@localhost [test] 10:59:49>select * from account where id=3 for update; +------+------+---------+ | id | name | balance | +------+------+---------+ | 3 | wb3 | 38000 | +------+------+---------+ 1 row in set (0.00 sec) root@localhost [test] 11:01:25>select * from account where id=2 for update; +------+------+---------+ | id | name | balance | +------+------+---------+ | 2 | wb2 | 28000 | | 2 | wb4 | 58000 | +------+------+---------+ 2 rows in set (0.00 sec) root@localhost [test] 11:01:43>commit; Query OK, 0 rows affected (0.00 sec) root@localhost [test] 11:02:24>begin; Query OK, 0 rows affected (0.00 sec) root@localhost [test] 11:02:28>select * from account where id=3; +------+------+---------+ | id | name | balance | +------+------+---------+ | 3 | wb3 | 38000 | +------+------+---------+ 1 row in set (0.00 sec) root@localhost [test] 11:02:38>select * from account where id=3; +------+------+---------+ | id | name | balance | +------+------+---------+ | 3 | wb3 | 38000 | +------+------+---------+ 1 row in set (0.00 sec) root@localhost [test] 11:03:01>select * from account where id=3; +------+------+---------+ | id | name | balance | +------+------+---------+ | 3 | wb3 | 38000 | +------+------+---------+ 1 row in set (0.00 sec) root@localhost [test] 11:03:09>select * from account where id=3; +------+------+---------+ | id | name | balance | +------+------+---------+ | 3 | wb3 | 38000 | +------+------+---------+ 1 row in set (0.00 sec) root@localhost [test] 11:03:13>commit; Query OK, 0 rows affected (0.00 sec) root@localhost [test] 11:03:50>select * from account where id=3; +------+------+---------+ | id | name | balance | +------+------+---------+ | 3 | wb3 | 38000 | | 3 | wb4 | 68000 | +------+------+---------+ 2 rows in set (0.00 sec) root@localhost [test] 11:03:52>select * from account ; +------+------+---------+ | id | name | balance | +------+------+---------+ | 1 | wb | 18000 | | 2 | wb2 | 28000 | | 3 | wb3 | 38000 | | 2 | wb4 | 58000 | | 3 | wb4 | 68000 | +------+------+---------+ 5 rows in set (0.00 sec) root@localhost [test] 11:05:19>select * from account where id=1; +------+------+---------+ | id | name | balance | +------+------+---------+ | 1 | wb | 18000 | +------+------+---------+ 1 row in set (0.00 sec) root@localhost [test] 11:05:24>select * from account where id=1; +------+------+---------+ | id | name | balance | +------+------+---------+ | 1 | wb | 18000 | +------+------+---------+ 1 row in set (0.00 sec) root@localhost [test] 11:05:32>select * from account where id=1; +------+------+---------+ | id | name | balance | +------+------+---------+ | 1 | wb | 18000 | +------+------+---------+ 1 row in set (0.01 sec) root@localhost [test] 11:05:34>select * from account where id=1; +------+------+---------+ | id | name | balance | +------+------+---------+ | 1 | wb | 18000 | +------+------+---------+ 1 row in set (0.00 sec) root@localhost [test] 11:05:44>commit; Query OK, 0 rows affected (0.00 sec) root@localhost [test] 11:05:46>select * from account where id=1; +------+------+---------+ | id | name | balance | +------+------+---------+ | 1 | wb | 17950 | +------+------+---------+ 1 row in set (0.00 sec)

【总结】在Repeatable read级别中,脏读、不可重复读、幻读现象都没有。在mysql中,该级别也是默认的事务隔离级别,我们日常在开发中,也是主要使用该隔离级别。

3.4 串行化(serializable)

实验步骤 1)session1 与 session2分别设置隔离级别set session transaction isolation level serializable; 2) session1 与 session2分别执行begin;开始事务 3)session1 与 session2分别执行查询语句select * from account where id=2; 4)session1上执行update语句update account set balance=balance+100 where id=2; 5)session1上提示报错ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction,有行锁等待 6)session2上执行update语句update account set balance=balance+100 where id=1;能成功更新,说明session2只是行级锁定。 ##########session1########## root@localhost [test] 11:23:58>select @@tx_isolation; +----------------+ | @@tx_isolation | +----------------+ | SERIALIZABLE | +----------------+ 1 row in set, 1 warning (0.00 sec) root@localhost [test] 11:24:31>begin; Query OK, 0 rows affected (0.00 sec) root@localhost [test] 11:24:43>select * from account where id=2; +------+------+---------+ | id | name | balance | +------+------+---------+ | 2 | wb2 | 28000 | | 2 | wb4 | 58000 | +------+------+---------+ 2 rows in set (0.00 sec) root@localhost [test] 11:25:10>update account set balance=balance+100 where id=2; ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction root@localhost [test] 11:30:18>update account set balance=balance+100 where id=1; Query OK, 1 row affected (0.00 sec) Rows matched: 1 Changed: 1 Warnings: 0 ##########session2########## root@localhost [test] 11:22:59>set session transaction isolation level serializable; Query OK, 0 rows affected (0.00 sec) root@localhost [test] 11:24:22>select @@tx_isolation; +----------------+ | @@tx_isolation | +----------------+ | SERIALIZABLE | +----------------+ 1 row in set, 1 warning (0.00 sec) root@localhost [test] 11:24:37>select * from account where id=2; +------+------+---------+ | id | name | balance | +------+------+---------+ | 2 | wb2 | 28000 | | 2 | wb4 | 58000 | +------+------+---------+ 2 rows in set (0.00 sec)

Serializable完全串行化的读,每次读都需要获得表级共享锁,读写相互会相互互斥,这样可以更好的解决数据一致性的问题,但是同样会大大的降低数据库的实际吞吐性能。所以该隔离级别因为损耗太大,一般很少在开发中使用。

转载于:https://www.cnblogs.com/wanbin/p/9514662.html

相关资源:JAVA上百实例源码以及开源项目
最新回复(0)