概述:我们的数据库一般都会并发执行多个事务,多个事务可能会并发的对相同的一批数据进行增删改查操作,可能会导致我们所说的脏写、脏读、不可重复读、幻读这些问题。
为了解决这些问题,数据库设计了事务隔离机制、锁机制、MVCC多版本并发控制隔离机制,用一整套机制来解决多事务并发问题。
ACID属性:
原子性:事务的一个原子操作单元,对数据的修改要么全部执行,要么全部不执行
一致性:事务开始和完成时,数据都必须保持一致状态。所有数据规则都必须应用于事务的修改
隔离性:数据库系统提供了一定的隔离机制,保证事务在不受外部并发操作影响独立执行
持久性:事务完成后,对于数据的修改是永久性的,即使出现系统故障也能保持
并发带来的问题
更新丢失或脏写:当两个或多个事务选择同一行,然后基于最初选定的值更新该行时,由于事务都不知道其他事务的存在,就会发生丢失更新问题,最后的更新覆盖了其他事务所做的更新
脏读:事务A读取到了事务B已经修改但尚未提交的数据。一个事务正在对一条记录做修改,在这个事务完成并提交前,这条记录的数据就处于不一致的状态;这时,另一个事务也来读取同一条记录,如果不加控制,第二个事务读取了这些脏数据,并据此进一步的处理,就会产生未提交的数据依赖关系。这种现象叫做脏读。
不可重复读:一个事务读取某些数据后的某个时间,再次读取以前读过的数据,却发现其读出的数据已经发生了改变,或某些记录已经被删除。
幻读:事务A读取到了事务B提交的新增数据,不符合隔离性。
事务隔离级别
查看事务隔离级别:show variables like 'tx_isolation'; mysql8改为show variables like '%isolation'
设置事务隔离级别:set tx_isolation='REPEATABLE-READ';mysql8改为show variables like 'tx_isolation'
MYSQL默认是事务隔离级别是可重复读,如果spring设置了就用已经设置的隔离级别
锁
从性能上分为乐观锁(用版本对比实现)和悲观锁
从对数据操作的粒度分为表锁和行锁
从对数据库操作的类型分为读锁和写锁(都属于悲观锁),还有意向锁
读锁(共享锁,S锁-share):针对同一份数据,多个读操作可以同时进行而不会相互影响
写锁:排它锁,X锁:当前写操作没有完成前会阻断其他写锁和读锁,数据修改操作都会加写锁,查询也可以通过for update 加写锁,select X for update
意向锁:又称I锁,争对表锁,主要是为了提高加表锁的效率,是MySQL数据库自己加的。当有事务给表的数据行加了共享锁或排他锁,同时会给表设置一个标识,代表已经有了行锁了,其他事务想对表加表锁时,就不必逐行判断有没有行锁可能和表锁冲突,直接读这个标识就可以确定自己该不该加表锁。特别是表中的记录很多时,逐行判断加表锁的方式效率低,而这个标识就是意向锁。
意向锁:共享锁--排它锁
表锁
每次操作锁住整张表。开销小,加锁快,不会出现死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低;一般用在整表数据迁移的场景。
- 手动增加表锁
lock table 表名称 read(write),表名称2 read(write);
- 查看表上加过的锁
show open tables;
- 删除表锁
unlock tables;
加读锁:
当前session和其他session都可以读该表
当前session中插入或者更新锁定的表都会报错,其他session插入或更新则会等待
加写锁:
当前session对该表的增删改查都没有问题,其他session对该表的所有操作被阻塞
案例结论
1、对MyISAM表的读操作(加读锁) ,不会阻塞其他进程对同一表的读请求,但会阻塞对同一表的写请求。只有当读锁释放后,才会执行其它进程的写操作。
2、对MylSAM表的写操作(加写锁) ,会阻塞其他进程对同一表的读和写操作,只有当写锁释放后,才会执行其它进程的读写操作
行锁
每次操作锁住一行数据。开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度最高。
行锁演示
一个session开启事务更新不提交,另一个session更新同一条记录会阻塞,更新不同记录不会阻塞
总结:
InnoDB在执行查询语句SELECT时(非串行隔离级别),不会加锁。但是update、insert、delete操作会加行锁。
简而言之,就是读锁会阻塞写,但是不会阻塞读。而写锁则会把读和写都阻塞。
MVCC多版本并发控制机制
同样的sql查询在一个事务里多次执行查询结果相同,就算其他事务对数据有修改也不会影响当前事务sql语句的查询结果。这个隔离性就是靠MVCC机制来保证的,对一行数据的读和写两个操作默认是不会通过加锁互斥来保证隔离性,避免频繁加锁互斥,而在串行化隔离级别为了保证较高的隔离性是通过所有操作加锁互斥来实现的。
MySQL在读已提交和可重复读隔离级别下都实现了MVCC机制
undo日志版本链与read view机制详解
undo日志版本链是指一行数据被多个事务依次修改过后,在每个事务修改完成后,MySQL会保留修改前的数据undo回滚日志,并且用两个隐藏字段trx_id和roll_pointer把这些undo日志串联起来形成一个历史记录版本链
在可重复读隔离级别,当事务开启,执行任何查询SQL时会生成当前事务的一致性视图read-view,该视图在事务结束之前都不会变化(如果是读已提交隔离级别在每次执行查询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,同时在该条记录的头信息里的标记位写上true,来表示当前记录已经被删除,在查询时按照上面的规则查到对应的记录,如果标记位为true,意味着记录已被删除,则不返回数据。
注意:begin/start transaction命令并不是一个事务的起点,在执行到它们之后的第一个修改操作innodb表的语句,事务才真正的启动,才会向MySQL申请事务id,MySQL内部是严格按照事务的启动顺序来分配事务id的。
总结:MVCC机制的实现就是通过read-view机制和undo版本链对比机制,使得不同的事务会根据数据版本链对比规则读取同一条数据在版本链上的不同版本数据。
为什么Mysql不能直接更新磁盘上的数据而且设置这么一套复杂的机制来执行SQL了?
因为来一个请求就直接对磁盘文件进行随机读写,然后更新磁盘文件里的数据性能可能相当差。
因为磁盘随机读写的性能是非常差的,所以直接更新磁盘文件是不能让数据库抗住很高并发的。
Mysql这套机制看起来复杂,但它可以保证每个更新请求都是更新内存BufferPool,然后顺序写日志文件,同时还能保证各种异常情况下的数据一致性。
更新内存的性能是极高的,然后顺序写磁盘上的日志文件的性能也是非常高的,要远高于随机读写磁盘文件。
正是通过这套机制,才能让我们的MySQL数据库在较高配置的机器上每秒可以抗下几干甚至上万的读写请求。