深入理解MYSQL事务与锁机制

深入理解MYSQL事务与锁机制

事务及其ACID属性

事务是由一组SQL语句组成的逻辑处理单元。
事务具有以下4个属性,通常简称为事务的ACID属性。
1、原子性(Atomicity) :事务是一个原子操作单元,其对数据的修改,要么全都执行,要么全都不执行。
2、 一致性(Consistent) :在事务开始和完成时,数据都必须保持一致状态。这意味着所有相关的数据规 则都必须应用于事务的修改,以保持数据的完整性。
3、隔离性(Isolation) :数据库系统提供一定的隔离机制,保证事务在不受外部并发操作影响的“独 立”环境执行。这意味着事务处理过程中的中间状态对外部是不可见的,反之亦然。
4、持久性(Durable) :事务完成之后,它对于数据的修改是永久性的,即使出现系统故障也能够保持。

原子性是从操作上强调整个事务的操作是不可分割的,相当于事务中所有的SQL等于一次操作,
要么一起成功,要么一起失败。
一致性是从数据完整性方面强调整个事务中的SQL数据操作的数据变化一致性,要么一起成功,要么一起失败。

并发事务处理带来的问题

更新丢失(Lost Update)或脏写

当两个或多个事务选择同一行,然后基于最初选定的值更新该行时,由于每个事务都不知道其他事务的存 在,就会发生丢失更新问题。
一句话: 最后的更新覆盖了由其他事务所做的更新。

脏读(Dirty Reads)

一个事务正在对一条记录做修改,在这个事务完成并提交前,这条记录的数据就处于不一致的状态;这 时,另一个事务也来读取同一条记录,如果不加控制,第二个事务读取了这些“脏”数据,并据此作进一步的 处理,就会产生未提交的数据依赖关系。这种现象被形象的叫做“脏读”。
一句话:事务A读取到了事务B已经修改但尚未提交的数据,还在这个数据基础上做了操作。此时,如果B 事务回滚,A读取的数据无效,不符合一致性要求。

不可重读(Non-Repeatable Reads)

一个事务在读取某些数据后的某个时间,再次读取以前读过的数据,却发现其读出的数据已经发生了改 变、或某些记录已经被删除了!这种现象就叫做“不可重复读”。
一句话:事务A内部的相同查询语句在不同时刻读出的结果不一致,不符合隔离性

幻读/虚读(Phantom Reads)

一个事务按相同的查询条件重新读取以前检索过的数据,却发现其他事务插入了满足其查询条件的新数 据,这种现象就称为“幻读”。
一句话:事务A读取到了事务B提交的新增数据,不符合隔离性

事务隔离级别

“脏读”、“不可重复读”和“幻读”,其实都是数据库读一致性问题,必须由数据库提供一定的事务隔离机制 来解决。
事务隔离级别
数据库的事务隔离越严格,并发副作用越小,但付出的代价也就越大,因为事务隔离实质上就是使事务在一定程度 上“串行化”进行,这显然与“并发”是矛盾的。 同时,不同的应用对读一致性和事务隔离程度的要求也是不同的,比如许多应用对“不可重复读"和“幻读”并不 敏感,可能更关心数据并发访问的能力。
查看当前数据库的事务隔离级别: show variables like ‘tx_isolation’;
设置事务隔离级别:set tx_isolation=‘REPEATABLE-READ’;
Mysql默认的事务隔离级别是可重复读(REPEATABLE-READ),用Spring开发程序时,如果不设置隔离级别默认用Mysql设置的隔 离级别,如果Spring设置了就用已经设置的隔离级别。

MYSQL8.0版本--设置会话级别的隔离级别:
   set SESSION TRANSACTION ISOLATION LEVEL  READ UNCOMMITTED;
   set SESSION TRANSACTION ISOLATION LEVEL  READ COMMITTED;
   set SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ ;
   set SESSION TRANSACTION ISOLATION LEVEL SERIALIZABLE ;
   事务脚本:
   	BEGIN;
   	SQL脚本;
   	COMMIT;

