并发事务问题
每个客户端和服务器的一次连接,就是一个会话,而每个客户端可以在自己的会话中发出事务请求,一般来说一个服务器可以连接若干个客户端,所以一个服务器可以同时处理很多事务请求,但理论上某个事务在对某个数据在进行访问时,其他事务应该排队等待。但这样在高并发下会严重影响性能,所以只能设计事务隔离级别来兼顾事务的隔离性和提高多个事务的性能。
事务问题
如果事务执行不保证串行执行,也就是并发执行会遇到以下几个问题:
- 脏写:一个事务修改了另一个未提交事务修改过的数据
- 脏读:一个事务读到另一个未提交事务修改过的数据
- 不可重复读:一个事务能读到其他已提交事务修改过后的值,并且每次其他事务修改并提交,该事务都能查到最新值,但我们其实需要的是第一次读的那个值
- 幻读:一个事务先根据某些条件查询出一个范围的记录,之后另一个事务又向表中插入了符合这些条件的记录,原先的事务再次按照该条件查询时,能把另一个事务插入的记录也读出来,即读到了之前没有读到的记录
隔离级别
根据这几个问题,SQL标准设计了4个隔离级别,在不同程度上禁止了这些问题的发生。
READ UNCOMMITTED
隔离级别下,可能发生脏读
、不可重复读
和幻读
问题。READ COMMITTED
隔离级别下,可能发生不可重复读
和幻读
问题,但是不可以发生脏读
问题。REPEATABLE READ
隔离级别下,可能发生幻读
问题(MySQL下会禁止),但是不可以发生脏读
和不可重复读
的问题。SERIALIZABLE
隔离级别下,各种问题都不可以发生。
InnoDB使用锁来保证脏写的情况不会发生。
解决
解决脏读,不可重复读,幻读可以采用两种方法:
-
读操作利用MVCC,写操作加锁(读-写事务不冲突)
- 版本链:READ UNCOMMITTED
- ReadView:READ COMMITTED,REPEATABLE READ
-
读写操作都加锁(读-写事务需要排队执行)
- SERIALIZABLE
MVCC原理
(Multi-Version Concurrency Control ,多版本并发控制),MVCC指的就是在使用指的就是在使用READ COMMITTD
、REPEATABLE READ
这两种隔离级别的事务在执行普通的SEELCT
操作时访问记录的版本链的过程,这样子可以使不同事务的读-写
、写-读
操作并发执行,从而提升系统性能。而剩下两种隔离级别执行普通select操作时的区别就是生成ReadView的时机不同;事务利用MVCC进行的读操作也被称为一致性读/一致性无锁读/快照读
版本链
每条记录在插入时都会隐式的生成两个列:trix_id,roll_pointer,trix_id是它最新的事务id,在记录每次更新时,就会将旧值写入undo日志,并将roll_pointer指向它;每条undo日志也有自己roll_pointer属性和事务id属性,所以可以将这些记录根据roll_pointer串成一条链表(版本链),版本链的头节点就是当前记录的最新值
READ UNCOMMITTED隔离级别的事务只需要读取版本链中最新的记录就可以了,不需要管它是否已经被提交
ReadView
READ COMMITTED,REPEATABLE READ隔离级别的事务都是基于已提交的事务的,所以需要判断版本链中哪个版本是 对当前事务可见的,所以InnoDB中使用ReadView方式来判断。
ReadView由事务生成时创建
- m_ids:当前系统中活跃的读写事务的事务id列表
- min_trix_id:m_ids中最小值
- max_trix_id:应该分配给下个事务的id值
- creator_trix_id:生成该ReadView的事务的事务id
如何判断记录的某个版本是否对当前事务可见,如果不可见,就需要沿着版本链一直向下比较
- 被访问版本的trix_id=creator_trix_id,自己访问自己,可见
- 被访问版本的trix_id<min_trix_id,被访问记录事务已提交,可见
- 被访问版本的trix_id>max_trix_id,当前事务在被访问记录生成后开启,不可见
- 被访问版本的trix_id in (min_trix_id, max_trix_id),需要判断被访问版本的trxi_id是否在m_ids中,如果在则说明它的事务仍旧活跃,不可见;如果不在,说明已被提交,可见
READ COMMITTED
每次读取数据前都生成一个独立的ReadView,这样根据版本链依次判断,然后将可见的那个版本返回回去。
REPEATABLE READ
只在第一次读取数据时生成一个ReadView,后面的select重复使用一个ReadView
锁
SERIALIZABLE隔离级别的事务需要加锁来访问记录
锁定读
-
共享锁Shared Locks
//对读取加s锁 SELECT ... LOCK IN SHARE MODE;
-
独占锁Exclusive Locks
定位某条需要修改/删除记录在B+树位置后,再获取一下记录的x锁,其实就是一个获取x锁的锁定读
//对读取加x锁 SELECT ... FOR UPDATE;
s锁和x锁是互斥的,s锁和s锁不互斥,x锁和x锁互斥
多粒度锁
-
表级锁
- s锁/x锁
- 意向锁方便对表上表锁(s锁/x锁)前得知这个表内是否有记录已经被上了锁,IX锁和IS锁是互相兼容的,但和s锁/x锁不兼容;在对表内某记录加了s锁/x锁后会加上对应意向锁
-
意向共享锁IS/意向独占锁IX
-
表中某列带有AUTO_INCREMENT属性
-
AUTO-INC锁(适用于不确定数量的插入insert....select)
在执行一个插入语句时就在表级别加一个AUTO-INC锁,然后为每个需要自增的列生成值,在语句执行结束后,就释放锁;在这个锁被持有的过程中,其他需要插入的语句是被阻塞的
-
轻量级的锁(适用于确定记录的插入)
在执行一个插入语句时获取这个轻量级锁,然后为每个需要自增的列生成值后,就释放锁;在这个锁被持有的过程中,其他需要插入的语句是被阻塞的
-
-
行级锁
InnoDB锁的内存结构
符合下列条件的记录集,就会将它们的锁放到一个锁结构中,减少一条记录一个锁占用的空间
- 在同一个事务中进行加锁操作
- 被加锁的记录在同一个页面中
- 加锁的类型是一样的
- 等待状态是一样的
锁结构
-
锁所在的事务信息:指向生成这个锁结构的事务信息
-
索引信息:指向记录加锁的记录属于哪个索引(聚簇索引)
-
表锁/行锁信息:
- 表锁:表信息
- 行锁:space id,page number,n_bits使用比特位表示加锁的记录
-
type_mode
- lock_mode锁模式:LOCK_IS,LOCK_IX,LOCK_AUTO_INC,LOCK_S和LOCK_X
- lock_type锁类型:LOCK_TABLE表级锁,LOCK_REC行级锁
- rec_lock_type行锁具体类型:LOCK_ORDINARY,LOCK_GAP,LOCK_REC_NOT_GAP,LOCK_INSERT_INTENTION...
- LOCK_WAIT:第九个比特位为1时,is_waiting为true,为0就是false;表示当前事务是否处于等待状态
类型
- LOCK_REC_NOT_GAP:分S锁和X锁
- LOCK_GAP:在某条记录上加锁后,就不允许其他事务在这条记录前插入记录,以防止幻读,只有当y拥有整个锁的事务提交后才能插入;同时这个gap锁和其他锁不互斥。如果不允许在最后一条之后插入记录,可以在伪记录Supremum之前插入。
- Next-Key Locks:锁住记录的同时,不允许在这条记录之前插入新记录
- Insert Intention Locks:插入意向锁,表明因为某条记录被加上了gap/next-key锁,而不能在记录前的间隙插入新记录,只能阻塞等待时,所以InnoDB规定在事务等待时会在内存中生成一个锁结构,表示有个事务想在间隙插入记录,但在等待。
- 隐式锁:在一个事务中记录insert插入时,是没有与锁做关联的,但因为事务id的存在,相当于加了一个锁;当别的事务希望对这个记录加上s/x锁时,会先比较这个事务id是否活跃,即存在隐式锁,是的话,就会帮这个事务生成一个锁结构isWaiting=false,再为自己生成一个锁结构isWaiting=true