事务并发可能产生的问题:(在不考虑事务隔离的情况下)
脏读:
脏读是指在一个事务处理过程里读取了另一个未提交的事务中的数据。
不可重复读:
不可重复读是指在对于数据库中的某个数据,一个事务范围内多次查询却返回了不同的数据值,这是由于在查询间隔,被另一个事务修改并提交了。
幻读/虚读:
是指当事务不是独立执行时发生的一种现象,例如第一个事务对一个表中的数据进行了修改,这种修改涉及到表中的全部数据行。同时,第二个事务也修改这个表中的数据,这种修改是向表中插入一行新数据。那么,以后就会发生操作第一个事务的用户发现表中还有没有修改的数据行,就好象发生了幻觉一样。
不可重复读与幻读的区别:
不可重复读的重点是修改,同样的条件,你读取过的数据,再次读取出来发现值不一样;(主要在于update)
幻读的重点在于新增或者删除,同样的条件,第 1 次和第 2 次读出来的记录数不一样。(主要在于insert和delete)
MVCC(多版本并发控制)
MVCC就是为了实现读写冲突不加锁
多版本并发控制,顾名思义,在并发访问的时候,数据存在版本的概念,可以有效地提升数据库并发能力,常见的数据库如MySQL、MS SQL Server、IBM DB2、Hbase、MongoDB等等都在使用。
简单讲,如果没有MVCC,当想要读取的数据被其他事务用排它锁锁住时,只能互斥等待;而这时MVCC可以通过提供历史版本从而实现读取被锁的数据的历史版本,从而避免了互斥等待。
InnoDB采用的MVCC实现方式是:在需要时,通过undo日志构造出历史版本。
事务的隔离级别:
为了解决上面说的并发所导致的问题,就需要设置数据的隔离级别,事务的隔离级别是通过锁、MVCC的方式实现
Read uncommitted:读未提交,最低级别,以上情况都无法保证
实现机制:在前文有说到所有写操作都会加排它锁,那还怎么读未提交呢?因为排他锁会阻止其它事务再对其锁定的数据加读或写的锁,但是对不加锁的读就不起作用了。READ UNCOMMITTED隔离级别下, 读不会加任何锁。而写会加排他锁,并到事务结束之后释放。
Read committed:读已提交,防止数据脏读
实现机制:事务中的修改操作会加排他锁,直到事务提交时才释放锁。读取数据不加锁而是使用了MVCC机制。因此在读已提交的级别下,都会通过MVCC获取当前数据的最新快照,不加任何锁,也无视任何锁(因为历史数据是构造出来的,身上不可能有锁)。
为什么Read committed可以防止数据脏读:脏读是因为读取了其他事务未提交的数据,之后事务回滚了,导致脏读。但是如果在事务中修改数据时加了排他锁,并且直到事务提交时才释放排他锁,在这之间不允许其他事务查询此记录,所以不会出现脏读。
为什么遗留了不可重复读和幻读问题:MVCC版本的生成时机: 是每次select时。这就意味着,如果我们在事务A中执行多次的select,在每次select之间有其他事务更新了我们读取的数据并提交了,那就出现了不可重复读,即:重复读时,会出现数据不一致问题,后面我们会讲解超支现象,就是这种引起的。
Repeatable read:
实现机制:READ COMMITTED级别不同的是MVCC版本的生成时机,即:一次事务中只在第一次select时生成版本,后续的查询都是在这个版本上进行,从而实现了可重复读。
Serializable:
实现机制:所有的读操作均为当前读,读加读锁 (S锁),写加写锁 (X锁)。采用的是范围锁RangeS RangeS_S模式,锁定检索范围为只读,这样就避免了幻影读问题。
Serializable隔离级别下,读写冲突,因此并发度急剧下降,在MySQL/InnoDB下不建议使用。
查看当前数据库隔离级别:
show global variables like '%isolation%';
MySQL数据库设置事务隔离级别:(设置数据库的隔离级别要在开启事务之前)
set [global | session] transaction isolation level 隔离级别名称; 或 set tx_isolation='隔离级别名称'
set session transaction isolation level read uncommitted; -- 设置read uncommitted级别
set session transaction isolation level read committed; -- 设置read committed级别
set session transaction isolation level repeatable read; -- 设置repeatable read级别
set session transaction isolation level serializable; -- 设置serializable级别
ADO.NET设置事务隔离级别:
var tran = conn.BeginTransaction(IsolationLevel.ReadUncommitted)
测试几种隔离级别:
read uncommitted:
打开两个会话,先后执行会话1、会话2中的代码
会话1:
set autocommit=0; -- 设置不自动提交
update actor set first_name='fan' where actor_id=1; -- 将姓名修改为fan,不提交
会话2:
set session transaction isolation level read uncommitted; -- 将当前会话隔离级别设置为read uncommitted
select * from actor where actor_id=1; -- 可以读取到会话1修改后的数据
read committed:
会话1:
set autocommit=0;
update actor set first_name='fan' where actor_id=1;
会话2:
set session transaction isolation level read committed; -- 将当前会话隔离级别设置为read committed
select * from actor where actor_id=1; -- 会话2读取到的还是原来的数据,直到会话1提交后,会话2才可以读到修改后的数据
再测试一下是否可以重复读:
会话1:
set session transaction isolation level read committed;
set autocommit=0;
select * from actor where actor_id=1; -- 先执行这个sql,查看结果。然后再执行会话2
select * from actor where actor_id=1; -- 执行完会话2后再执行一次查询,对比两次结果是否一致
会话2:
update actor set first_name='fan' where actor_id=1; -- 将姓名修改为fan
结果是会话1中两次查询结果不一致
repeatable read:
会话1:
set session transaction isolation level repeatable read;
set autocommit=0;
select * from actor where actor_id=1; -- 先执行这个sql,查看结果。然后再执行会话2
select * from actor where actor_id=1; -- 执行完会话2后再执行一次查询,对比两次结果是否一致
会话2:
update actor set first_name='fan' where actor_id=1; -- 将姓名修改为fan
结果是会话1中两次查询结果一致
serializable:
会话1:
set session transaction isolation level serializable;
set autocommit=0;
select * from actor where actor_id=1;
会话2:
update actor set first_name='fan' where actor_id=1;