MySql的事务和锁(三)

1 数据库事务

   1.1 什么情况下用事务:

      事务的提出主要是为了解决并发情况下保持数据一致性的问题(类似于多线程)事务是并发控制的基本单位。所谓的事务,这些操作要么都执行,要么都不执行,它是一个不可分割的工作单位。例如,银行转账工作:从一个账号扣款并使另一个账号增款,这两个操作要么都执行,要么都不执行,在关系数据库中,一个事务可以是一条SQL语句、一组SQL语句或整个程序。 所以,应该把它们看成一个事务。事务是数据库维护数据一致性的单位,在每个事务结束时,都能保持数据一致性。

  1.2 事务定义

   事务是应用程序中一系列严密的操作,所有操作必须成功完成,否则在每个操作中所作的所有更改都会被撤消。也就是事务具有原子性,一个事务中的一系列的操作要么全部成功,要么一个都不做。 
   事务的结束有两种,当事务中的所以步骤全部成功执行时,事务提交。如果其中一个步骤失败,将发生回滚操作,撤消撤消之前到事务开始时的所以操作。

  1.3.事务的 ACID 

     事务具有四个特征:原子性、一致性、隔离性和持续性。这四个特性简称为 ACID 特性。

      1 、原子性 

      事务是数据库的逻辑工作单位,事务中包含的各操作要么都做,要么都不做 。原子性,在 InnoDB 里面是通过 undo log 来实现的,它记录了数据修改之前的值(逻辑日志),             一旦发生异常,就可以用 undo log 来实现回滚操作。

       2 、一致性 
      事务执行的结果必须是使数据库从一个一致性状态变到另一个一致性状态。因此当数据库只包含成功事务提交的结果时,就说数据库处于一致性状态。如果数据库系统 运行中        发生故障,有些事务尚未完成就被迫中断,这些未完成事务对数据库所做的修改有一部分已写入物理数据库,这时数据库就处于一种不正确的状态,或者说是 不一致的状态。 
       3 、隔离性 
       一个事务的执行不能其它事务干扰。即一个事务内部的操作及使用的数据对其它并发事务是隔离的,并发执行的各个事务之间不能互相干扰。 
       4 、持续性 
       也称永久性,指一个事务一旦提交,它对数据库中的数据的改变就应该是永久性的。接下来的其它操作或故障不应该对其执行结果有任何影响。 

  1.4  数据库啥时候会出现事务

        无论是我们在 Navicat 的这种工具里面去操作,还是在我们的 Java 代码里面通过API 去操作,还是加上@Transactional 的注解或者 AOP 配置,其实最终都是发送一个指令到数据库去执行,Java 的 JDBC 只不过是把这些命令封装起来了。我们先来看一下我们的操作环境。版本(5.7),存储引擎(InnnoDB),事务隔离级别(RR)。

   

 

 

 

 

我们知道,在执行一条更新语句时,实际上,它自动开启了一个事务,并且提交了,所以最终写入了磁盘。这个是开启事务的第一种方式,自动开启和自动提交。InnoDB 里面有一个 autocommit 的参数(分成两个级别, session 级别和 global级别)。 

 

 

 

    它的默认值是 ON。autocommit 这个参数的意思是是否自动提交。如果它的值是 true/on 的话,我们在操作数据的时候,会自动开启一个事务,和自动提交事务。否则,如果我们把 autocommit 设置成 false/off,那么数据库的事务就需要我们手动地去开启和手动地去结束。 
   手动开启事务也有几种方式,一种是用 begin;一种是用 start transaction。结束也有两种方式,第一种就是提交一个事务,commit;还有一种就是 rollback,回滚的时候,事务也会结束。还有一种情况,客户端的连接断开的时候,事务也会结束。当我们结束一个事务的时候,事务持有的锁就会被释放,无论是提交还是回滚。

1.5 事务并发带来的问题

   当很多事务并发地去操作数据库的表或者行的时候,如果没有我们刚才讲的事务的Isolation 隔离性的时候会带来 脏读、不可重复读、幻读问题:

   脏读:所谓的脏读,其实就是读到了别的事务回滚前的脏数据。比如事务B执行过程中修改了数据X,在未提交前,事务A读取了X,而事务B却回滚了,这样事务A就形成了脏读。也就是说,当前事务读到的数据是别的事务想要修改成为的但是没有修改成功的数据。

  不可重复读:事务A首先读取了一条数据,然后执行逻辑的时候,事务B将这条数据改变了,然后事务A再次读取的时候,发现数据不匹配了,就是所谓的不可重复读了。也就是说,当前事务先进行了一次数据读取,然后再次读取到的数据是别的事务修改成功的数据,导致两次读取到的数据不匹配,也就照应了不可重复读的语义。

   幻读:事务A首先根据条件索引得到N条数据,然后事务B改变了这N条数据之外的M条或者增添了M条符合事务A搜索条件的数据,导致事务A再次搜索发现有N+M条数据了,就产生了幻读。也就是说,当前事务读第一次取到的数据比后来读取到数据条目少。

