MariaDB—— 15. 事务

事务中的所有sql语句被当做一个操作单元,换句话说,事务中的sql语句要么都执行成功,要么全部执行失败,事务内的sql语句被当做一个整体,被当做一个原子进行操作。

mysql中,innodb存储引擎是支持事务的,而且innodb存储引擎的事务完全符合ACID的特性,ACID是如下四大特性的首字母缩写。
A:atomicity 原子性
C:consistency 一致性
I:isolation 隔离性
D:durability 持久性

原子性:整个事务中的所有操作要么全部执行成功,要么全部执行失败后混滚到最初状态。
一致性:数据库总是从一个一致性状态转为另一个一致性状态。
隔离性:一个事务在提交之前所做出的的操作是否能为其他事务可见,由于不同的场景需求不同,所以针对隔离性来说,有不同的隔离级别。
持久性:事务一旦提交,事务所做出的修改将会永久保存,此时即使数据库崩溃,修改的数据也不会丢失。

1. redo log

mysql会将事务中的sql语句涉及到的所有数据操作先记录到redo log中,然后再将操作从redo log中同步到对应数据文件中(此处假设事物操作的数据量并非巨大),换句话说,在事务执行提交成功以前,在修改对应的数据文件中的记录之前,一定要保证 对应的所有修改操作已经记录到了redo log中,假设事务中的sql语句涉及到60条记录的修改,那么在修改这60条记录之前,要将这60条修改操作记录到redo log中,当这60条操作都记录到redolog中以后,再从redo log中一条一条同步到数据文件的对应记录中。所以,即使数据文件中的数据被修改到一半时被打断(比如停电),那么也能依靠redo log中的日志将剩余的部分操作再次同步到对应的数据文件中。

使用redo log,能够实现ACID中的A,也就是原子性,即事务中的所有sql被当做一个执行单元。
redo log其实由两部分组成:redo log buffer(重做日志缓冲) 和 redo log file(重做日志文件)
redo log buffer存在于内存之中,是易失的,redo log file是持久的,存在于磁盘上。
重做日志先被写入到redo log buffer中,虽然内存的速度极快,但是无法满足持久性的需求,因为内存中的数据是易失的,所以为了满足持久性,需要将redo log buffer中的日志写入到redo log file中,相当于从内存中同步到磁盘上,所以磁盘的性能会影响事务的性能,由于redo log file是磁盘上一段连续的空间,所以写速度还是比较快的,比离散的写操作要快很多,当操作记录被记录到redo log file中以后,再从redo log file中将操作同步到数据文件中。
虽然,我们应该实时将redo log buffer中的数据写入到redo log file中以保证数据的安全性,但是这样会极大的降低性能,我们可以通过设置innodb_flush_log_at_trx_commit参数来修改从 redo log buffer写入redo log file的策略,但是如果这样做,则会丧失持久性,有可能会丢失部分数据,具体使用怎样的刷写策略,还需要根据实际情况自己权衡。

redo log是物理日志,之所以说它是物理日志,是因为redo log 中记录的是数据库对页的操作,而不是逻辑上的增删改查,重做日志具有幂等性。

2. undo log

可以把undo log理解成数据被修改前的备份。如果说事务进行了一半,有一条sql没有执行成功,那么数据库可以根据undo log进行撤销,将所有修改过的数据从逻辑上恢复到修改之前的样子,注意,是逻辑上还原成原来的样子,比如,之前insert了1000条数据 ,那么就delete它们,如果delete了2000条,就insert它们,如果update了500条数据,就再次根据undo log去update它们,所以,undo log是逻辑日志,与redo log记录的页操作物理日志不同。

3. log group

