数据库事务的四大特性(ACID)
- 原子性(Atomic):事务包含的所有操作要么全部成功,要么全部失败回滚;成功必须要完全应用到数据库,失败则不能对数据库产生影响;
- 一致性(Consistency):事务执行前和执行后必须处于一致性状态,
- 隔离性(Isolation):当多个用户并发访问数据库时,数据库为每一个用户开启的事务,不被其他事务的操作所干扰,多个并发事务之间要相互隔离;
- 持久性(Durability):一个事务一旦被提交了,那么对数据库中的数据的改变就是永久性的,即便在数据库系统遇到故障的情况下也不会丢失事物的操作。
事务隔离级别以及各级别下的并发访问问题
事务并发访问引起的问题以及如何避免
更新丢失——mysq所有事务隔离级别在数据库层面上均可避免
更新丢失:
取款事务 | 存款事物 |
---|---|
查询转账余额为100元 | |
查询转账余额为100元 | |
存入20,余额变为120元 | |
提交事物 | |
取出10元,余额改为90元 | |
回滚事物,余额恢复为100元 | 更新丢失 |
解决方案:所有事物隔离级别,即READ-UNCOMMITTED以上
READ-UNCOMMITTED(可读取未提交):写事务阻止其他写事务,避免了更新遗失。但是没有阻止其他读事务。
问题:脏读
脏读——READ-COMMITTED事务隔离级别以上可避免
脏读:也就是当数据库的一个事务A正在使用一个数据但还没有提交,另外一个事务B也访问到了这个数据,还使用了这个数据,这就会导致事务B使用了事务A没有提交之前的数据。
解决方案:事物隔离级别READ-COMMITTED以上
READ-COMMITTED(可读已提交):写事务会阻止其他读写事务。读事务不会阻止其他任何事务。
问题:不可重复读
不可重复读——REPEATABLE-READ事务隔离级别以上可避免
不可重复读:在一个事务A中多次操作一个数据,在这两次或多次访问这个数据的中间,事务B也操作此数据,并使其值发生了改变,这就导致同一个事务A在两次操作这个数据的时候值不一样,这就是不可重复读。
解决方案:事物隔离级别REPEATABLE-READ以上
REPEATABLE-READ(可重复读):读事务会阻止其他写事务,但是不会阻止其他读事务。
问题:幻读
幻读——SERIALIZABLE事物隔离级别可以避免
幻读:是指事务不独立执行产生的一种现象。事务A读取与搜索条件相匹配的若干行。事务B以插入或删除行等方式来修改事务A的结果集,然后再提交。这样就会导致当A本来执行的结果包含B执行的结果,这两个本来是不相关的,对于A来说就相当于产生了“幻觉”。
解决方案:事物隔离级别SERIALIZABLE以上
SERIALIZABLE(序列化):可避免幻读。读加共享锁,写加排他锁。这样读取事务可以并发,但是读写,写写事务之间都是互斥的,基本上就是一个个执行事务,所以叫序列化。
总结:(SQL Server、oracle默认为READ-COMMITTED,MySQL默认为REPEATABLE-READ)
事务隔离级别 | 更新丢失 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|---|
READ-UNCOMMITTED | 避免 | 发生 | 发生 | 发生 |
READ-COMMITTED | 避免 | 避免 | 发生 | 发生 |
REPEATABLE-READ | 避免 | 避免 | 避免 | 发生 |
SERIALIZABLE | 避免 | 避免 | 避免 | 避免 |
InnoDB可重复读隔离级别下如何避免幻读
- 表象:快照读(非阻塞读)——伪MVCC(MVCC:多版本控制)
- 内在: next-key锁(行锁+gap锁)
快照读和当前读
快照读:简单的select操作,属于快照读,不加锁。(当然,也有例外)
select * from table where ?;
当前读:特殊的读操作,插入/更新/删除操作,属于当前读,需要加锁。
select * from table where ? lock in share mode;--共享锁,其他全为排它锁
select * from table where ? for update;
insert into table values (…);
update table set ? where ?;
delete from table where ?;
从图中,可以看到,一个Update操作的具体流程。当Update SQL被发给MySQL后,MySQL Server会根据where条件,读取第一条满足条件的记录,然后InnoDB引擎会将第一条记录返回,并加锁 (current read)。待MySQL Server收到这条加锁的记录之后,会再发起一个Update请求,更新这条记录。一条记录操作完成,再读取下一条记录,直至没有满足条件的记录为止。因此,Update操作内部,就包含了一个当前读。同理,Delete操作也一样。Insert操作会稍微有些不同,简单来说,就是Insert操作可能会触发Unique Key的冲突检查,也会进行一个当前读。
参考:这是一个厉害的博客
RC、RR级别下的 InnoDB的非阻塞读如何实现
- 数据行里的DB_TRX_ID、 DB_ROLL_PTR、DB_ROW_ID字段
- DB_TRX_ID:该字段用来记录最近一次对该条记录进行修改的事务的ID
- DB_ROLL_PTR:回滚指针(重建)
- DB_ROW_ID:( InnoDB创建的隐主键字段)
- undo日志
- read view
当我们执行快照读的时候,会创建一个readview,指的是当前我们可以看到的是哪一个版本的值。
readview可见性算法:将要修改的数据的DB_TRX_ID与其他系统活跃的事务id相比,如果DB_TRX_ID比这些活跃事务ID大,就通过DB_ROLL_PTR取出undo日志上一层的DB_TRX_ID,直到DB_TRX_ID小于这些活跃事务ID位置,这样就保证了我们获取到的数据版本是最稳定的版本。
正是因为创建read view(快照的时机不同),才形成了RC、RR级别两种隔离级别的不同可见性:在REPEATABLE-READ级别下,一个事务下,RR 第一条 dml 语句(增删改查)读数据会对已存在的 undo log 按read view的规则创建一个快照。同一个事务下,多次执行dml 语句,也是只能读到同一个快照,即第一个快照的内容;在READ-COMMITTED中,一个事务下,RC 每条 dml 语句(增删改查)读数据都会创建一个新的 read view (快照),其DB_TRX_ID 每次读都 +1。
这就是为什么在RC下可以看到别的快照读已提交的对表记录的增删改;而在RR下,主要看read view的首次使用时间:如何是在别的事务做出增删改之前的使用的,此后,其他事务做出增删改,就不知道事务做出改变的原因了。