事务及事务隔离级别

什么是事务

        事务,就是一组操作数据库的动作集合。数据库应用系统通过事务集来完成对数据库的存取。事务的正确执行使得数据库从一种状态转换为另一种状态

       事务必须服从ISO/IEC所制定的ACID原则。ACID是原子性(atomicity)、一致性(consistency)、隔离性(isolation)、持久性(durability)的缩写,这四种状态的意思是:

    1、原子性

      即不可分割,事务要么全部被执行,要么全部不执行。如果事务的所有子事务全部提交成功,则所有的数据库操作被提交,数据库状态发生变化;如果有子事务失败,则其他子事务的数据库操作被回滚,即数据库回到事务执行前的状态,不会发生状态转换

    2、一致性

    事务的执行使得数据库从一种正确状态转换成另外一种正确状态

    3、隔离性

     在事务正确提交之前,不允许把事务对该数据的改变提供给任何其他事务,即在事务正确提交之前,它可能的结果不应该显示给其他事务

    4、持久性

   事务正确提交之后,其结果将永远保存在数据库之中,即使在事务提交之后有了其他故障,事务的处理结果也会得到保存

事务的作用

     事务管理对于企业级应用而言至关重要,它保证了用户的每一次操作都是可靠的,即便出现了异常的访问情况,也不至于破坏后台数据的完整性。就像银行的自动提款机ATM,通常ATM都可以正常为客户服务,但是也难免遇到操作过程中及其突然出故障的情况,此时,事务就必须确保出故障前对账户的操作不生效,就像用户刚才完全没有使用过ATM机一样,以保证用户和银行的利益都不受损失。

并发下事务会产生的问题  

       举个例子,事务A和事务B操纵的是同一个资源,事务A有若干个子事务,事务B也有若干个子事务,事务A和事务B在高并发的情况下,会出现各种各样的问题。"各种各样的问题",总结一下主要就是五种:第一类丢失更新、第二类丢失更新、脏读、不可重复读、幻读

1.第一类丢失更新 

    A事务撤销时,把已经提交的B事务的更新数据覆盖了。这种错误可能造成很严重的问题,通过下面的账户取款转账就可以看出来:

时间

取款事务A

转账事务B

T1

开始事务

 

T2

开始事务

T3

查询账户余额为1000元    

T4

查询账户余额为1000元

T5

汇入100元把余额改为1100元

T6

提交事务

T7

取出100元把余额改为900元

T8

撤销事务

T9

余额恢复为1000 元(丢失更新)

A事务在撤销时,“不小心”将B事务已经转入账户的金额给抹去了。 

  2.第二类丢失更新 

A事务覆盖B事务已经提交的数据,造成B事务所做操作丢失:   

时间

转账事务A

取款事务B

T1

 

开始事务

T2

开始事务

                         

T3

               

查询账户余额为1000元    

T4

查询账户余额为1000元

                         

T5

取出100元把余额改为900元

T6

提交事务           

T7

汇入100元

T8

提交事务

T9

把余额改为1100 元(丢失更新)

    上面的例子里由于支票转账事务覆盖了取款事务对存款余额所做的更新,导致银行最后损失了100元,相反如果转账事务先提交,那么用户账户将损失100元。

    有些系统中第二类丢失更新可能就影响很大了,举个简单的例子:
    财务系统加工资,若公司本次调薪决定给员工张三加1k人民币,财务部两名操作人员A和B,过程情况若是这样的:
   1)A操作员在应用系统的页面上查询出张三的薪水信息,然后选择薪水记录进行修改,打开修改页面但A突然有事离开了,页面放在那没有做任何的提交。
    2)这时候B操作员同样在应用中查询出张三的薪水信息,然后选择薪水记录进行修改,录入增加薪水额1000,然后提交了。
    3)这时候A操作员回来了,在自己之前打开的薪水修改页面上也录入了增加薪水额1000,然后提交了。
    其实上面例子操作员A和B只要一前一后做提交,悲剧就出来了。后台修改薪水的sql:update 工资表 set salary = salary + 增加薪水额 where staff_id = ‘员工ID’。这个过程走下来后结果是:张三开心了这次涨了2k,操作员A和B都郁闷了。