log group为重做日志组,一个重做日志组(log group)中有多个重做日志文件(redo log file),当日志组中的第一个logfile被写满,则会开始将redo log写入日志组中的下一个重做日志文件中,以此类推,当日志组中的所有redo log file都被写满,则将redo log再写入第一个redo log file 中,覆盖原来的redo log,以便新的redo log 被写入。
如果重做日志所在的设备崩溃了,那么redo log将有可能丢失,这样就无法保证redo log在任何时候都是可用的,所以,log group还支持日志组镜像,为了保险起见,我们应该将log group放在有冗余能力的设备上,比如raid1。
redo log存储于重做日志文件中,undo log则不同,undo存放在数据库内部的特殊段中,这个段被称为undo段(undo segment),undo段位于共享表空间中。
不管是redo log或者undo log,都是innodb的产物,或者说是innodb存储引擎层面的产物,而在mysql中,还有另外一种重要的日志,二进制日志,也就是平常所说的 binlog,它是建立mysql主从复制环境时所必须的日志,但是binlog并不是innodb存储引擎层面的产物,而是整个mysql数据库层面的 产物,换句话说,binlog不止针对于innodb,mysql数据库中的任何存储引擎对于数据库的更改都会产生二进制日志(binlog)。
innodb的redo log记录的是物理格式的日志,记录了对页的操作,而binlog记录的是逻辑日志,记录的是对应的SQL。
redolog与binlog写入磁盘的时机也不同,innodb的redo log在事务进行时会不断的写入redo log file,binlog只在事务提交完成后进行一次磁盘写入。
其实,不管是redo log 还是 undo log,都可以理解成恢复数据库的手段。

4. 事务日志参数

mysql> show global variables like '%innodb%log%';
+----------------------------------+------------+
| Variable_name                    | Value      |
+----------------------------------+------------+
| innodb_api_enable_binlog         | OFF        |
| innodb_flush_log_at_timeout      | 1          |
| innodb_flush_log_at_trx_commit   | 1          |
| innodb_locks_unsafe_for_binlog   | OFF        |
| innodb_log_buffer_size           | 16777216   |
| innodb_log_checksums             | ON         |
| innodb_log_compressed_pages      | ON         |
| innodb_log_file_size             | 50331648   |
| innodb_log_files_in_group        | 2          |
| innodb_log_group_home_dir        | ./         |
| innodb_log_write_ahead_size      | 8192       |
| innodb_max_undo_log_size         | 1073741824 |
| innodb_online_alter_log_max_size | 134217728  |
| innodb_undo_log_truncate         | OFF        |
| innodb_undo_logs                 | 128        |
+----------------------------------+------------+

innodb_log_file_size 表示每个redo log file的大小,单位为字节,上图中的设置表示每个重做日志文件的大小为5M
innodb_log_files_in_group 表示每个重做日志组中有几个redo log file
innodb_log_group_home_dir 表示重做日志组文件所在路径,此处的相对路径表示数据所在目录,默认情况下为/var/lib/mysql,此目录中的ib_logfile0与 ib_logfile1即为日志组中的两个重做日志,可以看到,这两个日志文件的大小为5M,也对应了innodb_log_file_size的值
innodb_mirrored_log_groups 表示一共有几组日志组,1表示只有1组镜像日志组,就是当前日志组本身,说白了,如果此值为1,表示没有冗余的日志组,如果想要有冗余的镜像日志组,此值至少要设置为2,此值容易被字面误解,需注意,如果重做日志所在的硬件设备并没有冗余能力,同时用户对数 据安全性要求较高,那么往往需要将此值设置为大于等于2的值。
innodb_flush_log_at_trx_commit 表示当事务提交以后,是否立即将redo log从内存(log buffer)刷写到redo log file中。
如果此值设置为1(默认值),表示事务提交时必须将redo log从log buffer中刷写到redologfile(磁盘)中,过程为:事务提交–log buffer–os buffer–log file,此值为1时完全满足ACID的要求。
如果此值设置为0,事务提交时并不会将redo log从log buffer刷写到redo log file,但是会在每秒钟自动刷写一次,也就是说每一秒钟都自动将内存中的redo log刷写到redo log file(磁盘)中,可以理解为,当事务提交时,redo log存在于log buffer中,每秒钟,log从log buffer中经过os buffer,刷写到log file中一次,当此值设置为0时,如果mysql数据库崩溃,最多会丢失1秒钟的redo log。
如果此值设置为2,表示在事务提交时,只会将redo log写入到文件系统内存(os buffer)中,但是不会立即写入到redo log file(磁盘)中,而是每秒钟从文件系统缓存中将数据刷写至redo log file(磁盘)中一次,可以理解为,当事务提交时,redo log存在于log buffer和os buffer中,每秒钟,log从os buffer中刷写到log file中一次,此值为2时,如果只是mysql数据库宕机,但是操作系统没有宕机,则数据不会丢失,如果此时操作系统宕机,重启数据库后,则会丢失未从 文件系统内存刷写到redo log file中的那部分事务(约1秒钟的数据),因为只有mysql宕机而操作系统没有宕机时,并不会丢失数据,所以可靠性 比此值设置为0时要高一些。
理论上来说,此值设置为1,安全性最高,性能最低,设置为0,性能最高,安全性最低,设置为2,性能较高,安全性较低,此值设置为1,能够满足 ACID的特性,设置为0或2,将会失去ACID的特性。但是需要注意,很多操作系统或者硬盘设备会欺骗mysqld进程,让mysqld进程认为刷写操 作已经完成,但是实际上并没有,在这种情况下,即使innodb_flush_log_at_trx_commit的值设置为1,也不能保证事务的可用 性。

