深入理解Mysql事务隔离级别与锁机制

  作者:图灵javaer


在线预览


概述


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


这些问题的本质都是数据库的多事务并发问题,为了解决多事务并发问题,数据库设计了事务隔离机制锁机制MVCC多版本并发控制隔离机制用一整套机制来解决多事务并发问题。接下来,我们会深入讲解这些机制,让大家彻底理解数据库内部的执行原理。


事务及其ACID属性


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


  • 原子性(Atomicity) :事务是一个原子操作单元,其对数据的修改,要么全都执行,要么全都不执行
  • 一致性(Consistent) :在事务开始和完成时,数据都必须保持一致状态。这意味着所有相关的数据规则都必须应用于事务的修改,以保持数据的完整性。
  • 隔离性(Isolation) :数据库系统提供一定的隔离机制,保证事务在不受外部并发操作影响的“独立”环境执行。这意味着事务处理过程中的中间状态对外部是不可见的,反之亦然。
  • 持久性(Durable) :事务完成之后,它对于数据的修改是永久性的,即使出现系统故障也能够保持。不能说宕机了之后数据就消失了,再次启动后数据还是存在的


并发事务处理带来的问题


更新丢失(Lost Update)或脏写


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



脏读(Dirty Reads)


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



不可重读(Non-Repeatable Reads)


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


幻读(Phantom Reads)


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



幻读与不可重复读区别:

  • 幻读读取到了新增的数据,而不可重复读读取到了更新后未提交的数据


锁详解


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


表锁


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


实践用建表(MyISAM)语句


--  表 test1.mylock 结构
CREATE TABLE IF NOT EXISTS `mylock` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `NAME` varchar(20) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=MyISAM AUTO_INCREMENT=5 DEFAULT CHARSET=utf8;
show open TABLES LIKE "mylock" # 查看表mylock锁状态



手动增加表读锁

LOCK TABLE mylock READ;

再次查看表mylock锁状态

in_use字段变成1说明该表已被加锁


删除表锁

unlock tables;

再次查看表mylock锁状态

in_use字段变成0说明该表锁已被释放


当前session其他session都可以读该表

当前session中插入或者更新锁定的表都会报错其他session插入或更新则会等待


手动增加表写锁


LOCK TABLE mylock write;

再次查看表mylock锁状态

in_use字段变成1说明该表已被加锁


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


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

2、对MylSAM表的写操作(加锁) ,会阻塞其他进程对同一表的读和写操作,只有当写锁释放后,才会执行其它进程的读写操作


行锁InnoDB


每次操作锁住一行数据。开销大,加锁慢会出现死锁锁定粒度最小,发生锁冲突的概率最低,并发度最高


InnoDB与MYISAM的最大不同有两点:

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


# 会话1 开启事务未提交
mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> update test_row_lock set name = "aaa" where id = 1;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0
# 会话2 直接更新该条记录
mysql> update test_row_lock set NAME = "b" where id = 1;
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction

可以发现,会话2直接被行锁锁住,无法进行操作,等待超时后抛出异常


总结:

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

InnoDB在执行查询语句SELECT时(非串行隔离级别),不会加锁。但是update、insert、delete操作会加行锁


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


行锁与事务隔离级别案例分析(示例详见文档


案例建表语句:

--  表 test1.account 结构
CREATE TABLE IF NOT EXISTS `account` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(255) DEFAULT NULL,
  `balance` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;

INSERT INTO `account` (`id`, `name`, `balance`) VALUES
	(1, 'lilei', 450),
	(2, 'hanmei', 16000),
	(3, 'lucy', 2400);


隔离级别
脏读(Dirty Read)
不可重复读(NonRepeatable Read)
幻读(Phantom Read)

读未提交

(Read uncommitted)

可能
可能
可能

读已提交

(Read committed)

不可能
可能
可能

可重复读

(Repeatable read)

不可能
不可能
可能

可串行化

(Serializable)

不可能
不可能
不可能
读未提交:

set tx_isolation='read-uncommitted';


读已提交:


set tx_isolation='read-committed';


可重复读:


set tx_isolation='repeatable-read';


串行化


set tx_isolation='serializable';


间隙锁(Gap Lock)


间隙锁,锁的就是两个值之间的空隙。Mysql默认级别是repeatable-read




那么间隙就有 id 为 (3,10),(10,20),(20,正无穷) 这三个区间,


# 会话1 更新区间(8, 18)的数据 
+----+---------+---------+
|  1 | lilei   |     450 |
|  2 | hanmei  |   16000 |
|  3 | lucy    |    2400 |
| 10 | zhuge   |    1000 |
| 20 | yangguo |    2000 |
+----+---------+---------+
5 rows in set (0.00 sec)

mysql> begin;
Query OK, 0 rows affected (0.00 sec)

mysql> update account set name='zhuge' where id > 8 and id < 18;
Query OK, 0 rows affected (0.01 sec)
Rows matched: 1  Changed: 0  Warnings: 0
# 会话2 向区间(8, 18)内插入数据 
mysql> insert into account values (12, 'zhuge', 999);
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction

临键锁(Next-key Locks)


并且除了(8,18)区间有间隙锁,由于8在区间 (3,10)内,那么在这区间内的所有记录也都会被加锁,由于18在区间(10,20)内。那么在这区间的所有记录也都会被加锁。像上面那个例子里的这个(3,20]的整个区间可以叫做临键锁。


无索引行锁会升级为表锁


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


  • session1 执行:update account set balance = 800 where name = 'lilei';

  • session2 对该表任一行操作都会阻塞住


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


锁优化建议


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

  • 尽可能低级别事务隔离(避免使用串行化,推荐使用可重复读)


相关推荐

评论 抢沙发

表情

分类选择