MySQL的事务、锁和MVCC机制
# MySQL的事务、锁和MVCC机制
# 一、简介
数据库设计了事务隔离机制、锁机制、MVCC机制,来解决多事务并发导致的脏写、脏读、不可重复读、幻读这些问题
脏写或 更新丢失(Lost Update)
事务A覆盖了事务B所做的更新
脏读(Dirty Read)
事务A读到了事务B已经修改但尚未提交的数据
不可重读(Non-Repeatable Read)
事务A多次查询的结果不一致,不符合隔离性
幻读(Phantom Read)
事务A读到了事务B提交的新增数据,不符合隔离性
# 二、事务隔离机制
# 2.1、事务的特性--ACID
原子性(Atomicity) :
事务是一个原子操作,其对数据的修改,要么全都执行,要么全都不执行。
一致性(Consistent) :
这里的一致性是指数据的一致性,例如:商品卖出后,对应的库存会减少,这里的数据是一致的
隔离性(Isolation) :
数据库系统提供一定的隔离机制,保证事务处理过程中的中间状态对外部是不可见的
持久性(Durable) :
事务完成之后,它对于数据的修改是永久性的,即使出现系统故障也能够保持
# 2.2、事务的隔离级别
事务的隔离级别有4种,由低到高分别为 read-uncommitted 、read-committed 、repeatable-read 、serializable
间隙锁(Gap Lock)
间隙锁,锁的就是两个值之间的空隙。
Mysql默认级别是repeatable-read,有办法解决幻读问题吗?间隙锁在某些情况下可以解决幻读问题。
假如一个表存在1,2,3,10,20这五条数据,那么间隙就有 id 为 (3,10),(10,20),(20,正无穷) 这三个区间,
在Session_1下面执行 update account set name = 'zhuge' where id > 8 and id <18;,则其他Session没
法在这个范围所包含的所有行记录(包括间隙行记录)以及行记录所在的间隙里插入或修改任何数据,即id在
(3,20]区间都无法修改数据,注意最后那个20也是包含在内的。
间隙锁是在可重复读隔离级别下才会生效。
临键锁(Next-key Locks)
Next-Key Locks是行锁与间隙锁的组合。像上面那个例子里的这个(3,20]的整个区间可以叫做临键锁
查看当前数据库的事务隔离级别:
show variables like 'tx_isolation'
;设置事务隔离级别:
set tx_isolation='REPEATABLE-READ'
;Mysql默认的事务隔离级别是可重复读,用Spring开发程序时,如果不设置隔离级别默认用Mysql设置的隔离级别,如果Spring设置了就用已经设置的隔离级别
# 三、锁机制
# 3.1、锁分类
从性能上分为乐观锁(用版本对比来实现)和悲观锁
从对数据库操作的类型分为读锁和写锁(都属于悲观锁)
读锁(共享锁,S锁(Shared)):针对同一份数据,多个读操作可以同时进行而不会互相影响
写锁(排它锁,X锁(exclusive)):当前写操作没有完成前,它会阻断其他写锁和读锁
从对数据操作的粒度分为表锁和行锁
# 3.2、表锁
每次操作锁住整张表。开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低;一般用在整表数据迁移的场景。
- 手动增加表锁 lock table 表名称 read(write),表名称2 read(write);
- 查看表上加过的锁 show open tables;
- 删除表锁 unlock tables;
# 3.3、行锁
每次操作锁住一行数据。开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度最高。
无索引行锁会升级为表锁
锁主要是加在索引上,如果对非索引字段更新,行锁可能会变表锁
session1 执行:update account set balance = 800 where name = 'lilei';
session2 对该表任一行操作都会阻塞住
InnoDB的行锁是针对索引加的锁,不是针对记录加的锁。并且该索引不能失效,否则都会从行锁升级为表锁。
# 3.3、锁相关信息查看
-- 查看事务
select * from INFORMATION_SCHEMA.INNODB_TRX;
-- 查看锁
select * from INFORMATION_SCHEMA.INNODB_LOCKS;
-- 查看锁等待
select * from INFORMATION_SCHEMA.INNODB_LOCK_WAITS;
-- 释放锁,trx_mysql_thread_id可以从INNODB_TRX表里查看到
kill trx_mysql_thread_id
-- 查看锁等待详细信息
show engine innodb status\G;
2
3
4
5
6
7
8
9
10
11
12
总结:
MyISAM在执行查询语句SELECT前,会自动给涉及的所有表加读锁,在执行update、insert、delete操作会自动给涉及的表加写锁。
InnoDB在执行查询语句SELECT时(非串行隔离级别),不会加锁。但是update、insert、delete操作会加行锁。
简而言之,就是读锁会阻塞写,但是不会阻塞读。而写锁则会把读和写都阻塞。
# 四、MVCC机制
多版本并发控制(Multi Version Concurrency Control),简称 MVCC,通过ReadView机制与undo日志版本链比对机制,使得不同的事务会根据数据版本链对比规则读取同一条数据在版本链上的不同版本数据。
MySQL 在读已提交和可重复读隔离级别下都实现了MVCC机制。
# 4.1、undo日志版本链
undo日志版本链是指一行数据被多个事务依次修改过后,在每个事务修改完后,Mysql会保留修改前的数据undo回滚日志,并且用两个隐藏字段trx_id和roll_pointer把这些undo日志串联起来形成一个历史记录版本链
# 4.2、ReadView机制
在可重复读隔离级别,当事务开启,执行任何查询sql时会生成当前事务的一致性视图ReadView(RC级别每次执行查询SQL都会生成),这个视图由所有未提交事务id数组(数组里最小的id为min_id)和已创建的最大事务id(max_id)组成,事务里的任何sql查询结果需要从对应版本链里的最新数据开始逐条跟read-view做比对从而得到最终的快照结果。
版本链比对规则:
如果 row 的 trx_id 落在绿色部分( trx_id<min_id ),表示这个版本是已提交的事务生成的,这个数据是可见的;
如果 row 的 trx_id 落在红色部分( trx_id>max_id ),表示这个版本是由将来启动的事务生成的,是不可见的(若 row 的 trx_id 就是当前自己的事务是可见的);
如果 row 的 trx_id 落在黄色部分(min_id <=trx_id<= max_id),那就包括两种情况
a. 若 row 的 trx_id 在视图数组中,表示这个版本是由还没提交的事务生成的,不可见(若 row 的 trx_id 就是当前自己的事务是可见的);
b. 若 row 的 trx_id 不在视图数组中,表示这个版本是已经提交了的事务生成的,可见。
对于删除的情况可以认为是update的特殊情况,会将版本链上最新的数据复制一份,然后将trx_id修改成删除操作的trx_id,同时在该条记录的头信息(record header)里的(deleted_flag)标记位写上true,来表示当前记录已经被删除,在查询时按照上面的规则查到对应的记录如果delete_flag标记位为true,意味着记录已被删除,则不返回数据。
注意:begin/start transaction 命令并不是一个事务的起点,在执行到它们之后的第一个修改操作InnoDB表的语句,事务才真正启动,才会向mysql申请事务id,mysql内部是严格按照事务的启动顺序来分配事务id的。