【MySQL】深入理解MySQL事务隔离级别与锁机制

【MySQL】深入理解MySQL事务隔离级别与锁机制

MySQL事务及ACID特性详解

概述

事务及其ACID属性

原子性(Atomicity)

一致性(Consistent)

隔离性(Isolation)

持久性(Durable)

并发事务处理带来的问题

更新丢失(Lost Update)或脏写

脏读(Dirty Reads)

不可重读(Non-Repeatable Reads)

幻读(Phantom Reads)

MySQL事务隔离级别详解

四种隔离级别 

Spring中设置的隔离级别

XML方式

注解的方式

MySQL锁机制详解

MySQL中各种锁的分类

1. 按照性能来分类(乐观锁 和 悲观锁)

2. 按照数据库操作类型分类(读锁、写锁、意向锁)

3. 按照数据操作的粒度(表锁 和 行锁)

行锁与事务隔离级别案例分析

案例SQL准备

(1)读未提交案例

(2)读已提交案例

(3)可重复读案例

(4)串行化 

间隙锁(Gap Lock)

临键锁(Next-key Locks) 

总结!!!

行锁性能分析

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

死锁

MySQL锁优化建议

下一节——深入理解MVCC与BufferPoll缓存机制


MySQL事务及ACID特性详解

概述

我们的数据库一般都会并发执行多个事务,多个事务可能会并发的对相同的一批数据进行增删改查操作,可能就会导致我们说的脏写、脏读、不可重复读、幻读这些问题。

这些问题的本质都是数据库的多事务并发问题,为了解决多事务并发问题,数据库设计了事务隔离机制、锁机制、MVCC多版本并发控制隔离机制,用一整套机制来解决多事务并发问题。

事务及其ACID属性

事务是由一组SQL语句组成的逻辑处理单元,事务具有以下4个属性,通常简称为事务的ACID属性。

原子性(Atomicity)

事务是一个原子操作单元,其对数据的修改,要么全都执行,要么全都不执行。(同时成功,同时失败)

一致性(Consistent)

在事务开始和完成时,数据都必须保持一致状态。这意味着所有相关的数据规则都必须应用于事务的修改,以保持数据的完整性。(修改过的数据,必须都修改,不能有的改了,有的没有改)

隔离性(Isolation)

数据库系统提供一定的隔离机制,保证事务不受外部并发操作的影响。这意味着事务处理过程中的中间状态对外部是不可见的,反之亦然。

持久性(Durable)

事务完成之后,它对于数据的修改是永久性的,即使出现系统故障也能够保持。

并发事务处理带来的问题

更新丢失(Lost Update)或脏写

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

最后的更新覆盖了由其他事务所做的更新

脏读(Dirty Reads)

一个事务正在对一条记录做修改,在这个事务完成并提交前,这条记录的数据就处于不一致的状态;这时,另一个事务也来读取同一条记录,如果不加控制,第二个事务读取了这些“脏”数据,并据此作进一步的处理,就会产生未提交的数据依赖关系。这种现象被形象的叫做“脏读”。

事务A 读取到了 事务B 已经修改但尚未提交的数据,还在这个数据基础上做了操作。此时,如果B事务回滚(rollback),A读取的数据无效,不符合一致性要求

不可重读(Non-Repeatable Reads)

一个事务在读取某些数据后的某个时间,再次读取以前读过的数据,却发现其读出的数据已经发生了改变、或某些记录已经被删除了!这种现象就叫做“不可重复读”。

一个事务内 对于 相同的查询语句,其查询结果不一致

幻读(Phantom Reads)

一个事务按相同的查询条件重新读取以前检索过的数据,却发现其他事务插入了满足其查询条件的新数据,这种现象就称为“幻读”。

事务A读取到了事务B提交的新增数据,不符合隔离性

MySQL事务隔离级别详解

四种隔离级别 

上面我们提到的 “脏读”、“不可重复读”和“幻读”,其实都是数据库读一致性问题,必须由数据库提供一定的事务隔离机制来解决。