解决思路:
       基本两种思路,一种是悲观锁,另外一种是乐观锁
      简单的说就是一种假定这样的问题是高概率的,最好一开始就锁住,免得更新老是失败;另外一种假定这样的问题是小概率的,最后一步做更新的时候再锁住,免得锁住时间太长影响其他人做有关操作。

解决方案1(悲观锁)

   a.传统的悲观锁法(不推荐):
         以上面的例子来说明,在弹出修改工资的页面初始化时(这种情况下一般会去从数据库查询出来),在这个初始化查询中使用select ...for update nowait, 通过添加for update nowait语句,将这条记录锁住,避免其他用户更新,从而保证后续的更新是在正确的状态下更新的。然后在保持这个链接的状态下,在做更新提交。当然这个有个前提就是要保持链接,就是要对链接要占用较长时间,这个在现在web系统高并发高频率下显然是不现实的。

  b.现在的悲观锁法(推荐优先使用):
        在修改工资这个页面做提交时先查询下,当然这个查询必须也要加锁(select ...for update nowait),有人会说,在这里做个查询确认记录是否有改变不就行了吗,是的,是要做个确认,只是你不加for update就不能保证你在查询到更新提交这段时间里这条记录没有被其他会话更新过,所以这种方式也需要在查询时锁定记录,保证在这条记录没有变化的基础上再做更新,若有变化则提示告知用户。

 解决方案2(乐观锁)

a.旧值条件(前镜像)法:
       就是在sql更新时使用旧的状态值做条件,SQL大致如下 Update table set col1 = newcol1value, col2 = newcol2value…. where col1 = oldcol1value and col2 = oldcol2value….,在上面的例子中我们就可以把当前工资作为条件进行更新,如果这条记录已经被其他会话更新过,则本次更新了0行,这里我们应用系统一般会做个提示告知用户重新查询更新。这个取哪些旧值作为条件更新视具体系统实际情况而定。(这种方式有可能发生阻塞,如果应用其他地方使用悲观锁法长时间锁定了这条记录,则本次会话就需要等待,所以使用这种方式时最好统一使用乐观锁法。)

 b.使用版本列法(推荐优先使用):
       其实这种方式是一个特殊化的前镜像法,就是不需要使用多个旧值做条件,只需要在表上加一个版本列,这一列可以是NUMBER或 DATE/TIMESTAMP列,加这列的作用就是用来记录这条数据的版本(在表设计时一般我们都会给每个表增加一些NUMBER型和DATE型的冗余字段,以便扩展使用,这些冗余字段完全可以作为版本列用),在应用程序中我们每次操作对版本列做维护即可。在更新时我们把上次版本作为条件进行更新。

  c.使用校验和法(不推荐)

 d.使用ORA_ROWSCN法(不推荐)     
        综上所述,我们对丢失更新问题建议采取上面的悲观锁b方法或乐观锁b方法(红字体已标注),其实这两种方式的本质都一样,都是在更新提交时做一次查询确认在更新提交,我个人觉得都是乐观的做法,区别在于悲观锁b方法是通过select..for
update方式,这个可能会导致其他会话的阻塞,而乐观锁b方法需要多一个版本列的维护。

3、脏读

     所谓脏读,就是指事务A读到了事务B还没有提交的数据,指当一个事务正在访问数据,并且对数据进行了修改,而这种修改还没有提交到数据库中,这时,另外一个事务也访问这个数据,然后使用了这个数据。
 e.g.
       1.Mary的原工资为1000, 财务人员将Mary的工资改为了8000(但未提交事务)
        2.Mary读取自己的工资 ,发现自己的工资变为了8000,欢天喜地!
       3.而财务发现操作有误,回滚了事务,Mary的工资又变为了1000
         像这样,Mary记取的工资数8000是一个脏数据。

