当前位置: 首页 > 图灵资讯 > 技术篇> 事务隔离以及隔离级别详解

事务隔离以及隔离级别详解

来源:图灵教育
时间:2023-04-07 10:23:38

  事务是指访问并可能更新数据库中各种数据项的一个程序执行单元(unit)。‘隔离’顾名思义,断绝接触;断绝往来。从它的出处—“蜀山兀,阿房出。覆压三百余里,隔离天日。” 杜牧的《阿房宫赋》中我们可以体会到隔离的奥妙。而事务的隔离性和它如出一辙。正如人患了传染病需要被隔离一样,为避免事务引发的问题,事务也是需要隔离的。说到事务的隔离级别我们不得不想到事务的四大特性,即原子性,隔离性,一致性和持久性。我们通过事务隔离及事务隔离级别来深入了解事务的一些特性和在实例中的应用。

  一、事务的基本要素:

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

  2、一致性(Consistency):事务开始前和结束后,数据库的完整性约束没有被破坏 。比如A向B转账,不可能A扣了钱,B却没收到。

  3、隔离性(Isolation):同一时间,只允许一个事务请求同一数据,不同的事务之间彼此没有任何干扰。比如A正在从一张银行卡中取钱,在A取钱的过程结束前,B不能向这张卡转账。

  4、持久性(Durability):事务完成后,事务对数据库的所有更新将被保存到数据库,不能回滚。

  二、事务的并发问题

  在并发事务没有进行隔离的情况会出现脏读,不可重复读,幻读的情况。下面我们具体来说一下这些事务的并发问题:

  1、脏读:即事务A读取了事务B更新的数据,然后B回滚操作,那么A读取到的B未提交的数据是脏数据。

  2、不可重复读:事务 A 多次读取同一数据,事务 B 在事务A多次读取的过程中,对数据作了更新并提交,导致事务A多次读取同一数据时,结果不一致。不可重复读和脏读的区别是,脏读是读取前一事务未提交的脏数据,不可重复读是重新读取了前一事务已提交的数据。

  3、幻读:指在一个事务内读取到了别的事务插入的数据,导致前后读取不一致。举个简单的例子:系统管理员A将数据库中所有学生的成绩从具体分数改为ABCDE等级,但是系统管理员B就在这个时候插入了一条具体分数的记录,当系统管理员A改结束后发现还有一条记录没有改过来,就好像发生了幻觉一样,这就叫幻读。

  四、事务的隔离级别

  为了更好地理解事务的隔离级别和事务并发问题之间的关系,我们制作了下面的表格能让你一目了然。

  1.读未提交:隔离级别最低的一种事务级别。在这种隔离级别下,会引发脏读、不可重复读和幻读。

  2.读已提交:读到的都是别人提交后的值。这种隔离级别下,会引发不可重复读和幻读,但避免了脏读。

  3.可重复读:这种隔离级别下,会引发幻读,但避免了脏读、不可重复读。

  4.串行化:是最严格的隔离级别。在Serializable隔离级别下,所有事务按照次序依次执行。脏读、不可重复读、幻读都不会出现。

  五、实例分析

  首先, 我们在数据库里建一个测试表,并插入数据:

  create table test_table (

  col1 int,

  col2 varchar(15)

  )

  go

  alter table test_table lock datarows

  go

  create unique index ux_idx1 on test_table (col1)

  go

  insert test_table (col1, col2) values (1, "test")

  insert test_table (col1, col2) values (2, "test")

  insert test_table (col1, col2) values (3, "test")

  insert test_table (col1, col2) values (4, "test")

  insert test_table (col1, col2) values (5, "test")

  Go

  显示一下数据:

  col1 col2

  ----------- ---------------

  1 test

  2 test

  3 test

  4 test

  5 test

  (5 rows affected)

  注意上面建表的时候,把表的锁改为了行模式

  alter table test_table lock datarows

  go

  现在打开两个窗口, 分别叫做窗口A,和窗口B。

  1.读未提交

  设置窗口A的事务隔离级别为 0 (读未提交)

  SET TRANSACTION ISOLATION LEVEL 0

  go

  在窗口B中,执行如下SQL

  begin tran

  update test_table

  set col2 = "TEST UPDATE"

  where col1 = 1

  go

  我们看到输出:

  (1 row affected)

  表示上面的SQL执行成功。 注意: 上面的SQL开启了一个事务,但是事务并没有提交。这时,在窗口A,执行如下SQL,查看同一条记录(col1=1)的值:

  select * from test_table where col1 = 1

  go

  我们看到输出:

  col1 col2

  ----------- ---------------

  1 TEST UPDATE

  (1 row affected)

  我们看到窗口B里更新后的值。也就是,B窗口里并没有提交的数据, 我们在窗口A中读到了。这就是读未提交。

  2.读已提交

  下面介绍读已提交,这时候,把窗口A的事务隔离级别设置为1(读已提交)

  SET TRANSACTION ISOLATION LEVEL 1

  go

  并在窗口A执行如下SQL

  select * from test_table where col1 = 1

  go

  这时候,我们发现上面的SQL语句阻塞了。因为事务隔离级别是读已提交,窗口A中的事务和窗口B中的事务操作了同一条记录,它在等待窗口B中的事务提交或者回滚。如果窗口B中的事务没有完成,它就一直等待下去。

  这时候,我们提交窗口B中的事务,窗口B执行:

  commit

  go

  现在,再看窗口A, 刚才的阻塞解除了,窗口A看到如下输出:

  col1 col2

  ----------- ---------------

  1 TEST UPDATE

  (1 row affected)

  窗口A的事务,读到了窗口B提交后的数据。

  总结:事务隔离界别设置为读已提交,只有提交后的数据才能被读取。如果当前事务读取的记录在另外一个事务中更新了,且还没有的提交,当前读取操作会被阻塞。

  3.可重复读

  再来介绍可重复读,可重复读对应的就是不可重复读,先介绍什么是不可重复读。现在窗口A中的事务隔离级别还是读已提交,先不用改它。我们在窗口A中执行如下SQL

  begin tran

  select * from test_table where col1 = 2

  go

  我们立即看到输出:

  col1 col2

  ----------- ---------------

  2 test

  (1 row affected)

  上面的SQL,开启了一个事务,并读了一行数据 col2=2, 但是并没有提交事务。这时,在窗口B中,我们执行如下SQL,修改col2=2的值:

  update test_table

  set col2 = "TEST UPDATE"

  where col1 = 2

  go

  SQL执行成功,立即看到输出:

  (1 row affected)

  表示更新成功。

  这时候,回到窗口A,在来查询 col2=2的值,

  select * from test_table where col1 = 2

  go

  col1 col2

  ----------- ---------------

  2 TEST UPDATE

  (1 row affected)

  这时,读出来的值是窗口B中更新后的值。这样问题就来了,在窗口A,同一个事务里,两次读取同一个值,返回的结果不一样,这就是不可重复读。

  4.串行化读

  最后来看看串行化读,提交窗口A中的事务,并设置事务隔离级别到串行化读,窗口A中执行:

  commit tran

  go

  SET TRANSACTION ISOLATION LEVEL 3

  go

  现在窗口A执行如下SQL:

  begin tran

  select * from test_table

  go

  输出:

  col1 col2

  ----------- ---------------

  1 TEST UPDATE

  2 TEST UPDATE

  3 test

  4 TEST UPDATE UPD

  5 test

  6 test

  (6 rows affected)

  切换到窗口B中,执行:

  insert test_table (col1, col2) values (7, "test")

  go

  这是,窗口B中,上面的SQL语句被阻塞了,插入操作不能完成。

  窗口A中,执行select *:

  select * from test_table

  go

  输入和上次的一样:

  col1 col2

  ----------- ---------------

  1 TEST UPDATE

  2 TEST UPDATE

  3 test

  4 TEST UPDATE UPD

  5 test

  6 test

  (6 rows affected)

  可见,串行化解决了泛读的问题。

  窗口A中,执行

  commit tran

  go

  这时,窗口B阻塞解除,

  (1 row affected)

  查询一下:

  select * from test_table

  go

  col1 col2

  ----------- ---------------

  1 TEST UPDATE

  2 TEST UPDATE

  3 test

  4 TEST UPDATE UPD

  5 test

  6 test

  7 test

  (7 rows affected)

  根据上面的实例我们不难看出事务隔离级别为读提交时,写数据只会锁住相应的行事务。一个间隙被事务加了锁,其他事务是不能在这个间隙插入记录的,这样可以防止幻读。事务隔离级别为串行化时,读写数据都会锁住整张表。

  熟练掌握事务的相关知识是我们打开数据库知识大门的钥匙,希望我们都能够拿到这把钥匙。使用事务不仅仅可以保证数据的一致性和完整性,让我们更好地保存和调用数据,为数据库的稳定和优化打下了坚实的基础。