不可重复读和幻读比较:
两者有些相似,但是前者针对的是update或delete,后者针对的insert。
小结:刚才说的事务并发带来的三大问题,无论是脏读,还是不可重复读,还是幻读,它们都是数据库的读一致性的问题,都是在一个事务里面前后两次读取出现了不一致的情况。读一致性的问题,必须要由数据库提供一定的事务隔离机制来解决。

1.6 SQL92 标准 

     为解决读一致问题,很多的数据库联合制定了一个标准,提供一定的事务隔离级别,来解决事务并发的问题,这个就是 SQL92 标准

 

 

  里面定义了四个隔离级别:

    第一个隔离级别叫做:Read Uncommitted(未提交读),一个事务可以读取到其他事务未提交的数据,会出现脏读,所以叫做 RU,它没有解决任何的问题。

    第二个隔离级别叫做:Read Committed(已提交读),也就是一个事务只能读取到其他事务已提交的数据,不能读取到其他事务未提交的数据,它解决了脏读的问题,但是会出现不可重复读的问题。
   第三个隔离级别叫做:Repeatable Read (可重复读),它解决了不可重复读的问题,也就是在同一个事务里面多次读取同样的数据结果是一样的,但是在这个级别下,没有定义解决幻读的问题。
  最后一个就是:Serializable(串行化),在这个隔离级别里面,所有的事务都是串行执行的,也就是对数据的操作需要排队,已经不存在事务的并发操作了,所以它解决了所有的问题。

1.7 MySQL InnoDB 对隔离级别的支持 

   在 MySQL InnoDB 里面,不需要使用串行化的隔离级别去解决所有问题。下图是InnoDB里面对事务隔离级别的支持程度;

 

 

 InnoDB 支持的四个隔离级别和 SQL92 定义的基本一致,隔离级别越高,事务的并发度就越低。唯一的区别就在于,InnoDB 在 RR 的级别就解决了幻读的问题。这个也是InnoDB 默认使用 RR 作为事务隔离级别的原因,既保证了数据的一致性,又支持较高的并发度。至于InnoDB是怎么做到在RR级别就解决了幻读问题在后面会说到。

 1.8   基于锁的并发控制和多版本的并发控制

      如果要解决读一致性的问题,保证一个事务中前后两次读取数据结果一致,实现事务隔离,总体上来说有两大类的方案:

      第一种,既然要保证前后两次读取数据一致,那么在读取数据的时候,锁定要操作的数据,不允许其他的事务修改就行了。这种方案我们叫做基于锁的并发控制。如果仅仅是基于锁来实现事务隔离,一个事务读取的时候不允许其他时候修改,那就意味着不支持并发的读写操作,而我们的大多数应用都是读多写少的,这样会极大地影响操作数据的效率。因此引出了另一种解决方案;在修改数据的时候给它建立一个备份或者叫快照,后面再来读取这个快照就行了。这种方案我们叫做多版本的并发控制。多版本的并发控制的核心思想是: 我可以查到在我这个事务开始之前已经存在的数据,即使它在后面被修改或者删除了。在我这个事务之后新增的数据,我是查不到的。下面我们来聊下快照的原理:

  

InnoDB 为每行记录都实现了两个隐藏字段:
    DB_TRX_ID,6 字节:插入或更新行的最后一个事务的事务 ID,事务编号是自动递增的(我们把它理解为创建版本号,在数据新增或者修改为新数据的时候,记录当前事务ID)。
    DB_ROLL_PTR,7 字节:回滚指针(我们把它理解为删除版本号,数据被删除或记录为旧数据的时候,记录当前事务 ID)。我们把这两个事务 ID 理解为版本号。
   我们可以在自己的数据库建一个简单的用户表,在可视化工具中,第一步开启一个事务,并提交两个数据;此时的数据创建版本是当前事务的ID,版本为1。删除版本是空。

    

 

 

 

第二个事务,执行第 1 次查询,读取到两条原始数据,这个时候事务 ID 是 2:

 

 

 第三个事务,我们新建一个查询页面,在里面执行第三个事务,插入一条数据:

 

 

 

 此时的数据,会多一条张三的数据,它的创建事务版本号是3;接着我们再在第二个事务中执行相同的查询语句会发现我们在事务三中提交的数据没有查询出来;但是我们去系统表中去查时会发现新增的数据实实在在的存在表中,我们提交事务二,然后再查询,会发现新增的数据又可以看到了

 

 

 

 上面的演示表示多版本的并发控制的查询规则:

只能查找创建时间小于等于当前事务 ID 的数据,和删除时间大于当前事务 ID 的行(或未删除)。也就是不能查到在我的事务开始之后插入的数据,张三 的创建 ID 大于 2,所以还是只能查到两条数据;
有兴趣的同学可以再开启一个事务来验证下删除和更新数据事务的变化 ,会发现即使在事务四中你把数据删除了,但是只要你事务二没有提交,依然可以在事务二中查询到事务四中删除的数据。通过验证后你会发现通过版本号的控制,无论其他事务是插入、修改、删除,第一个事务查询到的数据都没有变化。 在 InnoDB 中,版本号的控制 是通过 Undo log 实现的。Oracle等等其他数据库都有 版本号的控制 的实现。 我们聊完了第二个解决方案,接着来说下第一个方案锁实现的读的一致性

2 MySQL InnoDB锁的基本类型

   官网网址是https://dev.mysql.com/doc/refman/5.7/en/innodb-locking.html,想要深入了解的话可以进官网查看,官网把锁分成了 8 类。把前面的两个行级别的锁,和两个表级别的锁称为锁的基本模式。后面三个 Record Locks、Gap Locks、Next-Key Locks,我们把它们叫做锁的算法,也就是分别在什么情况下锁定什么范围。

 

2.1 锁的粒度

    表锁,锁住一张表;行锁就是锁住表里面的一行数据。锁定粒度,表锁是大于行锁的;在加锁效率上,表锁只需要直接锁住这张表就行了,而行锁,还需要在表里面去检索这一行数据,所以表锁的加锁效率更高;在冲突的概率上,表锁的冲突概率比行锁大,因为当我们锁住一张表的时候,其他任何一个事务都不能操作这张表。但是我们锁住了表里面的一行数据的时候,其他的事务还可以来操作表里面的其他没有被锁定的行,所以表锁的冲突概率更大。

2.2  共享锁 

 共享锁就是允许多个线程同时获取一个锁,一个锁可以同时被多个线程拥有。 ,注意不要在加上了共享锁以后去写数据,不然的话可能会出现死锁的情况。我们在多个事务中用共享锁去查询同一条数据时,都可以查询的到,这表示了锁可以共享。释放锁有两种方式,只要事务结束,锁就会自动事务,包括提交事务和结束事务。

 

2.3 排它锁

 第二个行级别的锁叫做 Exclusive Locks(排它锁),它是用来操作数据的,所以又叫做写锁。只要一个事务获取了一行数据的排它锁,其他的事务就不能再获取这一行数据的共享锁和排它锁。排它锁的加锁方式有两种,第一种是自动加排他锁。我们在操作数据的时候,包括增删改,都会默认加上一个排它锁。还有一种是手工加锁,我们用一个 FOR UPDATE 给一行数据加上一个排它锁。

2.4 意向共享锁/ 意向排它锁

   意向锁是由数据引擎自己维护的,用户无法手动操作意向锁 。意向共享锁表示事务准备给数据行加入共享锁,也就是说一个数据行加共享锁前必须先取得该表的意向共享锁。意向排他锁表示事务准备给数据行加入排他锁,说明事务在一个数据行加排他锁前必须先取得该表的意向排他锁。如果一张表上面至少有一个意向共享锁,说明有其他的事务给其中的某些数据行加上了共享锁。如果一张表上面至少有一个意向排他锁,说明有其他的事务给其中的某些数据行加上了排他锁。 意向锁存在的意义是,如果我们在加表锁时,如果没有意向锁的话就要进行全局扫描,这样会影响效率,现在我们有了意向锁后,我们在进行表锁时只用看下表中有没有意向共享锁/ 意向排它锁就可以确定表锁能不能锁成功。有兴趣的朋友可以息验证下;

行锁:

BEGIN;
SELECT * FROM student where id=1 FOR UPDATE;

ROLLBACK;
COMMIT;

表锁:

begin;
LOCK TABLES student WRITE;
UNLOCK TABLES;

 3 行锁的原理

  前面我们讲了锁的实现,接下来我们说下,锁的实现原理,这里面我们先建三张表ABC,一张没有索引,一张有主键索引,一张有唯一索引,

  

 我们先假设InnoDB的锁锁住的是一行数据或记录,接着我们手工开启两个事务,在第一个事中

 

在第二个事务中我们给别一行记录加锁或者是添加新的数据都会被阻塞,这个测试验证了InnoDB中的锁锁的不是一行数据或者一条记录。

 

 上一个测试是在没有索引的表中测试的,接下来我们在有主键的表中执行相同的语句

 

然后另开一个事务执行下面两个语句

 

 

 

 

 

测试发现,使用相同的 id 值去加锁,冲突;使用不同的 id 加锁,可以加锁成功。那么,既然不是锁定一行数据,有没有可能是锁住了 id 的这个字段呢?为验证猜想我们接下来在有唯一索引的表中进行第三种情况的测试

 

