MVCC解决的主要是读的问题,写的问题解决就是加锁
事物
我们把需要保证原子性、隔离性、一致性和持久性的一个或多个数据库操作称之为一个事务。
事物的ACID属性
原子性(Atomicity)
原子性是指事务是一个不可分割的工作单位,事务中的操作要么都发生,要么都不发生。
【转账操作是一个不可分割的操作,要么转失败,要么转成功】
一致性(Consistency)
事务必须使数据库从一个一致性状态变换到另外一个一致性状态。
【每次转账完成后,都需要保证系统的余额等于所有账户的收入减去所有账户的支出】
隔离性(Isolation)
事务的隔离性是指一个事务的执行不能被其他事务干扰,即一个事务内部的操作及使用的数据对并发的其他事务是隔离的,并发执行的各个事务之间不能互相干扰。
【小明向小强转10元, 小明向小红转10元。 这两个操作不能相互影响】
持久性(Durability)
持久性是指一个事务一旦被提交,它对数据库中数据的改变就是永久性的,接下来的其他操作和数据库故障不应该对其有任何影响。
事物提交
自动提交
默认情况下,如果我们不显式的使用START TRANSACTION或者BEGIN语句开启一个事务,系统变量autocommit是自动开启的,那么每一条语句都算是一个独立的事务,这种特性称之为事务的自动提交。
关闭自动提交的方法:
- START TRANSACTION或者BEGIN语句开启一个事务
这样在本次事务提交或者回滚前会暂时关闭掉自动提交的功能 - 把系统变量autocommit值设置为OFF
SET autocommit = OFF;
手动终止/事物回滚
rollback
这里需要强调一下,ROLLBACK语句是我们程序员手动的去回滚事务时才去使用的,如果事务在执行过程中遇到了某些错误而无法继续执行的话,事务自身会自动的回滚
隐式提交
当自动提交关闭,不只是commit才会提交事物, 我们输入某些语句事物也会悄悄提交,这些会导致事物隐式提交的语句包括:
- 定义或修改数据库对象的数据定义语言(Data definition language,缩写为:DDL)
ALTER、CREATE、DROP - 隐式使用或修改mysql数据库中的表
当我们使用ALTER USER、CREATE USER、GRANT、RENAME USER、SET PASSWORD等语句也会隐式提交前面语句所属事物 - 事务控制或关于锁定的语句
当我们在一个事务还没有提交或者回滚时就又使用START TRANSACTION或者BEGIN语句开启另一个事务,会隐式的提交上一个事物 - 加载数据的语句
使用LOAD DATA来批量往数据库中导入数据时,也会隐式的提交前边语句所属的事务 - 其它的一些语句
使用ANALYZE TABLE、CACHE INDEX、CHECK TABLE、FLUSH、 LOAD INDEX INTO CACHE、OPTIMIZE TABLE、REPAIR TABLE、RESET等语句也会隐式的提交前边语句所属的事务。
保存点
如果你开启了一个事务,并且已经敲了很多语句,忽然发现上一条语句有点问题,你只好使用ROLLBACK语句来让数据库状态恢复到事务执行之前的样子,然后一切从头再来,总有一种一夜回到解放前的感觉。
所以MYSQL提出了一个保存点(英文:savepoint)的概念,就是在事务对应的数据库语句中打几个点,我们在调用ROLLBACK语句时可以指定回滚到哪个点,而不是回到最初的原点。定义保存点的语法如下:
SAVEPOINT 保存点名称;
ROLLBACK [WORK] TO [SAVEPOINT] 保存点名称;
隔离性
-- 修改隔离级别
mysql> set session transaction isolation level read uncommitted;
-- 查看隔离级别
mysql> select @@tx_isolation;
隔离级别
读未提交(Read Uncommitted)
一个事务可以读到其他事务还没有提交的数据,会出现脏读。
读已提交(Read Committed)
一个事务只能读到另一个已经提交的事务修改过的数据,并且其他事务每对该数据进行一次修改并提交后,该事务都能查询得到最新值,会出现不可重复读、幻读。
幻读:一个事务根据某些条件查询出一些记录,之后另一个事务又向表中插入了符合这些条件的记录,原先的事务再次查询时,能把另一个事务插入的记录也读出来。
就是两次查询时的行数变多了!
可重复读(Repeatable read)
一个事务第一次读过某条记录后,即使其他事务修改了该记录的值并且提交,该事务之后再读该条记录时,读到的仍是第一次读到的值,而不是每次都读到不同的数据,
这就是可重复读,这种隔离级别解决了不可重复,但是还是会出现幻读。
特别需要注意的是MySQL在REPEATABLE READ隔离级别下,是可以禁止幻读问题的发生的。
串性化(Serilizable)
串行化就是对同一条记录的操作都是串行的,也就是不允许读-写,写-读的并发操作, (读-读是允许并发操作的)
MVCC 多版本并发控制
MVCC(Multi-Version Concurrency Control,多版本并发控制),指的就是在读已提交和可重复读级别的事务在执行普通的select操作时访问记录的版本链的过程。
读已提交和可重复读的不同在于:生成ReadView的时机不同:
-
读已提交 每一次进行select前都会生成一个ReadView
读已提交 就是每次select都会生成readView,然后就去版本链里找已经提交的事务
-
可重复读 只是在第一次进行select操作前生成一个readView,之后的查询操作都用这个
可重复读 只会在第一次select的时候生成readView,第二次select的时候还是用的之前生成的readView,所以即使已经有已经提交的事务了,我们读的是之前的readView ,所以就实现了可重复读。
select readView [100,200] 200 commit select readView [100,200] //用的是之前select的readView 并没有生成
MVCC可以使不同事物的读-写、写-读操作并发执行,从而提高系统系统。 也就是
版本链
- 对于使用InnoDB存储引擎的表来说,它的主键索引记录中都包含两个必要的隐藏列(row_id并不是必要的,我们创建的表中有主键或者非NULL唯一键时都不会包含row_id列):
- trx_id:每次对某条记录进行改动时,都会把对应的事务id赋值给trx_id隐藏列
- roll_pointer:每次对某条记录进行改动时,这个隐藏列会存一个指针,可以通过这个指针找到该记录修改前的信息。
如下图就是一个版本链:
trix_id记录了每次对这一行进行更新的事务id,然后通过roll_pointer把之前的链上
ReadView
ReadView生成的时机是在使用select的时候。
ReadView的属性:
-
m_ids:表示在生成ReadView时当前系统中活跃的读写事务的事务id列表。也就是所有**未提交的事务 **
-
min_trx_id:表示在生成ReadView时当前系统中活跃的读写事务中最小的事务id,也就是m_ids中的最小值。
-
max_trx_id:表示生成ReadView时系统中应该分配给下一个事务的id值。
-
creator_trx_id:表示生成该ReadView的事务的事务id。
有了这个ReadView,这样在访问某条记录时,只需要按照下边的步骤判断记录的某个版本是否可见
- 如果被访问版本的trx_id属性值与ReadView中的creator_trx_id值相同,意味着当前事务在访问它自己修改过的记录,所以该版本可以被当前事务访问。
- 如果被访问版本的trx_id属性值小于ReadView中的min_trx_id值,表明生成该版本的事务在当前事务生成ReadView前已经提交,所以该版本可以被当前事务访问
- 如果被访问版本的trx_id属性值大于ReadView中的max_trx_id值,表明生成该版本的事务在当前事务生成ReadView后才开启,所以该版本不可以被当前事务访问。
- 如果被访问版本的trx_id属性值在ReadView的min_trx_id和max_trx_id之间,那就需要判断一下trx_id属性值是不是在m_ids列表中,如果在,说明创建ReadView时生成该版本的事务还是活跃的,该版本不可以被访问;如果不在,说明创建ReadView时生成该版本的事务已经被提交,该版本可以被访问。
锁
锁按照不同的分类方式可以分成以下几种
读锁与写锁
-
读锁:共享锁、Shared Locks、S锁。
A加了读锁,其他B不能加写锁,但是还是能加读锁。 -
写锁:排他锁、Exclusive Locks、X锁
A加了写锁,B既不能加读锁也不能加写锁。 -
select:InnoDB不会加任何锁
如果一个语句不涉及到加锁的话,那就不会涉及到锁的冲突问题,比如select 语句 什么时候都是能读的,这方面已经通过MVCC来处理了。
读操作
对于普通SELECT语句,InnoDB不会加任何锁。
select …… lock in share mode
对查出来的数据加读锁的语法:
select …… lock in share mode
将查找到的数据加上一个S锁,允许其他事务继续获取这些记录的S锁,不能获取这些记录的X锁(会阻塞)
使用场景:读出数据后,其他事务不能修改,但是自己也不一定能修改,因为其他事务也可以使用select … lock in share mode 继续加读锁
通俗来讲:
A加了读锁后,A可以进行update,其他事物不能进行update;但是当有另一个事物也加了读锁,那A也不能进行update了。
select …… for update
对查出来的数据加写锁
将查找到的数据加上一个X锁,不允许其他事务获取这些记录的S锁和X锁。
使用场景:读出数据后,其他事务即不能写,也不能加读锁,那么就导致只有自己可以修改数据
写操作
- DELETE:删除一条数据时,先对记录加X锁,再执行删除操作。
- INSERT:插入一条记录时,会先加隐式锁来保护这条新插入的记录在本事务提交前不被别的事务访问到。
隐式锁:一个事务插入一条记录后,还未提交,这条记录会保存本次事务id,而其他事务如果想来对这个记录加锁时会发现事务id不对应,这时会产生X锁,所以相当于在插入一条记录时,隐式的给这条记录加了一把隐式X锁。
也就是先不加锁,先加一个事务id(利用mvcc挡住),如果这时候有别的事务要进行更新操作,再加X锁。 - UPDATE
- 如果被更新的列,修改前后没有导致存储空间变化,那么会先给记录加X锁,再直接对记录进行修改。
- 如果被更新的列,修改前后导致存储空间发生了变化,那么会先给记录加X锁,然后将记录删掉,再Insert一条新记录。
行锁与表锁
行锁
- LOCK_REC_NOT_GAP:单个行记录上的锁
- LOCK_GAP:间隙锁,锁定一个范围,但不包括记录本身。
GAP锁的目的,是为了防止同一事务的两次当前读,出现幻读的情况。 - LOCK_ORDINARY:锁定一个范围,并且锁定记录本身。
对于行的查询,都是采用该方法,主要目的是解决幻读的问题
不同级别下加的锁
其实就是走索引了就会在走过的索引加锁。 因为只要走索引肯定都要回到聚集索引对应的那条记录。如果有走辅助索引的话,就在辅助索引那里也加。
然后全表查询的话,也就是没走索引,那就是读已提交只会对查出来的数据加锁,可重复读是对所有整个表的记录和间隙加锁。
READ COMMITTED级别下
-
查询用的是主键
就在主键值对应的那一条数据上加锁
-
查询使用的唯一索引
只需要对查询值所对应的唯一索引记录项和对应的聚集索引上的项加锁即可。
在唯一索引记录项加锁的原因:其他事务按索引查的时候就很快会发现已经被加锁了,不用到聚集索引那边才会判断出来,这样就能更快的发现锁冲突。
在聚集索引加锁的原因:聚集索引其实就存的是数据。当换一个查询条件,不会走上面的辅助索引进行查询时,但两者可能查询到的数据是同一条,所以要在聚集索引的记录处加锁。
-
查询使用的普通索引
查询使用的普通索引时,会对满足条件的索引记录都加上锁,同时对这些索引记录对应的聚集索引上的项也加锁。
我们通过看按e排好序的表如下:
因为e='6’查出了两条记录,分别是a=6和a=12,所以这两条记录都已经加了写锁。
当我们再插入e=‘6’ 时,插入进去,原来的事务再进行select就会多读出一行我们刚刚插入的数据,这就产生了幻读!(当前的隔离级别是读已提交)session2做的操作:
session1去读:
-
查询使用没有用到索引
查询的时候没有走索引,也只会对满足条件的记录加锁。
即我们用c='1’做查询条件的时候,不会用到索引,这时候就会进行全表查询,全表查询的时候会给整个表的所有记录都加锁,然后判断不符合条件的把锁都释放掉,所以最后的我们看到的结果是把查出来的符号条件的记录都加了锁。
REPEATABLE READ级别下
- 查询用的是主键
和READ COMMITTED级别一样 - 查询时用的唯一索引
和READ COMMITTED级别一样 - 查询使用的普通索引
为了解决幻读,在查出来的记录上加的是GAP锁。
这样只要是只要插入数据是在这些记录的间隙 是插不进去的,也就解决了幻读现象。 - 查询使用没有用到索引
会对表中所有的记录以及间隙加锁 (锁住了表)
因为这里是全表扫描, 我们会查询所有的数据,当我们对c的值更新成了符合条件的,那下一次查询就会多出这条数据,就又会出现幻读现象,所以要把整个表都加锁。
表锁
表级别的S锁、X锁
IS锁、IX锁
- IS锁:意向共享锁、Intention Shared Lock。当事务准备在某条记录上加S锁时,需要先在表级别加一个IS锁。
- IX锁,意向排他锁、Intention Exclusive Lock。当事务准备在某条记录上加X锁时,需要先在表级别加一个IX锁。
IS、IX锁是表级锁,它们的提出仅仅为了在之后加表级别的S锁和X锁时可以快速判断表中的记录是否被上锁,以避免用遍历的方式来查看表中有没有上锁的记录。
通俗来讲就是当我们有某一行数据加了写锁时,我们想在整个表加读锁是不能加的,但是知道不能加前提是要知道有行是加了写锁的,我们为了不再去遍历每行数据是否加了写锁,所以就在对这个行加写锁的时候,加一个表级别的IX锁。
AUTO-INC锁
有两种级别的实现方式:
- 在执行插入语句时就在表级别加一个AUTO-INC锁,然后为每条待插入记录的AUTO_INCREMENT修饰的列分配递增的值,在该语句执行结束后,再把AUTO-INC锁释放掉。这样一个事务在持有AUTO-INC锁的过程中,其他事务的插入语句都要被阻塞,可以保证一个语句中分配的递增值是连续的。
- 采用一个轻量级的锁,在为插入语句生成AUTO_INCREMENT修饰的列的值时获取一下这个轻量级锁,然后生成本次插入语句需要用到的AUTO_INCREMENT列的值之后,就把该轻量级锁释放掉,并不需要等到整个插入语句执行完才释放锁
悲观锁和乐观锁
- 悲观锁
上面的那些锁都是悲观锁,就是认为数据库会发生并发冲突,直接上来就把数据锁住,其他事务不能修改,直至提交了当前事务。 - 乐观锁
乐观锁是一种思想,认为不会锁定的情况下去更新数据,如果发现不对劲才不更新(回滚)。实现方式就是在数据库中添加一个version字段来实现。
在用的时候根据version看一下是不是我要用的版本,不是的话说明用之前被更新过,。
死锁
避免死锁:
-
以固定的顺序访问表和行
-
大事务拆小,大事务更容易产生死锁
-
在同一个事务中,尽可能做到一次锁定所需要的所有资源,减少死锁概率
-
降低隔离级别
-
为表添加合理的索引