事务的特性
- (A)原子性:事务是一系列操作的集合,这些操作要么同时成功,要么同时失败
- (C)一致性:事务操作前后,数据库保持一致性状态。比如转账的例子,A给B转账,A的余额减少了,B的余额则要增加
- (I)隔离性:多个事务并发执行,互不干扰。
- (D)持久性:事务一旦提交,对于数据的修改就是永久的,即使系统故障也不会丢失。
并发事务会引发什么问题?
- 脏读:一个事务读到了另一个未提交事务修改过的数据,而这个数据随时可能被另一个事务回滚,从而读到过期的数据。
- 不可重复读:一个事务前后两次读取同一个数据,读到的数据不一样。(先后读取账户余额,这期间有其它事务修改了余额,发现不一样)
- 幻读:一个事务前后两次查询某个查询条件下的记录数量,发现前后两次读到的记录数量不一样。(先后查询账户余额大于100万的用户,分别是5条和6条,也就是这期间有其它事务插入了数据)
事务的隔离级别
介绍
-
读未提交:一个事务还没提交,它所做的变更就能被其它事务看到
-
读提交:一个事务提交之后,它所做的变更才能被其它事务看到
-
可重复读(MySQL默认事务隔离级别):一个事务执行过程中看到的数据和这个事务启动时看到的数据是一样的,只有当前事务提交之后重新开启,才能看到最新的数据
-
串行化:对记录加上读写锁,当多个事务对该记录进行读写操作时,如果发生读写冲突,后访问的事务必须等待前一个事务完成,才能继续执行
-
读提交之上解决了读未提交的问题
-
可重复读之上解决了不可重复读和读未提交的问题,很多程度解决幻读问题
-
一般不支持事务串行化,因为一旦串行化,事务相当于不能并发执行,性能很低
-
MySQL默认的事务隔离级别是可重复读,它很大程度上避免了幻读问题,但是还是有幻读的可能。可以通过以下方案解决:
- 快照读(普通的select语句):通过MVCC解决幻读问题。因为在可重复读情况下,也就是每次读取数据和事务开启时读取到的数据保持一致,所以即使有其它事务中途插入了数据,也是看不到的,从而避免了幻读问题
- 当前读(select…for update):通过给记录加上next-key loc(记录锁+间隙锁)解决幻读问题。当有其它事务视图插入一条数据(之前已经进行了当前读,所以已经加上锁),这个时候就会出现锁冲突,无法插入成功,从而解决幻读问题
实现
- 读未提交通过每次读取最新的数据即可
- 串行化则是通过给记录添加读写锁
- 读提交和可重复读则是通过ReadView实现的。他们不同在于创建ReadView的时机不同。
读提交在每个语句执行之前创建,而可重复读则在每次事务启动的时候创建,之后事务执行过程中都复用这个ReadView。
- 执行“开始事务”的命令,不代表“启动事务”
begin/start transaction
执行后,事务不会其它,而是在执行了增删改查的SQL语句时,才启动事务
start transaction with consistent snapshot
执行后,马上启动事务
- 记录有三个隐藏列,除了row_id,还有trx_id、roll_pointer
trx_id表示修改记录的事务id
roll_pointer指针向undo 日志中的下一个旧版本的记录
ReadView
每次“开启事务”就会创建一个ReadView
ReadView有四个字段
-
create_trx_id:创建ReadView的事务id
-
m_ids:活跃事务id(未提交的事务id)
-
min_trx_id:最小的活跃事务id
-
max_trx_id:创建ReadView时要分配的事务id(全局的事务中最大的事务id + 1)
-
一个事务去查询某条记录的时候,除了自己更新的记录总是可见之外,对于其它不是自己更新的记录,它需要拿着自己的trx_id和事务当前的ReadView,根据下面的可见性算法,判断该记录是否可见:
- 如果trx_id < min_trx_id,说明这个版本的记录是在创建ReadView前已经提交的事务生成的,因此可见
- 如果trx_id >= max_trx_id,说明这个版本的记录是在创建ReadView之后才启动的事务生成的,因此不可见
- 如果 min_trx_id <= trx_id < max_trx_id,这个时候分两种情况,可能trx_id不在m_ids 中,因为生成该版本的事务已经提交了,则可见;如果在m_ids中,则说明生成该版本的事务还未提交,不可见。
- 这种通过版本链来控制并发事务访问同一个记录的行为,就叫MVCC(多版本并发控制)
可重复读的实现
初始时,启动两个事务A、B,事务id分别为51、52
初始的记录中的事务id为50,B事务查询记录发现余额为1000000
接着,A事务修改余额为2000000,版本链生成
B事务第二次查询余额,由于是可重复读,ReadView不会重新创建,因此这个时候,事务A的事务id满足min_trx_id <= id < max_trx_id,并且在m_ids中,所以不可见(避免了脏读问题),因此只能沿着版本链,找到小于事务B的ReadView中min_trx_id的第一条记录,读取到的余额还是1000000。
A事务提交事务
B事务第三次查询余额,由于是可重复读,ReadView不会重新创建,因此还是和第二次查询一样,不会读取trx_id为51的记录余额仍旧是1000000。
读提交的实现
还是上面的例子,A、B事务启动时id还是51、52,事务B第一次读取记录余额为1000000
A事务修改余额后,还未提交,这个时候,事务B读取记录事务id为51的记录发现它大于等于min_trx_id小于max_trx_id,并且它在m_ids中,所以不可见(避免了脏读问题),只能沿着版本链向下找到第一条事务id小于min_trx_id的记录,也就是读取到余额还是1000000
而当事务A提交之后
事务B重新读取数据时重新创建ReadView,这个时候ReadView就和之前不一样了
也就是事务A对记录修改的事务id(最新的记录id51)它虽然还是大于等于min_trx_id小于max_trx_id,但是已经不在m_ids中了,所以是可见的。因此这时读取到的余额为2000000。