5. 事务控制语句

每执行一条sql语句,mysql都会把这条sql当做一个单语句事务进行提交,而且默认是自动提交的,我们可以使用如下语句查看mysql是否开启了自动提交功能,查看全局与当前会话是否开启了自动提交功能。

mysql> show global variables like 'autocommit%';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit    | ON    |
+---------------+-------+
1 row in set (0.00 sec)

mysql> show session variables like 'autocommit%';
+---------------+-------+
| Variable_name | Value |
+---------------+-------+
| autocommit    | ON    |
+---------------+-------+
1 row in set (0.01 sec)

start transaction 或者 begin :表示显示的开始一个事务,虽然begin和start transaction都表示显式的开启一个事务,但是在存储过程中,mysql会将begin识别为begin···end,所以,在存储过程中,只能 使用start transaction来表示开始一个事务。
commit 或者 commit work :表示提交事务,也就是说从begin到commit之间的所有sql语句对数据库所作出的修改将会被真正的执行,成为永久性的操作。
rollback 或者 rollback work :表示回滚事务,回滚事务会撤销所有未提交的修改并结束当前事务,注意,使用rollback回滚事务以后,当前事务会结束,后面的操作不算在当前事务以内。
savepoint 标识符 :表示创建一个事务的保存点,以便我们回滚到当前保存点,而不是回滚整个事务,就好比我们的游戏存档一样,如果你在当前位置设置了保存点,那么当你game over的时候,可以从这个保存点继续,而不是从游戏的开始处继续,一个事务中可以创建多个保存点。
rollback to savepoint 标识符 :表示根据标识符回滚到指定。
release savepoint 标识符 :表示删除一个保存点。

mysql> desc test2;
+-------+-------------+------+-----+---------+-------+
| Field | Type        | Null | Key | Default | Extra |
+-------+-------------+------+-----+---------+-------+
| id    | int(11)     | NO   | PRI | NULL    |       |
| name  | varchar(30) | YES  |     | NULL    |       |
+-------+-------------+------+-----+---------+-------+
2 rows in set (0.00 sec)

mysql> select * from test2;
Empty set (0.00 sec)

mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> insert into test2(id,name) values  (1,'blueicex'),(2,'gege');
Query OK, 2 rows affected (0.01 sec)
Records: 2  Duplicates: 0  Warnings: 0

mysql> rollback;
Query OK, 0 rows affected (0.00 sec)

mysql> commit;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from test2;
Empty set (0.00 sec)

mysql> select * from test2;
Empty set (0.00 sec)

