欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

MySQL 四种事务隔离级别 + 锁

程序员文章站 2024-03-13 20:41:48
...

一、事务的基本要素(ACID)

  1、原子性(Atomicity):事务开始后所有操作,要么全部做完,要么全部不做,不可能停滞在中间环节。事务执行过程中出错,会回滚到事务开始前的状态,所有的操作就像没有发生一样。也就是说事务是一个不可分割的整体,就像化学中学过的原子,是物质构成的基本单位。

   2、一致性(Consistency):事务开始前和结束后,数据库的完整性约束没有被破坏 。比如A向B转账,不可能A扣了钱,B却没收到。

   3、隔离性(Isolation):同一时间,只允许一个事务请求同一数据,不同的事务之间彼此没有任何干扰。比如A正在从一张银行卡中取钱,在A取钱的过程结束前,B不能向这张卡转账。

   4、持久性(Durability):事务完成后,事务对数据库的所有更新将被保存到数据库,不能回滚。

二、事务的并发问题

  1、脏读:事务A读取了事务B更新的数据,然后B回滚操作,那么A读取到的数据是脏数据

  2、不可重复读(修改):事务 A 多次读取同一数据,事务 B 在事务A多次读取的过程中,对数据作了更新并提交,导致事务A多次读取同一数据时,结果 不一致。【需要行锁就可以解决】

  3、幻读(新增/删除):系统管理员A将数据库中所有学生的成绩从具体分数改为ABCDE等级,但是系统管理员B就在这个时候插入了一条具体分数的记录,当系统管理员A改结束后发现还有一条记录没有改过来,就好像发生了幻觉一样,这就叫幻读。【需要表锁才能解决】

  小结:不可重复读的和幻读很容易混淆,不可重复读侧重于修改,幻读侧重于新增或删除。解决不可重复读的问题只需锁住满足条件的行,解决幻读需要锁表

 

三、MySQL事务隔离级别,其实很好记,其实就是读的范围不同而已,看后面的标注就很好记了

1.可以读到未提交的数据

2.可以读到已提交的数据

3.可以重复的读同一条数据没问题

4.串行化

事务隔离级别 脏读 不可重复读 幻读
读未提交(read-uncommitted)可以读到未提交的数据
读已提交(read-committed)可以读到已提交的数据
可重复读(repeatable-read)
串行化(serializable)

并发性能也是从上到下,越来越差,因为越来越严格
 

mysql默认的事务隔离级别为repeatable-read

show VARIABLES like '%ISOLATION%'
transaction_isolation	REPEATABLE-READ

四、用例子说明各个隔离级别的情况

 1、读未提交:

    (1)打开一个客户端A,并设置当前事务模式为read uncommitted(未提交读),查询表account的初始值:

 MySQL 四种事务隔离级别 + 锁

    (2)在客户端A的事务提交之前,打开另一个客户端B,更新表account:

 MySQL 四种事务隔离级别 + 锁

 

    (3)这时,虽然客户端B的事务还没提交,但是客户端A就可以查询到B已经更新的数据:

 MySQL 四种事务隔离级别 + 锁

    (4)一旦客户端B的事务因为某种原因回滚,所有的操作都将会被撤销,那客户端A查询到的数据其实就是脏数据:

 MySQL 四种事务隔离级别 + 锁

     (5)在客户端A执行更新语句update account set balance = balance - 50 where id =1,lilei的balance没有变成350,居然是400,是不是很奇怪,数据不一致啊,如果你这么想就太天真 了,在应用程序中,我们会用400-50=350,并不知道其他会话回滚了,要想解决这个问题可以采用读已提交的隔离级别

MySQL 四种事务隔离级别 + 锁

 

  2、读已提交

    (1)打开一个客户端A,并设置当前事务模式为read committed(未提交读),查询表account的所有记录:

 MySQL 四种事务隔离级别 + 锁

    (2)在客户端A的事务提交之前,打开另一个客户端B,更新表account:

 MySQL 四种事务隔离级别 + 锁

    (3)这时,客户端B的事务还没提交,客户端A不能查询到B已经更新的数据,解决了脏读问题:

 MySQL 四种事务隔离级别 + 锁

    (4)客户端B的事务提交

MySQL 四种事务隔离级别 + 锁

    (5)客户端A执行与上一步相同的查询,结果 与上一步不一致,即产生了不可重复读的问题

MySQL 四种事务隔离级别 + 锁

 

   3、可重复读

     (1)打开一个客户端A,并设置当前事务模式为repeatable read,查询表account的所有记录

MySQL 四种事务隔离级别 + 锁

    (2)在客户端A的事务提交之前,打开另一个客户端B,更新表account并提交

MySQL 四种事务隔离级别 + 锁

    (3)在客户端A查询表account的所有记录,与步骤(1)查询结果一致,没有出现不可重复读的问题

MySQL 四种事务隔离级别 + 锁

    (4)在客户端A,接着执行update balance = balance - 50 where id = 1,balance没有变成400-50=350,lilei的balance值用的是步骤(2)中的350来算的,所以是300,数据的一致性倒是没有被破坏。可重复读的隔离级别下使用了MVCC机制,select操作不会更新版本号,是快照读(历史版本);insert、update和delete会更新版本号,是当前读(当前版本)。

MySQL 四种事务隔离级别 + 锁

(5)重新打开客户端B,插入一条新数据后提交

MySQL 四种事务隔离级别 + 锁

(6)在客户端A查询表account的所有记录,没有 查出 新增数据,所以没有出现幻读

MySQL 四种事务隔离级别 + 锁

下面来一个更直观的例子:

  1. 准备两个终端,在此命名为 mysql 终端 1 和 mysql 终端 2,准备一张测试表 test 并调整隔离级别为 REPEATABLE READ,任意一个终端执行即可。
    SET @@session.transaction_isolation = 'REPEATABLE-READ';
    
    create database test;
    
    use test;
    
    create table test(id int primary key,name varchar(20));

     

  2. 登录 mysql 终端 1,开启一个事务。
    begin;
    
    select * from test; -- 无记录

     

  3. 登录 mysql 终端 2,开启一个事务。
    begin;
    
    select * from test; -- 无记录

     

  4. 切换到 mysql 终端 1,增加一条记录并提交。
    insert into test(id,name) values(1,'a');
    
    commit;

     

  5. 切换到 msyql 终端 2。
    	
    select * from test; --此时查询还是无记录

    通过这一步可以证明,在该隔离级别下已经读取不到别的已提交的事务,如果想看到 mysql 终端 1 提交的事务,在 mysql 终端 2 将当前事务提交后再次查询就可以读取到 mysql 终端 1 提交的事务。我们接着实验,看看在该隔离级别下是否会存在别的问题。

  6. 此时接着在 mysql 终端 2 插入一条数据。
    insert into test(id,name) values(1,'b'); 
    -- 此时报主键冲突的错误

     

也许到这里您心里可能会有疑问,

明明在第 5 步没有数据,为什么在这里会报错呢?其实这就是该隔离级别下可能产生的问题,MySQL 称之为幻读。

注意我在这里强调的是 MySQL 数据库,Oracle 数据库对于幻读的定义可能有所不同

补充:

  1、事务隔离级别为读提交时,写数据只会锁住相应的行

  2、事务隔离级别为可重复读时,如果检索条件有索引(包括主键索引)的时候,默认加锁方式是next-key 锁;如果检索条件没有索引,更新数据时会锁住整张表。一个间隙被事务加了锁,其他事务是不能在这个间隙插入记录的,这样可以防止幻读。

  3、事务隔离级别为串行化时,读写数据都会锁住整张表

   4、隔离级别越高,越能保证数据的完整性和一致性,但是对并发性能的影响也越大。


锁:[二者关系:锁是实现事务隔离级别的手段]

锁也是数据库管理系统区别文件系统的重要特征之一。

