MySQL事务与锁学习笔记

一,什么是数据库的事务?

版本(5.7),存储引擎(InnoDB),事务隔离级别(RR)

select version();
show variables like '%engine%';
show global variables like 'tx_isolation';
1,事务的使用场景

在方法上添加@Transactional注解,或者在xml文件里配置切面。


    
      
      
      
      
      
      
    

2,事务的定义

事务是数据库管理系统(DBMS)执行过程中的一个逻辑单位,由一直有限的数据库操作序列构成。----维基百科
所谓的逻辑单位,是数据库最小的工作单元,是不可再分的。包含了一个或者一系列的DML语句,包括insert,delete,update。
单条DDL(create drop) 和DCL(grant revoke)也会有事务

3,哪些存储引擎支持事务

除了做集群的NDB之外,只有InnoDB支持事务。

4,事务的四大特性

1,原子性,Atomicity,不可再分,意味着对数据库的一系列操作,要么都成功,要么都失败,不可能出现部分成功或部分失败的情况。如果部分成功了,如果要让他全部失败,就要回滚。原子性,是通过undo log来实现的,它记录了数据修改之前的值,一旦发生异常,就可以用undo log来实现回滚操作。
2,隔离性,Isolation,操作同一张表或者同一行数据时,对表或者行的操作,应该是透明的,互相不干扰的。
3,持久性,Durability,对数据库的操作,增删改,只要事务提交成功。那结果就是永久性的,不可能因为数据库掉电、宕机、意外重启,又变成原来的状态。这个就是事务的持久性。
持久性是通过redo log和double write buffer(双写缓冲)来实现,操作数据的时候,会先写到内存的buffer pool里面,同时记录redo log,如果在刷盘之前出现异常,在重启后可以读取redo log的内容,写到磁盘,保证数据的持久性。
4,一致性,consistent,指的是数据库的完整性约束没有被破坏,事务执行的前后都是合法的数据状态。数据库自身提供了一些约束:比如主键必须是唯一的,字段长度符合要求,另外还有用户自定义的完整性(如余额不能小于0等)。

5,数据库什么时候出现事务

增删改的语句会自动开启事务,当然是一条SQL一个事务。每个事务都是有编号的,这个编号是一个整数,有递增的特性。
如果把多条SQL放在一个事务里面,就要手动开启事务。手动开启事务有两种方式:一种是begin,一种是start transaction。
结束事务的方式有两种,一种是回滚事务rollback,另一种是提交事务commit。
InnoDB里有一个autocommit参数(分为两个级别,session级别和global级别)。

show variables like 'autocommit';

默认值是ON。autocommit即自动提交,值为true/on的话,在操作数据的时候,会自动提交事务。如果把autocommit设置为false/off,数据库事务就需要手动的结束,用rollback或者commit。

6,事务并发带来的问题

1,脏读:在一个事务里,由于其它事务修改的数据没有提交,导致前后两次读取数据不一致的情况,这种事务并发的问题叫脏读。


脏读

2,不可重复读:一个事务读取到了其它事务已提交的数据导致前后两次读取数据不一致的情况,叫不可重复读。


不可重复读

3,幻读:一个事务前后两次读取的数据不一致,是由于其它事务插入数据造成的,这种情况叫做幻读。
幻读

不可重复读和幻读的区别? 修改或者删除造成的读不一致叫做不可重复读,插入造成的读不一致叫做幻读。

7,SQL92标准

美国国家标准协会(ANSI)制定了一个SQL标准,建议数据库厂商都按照这个标准,提供一定的事务隔离级别,来解决事务并发的问题。最常见的就是SQL92标准。
http://www.contrib.andrew.cmu.edu/~shadow/sql/sql1992.txt (搜索_iso)

SQL92标准

定义了四种隔离级别,P1 P2 P3代表事务并发的3个问题,脏读,不可重复读,幻读。Possible代表在这个隔离级别下,这个问题有可能发生,Not Possible就是解决了这个问题。
1,Read Uncommitted(未提交读),一个事务可以读取到其它事务未提交的数据,会出现脏读,叫做RU,它没有解决任何问题。
2,Read Committed(已提交读),一个事务只能读取到其它事务已提交的数据,不能读取到其它事务未提交的数据,解决了脏读的问题,但是会出现不可重复读的问题。
3,Repeatable Read(可重复读),它解决了不可重复读的问题,就是在同一个事务里面多次读取同样的数据结果是一样的,这个级别下,没有定义幻读的问题。
4,Serializable(串行化),在这个隔离级别中,所有的事务都是串行执行的,也就是对数据的操作需要排队,就不存在事务并发的问题了,它解决了所有的问题。
事务的隔离级别是可以修改的:

set global transaction isolation level read uncommitted;
set global transaction isolation level read committed;
set global transaction isolation level read repeatable read;
set global transaction isolation level serializable;
8,MySQL InnoDB对隔离级别的支持
事务隔离级别 脏读 不可重复读 幻读
未提交读(Read Uncommitted) 可能 可能 可能
已提交读(Read Committed) 不可能 可能 可能
可重复读(Repeatable Read) 不可能 不可能 对InnoDB不可能
串行化(Serializable) 不可能 不可能 不可能

