7 MySQL事务
事务特性
事务特性:ACID(Atomicity 即原子性、Consistency 一致性、Isolation 隔离性、Durability 持久性)
事务可能存在的问题
可能出现的问题:脏读(dirty read)、幻读(phantom read)、不可重复读(non-repeatable read);
- 脏读:一个事务里出现了其他事务未提交的数据
- 不可重复读: 一个事务里两次读取的数据不一致(其他事务在两次读取数值之间提交了变更)
- 幻读:
不可重复读和幻读的区别:侧重点不同,不可重复读描述的侧重点是修改操作,而幻读描述的侧重点是添加和删除操作。
事务隔离级别
隔离级别:读未提交(read uncommitted)、读已提交(read committed)、可重复读(repeatable read)和串行化(serializable)
- 读未提交:一个事务还没提交时,它做的变更就能被别的事务看到。
- 读提交:一个事务提交之后,它做的变更才会被其他事务看到。
- 可重复读:一个事务执行过程中看到的数据,总是跟这个事务在启动时看到的数据是一致的。当然在可重复读隔离级别下,未提交变更对其他事务也是不可见的。
- 串行化读:对于同一行记录,“写”会加“写锁”,“读”会加“读锁”。当出现读写锁冲突的时候,后访问的事务必须等前一个事务执行完成,才能继续执行。
数据库里面会创建一个视图,访问的时候以视图的逻辑结果为准。
“读未提交”隔离级别下,直接返回记录上的最新值,没有视图概念
“读已提交”隔离级别下,这个视图是在每个 SQL 语句开始执行的时候创建的
“可重复读”隔离级别下,这个视图是在事务启动时创建的,整个事务存在期间都用这个视图
“串行化”隔离级别下,直接用加锁的方式来避免并行访问
事务隔离级别演示
初始化
1 | CREATE TABLE `test` ( |
下面的可以查看连接情况和事务隔离级别
1 | show processlist; |
读未提交- 脏读
新建两窗口
步骤 | 窗口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 | INSERT INTO `test` VALUES (11, 'name11', 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 | CREATE TABLE `t` ( |
我们通过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