目录
- 一.事务的ACID
- 二.隔离级别
- 三.并发事务中的问题
- 1.脏写
- 2.脏读
- 3.不可重复读
- 4.幻读
- 四.MVCC机制
- 五.读锁与写锁
- 六.大事务的影响
- 七.事务优化
一.事务的ACID
- 原子性(Atomicity):一个事务中的所有操作,要么全部成功,要么失败全部回滚,不会结束在某个中间环节。原子性由undo log日志来实现
- 一致性(Consistency):事务执行的结果必须是使数据库从一个一致性状态变到另一个一致性状态。由其他三个特性及业务代码正确逻辑来实现
- 隔离性((Isolation):一个事务内部的操作及使用的数据对其它并发事务是隔离的,并发执行的各个事务之间不能互相干扰。隔离性由MySQL各种锁以及MVCC机制来实现
- 持久性(Durable):一个事务一旦提交,它对数据库中的数据的改变就应该是永久性的,即使系统故障也不会丢失。持久性由redo log日志来实现
二.隔离级别
InnoDB引擎定义了四种隔离级别,级别越高事务隔离性越好,但性能就越低
- read uncommit(读未提交),事务A读取到了事务B已经修改但尚未提交的数据
- read commit(读已提交):Oracle和SQL Server的默认隔离级别,一个事务只能看见已经提交事务所做的改变
- repeatable read(可重复读):MySQL的默认隔离级别,无论事务B如何修改数据,事务A内部相同查询语句在不同时刻查询出来的结果都是一致的,即在第一次查询之后就不会发生变化(除非事务A更新了这行数据,由快照读变为当前读)
- serializable(串行化) :所有事务对同一条数据的读和写只允许串行执行,解决了脏读、不可重复读、幻读的问题
隔离级别 | 脏写 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|---|
read uncommit | ❌ | ✔ | ✔ | ✔ |
read commit | ❌ | ❌ | ✔ | ✔ |
repeatable read | ❌ | ❌ | ❌ | ✔ |
serializable | ❌ | ❌ | ❌ | ❌ |
✔表示存在此并发问题,❌表示不存在
三.并发事务中的问题
account表(只有一条id为1的数据)
表结构
CREATE TABLE `account` (
`id` int(11) NOT NULL,
`balance` int(11) NOT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8
1.脏写
事务A对一行数据进行修改,之后事务B也修改了同一行数据,事务A进行回滚,由于undo日志记录的是事务A修改这行数据的反向操作(执行insert就会记录delete),数据就会恢复到A修改前的初始状态
所有的隔离级别都不会出现脏写的问题,因为事务A更新数据的时候,会对更新的行记录加写锁,除非事务A提交或者回滚,否则事务B因获取不到锁,从而不能更新这行数据
2.脏读
事务A读取到了事务B已经修改但尚未提交的数据
- 1.事务B将序号1的余额增加500但未提交
set tx_isolation = 'read-uncommitted'; begin; update account set balance = balance + 500 where id = 1;
- 2.事务A读取到序号1的余额为500
set tx_isolation = 'read-uncommitted'; select * from account where id = 1;
- 3.事务B回滚之前增加余额的操作,那么事务A读取到的数据就是脏数据
rollback;
3.不可重复读
事务A内部相同查询语句在不同时刻查询出来的结果不一致
- 1.事务A查询序号1的余额为0
set tx_isolation = 'read-committed'; begin; select * from account where id = 1;
- 2.事务B将序号1的余额增加500并提交
set tx_isolation = 'read-committed'; begin; update account set balance = balance + 500 where id = 1; commit;
- 3.事务A再次查询序号1的余额为500
select * from account where id = 1; commit;
4.幻读
事务A读取到了事务B新增且已经提交的数据
- 1.事务A读取表中所有数据,发现只有一条id为1的数据,select会生成快照读(之后再读同一行数据会从历史版本读取)
set tx_isolation = 'repeatable-read'; begin; select * from account;
- 2.事务B往表中添加一条id为2的记录并提交
set tx_isolation = 'repeatable-read'; begin; select * from account;
- 3.事务A读取表中所有数据,发现还是只有一条id为1的数据(证明已解决不可重复读的问题)
select * from account;
- 4.事务A更新表中所有数据(余额都加500)
update account set balance = balance + 500;
- 5.事务A读取表中所有数据,发现突然多出来一条id为2的数据(好像幻觉一样),这是因为之前修改到了id为2的数据,快照读变成了当前读,会读取当前最新数据
update account set balance = balance + 500; select * from account; commit;
四.MVCC机制
MVCC(Multi-Version Concurrency Control)多版本并发控制,就可以做到读写不阻塞(多个事务的读和写操作可以并行执行),且避免了类似脏读的问题,主要通过undo日志链来实现
- select操作是快照读(历史版本)
- insert、update和delete是当前读(当前版本)
- read commit(读已提交),语句级快照
- repeatable read(可重复读),事务级快照
五.读锁与写锁
- 读锁(共享锁)
读锁是共享的,多个事务可以读取同一个资源,但不允许其他事务修改,语法是select … lock in share mode; - 写锁(排它锁)
写锁是排它的,会阻塞其他的写锁和读锁,update、delete、insert,语法是select … for update;
六.大事务的影响
- 并发场景下,数据库连接池容易被撑爆
- 锁定太多的数据,造成大量的阻塞和锁超时
- 执行时间长,容易造成主从延迟
- 回滚所需要的时间比较长
- undo日志膨胀
- 容易导致死锁
七.事务优化
- 尽量将查询等数据准备操作放到事务外
- 事务中避免远程调用,远程调用要设置超时,防止事务等待时间太久
- 事务中避免一次性处理太多数据,可以拆分成多个事务分次处理
- 更新等涉及加锁的操作尽可能放在事务靠后的位置
- 为了保证数据的一致性,以非事务的方式执行