MYSQL锁详解

锁是计算机协调多个进程或线程并发访问某一资源的机制。 在数据库中,除了传统的计算资源(如CPU、RAM、I/O等)的争用以外,数据也是一种供需要用户共享的资 源。如何保证数据并发访问的一致性、有效性是所有数据库必须解决的一个问题,锁冲突也是影响数据库并发 访问性能的一个重要因素。

锁分类

1、从性能上分为乐观锁(用版本对比来实现)和悲观锁
2、从对数据库操作的类型分,分为读锁和写锁(都属于悲观锁) 读锁(共享锁,S锁(Shared)):针对同一份数据,多个读操作可以同时进行而不会互相影响 写锁(排它锁,X锁(eXclusive)):当前写操作没有完成前,它会阻断其他写锁和读锁
3、从对数据操作的粒度分,分为表锁行锁

表锁

每次操作锁住整张表。开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低; 一般用在整表数据迁移的场景。
手动操作:

手动增加表锁 
  lock table 表名称 read(write),表名称2 read(write); 
查看表上加过的锁
  show open tables; 
删除表锁 
   unlock tables;

1、对MyISAM表的读操作(加读锁) ,不会阻寒其他进程对同一表的读请求,但会阻赛对同一表的写请求。只有当 读锁释放后,才会执行其它进程的写操作。
2、对MylSAM表的写操作(加写锁) ,会阻塞其他进程对同一表的读和写操作,只有当写锁释放后,才会执行其它进 程的读写操作

行锁

每次操作锁住一行数据。开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度最高。
InnoDB与MYISAM的最大不同有两点:
InnoDB支持事务(TRANSACTION)
InnoDB支持行级锁。

MyISAM在执行查询语句SELECT前,会自动给涉及的所有表加读锁,在执行update、insert、delete操作会自 动给涉及的表加写锁。
InnoDB在执行查询语句SELECT时(非串行隔离级别),不会加锁。但是update、insert、delete操作会加行 锁。
简而言之,就是读锁会阻塞写,但是不会阻塞读。而写锁则会把读和写都阻塞。

间隙锁(Gap Lock)

间隙锁,锁的就是两个值之间的空隙。
间隙可能跨越单个索引值、多个索引值,甚至是空的。Mysql默认级别是repeatable-read,间隙锁在某些情况下可以解决幻读问题。间隙锁是在可重复读隔离级别下才会生效。
如下:
CREATE TABLE t_student (
id int NOT NULL AUTO_INCREMENT COMMENT ‘学号’,
name varchar(10) DEFAULT NULL COMMENT ‘学生姓名’,
time datetime DEFAULT NULL COMMENT ‘时间’,
PRIMARY KEY (id)
) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8;
data
上表中存在间隙区间(3,10)(10,20)(20,50)(50,正无穷)。
如果此时有个session:
UPDATE t_student SET name = ‘老王’ WHERE id > 5 and id < 15;
那么其他session,此时是不能对这个区间的数据进行修改或插入的。也就是说(3,20]区间都无法进行修改和插入。并且20都是不可以的。
详情可参考官方文档
Gap Locks

临键锁(Next-key Locks)

Next-Key Locks是行锁与间隙锁的组合。像上面那个例子里的这个(3,20]的整个区间可以叫做临键锁。
无索引行锁会升级为表锁
锁主要是加在索引上,如果对非索引字段更新,行锁可能会变表锁 session1 执行:
update t_student set time= ‘2021-12-31 00:00:00’ where name = ‘王五’;
session2 对该表任一行操作都会阻塞住 InnoDB的行锁是针对索引加的锁,不是针对记录加的锁。并且该索引不能失效,否则都会从行锁升级为表锁。

手动锁定某一行还可以用lock in share mode(共享锁) 和for update(排它锁)
如:select * from t_student  where id= 2 for update;

