事务是mysql Innodb引擎的一大特点,可以说,在日常开发中,对于mysql事务的使用无处不在,因此深入了解并掌握mysql的事务原理很有必要。
比如 : 张三给李四转账1000块钱,张三银行账户减少1000元,而李四银行账户的钱要增加1000元。 这一组操作就必须在一个事务的范围内,要么都成功,要么都失败
是事务的四大特性,简称ACID
下面来模拟一个事务操作,准备如下一张表,并初始化两条数据,来模拟转账的事务操作;
1) 测试一个正常的操作
-- 1. 查询张三余额
select * from account where name = '张三';
-- 2. 张三余额减1000
update account set money = money - 1000 where name = '张三';
-- 3. 李四余额加1000
update account set money = money + 1000 where name = '李四';
执行完成后,可以看到效果是预期的;
2)测试异常情况
-- 1. 查询张三余额
select * from account where name = '张三';
-- 2. 张三余额减1000
update account set money = money - 1000 where name = '张三';
1/0
-- 3. 李四余额加1000
update account set money = money + 1000 where name = '李四';
由于这个语句中出现了一个不符合sql语法的错误,执行到1/0的时候报错,导致第三步无法正常执行,最终的结果如下,即张三扣减了1000,但是李四并没有加1000,即数据在操作前后不一致了;
基于上面产生的异常情况,在实际开发过程中,假如是运行在程序中的,为了避免出现这样的问题,就需要通过事务来进行控制;
在操作之前,我们需要了解下面两个命令
1、查看当前的事务提交方式
SELECT @@autocommit ;
“1”表示自动提交,即在默认情况下,事务是自动提交的,为了模拟事务的效果,我们需要修改下这个事务的自动提交方式;
2、设置事务提交方式
SET @@autocommit = 0 ;
即将提交方式设置为手动提交
1) 测试正常操作
-- 开启事务
start transaction
-- 1. 查询张三余额
select * from account where name = '张三';
-- 2. 张三余额减1000
update account set money = money - 1000 where name = '张三';
-- 3. 李四余额加1000
update account set money = money + 1000 where name = '李四';
-- 如果正常执行完毕, 则提交事务
commit;
注意,在未走到 commit之前,上面的更新操作不会被写入到表中,只有执行commit,才算结束,观察数据,发现满足预期的效果;
2)测试异常操作
在上面没有添加事务操作时,走到 1/0 的时候发现数据最终异常了,这时候我们添加上事务的操作,看看效果如何;
首先执行下面一系列操作
start transaction
select * from account where name = '张三';
update account set money = money - 1000 where name = '张三';
1/0;
update account set money = money + 1000 where name = '李四';
这时候,由于有 1/0 的存在,导致执行错误,但是这个时候由于我们开启了手动提交事务,在这种出现了异常的情况下,可以通过执行rollback,执行完成之后,即便发生异常,数据仍然恢复到操作之前的一致状态;
-- 如果执行过程中报错, 则回滚事务
rollback;
在真实的业务场景中,并发操作在大部分情况下,最终将归为对数据库表的并发操作,并发需要解决的问题也就是mysql事务并发需要解决的问题,一般来说,数据库的事务并发带来的影响也是不同的,常见的问题主要分为下面几种;
上面介绍了几种并发事务执行过程中可能遇到的问题,这些问题有轻重缓急之分,我们给这些问题按照严重性来排一下序:
脏写 > 脏读 > 不可重复读 > 幻读
一般来说,我们愿意舍弃一部分隔离性来换取一部分性能,可以结合业务的实际情况,设立不同的隔离级别,当然隔离级别越低,并发问题发生的就越多。常用的隔离级别总结如下:
针对不同的隔离级别,并发事务可以发生不同严重程度的问题,具体情况如下:
1、查看事务隔离级别
SELECT @@TRANSACTION_ISOLATION;
mysql默认事务隔离级别:REPEATABLE-READ(可重复读)
2、设置事务隔离级别
可以通过下面的语句来手动设置事务的隔离级别
SET [ SESSION | GLOBAL ] TRANSACTION ISOLATION LEVEL { READ UNCOMMITTED | READ COMMITTED | REPEATABLE READ | SERIALIZABLE }
注意:事务隔离级别越高,数据越安全,但是性能越低
1、读未提交
这种隔离级别最低,即A事务可以读取到另一个事务未提交的数据,仍然使用上面的accounbt表,开启两个命令行操作窗口;
将当前左边的事务会话隔离级别设置为读未提交
然后,在两个session会话窗口分别开启事务,这时候,在右边的窗口执行更新操作,再次在左边的窗口查询右边窗口更新的这条数据,发现竟然读到了右边窗口未提交的数据,这就是读未提交的效果(脏读);
2、读已提交
在读未提交,产生了脏读问题,那么使用读已提交这个隔离级别就可以解决这个问题
设置左边的隔离级别为读已提交
set session TRANSACTION ISOLATION LEVEL read COMMITTED;
将表的数据复原后再次重复上面的操作过程,通过结果展示可以发现,在这种隔离级别下,脏读的问题就解决了;
剩下的其他两种操作,有兴趣的同学可以按照同样的方式来操作下,要注意的是各自解决的问题点是什么即可;
在springboot项目中,通常不需要大家手动去配置事务管理器,这是spring框架在启动的时候,默认会启用jdbc的事务管理器,只需要在使用事务的方法上面,去配置相关的注解即可;
在spring的spring.factories配置文件中,提供了一个默认的 DataSourceTransactionManagerAutoConfiguration 事务管理器,在spring容器初始化的时候,会将这个默认的事务管理器加载到容器中;
通过上面的讲解,我们了解到mysql的事务有4种特性:原子性、一致性、隔离性和持久性。那么事务的四种特性是基于什么机制实现呢?
重做日志 ,提供再写入操作,恢复提交事务修改的页操作,用来保证事务的持久性
该日志文件由两部分组成:重做日志缓冲(redo log buffer)及重做日志文件(redo logfile),前者是在内存中,后者在磁盘中。当事务提交之后会把所有修改信息都存到该日志文件中, 用于在刷新脏页到磁盘,发生错误时, 进行数据恢复使用;
为什么需要REDO日志
假如刷写脏页的数据到磁盘过程中出错了,而提示给用户的却是事务提交成功,这样一来,数据就没有持久化到磁盘,这就有问题了,即没有保证事务的持久性,大致的流程如下:
如何解决上面的问题呢?这就要用到 redo log了,在InnoDB中提供了一份日志 redo log;
为什么每次提交事务,要刷新redo log 到磁盘中呢,而不是直接将buffer pool中的脏页刷新到磁盘呢 ?
因为在业务操作中,我们操作数据一般都是随机读写磁盘的,而不是顺序读写磁盘。 而redo log在往磁盘文件中写入数据,由于是日志文件,所以是顺序写的。顺序写的效率,要远大于随机写。 这种先写日志的方式,称之为 WAL(Write-Ahead Logging)。
Redo log可简单分为以下两部分:
参数设置:innodb_log_buffer_size,redo log buffer 大小,默认 16M ,最大值是4096M,最小值为1M。可以通过命令行进行查看,
show variables like ‘%innodb_log_buffer_size%’;
最后总结下,以一个更新事务为例,redo log 流转过程,如下图所示:
回滚日志 ,回滚行记录到某个特定版本,用来保证事务的原子性、一致性
其实在使用分布式事务框架的时候,其底层实现原理也是借助了 undo log的思想,记录了反向操作的sql语句,以便于事务回滚时使用;
undo 的类型
在InnoDB存储引擎中,undo log分为:
undo log 生成过程
在innodb 中,表的数据行记录结构如下所示,可以理解这是一个数据行完整的逻辑存储结构;
当我们执行一个insert操作,即给表中添加一条记录时,比如下面这条语句:
INSERT INTO user (name) VALUES ("tom");
其实对应的undo log中将会反向生成一条delete的记录
同样,当执行 update语句的时候,将会记录本次更新数据之前的相关列字段信息,有了这样的认识后,我们来总结下,当发生回滚的时候,undo log是如何进行的: