
innodb 为了提高磁盘I/O读写性能,存在一个 buffer pool 的内存空间,数据页读入会缓存到 buffer pool,事务的提交则实时更新到 buffer pool,而不实时同步到磁盘(innodb 是按 16KB 一页同步的,一事务可涉及多个数据页,实时同步会造成浪费,随机I/O)。事务暂存在内存,则存在一致性问题,为了解决系统崩溃,保证事务的持久性,我们只需把事务对应的 redo 日志持久化到磁盘即可(redo 日志占用空间小,顺序写入磁盘,顺序I/O)
sql 语句在执行的时候,可能会修改多个页面,还会更新聚簇索引和二级索引的页面,过程产生的redo会被分割成多个不可分割的组(Mini-Transaction)。MTR怎么理解呢?如一条 insert 语句可能会使得页分裂,新建叶子节点,原先页的数据需要复制到新数据页里,然后将新记录插入,再添加一个目录项指向新建的页子。这对应多条 redo 日志,它们需要在原子性的 MTR 内完成
MTR 产生的 redo 日志先会被复制到一个 log buffer 里(类似 buffer pool)。而同步到磁盘的时机如下:
事务需要保证原子性,也是说事务中的 *** 作要么全部完成,要么什么也不做。如果事务执行到一半,出错了怎么办-回滚。但是怎么回滚呢,靠 undo 日志。undo 日志就是我们执行sql的逆 *** 作
binlog有三种格式:Statement、Row以及Mixed。
redolog 中的事务如果经历了二阶段提交中的prepare阶段,则会打上 prepare 标识,如果经历commit阶段,则会打上commit标识(此时redolog和binlog均已落盘)。崩溃恢复逻辑如下:
今天我们来看看多个事务对缓存页里的同一条数据同时进行更新或者查询,此时会产生哪些问题?这里实际会涉及到 脏写、脏读、不可重复读、幻读, 四中问题。
这个脏写的话,它的意思是说有两个事务,事务A和事务B同时在更新一条数据,事务A先把它更新为A值,事务B紧接着就把它更新为B值。事务A是先更新的,它在更新之前,这行数据的值为NULL,所以此时事务A的undo log日志大概是这样的:更新之前这行数据的值为NULL,主键为XX 。
那么此时事务B更新完了数据的值为B,结果此时事务A突然回滚了,那么就会用它的undo log日志去回滚。此时事务A一回滚,直接就会把那行数据的值更新回之前的NULL值。所以对于事务B看到的场景,就是自己明明更新了,结果值却没了,这就是 脏写。
假设事务A更新了一行数据的值为A,此时事务B去查询了一些这行数据的值,看到的值是A,然后事务B拿着刚查询到的A值去处理各种业务。但是此时不幸的事情发生了,事务A突然回滚了,导致它刚才更新的A值没了,此时那行数据的值回滚为NULL值。这就是所谓的 脏读。 它的本质是事务B去查询了事务A修改过的数据,但是此时事务A还没有提交,事务A随时会回滚导致事务B查询了一个不存在的值。
接着我们来看一下 的问题,假设我们有一个事务A开启了,在这个事务A里会多次对一条数据进行查询。然后另外有两个事务,一个是事务B,一个是事务C,它们都是对一条数据进行更新的。假设缓存页里一条数据原来的值是A值,此时事务A开启之后,第一次查询这条数据,读取到的是A值。接着事务B更新了那行数据的值为B,同时提交事务,然后事务A第二次查询该行数据,此时查到的是事务B修改过的值B 。接着事务C更新了那行数据的值为C,同时提交事务,然后事务A第三次查询该行数据,此时查到的是事务C修改过的值C值。
那么上面的场景有什么问题呢?其实要说没问题也是可以的,毕竟事务B和C都提交事务了。但是要说有问题也是可以的,就是事务A可能第一次查询到的是A值,那么它可能希望的是在事务执行期间,如果多次查询数据,都是同样的一个A值。但是该场景下,A值明显不是可重复读的。
这种情况算不算一个问题呢?其实这是根据你的业务决定的。有的业务要的是可重复读,而有的业务却需要不可重复读。
假设一个事务A先发送一条SQL语句,里面有一个条件,要查询一批数据出来,比如“select * from table where id >10”,类似这种SQL,它一开始查询出了10条数据。然后事务B往表里插入了几条数据,而且事务B还提交了。此时事务A再次查询,由于事务B插入了几条数据,导致这次它查询出来了12条数据。同样的SQL语句,两次的查询结果却不一样,所以开始怀疑自己是不是出现了幻觉?导致刚才幻读了?这就是幻读一词的由来。
在SQL标准中规定了4种事务隔离级别,就是说多个事务并发运行的时候,互相是如何隔离的,从而避免一些事务并发问题。这4种级别包括了: read uncommitted(读未提交)、read committed(读已提交)、repeatable read(可重复读)、serializable(串行化) 。
第一个read uncommitted隔离级别是不允许发生脏写的。也就是说,不可能两个事务在没提交的情况下去更新同一行数据的值,但是在这种隔离级别下,可能发生脏读、不可重复读、幻读。所以一般来说,是没有人做系统开发的时候把事务隔离级别设置为读未提交这个级别的。
第二个是read committed隔离级别,也就是俗称的RC级别,这个级别不会发生脏写和脏读。也就是说,别的事务没提交的情况下修改的值,你是绝对读不到的。但是,可能会发生不可重复读和幻读问题。
第三个是repeatable read隔离级别,也就是俗称的RR级别,就是可重复读级别。这个级别下,不会发生脏写、脏读、不可重复读的问题。事务一旦开启,多次查询一个值,会一直读到同一个值。但是它会发生幻读的问题。
最后一个隔离级别,就是serializable级别,这种级别,根本不允许多个事务并发执行,只能串行执行,所以不可能有幻读问题。但是这种级别一般除非脑子坏了,否则不可能设置这种级别。
MySQL默认设置的事务隔离级别都是RR级别的,而且MySQL的RR级别是可以避免幻读发生的。
下面的命令可以修改MySQL的默认事务隔离级别:
另外,给大家一个彩蛋,假设你在开发业务系统的时候,比如用spring里的@Transaction注解来做事务这块,假设某个事务你就是有点手痒,想搞成RC级别,那么没问题,在@Transaction注解里是有一个isolation参数的,里面是可以设置事务隔离级别的,具体的设置方式如下:
@Transaction(isolation=Isolation.DEFAULT),默认的就是DEFAULT值,这个就是MySQL默认支持什么隔离就是什么隔离级别。但是你可以手动改成其它的隔离级别,比如,isolation = Isolation.READ_COMMITTED级别,此时你就可以读取到其它事务已提交的数据。
简单来说,我们每条数据其实都有两个隐藏字段,一个是trx_id,一个是roll_pointer,这个trx_id就是最近一次更新这条数据的事务id,roll_pointer就是指向了你更新这个事务之前生成的undo log,关于undo log之前都讲过了。
举个例子,假设有一个事务A(id=50),插入了一条数据,那么此时这条数据的隐藏字段以及指向的undo log如下图所示:
插入的这条数据的值是A,因为事务A的id是50,所以这条数据的trx_id就是50,roll_pointer指向一个空的undo log,因为之前这条数据是没有的。接着有一个事务B修改了一下这条数据,把值改成了B,事务B的id是58,那么此时更新之前会生成一个undo log记录之前的值,然后会让roll_pointer指向这个实际的undo log回滚日志,如下图所示:
欢迎分享,转载请注明来源:内存溢出
微信扫一扫
支付宝扫一扫
评论列表(0条)