然后我们另启一个事务执行下面语句

 

 

 

 

 

 

 比较上面的测试发现只有第三个查询成功了,其它两个全部被阻塞了,第一个是尝试获取一样的排他锁被阻塞正常,但是第二个,我们换了字段还是阻塞了,但第三个换了行却成功了,这说明锁住的既不是行也不是列,但InnoDB在上锁时却实实在在的会锁住某些东西,至于他锁的到底是什么我们来分析下,我们分析三个表的差异会发现导致三个表的查询结果差异的原因就是索引,InnoDB 的行锁,就是通过锁住索引来实现的。既然说InnoD是通过锁住索引来实现的,那么问题又出现了,我们第一张表中没有创建索引,我们锁住一行数据为什么导致了锁表;如果看过官网的人会发现,在InnoDB中如果我们定义了主键,那么 InnoDB 会选择主键作为聚集索引。如果没有显式定义主键,则 InnoDB 会选择第一个不包含有 NULL 值的唯一索引作为主键索引。如果也没有这样的唯一索引,则 InnoDB 会选择内置 6 字节长的 ROWID 作为隐藏的聚集索引,它会随着行记录的写入而主键递增。所以锁表的原因,是因为查询没有使用索引,会进行全表扫描,然后把每一个隐藏的聚集索引都锁住了。第一个表的总理解决了那么第二张表就很好理解了,但是在第三张表中我们又有问题了,那就是我们在给唯一索引的数据加锁时主键索引也会被锁,想要了解这是为什么的话就要去看下我上节写的辅助索引,在InnoDB中辅助索引检索还是会经过主键索引进行查找,这就是为什么表三中唯一索引的数据加锁时主键索引也会被锁

 

4 锁的算法

 为了解锁的算法,我们新建一张表,插入如下数据

   

      
     4.1 记录锁

         当我们对于唯一性的索引(包括唯一索引和主键索引)使用等值查询,精准匹配到一条记录的时候,这个时候使用的就是记录锁。比如查询上表中的4个值,这个时候就会用到记录锁的算法,记录锁锁住的仅仅是我们命中的那一行数据

 

  4.2 间隙锁     
当我们查询的记录不存在,没有命中任何一个记录,无论是用等值查询还是范围查询的时候,它使用的都是间隙锁 ,他锁住的是我们数据不存在的区间,比喻我们上表为例查询where id >7 and id <10 for update;他锁的区间是(7,10);间隙锁主要是阻塞插入 insert。相同的间隙锁之间不冲突。
我们在事务1中查询id=6,此时锁住了区间(4,7)

我们在事务2中执行查询和插入语句,会发现区间插入会发生阻塞,但查询是正常的

 

 

   

4.3 临键锁

   当我们使用了范围查询,不仅仅命中了 行记录,还包含了间隙,在这种情况下我们使用的就是临键锁,它是 MySQL 里面默认的行锁算法,相当于记录锁加上间隙锁。比喻我们上表查询条件是where id>2 and id<7 for update;此时锁住的区间是(,4],(4,7];

在事务1中执行下面语句

 

 另启一个事务,在里面执行以下语句会发现执行锁住区间的操作会失败,区间外的可以成功

 

 我们在前面说过InnoDB 的 RR 级别能够解决幻读的问题,就是用临键锁实现的

 

 既然在这里重新聊到了事务隔离级别,那么就再补充点东西,RU隔离级别就不用说了,他是不加锁的,串行化所有的 select 语句都会被隐式的转化为 select ... in share mode,会和 update、delete 互斥。至于RR隔离级别,在普通的查询使用快照读,底层实现使用前面所说的多版本的并发控制来实现;加锁的查询更新删除等语句使用当前读,底层用我们上面讲的什么记录锁间隙锁、临键锁;RC隔离级别普通的 select 都是快照读。加锁的 select 都使用记录锁,

 

5 如何选择事务隔离级别 

   官网上描述的隔离级别选取很详细https://dev.mysql.com/doc/refman/5.7/en/innodb-transaction-isolation-levels.html经过上面讲解和一样原理的解释我们应该明白RU和串行化是不可采取的,下面我们就RC和RR的区别进行比较下:

       1、 RR 的间隙锁会导致锁定范围的扩大。

       2、 条件列未使用到索引,RR 锁表,RC 锁行。
       3、 RC 的“半一致性”读可以增加 update 操作的并发性。
 
  

 

 上图是从官网截取的一段话,上图讲解了用RC的好处,这也是为什么有的公司鼓励用RC的原因,我个人其实比较喜欢使用InnoDB默认的RR,我感觉只要在遵守InnoDB的使用条例,采用默认引擎也是挺好的;至于个人是选取RR还是RC那看每个人实际开发情况喽

posted @ 2020-02-05 20:45  童话述说我的结局  阅读(135)  评论(0)    收藏  举报