MySQL 四种事务隔离级别 + 锁

一、事务的基本要素(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的初始值:

 

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

 

 

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

 

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

 

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

 

  2、读已提交

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

 

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

 

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

 

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

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

 

   3、可重复读

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

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

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

    (4)在客户端A,接着执行update balance = balance - 50 where id = 1,balance没有变成400-50=350,lilei的balance值用的是步骤(2)中的350来算的,所以是300,数据的一致性倒是没有被破坏。

可重复读的隔离级别下使用了MVCC机制

select操作不会更新版本号,是快照读(历史版本);

insert、update和delete会更新版本号,是当前读(当前版本)。

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

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

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

  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 锁。也就是说这个语句对应的锁就相当于update带来的效果 

  • 一旦一个事务获取了这个锁,其他的事务是没法在这些数据上执行 for update

  • 虽然我们平时几乎不会使用select for update进行查询,但是,要记住,update语句之前就是要进行一次for update的select查询的!

  • 【结论】: 

    lock in share mode适用于两张表存在业务关系时的一致性要求,

  • for  update适用于操作同一张表时的一致性要求。

  • 注:普通 select 语句默认不加锁,而CUD操作默认加排他锁。

行锁的算法

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 因为实现事务隔离级别所带来的附加成本。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章