Mysql事务隔离级别与锁

来源:互联网 发布:java实现音乐网站 编辑:程序博客网 时间:2024/06/05 14:10
    数据库的事务有几种特性,例如一致性和隔离性,一般通过加锁来实现。同时数据库又是一个高并发的应用,如果加锁过度或者不当将严重影响性能。数据库提供了几种隔离级别来供选择,本文通过解析InnoDB的加锁机制是如何实现几种隔离级别,来更深刻的理解mysql的锁。

   两阶段锁
    首先,事务的所操作分为两个阶段:加锁和解锁,两者不想交。因为事务开始时,并不知道会用到哪些数据,所以加锁阶段随着事务的执行,可能一直在执行。事务结束时,一起将锁释放。注意:不相交!这是两阶段锁的原则,但是有时为了效率也会违反,后面再详述。这种方法由于加锁不是一次获取全部的锁,可能出现死锁,但是事务的并发调度是串行化的。

   四种隔离级别:
  • Read Uncommit:未提交读。允许脏读。
  • Read Commit : 提交读。只能读到其他事务已提交的内容。允许不重复读。
  • Repeated Read : 可重复读。同一个事务内的查询与事务开始时是一致的。允许幻读。Mysql默认的级别。
  • Serializable : 串行化的读。每次读都要获取表级锁,读写互相阻塞。
    Read Uncommit一般不会使用到,并且没有加锁,所以不讨论。
    Serializable也比较简单粗暴,串行化的读写就不会有问题,但是效率低下,称为悲观锁。相对于悲观锁,乐观锁在一定程度上环节了效率的问题。乐观锁大多是基于八本纪录机制实现的,在mysql中为MVCC(多版本并发控制)。我们主要来谈MVCC和RC、RR。

    MVCC  (详见http://blog.csdn.net/endlu/article/details/51518377)
    InnoDB总为每一行后面加入了两个隐藏的列,来实现MVCC。这两个列分别纪录了数据最后一次被哪个事务创建、更新的事务号;该事物是否被删除,被删除的事务的事务号。事务号是递增的。虽然这格外增加的存储空间,每一行都要存储额外的历史版本,而且还要定期删除。但是多版本的实现,为大多数的读惭怍提供了方便:读数据只需要根据事务号来读取某一历史版本,不用再担心并发读写的问题而加锁,效率高,并且可以实现隔离级别中要求的只读取符合条件的值。我们称这种读叫快照读,相反如果读取当前的最新数据叫当前读。mysql在RC/RR下的普通select都为快照读,不用加锁。
    在RR级别下,各种操作在MVCC下会有怎么样的效果呢?
  • select时,读取版本号<=当前事务号并且删除版本号为空或>当前事务号的行。
  • insert,保存事务号为当前事务号。
  • delete,保存当前事务号为删除版本号。
  • update,创建新的一行,保存事务号为当前事务号。

   快照读/当前读
    说完MVCC,我们回到隔离级别。上面说的隔离级别的介绍中,关于RR不能解决幻读的问题,是耳熟能详的,到处都是这么写的。但是实际呢?经过实验,session A select * from test where age = 12; 然后Session B插入一个age=12的行,Session A再次select,并没有返回新插入的行。由此可见Mysql中幻读的读问题已经解决了。原理就是上文提到的快照读。但是,当前读的冲突问题呢?
  • 快照读:
    • select * from test where ... ;
  • 当前读:
    • select * from test where ... in share mode ;
    • select * from test where ... for update ;
    • insert
    • update
    • delete
    事务的隔离级别实际上是定义了当前读的级别。为了减少锁竞争,引入了快照读,使得普通的select不同加锁。其他操纵还是需要加锁的。
    为了解决当前读的幻读问题,Mysql使用了Next-key锁。就是GAP(间隙锁)和行锁的合并。行锁可以避免并发修改同一行带来的问题,但是插入操作呢?间隙锁GAP就是为了解决这个问题。
    在RC级别中,表test,age为主键:
  1. A:select * from test where age = 10;  返回一条记录
  2. A:update test set name = ‘a’ where age = 10;
  3. B:insert into test values(10,‘b’);
  4. B:commit;
  5. A:select * from test where age = 10;  返回两条记录
  6. A:commit ;
    在这个例子中,步骤二虽然进行了update,对age=10的记录加了行锁。但是session B插入了新纪录,并提交。A就可以读到。
    在RR级别中,重新进行这个实验,A的第二次读,仍然返回一条记录。因为在步骤二中,不止对age=10的行加了行锁,还有间隙锁。session B的插入将被阻塞,等待获取锁,A提交后才能被执行。
    
   InnoDB行锁
    上面说到,InnoDB当前读,会对行加锁,防止并发问题。这里有个前提:where条件是索引,可以通过索引来过滤行来对指定行加锁。如果不是索引,InnoDB会对所有行加锁,但是为了提供并发效率。等存储系统返回数据后,会过滤后再对不符合条件的行释放锁。还记得本文开头介绍两阶段锁时,说过有时为了效率会违反这个原则么。就是现在所说的这种情况。先获取全部锁,再释放掉多余的锁。

    特殊情况
    这里举一个例子:
  1. A:select * from test where age = 10;  返回一条记录
  2. B:insert into test values(10,‘b’);
  3. B:commit;
  4. A:update test set name = ‘qwe’where age = 10;
  5. A:select * from test where age = 10;  返回两条记录
    看到这个结果,有人不禁起了疑问:不是说InnoDB在RR级别解决了幻读问题么?怎么同一个事务中两次读读到了不同的行数。
    这里A的前后两次读,均为快照读,而且是在同一个事务中。但是B先插入直接提交,此时A再update,update属于当前读,所以可以作用于新插入的行,并且将改行的当前版本号设为A的事务号,所以第二次的快照读,是可以读取到的,因为同事务号。这种情况符合MVCC的规则,如果要称为一种幻读也非不可,算为一个特殊情况来看待吧。
1 0
原创粉丝点击