文章目录
- 1、了解事务
- 2、事务提交
- 3、事务隔离级别
- 1、隔离性和隔离级别
- 2、查看、设置隔离级别
- 3、读未提交
- 4、读提交
- 5、可重复读
- 6、串行化
- 7、总结
- 4、事务一致性
- 5、事务隔离性
- 1、隐藏字段
- 2、undo日志
- 3、模拟MVCC
- 4、Read View
- 6、读提交RC、可重复读RR的区别
1、了解事务
MySQL内部是用多线程实现的。事务就是多个mysql语句组成的。对于用户来说这是事务,比如要查找一下以前的消费记录,要转账等,对于程序员就是多个mysql语句。事务有4个特性
4个特性统称ACID。事务并不是数据库自己有的,而是因为应用层的需求而存在的。
show engines能查看当前支持的搜索引擎,只有innodb支持事务。
事务有自、手动提交方式,可用show variable like 'autommit’来看,默认是自动提交。修改成手动提交。
set autocommit=0;
2、事务提交
为了操作事务后能够方便地看到,先把隔离级别设置成读未提交。
set global transaction isolation level READ UNCOMMITTED;//可以小写
设置好后重登一下mysql就行。查看一下
select @@tx_isolation;
查看当前有多少客户端在访问数据库
show processlist;
创建一个表用来作例子。
create table if not exists account( id int primary key, name varchar(50) not null default '', blance decimal(10,2) not null default 0.0 )ENGINE=InnoDB DEFAULT CHARSET=UTF8;
看一下当前事务提交方式
show variables like 'autocommit';
启动事务
//两种
start transaction;
begin;
开两个客户端并发访问mysql服务器。
设置一个保存点
savepoint 名字;
保存点就相当于一个位置,可以回滚到这个位置。
插入一些数据,插入之中可以再多设置一些保存点。
回滚到s3位置。
rollback to s3;
结束事务,提交事务。结束时会保存最后一次操作后的表的内容,永久。即使结束后又回滚。回滚只在事务运行期间才能回滚。
commit;
这时候再select * 会看到是最后一次操作后的表内容。如果没有保存点输入rollback就会直接回滚到最一开始,也就是数据全没了。
如果事务运行过程中,插入了数据,但是没commit,而是异常退出或者服务端退出,那就会自动回滚,插入的数据就没有插入。
默认情况下,autocommit是ON的,被开启的,但如果我们不手动commit就不会提交。autocommit不影响这个,因为我们是手动begin,手动begin就得手动commit,和autocommit没关系。
像之前写的mysql语句,并没有begin和commit但也能保存下来插入的内容,是因为默认autocommit是ON的,所以自动打开事务,退出时自动提交。如果本身是ON,在事务运行过程中变成OFF,那么得commit才能提交所有的变更。
3、事务隔离级别
1、隔离性和隔离级别
MySQL必然有多个事务同时进行,状态各自不同,最重要的就是执行中的事务不被受影响。每个事务都要看到它运行时看到的数据,而不是一定要新数据。假如有更新和读取两个事务,读取先发生,那就读取,更新后发生,那就更新,而不是读取一定要读最新的数据,这其实就是事务被隔离起来了。比如转账和查看余额,转账后,余额才能显示新数字,不转,就看不到。隔离是必要的,隔离是保证执行中的多个事务互相隔离,互不影响。隔离级别决定了隔离的程度。
MySQL有四种隔离级别,读未提交RU,读提交RC,可重复读RR,串行化S,默认为可重复读,最高为串行化。
读未提交:两个事务并发运行,一个事务更改数据,没提交,另一个事务能看到变更。
读提交:更改完提交后,其它事务才能看到。
可重复读:甲乙事务必须都提交结束,乙再新起一个事务才能查看到甲事务的数据变更,而在两者都未结束时,谁都不能看到对方的更改。
串行化:一个事务结束后另一个事务才能开始运行,没结束就都在等待,也就是很多个事务都只能等一个事务结束。
上面的隔离级别都是关于CURD的,是关于读写的,这四个并不适用于数据库所有的操作。
2、查看、设置隔离级别
查看
//查看全局隔离级别
select @@global.tx_isaolation;
//查看会话(当前)全局隔离级别
select @@session.tx_isolation;
//同上
select @@tx_isolation;
登录mysql后,会默认拷贝全局隔离级别到当前会话,如果改会话的级别,只影响会话,如果改变全局,那么以后的每次登录都会被改变隔离级别。
设置隔离级别
set session或global transaction isolation level 隔离级别;
//例子
set session transaction isolation level {read committed};
设置完后可以重登一下。
3、读未提交
先设置全局的为read-uncommitted,再手动启动事务。开两个窗口去访问mysql。begin后,一个客户端插入数据,另一个客户端就会查看到。让插入数据的客户端回滚到最一开始,另一个客户端此时就查看不到刚才插入的数据。
一个事务在执行中,读到另一个执行中事务的更新(或其它操作)但是未commit的数据,这种现象叫脏读。
4、读提交
全局改成读提交read committed。读提交是当前事务提交后,其它事务才能看到变更,无论其它事务是否结束。因为这样的特性,有可能两次select看到不一样的结果,结果会影响上层决策,这就是不可重复读的特性。
这里的问题就是一个事务提交了,其它事务即使在运行中也能看到新的变更,这是不是没有保证原子性?应该让正在运行的事务看不到,并让它们结束,再起一个事务后才看到新的变更。所以这个读提交同读未提交一样,都不推荐使用。
5、可重复读
mysql默认级别,repeatable read。事务在修改不会影响其它事务的运行。可以开两个窗口,两个客户端访问同一个表,一个事务插入,另一个事务查看。事务之间不影响,这也就是可重复读的特性。
RR级别的多个事务的更新、增加、删除都是加锁的。
一般的数据库下,无法屏蔽其它事务的insert操作,隔离性难以实现,这是因为各理性是对数据加锁来完成的,insert插入的数据并不存在,加锁无法屏蔽这类问题。所以这就会导致,即使可重复读,inset的数据在多次查找时会被查找出来,这就是幻读,在RC、RU、RR级别的问题。而MySQL解决了这个问题,是用过Next-Key锁来解决的。
6、串行化
全局设置serializable,同样可以开两个客户端,访问同一个数据库中的表来做例子。
串行化将所有的事务进行串行化。事务按照到来的顺序进行排队。多个事务并发过程中,对数据的变更需要按照顺序来做,不能同时做,也就是说,几个mysql语句会放入到一个等待队列,当前语句加锁,完成语句后,再从队列中拿出下一个语句去执行。
7、总结
隔离级别越严格,安全性越高,但数据库的并发性能也就越低,往往需要在两者之间找一个平衡点。
不可重复读的重点是修改和删除:同样的条件,你读取过的数据,再次读取出来发现值不一样了。
幻读的重点在于新增:同样的条件,第1次和第2次读出来的记录数不一。一般情况下mysql默认的RR级别尽量不要改。
事务也有长短事务这样的概念。事务间互相影响,指的是事务在并行执行的时候,即都没有commit的时候,影响会比较大。
4、事务一致性
MySQL对于一致性并没有做什么,一致性是和事务相关的。
事务执行的结果,必须使数据库从一个一致性状态,变到另一个一致性状态。当数据库只包含事务成功提交的结果时,数据库处于一致性状态。如果系统运行发生中断,某个事务尚未完成而被迫中断,而改未完成的事务对数据库所做的修改已被写入数据库,此时数据库就处于一种不正确(不一致)的状态。因此一致性是通过原子性来保证的。
一致性和用户的业务逻辑强相关,一般MySQL提供技术支持,但是一致性还是要用户业务逻辑做支撑,也就是,一致性,是由用户决定的。
在技术层面上,MySQL实现了原子性、持久性、隔离性,通过这三个保证了一致性,即AID保证C。数据库提供技术,而真正的实现由用它的人来共同维护。
5、事务隔离性
数据库并发的场景有3种,读读并发,读写并发,写写并发。读读不需要并发控制。读写并发是主要的问题。
读写并发采用的是多版本并发控制(MVCC)的无锁并发机制来就解决读写冲突。
MySQL为每个事务分配一个单向增长的事务ID,ID越小,事务来得越早。mysqld对于事务的管理是先描述再组织,也就是要么是一个结构体要么是一个类,因为mysqld是用C++写的,另外事务也有自己的结构体。
1、隐藏字段
建表时,有一些字段是默认加上去的。
DB_TRX_ID:6 byte,最近修改( 修改/插入 )事务ID,记录创建这条记录/最后一次修改该记录的事务ID。初始为null。
DB_ROLL_PTR:7 byte,回滚指针,指向这条记录的上一个版本(简单理解成,指向历史版本就行,这些数据一般在 undo log 中)。在改数据前,先保存一份,这样就可以回滚了。默认为1。
DB_ROW_ID:6 byte,隐含的自增ID(隐藏主键),如果数据表没有主键, InnoDB 会自动以DB_ROW_ID 产生一个聚簇索引。这个ID是隐藏的,对应着隐藏的主键,需要用这个隐藏的主键才能用到这个隐藏的索引,所以当我们没有设置主键时,是不会动用这个隐藏索引的。默认为null。
另外有一个字段是flag,用来表示删除的状态,更新或者删除不是真的删除,而是flag变了。
2、undo日志
mysql服务启动时会有自己的一个内存缓冲区,其中有一块空间是undo log。所以undo log就是mysql维护的一块内存缓冲区,以守护进程的形式运行,用来保存日志数据。
3、模拟MVCC
假设有个事务7,要把stu表中记录进行修改,name张三改成name李四。
因为事务7要修改,所以对表的记录先加锁保护;将要修改的记录拷贝到undo log中,此时原记录中回滚指针里存的就是undo log中本记录存储的地址;在原表中修改,修改后事务ID更新为7;最后提交,释放锁。
下一个事务开始运行时,也是一样的步骤,回滚指针需要更新,事务ID需要更新。undo log里不断添加拷贝过来的记录,记录之间可通过回滚指针来访问到,这就构成了一个版本链。
回滚时就是通过回滚指针,拿到undo log中对应的记录,覆盖一下即可。如果是insert记录,那么放到undo log中的记录可以变成相反的记录,delete,这样回滚时就能返回之前的记录了。对于不同的操作,mysql的回滚有对应的操作。
undo log中的一个个记录,就是一个个版本,也叫快照,多版本就是MVCC机制。当一个事务提交后,和这个事务相关的快照全都释放。
update和delete有版本链,insert并没有版本,但它要插入的数据也会存在undo log里,存的形式是delete,所以回滚的时候就是在删除之前插入的数据。
对于当前版本的update,delete,select都叫当前读,select可以当前读,也可以快照读,也就是读历史版本。隔离级别决定了select如何读。如果删改都是当前读,select是快照读,那么它们之间就不受加锁限制,效率更高;如果三者都是当前读,那么就得串行化执行。所以MySQL默认是可重复读级别。
事务之间有顺序,并发执行,所以各种操作必然有交错,这就是隔离性和隔离级别要解决的问题。
4、Read View
Read View是事务在快照读时生成的读视图。读视图本质是一个类,用来做可见性判断。当某个事务进行快照读时,对该记录创建一个读视图,用读视图中的字段来判断能看到哪些版本的数据。
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;
/** 标记视图是否被关闭*/
bool m_closed;
// 省略...
};
m_ids; //一张列表,用来维护Read View生成时刻,系统正活跃的事务ID
up_limit_id; //记录m_ids列表中事务ID最小的ID
low_limit_id; //ReadView生成时刻系统尚未分配的下一个事务ID,也就是目前已出现过的系统的事务ID的最大值+1
creator_trx_id //创建该ReadView的事务ID
源码
读视图是事务可见性的一个类,不是事务创建出来时就有读视图,而是当事务已经存在并首次进行快照读时,mysql才形成读视图。
例子
所以事务4的更改,应该看到。事务2能读到的最新数据记录是事务4所提交的版本,而事务4提交的版本也是全局角度上最新的版本
6、读提交RC、可重复读RR的区别
RR级别下,两个事务同时进行,第二个事务开启后先select一下表;第一个事务更改,另一个再select就看不到了,这时可以写select * from 表名 lock in share mode让这个事务做当前读。如果第二个事务等到第一个事务更改完并提交后再第一次select那么就可以看到了,不需要多谢lock…。第一种情况读视图形成时,读视图认为两个事务都在运行,所以不能看到;第二种情况读视图形成前第一个事务就结束了,所以此时对于读视图来说,第一个事务是之前的事务,可以查看。
所以开始快照读的时机很重要,这个影响了快照读的能力。
在RR级别下的某个事务的对某条记录的第一次快照读会创建一个快照及Read View,将当前系统活跃的其他事务记录起来;此后在调用快照读的时候,还是使用的是同一个Read View,所以只要当前事务在其他事务提交更新之前使用过快照读,那么之后的快照读使用的都是同一个Read View,所以对之后的修改不可见;即RR级别下,快照读生成Read View时,Read View会记录此时所有其他活动事务的快照,这些事务的修改对于当前事务都是不可见的。而早于Read View创建的事务所做的修改均是可见。
在RC级别下,事务中,每次快照读都会新生成一个快照和Read View,这就是在RC级别下的事务中可以看到别的事务提交的更新的原因
在RC隔离级别下,每个快照读都会生成并获取最新的Read View;而在RR隔离级别下,则是同一个事务中的第一个快照读才会创建Read View,之后的快照读获取的都是同一个Read View。RC每次快照读,都会形成Read View,所以,RC才会有不可重复读问题。
结束。