深入理解事务、数据库锁以及事务隔离级别(小白必看!结合实际案例)

上一篇文件讲完了慢sql优化,这一篇文章我们今天来聊一聊事务。

在关系型数据库当中,事务,可以是一条sql语句,或者是一组sql语句,亦或者整个程序。

目前业界用的最广泛的关系型数据库就是MySQL了,用的最广泛的执行引擎是InnoDB。
这里我主要针对InnoDB存储引擎来进行讲解。

一、事务的四种特性

事务具备四种属性,分别是原子性,一致性,隔离性,持久性,也就是我们经常说的ACID特性。

原子性(atomicity)

一个事务是一个不可分割的单位,事务中的操作要么都做,要么都不做。

一致性(consistency)

事务是必须使数据库从一个一致性状态到另一个一致性状态。一致性与原子性密不可分。

隔离性(isolation)

一个事务的执行,不能被其他事务干扰。即一个事务内部的操作及使用的数据对并发的其他事务是隔离开来的,并发执行的各个事务之间不能互相干扰。

持久性(durability)

指一个事一旦提交,对于数据库的修改是永久性的,接下来的其他操作跟故障都不能对其有任何影响。

看完这四个概念的描述,你可能还不太清晰,毕竟不是白话文描述。不过没关系,第四节我们会根据实战,来让你更具体的了解事务的这四个特性。

二、详细认识一下锁

说到锁,大家应该都不陌生。
锁是协调多个计算机通过进程或者线程访问某一个资源的机制。

1.从性能上区分锁

乐观锁

通过状态或者版本来加锁,效率较高,不会阻塞。

悲观锁

显示的进行加锁,会阻塞其他操作,并发效率低。

2.从数据库的操作类型区分锁

我们可以把锁分为读锁跟写锁(都是悲观锁)

读锁(又称共享锁,S锁(shared))

针对同一份数据,多个读操作可以同时进行但不会互相影响。

写锁(又称排他锁,X锁(eXclusive))

当前操作没有完成,会阻断其他写跟读的操作。

3.从对数据操作粒度区分锁

行锁

每次进行操作,锁定自己操作的那一行数据。加锁慢,开销大,会产生死锁。锁粒度小,发生锁冲突的概率不高,并发性高。