2、不可重复读

     所谓不可重复读,就是指在一个事务里面读取了两次某个数据,读出来的数据不一致还是以银行取钱为例,事务A开启事务-->查出银行卡余额为1000元,此时切换到事务B事务B开启事务-->事务B取走100元-->提交,数据库里面余额变为900元,此时切换回事务A,事务A再查一次查出账户余额为900元,这样对事务A而言,在同一个事务内两次读取账户余额数据不一致,这就是不可重复读。

3、幻读

     所谓幻读,就是指在一个事务里面的操作中发现了未被操作的数据比如学生信息,事务A开启事务-->修改所有学生当天签到状况为false,此时切换到事务B,事务B开启事务-->事务B插入了一条学生数据,此时切换回事务A,事务A提交的时候发现了一条自己没有修改过的数据,这就是幻读,就好像发生了幻觉一样。幻读出现的前提是并发的事务中有事务发生了插入、删除操作。

事务隔离级别

      事务隔离级别,就是为了解决上面几种问题而诞生的。为什么要有事务隔离级别,因为事务隔离级别越高,在并发下会产生的问题就越少,但同时付出的性能消耗也将越大,因此很多时候必须在并发性和性能之间做一个权衡所以设立了几种事务隔离级别,以便让不同的项目可以根据自己项目的并发情况选择合适的事务隔离级别,对于在事务隔离级别之外会产生的并发问题,在代码中做补偿。

   事务隔离级别有4种,但是像Spring会提供给用户5种,来看一下:

   1、DEFAULT

     默认隔离级别,每种数据库支持的事务隔离级别不一样,如果Spring配置事务时将isolation设置为这个值的话,那么将使用底层数据库的默认事务隔离级别。顺便说一句,如果使用的MySQL,可以使用"select @@tx_isolation"来查看默认的事务隔离级别

2、READ_UNCOMMITTED

     读未提交,即能够读取到没有被提交的数据,所以很明显这个级别的隔离机制无法解决脏读、不可重复读、幻读中的任何一种,因此很少使用

3、READ_COMMITED

     读已提交,即能够读到那些已经提交的数据,自然能够防止脏读,但是无法限制不可重复读和幻读

4、REPEATABLE_READ(mysql 默认隔离级别)

     重复读取,即在数据读出来之后加锁,类似"select * from XXX for update",明确数据读取出来就是为了更新用的,所以要加一把锁,防止别人修改它。REPEATABLE_READ的意思也类似,读取了一条数据,这个事务不结束,别的事务就不可以改这条记录,这样就解决了脏读、不可重复读的问题,但是幻读的问题还是无法解决

5、SERLALIZABLE

     串行化,最高的事务隔离级别,不管多少事务,挨个运行完一个事务的所有子事务之后才可以执行另外一个事务里面的所有子事务,这样就解决了脏读、不可重复读和幻读的问题了

网上专门有图用表格的形式列出了事务隔离级别解决的并发问题:

       再必须强调一遍,不是事务隔离级别设置得越高越好,事务隔离级别设置得越高,意味着势必要花手段去加锁用以保证事务的正确性,那么效率就要降低,因此实际开发中往往要在效率和并发正确性之间做一个取舍,一般情况下会设置为READ_COMMITED,此时避免了脏读,并发性也还不错,之后再通过一些别的手段去解决不可重复读和幻读的问题就好了。

事物隔离级别查看及修改

      首先说明一下MySQL查看和修改事务隔离级别的几个命令:

  • 查看事务隔离级别使用select @@tx_isolation
  • 修改当前会话事务隔离级别使用SET session TRANSACTION ISOLATION LEVEL Serializable;(参数可以为:Read uncommitted|Read committed|Repeatable read|Serializable)
  • 修改全局事务隔离级别使用SET global TRANSACTION ISOLATION LEVEL Serializable;(参数可以为:Read uncommitted|Read committed|Repeatable read|Serializable)

修改了会话的事务隔离级别,比如MyBatis,getSqlSession()的时候,只针对这一次拿到的Session有效;比如CMD命令行,只对这一次的窗口有效。

修改了全局的事务隔离级别,那么针对此后所有的会话有效,当前已经存在的会话不受影响

猜你喜欢

转载自blog.csdn.net/laomumu1992/article/details/82743891
今日推荐