上篇我们详细说了数据库的索引机制,今天我再来讲讲数据库的事务和锁,这些都是面试必问的东西。所谓八股文,不是背了就行的,只有我们真正理解了,才能举一反三…
每篇文章如果我发现有新的面试题,都会更新到对应的篇章下。有问题的小伙伴也可以在评论区告诉我。。。
事务是数据库系统执行过程的逻辑单元,里面包含一个或者多个业务操作,这些业务操作要么都失败,要么都成功。
1.事务为数据库操作提供了一个从失败状态恢复到正常状态的方法,同时提供了数据库即使在异常状态下仍能保持一致性的方法
2.当多个进程并发执行数据库操作时,事务为数据库提供隔离机制,防止这些进程互相干扰。
事务的原子性是指事务操作要么全部成功,要么全部失败。成功的操作要全部应用到数据库中,而失败的操作不能影响数据库。
其他三种特性都是为了一致性服务的。一致性规定数据库只能从一个一致性状态转变成另一个一致性状态。
如果一个事务中断了,但是部分修改的结果已经写入物理数据库中,这时候数据库就处于一个不正确的状态,也叫不一致状态。
事务与事务的操作之间应该是互不干扰的,相互隔离的。
一个事务成功提交后,这个事务所做的操作应该是永久的,即便是在数据库系统遇到故障的情况下也不会丢失已提交事务所做的操作。
一个事务读到了其他事务未提交的数据,而造成同一个事务的两次相同查询结果不一致,这种情况我们叫做脏读。
这个被读到的在内存中还没同步到磁盘的数据,我们叫做脏数据。
发生了脏读而事务B回滚,这就破坏了一致性
在同一个事务里,执行两个相同查询操作得到不同的结果,是由于读到了其他事务已提交的数据造成的,我们叫做不可重复读。
注意:这个已提交的操作,可以是更新updata,也可以是删除delete
同一事务执行两次相同的范围查询,由于其他事务在范围内新增insert了一条或者多条数据,导致两次查询结果不一致,这种情况叫做幻读
Read Uncommitted(读未提交):三个事务并发问题都没有解决。
Read Committed(已提交读):解决了脏读问题,但是会发生不可重复读,和幻读
Repeatable read(重复读):解决了脏读和不可重复读的问题,但是会发生幻读;
实际上InnoDB在这个隔离机制基础上已经部分解决了幻读。为什么说是部分呢?部分解决是因为临键锁(next-key)和mvcc,后面会详细讲
Serializable(串行化):所谓串行,就是串式运行,不存在并发的问题,但是代价太大,运行效率太低,几乎不会使用。
mvcc是多版本并发控制机制,简单的来说,是通过建立快照的方式,不加锁来处理事务并发问题。
熟悉mvcc之前,我们要首先了解几个概念:
1.事务id
每个事务都会有对应的事务id,事务id是根据事务创建的时间戳来递增创建的。
2.一行数据的默认隐藏字段
DB_TRX_ID(6字节):表示最近一次对本记录行作修改(insert | update)的事务ID。至于delete操作,InnoDB认为是一个update操作,不过会更新一个另外的删除位,将行表示为deleted。并非真正删除。
DB_ROLL_PTR(7字节):回滚指针,指向当前记录行的undo log信息
DB_ROW_ID(6字节):随着新行插入而单调递增的行ID。理解:当表没有主键或唯一非空索引时,innodb就会使用这个行ID自动产生聚簇索引。
3.read view
read view 是用来做可见性判断的,里面保存的是对当前活跃的事务id
4.undolog
我们上面提到了行数据的隐藏字段DB_ROLL_PTR,我们每次对数据进行一次更新,都会形成新的数据版本,然后新版本的DB_ROLL_PTR指向旧版本,旧版本被放入undo log中,形成一个版本链。
熟悉完我们这些概念之后,我们简述下mvcc是怎么工作的:
当我们执行select lock in share mode, select for update ; update, insert ,delete这些加锁的操作时,我们都是读取数据最新的版本,这就叫做当前读;
而当我们执行不加锁的select时,我们就执行快照读。快照读是说我们只能读取带有我们当前事务可见的事务id字段的版本数据。这个可见性分析是通过read view实现的。具体的分析我想单独作为一篇文章来记录,网上也有很多非常详细,讲的非常好的mvcc解读。
例如:MySQL中MVCC的正确打开方式(源码佐证)
1.从锁的用法来看:
乐观锁:
乐观锁是说对并发情况下的事务冲突持有乐观态度,认为产生事务并发问题很小,在提交更新的时候会判断一下在此期间别人有没有去更新这个数据,所以不加锁,而是采用更轻量级的操作来实现并发处理,mysql数据库中乐观锁的体现是mvcc中的当前读和快照读
悲观锁:
悲观锁是说:对并发情况下的事务冲突起一个悲观的态度,认为产生事务并发问题的概率很大,所以必须在操作数据时加锁。在mysql数据库中悲观锁的体现主要是排它锁。
2.从锁的粒度来看:
行锁:行锁是给一行数据加锁,行锁又分为共享锁和排他锁;
共享锁:共享锁又叫读锁,顾明思义,共享锁就是多个事务可以共享一个锁,但是只能是读数据的时候。数据有读锁时,如果事务对数据进行更新会进入死锁状态。所以读锁就是防止事务在读数据时,数据被其他事务修改。
加共享锁:
我们可以通过在sql后面加上LOCK IN SHARE MODE,来加上读锁。当一个数据被事务A加上读锁时,事务B也可以通过在自己的sql后加上LOCK IN SHARE MODE来读到这条数据,这就是共享。
解除共享锁:
commit/rollback;
排他锁:排他锁又叫写锁,顾名思义,排他锁不能和其他锁共存,只有给这行数据加上排他锁的事务才能对这行数据进行操作。
加锁:
自动加锁:update 、delete、insert 默认加排它锁
手动加锁:在select语句后面加上FOR UPDATE;
释放锁:
commit、rollback
表锁:表锁又分为意向共享锁和意向排它锁
意向共享锁:表示事务准备给数据行加入共享锁,也就是说事务给一行数据加共享锁时,必须先获得意向共享锁
意向排他锁:表示事务准备给数据行加入排他锁,也就是说事务给一行数据加共享锁时,必须先获得意向共享锁
意向锁是执行引擎自己维护的,不需要我们手动添加,我们可以简单的理解为这是一个标识,可以通过查看这个表有没有意向锁来了解表内部有没有行锁。
3.从锁的算法来看(通过当前数据已有的记录进行划分):
一.记录锁(Recrod):当唯一性索引(唯一/主键)等值查询时,精准匹配使用
例子:select * from table_name where id =4 for update; 锁住就是id =4这个记录
二.间隙锁(Gap):锁住查询范围所在的区间,注意:这个范围是根据已有的记录区间来划分的,而且间隙锁和间隙锁之间不冲突
例子:有一个记录 7 和 10
当我们执行:select * from table_name where id >20 for update;
锁住的就是[10,+00],10到正无穷的这个区间
三.临键锁(next-key):范围查询,包括记录和区间。
临键锁可以说是记录锁加上间隙锁,InnoDB是默认使用临键锁的,所以即使是在RR的隔离级别下,也部分的解决了幻读的问题,因为我们查询数据时锁住了记录和区间吗,这样我们其他事务就没法插入数据,幻读自然就不会发生了。
4.从锁的问题来看
死锁:我们说死锁是两个进程因为争夺资源而造成相互等待的情况。在数据库中,也会出现死锁的问题,比如说我们在数据有共享锁的时候对他进行修改。
在数据库中的死锁,往往是因为获取锁的顺序不一致造成的。也就是说事务A需要先获取x锁,再获取y锁;事务B需要先获取y锁,再获取x锁;
当A获取了x,需要再获取y时;B也获取了y,需要再获取x;这样两个事务都持有对方需要的锁,而等待对方持有的锁,这就造成了死锁的局面。
参考文章:
事务的隔离级别