MySQL事务以及锁原理讲解
事务(ACID)
场景:小明向小强转账10元
原子性(Atomicity)
转账操作是一个不可分割的操作,要么转成功,要么转失败,不能存在中间的状态,也就是说转了一半这种情况。我们把这种要么全做,要么全不做的规则称之为原子性。
隔离性(Isolation)
另外一个场景:
- 小明向小强转账10元
- 小明向小红转账10元
隔离性表示上面两个操作是不能相互影响的。
一致性(Cinsistency)
对于上面的转账场景,一致性表示每一次转账完成之后,都需要保证整个系统的余额等于所有账户的收入减去所有账户的支出。
如果不遵守原子性,也就是如果小明向小强转账10元,但是只转了一半,小明账户少了10元,小强账户并没有增加,所有没有满足一致性了。
同样,如果不满足隔离性,也有可能破坏一致性。
所有说,数据库某些操作的原子性和隔离性都是保证一致性的一种手段,在操作执行完成后保证符合既定的约束则是一种结果。
实际上我们也可以对表建立约束来保证一致性。
持久性(Durability)
对于转账的交易记录,需要永久保存。
事务的概念
我们把需要保证原子性、隔离性、一致性和持久性的一个或多个数据库操作称之为一个事务。
版本链
对于使用InnoDB存储引擎的表来说,它的聚簇索引记录中都包含两个必要的隐藏列,(row_id不是必要的,我们创建的表中有主键或者非NULL唯一键时都不会包含row_id列)
| 列名 | 是否必须 | 占用空间 | 描述 |
|---|---|---|---|
| row_id | 否 | 6字节 | 唯一标识一条记录 |
| transaction_id | 是 | 6字节 | 事务id |
| roll_pointer | 是 | 7字节 | 回滚指针 |
- trx_id:每次对某条记录进行改动时,都会吧对应的事务id赋值给trx_id隐藏列。
- roll_pointer:每次对某条记录进行改动时,这个隐藏列会存一个指针,可以通过这个指针找到该记录修改前的信息。
ReadView
对于使用READ UNCOMMITED隔离级别的事务来说,直接读取记录的最新版本就好了,对于使用SERIALIZABLE隔离级别的事务来说,使用加锁的方式来访问记录。对于使用READ COMMITTED和REPEATABLE READ隔离级别的事务来说,就需要用到我们上边所说的版本链了,核心问题就是:需要判断一下版本链中的哪个版本是当前事务可见的。
ReadView中主要包含4个比较重要的内容:
- m_ids:表示在生成ReadView时当前系统中活跃的读写事务的事务id列表(活跃的事务id是指还没有提交事务的id)
- min_trx_id:表示在生成ReadView时当前系统中活跃事务中最小的事务id,也就是m_ids中的最小值
- max_trx_id:表示生成ReadView时系统中应该分配给写一个事务的id值。
- creator_trx_id:表示生成该ReadView的事务的事务id。
MVCC总结
MVCC(Multi-Version Concurrency Control 多版本并发控制)指的就是在使用Read Committed、Repeatable Read这两种隔离级别的事务在执行普通的select操作时访问记录的版本链的过程。可以使不同的事务读-写、写-读操作并发执行,从而提升系统性能。Read Committed、Repeatable Read这两个隔离级别的一个很大的不同就是:生成ReadView的时机不同,Read Committed在每一次执行普通的select操作前都会生成一个ReadView,而Repeatable Read只在第一次进行普通的select操作前生成一个ReadView,之后的查询操作都重复使用这个ReadView就好了。
锁
读锁与写锁
- 读锁:共享锁、Shared Locks、S锁(加了这把读锁,其他人只能读,不能写)
- 写锁:排他锁、Exclusive Locks、X锁(加了这把写锁,其他人不能读不能写)
- select:不加锁(穿墙术,可以越过所有锁)
| X锁 | S锁 | |
|---|---|---|
| X锁 | 冲突 | 冲突 |
| S锁 | 冲突 | 不冲突 |
读操作
对于普通的select语句,InnoDB不会加任何所。
select ... lock in share mode
将查询到的数据加上一个S锁,允许其他事务继续获取这些记录的S锁,不能获取这些记录的X锁(会阻塞)
使用场景:读出数据后,其他事务不能修改,但是自己也不一定能修改,因为其他事务也可以使用select ... lock in share mode继续加读锁。
select ... for update
将查询到的数据加上一个X锁,不允许其他事务获取这些记录的S锁和X锁。
使用场景:读出数据后,其他事务即不能写,也不能加读锁,那么就导致只有自己可以修改数据。
写操作
- delete:删除一条数据时,先对记录加X锁,然后执行删除操作
- insert:插入一条记录是,会先加隐式锁来保护这条新插入的记录在本事务提交前不被其他事务访问到。
- update
- 如果被更新的列,修改前后没有导致存储空间变化,那么先给记录加X锁,在直接对记录进行修改
- 如果被更新的列,修改前后导致了存储空间发生了变化,那么会先给记录加X锁,然后将记录删除,再insert一条新纪录。
隐式锁:一个事务插入一条记录后,还未提交,这条记录会保存本次事务id,而其他事务如果想对这个记录加锁时发现事务id不对应,这时会产生X锁,所以相当于在插入一条记录时,隐式的给这条记录加了一把隐式X锁。
行锁
- Lock_REC_NOT_GAP:单个行记录上的锁
- LOCK_GAP:间隙锁,锁定一个范围,但不包括记录本身。GAP锁的目的,是为了防止同一事务的两次当前读,出现幻读的情况。
- LOCK_ORDINARY:锁定一个范围,并且锁定记录本身,对于行的查询,都是采用该方法,主要目的是解决幻读的问题。(前两种锁的合体)
在read committed下,写锁只会锁住查询到的数据。
在repeatable read下,写锁会锁住查询到的数据和相应的间隙,其实这样就解决了幻读现象。