事务特性

事务特性:ACID(Atomicity 即原子性、Consistency 一致性、Isolation 隔离性、Durability 持久性)

事务可能存在的问题

可能出现的问题:脏读(dirty read)、幻读(phantom read)、不可重复读(non-repeatable read);

  • 脏读:一个事务里出现了其他事务未提交的数据
  • 不可重复读: 一个事务里两次读取的数据不一致(其他事务在两次读取数值之间提交了变更)
  • 幻读:

不可重复读和幻读的区别:侧重点不同,不可重复读描述的侧重点是修改操作,而幻读描述的侧重点是添加和删除操作。

引用:保姆级教程,终于搞懂脏读、幻读和不可重复读了!

事务隔离级别

隔离级别:读未提交(read uncommitted)、读已提交(read committed)、可重复读(repeatable read)和串行化(serializable)

  • 读未提交:一个事务还没提交时,它做的变更就能被别的事务看到。
  • 读提交:一个事务提交之后,它做的变更才会被其他事务看到。
  • 可重复读:一个事务执行过程中看到的数据,总是跟这个事务在启动时看到的数据是一致的。当然在可重复读隔离级别下,未提交变更对其他事务也是不可见的。
  • 串行化读:对于同一行记录,“写”会加“写锁”,“读”会加“读锁”。当出现读写锁冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行。

数据库里面会创建一个视图,访问的时候以视图的逻辑结果为准。
“读未提交”隔离级别下,直接返回记录上的最新值,没有视图概念
“读已提交”隔离级别下,这个视图是在每个 SQL 语句开始执行的时候创建的
“可重复读”隔离级别下,这个视图是在事务启动时创建的,整个事务存在期间都用这个视图
“串行化”隔离级别下,直接用加锁的方式来避免并行访问

事务隔离级别演示

初始化

1
2
3
4
5
6
7
8
9
10
11
CREATE TABLE `test`  (
`id` int NOT NULL AUTO_INCREMENT,
`name` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
`age` tinyint UNSIGNED NULL DEFAULT NULL,
`sex` tinyint NULL DEFAULT NULL,
`is_deleted` bit(1) NULL DEFAULT b'0',
`money` int NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB ;

INSERT INTO `test` VALUES (1, 'name1', 11, 1, b'0', 100);

下面的可以查看连接情况和事务隔离级别

1
2
3
show processlist;
# (5.7+版本隔离级别)
select @@global.transaction_isolation,@@transaction_isolation;

读未提交- 脏读

新建两窗口

步骤 窗口1 窗口2 说明
1 set session transaction isolation level read uncommitted;
start transaction;
SELECT money from test where id=1;
开启隔离级别未提交,启动事务,查询得到money值为100
2 start transaction;
update test set money=101 where id =1;
默认隔离级别可重复读,开启事务,修改money为101
3 SELECT money from test where id=1; 查询数据得到Money=101
4 rollback; 回滚
5 SELECT money from test where id=1; 查询数据得到Money=100

以上得知:实际上窗口2并没有更新成功,但是窗口1在第二步查询到了修改的脏数据

完成之后记得提交或回滚窗口1

读已提交 - 不可重复读

新建两窗口

步骤 窗口1 窗口2 说明
1 set session transaction isolation level read committed;
start transaction;
SELECT money from test where id=1;
开启隔离级别读已提交,启动事务,查询得到money值为100
2 start transaction;
update test set money=101 where id =1;
默认隔离级别可重复读,开启事务,修改money为101
3 SELECT money from test where id=1; 查询数据得到Money=100 未出现脏读
4 commit; 客户端2提交变更
5 SELECT money from test where id=1; 查询数据得到Money=101

以上得知,窗口1更新了数据,但是窗口2的更新提交之后窗口1查询两次的值不一样,因此不可重复读

完成之后记得提交或回滚窗口1

可重复读 - 幻读

插入两条数据

1
2
INSERT INTO `test` VALUES (11, 'name11', 11, 1, b'0', 100);
INSERT INTO `test` VALUES (13, 'name13', 11, 1, b'0', 100);

新建两窗口

步骤 窗口1 窗口2 说明
1 set session transaction isolation level read committed;
start transaction;
select money from test where id=2
开启隔离级别读已提交,启动事务,未得到id=2的数据
2 start transaction;
INSERT INTO test VALUES (2, 'name2', 11, 1, b'0', 100);
commit;
默认隔离级别可重复读,开启事务,插入id 为2的数据,并提交
3 SELECT money from test where id=2; 查询数据未得到id=2的数据,说明未出现不可重复读
4 INSERT INTO test VALUES (2, 'name2', 11, 1, b'0', 100);
SELECT money from test where id=2;
插入报错Duplicate entry '2' for key 'test.PRIMARY'已经存在,窗口1查询没有数据,但是插入数据报已存在,出现了幻读,再次查询仍然没有数据

幻读主要就是针对数据条数,查询不存在但是插入却有数据,查询数据列表没有数据,但是插入却出现异常

完成之后记得提交或回滚窗口1

MVCC 多版本并发控制

多版本并发控制是InnoDB的产物

在可重复读隔离级别下,事务在启动的时候就“拍了个快照”。注意,这个快照是基于整库的。实际上并不是把整库备份下来,而是通过事务ID的大小来实现

InnoDB 里面每个事务有一个唯一的事务 ID,叫作 transaction id。它是在事务开始的时候向 InnoDB 的事务系统申请的,是按申请顺序严格递增的。
每次事务更新数据的时候,都会生成一个新的数据版本,并且把 transaction id 赋值给这个数据版本的事务 ID,记为 row trx_id。
这样每条数据都得到了个数组修改集,undolog记录了数据的回滚信息;虚线就代表了undolog记录
极客时间

每个事务查询时只查询事务Id比自己小且已经提交的数据才认可;

下面是一个图说明这个过程
建表

1
2
3
4
5
6
CREATE TABLE `t` (
`id` int(11) NOT NULL,
`k` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB;
insert into t(id, k) values(1,1),(2,2);

流程
事务流程

我们通过start transcation with consistent snapshot来开启事务

  • A 开启事务 版本100
  • B开启事务 版本101
  • C开启事务 版本102
  • C修改 得到数据(1,2) -> 提交
  • B修改 得到数据(1,3) -> 提交 这里不是(1,2)的原因是此时需要读当前读,防止C的更新丢失
  • A查询 得到数据(1,1) A查询还是开事务的时候的数据

当前读

读取事务外的最新的数据,就不会严格遵循事MVCC了

当一个数据需要更新时如update T set a=a+1 这种情况为了保证其他事务的修改不会丢失。
如A开启了事务,读到a=1,B开启事务修改了a=2,然后a 再去修改

  • 如果A不采用当前读去读最新的数据,那么a=a+1 就只能把a从1修改成2,这样就丢失了B的修改
  • 如果A采用当前读,则当执行update语句时会先读当前的数据也就是B修改过的数据1,在执行a=a+1则可以正确显示为2