数据库的事务隔离越严格,并发副作用越小,但付出的代价也就越大(性能低!),因为事务隔离实质上就是使事务在一定程度上“串行化”进行,这显然与“并发”是矛盾的。 

同时,不同的应用对读一致性事务隔离程度的要求也是不同的,比如许多应用对“不可重复读"和“幻读”并不敏感,可能更关心数据并发访问的能力!

常看当前数据库的事务隔离级别 

-- 在 MySQL8.0.3 之前,现在也是被废了...
show variables like 'tx_isolation'

官网描述 

The tx_isolation and tx_read_only system variables have been removed. Use transaction_isolation and transaction_read_only instead.

MySQL :: MySQL 8.0 Reference Manual :: 1.3 What Is New in MySQL 8.0https://dev.mysql.com/doc/refman/8.0/en/mysql-nutshell.html

所以现在查看 | 设置事务隔离级别的方法如下: 

-- 查询隔离级别,REPEATABLE-READ
show variables like 'transaction_isolation';

-- 设置隔离级别
set transaction_isolation='REPEATABLE-READ';

MySQL默认的事务隔离级别是可重复读。

Spring中设置的隔离级别

Spring开发程序时,如果不设置隔离级别默认用MySQL设置的隔离级别(可重复读)。如果Spring设置了就用已经设置的隔离级别!

XML方式

<tx:advice id="advice" transaction-manager="transactionManager">
    <tx:attributes>
        <tx:method name="fun*" propagation="REQUIRED" isolation="DEFAULT"/>
 	</tx:attributes>
</tx:advice>

注解的方式

@Transactional(isolation=Isolation.DEFAULT)
public void fun(){
	dao.add();
	dao.udpate();
}

MySQL锁机制详解

在数据库中,除了传统的计算资源(如CPU、RAM、I/O等)的争用以外,数据也是一种供需要用户共享的资源。

如何保证数据并发访问的一致性、有效性是所有数据库必须解决的一个问题,锁冲突也是影响数据库并发访问性能的一个重要因素。

MySQL中各种锁的分类

1. 按照性能来分类(乐观锁 和 悲观锁)

乐观锁: 顾名思义,就是对数据的处理持乐观态度,乐观的认为数据一般情况下不会发生冲突,只有提交数据更新时,才会对数据是否冲突进行检测。

我们一般会采用添加版本号的方式来实现乐观锁。

在更新的时候set一下version+1,然后再where里面判断一下version是否符合,不符合则回滚。

<!-- 乐观锁更新 -->
<update id="updateCount">
    update
    goods_sale
    set count = #{record.count}, data_version = data_version + 1
    where goods_sale_id = #{record.goodsSaleId}
    and data_version = #{record.dataVersion}
</update>

悲观锁: 顾名思义,就是对于数据的处理持悲观态度,总认为会发生并发冲突,获取和修改数据时,别人会修改数据。所以在整个数据处理过程中,需要将数据锁定。

2. 按照数据库操作类型分类(读锁、写锁、意向锁)

