并发事物可能带来的问题?
肮读,丢失修改,幻读,不可重复读
脏读:A事物修改后的数据被B事物读取的到,然后A事物又执行了回滚操作,这个时候B读取道德数据就是脏数据。
丢失修改:A和B同时对一份数据进行修改,导致B覆盖A
幻读:一个事物前后读取数据不一致
不可重复度:一个事物前后读取数据量不一致
那么mysql是如何解决这个问题的呢?
mysql为解决上面问题为我们提供了四个隔离级别
读取未提交(RU):会出现肮读,幻读,不可重复读
读取已提交(RC):解决肮读
可重复读(RR): 解决肮读,幻读(mysql默认的隔离级别)
可串行化(SERIALIZABLE):解决肮读,幻读,不可重复读
那么底层是如何实现的呢?
锁和MVCC机制来实现
RC和RR是通过MVCC机制实现的
SERIALIZABLE是通过加锁来实现的
解决幻读有两种方案: 1)隔离级别直接设置为SERIALIZABLE 2)在RR隔离级别的基础上加上表锁(对于非索引字段查询解决方案)或间隙锁(索引字段)
mysql锁:
mysql采用读写锁的形式来进行并发编程控制,读锁是共享锁(事务在读取记录的时候获取共享锁,允许多个事务同时获取(锁兼容)写锁是排他锁(事务在修改记录的时候获取排他锁,不允许多个事务同时获取。如果一个记录已经被加了排他锁,那其他事务不能再对这条事务加任何类型的锁)
按照锁的颗粒可划分为行锁和表锁(无论是行锁还是表锁都按照共享锁和排他锁划分,只不过锁的对象不一样)
- 表级锁: MySQL 中锁定粒度最大的一种锁(全局锁除外),是针对非索引字段加的锁,对当前操作的整张表加锁,实现简单,资源消耗也比较少,加锁快,不会出现死锁。不过,触发锁冲突的概率最高,高并发下效率极低。表级锁和存储引擎无关,MyISAM 和 InnoDB 引擎都支持表级锁。
- 行级锁: MySQL 中锁定粒度最小的一种锁,是 针对索引字段加的锁 ,只针对当前操作的行记录进行加锁。 行级锁能大大减少数据库操作的冲突。其加锁粒度最小,并发度高,但加锁的开销也最大,加锁慢,会出现死锁。行级锁和存储引擎有关,是在存储引擎层面实现的。
行锁种类:
在 InnoDB 默认的隔离级别 REPEATABLE-READ 下,行锁默认使用的是 Next-Key Lock。但是,如果操作的索引是唯一索引或主键,InnoDB 会对 Next-Key Lock 进行优化,将其降级为 Record Lock,即仅锁住索引本身,而不是范围。
- 记录锁(Record Lock):也被称为记录锁,属于单个行记录上的锁。
- 间隙锁(Gap Lock):锁定一个范围,不包括记录本身。
- 临键锁(Next-Key Lock):Record Lock+Gap Lock,锁定一个范围,包含记录本身,主要目的是为了解决幻读问题(MySQL 事务部分提到过)。记录锁只能锁住已经存在的记录,为了避免插入新记录,需要依赖间隙锁。
通过唯一/主键索引等值加锁,只会锁具体的行,非唯一索引则不一定,SQL优化器会基于数据分布选择记录锁,或临键锁。
只有在RR级别下才有间隙锁,目的是为了解决幻读,如果操作的数据是跨多个范围,就会加多个区间的间隙锁。
MySQL默认的锁就是【临键锁】,所以在执行SQL的时候,记录锁和间隙锁是会同时存在的。范围是左开右闭的区间。
意向锁
是一种辅助的表锁,在获取表锁前需要获取的锁,分为意向排他锁(表),意向共享锁(表),它的主要作用是让表锁和行锁更高效的共存。
在一张表中存在行锁的情况下不允许添加表锁的
意向锁互相之间是兼容的。
意向锁和共享锁和排它锁互斥(这里指的是表级别的共享锁和排他锁,意向锁不会与行级的共享锁和排他锁互斥)。
IS 锁 | IX 锁 | |
---|---|---|
S 锁 | 兼容 | 互斥 |
X 锁 | 互斥 | 互斥 |
意向锁是由数据引擎自己维护的,用户无法手动操作意向锁,在为数据行加共享/排他锁之前,InooDB 会先获取该数据行所在在数据表的对应意向锁。
如果一张表中存在事物获取行锁的话意向锁肯定也是被同时获取了的,那么在此刻添加表锁是必然不成功的因为表锁和意向锁是互斥的(只有共享表锁和共享意向锁不互斥)
从上面的总结来看,若只要是发生了写操作,都会触发写锁,这个时候别的事物要想来读取锁定的信息是需要等待锁释放的。有没有一种方案让数据被修改的时候,我们仍然可以读取之前版本的数据呢?
MVCC机制
MVCC称为多版本控制方案,它是通过在每个数据行上维护多个版本的数据来实现的。当一个事务要对数据库中的数据进行修改时,MVCC 会为该事务创建一个数据快照,而不是直接修改实际的数据行。
通过事物的可见性来保证各个事物看到自己应该看到的数据版本。
MVCC机制实现:
隐藏字段:主要依赖于两个隐藏字段
1)最后修改行记录的事物id
2)这条记录上一个版本
快照:数据某一时刻的状态
回滚日志:记录了每个行记录的多个版本
前两者用来控制事物的可见性
- 当一个事务执行读操作时,它会使用快照读取。快照读取是基于事务开始时数据库中的状态创建的(获取事物开始时间之前最新的数据版本),因此事务不会读取其他事务尚未提交的修改。
- 当一个事务执行写操作时,为要修改的数据行创建一个新的版本,并将修改后的数据写入数据库。
- 当一个事务提交时,它所做的修改将成为数据库的最新版本,并且对其他事务可见。
- 当一个事务回滚时,它所做的修改将被撤销,对其他事务不可见。
MVCC 会定期进行版本的回收。回收机制会删除已经不再需要的旧版本数据,从而释放空间。
当前读和快照读
快照读(一致性非锁定读)
读取的行记录要存在写操作,则读取版本快照
对于Rc->每次读取的是被锁定行的最新快照
对于Rr->每次读取都是事物开始时间之前最新的数据版本快照
当前读(一致性锁定读)
读取的是数据的最新版本,这种读也被称为 当前读(current read)
。锁定读会对读取到的记录加锁
读操作:对记录加 S
锁,其它事务也可以加S
锁,如果加 x
锁则会被阻塞
写/删:对记录加 X
锁,且其它事务不能加任何锁