InnoDB支持和隔离级别和SQL92的标准一致,隔离级别越高,事务的并发度就越低。唯一的区别是,InnoDB在RR的级别就解决了幻读的问题。也就是不需要使用串行化的隔离级别去解决所有问题,既保证了数据的一致性,又支持了较高的并发度。这就是InnoDB默认使用RR作为事务隔离级别的原因。

9,两大实现方案

1,基于锁的并发控制 Lock Based Concurrency Control(LBCC)。
2,多版本的并发控制 Multi Version Concurrency Control(MVCC)。

二,InnoDB锁的基本类型

1,锁的粒度

MyISAM只支持表锁,用lock table语法加锁。

lock tables xxx read;
lock tables xxx write;
unlock tables;

InnoDB支持表锁和行锁。

  • 锁定粒度:表锁 > 行锁
  • 加锁效率:表锁 > 行锁
  • 冲突概率:表锁 > 行锁
  • 并发性能: 表锁 < 行锁
2,锁的类型
锁的类型

我们把两个行级别的锁(Shared and Exclusive Locks),和两个表级别的锁(Intention Locks)称为锁的基本形式。
后面三个Record Locks、Gap Locks、Next-Key Locks叫做锁的算法。
插入意向锁:是一个特殊的间隙锁。间隙锁不允许插入数据,但是插入意向锁允许多个事务同时插入数据到同一个范围。不会发生锁等待。
自增锁:是一种特殊的表锁,用来防止自增字段重复,数据插入以后会释放,不需要等待事务提交才释放。如果需要选择更快的自增值生成速度或者更加连续的自增值,要修改自增锁的模式改变。

show variables like 'innodb_autoinc_lock_mode';

0:traditional(每次都会产生表锁)
1:consecutive(会产生一个轻量锁,simple insert会获得批量的锁,保证连续插入,默认值)
2:interleaved(不会锁表,来一个处理一个,并发最高)

3,共享锁

共享锁(Shared Locks):获取一行数据的读锁之后,可以用来读取数据,所以又叫做读锁。不要在加上读锁之后去写数据,不然可能会造成死锁的情况。多个事务可以共享一把读锁。

共享锁的作业:因为共享锁会阻塞其它事务的修改,所以可以用在不允许其它事务修改数据的情况。
加锁: select ...... lock in share mode; 加一把读锁。
释放的方式:提交事务和结束事务。

4,排它锁

排它锁(Exclusive Locks):是用来操作数据的,所以又叫写锁。只要一个事务获取了一行数据的排它锁,其它的事务就不能再获取这一行数据的共享锁和排它锁。
排它锁的加锁方式有两种,1,自动加排它锁,在操作数据的时候,包括增删改,都会默认加上一把排它锁。2,手工加锁,用for update给一行数据加一个排它锁。
验证:


排它锁验证
5,意向锁

意向锁是由数据库自己维护的。当我们给一行数据加上共享锁之前,数据库会自动在这张表上面加一个意向共享锁。
当我们给一行数据加上排它锁之前,数据库会自动在这张表上面加一个意向排它锁。
意向锁跟意向锁不冲突,意向锁跟行锁也不冲突。
如果没有意向锁的话,当准备给一张表加上表锁的时候,需要去判断有没有其它的事务锁定了其中的某些行,这时要扫描整个表才能确定能不能加表锁,效率很低。有了意向锁之后,只需要判断这张表上有没有意向锁,如果有,就直接返回失败。如果没有,就可以加锁成功。


意向锁

三,行锁的原理

1,没有索引的表


行锁的原理探索

如上图可知,当锁定一行记录时,整个表都被锁住了,InnoDB的行锁锁住的应当不是Record。
2,有主键索引的表


行锁的原理探索

使用相同的id,加锁会冲突,使用不同的id加锁,可以加锁成功。
3,唯一索引(假设锁住字段)
行锁的原理探索

如上图,说明行锁锁住的是字段这个推测也是错的。
那么InnoDB锁锁住的到底是什么?InnoDB的行锁,是通过锁住索引来实现的。
问题一:为什么表里没有索引的时候,锁住一行数据会导致锁表?如果锁住的是索引,一张表没有索引怎么办?
如果定义了主键(Primary key),那么InnoDB会选择主键作为聚集索引。如果没有显示定义主健,则InnoDB会选择第一个不包含NULL值的唯一索引作为主健索引。如果也没有这样的唯一索引,则InnoDB会选择内容6字节长的ROWID作为隐藏的聚集索引,会随着记录的写入而主健递增。所以,查询的时候没有使用索引,会进行全表扫描,然后把每一个隐藏的聚集索引都锁住了。
问题二:为什么通过唯一索引给数据行加锁,主健索引也会被锁住?
在辅助索引中,索引存储的是二级索引和主健的值。而主健索引除了索引之外,还存储了完整的数据。所以通过辅助索引锁定一行数据的时候,它跟我们检索数据的步骤是一样的,会通过主健值找到主健索引,然后也锁定。


辅助索引锁定

