发布时间:2023-04-11 11:30
本文主要就自己的理解讲述MySQL是如何实现事务的隔离的
众所周知,事务即一组行为,这一组行为处理了业务上的一个完整的逻辑链路。比如转账,检查A的余额,扣钱,更新B的余额,这三步要么一起成功要么都失败(回滚)。
事务具有四大特性,ACID,原子性,一致性,隔离性,持久性。
1.原子性用回滚来保证,通过undo log记录了一个隐藏字段DATA_ROLL_PTR指向undo log中旧版本的数据来进行数据回滚,如果事务失败则rollback,这里不过多解释
2.一致性其实是通过另外三个特性来满足的。
3.隔离性通过设置四个隔离级别按照业务场景选择自己需要的隔离级别,下面会详细解释。
4.持久性通过redo log,在commit前先写入redo log,确保commit成功,这时候就算断电也能在重启后完成数据的持久化。
事务为啥要隔离?并发环境下操作数据库,不同的会话如果同时读还好,同时写,或者同时读写呢?隔离确保事务之间不会相互影响,让业务逻辑正确的走下去。并发环境会出现以下4种单线程不会出现的问题:
1.sessionA还没commit,别的session去读到未commit的数据,这时候A回滚,那其他的会话读取到的数据岂不是有问题?这就叫脏读。比如:财务给张三发了1000元的工资,然后张三查询自己的账户,果然多了1000元,结果财务操作过程有误,事务回滚。当张三再查账户时,却发现账户没钱了以为被盗了,这就有大问题。
2.那么我们只读提交了事务的数据就行了,但是如果我比较慢,我先读取数据x的值,然后进行处理,处理期间,别的session修改了x的值并且commit了,那么我在一次事务期间前后读取的值就是不一样的,这就叫不可重复读。比如:张三去买新手机,要1000元,扣款前看了一眼账户上有1000元,扣钱期间,他老婆花了500买了新衣服,这时候扣钱就失败。
3.确保一次事务期间读取的数据一致后,先读取x=5的数据有一条,这时候别的事务提交一条insert插入了一条新的x=5的数据,这种条件范围数量发生变化被称为幻读。比如:老板对张三这个名字不爽,决定扣公司里所有叫张三的人的工资1000元,人事根据名字拿到工资数据一起扣钱,这期间又入职了个叫张三的,扣完后发现怎么还有个张三没扣钱,跟幻觉一样。
4.更新丢失就是两个事务在并发下同时进行更新,后一个事务的更新覆盖了前一个事务更新的情况。由于目前互联网项目大多是分布式项目,正常来说会先通过redis加上分布式锁防止同时操作,到了数据层MySQL是通过行锁,间隙锁,next-key lock来处理并发写。
下面来说数据库提供的隔离级别,并发程度由高到低,安全性由低到高:
读未提交(READ UNCOMMITTED),字面意思,啥也没限制,几种情况都可能出现
读提交 (READ COMMITTED),限制事务不会读取到还没提交的事务修改的数据,解决脏读
可重复读 (REPEATABLE READ),其他事务修改了数据,不会影响一开始读取的数据,解决脏读、不可重复读
串行化 (SERIALIZABLE),串行,事务排着队一个个来,解决脏读、不可重复读、幻读
MySQL默认的隔离级别是可重复读,而目前其他如Oracle等数据库默认采用读提交为默认隔离级别。是因为MySQL在5.0之前的版本中有bug,是由binlog导致的主从不一致问题。这里后面再讲。下面来讲讲隔离级别实现的原理:
性能最好,并发性最高,安全性最低,其实就是没有做隔离,人人都能直接改数据。
性能最差,并发性最低,安全性最高,其实就是读的时候加共享锁(读锁),也就是其他事务可以并发读,但是不能写。写的时候加排它锁(写锁),其他事务不能并发写也不能并发读。
这俩可以放到一起说。MVCC(多版本并发控制)是MySQL的一个很重要的概念,实现的方式是快照和undo log,每行数据会有三个隐藏字段,分别是DB_ROW_ID(如果没有指定主键列会帮你加上这个作为主键),DB_TRX_ID使其产生的事务的 id,事务 ID 记为 transaction id,它在事务开始的时候向事务系统申请,按时间先后顺序递增。DB_ROLL_PTR向当前记录项的undo log信息。
按照上面这张图理解,一行记录现在有 3 个版本,每一个版本都记录这使其产生的事务 ID,比如事务A的transaction id 是100,那么版本1的row trx_id 就是 100,同理版本2和版本3。
在上面介绍读提交和可重复读的时候都提到了一个词,叫做快照,学名叫做一致性视图,这也是可重复读和不可重复读的关键,可重复读是在事务开始的时候生成一个当前事务全局性的快照,而读提交则是每次执行语句的时候都重新生成一次快照。
对于一个快照来说,它能够读到那些版本数据,要遵循以下规则:
利用上面的规则,再返回去套用到读提交和可重复读的那两张图上就很清晰了。还是要强调,两者主要的区别就是在快照的创建上,可重复读仅在事务开始时创建一次,而读提交每次执行语句的时候都要重新创建一次。
MySQL通过next-key lock,锁住当前行以及它的左右的两个区间(行锁和间隙锁的组合),来防止对某一条件的数据进行修改时插入或删除了符合条件的数据,解决了幻读的问题。
比如事务A更新所有的id=10的数据的时候,由于条件 where age = 10 ,数据库不仅在 age =10 的行上添加了行锁,而且在这条记录的两边,也就是(负无穷,10]、(10,30]这两个区间加了间隙锁,这个区间是根据索引这颗b+tree来构建的,从而导致其他插入操作无法完成,只能等待事务A提交。不仅插入 age = 10 的记录需要等待事务A提交,age<10、10 这是有索引的情况,如果 age 不是索引列,那么数据库会为整个表加上间隙锁。所以,如果是没有索引的话,不管 age 是否大于等于30,都要等待事务A提交才可以成功插入。