现在有这么一张表
CREATE TABLE `bank_account` (
  `id` int(10) NOT NULL AUTO_INCREMENT,
  `user_id` int(10) NOT NULL DEFAULT '0' COMMENT '用户id',
  `user_name` varchar(24) NOT NULL DEFAULT '' COMMENT '用户名称',
  `account` int(10) NOT NULL DEFAULT 0 COMMENT '用户账户金额',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='银行账户表'
测试数据
insert into bank_account VALUES(null, 1, '张三', 2000);
insert into bank_account VALUES(null, 1, '李四', 500);
insert into bank_account VALUES(null, 1, '王五', 1000)

mysql的事务隔离级别默认是可重复读(REPEATABLE-READ)
我们在使用mysql的默认事务隔离级别下,进行举例。大家暂时可以不去深究隔离级别,细节的东西我会在第四节实战进行举例,这里只是为了让大家看到行锁这个现象。
在这里插入图片描述
可以看到,在可重复读的隔离级别下,两个事务同时对同一行数据发起修改,后修改事务的会被前面一行修改但是还没提交的事务阻塞,直到前一个事务提及成功,或者是后一个事务等待超时,这个阻塞才会断开。请注意,这里是同一行,也就是说,后面发起的事务可以对该表的其他行进行修改,而不会被阻塞。

在这里插入图片描述

这里,我留给你一个问题,在可重复读的隔离级别下,如果我是两个事务通过user_id先后对表中的id=1的行进行修改(两个事务都还未提交),第一行是肯定会被锁住的,那第二行还可以像我上面这个例子一样进行修改吗?如果不行,原因是什么?

表锁

每次进行操作都会锁住整张表。加锁快,开销小,不会产生死锁,锁粒度大,发生锁冲突的概率很高,表锁的使用场景一般用在数据迁移这种使用场景上,还有给表添加索引也会对整张表进行加锁,阻塞其他想访问被锁住的表的操作,并发性低。
我们可以使用
lock table 表名称1 read(write),表名称2 read(write)来对表进行加锁。
下面举一个表锁的例子,加深一下大家的印象。

现在有这么一张表
CREATE TABLE `bank_account` (
  `id` int(10) NOT NULL AUTO_INCREMENT,
  `user_id` int(10) NOT NULL DEFAULT '0' COMMENT '用户id',
  `user_name` varchar(24) NOT NULL DEFAULT '' COMMENT '用户名称',
  `account` int(10) NOT NULL DEFAULT 0 COMMENT '用户账户金额',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='银行账户表'
测试数据
insert into bank_account VALUES(null, 1, '张三', 2000);
insert into bank_account VALUES(null, 1, '李四', 500);
insert into bank_account VALUES(null, 1, '王五', 1000)

加入我们银行现在要迁移一下用户的金额数据表,现在要对这张表进行加锁,可以允许他们继续看看自己的余额,但是在我迁移完之前,不能让他们改变自己的余额,避免数据丢失。
那么我们现在这样做
在这里插入图片描述
可以看到,当我们对表加了读锁以后,用户可以正常访问表的数据,但是当他想修改金额的时候,被阻塞住了,直到我们释放表锁,他才能继续执行自己的修改操作。
在这里插入图片描述
读锁是允许了读操作,锁定的写操作,那么写锁则是读写操作都进行锁定了。这里大家有兴趣的可以试试,这里不做过多赘述。

4.死锁演示

双方互相持有对方的锁而不进行释放,最终导致死锁。
举个例子
在这里插入图片描述
最终,事务1成功执行,事务2被回滚了。请注意,这里的操作顺序是事务1先开启,修改第一行,然后事务2开启修改第二行,然后事务1再修改第二行,最后是事务2修改第一行。

5.间隙锁

什么叫间隙锁?我先给你看一个现象。
在这里插入图片描述
前提:客户端A跟客户端B的隔离级别都是可重复读(只有这个隔离级别会发生间隙锁)

1.在客户端A开启了一个事务,然后执行了范围修改(事务还未提交)
update bank_account set account = 10 where id > 5 and id < 8;
2.在客户端B开启了一个事务,然后执行行修改
update bank_account set account = 200 where id = 10;
3.注意看,客户端B修改id为10的这一行,被阻塞了。

原因是什么呢? 为什么我修改范围之外的行也会被锁住,难道是锁表了吗?
再看下面这张图,这明显不是锁表了。因为我更新id为1的行,又可以更新。
在这里插入图片描述

看完上面这个案例,我再来给你讲讲,什么叫做间隙锁。
可以看到,客户端更新的id范围是id为5-8的这个区间。
我们来仔细看一下bank_account这张表,你可以发现id并不是连续的,而是中间出现了几个间断的区间。
如 [36][7,10]。这两个间断的区间中的id是没有的,这些区间就叫做间隙。
如上的例子
update bank_account set account = 10 where id > 5 and id < 8;
这个范围刚好落在了[36][7,10]两个间隙之间。
那么,这个间隙区间,我们就可以认为已经被锁上了,而这把锁就叫间隙锁。
他锁住了这个区间除开37的所有范围。
也就是说这两个区间,id为3跟id为7的行我们还是可以访问,但是其他范围的行已经是都被上锁了。

三、对于几种并发出现的事务问题进行分析

并发事务如果隔离级别处理不当,可能会出现以下几种问题。其中,脏读的危害性最大,不可重复读跟幻读需要根据实际的业务场景来看。

脏读

第二个事务读的到第一个事务未提交的数据,然后第一个事务发生异常进行了回滚,第二个事务仍以第一个事务回滚前的数据进行了接下来的操作。
举个例子:
1.张三账户有2000元,李四账户有1500元,张三给李四转了500元,这个时候银行系统因为网络问题,这个转钱的过程还没结束(也就是当前两个事务都还没有进行提交),

2.这个时候,李四去淘宝上买了一个1800元的手机,银行在处理买手机的操作时,读到李四的账户余额是这样的 李四原本的账户余额1500 + 张三转的500 = 2000

3.但是在这个时候,转账操作发生了异常,进行了回滚,张三账户重新变成了2000元,李四的账户重新变成1500元,但是李四却成功下单了1800元的手机。这不就乱套了吗?
所以,我们需要去避免脏读的情况发生

不可重复读

一个事务在执行过程中相同的条件重复读取某一条记录,拿到的结果是不一致的,这就叫不可重复读
举个例子:
1.比如张三现在余额有1000元,他看了一下自己余额,发现自己可以买一个500元的风扇,于是下单买了一个风扇
2.这个时候,李四突然想起来欠了张三500元,于是他转了500元给张三
3.张三在买了风扇以后想看自己还剩多少钱
4.结果发现自己还有1000元

张三买风扇的过程你可以理解为开启了事务A,事务A中一系列的主动操作(看余额-买风扇-看余额),你都可以理解为在同一个事务当中执行了两次读取数据操作.
只不过在第二次读取余额前,有另外一个事务给张三的余额转了500并且成功提交了。所以张三在第二次查余额的时候会看到1000元。

幻读

这里同学们可能经常会把不可重复读跟幻读的概念搞混淆,幻读更看重的是新增。而不可重复度看重的是修改。那么什么叫幻读呢?也就是一个事务按相同条件重复读取记录,读取内容不一致,可以读到别人新增的数据。

四、数据库隔离级别对并发事务的影响-实战

对于并发事务的控制可以通过多种手段进行,比如加锁,比如事务隔离级别,比如MVCC机制。
这里我们针对数据库的四种隔离级别做一个简单的实战,来看一下数据库的四种事务隔离级别对并发事务的影响。
MySQL事务隔离级别

通过show VARIABLES like ‘tx_isolation’;查看MySQL当前的事务隔离级别

1.读未提交(Read uncommitted)

如果设置了未提交,在同一个事务中,可以读取到不同事务还未提交的数据。就有可能会发生脏读,幻读,不可重复读的问题。

设置数据库读未提交 set tx_isolation = ‘read-uncommitted’;

在这里插入图片描述

2.读已提交(Read committed)

在同一个事务中,当前事务可以读到其他事务修改并且已经提交的数据。

设置数据库读已提交 set tx_isolation = ‘read-committed’;
在这里插入图片描述

3.可重复读(Repeatable read,MYSQL默认事务隔离级别)

在同一个事务中,当前事务不可以读到其他事务修改并且已经提交的数据。但是可以修改其他事务新增的数据,并且修改之后,可以读取到其他事务新增并且已经提交的数据。

设置数据库读已提交 set tx_isolation = ‘repeatable-read’;

当前未提交事务无法读取其他已经修改并提交的数据
在这里插入图片描述
当前未提交事务无法读取其他已经修改并提交的数据,但是可以修改其他事务新增的数据,并且修改之后,可以读取到其他事务新增并且已经提交的数据。听起来有点绕,看个例子,可能会让你更加好理解一些。
在这里插入图片描述
你可以把这种现象理解为幻读。

4.可串行化(Serializable)

串行化,第一个事务读取的数据或者操作的数据都会被数据库上锁,这是最好的数据库隔离级别,不会产生并发问题,但是发生并发事务时,允许的并发程度几乎为0.工作中不建议使用这样的隔离级别。
在这里插入图片描述

另外要注意的一点是,数据库的隔离级别越好,并发事务问题越小,但是可以允许的并发程度就越小。所以事务的隔离级别还要根据不同的业务场景去 决定采用何种并发隔离级别。

5.总结

各隔离级别下可能会出现的并发事务问题
在这里插入图片描述

各隔离级别下可能会出现的锁问题

隔离级别读是否上锁根据条件修改是否上锁
读未提交读数据不会上锁不论是否根据索引条件进行更改,修改同一行都会产生行锁 不会产生表锁
读已提交读数据不会上锁不论是否根据索引条件进行更改,修改同一行都会产生行锁 不会产生表锁
可重复读读数据不会上锁根据索引条件更改,修改同一行会产生行锁,根据非索引条件修改,修改同一行会产生表锁
可串行化读数据会把相关联的数据全部加锁修改同一行会产生行锁,根据非索引条件修改,修改同一行会产生表锁

对于我在行锁那一节留给你的问题,我想你看到这里应该就会明白会出现什么问题了。

五、锁的优化建议

这里无非就是在可允许的业务程度下,减少锁的出现,增加MySQL处理的并发程度。主要可以从以下几个方面来入手。

1.尽量让修改的数据检索让索引来完成,避免行锁变成表锁

2.合理设计索引,缩小锁的范围

3.尽可能的降低事务隔离级别

4.尽量缩小范围修改,避免间隙锁

5.涉及事务的多行操作,最有可能发生锁冲突的行操作放最后

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值