MySQL · 事务

场景

一组sql(增删改)要么都执行,要么都不执行。

场景:A 有 10000 元,B 有 10000 元,A 向 B 转账 500,是两条更新

update bill set 余额 = 余额 - 500 where A;

update bill set 余额 = 余额 + 500 where B;

这两条数据必须要么都执行,要么都不执行,如果执行了一半,发生问题,那么执行过的 sql 要回滚。

事务

定义:事务就是要保证一组数据库操作,要么全部成功,要么全部失败。

特性:A (Atomicity) 原子性、C (Consistency) 一致性、I (Isolation) 隔离性、D (Durability) 持久性。

作用:事务会把数据库从一种一致状态转换为另一种一致状态,用以保证数据的完整性。

事务特性

在 MySQL 中,事务支持是在引擎层实现的,只有 InnoDB 存储引擎才支持事务,完全符合 ACID 特性:

  • 原子性(Atomicity):是指整个数据库事务是不可分割的工作单位。只有使事务中所有的数据库操作都执行成功,才算整个事务成功。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样,通常使用 undo log 来实现。
  • 一致性(Consistency):是指事务将数据库从一种状态转变为下一种一致的状态。如果事务中某个动作失败了,系统可以自动撤销事务,返回初始化的状态,通常使用 undo log 来实现。
  • 隔离性(Isolation):数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致,通常使用锁和 MVCC 机制来实现。
  • 持久性(durability):事务一旦提交,其结果将是永久性的。即使发生宕机等故障,数据库也能将数据恢复m,通常使用 redo log 来实现。

事务隔离级别

当数据库上有多个事务同时执行的时候,就可能出现脏读(dirty read)、不可重复读(non-repeatable read)、幻读(phantom read)的问题,为了解决这些问题,就有了“隔离级别”的概念。还需要清楚的是,隔离得越严实,效率就会越低。

  • Read uncommitted(读未提交):一个事务还没提交时,它做的变更就能被别的事务看到。
  • Read committed(读提交):一个事务提交之后,它做的变更才会被其他事务看到。
  • Repeatable read(可重复读):一个事务在执行期间看到的数据前后必须是一致的。当然在可重复读隔离级别下,未提交变更对其他事务也是不可见的。
  • Serializable(串行化):一个事务尚未提交,别人就别想操作。
隔离级别 脏读 不可重复读 幻读
Read uncommitted(未提交读)
Read committed(提交读) ×
Repeatable read(可重复读) × ×
Serializable(串行化) × × ×
1
2
3
4
5
6
mysql> show variables like 'transaction_isolation';
+-----------------------+-----------------+
| Variable_name | Value |
+-----------------------+-----------------+
| transaction_isolation | REPEATABLE-READ |
+-----------------------+-----------------+

img

事务实现原理

事务的隔离性是由锁来实现的。原子性、一致性、持久性是通过数据库的 redo log 和 undo log 来完成。redo log 称为重做日志,用来保证事务的原子性和持久性,undo log 用来保证事务的一致性。

redo 和 undo 的作用都可以视为一种恢复操作,redo 恢复提交事务修改的页操作,而 undo 回滚行记录到某个特定版本。因此两者记录的内容不同,redo 通常是物理日志,记录的是页的物理修改操作。undo 是逻辑日志,根据每行记录进行记录。

redo log

介绍:其由两部分组成:一是内存中重做日志缓冲(redo log buffer),在内存中是易失的;二是重做日志文件(redo log file),在磁盘中是持久的,当事务提交之后会把所有修改信息都会存到该日志中。

用途:redo log 重做日志用来实现事务的持久性,即事务 ACID 中的 D。

场景:MySQL 为了提升性能不会把每次的修改都实时同步到磁盘,而是会先存到 Boffer Pool(缓冲池)里头,把这个当作缓存来用,然后使用后台线程去做缓冲池和磁盘之间的同步。如果还没来的及同步的时候宕机或断电了怎么办?这样会导致丢部分已提交事务的修改信息!所以引入了 redo log 来记录已成功提交事务的修改信息,并且会把 redo log 持久化到磁盘,系统重启之后在读取 redo log 恢复最新数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 原始bank表
mysql> select * from bank;
+----+----------+---------+
| id | name | balance |
+----+----------+---------+
| 1 | zhangsan | 1000 |
+----+----------+---------+

// 原始finance表
mysql> select * from finance;
+----+----------+--------+
| id | name | amount |
+----+----------+--------+
| 1 | zhangsan | 0 |
+----+----------+--------+

start transaction;
select balance from bank where name="zhangsan";

// 生成重做日志 balance=600
update bank set balance = balance - 400;

// 生成重做日志 amount=400
update finance set amount = amount + 400;

undo log

介绍:undo log 叫做回滚日志,用于记录数据被修改前的信息。

用途:假如由于系统错误或者 rollback 操作而回滚的话可以根据 undo log 信息来进行回滚到没被修改前的状态,实现事务的一致性,即事务 ACID 中的 C。

场景:每次写入数据或者修改数据之前都会把修改前的信息记录到 undo log。

用途:锁用来实现事务的隔离性,即事务 ACID 中的 I。

场景:当有多个请求来读取表中的数据时可以不采取任何操作,但是多个请求里有读请求,又有修改请求时,必须有一种措施来进行并发控制,不然很有可能会造成不一致。解决上述问题很简单,只需用两种锁的组合来对读写请求进行控制即可。

  • 共享锁 (shared lock),又叫做 “读锁”:读锁是可以共享的,或者说多个读请求可以共享一把锁读数据,不会造成阻塞。
  • 排他锁 (exclusive lock),又叫做 “写锁”: 写锁会排斥其他所有获取锁的请求,一直阻塞,直到写入完成释放锁。

MVCC

介绍:MVCC (Multi Version Concurrency Control) 叫做多版本并发控制。一般情况下,事务性储存引擎不是只使用表锁,行锁处理数据,而是结合了 MVCC 机制,以处理更多的并发问题。

用途:读不加锁,读写不冲突,在读多写少的 OLTP 应用中,读写不冲突是非常重要的,极大的增加了系统的并发性能。

实现方法:InnoDB 的 MVCC ,是通过在每行记录的后面保存两个隐藏的列来实现的,这两个列, 一个保存了行的创建时间,一个保存了行的过期时间, 当然存储的并不是实际的时间值,而是系统版本号(可以理解为事务 ID),每开始一个新的事务,系统版本号就会自动递增。

事务控制

事务控制:

  • BEGIN:开始一个事务
  • ROLLBACK:事务回滚
  • COMMIT:事务提交

事务提交模式:

  • SET AUTOCOMMIT=0:禁止自动提交
  • SET AUTOCOMMIT=1:开启自动提交

对于事务操作的统计:

TPS:Transaction Per Second,每秒事务处理数。计算方法:(com_commit + com_rollback) / time,计算的前提是所有事务必须是显式提交的。

需要注意的是,DDL 语句是隐式提交的 SQL 语句,如 TRUNCATE TABLE,就不会统计在内,也不能被回滚。

分布式事务

分布式事务(Distributed Transactions):通常是一个在分布式环境下运行的扁平事务,因此需要根据数据所在位置访问网络中的不同节点。对于分布式事务,其同样需要满足 ACID 特性,要么都发生,要么都失效。在使用分布式事务时,InnoDB 存储引擎的事务隔离级别必须设置为 Serializable。