本篇文章主要简单描述一下Mysql事务的实现方式,MVCC机制,以及分析在不同事务隔离级别下,一条sql会加什么样的锁,如表锁,行锁,共享锁,排他锁等
一、事务的四大特性(ACID)
1.原子性(Atomicity)原子性是指事务包含的所有操作要么全部成功,要么全部失败回滚
2.一致性(Consistency)一致性是指事务必须使数据库从一个一致性状态变换到另一个一致性状态
3.隔离性(Isolation)多个并发事务之间要相互隔离。
4.持久性(Durability)持久性是指一个事务一旦被提交了,那么对数据库中的数据的改变就是永久性的
复制代码
二、事务的隔离级别
SQL标准定义了以下几种事务隔离级别
READ_UNCOMMITTED 读未提交:最低级别,一个事务可以读取另一个未提交事务的数据。幻想读、不可重复读和脏读都允许。
READ_COMMITTED 读已提交:一个事务要等另一个事务提交后才能读取数据。允许幻想读、不可重复读,不允许脏读。
REPEATABLE_READ 可重复读:在开始读取数据(事务开启)时,不再允许修改操作。允许幻想读,不允许不可重复读和脏读。
SERIALIZABLE 可串行化:*别,在该级别下,事务串行化顺序执行。幻想读、不可重复读和脏读都不允许。
复制代码
事务不同隔离级别引发的问题
1 脏读,脏读是指在一个事务处理过程里读取了另一个未提交的事务中的数据。
2 不可重复读,不可重复读是指在对于数据库中的某个数据,一个事务范围内多次查询却返回了不同的数据值,这是由于在查询间隔,被另一个事务修改并提交了。
3 幻读,幻读和不可重复读都是读取了另一条已经提交的事务(这点就脏读不同),所不同的是不可重复读查询的都是同一个数据项,而幻读针对的是一批数据整体。
复制代码
MySQL数据库中,默认的隔离级别为Repeatable read (可重复读);
三、事务的实现方式
事务的实现可通过无锁的读和加锁两种方式。
首先要了解一下MVCC机制:
MVCC (Multiversion Concurrency Control),即多版本并发控制技术,它使得大部分支持行锁的事务引擎,不再单纯的使用行锁来进行数据库的并发控制,取而代之的是把数据库的行锁与行的多个版本结合起来,只需要很小的开销,就可以实现非锁定读,从而大大提高数据库系统的并发性能
MVCC的理念是采用无锁的读的方式大大降低并发的开销,但完全无锁很难实现。
在MVCC中读操作可以分为两种:快照读和当前读。
- 快照读:简单的Select操作,不加锁(在隔离级别为序列化时,MySQL会取消快照读,所有的读操作也会加读锁)
例:select * from table where ?;
其中:
Read Committed(读已提交)隔离级别:每次select都生成一个快照读。
Read Repeatable(可重复读)隔离级别:开启事务后第一个select语句才是快照读的地方,
而不是一开启事务就快照读,之后每次读取为同一个快照。
复制代码
- 当前读:特殊的读操作,Insert、Update、Delete操作,需要加锁
例:
select * from table where ? lock in share mode;(共享读锁)
select * from table where ? for update(排他锁);
insert into table values (…)(插入意向锁);
update table set ? where ?(排他锁);
delete from table where ?(排他锁);
复制代码
为什么将插入/更新/删除操作,都归为当前读?我们看下一个Update操作的具体流程。
当Update SQL被发给MySQL后,MySQL Server会根据where条件,读取第一条满足条件的记录,然后InnoDB引擎会将第一条记录返回,并加锁 (共享锁)。待MySQL Server收到这条加锁的记录之后,会再发起一个Update请求,申请升级为排他锁,更新这条记录。一条记录操作完成,再读取下一条记录,直至没有满足条件的记录为止。因此,Update操作内部,就包含了一个当前读。同理,Delete操作也一样。Insert操作会稍微有些不同,简单来说,就是Insert操作可能会触发Unique Key的冲突检查(插入意向锁),也会进行一个当前读。
MVCC主要是针对RR级别做的,我们主要来看看InnoDB中的MVCC。 在这种情况下,每行记录还会保存了两个不同的时间点在两个隐藏的列中,第一列保存的是行的创建时间,第二行保存的是行的删除时间(未删除是undefined);这里存储的并不是实际的时间,而是事务的ID(系统的版本号),每开启一个事务,系统的版本号就会自动加1。
然后我们来看看不同操作时MVCC机制的体现:
Insert:InnoDB在MVCC机制中,会在创建时间隐藏列更新为当前的事务ID,删除时间列是undefined
Select:MVCC中只会返回创建时间小于或等于当前事务ID且删除时间要么是undefined要么大于当前事务ID的记录,两个条件都满足的记录才是正确的
Delete:MVCC会在删除时间列更新当前事务ID,然后真正删除是由MySQL后台运行的独立线程去定时清理的
Update:MVCC中的Update被拆分成了Insert和Delete操作,显示插入一个更新后的记录(主键可能重复),然后标记原始记录,等待其被删除
关于MySQL对于MVCC的实现想了解
四、MySQL中的锁
MySQL 锁分成两类:锁类型(lock_type)和锁模式(lock_mode),锁模式和锁类型通常结合使用
锁类型描述的锁的粒度,也可以说是把锁具体加在什么地方,如行锁,表锁,间隙锁;
表锁: 操作对象是数据表。Mysql大多数锁策略都支持(常见mysql innodb),是系统开销最低但并发性最低的一个锁策略。事务对整个表加读锁,则其他事务可读不可写,若加写锁,则其他事务增删改都不行。
行级锁: 操作对象是数据表中的一行。是MVCC技术用的比较多的,但在MYISAM用不了,行级锁用mysql的储存引擎实现而不是mysql服务器。但行级锁对系统开销较大,处理高并发较好。
其中行锁细分为间隙锁(锁住两条记录的间隙,用于防止幻读),记录锁(普通的锁一条记录),Next-Key锁(间隙锁和记录锁的结合),插入意向GAP锁(插入时的锁,只与间隙锁冲突)
InnoDB存储引擎支持行锁和表锁,MyISAM只支持表锁
锁模式描述的是到底加的是什么锁,譬如读锁或写锁。
读锁: 也叫共享锁、S锁,若事务T对数据对象A加上读锁,则事务T可以读A但不能修改A(修改需尝试升级为写锁),其他事务只能再对A加读锁,而不能加写锁,直到事务T释放A上的读锁。这保证了其他事务可以读A,但在T释放A上的S锁之前不能对A做任何修改。
写锁: 又称排他锁、X锁。若事务T对数据对象A加上写锁,事务T可以读A也可以修改A,其他事务不能再对A加任何锁(读写都不行),直到事务T释放A上的锁。这保证了其他事务在T释放A上的锁之前不能再修改A。
五、实践分析任一条sql加锁类型
首先MyISAM存储引擎不支持事务,且只支持表锁,故一般用于主从集群中的从库(读库),此文不做分析,InnoDB存储引擎支持事务,且支持表锁,行锁,间隙锁,意向锁等
在InnoDB存储引擎下, RC(读已提交)隔离级别不支持间隙锁,RR(可重复读)隔离级别支持间隙锁,Next-key 锁,我们这里分析RR隔离级别,同样的SQL在RC隔离级别只需去除间隙锁即可。
前提:所有的锁都是在开启事务的情况下才会加锁,没有开始事务不会加任何锁
在具体分析之前,先说一下结论:在MySQL中,InnoDB存储引擎下,RR(可重复读)隔离级别情况下,加锁种类最多,也是安装MySQL后默认的配置,故以此场景下分析,其次加锁的粒度需要找到对应的记录才能加行锁,故需要使用索引,在没有索引或有索引但是没有生效的情况下会锁住整张表(即表锁),引发线上灾难,严禁UPDATE或DELETE数据时WHERE条件中未使用索引。
关于查看一条sql是否使用索引可以使用Explain关键字查看
场景一:select * from table where id = 1;
不加锁,简单的查询语句是快照读,自身不加锁,也不会被其他加锁的sql阻塞。
场景二:select * from table where id = 1 lock in share mode;
若id是主键,对id为1的这条记录加共享锁,其他事务可也加共享锁,不可加排他锁。
场景三:select * from table where id = 1 for upfate;
若id是主键,先对id为1的这条记录加共享锁,然后尝试升级为排他锁,若升级为排他锁时被其他事务加了共享锁则被阻塞,加锁成功后其他事务不可加共享锁,不可加排他锁。
场景四:update user set username = 2 where id = 1;
若ID是主键,则与场景三相同,先加共享锁,后升级为排他锁。
场景五:update user set username = 2 where name = 1;
若name不是主键,是唯一索引,则先通过name=1在唯一索引上加锁,后找到name=1的主键 ID,再去聚簇索引(即主键索引)上加锁。
关于MySQL聚族索引,二级索引的区别
场景二到场景五中:若通过主键或唯一索引查找这条记录不存在时,则对此位置加间隙锁,此时插入对应条件的的数据会尝试加插入意向GAP锁会被阻塞,这防止了幻读的发生
场景六:update user set name = 2 where username = 5;
若username不是主键,不是唯一索引,加了普通索引,则先在普通索引上找到对应的记录,加排他锁,并在其索引附近间隙加GAP锁(间隙锁),然后找到对应的主键索引加排他锁,其中间隙锁的范围需实际分析,若数据库数据为,username: 2,3,8,9,则间隙锁范围为[3,8],左闭右闭。
场景七:update user set name = 2 where username > 5;
与场景六的区别在于,范围扫描,当范围扫描结果很多时,mysql优化会放弃使用索引进行全表扫描,若数量较少会使用索引,需用Explain关键字实测,若使用了索引,则会扫描符合条件的记录,扫描到第一条后执行场景六的操作,之后继续扫描下一条记录直到扫描到不满足条件的记录停止加锁。
场景八:update user set name = 2 where username = 5;
若username没有索引或执行计划中未使用索引,则进行全表扫描,加表锁以及全表间隙锁,其他事务不能更新数据,不能插入数据,不能删除数据,只能进行快照读(无锁操作),直至次事务执行完毕释放锁。