likes
comments
collection
share

MySQL数据库事务的原理浅析

作者站长头像
站长
· 阅读数 14

1.什么是事务

事务是数据库操作的最小工作单元,是作为单个逻辑工作单元执行的一系列操作;

事务有四个特性:

-- 原子性:事务中更新的数据要么全都更新成功,要么全部回滚;

-- 一致性:事务执行的结果必须从一个一致性的状态到另外一个一致性的状态,当数据库发生故障时,也要保障一个事务中的操作的一致性;

-- 隔离性:一个事务的执行不能被其他事务干扰;

-- 持久性:一旦事务提交了,那这个修改对数据库的改变应该是永久性的,不会因为数据库发生故障而对它有影响;

2.更新一条数据,MySQL事务的一个完整过程

MySQL数据库事务的原理浅析

步骤一:由于磁盘的随机读写性能较差,在对数据进行更新之前,需要将数据所在的数据页(数据页是数据库处理的最小单位)加载到内存的Buffer Pool中,内存对数据处理的性能远远高于磁盘的随机读写;

步骤二:每条数据都有一个影藏字段trx_id,每次在更新数据之前会将数据中的trx_id更新为当前trx_id;

步骤三:每次在更新前,为了能在需要回滚的时候,能正确的回滚数据,会生成一条undo日志,把回滚时所需的东西记录下来,比如update一条数据,会把修改这条记录前的旧值记录下来,这样回滚就再把这条记录的值更新为旧值;每条数据还有一个影藏字段roll_pointer,会指向更新产生的undo日志;

步骤四:像Buffer Pool一样,内存中有一个空间叫Redo Log Buffer,可以配置这个空间的大小,会将修改数据产生的redo日志先写到内存的Redo Log Buffer中;

步骤五:根据参数innodb_flush_log_at_trx_commit的值,判断是否需要在提交事务时,将内存中的redo日志刷到磁盘的redo日志文件中,只有参数值为1时,会在事务完成前,将内存中的redo日志刷到磁盘的redo日志文件中,这时候即使还没有将改动的数据页刷到磁盘中,也不需要担心数据库挂了,导致数据改动丢失了,因为数据库在重启后,会从磁盘的redo日志中读取对应的记录将数据的改动更新到数据库的磁盘文件中;

3.多个事务并发读写同一条数据会导致的问题

1)脏读

-- 定义:读到其他事务还未提交的更新;

-- 例子:初始id=1这条数据中name=0,事务A将id=1的这条数据的name更新为A,还未提交,然后事务B读取了id=1的name为A,此时事务A抛出错误或者网络问题进行了回滚,将id=1的name的值回滚为0,此时事务B读到的数据就是脏数据,这就是脏读;

2)脏写

-- 定义:一个事务在其他事务修改了还未提交的数据上进行了修改;

-- 例子:初始id=1这条数据中name=0,事务A将id=1这条数据的name更新为A,事务B又将id=1这条数据的name更新为B,此时事务A进行回滚,会将id=1这条数据的name回滚为0,导致更新为B的操作被丢失了,这就是脏写;

3)不可重复读

-- 定义:一个事务在不同阶段读取到的同一条数据的值不同;

-- 例子:初始id=1这条数据中name=0,事务A读取id=1这条数据的name为0,然后事务B将id=0这条数据的name改成了B,此时事务A又读取了一把id=1这条数据,此时读到的name为B,跟上一次读到的不一样,这就是不可重复读;

4)幻读

-- 定义:一个事务用一样的sql,每次都查到之前没查到的数据;

-- 例子:事务A根据SELECT NAME FROM USER WHERE ID < 5查到了三条数据,事务B新增了一条数据id=4,并提交了事务,然后事务A又查询了SELECT NAME FROM USER WHERE ID < 5这条SQL,此时查到了四条数据,此时就产生了幻读;

4.MySQL数据库事务如何解决上述的问题

1)解决脏写

当事务12要更新一条数据时,需要先查看这条数据有没有其他事务加锁,锁的数据结构是trx_id(事务id)+等待状态,如果没有加锁,则给这条加一个锁(12+false),如果有其他事务也给这个数据加了锁,则事务12也会给这个数据再加个锁(12+true),表示自己正在等待,当其他事务执行完成之后释放锁,会看是否有其他事务正在等待,有的话,则将他的锁的等待状态改成false,并唤醒这个事务,通过这个流程,就不会有两个事务同时更新一条数据导致脏写;

