系统的理解关系型数据库中的事务
ACID是为了保证事务能够正确执行提出的概念:
⑴ 原子性(Atomicity)
原子性是指事务包含的所有操作要么全部成功,要么全部失败回滚,因此事务的操作如果成功就必须要完全应用到数据库,如果操作失败则不能对数据库有任何影响。
⑵ 一致性(Consistency)
系统保证一个事务单元运行结束之后,其中涉及到的数据才会对其他事务单元可见。事务中间的状态是不会被外部看到的。
⑶ 隔离性(Isolation)
隔离性是当多个用户并发访问数据库时,比如操作同一张表时,数据库为每一个用户开启的事务,不能被其他事务的操作所干扰,多个并发事务之间要相互隔离。
⑷ 持久性(Durability)
持久性是指一个事务一旦被提交了,那么对数据库中的数据的改变就是永久性的,即便是在数据库系统遇到故障的情况下也不会丢失提交事务的操作。
理解这四个条目并不难。但是MySQL数据库中有一个概念叫事务的隔离级别,分为:
① Serializable (串行化):可避免脏读、不可重复读、幻读的发生。
② Repeatable read (可重复读):可避免脏读、不可重复读的发生。
③ Read committed (读已提交):可避免脏读、不可重复读的发生。
④ Read uncommitted (读未提交):最低级别,任何情况都无法保证。
有些同学可能比较困惑了,ACID已经可以保证事务的正确执行了,为什么还要提出事务的隔离级别这个概念呢?其中的脏读幻读又是什么?
ACID虽然可以保证事务的正确执行,但是人们想要在事务可以正确执行的时候尽可能的快速执行。对事务的操作实际上就是读和写两种操作。实现ACID最容易理解的方法(可能你也可以想到)就是:
让待处理的事务们排成串,一个执行完了再执行另一个,这就叫串行化(Serializable )。这样肯定可以保证ACID,然而速度太慢了。
每一个事务的执行都会对数据库加锁,其他任何事务都只能等待这个事务执行完毕才能执行。如果一个事务浪费了2 ms,那么它后面n个事务就要因为他浪费n*2 ms!所以,必须做出优化。
1阶优化方案:假如我的数据库中有一万条记录,有没有可能某个事务会把其中一万条记录都读/写一遍呢?几乎没有。我们可以只对一个操作所用到的几行记录进行加锁,只要用到的数据行不受干扰,事务就依然是安全的。其他行可以在同时被其他事务操作。
一个队列被分为多个队列,优化效果很明显!
2阶优化方案:读写锁。为数据库的读操作和写操作加不同的锁。因为读操作和写操作有着本质的区别。区分锁的好处是可以有针对性的进一步的优化,比如让读操作之间可以并行。理论上读与读之间是不会破坏事务的。事物依然是完整的,并且性能得到了提升。对应的隔离级别是repeat read。
3阶优化方案。从这里往后的优化策略,都会不同程度的破坏事务的隔离性。
上面只是让读与读之间不冲突,这里的原理就是让读不影响后续的写入操作。换句话说,读锁可以被写锁取代。但是反过来还不行。此时的操作是这样的:假如现在有1个读操作后面跟着9个读操作和一个写操作。因为读的时候可以被写,所以这10个读操作执行期间,写操作是可以被执行的。但是一旦写操作被执行了(比如在第5个读操作之后被执行),后续的读就只能等着了,因为此时写的同时不能读。当这个操作写完了之后后续的读才能进行。
现实中遇到的业务逻辑,往往都包含了多条SQL,比如A转账给B,其中涉及到了读A、读B、写A、写B四条SQL。这四条SQL组成了一个事务。现在考虑这样两个事物:
事物X:写A,读A
事物Y:写A
加入A是用于记录名字的,事物X的写操作吧A改为“list”,由本段的优化策略可知,事物Y的写A极有可能发生在读A之前。如果事物Y把名字改成了“set”,事物X的读操作就会很疑惑:名字对不上了?这就是幻读。对应的隔离级别是:Read committed
现在我们再考虑这样事务:
事物X:读A,读A。
事务Y:写A。
本段描述的优化策略下,事务X的执行是不会排斥事务Y的执行的,如果事务X执行到一半,事务Y执行(此时会加写锁),然后事务X继续执行后面一个读A,就会发现两次得到的结果并不一样。这就是不可重复读。
对应的隔离级别是Read committed
幻读、不可重复读都是因为让读的同时可以写引起的。从隔离级别上和原因上考虑,他们是同一个问题。为什么要把一样的事情定义成两个隔离级别我目前未想明白。也有很多人说这是两个不同的隔离级别
http://blog.csdn.net/fg2006/article/details/6937413
甚至有人给出了幻读和不可重复读的区别
http://blog.csdn.net/stu_hsj/article/details/46603681
如果他们说的对,MySQL是如何做到可以幻读的情况下避免不可重复读的?
此坑我实在是填不上了!或许从读写锁这方面考虑的思路不对。引入更新锁之后我彻底懵逼。
4阶优化方案。上面那种优化方案可以让读的时候加入写操作,然而写操作一旦加入还是会锁住数据。这并不是真正的读写并行,所以本段描述的优化策略是为了让写操作与读操作完全不冲突,此时的操作是这样的:
数据库中只剩下写与写的操作是串行的,读操作在任何时候都可以执行。这种优化的代价是带来了很多问题。考虑这种情况:
事务X:读A,读A。事务Y:写A、写A、写A
如果写的同时可以读,就很可能导致读到初始化了一半的数据。一旦这种情况发生,读到的数据肯定是很诡异的。这种策略对应的隔离级别是④Read uncommitted (读未提交):最低级别,任何情况都无法保证。这种隔离级别不该在开发中被使用。
5阶优化方案SQL92标准定义了以上的隔离级别和ACID的概念。然而工程师们随后想到了一种新的隔离级别用来优化事务:快照隔离级别。既可以保证读写并行,又可以保证不会读到未提交的数据(脏读)。
想象一下一个写操作。先设定一下这个写操作周围的2个点:
1. 写操作开始之前的那一个时间点
2. 写操作结束之后的那一个时间点
如果让读发生在2之后(也就是3阶优化,写的过程中不能读),那就叫串行。如果让读发生在1~2之间(也就是4阶优化),读出的数据很可能是有问题的。
快照隔离级别就是让读发生在1之前。原理是:数据库在执行事务之前会先保存一份数据以备回滚,这份数据就叫快照。直接让读操作读这个快照数据。由上述可知,这样既可以让写的同时读,又可以避免脏读。但是我认为这个实现方案会在某些场景下无比的蠢。因为本质上读到的是一个即将过时的数据。
上述描述中可知,在SQL92标准中并没有定义这种策略的隔离级别。于是工程师们只能强行把快照归类到Serializable 上面去了。大概是考虑到Serializable 由于性能问题而不可用,是一个名存实亡的隔离级别,所以快照占了这个坑。