Innodb存储引擎由于实现了行级锁定,虽然在锁定机制的实现方面所带来的性能损耗可能比表级锁定会要更 高一下,但是在整体并发处理能力方面要远远优于MYISAM的表级锁定的。当系统并发量高的时候,Innodb 的整体性能和MYISAM相比就会有比较明显的优势了。 但是,Innodb的行级锁定同样也有其脆弱的一面,当我们使用不当的时候,可能会让Innodb的整体性能表现 不仅不能比MYISAM高,甚至可能会更差。

行锁分析
通过检查InnoDB_row_lock状态变量来分析系统上的行锁的争夺情况

show status like 'innodb_row_lock%'; 

data2

对各个状态量的说明如下:
Innodb_row_lock_current_waits: 当前正在等待锁定的数量
Innodb_row_lock_time: 从系统启动到现在锁定总时间长度
Innodb_row_lock_time_avg: 每次等待所花平均时间
Innodb_row_lock_time_max:从系统启动到现在等待最长的一次所花时间
Innodb_row_lock_waits:系统启动后到现在总共等待的次数 对于这5个状态变量,
比较重要的主要是:
Innodb_row_lock_time_avg (等待平均时长)
Innodb_row_lock_waits (等待总次数)
Innodb_row_lock_time(等待总时长) 尤其是当等待次数很高,而且每次等待时长也不小的时候,我们就需要分析系统中为什么会有如此多的等待, 然后根据分析结果着手制定优化计划。

查看INFORMATION_SCHEMA系统库锁相关数据表

‐‐ 查看事务 
 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;
死锁

set tx_isolation=‘repeatable-read’;
Session_1执行:select * from account where id=1 for update;
Session_2执行:select * from account where id=2 for update;
Session_1执行:select * from account where id=2 for update;
Session_2执行:select * from account where id=1 for update;
查看近期死锁日志信息:show engine innodb status\G; 大多数情况mysql可以自动检测死锁并回滚产生死锁的那个事务,但是有些情况mysql没法自动检测死锁
锁优化建议
1、尽可能让所有数据检索都通过索引来完成,避免无索引行锁升级为表锁
2、合理设计索引,尽量缩小锁的范围
3、尽可能减少检索条件范围,避免间隙锁
4、尽量控制事务大小,减少锁定资源量和时间长度,涉及事务加锁的sql尽量放在事务最后执行
5、尽可能低级别事务隔离

MVCC(Multi-Version Concurrency Control)

我们知道,Mysql在可重复读隔离级别下可以保证事务较高的隔离性。
在同一个事务中,同样的sql查询语句在多次执行查询结果是相同的,即其它事务对数据有修改也不会影响当前事务sql语句的查询结果,除非自己事务内部修改了数据。 这个隔离性就是靠MVCC(Multi-Version Concurrency Control)机制来保证的,对一行数据的读和写两个操作默认是不会通过加锁互斥来保证隔离性,避免了频繁加锁互斥,而在串行化隔离级别为了保证较高的隔离性是通过将所有操作加锁互斥来实现的。 Mysql在读已提交和可重复读隔离级别下都实现了MVCC机制。
MVCC(Multi-Version Concurrency Control) 全称叫多版本并发控制。下面介绍具体原理:

undo日志版本链与read view机制

undo日志版本链是指一行数据被多个事务依次修改过后,在每个事务修改完后,Mysql会保留修改前的数据undo回滚 日志,并且用两个隐藏字段trx_id(事务id)和roll_pointer把这些undo日志串联起来形成一个历史记录版本链,如图:
undo日志