锁机制使得在对数据库进行并发访问时,可以保障数据的完整性和一致性。

对于锁的实现,各个数据库厂商的实现方法都会有所不同。

本文讨论 MySQL 中的 InnoDB 引擎的锁

锁的类型

InnoDB 实现了两种类型的行级锁:

  • 共享锁(也称为 S 锁)【读锁】:允许事务读取一行数据。

    可以使用 SQL 语句 select * from tableName where … lock in share mode; 手动加 S 锁。

  • 独占锁(也称为 X 锁)【写锁】:允许事务删除或更新一行数据。

    可以使用 SQL 语句 select * from tableName where … for update; 手动加 X 锁。

行锁的算法

InnoDB 存储引擎使用三种行锁的算法用来满足相关事务隔离级别的要求。

  • Record Locks

    该锁为索引记录上的锁,如果表中没有定义索引,InnoDB 会默认为该表创建一个隐藏的聚簇索引,并使用该索引锁定记录。

  • Gap Locks

    该锁会锁定一个范围,但是不括记录本身。可以通过修改隔离级别为 READ COMMITTED 或者配置 innodb_locks_unsafe_for_binlog 参数为 ON

  • Next-key Locks

    该锁就是 Record Locks 和 Gap Locks 的组合,即锁定一个范围并且锁定该记录本身。InnoDB 使用 Next-key Locks 解决幻读问题。需要注意的是,如果索引有唯一属性,则 InnnoDB 会自动将 Next-key Locks 降级为 Record Locks。举个例子,如果一个索引有 1, 3, 5 三个值,则该索引锁定的区间为 (-∞,1], (1,3], (3,5], (5,+ ∞)

死锁

死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程

InnoDB 引擎采取的是 wait-for graph 等待图的方法来自动检测死锁,如果发现死锁会自动回滚一个事务。

  1. 准备两个终端,在此命名为 mysql 终端 1 和 mysql 终端 2,分别登入 mysql,再准备一张测试表 test 写入两条测试数据,并调整隔离级别为 SERIALIZABLE,任意一个终端执行即可。
    SET @@session.transaction_isolation = 'REPEATABLE-READ';
    
    create database test;
    
    use test;
    
    create table test(id int primary key);
    
    insert into test(id) values(1),(2);

     

  2. 登录 mysql 终端 1,开启一个事务,手动给 ID 为 1 的记录加 X 锁。
    begin;
    
    select * from test where id = 1 for update;

     

  3. 登录 mysql 终端 2,开启一个事务,手动给 ID 为 2 的记录加 X 锁。
    begin;
    
    select * from test where id = 2 for update;

     

  4. 切换到 mysql 终端 1,手动给 ID 为 2 的记录加 X 锁,此时会一直卡住,因为此时在等待第 3 步中 X 锁的释放,直到超时,超时时间由 innodb_lock_wait_timeout 控制。
    select * from test where id = 2 for update;
     

此时,通过 show engine innodb status\G 命令可以看到 LATEST DETECTED DEADLOCK 相关信息,即表明有死锁发生;或者通过配置 innodb_print_all_deadlocks(MySQL 5.6.2 版本开始提供)参数为 ON 将死锁相关信息打印到 MySQL 的错误日志。

 

锁的优化建议

锁如果利用不好,会给业务造成大量的卡顿现象,在了解了锁相关的一些知识点后,我们可以有意识的去避免锁带来的一些问题。

  1. 合理设计索引,让 InnoDB 在索引键上面加锁的时候尽可能准确,尽可能的缩小锁定范围,避免造成不必要的锁定而影响其他 Query 的执行。
  2. 尽可能减少基于范围的数据检索过滤条件,避免因为间隙锁带来的负面影响而锁定了不该锁定的记录。
  3. 尽量控制事务的大小,减少锁定的资源量和锁定时间长度。
  4. 在业务环境允许的情况下,尽量使用较低级别的事务隔离,以减少 MySQL 因为实现事务隔离级别所带来的附加成本。