mysql的事务有以下特征:
atomicity 是说事务是一个原子操作,在一个事务里面的操作要么一起成功,要么一起失败
consistent 是说事务的前后,数据保持一致的状态,这个有点像能量守恒,也就是说能量不会无缘无故减少,如果减少一定会在某个地方增多。打个比喻 张三和李四账户里面都有 1000 元,总额就是2000元,张三转账给李四 500 元,那么张三账户里面是 500 元,李四账户里面是 1500 元,总额是不变的还是 2000 元。
isolation 是指数据库允许多个并发事务同时对数据进行操作,隔离性保证各个事务相互独立,事务处理时的中间状态对其它事务是不可见的,以此防止出现数据不一致状态。隔离性是这四个特性之中最不好理解的,因为隔离级别的不同会出现各种bug。在这篇文章后面会详细说明。
durability 是指事务一旦提交,对数据库的修改是永久的,即使系统故障也不会丢失。
介绍隔离性的时候就介绍了数据库允许多个并发事务同时对数据进行操作,就像多线程一样,加锁的力度大小不同,可能会产生不同的问题,多个事务同时处理也一样会产生问题,这里首先介绍一下,由于隔离性不同可能产生的问题
脏读
一个事务正在对一行记录做修改,还未提交之前另外一个事务读取了这行还未提交的数据。如图所示:
这里会话1得到的age是10,但是如果会话2在更新完之后又回滚了,那么会话一拿到的就是脏数据,数据库里面不存在这条数据。
会话一读取这行记录的时间是会话而修改之后提交之前
幻读
所谓幻读,指的是当某个事务在读取某个范围内的记录时,另外一个事务又在该范围内插入了新的记录,当之前的事务再次读取该范围的记录时,会产生幻行。InnoDB存储引擎通过行锁加间隙锁解决了幻读的问题。
不可重复读
不可重复读是一个事务读取同一条记录两次得到的结果不一样,和幻读差不多也是两次读取中间,有其他事务修改了这条件记录并提交了。
和脏读不一样的是这里的会话2的记录是已经提交了的,而且会话A读取这行记录的时间分别是会话2提交之前和提交之后
和幻读不一样的地方是,不可重复读发生在update和delete操作中,而幻读发生在insert操作中
要解决以上的三个问题,就要设置不同的隔离界别,隔离级别越高,出现的问题越少
mysql提供了四种隔离级别
隔离级别从高到低排序为:可串行化 > 可重复读 > 读已提交 > 读未提交
隔离级别越高,出现的问题越少,但是对性能的影响越大。MYSQL 默认的隔离级别是 可重复读
从字面上意思知道,事务A可以读取到事务B修改过但未提交的数据,隔离级别最小。可能发生 脏读,幻读,不可重复读
读已提交,不允许事务B读取事务A已修改但未提交的记录,事务B只能读取事务A已经提交了的记录。可能出现 幻读,不可重复读
读已提交的效果如下
这里每个select 语句都有自己的一份快照,而不是一个事务共享一份,所以在不同的时刻,查询出来的数据可能是不一致的。
事务A在事务过程中看到的数据和开启事务时的数据是一样的,可以解决 脏读和不可重复读,可能出现 幻读,这个隔离级别也是MYSQL默认的隔离级别
可重复读的效果如下
上面的图之所以在事务A提交了,事务B的读取结果还是和上次一样是通过快照实现的。但是可重复读只对已有行的更改操作有效,对于新插入的数据无效,也就是说可能会产生 幻读
最高隔离级别,所有的事务串行执行,不能并发执行,不会出现问题,但是性能太差一般不用。
不应该把隔离级别设置的太高,交给应用程序根据使用场景使用锁去实现不同的需求
不同的隔离级别会导致不同的问,总结如下
排它锁(Exclusive),又称为X 锁,写锁。
共享锁(Shared),又称为S 锁,读锁。
读写锁之间有以下的关系:
即读写锁之间的关系可以概括为:多读单写
读未提交不加锁,也就是没有隔离
读的时候是共享锁,写的时候是排它锁,也就是说事务A在写的时候,其他的事务不能读和写。只要有事务再进行写操作,其他事务就要阻塞,导致性能很差
MySQL 采用了 MVVC (多版本并发控制) 的方式实现可重复读。
我们在数据库表中看到的一行记录可能实际上有多个版本,每个版本的记录除了有数据本身外,还要有一个表示版本的字段,记为 row trx_id,而这个字段就是使其产生的事务的 id,事务 ID 记为 transaction id,它在事务开始的时候向事务系统申请,按时间先后顺序递增。
按照上面这张图理解,一行记录现在有 3 个版本,每一个版本都记录这使其产生的事务 ID,比如事务A的transaction id 是100,那么版本1的row trx_id 就是 100,同理版本2和版本3。
在上面介绍读提交和可重复读的时候都提到了一个词,叫做快照,学名叫做一致性视图,这也是可重复读和不可重复读的关键,可重复读是在事务开始的时候生成一个当前事务全局性的快照,而读提交则是每次执行语句的时候都重新生成一次快照。
对于一个快照来说,它能够读到那些版本数据,要遵循以下规则:
利用上面的规则,再返回去套用到读提交和可重复读的那两张图上就很清晰了。还是要强调,两者主要的区别就是在快照的创建上,可重复读仅在事务开始是创建一次,而读提交每次执行语句的时候都要重新创建一次。
存在这的情况,两个事务,对同一条数据做修改。最后结果应该是哪个事务的结果呢,肯定要是时间靠后的那个。重要的是更新之前要先读数据,这里所说的读和上面说到的读不一样,更新之前的读叫做“当前读”,总是当前版本的数据,也就是多版本中最新一次提交的那版。
假设事务A执行 update 操作, update 的时候要对所修改的行加行锁,这个行锁会在提交之后才释放。而在事务A提交之前,事务B也想 update 这行数据,于是申请行锁,但是由于已经被事务A占有,事务B是申请不到的,此时,事务B就会一直处于等待状态,直到事务A提交,事务B才能继续执行,如果事务A的时间太长,那么事务B很有可能出现超时异常。如下图所示。
加锁的过程要分有索引和无索引两种情况,比如下面这条语句
update user set age=11 where id = 1
id 是这张表的主键,是有索引的情况,那么 MySQL 直接就在索引数中找到了这行数据,然后干净利落的加上行锁就可以了。
而下面这条语句
update user set age=11 where age=10
表中并没有为 age 字段设置索引,所以, MySQL 无法直接定位到这行数据。那怎么办呢,当然也不是加表锁了。MySQL 会为这张表中所有行加行锁,没错,是所有行。但是呢,在加上行锁后,MySQL 会进行一遍过滤,发现不满足的行就释放锁,最终只留下符合条件的行。虽然最终只为符合条件的行加了锁,但是这一锁一释放的过程对性能也是影响极大的。所以,如果是大表的话,建议合理设计索引,如果真的出现这种情况,那很难保证并发度。
前面刚说了并发写问题的解决方式就是行锁,而解决幻读用的也是锁,叫做间隙锁,MySQL 把行锁和间隙锁合并在一起,解决了并发写和幻读的问题,这个锁叫做 Next-Key锁。
假设现在表中有两条记录,并且 age 字段已经添加了索引,两条记录 age 的值分别为 10 和 30。
此时,在数据库中会为索引维护一套B+树,用来快速定位行记录。B+索引树是有序的,所以会把这张表的索引分割成几个区间。
如图所示,分成了3 个区间,(负无穷,10]、(10,30]、(30,正无穷],在这3个区间是可以加间隙锁的。
之后,我用下面的两个事务演示一下加锁过程。
在事务A提交之前,事务B的插入操作只能等待,这就是间隙锁起得作用。当事务A执行update user set name='风筝2号’ where age = 10;
的时候,由于条件 where age = 10 ,数据库不仅在 age =10 的行上添加了行锁,而且在这条记录的两边,也就是(负无穷,10]、(10,30]这两个区间加了间隙锁,从而导致事务B插入操作无法完成,只能等待事务A提交。不仅插入 age = 10 的记录需要等待事务A提交,age<10、10<age<30 的记录页无法完成,而大于等于30的记录则不受影响,这足以解决幻读问题了。
这是有索引的情况,如果 age 不是索引列,那么数据库会为整个表加上间隙锁。所以,如果是没有索引的话,不管 age 是否大于等于30,都要等待事务A提交才可以成功插入。