MySQL中Innodb的数据库事务隔离级别别和锁的关系的

没有更多推荐了,
不良信息举报
举报内容:
Innodb中的事务隔离级别和锁的关系
举报原因:
原文地址:
原因补充:
最多只允许输入30个字
加入CSDN,享受更精准的内容推荐,与500万程序员共同成长!没有更多推荐了,
不良信息举报
举报内容:
Innodb中的事务隔离级别和锁的关系
举报原因:
原文地址:
原因补充:
最多只允许输入30个字
加入CSDN,享受更精准的内容推荐,与500万程序员共同成长!博客分类:
数据库是要被广大客户所共享访问的,在大量用户并发访问数据库过程中很有可能出现以下几种不不确定情况:
A、脏读:一个事务读取到了另一个事务未提交的数据操作结果。这是相当危险的,因为很可能另一个事务的所有的操作都被;
B、更新丢失:两个事务都同时更新一行数据,一个事务对数据的更新把另一个事务对数据的更新覆盖了。这是因为系统没有执行任何的锁操作,因此并发事务并没有被隔离开来。
C、不可重复读:一个事务对同一行数据重复读取两次,但是却得到了不同的结果。
为了避免上面出现的几种情况,标准SQL规范中定义了四种事务隔离级别,包括了一些具体规则,用来限定事务内外的哪些改变是可见的,哪些是不可见的。不同的隔离级别对事务的处理不同,事务隔离级别越高,为避免冲突所花的精力也就越多。低级别的隔离级一般支持更高的并发处理,并拥有更低的系统开销。
四种事务隔离级别按等级从低到高分别为:
A、Read Uncommitted(读取未提交内容);
B、Read Committed(读取提交内容);
C、Repeatable Read(可重读);
D、Serializable(可串行化)。
下面分别对这四种事务隔离级别进行讲解并举例。
为了方便测试说明,先建立测试表user如下(仅含id和name两个字段,是不是够简单,没错,就是要简单):
以下四种事务隔离级别都用这个user表进行演示说明。
1、Read Uncommitted(读取未提交内容)
在该隔离级别,所有事务都可以看到其他未提交事务的执行结果。通俗地讲就是,在一个事务中可以读取到另一个事务中新增或修改但未提交的数据。因为另一个事务可能回滚,所以在第一个事务中读取到的数据很可能是无效的脏数据,造成脏读现象。读取未提交的数据,也被称为脏读。
实例如下:
更改事务隔离级别为read-uncommitted读取未提交内容:
分别开始两个事务A、B,做如下操作:
1、开始事务:
Query OK, 0 rows affected (0.00 sec)
2、查询user表的数据:
mysql& select *
Empty set (0.00 sec)
1、开始事务:
Query OK, 0 rows affected (0.00 sec)
2、查询user表的数据:
mysql& select *
Empty set (0.00 sec)
3、插入一条数据:
mysql& insert into user values(1,'user1');
Query OK, 1 row affected (0.00 sec)
4、查询user表的数据:
mysql& select *
+----+-------+
| id | name
+----+-------+
1 | user1 |
+----+-------+
1 row in set (0.00 sec)
3、查询user表的数据:
(在事务B插入数据但未提交时查看数据)
mysql& select *
+----+-------+
| id | name
+----+-------+
1 | user1 |
+----+-------+
1 row in set (0.00 sec)
5、回滚事务:
Query OK, 0 rows affected (0.01 sec)
6、查询user表的数据:
mysql& select *
Empty set (0.00 sec)
4、查询user表的数据:
(在事务B回滚后查看数据)
mysql& select *
Empty set (0.00 sec)
经过上面的实例可以看出,事务B插入了一条数据但是没有提交,此时事务A可以查询到那条未提交的数据,但一旦事务B回滚后,事务A就查询不到那条数据。事务A在事务B提交之前查询到的那条数据是无效的,即脏数据,因为没法确保事务B最后是否会成功提交或者成功提交之前是否还会对该数据进行修改,依据脏数据所做的操作可能是不正确的,极有可能造成脏读现象。
2、Read Committed(读取提交内容)
这是大多数数据库系统的默认隔离级别(但不是mysql默认的),它满足了隔离的简单定义:一个事务只能看见已经提交事务所做的修改。换句话说,一个事务从开始直到提交之前,所做的任何修改对其他事务都是不可见的。这个级别有时候也叫做不可重复读(nonrepeatable read),因为两次执行同样的查询,可能会得到不一样的结果。
实例如下:
更改事务隔离级别为read-committed读取未提交内容:
分别开始两个事务A、B,做如下操作:
1、开始事务:
Query OK, 0 rows affected (0.00 sec)
2、查询user表的数据:
mysql& select *
Empty set (0.00 sec)
1、开始事务:
Query OK, 0 rows affected (0.00 sec)
2、查询user表的数据:
mysql& select *
Empty set (0.00 sec)
3、插入一条数据:
mysql& insert into user values(1,'user1');
Query OK, 1 row affected (0.00 sec)
4、查询user表的数据:
mysql& select *
+----+-------+
| id | name
+----+-------+
1 | user1 |
+----+-------+
1 row in set (0.00 sec)
3、查询user表的数据:
(在事务B插入数据但未提交时查看数据)
mysql& select *
Empty set (0.00 sec)
5、提交事务:
Query OK, 0 rows affected (0.01 sec)
4、查询user表的数据:
(在事务B提交后查看数据)
mysql& select *
+----+-------+
| id | name
+----+-------+
1 | user1 |
+----+-------+
1 row in set (0.00 sec)
从实例可以看到,事务A在事务B插入数据但未提交时,查询不到数据,当直到事务B提交事务之后,事务A才可以查询到新增的数据。
经过上面的实例可以得出结论,已提交读隔离级别解决了脏读的问题,但是出现了不可重复读的问题,即事务A在两次查询的数据不一致,因为在两次查询之间事务B更新了一条数据。
3、Repeatable Read(可重读)
这是MySQL的默认事务隔离级别,它确保同一事务的多个实例在并发读取数据时,会看到同样的数据行。通俗来讲,可重复读在一个事务里读取数据,怎么读都不会变,除非提交了该事务,再次进行读取。
实例如下:
更改事务隔离级别为repeatable-read读取未提交内容:
分别开始两个事务A、B,做如下操作:
1、开始事务:
Query OK, 0 rows affected (0.00 sec)
2、查询user表的数据:
mysql& select *
+----+-------+
| id | name
+----+-------+
1 | user1 |
+----+-------+
1 row in set (0.00 sec)
1、开始事务:
Query OK, 0 rows affected (0.00 sec)
2、查询user表的数据:
mysql& select *
+----+-------+
| id | name
+----+-------+
1 | user1 |
+----+-------+
1 row in set (0.00 sec)
3、插入一条数据:
mysql& insert into user values(2,'user2');
Query OK, 1 row affected (0.00 sec)
4、查询user表的数据:
mysql& select *
+----+-------+
| id | name
+----+-------+
1 | user1 |
2 | user2 |
+----+-------+
2 rows in set (0.00 sec)
3、查询user表的数据:
(在事务B插入数据但未提交时查看数据)
mysql& select *
+----+-------+
| id | name
+----+-------+
1 | user1 |
+----+-------+
1 row in set (0.00 sec)
5、提交事务:
Query OK, 0 rows affected (0.01 sec)
4、查询user表的数据:
(在事务B提交后查看数据)
mysql& select *
+----+-------+
| id | name
+----+-------+
1 | user1 |
+----+-------+
1 row in set (0.00 sec)
5、事务A提交事务之后查询user表数据:
Query OK, 0 rows affected (0.00 sec)
mysql& select *
+----+-------+
| id | name
+----+-------+
1 | user1 |
2 | user2 |
+----+-------+
2 rows in set (0.00 sec)
从实例可以看到,事务A在事务B插入数据但未提交时,查询不到数据,当直到事务B提交事务之后,事务A才可以查询到新增的数据。
经过上面的实例可以得出结论,已提交读隔离级别解决了脏读的问题,但是出现了不可重复读的问题,即事务A在两次查询的数据不一致,因为在两次查询之间事务B更新了一条数据。
4、Serializable(可串行化)
这是最高的隔离级别,它通过强制事务排序,使之不可能相互冲突,从而解决幻读问题。简言之,它是在每个读的数据行上加上共享锁。通俗地讲就是,假如两个事务都操作到同一数据行,那么这个数据行就会被锁定,只允许先读取/操作到数据行的事务优先操作,只有当事务提交了,数据行才会解锁,后一个事务才能成功操作这个数据行,否则只能一直等待。就像java中的锁,写数据必须等待另一个事务结束。也正因为这样,可能导致大量的超时现象和锁竞争。
实例如下:
更改事务隔离级别为serializable读取未提交内容:
分别开始两个事务A、B,做如下操作(两个实例):
第1个实例:
1、开始事务:
Query OK, 0 rows affected (0.00 sec)
2、查询user表的数据:
mysql& select *
+----+-------+
| id | name
+----+-------+
1 | user1 |
+----+-------+
1 row in set (0.00 sec)
1、开始事务:
Query OK, 0 rows affected (0.00 sec)
2、查询user表的数据:
mysql& select *
+----+-------+
| id | name
+----+-------+
1 | user1 |
+----+-------+
1 row in set (0.00 sec)
3、插入一条数据:
mysql& insert into user values(2,'user2');
ERROR 1205 (HY000): Lock w try restarting transaction
3、提交事务
Query OK, 0 rows affected (0.01 sec)
第2个实例:
1、开始事务:
Query OK, 0 rows affected (0.00 sec)
2、查询user表的数据:
mysql& select *
+----+-------+
| id | name
+----+-------+
1 | user1 |
+----+-------+
1 row in set (0.00 sec)
1、开始事务:
Query OK, 0 rows affected (0.00 sec)
2、查询user表的数据:
mysql& select *
+----+-------+
| id | name
+----+-------+
1 | user1 |
+----+-------+
1 row in set (0.00 sec)
3、插入一条数据:
mysql& insert into user values(2,'user2');
一直等待一直等待一直等待...
3、提交事务
Query OK, 0 rows affected (0.01 sec)
Query OK, 1 row affected (0.00 sec)
4、查询user表的数据:
mysql& select *
+----+-------+
| id | name
+----+-------+
1 | user1 |
2 | user2 |
+----+-------+
2 rows in set (0.00 sec)
从以上两个实例可以看到,事务B对user表的新增操作需要等事务A提交之后才可以操作成功,若事务A一直不提交,在等待一定时间之后事务B就会超时报错。从实例中可以看出serializable完全锁定字段,若后一个事务来查询同一份数据就必须等待,直到前一个事务完成并解除锁定为止,是完整的隔离级别,会锁定对应的数据表,因而会有效率的问题。
PS:实践发现,事务A查询user数据但未提交时,事务B增删改时需要等待,但查询数据不需要等待;事务A增删改数据但未提交时,事务B增删查改都需要等待(当然前提是有操作到同一行数据)。
浏览: 3920 次
来自: 广州
(window.slotbydup=window.slotbydup || []).push({
id: '4773203',
container: s,
size: '200,200',
display: 'inlay-fix'博客分类:
数据库事务隔离级别与锁
一、事务的4个基本特征
&&& 当事务处理系统创建事务时,将确保事务有某些特性。组件的开发者们假设事务的特性应该是一些不需要他们亲自管理的特性。这些特性称为ACID特性。 ACID就是:原子性(Atomicity )、一致性( Consistency )、隔离性或独立性( Isolation)和持久性(Durabilily)。
1、原子性 (Atomicity )
&&& 原子性属性用于标识事务是否完全地完成,一个事务的任何更新要在系统上完全完成,如果由于某种原因出错,事务不能完成它的全部任务,系统将返回到事务开始前的状态。
让我们再看一下银行转帐的例子。如果在转帐的过程中出现错误,整个事务将会回滚。只有当事务中的所有部分都成功执行了,才将事务写入磁盘并使变化永久化。为了提供回滚或者撤消未提交的变化的能力,许多数据源采用日志机制。例如,SQL Server使用一个预写事务日志,在将数据应用于(或提交到)实际数据页面前,先写在事务日志上。但是,其他一些数据源不是关系型数据库管理系统 (RDBMS),它们管理未提交事务的方式完全不同。只要事务回滚时,数据源可以撤消所有未提交的改变,那么这种技术应该可用于管理事务。
2、一致性( Consistency )
&&& 事务在系统完整性中实施一致性,这通过保证系统的任何事务最后都处于有效状态来实现。如果事务成功地完成,那么系统中所有变化将正确地应用,系统处于有效状态。如果在事务中出现错误,那么系统中的所有变化将自动地回滚,系统返回到原始状态。因为事务开
始时系统处于一致状态,所以现在系统仍然处于一致状态。 再让我们回头看一下银行转帐的例子,在帐户转换和资金转移前,帐户处于有效状态。如果事务成功地完成,并且提交事务,则帐户处于新的有效的状态。如果事务出错,终止后,帐户返回到原先的有效状态。
记住,事务不负责实施数据完整性,而仅仅负责在事务提交或终止以后确保数据返回到一致状态。理解数据完整性规则并写代码实现完整性的重任通常落在开发者肩上,他们根据业务要求进行设计。 当许多用户同时使用和修改同样的数据时,事务必须保持其数据的完整性和一致性。因此我们进一步研究A C I D特性中的下一个特性:隔离性。
3、隔离性 ( Isolation)
&&& 在隔离状态执行事务,使它们好像是系统在给定时间内执行的唯一操作。如果有两个事务,运行在相同的时间内,执行相同的功能,事务的隔离性将确保每一事务在系统中认为只有该事务在使用系统。 这种属性有时称为串行化,为了防止事务操作间的混淆,必须串行化或序列化请求,使得在同一时间仅有一个请求用于同一数据。重要的是,在隔离状态执行事务,系统的状态有可能是不一致的,在结束事务前,应确保系统处于一致状态。但是在每个单独的事务中,系统的状态可能会发生变化。如果事务不是在隔离状态运行,它就可能从系统中访问数据,而系统可能处于不一致状态。通过提供事务隔离,可以阻止这类事件的发生。在银行的示例中,这意味着在这个系统内,其他过程和事务在我们的事务完成前看不到我们的事务引起的任何变化,这对于终止的情况非常重要。如果有另一个过程根据帐户余额进行相应处理,而它在我们的事务完成前就能看到它造成的变化,那么这个过程的决策可能
建立在错误的数据之上,因为我们的事务可能终止。这就是说明了为什么事务产生的变化,直到事务完成,才对系统的其他部分可见。隔离性不仅仅保证多个事务不能同时修改相同数据,而且能够保证事务操作产生的变化直到变化被提交或终止时才能对另一个事务可见,并发的事务彼此之间毫无影响。这就意味着所有要求修改或读取的数据已经被锁定在事务中,直到事务完成才能释放。大多数数据库,例如SQL Server以及其他的RDBMS,通过使用锁定来实现隔离,事务中涉及的各个数据项或数据集使用锁定来防止并发访问。
4、持久性 (Durabilily)
&&& 持久性意味着一旦事务执行成功,在系统中产生的所有变化将是永久的。应该存在一些检查点防止在系统失败时丢失信息。甚至硬件本身失败,系统的状态仍能通过在日志中记录事务完成的任务进行重建。持久性的概念允许开发者认为不管系统以后发生了什么变化,完
成的事务是系统永久的部分。 在银行的例子中,资金的转移是永久的,一直保持在系统中。这听起来似乎简单,但这,依赖于将数据写入磁盘,特别需要指出的是,在事务完全完成并提交后才写入磁盘的。 所有这些事务特性,不管其内部如何关联,仅仅是保证从事务开始到事务完成,不管事务成功与否,都能正确地管理事务涉及的数据 ,当事务处理系统创建事务 时,将确保事务有某些特性。组件的开发者们假设事务的特性应该是一些不需要他们亲自管理的特性。
二、为什么需要对事务并发控制
&&& 如果不对事务进行并发控制,我们看看数据库并发操作是会有那些异常情形
1、丢失更新(Lost update)
&&& 两个事务都同时更新一行数据,但是第二个事务却中途失败退出,导致对数据的两个修改都失效了。
2、脏读(Dirty Reads)
&&& 一个事务开始读取了某行数据,但是另外一个事务已经更新了此数据但没有能够及时提交。这是相当危险的,因为很可能所有的操作都被回滚。
3、非重复读(Non-repeatable Reads)
&& 一个事务对同一行数据重复读取两次,但是却得到了不同的结果。同一查询在同一事务中多次进行,由于其他提交事务所做的修改或删除,每次返回不同的结果集,此时发生非重复读。
4、二类丢失更新(Second lost updates problem)
&&& 无法重复读取的特例。有两个并发事务同时读取同一行数据,然后其中一个对它进行修改提交,而另一个也进行了修改提交。这就会造成第一次写操作失效。
5、幻像读(Phantom Reads)
&&& 事务在操作过程中进行两次查询,第二次查询的结果包含了第一次查
询中未出现的数据(这里并不要求两次查询的SQL语句相同)。这是因为在两次查询过程中有另外一个事务插入数据造成的。
三、数据库的隔离级别
&& 为了兼顾并发效率和异常控制,在标准SQL规范中,定义了4个事务隔离级别,(ORACLE和SQLSERER对标准隔离级别有不同的实现 )
1、未提交读(Read Uncommitted)
&&& 直译就是"读未提交",意思就是即使一个更新语句没有提交,但是别
的事务可以读到这个改变.这是很不安全的。允许任务读取数据库中未提交的数据更改,也称为脏读。
2、提交读(Read Committed)
&& 直译就是"读提交",可防止脏读,意思就是语句提交以后即执行了COMMIT以后
别的事务就能读到这个改变. 只能读取到已经提交的数据。Oracle等多数数据库默认都是该级别
3、可重复读(Repeatable Read):
&& 直译就是"可以重复读",这是说在同一个事务里面先后执行同一个查询语句的时候,得到的结果是一样的.在同一个事务内的查询都是事务开始时刻一致的,InnoDB默认级别。在SQL标准中,该隔离级别消除了不可重复读,但是还存在幻象读
4、串行读(Serializable)
&& 直译就是"序列化",意思是说这个事务执行的时候不允许别的事务并发执行. 完全串行化的读,每次读都需要获得表级共享锁,读写相互都会阻塞
四,隔离级别对并发的控制
下表是各隔离级别对各种异常的控制能力。
LU丢失更新 DR脏读 NRR非重复读SLU二类丢失更新 PR幻像读未提交读 RUYYYY Y提交读 RCNNYYY可重复读 RRNNNNY串行读 SNNNNN
顺便举一小例。
MS_SQL:
--事务一
set transaction isolation level serializable
begin tran
insert into test values('xxx')
--事务二
set transaction isolation level read committed
begin tran
select * from test
--事务三
set transaction isolation level read uncommitted
begin tran
select * from test
在查询分析器中执行事务一后,分别执行事务二,和三。结果是事务二会等待,而事务三则会执行。
ORACLE:
--事务一
set transaction isolatio
insert into test values('xxx');
select *
--事务二
set transaction isolation level read committed--ORACLE默认级别
select * from test
执行事务一后,执行事务二。结果是事务二只读出原有的数据,无视事务一的插入操作。
MYSQL
查看InnoDB系统级别的事务隔离级别:
以下为引用的内容:
mysql& SELECT @@global.tx_
+-----------------------+
| @@global.tx_isolation |
+-----------------------+
| REPEATABLE-READ   |
+-----------------------+
1 row in set (0.00 sec)
查看InnoDB会话级别的事务隔离级别:
以下为引用的内容:
mysql& SELECT @@tx_
+-----------------+
| @@tx_isolation |
+-----------------+
| REPEATABLE-READ |
+-----------------+
1 row in set (0.00 sec)
修改事务隔离级别:
以下为引用的内容:
  mysql& set global transaction isolation
  Query OK, 0 rows affected (0.00 sec)
  mysql& set session transaction isolation
  Query OK, 0 rows affected (0.00 sec)
InnoDB的可重复读隔离级别和其他数据库的可重复读是有区别的,不会造成幻象读(phantom read),所谓幻象读,就是同一个事务内,多次select,可以读取到其他session insert并已经commit的数据。下面是一个小的测试,证明InnoDB的可重复读隔离级别不会造成幻象读。测试涉及两个session,分别为 session 1和session 2,隔离级别都是repeateable read,关闭autocommit
以下为引用的内容:
mysql& select @@tx_
+-----------------+
| @@tx_isolation |
+-----------------+
| REPEATABLE-READ |
+-----------------+
1 row in set (0.00 sec)
  mysql& set autocommit=
  Query OK, 0 rows affected (0.00 sec)
  session 1 创建表并插入测试数据
  mysql& create table test(i int) engine=
  Query OK, 0 rows affected (0.00 sec)
  mysql& insert into test values(1);
  Query OK, 1 row affected (0.00 sec)
  session 2 查询,没有数据,正常,session1没有提交,不允许脏读
  mysql& select *
  Empty set (0.00 sec)
  session 1 提交事务
  mysql&
  Query OK, 0 rows affected (0.00 sec)
  session 2 查询,还是没有数据,没有产生幻象读
  mysql& select *
  Empty set (0.00 sec)
以上试验版本:
mysql& select version();
+-------------------------+
| version()       |
+-------------------------+
| 5.0.37-community-nt-log |
+-------------------------+
1 row in set (0.00 sec)
五、并发一致性问题的解决办法
1 封锁(Locking)
&&& 封锁是实现并发控制的一个非常重要的技术。所谓封锁就是事务T在对某个数据对象例如表、记录等操作之前,先向系统发出请求,对其加锁。加锁后事务T就对该数据对象有了一定的控制,在事务T释放它的锁之前,其它的事务不能更新此数据对象。 基本的封锁类型有两种:排它锁(Exclusive locks 简记为X锁)和共享锁(Share locks 简记为S锁)。
&&& 排它锁又称为写锁。若事务T对数据对象A加上X锁,则只允许T读取和修改A,其它任何事务都不能再对A加任何类型的锁,直到T释放A上的锁。这就保证了其它事务在T释放A上的锁之前不能再读取和修改A。
&&& 共享锁又称为读锁。若事务T对数据对象A加上S锁,则其它事务只能再对A加S锁,而不能加X锁,直到T释放A上的S锁。这就保证了其它事务可以读A,但在T释放A上的S锁之前不能对A做任何修改。
2 封锁协议
&&& 在 运用X锁和S锁这两种基本封锁,对数据对象加锁时,还需要约定一些规则,例如应何时申请X锁或S锁、持锁时间、何时释放等。我们称这些规则为封锁协议 (Locking Protocol)。对封锁方式规定不同的规则,就形成了各种不同的封锁协议。下面介绍三级封锁协议。三级封锁协议分别在不同程度上解决了丢失的修改、不 可重复读和读"脏"数据等不一致性问题,为并发操作的正确调度提供一定的保证。下面只给出三级封锁协议的定义,不再做过多探讨。
&&& 1 级封锁协议是:事务T在修改数据R之前必须先对其加X锁,直到事务结束才释放。事务结束包括正常结束(COMMIT)和非正常结束(ROLLBACK)。 1级封锁协议可防止丢失修改,并保证事务T是可恢复的。在1级封锁协议中,如果仅仅是读数据不对其进行修改,是不需要加锁的,所以它不能保证可重复读和不 读"脏"数据。
&&& 2级封锁协议是:1级封锁协议加上事务T在读取数据R之前必须先对其加S锁,读完后即可释放S锁。2级封锁协议除防止了丢失修改,还可进一步防止读"脏"数据。
&&& 3级封锁协议是:1级封锁协议加上事务T在读取数据R之前必须先对其加S锁,直到事务结束才释放。3级封锁协议除防止了丢失修改和不读'脏'数据外,还进一步防止了不可重复读。
六、一般处理并发问题时的步骤:
1、开启事务。
2、申请写权限,也就是给对象(表或记录)加锁。
3、假如失败,则结束事务,过一会重试。
4、假如成功,也就是给对象加锁成功,防止其他用户再用同样的方式打开。
5、进行编辑操作。
6、写入所进行的编辑结果。
7、假如写入成功,则提交事务,完成操作。
8、假如写入失败,则回滚事务,取消提交。
9、(7.8)两步操作已释放了锁定的对象,恢复到操作前的状态。
浏览: 99779 次
来自: 北京
-HashMap 类 (除了不同步和允许使用 null ...
讲得很形象,呵呵。
表达的很清晰!
引用 4,sleep必须捕获异常,而wait,notify和n ...
dadsdddddd
(window.slotbydup=window.slotbydup || []).push({
id: '4773203',
container: s,
size: '200,200',
display: 'inlay-fix'我们都知道事务的几种性质,数据库为了维护这些性质,尤其是一致性和隔离性,一般使用加锁这种方式。同时数据库又是个高并发的应用,同一时间会有大量的并发访问,如果加锁过度,会极大的降低并发处理能力。所以对于加锁的处理,可以说就是数据库对于事务处理的精髓所在。这里通过分析MySQL中InnoDB引擎的加锁机制,来抛砖引玉,让读者更好的理解,在事务处理中数据库到底做了什么。
一次封锁or两段锁?
因为有大量的并发访问,为了预防死锁,一般应用中推荐使用一次封锁法,就是在方法的开始阶段,已经预先知道会用到哪些数据,然后全部锁住,在方法运行之后,再全部解锁。这种方式可以有效的避免循环死锁,但在数据库中却不适用,因为在事务开始阶段,数据库并不知道会用到哪些数据。数据库遵循的是两段锁协议,将事务分成两个阶段,加锁阶段和解锁阶段(所以叫两段锁)
加锁阶段:在该阶段可以进行加锁操作。在对任何数据进行读操作之前要申请并获得S锁(共享锁,其它事务可以继续加共享锁,但不能加排它锁),在进行写操作之前要申请并获得X锁(排它锁,其它事务不能再获得任何锁)。加锁不成功,则事务进入等待状态,直到加锁成功才继续执行。
解锁阶段:当事务释放了一个封锁以后,事务进入解锁阶段,在该阶段只能进行解锁操作不能再进行加锁操作。
事务加锁/解锁处理
insert into test .....
加insert对应的锁
update test set...
加update对应的锁
delete from test ....
加delete对应的锁
事务提交时,同时释放insert、update、delete对应的锁
这种方式虽然无法避免死锁,但是两段锁协议可以保证事务的并发调度是串行化(串行化很重要,尤其是在数据恢复和备份的时候)的。
事务中的加锁方式
事务的四种隔离级别
在数据库操作中,为了有效保证并发读取数据的正确性,提出的事务隔离级别。我们的数据库锁,也是为了构建这些隔离级别存在的。
隔离级别脏读(Dirty Read)不可重复读(NonRepeatable Read)幻读(Phantom Read)
未提交读(Read uncommitted)
已提交读(Read committed)
可重复读(Repeatable read)
可串行化(Serializable )
未提交读(Read Uncommitted):允许脏读,也就是可能读取到其他会话中未提交事务修改的数据
提交读(Read Committed):只能读取到已经提交的数据。Oracle等多数数据库默认都是该级别 (不重复读)
可重复读(Repeated Read):可重复读。在同一个事务内的查询都是事务开始时刻一致的,InnoDB默认级别。在SQL标准中,该隔离级别消除了不可重复读,但是还存在幻象读
串行读(Serializable):完全串行化的读,每次读都需要获得表级共享锁,读写相互都会阻塞
Read Uncommitted这种级别,数据库一般都不会用,而且任何操作都不会加锁,这里就不讨论了。
MySQL中锁的种类
MySQL中锁的种类很多,有常见的表锁和行锁,也有新加入的Metadata Lock等等,表锁是对一整张表加锁,虽然可分为读锁和写锁,但毕竟是锁住整张表,会导致并发能力下降,一般是做ddl处理时使用。
行锁则是锁住数据行,这种加锁方法比较复杂,但是由于只锁住有限的数据,对于其它数据不加限制,所以并发能力强,MySQL一般都是用行锁来处理并发事务。这里主要讨论的也就是行锁。
Read Committed(读取提交内容)
在RC级别中,数据的读取都是不加锁的,但是数据的写入、修改和删除是需要加锁的。效果如下
MySQL& show create table class_teacher \G\
Table: class_teacher
Create Table: CREATE TABLE `class_teacher` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`class_name` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL,
`teacher_id` int(11) NOT NULL,
PRIMARY KEY (`id`),
KEY `idx_teacher_id` (`teacher_id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
1 row in set (0.02 sec)
MySQL& select * from class_
+----+--------------+------------+
| id | class_name
| teacher_id |
+----+--------------+------------+
1 | 初三一班
3 | 初二一班
4 | 初二二班
+----+--------------+------------+
由于MySQL的InnoDB默认是使用的RR级别,所以我们先要将该session开启成RC级别,并且设置binlog的模式
SET session transaction isolation level
SET SESSION binlog_format = 'ROW';(或者是MIXED)
事务A事务B
update class_teacher set class_name='初三二班' where teacher_id=1;
update class_teacher set class_name='初三三班' where teacher_id=1;
ERROR 1205 (HY000): Lock w try restarting transaction
为了防止并发过程中的修改冲突,事务A中MySQL给teacher_id=1的数据行加锁,并一直不commit(释放锁),那么事务B也就一直拿不到该行锁,wait直到超时。
这时我们要注意到,teacher_id是有索引的,如果是没有索引的class_name呢?update class_teacher set teacher_id=3 where class_name = '初三一班';那么MySQL会给整张表的所有数据行的加行锁。这里听起来有点不可思议,但是当sql运行的过程中,MySQL并不知道哪些数据行是 class_name = '初三一班'的(没有索引嘛),如果一个条件无法通过索引快速过滤,存储引擎层面就会将所有记录加锁后返回,再由MySQL Server层进行过滤。
但在实际使用过程当中,MySQL做了一些改进,在MySQL Server过滤条件,发现不满足后,会调用unlock_row方法,把不满足条件的记录释放锁 (违背了二段锁协议的约束)。这样做,保证了最后只会持有满足条件记录上的锁,但是每条记录的加锁操作还是不能省略的。可见即使是MySQL,为了效率也是会违反规范的。(参见《高性能MySQL》中文第三版p181)
这种情况同样适用于MySQL的默认隔离级别RR。所以对一个数据量很大的表做批量修改的时候,如果无法使用相应的索引,MySQL Server过滤数据的的时候特别慢,就会出现虽然没有修改某些行的数据,但是它们还是被锁住了的现象。
Repeatable Read(可重读)
这是MySQL中InnoDB默认的隔离级别。我们姑且分&读&和&写&两个模块来讲解。
读就是可重读,可重读这个概念是一事务的多个实例在并发读取数据时,会看到同样的数据行,有点抽象,我们来看一下效果。
RC(不可重读)模式下的展现
事务A事务B
select id,class_name,teacher_id from class_teacher where teacher_id=1;
idclass_nameteacher_id
update class_teacher set class_name='初三三班' where id=1;
select id,class_name,teacher_id from class_teacher where teacher_id=1;
idclass_nameteacher_id
读到了事务B修改的数据,和第一次查询的结果不一样,是不可重读的。
事务B修改id=1的数据提交之后,事务A同样的查询,后一次和前一次的结果不一样,这就是不可重读(重新读取产生的结果不一样)。这就很可能带来一些问题,那么我们来看看在RR级别中MySQL的表现:
事务A事务B事务C
select id,class_name,teacher_id from class_teacher where&teacher_id=1;
idclass_nameteacher_id
update class_teacher set class_name='初三三班' where id=1;
insert into class_teacher values (null,'初三三班',1);
select id,class_name,teacher_id from class_teacher where&teacher_id=1;
idclass_nameteacher_id
没有读到事务B修改的数据,和第一次sql读取的一样,是可重复读的。
没有读到事务C新添加的数据。
我们注意到,当teacher_id=1时,事务A先做了一次读取,事务B中间修改了id=1的数据,并commit之后,事务A第二次读到的数据和第一次完全相同。所以说它是可重读的。那么MySQL是怎么做到的呢?这里姑且卖个关子,我们往下看。
不可重复读和幻读的区别
很多人容易搞混不可重复读和幻读,确实这两者有些相似。但不可重复读重点在于update和delete,而幻读的重点在于insert。
如果使用锁机制来实现这两种隔离级别,在可重复读中,该sql第一次读取到数据后,就将这些数据加锁,其它事务无法修改这些数据,就可以实现可重复读了。但这种方法却无法锁住insert的数据,所以当事务A先前读取了数据,或者修改了全部数据,事务B还是可以insert数据提交,这时事务A就会发现莫名其妙多了一条之前没有的数据,这就是幻读,不能通过行锁来避免。需要Serializable隔离级别 ,读用读锁,写用写锁,读锁和写锁互斥,这么做可以有效的避免幻读、不可重复读、脏读等问题,但会极大的降低数据库的并发能力。
所以说不可重复读和幻读最大的区别,就在于如何通过锁机制来解决他们产生的问题。
上文说的,是使用悲观锁机制来处理这两种问题,但是MySQL、ORACLE、PostgreSQL等成熟的数据库,出于性能考虑,都是使用了以乐观锁为理论基础的MVCC(多版本并发控制)来避免这两种问题。
悲观锁和乐观锁
正如其名,它指的是对数据被外界(包括本系统当前的其他事务,以及来自外部系统的事务处理)修改持保守态度,因此,在整个数据处理过程中,将数据处于锁定状态。悲观锁的实现,往往依靠数据库提供的锁机制(也只有数据库层提供的锁机制才能真正保证数据访问的排他性,否则,即使在本系统中实现了加锁机制,也无法保证外部系统不会修改数据)。
在悲观锁的情况下,为了保证事务的隔离性,就需要一致性锁定读。读取数据时给加锁,其它事务无法修改这些数据。修改删除数据时也要加锁,其它事务无法读取这些数据。
相对悲观锁而言,乐观锁机制采取了更加宽松的加锁机制。悲观锁大多数情况下依靠数据库的锁机制实现,以保证操作最大程度的独占性。但随之而来的就是数据库性能的大量开销,特别是对长事务而言,这样的开销往往无法承受。
而乐观锁机制在一定程度上解决了这个问题。乐观锁,大多是基于数据版本( Version )记录机制实现。何谓数据版本?即为数据增加一个版本标识,在基于数据库表的版本解决方案中,一般是通过为数据库表增加一个 &version& 字段来实现。读取出数据时,将此版本号一同读出,之后更新时,对此版本号加一。此时,将提交数据的版本数据与数据库表对应记录的当前版本信息进行比对,如果提交的数据版本号大于数据库表当前版本号,则予以更新,否则认为是过期数据。要说明的是,MVCC的实现没有固定的规范,每个数据库都会有不同的实现方式,这里讨论的是InnoDB的MVCC。
Serializable
这个级别很简单,读加共享锁,写加排他锁,读写互斥。使用的悲观锁的理论,实现简单,数据更加安全,但是并发能力非常差。如果你的业务并发的特别少或者没有并发,同时又要求数据及时可靠的话,可以使用这种模式。
这里要吐槽一句,不要看到select就说不会加锁了,在Serializable这个级别,还是会加锁的!
阅读(...) 评论()}

我要回帖

更多关于 事务隔离级别 的文章

更多推荐

版权声明:文章内容来源于网络,版权归原作者所有,如有侵权请点击这里与我们联系,我们将及时删除。

点击添加站长微信