mysql> insert into test2(id,name) values  (1,'blueicex'),(2,'gege');
Query OK, 2 rows affected (0.01 sec)
Records: 2  Duplicates: 0  Warnings: 0

mysql> select * from test2;
+----+----------+
| id | name     |
+----+----------+
|  1 | blueicex |
|  2 | gege     |
+----+----------+
2 rows in set (0.00 sec)

mysql> savepoint a;
Query OK, 0 rows affected (0.00 sec)

mysql>  insert into test2(id,name) values  (3,'name1'),(4,'name2');
Query OK, 2 rows affected (0.01 sec)
Records: 2  Duplicates: 0  Warnings: 0

mysql> select * from test2;
+----+----------+
| id | name     |
+----+----------+
|  1 | blueicex |
|  2 | gege     |
|  3 | name1    |
|  4 | name2    |
+----+----------+
4 rows in set (0.00 sec)
mysql> rollback to savepoint a;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from test2;
+----+----------+
| id | name     |
+----+----------+
|  1 | blueicex |
|  2 | gege     |
+----+----------+
2 rows in set (0.00 sec)
#savapoint不可逆
mysql> rollback to savepoint b;
ERROR 1305 (42000): SAVEPOINT b does not exist
mysql> commit;
Query OK, 0 rows affected (0.00 sec)

6. 事务隔离级别

mysql中,innodb所提供的事务符合ACID的要求,而事务通过事务日志中的redo log和undo log满足了原子性、一致性、持久性,事务还会通过锁机制满足隔离性,在innodb存储引擎中,有不同的隔离级别,它们有着不同的隔离性。
使用show processlist语句,可以看到,已经有两个线程链接到当前数据库。

mysql> show processlist;
+----+----------+--------------+----------+---------+------+----------+------------------+
| Id | User     | Host         | db       | Command | Time | State    | Info             |
+----+----------+--------------+----------+---------+------+----------+------------------+
|  5 | root     | localhost    | blueicex | Query   |    0 | starting | show processlist |
|  9 | blueicex | master:36696 | NULL     | Sleep   |   20 |          | NULL             |
+----+----------+--------------+----------+---------+------+----------+------------------+
2 rows in set (0.00 sec)

如果两个用户对同一个表进行增删改查,就会出现事务的隔离问题。
READ-UNCOMMITTED : 此隔离级别翻译为 “读未提交”。
READ-COMMITTED : 此隔离级别翻译为 “读已提交” 或者 “读提交”。
REPEATABLE-READ : 此隔离级别翻译为 “可重复读” 或者 “可重读”。
SERIALIZABLE : 此隔离级别翻译为"串行化"。

而mysql默认设置的隔离级别为REPEATABLE-READ,即 “可重读”。使用如下语句可以查看当前设置的隔离级别。

mysql> show variables like 'tx_isolation';
+---------------+-----------------+
| Variable_name | Value           |
+---------------+-----------------+
| tx_isolation  | REPEATABLE-READ |
+---------------+-----------------+
1 row in set (0.00 sec)
6.1 隔离级别:可重读

用户1

MySQL [blueicex]> begin;
Query OK, 0 rows affected (0.00 sec)

MySQL [blueicex]> select * from test2;
+----+-----------+
| id | name      |
+----+-----------+
|  1 | blueicex  |
|  2 | liuzexuan |
+----+-----------+
2 rows in set (0.00 sec)

MySQL [blueicex]> update test2 set name='2' where id=2;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0

MySQL [blueicex]> select * from test2;
+----+----------+
| id | name     |
+----+----------+
|  1 | blueicex |
|  2 | 2        |
+----+----------+
2 rows in set (0.01 sec)

用户2

mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from test2;
+----+-----------+
| id | name      |
+----+-----------+
|  1 | blueicex  |
|  2 | liuzexuan |
+----+-----------+
2 rows in set (0.00 sec)

