MySQL 事务基础概念/Definition of Transaction
事务(Transaction)是访问和更新数据库的程序执行单元;事务中可能包含一个或多个 sql 语句,这些语句要么都执行,要么都不执行。
事务处理在各种管理系统中都有着广泛的应用,比如人员管理系统,很多同步数据库操作大都需要用到事务处理。比如说,在人员管理系统中,你删除一个人员,你即需要删除人员的基本资料,也要删除和该人员相关的信息,如信箱,文章等等,这样,这些数据库操作语句就构成一个事务!
删除的SQL语句
delete from userinfo where ~~~
delete from mail where ~~
delete from article where~~
如果没有事务处理,在你删除的过程中,假设出错了,只执行了第一句,那么其后果是难以想象的!
但用事务处理。如果删除出错,你只要rollback就可以取消删除操作(其实是只要你没有commit你就没有确实的执行该删除操作)
一般来说,在商务级的应用中,都必须考虑事务处理的!
MySQL 支持事务的存储引擎有 InnoDB、NDB Cluster 等,其中 InnoDB 的使用最为广泛;其他存储引擎不支持事务,如 MyIsam、Memory 等。
事务的特征
一个最小的不可再分的工作单元;通常一个事务对应一个完整的业务(例如银行账户转账业务,该业务就是一个最小的工作单元)
一个完整的业务需要批量的DML(insert、update、delete)语句共同联合完成
事务只和DML语句有关,或者说DML语句才有事务。这个和业务逻辑有关,业务逻辑不同,DML语句的个数不同
先来明确一下事务涉及的相关知识:
事务都应该具备ACID特征
所谓ACID是Atomic(原子性)/Consistent(一致性)/Isolated(隔离性)/Durable(持续性)四个词的首字母所写。
一般来说,事务是必须满足4个条件(ACID)
原子性(Autmic):事务是最小单位,不可再分。即:组成事务处理的语句形成了一个逻辑单元,不能只执行其中的一部分。事务在执行性,要做到"要么不做,要么全做!",就是说不允许事务部分得执行。即使因为故障而使事务不能完成,在rollback时也要消除对数据库得影响!
比如:银行转帐过程中,必须同时从一个帐户减去转帐金额,并加到另一个帐户中,只改变一个帐户是不合理的。
原子性由 Undo log 保证。Undo Log 会保存每次变更之前的记录,从而在发生错误时进行回滚。
一致性(Consistency):在事务处理执行前后,数据库是一致的。事务要求所有的DML语句操作的时候,必须保证同时成功或者同时失败。事务得操作应该使使数据库从一个一致状态转变倒另一个一致得状态!
比如:网上购物,你只有即让商品出库,又让商品进入顾客得购物篮才能构成事务!
比如:银行转帐过程中,要么转帐金额从一个帐户转入另一个帐户,要么两个帐户都不变,没有其他的情况。
一致性由原子性,隔离性,持久性来保证
隔离性(Isolation):一个事务处理对另一个事务处理没有影响。即:事务A和事务B之间具有隔离性。如果多个事务并发执行,应象各个事务独立执行一样!也就是说任何事务都不可能看到一个处在不完整状态下的事务。
隔离性追求的是并发情形下事务之间互不干扰。
比如:银行转帐过程中,在转帐事务没有提交之前,另一个转帐事务只能处于等待状态。
隔离性由 MVCC(Multi-Version Concurrency Control) 和 Lock(锁机制) 保证
持久性(Durability):事务处理的效果能够被永久保存下来。反过来说,事务应当能够承受所有的失败,包括服务器、进程、通信以及媒体失败等等。一个成功执行得事务对数据库得作用是持久得,即使数据库应故障出错,也应该能够恢复!持久性是事务的保证,事务终结的标志(内存的数据持久到硬盘文件中)。
比如:银行转帐过程中,转帐后帐户的状态要能被保存下来。
持久性由 Redo Log 保证。每次真正修改数据之前,都会将记录写到 Redo Log 中,只有 Redo Log 写入成功,才会真正的写入到 B+ 树中,如果提交之前断电,就可以通过 Redo Log 恢复记录。
InnoDB存储引擎两种事务日志:
redo log(重做日志): 用于保证事务持久性
undo log(回滚日志):事务原子性和隔离性实现的基础
undo log是现原子性的关键,是当事务回滚时能够撤销所有已经成功执行的 sql 语句。
InnoDB 实现回滚,靠的是 undo log:
当事务对数据库进行修改时,InnoDB 会生成对应的 undo log。
如果事务执行失败或调用了 rollback,导致事务需要回滚,便可以利用 undo log 中的信息将数据回滚到修改之前的样子。
undo log 属于逻辑日志,它记录的是 sql 执行相关的信息。
当发生回滚时,InnoDB 会根据 undo log 的内容做与之前相反的工作:
对于每个 insert,回滚时会执行 delete。
对于每个 delete,回滚时会执行 insert。
对于每个 update,回滚时会执行一个相反的 update,把数据改回去。
以 update 操作为例:当事务执行 update 时,其生成的 undo log 中会包含被修改行的主键(以便知道修改了哪些行)、修改了哪些列、这些列在修改前后的值等信息,回滚时便可以使用这些信息将数据还原到 update 之前的状态。
redo log 存在的背景
InnoDB 作为 MySQL 的存储引擎,数据是存放在磁盘中的,但如果每次读写数据都需要磁盘 IO,效率会很低。
为此,InnoDB 提供了缓存(Buffer Pool),Buffer Pool 中包含了磁盘中部分数据页的映射,作为访问数据库的缓冲:
当从数据库读取数据时,会首先从 Buffer Pool 中读取,如果 Buffer Pool 中没有,则从磁盘读取后放入 Buffer Pool。
当向数据库写入数据时,会首先写入 Buffer Pool,Buffer Pool 中修改的数据会定期刷新到磁盘中(这一过程称为刷脏)。
Buffer Pool 的使用大大提高了读写数据的效率,但是也带来了新的问题:如果 MySQL 宕机,而此时 Buffer Pool 中修改的数据还没有刷新到磁盘,就会导致数据的丢失,事务的持久性无法保证。
于是,redo log 被引入来解决这个问题:当数据修改时,除了修改 Buffer Pool 中的数据,还会在 redo log 记录这次操作;当事务提交时,会调用 fsync 接口对 redo log 进行刷盘。
如果 MySQL 宕机,重启时可以读取 redo log 中的数据,对数据库进行恢复。
redo log 采用的是 WAL(Write-ahead logging,预写式日志),所有修改先写入日志,再更新到 Buffer Pool,保证了数据不会因 MySQL 宕机而丢失,从而满足了持久性要求。
既然 redo log 也需要在事务提交时将日志写入磁盘,为什么它比直接将 Buffer Pool 中修改的数据写入磁盘(即刷脏)要快呢?
主要有以下两方面的原因:
刷脏是随机 IO,因为每次修改的数据位置随机,但写 redo log 是追加操作,属于顺序 IO。
刷脏是以数据页(Page)为单位的,MySQL 默认页大小是 16KB,一个 Page 上一个小修改都要整页写入;而 redo log 中只包含真正需要写入的部分,无效 IO 大大减少。
redo log 与 binlog
我们知道,在 MySQL 中还存在 binlog(二进制日志)也可以记录写操作并用于数据的恢复,但二者是有着根本的不同的。
作用不同:
redo log 是用于 crash recovery 的,保证 MySQL 宕机也不会影响持久性;
binlog 是用于 point-in-time recovery 的,保证服务器可以基于时间点恢复数据,此外 binlog 还用于主从复制。
层次不同:
redo log 是 InnoDB 存储引擎实现的,
而 binlog 是 MySQL 的服务器层实现的,同时支持 InnoDB 和其他存储引擎。
内容不同:
redo log 是物理日志,内容基于磁盘的 Page。
binlog 是逻辑日志,内容是一条条 sql。
写入时机不同:
redo log 的写入时机相对多元。前面曾提到,当事务提交时会调用 fsync 对 redo log 进行刷盘;这是默认情况下的策略,修改 innodb_flush_log_at_trx_commit 参数可以改变该策略,但事务的持久性将无法保证。
除了事务提交时,还有其他刷盘时机:如 master thread 每秒刷盘一次 redo log 等,这样的好处是不一定要等到 commit 时刷盘,commit 速度大大加快。
binlog 在事务提交时写入。
事务的隔离四种级别
ACID四大特征中,最难理解的不是一致性,而是事务的隔离性,数据库权威专家针对事务的隔离性研究出来了事务的隔离四种级别,四种事务隔离级别就是为了解决数据在高并发下产生的问题(脏读、不可重复读、幻读)
并发一致性问题:
脏读:比如有两个事务并行执行操作同一条数据库记录,A事务能读取到B事务未提交的数据。比如:事务B操作了数据库但是没有提交事务,此时A读取到了B没有提交事务的数据。这就是脏读的体现。
不可重复读:比如事务A在同一事务中多次读取同一记录,此时事务B修改了事务A正在读的数据并且提交了事务,但是事务A读取到了事务B所提交的数据,导致两次读取数据不一致。
幻读:事务A将数据表中所有数据都设置为100,此时事务B插入了一条值为200的数据并提交事务,当事务A修改完成提交事务后,发现还有一条数据的值不为100.这就是幻读的一种体现方式。
"脏读"、"不可重复读"和"幻读",其实都是数据库读一致性问题,必须由数据库提供一定的事务隔离机制来解决。
脏读、不可重复读和幻读有以下的包含关系,如果发生了脏读,那么幻读和不可重复读都有可能出现。
事务隔离级别
SQL 标准根据三种不一致的异常现象,将隔离性定义为四个隔离级别(Isolation Level),隔离级别和数据库的性能呈反比,隔离级别越低,数据库性能越高;而隔离级别越高,数据库性能越差,具体如下:
1、未提交读(RU),一个事务还没提交时,它做的变更就能被别的事务看到。
发生脏读的原因:RU 原理是对每个更新语句的行记录进行加锁,而不是对整个事务进行加锁,所以会发生脏读。而 RC 和 RR 会对整个事务加锁。
2、已提交读(RC),一个事务提交之后,它做的变更才会被其他事务看到。
不能重复读的原因:RC 每次执行 SQL 语句都会生成一个新的 Read View,每次读到的都是不同的。而 RR 的事务从始至终都是使用同一个 Read View。
3、可重复读(RR), 一个事务执行过程中看到的数据,总是跟这个事务在启动时看到的数据是一致的。当然在可重复读隔离级别下,未提交变更对其他事务也是不可见的。
默认是 MVCC 机制(“一致性非锁定读”)保证 RR 级别的隔离正确性,是不上锁的。可以选择手动上锁:select xxxx for update (排他锁); select xxxx lock in share mode(共享锁),称之为“一致性锁定读”。使用锁之后,就能在 RR 级别下,避免幻读。当然,默认的 MVCC 读,也能避免幻读。
4、串行化(serializable),所有事务一个接着一个的执行,这样可以避免幻读 (phantom read),对于基于锁来实现并发控制的数据库来说,串行化要求在执行范围查询的时候,需要获取范围锁,如果不是基于锁实现并发控制的数据库,则检查到有违反串行操作的事务时,需回滚该事务。
顾名思义是对于同一行记录,“写”会加“写锁”,“读”会加“读锁”。当出现读写锁冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行。
分布式事务产生的原因
分布式事务是伴随着系统拆分出现的,前面我们说过,分布式系统解决了海量数据服务对扩展性的要求,但是增加了架构上的复杂性,在这一点上,分布式事务就是典型的体现。
在实际开发中,分布式事务产生的原因主要来源于存储和服务的拆分。
存储层拆分
存储层拆分,最典型的就是数据库分库分表,一般来说,当单表容量达到千万级,就要考虑数据库拆分,从单一数据库变成多个分库和多个分表。在业务中如果需要进行跨库或者跨表更新,同时要保证数据的一致性,就产生了分布式事务问题。
事务隔离级别越严格,越消耗计算机性能,效率也越低。
数据库的事务隔离越严格,并发副作用越小,但付出的代价也就越大,因为事务隔离实质上就是使事务在一定程度上 “串行化”进行,这显然与“并发”是矛盾的。同时,不同的应用对读一致性和事务隔离程度的要求也是不同的,比如许多应用对“不可重复读”和“幻读”并不敏感,可能更关心数据并发访问的能力。
Mysql默认使用的数据隔离级别是REPEATABLE READ(可重复读,允许幻读)。MySql 的 REPEATABLE READ 级别不会导致幻读。
REPEATABLE READ级别会导致幻读,但是,由于 Mysql 的优化,在使用默认的 select 时,MySql 使用 MVCC 机制保证不会幻读;
也可以使用锁,在使用锁时,例如 for update(X 锁),lock in share mode(S 锁),MySql 会使用 Next-Key Lock 来保证不会发生幻读。
前者称为快照读,后者称为当前读。
Mysql默认使用REPEATABLE READ理由
Mysql在5.0这个版本以前,binlog只支持STATEMENT这种格式!而这种格式在读已提交(Read Commited)这个隔离级别下主从复制是有bug的,因此Mysql将可重复读(Repeatable Read)作为默认的隔离级别!
有什么bug呢?具体阅读《互联网项目中mysql应该选什么事务隔离级别》
mysql为什么不采默认用串行化(Serializable)?
因为每个次读操作都会加锁,快照读失效,一般是使用mysql自带分布式事务功能时才使用该隔离级别!
主从复制,是基于什么复制的?
是基于binlog复制的!
binlog有几种格式?
statement:记录的是修改SQL语句
row:记录的是每行实际数据的变更
mixed:statement和row模式的混合
事务开启的标志?事务结束的标志?
开启标志:任何一条DML语句(insert、update、delete)执行,标志事务的开启
结束标志(提交或者回滚):
提交:成功的结束,将所有的DML语句操作历史记录和底层硬盘数据来一次同步
回滚:失败的结束,将所有的DML语句操作历史记录全部清空
事物与数据库底层数据:
在事物进行过程中,未结束之前,DML语句是不会更改底层数据,只是将历史操作记录一下,在内存中完成记录。只有在事物结束的时候,而且是成功的结束的时候,才会修改底层硬盘文件中的数据。
什么是transaction及其控制?
Transaction包含了一系列的任务操作。这些操作可能是创建更新删除等等操作,是作为一个特定的结果来表现的。要么成功,要么不成功。
transaction有4种控制
commit,也就是提交
rollback,也就是回调
set transaction,对这个事务设定一个名字
save point。这个控制是用来设定某个点用来回退的
MYSQL的事务处理主要方法
用begin,rollback,commit来实现
begin 开始一个事务
rollback 事务回滚
commit 事务确认
改变mysql的自动提交模式
MYSQL默认事务是自动提交的,也就是你提交一个QUERY,它就直接执行!也就是说,只要执行一条DML语句就开启了事物,并且提交了事务。
我们可以通过设置autocommit来实现事务的处理。
set autocommit=0 禁止自动提交
set autocommit=1 开启自动提交
但注意当你用 set autocommit=0 的时候,你以后所有的SQL都将做为事务处理,直到你用commit确认或rollback结束,注意当你结束这个事务的同时也开启了个新的事务!按第一种方法只将当前的作为一个事务!
个人推荐使用第一种方法!
MYSQL中只有INNODB和BDB类型的数据表才能支持事务处理!其他的类型是不支持的!(切记!)
再来看看哪些问题会用到事务处理:
先假设一下问题的背景:网上购书,某书(数据库编号为123)只剩最后一本,而这个时候,两个用户对这本书几乎同时发出了购买请求,让我们看看整个过程:
在具体分析之前,先来看看数据表的定义:
-------------------------------------------------------------------------------
create table book
(
book_id unsigned int(10) not null auto_increment,
book_name varchar(100) not null,
book_price float(5, 2) not null, #我假设每本书的价格不会超过999.99元
book_number int(10) not null,
primary key (book_id)
)
type = innodb; #engine = innodb也行
-------------------------------------------------------------------------------
对于用户甲来说,他的动作稍微比乙快一点点,其购买过程所触发的动作大致是这样的:
-------------------------------------------------------------------------------
1. SELECT book_number FROM book WHERE book_id = 123;
book_number大于零,确认购买行为并更新book_number
2. UPDATE book SET book_number = book_number - 1 WHERE book_id = 123;
购书成功
-------------------------------------------------------------------------------
而对于用户乙来说,他的动作稍微比甲慢一点点,其购买过程所触发的动作和甲相同:
-------------------------------------------------------------------------------
1. SELECT book_number FROM book WHERE book_id = 123;
这个时候,甲刚刚进行完第一步的操作,还没来得及做第二步操作,所以book_number一定大于零
2. UPDATE book SET book_number = book_number - 1 WHERE book_id = 123;
购书成功
-------------------------------------------------------------------------------
表面上看甲乙的操作都成功了,他们都买到了书,但是库存只有一本,他们怎么可能都成功呢?再看看数据表里book_number的内容,已经变成 -1了,这当然是不能允许的(实际上,声明这样的列类型应该加上unsigned的属性,以保证其不能为负,这里是为了说明问题所以没有这样设置)
好了,问题陈述清楚了,再来看看怎么利用事务来解决这个问题,打开MySQL手册,可以看到想用事务来保护你的SQL正确执行其实很简单,基本就是三个语句:开始,提交,回滚。
开始:START TRANSACTION或BEGIN语句可以开始一项新的事务
提交:COMMIT可以提交当前事务,是变更成为永久变更
回滚:ROLLBACK可以回滚当前事务,取消其变更
此外,SET AUTOCOMMIT = {0 | 1}可以禁用或启用默认的autocommit模式,用于当前连接。
-------------------------------------------------------------------------------
那是不是只要用事务语句包一下我们的SQL语句就能保证正确了呢?比如下面代码:
-------------------------------------------------------------------------------
BEGIN;
SELECT book_number FROM book WHERE book_id = 123;
// ...
UPDATE book SET book_number = book_number - 1 WHERE book_id = 123;
COMMIT;
-------------------------------------------------------------------------------
答案是否定了,这样依然不能避免问题的发生,如果想避免这样的情况,实际应该如下:
-------------------------------------------------------------------------------
BEGIN;
SELECT book_number FROM book WHERE book_id = 123 FOR UPDATE ;
// ...
UPDATE book SET book_number = book_number - 1 WHERE book_id = 123;
COMMIT;
-------------------------------------------------------------------------------
由于加入了FOR UPDATE,所以会在此条记录上加上一个行锁,如果此事务没有完全结束,那么其他的事务在使用SELECT ... FOR UPDATE请求的时候就会处于等待状态,直到上一个事务结束,它才能继续,从而避免了问题的发生,需要注意的是,如果你其他的事务使用的是不带FOR UPDATE的SELECT语句,将得不到这种保护。
MySQL事务操作
mysql> use RUNOOB;
Database changed
mysql> CREATE TABLE runoob_transaction_test( id int(5)) engine=innodb; # 创建数据表
Query OK, 0 rows affected (0.04 sec)
mysql> select * from runoob_transaction_test;
Empty set (0.01 sec)
mysql> begin; # 开始事务
Query OK, 0 rows affected (0.00 sec)
mysql> insert into runoob_transaction_test value(5);
Query OK, 1 rows affected (0.01 sec)
mysql> insert into runoob_transaction_test value(6);
Query OK, 1 rows affected (0.00 sec)
mysql> commit; # 提交事务
Query OK, 0 rows affected (0.01 sec)
mysql> select * from runoob_transaction_test;
+------+
| id |
+------+
| 5 |
| 6 |
+------+
2 rows in set (0.01 sec)
mysql> begin; # 开始事务
Query OK, 0 rows affected (0.00 sec)
mysql> insert into runoob_transaction_test values(7);
Query OK, 1 rows affected (0.00 sec)
mysql> rollback; # 回滚
Query OK, 0 rows affected (0.00 sec)
mysql> select * from runoob_transaction_test; # 因为回滚所以数据没有插入
+------+
| id |
+------+
| 5 |
| 6 |
+------+
2 rows in set (0.01 sec)
参考链接:
MySQL——事务(Transaction)详解 https://blog.csdn.net/w_linux/article/details/79666086
MySql 三大知识点——索引、锁、事务 https://zhuanlan.zhihu.com/p/59764376
面试官问你:MYSQL事务和隔离级别,该如何回答 https://zhuanlan.zhihu.com/p/70701037
谈谈MySQL的锁 https://zhuanlan.zhihu.com/p/65721606
一文说尽MySQL事务及ACID特性的实现原理 https://zhuanlan.zhihu.com/p/56874694
MySQL 事务 https://www.runoob.com/mysql/mysql-transaction.html