事务就是一组DML语句组成,这些语句在逻辑上存在相关性,这一组DML语句要么全部成功,要么全部失败,是一个整体。
由于事务存在多个事务都访问同样的表数据、执行到一半出错或者不想再执行的情况,因此,一个完整的事务,如果只是简单的SQL语句集合,就会出现很多问题,因此事务还需要满足如下四个属性:
上面四个属性,可以简称为 ACID :
总结:
满足ACID属性的多条并在逻辑上存在相关性的SQL语句组成的集合称为事务。
事务不是伴随着数据库系统天生就有的,事务被设计出来,本质是为了当应用程序访问数据库的时候,事务能够简化我们的编程模型, 不需要我们去考虑各种各样的潜在错误和并发问题,因此事务本质上是为了应用层服务的。
备注:我们后面把 MySQL 中的一行信息,称为一行记录。
在 MySQL 中只有使用了 Innodb 数据库引擎的数据库或表才支持事务, MyISAM 不支持。
可以查看数据库引擎来获取是否支持事务的信息:
事务的提交方式常见的有两种:
查看事务自动提交是否打开:
show variables like 'autocommit';
事务自动提交的打开与关闭:
SET AUTOCOMMIT=0; #禁止自动提交
SET AUTOCOMMIT=1; #开启自动提交
测试准备:
为了更好地对事务进行实验,做了以下准备:
设置完之后需要重启客户端才能生效。
事务正常操作测试1:
由于s3保存点设置在插入第三条记录之前,因此回滚到s3保存点第三条记录会消失。
由于s1保存点设置在插入第一条记录之前,因此回滚到s1保存点之前插入的所有记录都会消失。
事务正常操作测试2:
由于没有保存点,会直接回滚到事务开始,因此插入的数据都会消失。
事务正常操作测试3:
事务已经提交了,回滚操作就无法生效了。
事务异常操作测试1:
事务未提交,客户端崩溃,MySQL自动会回滚(隔离级别设置为读未提交),因此插入的数据消失了。
事务异常操作测试2:
事务未提交,客户端关闭,MySQL自动会回滚(隔离级别设置为读未提交),因此插入的数据消失了。
事务异常操作测试3:
事务提交了,客户端崩溃,MySQL数据不会在受影响,已经持久化。
说明:
事务异常操作测试4:
事务异常操作测试5:
说明:
总结:
事务操作注意事项:
MySQL可以能被多个客户端访问,多个客户端可能访问相同的数据,这些操作都由事务完成,一个事务可能又包含多个SQL语句,事务的并发执行可能会出现问题,为了保证事务执行过程中尽量不受干扰,就有了一个重要特征:隔离性。 数据库中,允许事务受不同程度的干扰,就有了一种重要特征:隔离级别。
隔离级别:
SELECT @@global.tx_isolation; -- 查看全局隔离级别
SELECT @@session.tx_isolation; -- 查看会话(当前)全局隔离级别
SELECT @@tx_isolation; -- 默认同上
查看全局隔离级别:
查看会话隔离级别:
SET [SESSION | GLOBAL] TRANSACTION ISOLATION LEVEL {READ UNCOMMITTED | READ COMMITTED | REPEATABLE READ | SERIALIZABLE} -- 设置当前会话 or 全局隔离级别
设置当前会话的隔离级别测试:
由于原会话修改的当前会话的隔离级别,因此不影响新的会话的隔离级别。
设置全局隔离级别测试:
设置全局隔离级别后重启的客户端会话隔离级别才会跟着改变。
读未提交是所有的事务都可以看到其他事务没有提交的执行结果。
读未提交测试
rollback就相当于事务结束了。
测试出现的现象: 一个事务在执行中,读到另一个执行中事务的数据操作但是未commit的数据,这种现象叫做脏读 (dirty read)。
读提交是事务只能看到其他的已经提交的事务所做的改变。
读提交测试:
测试出现的现象: 同一个事务内,同样的读取,在不同的时间段 (依旧还在事务操作中),读取到了不同的值,这种现象叫做不可重复读(non reapeatable read)。
可重复读是同一个事务,在执行 中,多次读取操作数据时,会看到同样的数据行。
可重复读测试:
补充一下: insert的数据在可重复读情况被读取出来,导致多次查找时,会多查找出来新的记录,叫做幻读 (phantom read)。MySQL在可重复读情况不会出现幻读。
串行化是强制事务排序,多个事务必须串行化执行,使事务之间不可能相互冲突。
可串行化测试:
对MySQL中的隔离级别总结如下:
隔离级别 | 脏读 | 不可重复读 | 幻读 | 加锁读 |
---|---|---|---|---|
读未提交(read uncommitted) | √ | √ | √ | 不加锁 |
读已提交(read committed) | × | √ | √ | 不加锁 |
可重复读(repeatable read) | × | × | × | 不加锁 |
可串行化(serializable) | × | × | × | 加锁 |
√:会发生该问题
×:不会发生该问题
事务执行的结果,必须使数据库从一个一致性状态,变到另一个一致性状态,事务执行前后,数据库中只包含成功事务提交的结果。
也就是说,一致性实际是数据库最终要达到的效果,一致性不仅需要原子性、持久性和隔离性来保证,还需要上层用户编写出正确的业务逻辑。
数据库并发的场景有三种:
读-读
:不存在任何问题,也不需要并发控制 。读-写
:有线程安全问题,可能会造成事务隔离性问题,可能遇到脏读,幻读,不可重复读。写-写
:有线程安全问题,可能会存在更新丢失问题。多版本并发控制( MVCC )是一种用来解决 读-写冲突 的无锁并发控制。
关于事务:
关于表结构:
MySQL中每条记录中存在三个隐藏属性列字段:
DB_TRX_ID
:6 byte,记录创建这条记录/最后一次修改该记录的事务ID 。DB_ROLL_PTR
: 7 byte,回滚指针,指向这条记录的上一个版本。DB_ROW_ID
: 6 byte,隐含的自增ID(隐藏主键),如果数据表没有主键, InnoDB 会自动以 DB_ROW_ID 产生一个聚簇索引。**关于表结构补充: **实际还有一个删除flag隐藏字段, 记录被更新或删除,由于MySQL服务是在内存级的进程,因此删除表中只需修改记录的标志位,然后在和磁盘进行IO时使得数据库的数据保持一致。
给出如下表结构和表数据:
实际在MySQL中的表结构和数据:
name | aeg | DB_TRX_ID | DB_ROW_ID | DB_ROLL_PTR |
---|---|---|---|---|
张三 | 28 | 9(示例) | 1(示例) | null(示例) |
Undo日志:
Undo日志时MySQL 中的一段内存缓冲区,用来保存日志数据的。
现在有一个事务10,对student表中记录进行修改 – 将name(张三)改成 name(李四):
说明:
**MySQL会根据Undo日志生成"反操作"的SQL语句,执行这些SQL语句就能实现用历史数据,覆盖当前数据形成的回滚操作。**对于"反操作"语句举个例,比如Undo日志中记录的是插入操作,反操作记录的是删除操作。
由于多个事务会使用很多条SQL语句,Undo日志中会因此记录许多历史版本的记录,对于一个个版本,可以称之为一个个的快照。
插入、更新、删除数据都会形成历史版本。
当前读:读取最新的记录,就是当前读,增删改,都叫做当前读。
快照读:读取历史版本的记录。
select有可能当前读,也可能是快照读。
给出如下结论:
MVCC实现隔离性、不加锁的读写并发的原理就是让事务访问不同历史版本的记录。
Read View就是事务进行 快照读 操作的时候生产的 读视图。
ReadView简化后的源码如下:
class ReadView {
// 省略...
private:
/** 高水位:大于等于这个ID的事务均不可见*/
trx_id_t m_low_limit_id;
/** 低水位:小于这个ID的事务均可见 */
trx_id_t m_up_limit_id;
/** 创建该 Read View 的事务ID*/
trx_id_t m_creator_trx_id;
/** 创建视图时的活跃事务id列表*/
ids_t m_ids;
/** 配合purge,标识该视图不需要小于m_low_limit_no的UNDO LOG,
* 如果其他视图也不需要,则可以删除小于m_low_limit_no的UNDO LOG*/
trx_id_t m_low_limit_no;
/** 标记视图是否被关闭*/
bool m_closed;
// 省略...
};
部分成员变量说明:
在Undo日志中历史版本的数据形成了版本链,版本链中存有操作数据的事务id,利用版本链和READ VIEW中记录的事务id信息来判断快照读能否读取。
由于事务ID是单向增长的,因此根据Read View中的m_up_limit_id和m_low_limit_id,可以将事务ID分为三个部分:
示意图如下:
根据READ VIEW判断满足以下条件是能看到情况:
源码策略如下:
bool changes_visible(trx_id_t id, const table_name_t& name) const
MY_ATTRIBUTE((warn_unused_result))
{
ut_ad(id > 0);
//1、事务id小于m_up_limit_id(已提交)或事务id为创建该Read View的事务的id,则可见
if (id < m_up_limit_id || id == m_creator_trx_id) {
return(true);
}
check_trx_id_sanity(id, name);
//2、事务id大于等于m_low_limit_id(生成Read View时还没有启动的事务),则不可见
if (id >= m_low_limit_id) {
return(false);
}
//3、事务id位于m_up_limit_id和m_low_limit_id之间,并且活跃事务id列表为空(即不在活跃列表中),则可见
else if (m_ids.empty()) {
return(true);
}
const ids_t::value_type* p = m_ids.data();
//4、事务id位于m_up_limit_id和m_low_limit_id之间,如果在活跃事务id列表中则不可见,如果不在则可见
return (!std::binary_search(p, p + m_ids.size(), id));
}
说明一下: MySQL调用该函数时会将该版本的DB_TRX_ID传给参数id,判断当前事务能否看到这个版本。
READ VIEW示例
给出以下记录:
name | aeg | DB_TRX_ID | DB_ROW_ID | DB_ROLL_PTR |
---|---|---|---|---|
张三 | 28 | null | 1 | null |
给出如下事务操作(表格从上到下代表时间线):
事务1 [id=1] | 事务2 [id=2] | 事务3 [id=3] | 事务4 [id=4] |
---|---|---|---|
事务开始 | 事务开始 | 事务开始 | 事务开始 |
… | … | … | 修改且已提交 |
进行中 | 快照读 | 进行中 | |
… | … | … |
事务2的 Read View字段:
m_ids; // 1,3 -- 生成快照时事务4已经提交了
up_limit_id; // 1
low_limit_id; // 4 + 1 = 5
creator_trx_id // 2
此时的版本链:
事务2进行快照读会循环调用changes_visible函数,将版本链中的DB_TRX_ID依次传入,判断能否读到该版本的数据,直到找到能读取的版本为止:
//事务2的 Read View
m_ids; // 1,3
up_limit_id; // 1
low_limit_id; // 4 + 1 = 5,原因:ReadView生成时刻,系统尚未分配的下一个事务ID
creator_trx_id // 2
//事务4提交的记录对应的事务ID
DB_TRX_ID=4
//比较步骤
DB_TRX_ID(4)< up_limit_id(1) ? 不小于,下一步
DB_TRX_ID(4)>= low_limit_id(5) ? 不大于,下一步
m_ids.contains(DB_TRX_ID) ? 不包含,说明,事务4不在当前的活跃事务中,因此事务4的版本可读。
//结论
事务4的更改,应该看到。
所以事务2能读到的最新数据记录是事务4所提交的版本,而事务4提交的版本也是全局角度上最新的版本
测试示例1:
select * from user lock in share mode
,以加共享锁方式进行读取,对应的就是当前读。
测试示例2:
在只进行查询操作的会话中,由于事务中多次读取的结果相同,因此不违背可重复读的隔离级别。
测试结论:
RR 与 RC的本质区别