图解数据库事务的隔离级别

前言

乐观锁和悲观锁 与 数据库的隔离级别的关系 或者两者使用的场景是什么?

我在网上所能找到的答案,帮助个人的理解。

答案一:事务隔离级别是并发控制的整体解决方案,其实际上是综合利用各种类型的锁和行版本控制,来解决并发问题。锁是数据库并发控制的内部机制,是基础。对用户来说,只有当事务隔离级别无法解决一些并发问题和需求时,才有必要在语句中手动设置锁。

那么事务隔离级别无法解决哪些并发问题呢?

先来看看事务的隔离级别:

事务隔离级别

为了解决多个事务并发会引发的问题,进行并发控制。数据库系统提供了四种事务隔离级别供用户选择。

  • Read Uncommitted 读未提交:不允许第一类更新丢失。允许脏读,不隔离事务。

  • Read Committed 读已提交:不允许脏读,允许不可重复读。

  • Repeatable Read 可重复读:不允许不可重复读。但可能出现幻读。

  • Serializable 串行化:所有的增删改查串行执行。

读未提交

事务读不阻塞其他事务读和写,事务写阻塞其他事务写但不阻塞读。 可以通过写操作加“持续-X锁”实现。

图解数据库事务的隔离级别

读已提交

事务读不会阻塞其他事务读和写,事务写会阻塞其他事务读和写。 可以通过写操作加“持续-X”锁读操作加“临时-S锁”实现

图解数据库事务的隔离级别

可重复读

事务读会阻塞其他事务事务写但不阻塞读,事务写会阻塞其他事务读和写。 可以通过写操作加“持续-X”锁,读操作加“持续-S锁”实现。

图解数据库事务的隔离级别

串行化

“行级锁”做不到,需使用“表级锁”。

可串行化

如果一个并行调度的结果等价于某一个串行调度的结果,那么这个并行调度是可串行化的。

图解数据库事务的隔离级别

区分事务隔离级别是为了解决脏读、不可重复读和幻读三个问题的。

事务隔离级别回滚覆盖脏读不可重复读提交覆盖幻读
读未提交x可能发生可能发生可能发生可能发生
读已提交xx可能发生可能发生可能发生
可重复读xxxx可能发生
串行化xxxxx

既然事务的隔离级别可以做到这些。还需要悲观锁干什么呢?

我的理解是:(理解有错误的,请大家指正)

Mysql默认使用的隔离级别是:可重复读

MSSQL默认使用的隔离级别是:读已提交

如果在MSSQL使用默认使用的隔离级别时读已提交的同事也想开发过程中想解决:不可重复读,提交覆盖和幻读等问题就可以使用悲观锁实现。

MYSQL同理。

尽管悲观锁能够防止丢失更新和不可重复读这类问题,但是它非常影响并发性能,因此应该谨慎使用。

乐观锁不能解决脏读的问题,因此仍需要数据库至少启用“读已提交”的事务隔离级别

常用的解决方案

2.悲观锁和共享锁、排它锁有是什么关系呢?

共享锁和排它锁是悲观锁的不同的实现,它俩都属于悲观锁的范畴。即悲观锁由共享锁和排它锁来实现的。

从读写角度,分共享锁(S锁,Shared Lock)和排他锁(X锁,Exclusive Lock),也叫读锁(Read Lock)和写锁(Write Lock)。理解:

持有S锁的事务只读不可写。如果事务A对数据D加上S锁后,其它事务只能对D加上S锁而不能加X锁。 ? 持有X锁的事务可读可写。如果事务A对数据D加上X锁后,其它事务不能再对D加锁,直到A对D的锁解除

注:要使用悲观锁,我们必须关闭mysql数据库的自动提交属性,因为MySQL默认使用autocommit模式,也就是说,当你执行一个更新操作后,MySQL会立刻将结果进行提交。我们可以使用命令设置MySQL为非autocommit模式:set autocommit=0;设置完autocommit后,我们就可以执行我们的正常业务了。

开始事务使用begin;/begin work;/start transaction; (三者选一就可以);

提交事务使用commit;/commit work;

两种锁的具体实现如下:

  • 共享锁:悲观锁都是由数据库实现的,那共享锁在mysql中是通过什么命令来调用呢。通过在执行语句后面加上lock in share mode 就代表对某些资源加上共享锁了。

比如,我这里通过mysql打开两个查询编辑器,在其中开启一个事务,并不执行commit语句。city表DDL如下

  1. CREATE TABLE `city` (?`id` bigint(20) NOT NULL AUTO_INCREMENT,?`name` varchar(255) DEFAULT NULL,?`state` varchar(255) DEFAULT NULL,?PRIMARY KEY (`id`)?) ENGINE=InnoDB AUTO_INCREMENT=18 DEFAULT CHARSET=utf8;

图解数据库事务的隔离级别

begin;??SELECT * from city where id = "1"  lock in share mode;

然后在另一个查询窗口中,对id为1的数据进行更新

图解数据库事务的隔离级别

update city set name="666" where id ="1";

此时,操作界面进入了卡顿状态,过几秒后,也提示错误信息

[SQL]update city set name="666" where id ="1"; [Err] 1205 - Lock wait timeout exceeded; try restarting transaction

那么证明,对于id=1的记录加锁成功了。在上一条记录还没有commit之前,这条id=1的记录被锁住了,只有在上一个事务释放掉锁后才能进行操作,或用共享锁才能对此数据进行操作。

如果在上面一条记录加上commit;