mysql> update test1 set name='1' where id=2;
Query OK, 0 rows affected (0.00 sec)
Rows matched: 0  Changed: 0  Warnings: 0
#不能改变,内容可重复读,两个事务,只能有一个可以改变记录内容,另一个只能读原来的数据,也就是说,一个事务用户,独占了写表。具有写锁。
mysql> select * from test2;
+----+-----------+
| id | name      |
+----+-----------+
|  1 | blueicex  |
|  2 | liuzexuan |
+----+-----------+
2 rows in set (0.00 sec)
 

用户1

MySQL [blueicex]> commit;
Query OK, 0 rows affected (0.00 sec)

MySQL [blueicex]> select * from test2;
+----+----------+
| id | name     |
+----+----------+
|  1 | blueicex |
|  2 | 2        |
+----+----------+
2 rows in set (0.00 sec)

用户1和用户2都同时提交了事务,才能看到用户1改变的数据。在可重读情况下,只允许一个事务或用户更改数据,另一个用户只能读到未提交的数据。

用户2

mysql> select * from test2;
+----+-----------+
| id | name      |
+----+-----------+
|  1 | blueicex  |
|  2 | liuzexuan |
+----+-----------+
2 rows in set (0.00 sec)
mysql> commit ;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from test2;
+----+----------+
| id | name     |
+----+----------+
|  1 | blueicex |
|  2 | 2        |
+----+----------+
2 rows in set (0.00 sec)

用户1

MySQL [blueicex]> begin;
Query OK, 0 rows affected (0.00 sec)

MySQL [blueicex]> select * from test2;
+----+----------+
| id | name     |
+----+----------+
|  1 | blueicex |
|  2 | 2        |
+----+----------+
2 rows in set (0.00 sec)

MySQL [blueicex]> insert into test2(id,name) values(3,'test');
Query OK, 1 row affected (0.00 sec)
 

用户2

mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from test3;
ERROR 1146 (42S02): Table 'blueicex.test3' doesn't exist
mysql> select * from test2;
+----+----------+
| id | name     |
+----+----------+
|  1 | blueicex |
|  2 | 2        |
+----+----------+
2 rows in set (0.00 sec)

#由于用户1没有提交任务前,用户1独占了表,所以用户2不能够更改表的内容,一直等待,直到timeout
mysql> update test2 set name='test3';

ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
mysql> 
mysql> select * from test2;
+----+----------+
| id | name     |
+----+----------+
|  1 | blueicex |
|  2 | 2        |
+----+----------+
2 rows in set (0.00 sec

但是,假如用户1和用户2同时占有一个表,用户1提交了事务,用户2并没有提交,此时,就可能在用户查询或更新同一个表时,出现幻读。幻读不影响第用户2的写的结果。

6.2 隔离级别:串行化

用户1独占数据表,用户2无法在用户1提交数据前,读写同一个数据表中的数据。此时数据库没有并发能力。

6.3 隔离级别:读已提交

两个用户占用1个表,第一个用户,提交事务前后,用户2在自己单独的事务中,都能读到用户1修改的数据的变化。这种情况下,肯定会出现幻读。
在读已提交的隔离级别下,除了会出现幻读的情况,还会出现不可重读的情况。其实,“不可重读"与"幻读"的表象都非常相似,都是在同一个事务中,并没有操作某些数据,可是这些数据却莫名的被改变了,或者突然多出了某些数 据,又或者突然少了某些数据,这些状况好像都能用"幻象"这个词去理解,所以我一开始总是分不清到底什么是幻读,而且,mysql官方文档中也把不可重读 归为幻读,只是大家为了更加的细化它们的区别,把他们分成了"不可重读"与"幻读”,如果我们实在无法分清他们,我们可以这样理解,"幻读"的重点在于莫 名其妙的增加了或减少了某些数据,"不可重读"的重点在于莫名的情况下,数据被修改更新了。

6.4 隔离级别:读未提交

事务1上变化的数据,在事务未提交前,事务2都可见。此时会出现脏读、幻读和不可重复读。
在这里插入图片描述

————Blueicex 2020/3/28 22:37 [email protected]

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章