可重复读隔离级别,当事务开启,执行任何查询sql时会生成当前事务的一致性视图read-view,该视图在事务结束 之前都不会变化(如果是读已提交隔离级别在每次执行查询sql时都会重新生成),这个视图由执行查询时所有未提交事务id数组(数组里最小的id为min_id)和已创建的最大事务id(max_id)组成,事务里的任何sql查询结果需要从对应 版本链里的最新数据开始逐条跟read-view做比对从而得到最终的快照结果。
如上图:
红色代表事务还没结束,绿色代表事务已结束,黄色代表当前事务。
可重复读隔离级别下时:
1、事务2开启时,事务4已提交完成的最大事务id,事务3还未结束,此时事务2的一致性视图为[2,3]+4.
2、此时事务2第一次查询时,查询的数据则为name=tom1,即使事务3将数据更改为jack,但此数据对于事务2是不可见的。
3、数据更新后name=james,此时还未提交;
4、第二次查询时,由于在本事务中已经更新为james了,此时查得得是james,即使事务3此时已经提交,因为在可重复读隔离级别下,执行任何查询sql时会生成当前事务的一致性视图read-view,该视图在事务结束之前都不会变化。
5、然后将name=james改为tom,记录undo日志。

读已提交隔离级别下时:
1、事务2开启时,事务4已提交完成的最大事务id,事务3还未结束,此时事务2的一致性视图为[2,3]+4.
2、此时事务2第一次查询时,查询的数据则为name=tom1,即使事务3将数据更改为jack,但此数据对于事务2是不可见的。
3、数据更新后name=james,此时还未提交;
4、第二次查询时,由于事务3将数据提交更新为jack2了,此时查得得是jack2,因为在可重复读隔离级别下,在每次执行查询sql时都会重新生成一致性视图read-view
5、然后将name=james改为tom,记录undo日志。

版本链比对规则:
规则

  1. 如果 row 的 trx_id 落在绿色部分( trx_id<min_id ),表示这个版本是已提交的事务生成的,这个数据是可见的;
  2. 如果 row 的 trx_id 落在灰色部分( trx_id>max_id ),表示这个版本是由将来启动的事务生成的,是不可见的(若 row 的 trx_id 就是当前自己的事务是可见的);
  3. 如果 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的。

总结:
MVCC机制的实现就是通过read-view机制与undo版本链比对机制,使得不同的事务会根据数据版本链对比规则读取 同一条数据在版本链上的不同版本数据。

Innodb引擎SQL执行的BufferPool缓存机制

buffer pool
在mysql流程那张已经说过就不重复了,可参考:
mysql执行流程
重点说明引擎层buffer pool流程:
1、加载数据到Buffer Pool.
2、保存旧值,写入undo日志(便于数据回滚)。
3、更新数据到Buffer Pool。
4、写入redo日志。
5、准备提交事务,redo日志写入磁盘
6、准备提交事务,写入binlog日志(用于恢复磁盘数据和主从同步),记录磁盘。
7、标记提交事务,redo和binlog同步事务状态。
8、I/O线程以page为单位将更新后的Buffer Pool 数据随机I/O写入磁盘

redo日志
也是记录cud操作日志,其作用:防止数据在刷新buffer pool过程中,由于数据库宕机丢失数据。
MYSQL数据操作基于buffer pool操作的,如果在提交事务后数据宕机了,那么在数据库重启时,就会将redo日志的数据操作恢复到buffer pool中,保证数据的完整性。

Binlog日志
Binlog属于server层日志,主要记录cud操作,由于是server层,所以是可以用于外部使用的,需要主动配置开启,会额外耗数据库服务资源。
作用:
1、Binlog是追加记录数据库cud操作,主要用于在恶意或意外删除数据时对磁盘数据恢复
2、主从数据同步

为什么Mysql不能直接更新磁盘上的数据而且设置这么一套复杂的机制来执行SQL了?
因为来一个请求就直接对磁盘文件进行随机读写,然后更新磁盘文件里的数据性能可能相当差。
因为磁盘随机读写的性能是非常差的,所以直接更新磁盘文件是不能让数据库抗住很高并发的。 Mysql这套机制看起来复杂,但它可以保证每个更新请求都是更新内存BufferPool,然后顺序写日志文件,同时还能保证各种异常情况下的数据一致性。 更新内存的性能是极高的,然后顺序写磁盘上的日志文件的性能也是非常高的,要远高于随机读写磁盘文件。 正是通过这套机制,才能让我们的MySQL数据库在较高配置的机器上每秒可以抗下几干的读写请求。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值