2)MVCC机制

由于脏读,可重复读,幻读都需要用到MVCC机制,因此先介绍MVCC机制,MVCC机制是基于undo log版本链ReadView实现的;

-- undo log版本链:每条数据都有两个影藏字段,一个是trx_id:当前修改这条数据的事务id,一个是roll_pointer:指向的是上一个修改这条数据的undo log(更新数据的回滚日志),比如,当一条数据被是事务11新增,值为A,这时候这条数据上的trx_id是11,而roll_pointer指向的是个空的,因为这条数据没有上个事务,然后这条数据又被事务12修改了,值为B,此时会将这条数据上的trx_id改成12,并且在修改值之前生成一个undo log,此时roll_pointer就会指向的这个undo log,如下图所示,就形成了undo log版本链:

MySQL数据库事务的原理浅析

-- ReadView:ReadView中有四个字段,m_ids:此时还有哪些事务没有提交,min_trx_id:m_ids中最小的trx_id,max_trx_id:下一个要生成的trx_id(也就是当前还未存在的trx_id),create_trx_id:当前的trx_id;下面将用一个例子介绍一下ReadView的使用:

1)有一条数据是早被其他事务添加的,trx_id为11,值为A;

2)此时有两个事务并发过来,事务21想读取这条数据,事务31想修改这条数据,事务21会开启一个ReadView,m_ids是[21,31],min_trx_id为21,max_trx_id为33(假设还有个trx_id=32的事务已经提交了),create_trx_id为21;

3)事务21查询这条数据时,首先判断这条数据目前的trx_id(11)是否比min_trx_id(21)小,可以看到,11是必21小的,可以判断这条数据是早就已经提交的,因此可以查询这个值;

4)事务31修改这条数据,将这条数据的trx_id改成31,并在修改前生成undo log日志,将roll_pointer指向这个undo log日志;

5)事务21这时又来查询这条数据,发现此时这条数据的trx_id变成31了,是比min_trx_id(21)要大,且31是在m_ids里面,可以知道这个事务是并发执行的,因此不能读这个值,而要顺这个undo log版本链往下找,可以找到11,是比min_trx_id(21)要小,因此就查询这个值;

MySQL数据库事务的原理浅析

3)四种隔离级别

-- READ UNCOMMITED:可读到未提交的事务更新的数据,这种隔离方式仅能解决脏写,脏写是通过加锁解决的,每种隔离级别都有加锁机制;

-- READ COMMITED:只能读到已经提交的事务更新的数据,这种隔离方式能解决脏写,脏读;

-- REPEATABLE READ:一个事务中,多次查询同一个数据的值永远是一致的,这种隔离方式能解决脏写,脏读,幻读;

-- SERIALIZABLE:所有事务都串行执行,只有上一个事务执行完成,才能执行下一个事务,这种隔离级别性能很差,一般不推荐使用,因此所有的事务都是串行的,这种隔离级别可以解决所有的问题;

上面描述的四种隔离级别能解决的问题,都是MySQL对这四种隔离级别的支持,而SQL标准规定的和这个有一点不一样,比如SQL标准的REPEATABLE READ隔离级别不能解决幻读的问题;

4)READ COMMITED如何通过ReadView实现

READ COMMITED隔离级别的事务,会在每次查询前,开启一个ReadView,在这次查询中只能查到开启ReadView之前已经提交事务更新的数据,因此能解决脏读的问题,如果这个事务再查询一次这个数据,会重新开启一个ReadView,如果在这之前提交的事务更新的数据,这次查询还是可以读到,比如在这次开启前,上一次开启后,新增了一条数据,那这次是能读到这条数据的,因此没法解决幻读的问题;

5)REPEATABLE READ如何通过ReadView实现

REPEATABLE READ隔离级别的事务,只会在第一次查询时,开启一个ReadView,后面的查询都是用这一个ReadView,能保证每次读到的数据都是一致的,因此可以解决幻读的问题;