读锁(共享锁 (lock in share mode),S锁

针对同一份数据,多个读操作可以同时进行而不会互相影响。但是不允许其它事务进行“写”操作。

查询也可以通过 lock in share mode 加读锁 

select * from T where id=1 lock in share mode;

写锁(排它锁(for update),X锁

当前写操作没有完成前,它会阻断其他写锁和读锁,数据修改操作都会加写锁。

查询也可以通过for update写锁 

select * from T where id=1 for update;

意向锁(Intention Lock)

又称 I锁 ,针对表锁,主要是为了提高加表锁的效率是mysql数据库自己加的用于避免为了判断表是否存在行锁而去扫描全表的系统消耗

当有事务给表的数据行加了共享锁或排他锁,同时会给表设置一个标识,代表已经有行锁了,其他事务要想对表加表锁时,就不必逐行判断有没有行锁可能跟表锁冲突了,直接读这个标识就可以确定自己该不该加表锁。特别是表中的记录很多时,逐行判断加表锁的方式效率很低。而这个标识就是意向锁。

  • 意向共享锁,IS锁,对整个表加共享锁之前,需要先获取到意向共享锁。
  • 意向排他锁,IX锁,对整个表加排他锁之前,需要先获取到意向排他锁。

3. 按照数据操作的粒度(表锁 和 行锁)

(1)表锁

每次操作锁住整张表。

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

-- 建表SQL
CREATE TABLE mylock (
	`id` INT (11) NOT NULL AUTO_INCREMENT,
	`NAME` VARCHAR (20) DEFAULT NULL,
	PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- 插入数据
INSERT INTO mylock (`id`, `NAME`) VALUES ('1', 'a');
INSERT INTO mylock (`id`, `NAME`) VALUES ('2', 'b');
INSERT INTO mylock (`id`, `NAME`) VALUES ('3', 'c');
INSERT INTO mylock (`id`, `NAME`) VALUES ('4', 'd');

手动增加表锁

lock table 表名称 read(write);

加读锁 

  • 当前session和其他session都可以读该表
  • 当前session中插入或者更新锁定的表都会报错,其他session插入或更新则会等待

加写锁

  • 当前session对该表的增删改查都没有问题,其他session对该表的所有操作被阻塞

查看表上加过的锁

show open tables;

可以看到在In_use列被标记为1 

使用场景 

表锁在实际开发中基本没有用,一般用于数据迁移。在迁移过程中为了防止一些不必要的麻烦(迁移一半数据突然被别人修改了),一般在迁移前先加上表锁!

删除表锁

unlock tables;

(2)行锁

每次操作锁住一行数据。

开销大,加锁慢(需要找到那一行);会出现死锁;锁定粒度小,发生锁冲突的概率最低,并发度最高。

InnoDB MYISAM 的最大不同有两点

  • InnoDB支持事务(TRANSACTION)
  • InnoDB支持行级锁

行锁演示

如果长时间卡在这里就会报错:ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction

总得来说

MyISAM在执行查询语句SELECT前,会自动给涉及的所有表加读锁,在执行update、insert、delete操作会自动给涉及的表加写锁。

InnoDB在执行查询语句SELECT时,因为有MVCC机制是不会加锁的。但是update、insert、delete操作会加行锁

简而言之,就是读锁会阻塞写,但是不会阻塞读。而写锁则会把读和写都阻塞

行锁与事务隔离级别案例分析

案例SQL准备

CREATE TABLE account (
	`id` INT (11) NOT NULL AUTO_INCREMENT,
	`NAME` VARCHAR (20) DEFAULT NULL,
	`balance` int(11) DEFAULT NULL,
	PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
INSERT INTO `account` (`name`, `balance`) VALUES ('lilei', '450');
INSERT INTO `account` (`name`, `balance`) VALUES ('hanmei', '16000');
INSERT INTO `account` (`name`, `balance`) VALUES ('lucy', '2400');

(1)读未提交案例

我们在开始前先设置一下MySQL事务的隔离级别

-- 如果是5.7的版本,使用的应该是 tx_isolation
-- MySQL默认的是 REPEATABLE-READ
show variables like 'transaction_isolation';

-- 设置隔离级别为:读未提交
set transaction_isolation='read-uncommitted';

在第二个窗口中,可以查看到未提交事务!一旦窗口一的事务(执行的update)回滚,那么窗口二查到的数据就是脏数据!!!也就是发生了脏读

(2)读已提交案例

先修改一下隔离级别为:read-committed

set transaction_isolation='read-committed';

显然,读已提交是可以解决“脏读”的问题,但是不能解决“可重复读”的问题!!!

(3)可重复读案例

set transaction_isolation='repeatable-read';

注:左边是事务一,右边是事务二 

可重复读这种隔离级别中,数据的一致性倒是没有被破坏!

可重复读的隔离级别下使用了MVCC(multi-version concurrency control)机制

  • select操作不会更新版本号,是快照读(历史版本)
  • insert、update和delete会更新版本号,是当前读(当前版本)

可重复读中的幻读问题!

(4)串行化 

set transaction_isolation='serializable';

如果使用了串行化,那么之前我们所提到的所有问题(脏读、不可重复读、幻读)都可以解决!

不论是还是,MySQL的存储引擎都会给它上锁!

这种隔离级别并发性极低,开发中很少会用到! 

间隙锁(Gap Lock)

临键锁(Next-key Locks) 

总结!!!

无索引行锁会升级为表锁

  • 可重复读(RR)级别会升级为表锁
  • 读取已提交(RC)级别不会升级为表锁

锁主要是加在索引上,如果对非索引字段更新,行锁可能会变表锁

InnoDB的行锁是针对索引加的锁,不是针对记录加的锁。并且该索引不能失效,否则都会从行锁升级为表锁。 

另外!锁定某一行还可以用lock in share mode(共享锁)for update(排它锁)

select * from test_innodb_lock where a = 2 for update;

这样其他session只能读这行数据,修改则会被阻塞,直到锁定行的session提交 

行锁性能分析

show status like 'innodb_row_lock%';

上述的各个状态量含义: 

Innodb_row_lock_current_waits: 当前正在等待锁定的数量

Innodb_row_lock_time: 从系统启动到现在锁定总时间长度(等待总时长)

Innodb_row_lock_time_avg: 每次等待所花平均时间(等待平均时长)

Innodb_row_lock_time_max:从系统启动到现在等待最长的一次所花时间

Innodb_row_lock_waits: 系统启动后到现在总共等待的次数(等待总次数)

尤其是当等待次数很高,而且每次等待时长也不小的时候,我们就需要分析系统中为什么会有如此多的等待?然后找到相应的代码部分,进行分析、修改!

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

MySQL5.7版本 

-- 5.6版本的,在8.0版本就不可以这么用了...
-- 查看事务
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; 

MySQL8.0版本 

SELECT 
  r.trx_id waiting_trx_id, 
  r.trx_mysql_thread_id waiting_thread, 
  r.trx_query waiting_query, 
  b.trx_id blocking_trx_id, 
  b.trx_mysql_thread_id blocking_thread, 
  b.trx_query blocking_query 
FROM 
  PERFORMANCE_SCHEMA.data_lock_waits w 
  INNER JOIN information_schema.innodb_trx b ON b.trx_id = w.blocking_engine_transaction_id 
  INNER JOIN information_schema.innodb_trx r ON r.trx_id = w.requesting_engine_transaction_id;

MySQL :: MySQL 8.0 参考手册 :: 15.15.2.1 使用 InnoDB 事务和锁定信息https://dev.mysql.com/doc/refman/8.0/en/innodb-information-schema-examples.html?spm=a2c4g.11186623.0.0.1f01177e6oFvc6

死锁

set transaction_isolation='repeatable-read';

大多数情况mysql可以自动检测死锁并回滚产生死锁的那个事务

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锁优化建议

  • 尽可能让所有数据检索都通过索引来完成,避免无索引行锁升级为表锁
  • 合理设计索引,尽量缩小锁的范围
  • 尽可能减少检索条件范围,避免间隙锁
  • 尽量控制事务大小,减少锁定资源量和时间长度,涉及事务加锁的SQL尽量放在事务最后执行
  • 尽可能低级别事务隔离

下一节——深入理解MVCC与BufferPoll缓存机制

【MySQL】深入理解MVCC与BufferPoll缓存机制_面向鸿蒙编程的博客-CSDN博客_mysql mvcc和buffericon-default.png?t=M85Bhttps://blog.csdn.net/weixin_43715214/article/details/127672669

posted @ 2022-10-31 02:13  金鳞踏雨  阅读(113)  评论(0编辑  收藏  举报  来源