begin;??SELECT * from city where id = "1"  lock in share mode;??commit;

则 update city set name="666" where id ="1";可以执行成功!

再实验一下:

图解数据库事务的隔离级别

update city set name="666" where id ="1" lock in share mode;

[Err] 1064 - You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near ‘lock in share mode‘ at line 1

加上共享锁后,也提示错误信息了,通过查询资料才知道,对于update,insert,delete语句会自动加排它锁的原因

于是,我又试了试SELECT * from city where id = "1" lock in share mode;

图解数据库事务的隔离级别

这下成功了。

  • 排他锁。排它锁与共享锁相对应,就是指对于多个不同的事务,对同一个资源只能有一把锁。

与共享锁类型,在需要执行的语句后面加上for update就可以了。

排他锁的实现如下:

  • 使用场景举例:以MySQL InnoDB为例

商品t_items表中有一个字段status,status为1代表商品未被下单,status为2代表商品已经被下单(此时该商品无法再次下单),那么我们对某个商品下单时必须确保该商品status为1。假设商品的id为1。 如果不采用锁,那么操作方法如下:

//1.查询出商品信息select status from  t_items where id=1;?//2.根据商品信息生成订单,并插入订单表 t_orders insert into t_orders (id,goods_id) values (null,1)?//3.修改商品status为2update t_items set status=2;

但是上面这种场景在高并发访问的情况下很可能会出现问题。例如当第一步操作中,查询出来的商品status为1。但是当我们执行第三步Update操作的时候,有可能出现其他人先一步对商品下单把t_items中的status修改为2了,但是我们并不知道数据已经被修改了,这样就可能造成同一个商品被下单2次,使得数据不一致。所以说这种方式是不安全的。

  • 使用悲观锁来解决问题

在上面的场景中,商品信息从查询出来到修改,中间有一个处理订单的过程,使用悲观锁的原理就是,当我们在查询出t_items信息后就把当前的数据锁定,直到我们修改完毕后再解锁。那么在这个过程中,因为t_items被锁定了,就不会出现有第三者来对其进行修改了。需要注意的是,要使用悲观锁,我们必须关闭mysql数据库的自动提交属性,因为MySQL默认使用autocommit模式,也就是说,当你执行一个更新操作后,MySQL会立刻将结果进行提交。我们可以使用命令设置MySQL为非autocommit模式:set autocommit=0; 设置完autocommit后,我们就可以执行我们的正常业务了。具体如下:

//0.开始事务?begin;/begin work;/start transaction; (三者选一就可以)?//1.查询出商品信息?select status from t_items where id=1 for update;?//2.根据商品信息生成订单?insert into t_orders (id,goods_id) values (null,1);?//3.修改商品status为2?update t_items set status=2;?//4.提交事务?commit;/commit work;

上面的begin/commit为事务的开始和结束,因为在前一步我们关闭了mysql的autocommit,所以需要手动控制事务的提交。 上面的第一步我们执行了一次查询操作:select status from t_items where id=1 for update;与普通查询不一样的是,我们使用了select…for update的方式,这样就通过数据库实现了悲观锁。此时在t_items表中,id为1的那条数据就被我们锁定了,其它的事务必须等本次事务提交之后才能执行。这样我们可以保证当前的数据不会被其它事务修改(其他事务不能读也不能修改当前的数据)。需要注意的是,在事务中,只有SELECT ... FOR UPDATELOCK IN SHARE MODE 操作同一个数据时才会等待其它事务结束后才执行,一般SELECT ... 则不受此影响。拿上面的实例来说,当我执行select status from t_items where id=1 for update;后。我在另外的事务中如果再次执行select status from t_items where id=1 for update;则第二个事务会一直等待第一个事务的提交,此时第二个查询处于阻塞的状态,但是如果我是在第二个事务中执行select status from t_items where id=1;则能正常查询出数据,不会受第一个事务的影响。

  • Row Lock与Table Lock

使用select…for update会把数据给锁住,不过我们需要注意一些锁的级别,MySQL InnoDB默认Row-Level Lock,所以只有「明确」地指定主键或者索引,MySQL 才会执行Row lock (只锁住被选取的数据) ,否则MySQL 将会执行Table Lock (将整个数据表单给锁住。举例如下: 1、select * from t_items where id=1 for update; 这条语句明确指定主键(id=1),并且有此数据(id=1的数据存在),则采用row lock。只锁定当前这条数据。 2、select * from t_items where id=3 for update; 这条语句明确指定主键,但是却查无此数据,此时不会产生lock(没有元数据,又去lock谁呢?)。 3、select * from t_items where name=‘手机‘ for update; 这条语句没有指定数据的主键,那么此时产生table lock,即在当前事务提交前整张数据表的所有字段将无法被查询。 4、select * from t_items where id>0 for update; 或者select * from t_items where id<>1 for update;(注:<>在SQL中表示不等于) 上述两条语句的主键都不明确,也会产生table lock。 5、select * from t_items where status=1 for update;(假设为status字段添加了索引) 这条语句明确指定了索引,并且有此数据,则产生row lock。 6、select * from t_items where status=3 for update;(假设为status字段添加了索引) 这条语句明确指定索引,但是根据索引查无此数据,也就不会产生lock。

参考的链接有:https://www.jianshu.com/p/71a79d838443

https://blog.csdn.net/puhaiyang/article/details/72284702

https://blog.csdn.net/xiaokang123456kao/article/details/75268240

https://blog.csdn.net/yinni11/article/details/81238541

感谢这四位博主做的贡献!

相关推荐