事务需要满足ACID特征。Mysql的innodb引擎支持事务。
- 原子性,Atomicity, 一个事务是不可分割的整体,要么全部成功(committed),要么全部失败(rolled back),不存在部分成功。
- 一致性,Consistency,数据总是从一致性的状态转移到另一个一致性的状态,总是处于有意义的状态。比如转账,A的钱减少,B的钱增加,总和是不变的。
- 隔离性,Isolation,主要解决多个事务并发读写的问题,一个事务未提交,那么它对数据的修改对外是不可见的。
- 持久性,Durability,一个事务一旦提交,对数据的影响的永久性的,就算断电,系统崩溃也是如此。
四个性质最根本的是一致性,其他三个性质都是服务于一致性的。
隔离级别
当多个进程的事务同时读写数据时,就会出现一些问题。
- 脏读,dirty read,一个事务可以读到其他事务尚未提交的修改。尚未提交意味着可能回滚,那么该事务读到的就是无效的数据。
- 不可重复读,unrepeatable read,同一个事务范围内多次查询却返回了不同的数据,这是因为在查询间隙,数据被另外的事务修改了(update, delete)。
- 虚读,幻读,phantom read,同一个事务范围内,相同的操作两次读取的记录数不一样,比如多出来一行(add)。跟不可重复读的对象不一样,幻读针对的是一批数据,而后者指的是同一个数据。
innodb通过不同的锁策略支持四个级别的隔离性。
- Read uncommitted,最低级别,任何情况都无法保证。
- Read committed,可避免脏读。
- Repeatable read,在Sql标准中,RR级别可避免脏读和不可重复读,但是还存在幻读。RR是innodb的默认级别,innodb的RR解决了幻读的问题。
- Serializable,最高级别,可避免脏读,不可重复读,幻读的发生,效率最低,一般通过锁表来实现。
隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
未提交读 | Yes | Yes | Yes |
已提交读(RC) | No | Yes | Yes |
可重复读(RR) | No | No | Yes(注:Innodb的RR级别通过gap锁解决了幻读) |
可串行化 | No | No | No |
mysql中查看隔离级别:
1 | mysql> select @@global.tx_isolation, @@tx_isolation; |
innodb可以通过下面的命令设置隔离级别,注意带global, session或者都不带效果是不一样的。
1 | set [global | session] transaction isolation level [read uncommitted |read committed |repeatable read | serializable]; |
- With the GLOBAL keyword, the statement applies globally for all subsequent sessions. Existing sessions are unaffected.
- With the SESSION keyword, the statement applies to all subsequent transactions performed within the current session.
- Without any SESSION or GLOBAL keyword, the statement applies to the next (not started) transaction performed within the current session. Subsequent transactions revert to using the SESSION isolation level.
在事务内部无法修改下一个事务的隔离级别:
1 | mysql> begin; |
或者
1 | set tx_isolation = 'read-uncommitted'; |
2PL: Two-Phase Locking
传统 RDBMS 加锁的一个原则,二阶段锁。锁操作分为两个阶段:加锁阶段和解锁阶段,并且保证加锁阶段和解锁阶段不相交。
- 加锁阶段:只加锁,不放锁。
- 解锁阶段:只放锁,不加锁。
行锁
如果一个条件无法通过索引快速过滤,存储引擎层面就会将所有记录加锁后返回,再由MySQL Server层进行过滤。
但在实际使用过程当中,MySQL做了一些改进,在MySQL Server过滤条件,发现不满足后,会调用unlock_row方法,把不满足条件的记录释放锁 (违背了二段锁协议的约束)。
这样做,保证了最后只会持有满足条件记录上的锁,但是每条记录的加锁操作还是不能省略的。可见即使是MySQL,为了效率也是会违反规范的。
这种情况同样适用于MySQL的默认隔离级别RR。所以对一个数据量很大的表做批量修改的时候,如果无法使用相应的索引,
MySQL Server过滤数据的的时候特别慢,就会出现虽然没有修改某些行的数据,但是它们还是被锁住了的现象。
MVCC多版本并发控制
与MVCC相对的,是基于锁的并发控制,Lock-Based Concurrency Control,最大的好处是,读不加锁,读写不冲突。
InnoDB中,每行数据后添加两个额外的隐藏的值来实现MVCC。一个记录这行数据何时被创建,另外一个记录这行数据何时过期(或者被删除)。
在实际操作中,存储的并不是时间,而是事务的版本号。每开启一个新事务,事务的版本号就会递增。
在RR事务隔离级别下:
- select时,读取创建版本号<=当前事务版本号,删除版本号为空或者大于当前事务版本号的记录。
- insert时,保存当前事务版本号为行的创建版本号。
- delete时,保存当前事务版本号位行的删除版本号。
- update时,插入一条新记录,保存当前事务版本号为行创建版本号,同时保存当前事务版本号为原来行的删除版本号。
通过MVCC,每行记录都需要额外的存储空间,更多的行检查工作和额外的维护工作,但是可以减少锁的使用。大多数操作都不用加锁。
读数据操作很简单,性能很好,并且也能保证只会读取到符合标准的行,也只锁住必要行。
Mysql的RR级别是解决了幻读的问题的。
快照读 snapshot read
简单的select操作,属于快照读,读取记录的可见版本,不用加锁。
- select * from table where ?;
当前读 current read
特殊的读操作,以及插入,更新,删除操作,属于当前读。
- 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 ?;
读取的是记录的最新版本,并且当前读返回的记录,都会加上锁,保证其他事务不会再并发修改这条记录。
除了第一条语句对读取记录加S锁(共享锁)外,其他的操作都加的是X锁(排它锁)。
GAP间隙锁
幻读无法通过行锁来解决。
行锁防止别的事务修改或删除,GAP锁防止别的事务新增,行锁和GAP锁结合形成的的Next-Key锁共同解决了RR级别在写数据时的幻读问题。
有索引的时候,Gap锁很多时候会锁住不需要锁的区间。
没有索引的时候,Gap锁会锁住所有记录,但是innodb不会主动升级表锁。
Serializable
不区分快照读与当前读,所有读操作均为当前读。
读加共享锁(S锁),写加排它锁(X锁),读写互斥。使用悲观锁的理论,实现简单,数据更加安全,但是并发能力非常差。如果你的业务并发特别少或者没有并发,
同时有要求数据及时可靠的话,可以使用这种模式。这个级别下select也是会加锁的。
死锁
死锁的本质是两个以上的session对资源的加锁顺序不一致。解决死锁的关键在于让不同的session加锁有次序。