详解mysql事物隔离级别与锁机制 - Go语言中文社区

详解mysql事物隔离级别与锁机制


事物隔离级别

首先看下数据库事物四大特性,ACID,原子性,一致性,隔离性,持久性。

隔离性:由并发事务所作的修改必须与任何其它并发事务所作的修改隔离,互相不受影响。同一时间,只允许一个事务请求同一数据,不同的事务之间彼此没有任何干扰。mysql具有四种事物隔离级别,隔离力度依次递增,高度隔离会限制可并行执行的事务数,所以一些应用程序降低隔离级别以换取更大的吞吐量。不同业务场景下使用不同的数据库事物隔离性,部分关键业务采用隔离性高的隔离级别,以保证数据正确性。

msyql四种事物隔离级别:

  1. Read Uncommitted(读未提交)事物能读到不同事物没有提交(未commit)的数据结果,实际应用比较少,会产生脏读,事物已经读到其他事物未提交的数据,但数据被回滚,称为脏读。
  2. Read Committed(读已提交)事物读取其他事物已经提交的数据,读取到的是最新的数据,所以会出现在同一事物中select读取到的数据前后不一致,会出现不可重复读问题,不可重复读问题就是我们在同一个事务中执行完全相同的select语句时可能看到不一样的结果。
  3. Repeatable Read(可重复读)mysql默认事物隔离级别,在同一事物中多次读取同样的数据结果是一样的,解决了不可重复读的问题,此级别会出现幻读的问题,幻读:当用户读取某一范围的数据行时,另一个事务又在该范围内插入了新行,当用户再读取该范围的数据行时,会发现有新的“幻影” 行。
  4. Serializable(串行化)最高的事物隔离级别,串行化强制事物排序阻塞,避免事物冲突,解决了上述所有的问题,它使用了共享锁,执行效率低下,会导致大量的超时和锁切换竞争现象,实际开发应用很少。

数据库事务并发可能出现的问题:

脏读:在隔离级别读未提交中可能会出现,一个事务读取另外一个事务还没有提交的数据叫脏读,事物A读取了事物B更新的事物,事物B没有commit并且回滚,此时就事物A产生脏读,应用也没保证数据的正确性。

  • 新建表,就以财务转账为例子来演示出现的问题
create table account(id int(11) primary key auto_increment,name varchar(16),money float);
  •  设定mysql事物隔离级别

mysql默认的事物隔离级别为可重复读(Repeatable Read),可使用SELECT @@tx_isolation查看。

查看mysql默认事物隔离级别

 

设定事物隔离级别,mysql存储引擎InnoDB支持事物,MyISAM不支持事物,当前mysql默认存储引擎是InnoDB。

SET [SESSION|GLOBAL] TRANSACTION ISOLATION LEVEL [READ UNCOMMITTED|READ COMMITTED|REPEATABLE READ|SERIALIZABLE];

这里,我们开启两个mysql的session会话,把隔离级别都设定为读未提交: 

设定隔离级别为读未提交

 

  • 脏读

向表account中插入一条记录,zhangsan金额为1000

记录

 开启一个事物A,在该事物中把zhangsan的金额扣除300,暂时不提交和回滚:

begin;
update account set money=money-300 where id=1;
事物A

 

另开一个事物B,在事物B中查看zhangsan当前金额数:

begin;
select * from account where id=1;
事物B

 此时在事物B中读取到的数据就为脏数据,事物B读取到了事物A未提交(commit)的数据,当事物A回滚(rollback)时,事物B之前读取到的数据会给应用带来数据错误。

 

不可重复读:在隔离级别读已提交中可能会出现的问题,事物能读取其他事物对同一数据的更改。事务 A 多次读取同一数据,事务 B在事务A多次读取的过程中,对数据作了更新并提交,导致事务A多次读取同一数据时,结果不一致。

  •  设定mysql事物隔离级别为读已提交
set session transaction isolation level read committed;
  •  不可重复读

在A中开启一个事物A,第一次读取到zhangsan的金额数为1000:

begin;
select * from account where id=1;
事物A

然后另外开启一个事物B,添加金额200,并且提交事物:

begin;
update account set money=money+200 where id=1;
commit;

 

事物B

在事物B提交之后再在事物A中读取zhangsan的金额数:

事物A

 

可以看到事物A读取到的数据与之前读取到的不一致,此时就产生了不可重复读的现象。

 

 

幻读:在隔离级别可重复读中可能出现的问题,幻读是针对按范围读取多条数据的现象。同一事务A多次查询,若另一事务B只是update,则A事务多次查询结果相同;若B事务insert/delete数据,则A事务多次查询就会发现新增或缺少数据,出现幻读。

  • 设定mysql事物隔离级别为可重复读
set session transaction isolation level repeatable read;
  • 幻读

在事物A中查找表数据:

事物A

然后在B中开启新事物,插入一条数据,并提交事物:

begin;
insert into account value(5,'l5',500);
commit;
事物B

接下来我们在事物A中查询,发现查询不到第5条记录,但是我们在A中插入id为5的记录时会出问题:

事物A,插入数据

此时就产生了幻读现象,在事物A中查询不到数据,却被告知数据已经存在,解决幻读一般需要锁表。

 

综上,四种隔离级别分别会出现的问题,读未提交未解决任何问题,串行化避免了所有的问题,mysql默认可重复读:

√表示存在问题

 

MySql锁机制

mysql默认存储引擎是InnoDB,InnoDB与Myisam相比,InnoDB支持事物,支持多种锁机制,有行锁和表锁,Myisam只支持表锁,且不支持事物。

上述详细的介绍了mysql事物隔离级别,那么,mysql事物隔离是怎么实现的呢?底层原理是什么?

锁是用来实现数据库事物隔离级别的重要机制,mysql提供了多种锁机制,有共享锁、排他锁、意向共享锁、意向排他锁等,其中共享锁和排他锁都是行锁,意向锁是表锁,意向锁系统控制,人为操作不了。

数据库实现事务隔离的方式有两种:

  1. 一种是在读取数据前,对其加锁,阻止其他事务对数据进行修改,简称为MVCC
  2. 另一种是不用加任何锁,通过一定机制生成一个数据请求时间点的一致性数据快照(Snapshot),并用这个快照来提供一定级别(语句级或事务级)的一致性读取,简称LBCC。

锁的概念

在mysql中,锁分为表锁、行锁,行级锁有共享锁、排他锁。表锁有意向锁,意向共享锁和意向排他锁。

  1. 共享锁,又称为S锁、读锁,顾名思义,共享锁是多个事务对于同一数据可以共享一把锁,都能访问到数据,但是只能读不能修改。普通查询不会加任何锁,手动加共享锁使用select ... lock in share mode语句。
  2. 排他锁,又称为X锁、写锁,排他锁两个事物不能共存。如一个事务获取了一个数据行的排他锁,其他事务就不能再获取该行的其他锁,包括共享锁和排他锁,但是获取排他锁的事务是可以对数据就行读取和修改,所以排他锁会阻塞其他事物的读操作和写操作,直到释放排他锁。在 InnoDB中,update,delete,insert都会自动给涉及到的数据加上排他锁,select查询语句可使用select ...for update加排他锁,事物结束或者rollback/commit就会释放锁。
  3. 意向共享锁(IS):事务打算给数据行加行共享锁,事务在给一个数据行加共享锁前必须先取得该表的IS锁,反之亦然。
  4. 意向排他锁(IX):事务打算给数据行加行排他锁,事务在给一个数据行加排他锁前必须先取得该表的IX锁,反之亦然。意向共享锁和意向排他锁都是mysql自身管理,人为干预不了。

在可重复读中,幻读是通过什么方式解决的?

innoDB存储引擎的锁的算法有三种:

  1. Record lock:单个行记录上的锁
  2. Gap lock:gap锁,间隙锁,锁定一个范围,不包括记录本身
  3. Next-key lock:record+gap 锁定一个范围,包含记录本身

Gap锁设计的目的是为了阻止多个事务将记录插入到同一范围内,而这会导致幻读问题的产生。原理是对行数据操作时,gap锁会对相邻范围的行数据加锁,阻塞,直到操作完成,针对的是非唯一性索引数据。如果是有索引的数据则会使用recored lock行锁,相对来说锁的范围较小,阻塞程度小。

