看下如下2个demo,关闭了自动事务,select和update语句会自动给users表加上S锁(共享锁)和X锁(排它锁)。S锁和X锁是互斥的,按理说先执行的事务未commit或者rollback之前,另一个事务是无法对users表加互斥锁进行操作的,但是这里两个demo却可以正常执行。
demo1:
transaction1:
set session autocommit=off;
update users set lastUpdate=now() where id =1;//加x锁,使用记录锁Record锁
transaction2:
select * from users where id > 1;//未锁住,可以执行是正常的。
select * from users where id= 1;//按理说条数据行应该锁住,不能加S锁才对啊,为什么可以加S锁呢?
demo2:
transaction1:
begin
select * from users where id= 1;
transaction2:
update users set lastUpdate=now() where id =1;
transaction1:
select * from users where id= 1;
一 MVCC
MVCC (Mutil-Version Concurrency ControL),多版本并发控制。并发访问(read/write)数据库时,对正在事务内处理的数据做多版本的管理,避免因写操作堵塞而引发读操作的并发问题。
建一张简单的表,有如下三个字段,我们看到的结构如下。
主键 姓名 年龄
---------------------------------
| id | name | age |
----------------------------------
| | | |
----------------------------------
实际上MySQL底层还会帮助我们多建立2个字段:trx_id 和 roll_pointer,它们的取值均来自于全局事务ID生成器。
trx_id 用于记录
主键 姓名 年龄 数据行版本号 删除版本号
-------------------------------------------------------------
| id | name | age | trx_id | roll_pointer
-----------------------------------------------------------
| | | | |
------------------------------------------------------------
我们对表插入数据:
begin;//自动获取当前的全局事务ID,假设这里ID=1
insert into users (name,age) values('tom',18);
insert into users (name,age) values('lily',18);
commit;
那么sql执行后的实际上的表内容如下:
主键 姓名 年龄 数据行版本号 删除版本号
-------------------------------------------------------------
| id | name | age | trx_id | roll_pointer
-----------------------------------------------------------
| 1 | tom | 18 | 1 | null
------------------------------------------------------------
| 2 | lily | 18 | 1 | null
------------------------------------------------------------
删除数据
begin;//自动获取当前的全局事务ID,假设这里ID=22
delete users where id=2;
commit;
那么sql执行后的实际上的表内容如下:
主键 姓名 年龄 数据行版本号 删除版本号
-------------------------------------------------------------
| id | name | age | trx_id | roll_pointer
-----------------------------------------------------------
| 1 | tom | 18 | 1 | null
------------------------------------------------------------
| 2 | lily | 18 | 1 | 22
------------------------------------------------------------
看下修改操作:
begin;//自动获取当前的全局事务ID,假设这里ID=33
update users set age =20 where id = 1;
commit;
那么sql执行后的实际上的表内容如下:
主键 姓名 年龄 数据行版本号 删除版本号
-------------------------------------------------------------
| id | name | age | trx_id | roll_pointer
-----------------------------------------------------------
| 1 | tom | 18 | 1 | 33
------------------------------------------------------------
| 2 | lily | 18 | 1 | 22
------------------------------------------------------------
| 1 | tom | 19 | 33 | null
------------------------------------------------------------
update过程相当于如下两步:
1.将命中的数据行复制一份修改,包括业务数据age=19和trx_id=33
2.原来的数据roll_pointer标记为当前事务ID33。
基于前面的步骤,看下查询操作。
begin;//自动获取当前的全局事务ID,假设这里ID=44
select * from users;
commit;
查询规则:
1.查找数据行版本号早于当前事务版本的数据行,即版本号小于当前事务id的数据行。
这样可以保证当前事务读取的数据行,要么是事务开始前已经存在的,要么是事务自身插入后者修改过的。
2.查找删除版本号要么为null,要么要么大于当前事务版本号的数据行。
这样可以保证数据行在事务开启前没有被删除。
根据规则1,三条记录都符合,根据规则2只有第三条数据符合,查询结果如下:
主键 姓名 年龄 数据行版本号 删除版本号
-------------------------------------------------------------
| id | name | age | trx_id | roll_pointer
-----------------------------------------------------------
| 1 | tom | 19 | 33 | null
------------------------------------------------------------
UndoLog
事务开始之前,首先将命中的数据备份到一个undolog中。undolog是为了实现事务原子性而产生的,当事务过程中出现了异常或者执行了rollback,就可以利用undolog中的备份将数据恢复到事务开始之前的状态。
InnoDB存储引擎利用undolog中的备份数据作为快照,供其他并发事务进行读操作。
快照读:
sql读取的数据是快照版本,也就是历史版本,普通的select就是快照读,InnoDB快照读由cache + undo两部分组成。快照读通过MVCC解决幻读问题。
当前读:
sql读取的数据是最新版本,UPDATE,DELETE,INSERT,SELECT …LOCK IN SHAR MODE,SELECT … FOR UPDATE都是当前读。通过临键锁NEXT-KEY锁机制解决幻读问题。
Redo log
事务操作数据时,将最新的数据备份到redo log中。redo log是为了实现事务的持久性而出现的产物。