隔离实际上是通过锁来实现的,作用于整个事务,它通常在事务开始前指定,如 SET TRANSACTION ISOLATION LEVEL READ Committed,指定后面的事务为 已提交读;而锁是在我们执行某一具体的SQL语句时在from中指定锁模式来实现的,它可以覆盖掉已指定隔离级别下应用的锁类型。隔离级别牺牲并发性来实现一致性。
并发:是指在相同的时间,多个用户访问相同的数据。它通常引起以下问题:脏读;丢失更新;不可重复度;幻读;
- 脏读:一个进程读取了另一个进程尚未提交的数据。
- 不可重复读:一个进程先后两次读取的数据不相同,若另一个进程在一个进程两次读取的中间修改了数据。
- 幻读:事务在执行过程中进行了两次相同的查询,第二次查询的结果包含了第一次查询未出现的数据或没有第一次查询出现的数据。这是因为在两次查询的执行过程之间,这是因为事务没有对读取的范围进行锁定造成的,在两次查询之间,另外一个事务执行了插入insert或删除delete。
- 更新丢失(Lost Update)。两个进程读取相同的数据,并进行的修改,一个进程会覆盖另一个进程的修改。或两个事务都同时修改同一数据,但第二个事务因发生错误而回滚,导致两次事务的数据修改都丢失了。这是因为系统没有执行任何锁操作,并发的事务没有进行隔离。
事务的隔离级别定义
为了避免事务并发带来的以上问题,按照SQL-99 标准定义,事务的隔离级别有以下几个(隔离性从低到高):
1、未提交读(Read Uncommitted)。允许读取其他事务未提交的数据,即允许脏读,但不允许其他事务同时更新本事务已更新的数据,因此可以避免更新丢失现象。未提交读隔离级别的事务,在更新数据状态时对数据加排他锁,并保持到事务结束;读取器不请求共享锁,处于无锁模式,也就与任何锁无冲突,可以访问被 排它锁 锁定的数据,因此可以读取其他事务尚未提交的更新数据。
2、提交读(Read Committed)。在读取数据时对数据加共享锁,在读取结束后立即释放共享锁,在更新数据状态时对数据加排他锁,并保持到事务结束。因共享锁与排他锁互斥,因此如果其他事务已经修改了数据的状态并未提交,则提交读隔离级别的事务因无法添加共享锁,从而避免读取未提交的数据,可以避免脏读。
3、可重复读(Repeatable Read)。在读取数据时对数据加共享锁,并将共享锁保持到事务结束,在更新数据状态时对数据加排他锁,并保持到事务结束。如果其他事务要修改当前事务已读取的数据时,因为当前事务对已读取的数据保持共享锁,而共享锁与排他锁互斥,导致其他事务无法加排他锁,从而保证事务对于同一数据的读取是可重复的,可避免不可重复读。
4、可串行化(Serializable)。这是最严格的事务隔离级别。在读取数据时,对读取数据的范围加共享锁,并将共享锁保持到事务结束,在更新数据状态时对数据加排他锁,并保持到事务结束。因为是对读取范围加共享锁,将导致其他事务无法对当前事务作用范围内的数据执行操作(更新、插入等),可以避免幻象读现象。范围一般是查询SQL中的where条件指定的。
下面是四种隔离级别下允许的事务并发带来的负作用及区别对照表:
隔离级别 | 脏读 | 不可重复读取 | 幻象读 | 区别 |
未提交读 | √ | √ | √ |
读取数据(select):无锁状态,可以读取被其他事务的 排它锁 锁定的 未提交 的数据; 更新数据(update):排它锁,保持到事务结束。 |
已提交读 | × | √ | √ |
读取数据(select):共享锁,读取完数据立即释放; 更新数据(update):排它锁,保持到事务结束。 |
可重复读 | × | × | √ |
读取数据(select):共享锁,保持到事务结束; 更新数据(update):排它锁,保持到事务结束。 |
可串行化 | × | × | × |
读取数据(select):对满足筛选器(通常为where条件指定的范围)的数据(包括已存在和 不存在但满足筛选器的)加共享锁,保持到事务结束; 更新数据(update):排它锁,保持到事务结束。 |
示例 1 如下:
(1)新建查询:
set tran isolation level read committed --定义事务隔离级别为 已提交读
begin tran
update student_cj set cj =90 where name ='小虎' and km ='数学'
waitfor delay '00:00:15' --延迟25秒执行
commit
(2)再新建查询:
set tran isolation level read committed
begin tran
select * from student_cj where name ='小明' and km ='数学'
commit
在(1)中,事务被设置为 已提交读 ,意味着在更新(update)中,将获得 排它锁,且该锁将保持到事务结束。而在(2)中由于查询(select)要获得共享锁才能查询,因此要等待15秒直到(1)执行完才可获得共享锁进而才能执行查询。
示例 2 如下:
(1)新建查询:
set tran isolation level read committed --定义事务隔离级别为 已提交读
begin tran
update student_cj set cj =90 where name ='小李' and km ='数学'
waitfor delay '00:00:15' --延迟25秒执行
commit
(2)新建查询
set tran isolation level read uncommitted --定义事务隔离级别为 未提交读
begin tran
select * from student_cj where name ='小李' and km ='数学'
commit
在(1)中,事务被设置为 已提交读 ,意味着在更新(update)中,将获得 排它锁,且该锁将保持到事务结束。而在(2)中为 未提交读, select 无需获取锁即可查询,因此不管(1)中是否有排它锁都不妨碍无锁的查询。