此专栏所有章节快速导航
01_MySQL数据库_CentOS7安装MySQL
02_MySQL数据库_数据库基础知识
03_MySQL数据库_库的操作
04_MySQL数据库_表的操作
05_MySQL数据库_数据类型
06_MySQL数据库_表的约束
07_MySQL数据库_增删查改
08_MySQL数据库_复合查询
09_MySQL数据库_索引
10_MySQL数据库_事务管理
11_MySQL数据库_用户管理
12_MySQL数据库_CentOS7下C-C++链接MySQL
文章目录
一. 什么是事务?
1.1 简述
事务(transaction)是数据库中完成一件事需要的所有sql语句的集合,这些sql语句少执行一个这件事都不能完成,其实我们之前写的sql都是事务,只是他们的sql集合就只有一条sql语句。 比如说我们日常的银行转钱,a转给b100元,在数据库操作时这件事最起码得有两个操作吧,一个是在a的账户上-100,另一个是在b的账号上+100,这两个操作在数据库上就是一个事务。一个完整的事务不仅包括这个事务的sql语句集合,还应该满足4个特性,分别是原子性、持久性、隔离性、一致性。
1.2 事务的四大属性
原子性: 一个事务中的所有操作,要么全部完成,要么全部不完成。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。
持久性: 事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。
隔离性: 数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。事务隔离分为不同级别,包括读未提交、读提交、可重复读和串行化。
一致性(Consistency): 我的理解是我们执行一个事务,执行后的结果是跟自然客观下的预期结果是一致的。一致性是事务的最终目的,原子性、隔离性、持久性都是为了实现一致性。
MySQL
的语法规则和我们人为的正确操作共同去维护事务的一致性。
1.3 事务的提交方式
1.3.1 创建测试表
后面的相关测试也用这张表
# 创建表
create table if not exists account
(
id int unsigned primary key auto_increment,
name varchar(16) not null,
blance decimal(10, 2) not null default 0.0
)engine=InnoDB default charset=utf8;
# 插入数据
insert into account(name, blance) values('张益达', 10000), ('吕小布', 4000);
1.3.1 手动提交
题目:张益达给吕小布转账2000元
# 开始一个事务
begin;
# 转账
update account set blance = blance-2000 where id=1;
update account set blance = blance+2000 where id=2;
# 提交事务
commit; # 不提交事务,本次的操作将会是无效的
1.3.2 自动提交
自动提交就是不用我们
commit
,其实我们平常写的sql
命令执行完之后就是自动提交的。一旦进入begin
之后我们写到语句,对表更改后,都需要用commit
手动提交上去。
1.4 事务的回滚
1.4.1 回滚演示
如下面操作所示,一开始我们插入张三的这条记录,用
savepoint
保存了当前的状态为s1
。然后再插入李四这条记录,插入后我们用rollback to
回到s1
状态,最后我们提交事务查看能看到张三的这条记录,没有李四的。
begin;
insert into account(name, blance) values('张三', 60000);
savepoint s1; # 设置一个保存点s1,保存当前状态
insert into account(name, blance) values('李四', 80000);
rollback to s1; # 回滚到s1状态
commit;
1.4.2 回滚的总结
- 事务中我们可以在任何时刻保存当前状态,后面需要回滚时可以回滚到这些状态上。
- 事务开始后会自动保存一个初始状态,单执行
rollback
就可以回滚到初始状态。- 一旦
commit
事务了,就不能回滚了
二. 事务的隔离性
2.1 隔离级别
2.2.1 简述
为了保证事务执行过程中尽量不受干扰,就有了一个重要特征,隔离性;数据库中,允许事务受不同程度的干扰,就有了一种重要特征,隔离级别。
2.2.2 四大隔离级别
读未提交【Read Uncommitted】: 在该隔离级别,所有的事务都可以看到其他事务没有提交的执行结果。相当于没有任何隔离性,也会有很多并发问题,如脏读,幻读,不可重复读等,
读提交【Read Committed】 :该隔离级别是大多数数据库的默认的隔离级别,它满足了隔离的简单定义。一个事务只能看到其他的已经提交的事务所做的改变。这种隔离级别会引起不可重复读,即一个事务执行时,如果多次
select
, 可能得到不同的结果。可重复读【Repeatable Read】: 这是 MySQL 默认的隔离级别,它确保同一个事务,在执行中,多次读取操作数据时,会看到同样的数据行,但是会有幻读问题。
串行化【Serializable】: 这是事务的最高隔离级别,它通过强制事务排序,使之不可能相互冲突,从而解决了幻读的问题。它在每个读的数据行上面加上共享锁,但是可能会导致超时和锁竞。读读不会串行化。
2.2.3 脏读和幻读
脏读: 一个事务在处理过程中读取了另外一个事务未提交的数据。
幻读: 一般的数据库在可重复读情况的时候,无法屏蔽其他事务
insert
的数据。因为隔离性实现是对数据加锁完成的,而insert
待插入的数据因为并不存在,那么一般加锁无法屏蔽这类问题。其他事务insert
的数据在可重复读情况被读取出来,导致多次查找时,会多查找出来新的记录,就如同产生了幻觉。这种现象,叫做幻读(phantom read)。MySQL
通过一系列锁的加持下,在可重复读隔离级别下也是没有幻读的。
2.2.4 更改隔离级别
select @@global.tx_isolation; # 查看全局隔级别
SELECT @@session.tx_isolation; # 查看会话(当前)全局隔级别
SELECT @@tx_isolation;
# global和session关键字用来指定修改的事务隔离级别的范围
# session:表示修改的事务隔离级别将应用于当前cmd窗口内的所有事务
# global:表示修改的事务隔离级别将应用于所有所有事务
# 如果省略session和global,表示修改的事务隔离级别将应用于当前session内的下一个还未开始的事务
# 将隔离级别设置为读提交,全局有效
set global transaction isolation level read committed; # 设置全局的需要重新连接服务器才生效
三. 隔离性的深度解剖
3.1 数据库事务并发的场景
读-读 : 不存在任何问题,也不需要并发控制
读-写 : 有线程安全问题,可能会造成事务隔离性问题,可能遇到脏读,幻读,不可重复读。
写-写 : 有线程安全问题,可能会存在更新丢失问题。
3.2 多版本并发控制(MVCC)
3.2.1 简述
MVCC(Multi-Version Concurrency Control),是一种用来解决读-写冲突的无锁并发控制。为事务分配单向增长的事务
ID
,为每个修改保存一个版本,版本与事务ID
关联,读操作只读该事务开始前的数据库的快照。 在并发读写数据库时,可以做到在读操作时不用阻塞写操作,写操作也不用阻塞读操作,提高了数据库并发读写的性能。同时还可以解决脏读,幻读,不可重复读等事务隔离问题,但不能解决更新丢失问题。
3.2.2 三个隐藏字段
创建表时,数据库会自动生成三个隐藏字段,这些字段主要用来实现MVCC
DB_TRX_ID: 记录创建这条记录或者最后一次修改该记录的事务
ID
DB_ROLL_PTR: 回滚指针,指向这条记录的上一个版本,这些版本数据一般在undo log中
DB_ROW_ID : 隐含的自增
ID
(隐藏主键),如果数据表没有主键,InnoDB
会自动以DB_ROW_ID
产生一个聚簇索引。
# 创建一个表
create table if not exists student
(
name varchar(11) not null,
age int not null
);
insert into student values('张三', 28);
我们看到的
name | age |
---|---|
张三 | 28 |
实际上是这样的
name | age | DB_TRX_ID | DB_ROW_ID | DB_ROLL_PTR |
---|---|---|---|---|
张三 | 28 | null | 1 | null |
name | age | DB_TRX_ID | DB_ROW_ID | DB_ROLL_PTR |
---|---|---|---|---|
李四 | 38 | 11 | 1 | 0x12345688 |
3.2.3 undo log
以上面的
student
表为例,有一个事务ID
为10
的事务将张三改成李四并且提交,此时该条记录的隐藏字段就会记录下10
号事务的ID
,回滚指针也会记录下修改之前的这条记录的一个快照的地址0x12345678
,这个快照被放进undo_log
里面。
此时又来一个事务
ID
为11
,将年龄改成38
,此时该条记录的隐藏字段就会记录下11
号事务的ID
,回滚指针也会指向上一个状态的快照地址。我们可以看到undo_log
里面形成了一个历史版本链。
2.2.4 当前读和快照读
当前读: 读取当前的最新记录,也就是上图
undo_log
外面的那条记录。增删改,都叫做当前读,select
也有可能当前读。比如执行语句select * from student lock in share mode
快照读: 读取历史的记录,只能读取
Read View
生成之前提交的数据。在多个事务同时删改查的时候,都是当前读,是要加锁的。那同时有
select
过来,如果也要读取最新版(即当前读),那么也就需要加锁,这就是串行化。如果是快照读,读取历史版本的话,是不受加锁限制的。也就是可以并行执行。MVCC
的意义所在,就是在快照读时不上锁,提高了效率。隔离级别决定了select
是当前读还是快照读。
2.2.5 Read View
Read View
就是事务第一次进行快照读操作的时候生产的读视图,在该事务执行的快照读的那一刻,会生成数据库系统当前的一个读视图,记录并维护系统当前活跃事务的ID
。Read View
在MySQL
源码中,就是一个类,用来进行可见性判断的,下面我们看一下这个类的部分成员变量。
class ReadView {
// 省略...
private:
/** 高水位,大于等于这个ID的事务均不可见*/
/** ReadView生成时刻系统尚未分配的下一个事务ID,也就是目前已出现过的事务ID的最大值+1*/
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;
// 省略...
};
2.2.6 读提交和可重复读的本质区别
前面我们说了,一个事务首次快照读会生成一个读视图
Read View
。如下图所解释的那样,Read View
会对改事务后面的快照读有很大的影响。把Read View
比作条件,用来判断当前事务能够看到哪个版本的数据,既可能是当前最新的数据,也有可能是该行记录的undo log
里面的某个版本的数据。在RR(可重复读)模式下,这个Read View
一旦生成就不会改变了。在RC(读提交)模式下,每一次快照读时都会更新Read View
,RC才会有不可重复读问题。