为什么要存在意向锁?

innodb的意向锁主要用户多粒度的锁并存的情况。比如事务A要在一个表上加S锁,如果表中的一行已被事务B加了X锁,那么该锁的申请也应被阻塞。如果表中的数据很多,逐行检查锁标志的开销将很大,系统的性能将会受到影响。举个例子,如果表中记录1亿,事务A把其中有几条记录上了行锁了,这时事务B需要给这个表加表级锁,如果没有意向锁的话,那就要去表中查找这一亿条记录是否上锁了。如果存在意向锁,那么假如事务A在更新一条记录之前,先加意向锁,再加X锁,事务B先检查该表上是否存在意向锁,存在的意向锁是否与自己准备加的锁冲突,如果有冲突,则等待直到事务A释放,而无须逐条记录去检测。

乐观锁、悲观锁

锁从使用方式来分可分为乐观锁和悲观锁,乐观锁和悲观锁在很多应用当中都存在的概念,并不是实质存在的锁叫乐观锁悲观锁,在mysql数据中有,在hibernate、java等当中也有。

乐观锁是在遇到事物并发问题时,想法很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,认为这次的操作不会导致冲突,当出现事物并发问题时再处理。乐观锁由于加锁少,所以性能开销比较小,吞吐量大。

悲观锁的特点是先获取锁,再进行业务操作。每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,开始就默认会出现事物并发问题,所以在进行每次操作数据时都要通过获取锁才能进行对相同数据的操作,悲观锁需要耗费较多的时间,处理并发问题也相对严谨,在核心业务的关键之处可使用悲观锁,开销相对来说大。


经典高并发 ,如何避免库存超发问题

平时在购买商品操作库存流程大概如下:

基本购买流程

假如当前库存量为5,第一个购买请求数量为4个,顺利购买,当前剩余1,第二次购买4个,此时库存不足,程序直接返回库存不足,购买失败。

上述情况不是在高并发情况下,在高并发情况下,可能有多个请求同时购买,同时读取库存更新存库,两次请求同时读取到的库存都是5,然后都执行购买操作,减库存,就出现了库存超发现象,库存减至-3。

类似问题可以用乐观锁、悲观锁来解决。

1、悲观锁解决方案

出现超发的问题根本原因就是共享的数据被多个线程同时修改,如果单独的一个线程想要修改数据,在读取数据时将数据锁定,加排他锁,不允许其他线程读取和修改数据,直到存库修改完成释放锁,就不会出现超发现象。

//开始事物
select id,productid,stock from zproduct where id=? for update;

update zproduct set stock=stock-? where id=?;
//结束事物

使用for update给行数据加排他锁,其他事物就不能对它读和写,避免了数据同时被多个事物修改,此解决方案是实现比较简单,缺点是加排他锁影响系统的并发性能。

乐观锁解决方案

2、乐观锁解决方案

悲观锁很容易产生线程阻塞,为了提高性能,出现了乐观锁方案,不使用数据库锁,不阻塞线程并发。

实现方法:给商品添加一个version字段,代码行修改版本号,读取库存是拿到这个version版本号,在更新时再对比version版本号,如果版本号相同,说明库存没有被其他线程修改过,可以正常操作,同时把version数值加1。如果版本号不同,代表被别的线程修改过,则取消修改库存操作,返回购买失败。

update zproduct set stock=stock-?,version=version+1 where id=? and version=?;

为进一步完善,可以借鉴中间件的重试机制,更新失败后,重新来一遍,重新读取、修改,超过一定时间一定重试次数后,还不成功就放弃,返回失败,否则成功。

 

 

 

 

版权声明:本文来源CSDN,感谢博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/a281246240/article/details/86507118
站方申明:本站部分内容来自社区用户分享,若涉及侵权,请联系站方删除。
  • 发表于 2020-06-27 23:35:40
  • 阅读 ( 801 )
  • 分类:数据库

0 条评论

请先 登录 后评论

官方社群

GO教程

猜你喜欢