事务就是一组DML语句组成,这些语句在逻辑上存在相关性,这一组DML语句要么全部成功,要么全部失败,是一个整体。MySQL提供一种机制,保证我们达到这样的效果。事务还规定不同的客户端看到的数据是不相同的。
事务就是要做的或所做的事情,主要用于处理操作量大,复杂度高的数据
正如我们上面所说,一个 MySQL
数据库,可不止你一个事务在运行,同一时刻,甚至有大量的请求被包装成事务,在向 MySQL
服务器发起事务处理请求。而每条事务至少一条 SQL
,最多很多 SQL
,这样如果大家都访问同样的表数据,在不加保护的情况,就绝对会出现问题。甚至,因为事务由多条 SQL
构成,那么,也会存在执行到一半出错或者不想再执行的情况,那么已经执行的怎么办呢?
所有,一个完整的事务,绝对不是简单的 sql
集合,还需要满足如下四个属性:
ACID
。原子性(Atomicity,或称不可分割性)
一致性(Consistency)
隔离性(Isolation,又称独立性)
持久性(Durability)。
本质是为了当应用程序访问数据库的时候,事务能够简化我们的编程模型,不需要我们去
考虑各种各样的潜在错误和并发问题.可以想一下当我们使用事务时,要么提交,要么回滚,我们不会去考虑网络异常了,服务器宕机了,同时更改一个数据怎么办对吧?因此事务本质上是为了应用层服务的.而不是伴随着数据库系统天生就有的.
在 MySQL 中只有使用了 Innodb
数据库引擎的数据库或表才支持事务, MyISAM
不支持
查看数据库引擎
show engines \G -- 行显示
事务的提交方式常见的有两种:
查看事务提交方式
mysql> show variables like 'autocommit';
用 SET 来改变 MySQL 的自动提交模式:
mysql> SET AUTOCOMMIT=0; #SET AUTOCOMMIT=0 禁止自动提交
mysql> SET AUTOCOMMIT=1; #SET AUTOCOMMIT=1 开启自动提交
如果默认事务是自动提交的话,后续手动启动begin,默认自动提交是无效的
mysql> start transaction; -- 开始一个事务begin也可以,推荐begin
mysql> savepoint save1; -- 创建一个保存点save1
mysql> insert into account values (1, '张三', 100); -- 插入一条记录
mysql> savepoint save2; -- 创建一个保存点save2
mysql> insert into account values (2, '李四', 10000); -- 在插入一条记录
mysql> select * from account; -- 两条记录都在了
+----+--------+----------+
| id | name | blance |
+----+--------+----------+
| 1 | 张三 | 100.00 |
| 2 | 李四 | 10000.00 |
+----+--------+----------+
mysql> rollback to save2; -- 回滚到保存点save2
mysql> select * from account; -- 一条记录没有了
+----+--------+--------+
| id | name | blance |
+----+--------+--------+
| 1 | 张三 | 100.00 |
+----+--------+--------+
mysql> rollback; -- 直接rollback,回滚在最开始
mysql> select * from account; -- 所有刚刚的记录没有
Empty set (0.00 sec)
1.未commit,客户端崩溃,MySQL自动会回滚
mysql> begin; --开启事务
mysql> insert into account values (1, '张三', 100); -- 插入记录
mysql> select * from account; --数据已经存在,但没有commit,此时同时查看终端B
+----+--------+--------+
| id | name | blance |
+----+--------+--------+
| 1 | 张三 | 100.00 |
+----+--------+--------+
mysql> Aborted -- ctrl + \ 异常终止MySQL
mysql> select * from account; --数据自动回滚
Empty set (0.00 sec)
2.commit了,客户端崩溃,MySQL数据不会在受影响,已经持久化
mysql> select * from account; -- 当前表内无数据
Empty set (0.00 sec)
mysql> begin; -- 开启事务
mysql> insert into account values (1, '张三', 100); -- 插入记录
mysql> commit; --提交事务
mysql> Aborted -- ctrl + \ 异常终止MySQL
mysql> select * from account; --数据存在了,commit是将数据持久化到MySQL
+----+--------+--------+
| id | name | blance |
+----+--------+--------+
| 1 | 张三 | 100.00 |
+----+--------+--------+
3.自动提交-未commit,数据也会持久化到Mysql中
mysql> show variables like 'autocommit'; //查看自动提交是否开启
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit | OFF |
+---------------+-------+
mysql> set autocommit=1; --开启自动提交
mysql> select * from account;
+----+--------+--------+
| id | name | blance |
+----+--------+--------+
| 1 | 张三 | 100.00 |
+----+--------+--------+
mysql> insert into account values (2, '李四', 10000);
mysql> Aborted --异常终止
mysql> select * from account;--终端A崩溃并不影响已经持久化。autocommit起作用
+----+--------+----------+
| id | name | blance |
+----+--------+----------+
| 1 | 张三 | 100.00 |
| 2 | 李四 | 10000.00 |
+----+--------+----------+
MySQL
有 MVCC
)事务操作注意事项
InnoDB
支持事务, MyISAM
不支持事务隔离级别如何实现:隔离,基本都是通过锁实现的,不同的隔离级别,锁的使用是不同的。常见有,表锁,行锁,读锁,写锁,间隙锁(GAP),Next-Key锁(GAP+行锁)等。
查看
mysql> SELECT @@global.tx_isolation; --查看全局隔级别
mysql> SELECT @@session.tx_isolation; --查看会话(当前)全局隔级别
mysql> SELECT @@tx_isolation; --默认同上
设置
-- 设置当前会话 or 全局隔离级别语法
SET [SESSION | GLOBAL] TRANSACTION ISOLATION LEVEL {
READ UNCOMMITTED | READ COMMITTED | REPEATABLE READ | SERIALIZABLE}
--设置当前会话隔离性,另起一个会话,看不多,只影响当前会话
mysql> set session transaction isolation level serializable; -- 串行化
几乎没有加锁,虽然效率高,但是问题太多,严重不建议采用
--终端A
-- 设置隔离级别为 读未提交
mysql> set global transaction isolation level read uncommitted
--重启客户端
mysql> select @@tx_isolation;
+------------------+
| @@tx_isolation |
+------------------+
| READ-UNCOMMITTED |
+------------------+
mysql> select * from account;
+----+--------+----------+
| id | name | blance |
+----+--------+----------+
| 1 | 张三 | 100.00 |
| 2 | 李四 | 10000.00 |
+----+--------+----------+
mysql> begin; --开启事务
mysql> update account set blance=123.0 where id=1; --更新指定行
--没有commit!!!
--终端B
mysql> begin;
mysql> select * from account;
+----+--------+----------+
| id | name | blance |
+----+--------+----------+
| 1 | 张三 | 123.00 | -读到终端A更新但是未commit的数据[insert,delete同样]
| 2 | 李四 | 10000.00 |
+----+--------+----------+
一个事务在执行中,读到另一个执行中事务的更新(或其他操作)但是未commit的数据,这种现象叫做脏读(dirty read)
--终端A
-- 设置隔离级别为 读提交
mysql> set global transaction isolation level read committed
--重启客户端
mysql> select @@tx_isolation;
+------------------+
| @@tx_isolation |
+------------------+
| READ-COMMITTED |
+------------------+
mysql> select * from account;
+----+--------+----------+
| id | name | blance |
+----+--------+----------+
| 1 | 张三 | 123.00 |
| 2 | 李四 | 10000.00 |
+----+--------+----------+
mysql> begin; --开启事务,同步的,终端B也开始事务
mysql> update account set blance=321.0 where id=1; --更新指定行
--没有commit!!!
--终端B
mysql> begin;
mysql> select * from account;
+----+--------+----------+
| id | name | blance |
+----+--------+----------+
| 1 | 张三 | 123.00 | -终端A commit之前,查看不到,--老的值
| 2 | 李四 | 10000.00 |
+----+--------+----------+
mysql> select * from account;
+----+--------+----------+
| id | name | blance |
+----+--------+----------+
| 1 | 张三 | 321.00 | -终端A commit之后,查看到,--新的值
| 2 | 李四 | 10000.00 |
+----+--------+----------+
此时还在当前事务中,并未commit,那么就造成了,同一个事务内,同样的读取,在不同的时间段(依旧还在事务操 作中!),读取到了不同的值,这种现象叫做不可重复读(non reapeatable read)!!
--终端A
-- 设置隔离级别为 可重复读
mysql> set global transaction isolation level repeatable read;
--重启客户端
mysql> select @@tx_isolation;
+------------------+
| @@tx_isolation |
+------------------+
| REPEATABLE-READ |
+------------------+
mysql> select * from account;
+----+--------+----------+
| id | name | blance |
+----+--------+----------+
| 1 | 张三 | 321.00 |
| 2 | 李四 | 10000.00 |
+----+--------+----------+
mysql> begin; --开启事务,同步的,终端B也开始事务
mysql> update account set blance=4321.0 where id=1; --更新指定行
--没有commit!!!
--终端B
mysql> begin;
mysql> select * from account;
+----+--------+----------+
| id | name | blance |
+----+--------+----------+
| 1 | 张三 | 321.00 | -终端A commit之前,查看不到,--老的值
| 2 | 李四 | 10000.00 |
+----+--------+----------+
mysql> select * from account;
+----+--------+----------+
| id | name | blance |
+----+--------+----------+
| 1 | 张三 | 321.00 | -终端A commit之后,查看不到,--老的值
| 2 | 李四 | 10000.00 |
+----+--------+----------+
mysql> commit; --结束事务
mysql> select * from account;
+----+--------+----------+
| id | name | blance |
+----+--------+----------+
| 1 | 张三 | 4321.00 | --再次查看,看到最新的更新数据
| 2 | 李四 | 10000.00 |
+----+--------+----------+
–可以看到,在终端B中,事务无论什么时候进行查找,看到的结果都是一致的,这叫做可重复读!
–如果将上面的终端A中的update操作,改成insert操作,会有什么问题??
--终端A
mysql> select * from account;
+----+--------+----------+
| id | name | blance |
+----+--------+----------+
| 1 | 张三 | 321.00 |
| 2 | 李四 | 10000.00 |
+----+--------+----------+
mysql> begin; --开启事务,同步的,终端B也开始事务
mysql> insert into account (id,name,blance) values(3, '王五', 5432.0);
mysql> commit; --提交事务
--切换终端到终端B,查看数据。
mysql> select * from account;
+----+--------+----------+
| id | name | blance |
+----+--------+----------+
| 1 | 张三 | 4321.00 | -终端A commit之前,查看不到,--老的值
| 2 | 李四 | 10000.00 |
| 3 | 王五 | 5432.00 |
+----+--------+----------+
--终端B
mysql> begin; --开启事务
mysql> select * from account;
+----+--------+----------+
| id | name | blance |
+----+--------+----------+
| 1 | 张三 | 4321.00 | -终端A commit之后,查看到,--旧的值
| 2 | 李四 | 10000.00 |
+----+--------+----------+
mysql> select * from account;
+----+--------+----------+
| id | name | blance |
+----+--------+----------+
| 1 | 张三 | 4321.00 | -终端A commit之后,查看到,--旧的值
| 2 | 李四 | 10000.00 |
+----+--------+----------+
mysql> commit; --结束事务
mysql> select * from account;
+----+--------+----------+
| id | name | blance |
+----+--------+----------+
| 1 | 张三 | 4321.00 |
| 2 | 李四 | 10000.00 |
| 3 | 王五 | 5432.00 | ----看到更新
+----+--------+----------+
发现终端A在对应事务中insert的数据,在终端B的事务周期中,也没有什么影响,也符合可重复的特点。
但是,一般的数据库在可重复读情况的时候,无法屏蔽其他事务insert的数据,为什
么?
因为隔离性实现是对数据加锁完成的,而insert待插入的数据因为并不存在,那么一般加锁无法屏蔽这类问题),会造成虽然大 部分内容是可重复读的,但是insert的数据在可重复读情况被读取出来,导致多次查找时,会多查找出来新的记录,就如同产生了幻觉。这种现象,叫做幻读(phantom read)。
很明显,MySQL在RR级别的时候,是解决了幻读问题的(解决的方式是用 Next-Key锁(GAP+行锁)解决的
对所有操作全部加锁,进行串行化,不会有问题,但是只要串行化,效率很低,几乎完全不会被采用
mysql> set global transaction isolation level serializable;
mysql> select @@tx_isolation;
+----------------+
| @@tx_isolation |
+----------------+
| SERIALIZABLE |
+----------------+
mysql> begin; --开启事务,终端B同步开启
mysql> select * from account; --两个读取不会串行化,共享锁
+----+--------+----------+
| id | name | blance |
+----+--------+----------+
| 1 | 张三 | 4321.00 |
| 2 | 李四 | 10000.00 |
| 3 | 王五 | 5432.00 |
+----+--------+----------+
--终端A中有更新或者其他操作,会阻塞。直到终端B事务 提交。
mysql> update account set blance=1.00 where id=1;
--终端B
mysql> begin;
mysql> select * from account; --两个读取不会串行化
+----+--------+----------+
| id | name | blance |
+----+--------+----------+
| 1 | 张三 | 4321.00 |
| 2 | 李四 | 10000.00 |
| 3 | 王五 | 5432.00 |
+----+--------+----------+
mysql> commit; --提交之后,终端A中的update才会提交
数据库并发的场景有三种:
多版本并发控制( MVCC )是一种用来解决 读-写冲突 的无锁并发控制
为事务分配单向增长的事务ID,为每个修改保存一个版本,版本与事务ID关联,读操作只读该事务开始前的数据库的快照。 所以 MVCC 可以为数据库解决以下问题
MVCC:
3个记录隐藏字段
mysql> create table if not exists student(
name varchar(11) not null,
age int not null );
mysql> insert into student (name, age) values ('张三', 28);
mysql> select * from student;
+--------+-----+
| name | age |
+--------+-----+
| 张三 | 28 |
+--------+-----+
上面描述的意思是:
我们目前并不知道创建该记录的事务ID,隐式主键,我们就默认设置成null,1。第一条记录也没有其他版本,我们设置回滚指针为null
undo 日志
索引,事务,隔离性,日志等,都是在内存中完成的,即在 MySQL 内部的相关缓冲区中,保存相关数据,完成各种判断操作。然后在合适的时候,将相关数据刷新到磁盘当中的。
undo log,简单理解成,就是 MySQL 中的一段内存缓冲区,用来保存日志数据的就行
现在有一个事务10(仅仅为了好区分),对student表中记录进行修改(update):将name(张三)改成name(李四)。
现在又有一个事务11,对student表中记录进行修改(update):将age(28)改成age(38)
这样,我们就有了一个基于链表记录的历史版本链。所谓的回滚,无非就是用历史数据,覆盖当前数据。
上面的一个一个版本,可以称之为一个一个的快照。
那么,如何保证,不同的事务,看到不同的内容呢?也就是如何如何实现隔离级别?
Read View
Read View就是事务进行 快照读 操作的时候生产的 读视图 (Read View),在该事务执行的快照读的那一刻,会生成数据库系统当前的一个快照,记录并维护系统当前活跃事务的ID(当每个事务开启时,都会被分配一个ID, 这个ID是递增的,所以最新的事务,ID值越大)
Read View 在 MySQL 源码中,就是一个类,本质是用来进行可见性判断的。 即当我们某个事务执行快照读的时候,对该记录创建一个 Read View 读视图,把它比作条件,用来判断当前事务能够看到哪个版本的数据,既可能是当前最新的数据,也有可能是该行记录的 undo log 里面的某个版本的数据
当前读和快照读在RR级别下的区别