四,锁的算法

锁的算法

数据库里面存在的主健值,叫做Record,记录。根据主健,这些存在的Record隔开的数据不存在的区间,叫做Gap,间隙,它是一个左开右开的区间。间隙(Gap)连同它左边的记录(Record),叫做临键区间,是一个左开右闭的区间。
整型的主键索引,可以排序,才有区间。如果主键不是整形,是字符怎么办?任何一个字符集,都有对应的排序规则。


字符集排序规则
1,记录锁

对于唯一索引(包括唯一索引和主键索引)使用等值查询,精确匹配到一条记录的时候,这个时候使用的就是记录锁。


记录锁
2,间隙锁

当查询的记录不存在,没有命中任何一个record,无论是等值查询,还是范围查询,使用的都是间隙锁。


间隙锁

间隙锁举例

间隙锁主要是阻塞插入insert。相同的间隙锁之间不冲突。

3,临键锁

当使用范围查询,不仅命中了Record记录,还包含了Gap间隙,这种情况下使用的就是临键锁,是MySQL中默认的行锁算法,相当于记录锁加上间隙锁。
唯一性索引,待会查询匹配到一条记录的时候,退化成记录锁。没有匹配到任何记录的时候,退化成间隙锁。


临键锁

临键锁测试

临键锁,锁住最后一个key的下一个左开右闭区间。--为了解决幻读的问题。

4,隔离级别的实现

InnoDB的RR级别能够解决幻读的问题,就是用临键锁实现的。
1,Read Uncommited:RU隔离级别,不加锁。
2,Serializable:所有的select语句,都会被隐式的转化为select .... in share mode,会和update,delete互斥。
3,Repeatable Read:RR级别下,普通的select使用快照读(snapshot read),底层使用MVCC来实现。加锁的select(select .... in share mode/ select ... for update)以及更新操作update,delete等使用当前读(current read),底层使用记录锁、或间隙锁,或临键锁。
4,Read Commited:RC隔离级别下,普通的select都是快照读,使用MVCC实现。加锁的select 使用记录锁,因为没有Gap Lock。RC会出现幻读的问题。

5,事务的隔离级别怎么选

RC和RR的主要区别:
1,RR的间隙锁会导致锁定范围扩大
2,条件列未使用索引,RR锁表,RC锁行
3,RC的“半一致性”(semi-consistent),读可以增加update操作的并发性。在RC中,一个update语句,如果读到一行已经加锁的记录,InnoDB返回记录最近提交的版本。
如果能够正确的使用锁(避免不使用索引去加锁),只锁定需要的数据,用默认的RR级别就行了。

五,死锁

1,锁的释放与阻塞
锁的释放:当事务结束(commit,rollback),或者客户端连接断开时。
如果一个事务一直未释放锁,其它事务会阻塞多久呢,MySQL有一个参数来控制获取锁的等待时间,默认是50秒。

show variables like 'innodb_lock_wait_timeout';
锁的默认等待时间

2,死锁的发生和检测
在发生死锁时,InnoDB一般都能通过算法(wait-for graph)自动检测到。
死锁产生的条件:
(1) 同一时刻只能有一个事务持有这把锁。
(2) 其它的事务需要在这个事务释放锁之后才能获取锁,而不可以强行剥夺。
(3) 当多个事务形成等待环路的时候,即发生死锁。
3,查看锁信息(日志)
show status 命令中包括了一些行锁的信息:

show status like 'innodb_row_lock_%';
锁信息

Innodb_row_lock_current_waits:当前正在等待锁定的数据;
Innodb_row_lock_time:从系统启动到现在锁定的总时间长度,单位ms;
Innodb_row_lock_time_avg:每次等待所花平均时间;
Innodb_row_lock_time_max:从系统启动到现在等待最长一次所花的时间;
Innodb_row_lock_waits:从系统启动到现在总共等待的次数;

show 命令是一个概要信息。InnoDB提供了3张表来分析事务与锁的情况:

select * from information_schema.INNODB_TRX; -- 当前运行的所有事务,还有具体的语句
查看运行中的事务
select * from information_schema.INNODB_LOCKD; -- 当前出现的锁
锁的状态
select * from information_schema.INNODB_LOCK_WAITS; -- 锁等待的对应关系
锁等待关系

更加详细的锁信息,开启标准监控和锁监控:

set global innodb_status_output=ON;
set global innodb_status_output_locks=ON;

如果一个事务长时间持有锁不释放,可以通过kill事务对应的线程ID,也就是INNODB_TRX表中的trx_mysql_thread_id。
4,死锁的避免

  • 在程序中,操作多张表时,尽量以相同的顺序来访问(避免形成等待环路)
  • 批量操作单张表数据的时候,先对数据进行排序(避免形成等待环路)
  • 申请足够级的锁,如果要操作数据,就申请排它锁
  • 尽量使用索引访问数据,避免没有where条件的操作,避免锁表
  • 如果可以,大事务化为小事务
  • 使用等值查询而不是范围查询数据,命中记录,避免间隙锁对并发的影响

你可能感兴趣的:(MySQL事务与锁学习笔记)