0. 对MySQL的锁了解吗

当数据库有并发事务的时候,可能会产生数据的不一致,这时候需要一些机制来保证访问的次序,锁机制就是这样的一个机制。

就像酒店的房间,如果大家随意进出,就会出现多人抢夺同一个房间的情况,而在房间上装上锁,申请到钥匙的人才可以入住并且将房间锁起来,其他人只有等他使用完毕才可以再次使用。

锁的作用:用于管理对共享资源的并发访问,保证数据库的完整性和一致性

1. 隔离级别与锁的关系

在Read Uncommitted级别下,读取数据不需要加共享锁,这样就不会跟被修改的数据上的排他锁冲突

在Read Committed级别下,读操作需要加共享锁,但是在语句执行完以后释放共享锁;

在Repeatable Read级别下,读操作需要加共享锁,但是在事务提交之前并不释放共享锁,也就是必须等待事务执行完毕以后才释放共享锁。

SERIALIZABLE 是限制性最强的隔离级别,因为该级别锁定整个范围的键,并一直持有锁,直到事务完成。

2. MySQL三种锁的级别。按照锁的粒度分数据库锁有哪些?锁机制与InnoDB锁算法

在关系型数据库中,可以按照锁的粒度把数据库锁分为行级锁(INNODB引擎)、表级锁(MYISAM引擎)和页级锁(BDB引擎 )。

MyISAM和InnoDB存储引擎使用的锁:

MyISAM采用表级锁(table-level locking)。

InnoDB支持行级锁(row-level locking)和表级锁,默认为行级锁

表级锁

表级锁是MySQL中锁定粒度最大的一种锁,表示对当前操作的整张表加锁,它实现简单,资源消耗较少,被大部分MySQL引擎支持。最常使用的MYISAM与INNODB都支持表级锁定。表级锁定分为表共享读锁(共享锁)与表独占写锁(排他锁)。

特点

开销小,加锁快;

不会出现死锁;

锁定粒度大,发生锁冲突的概率最高,并发度最低。

行级锁

行级锁是Mysql中锁定粒度最细的一种锁,表示只针对当前操作的行进行加锁。

行级锁能大大减少数据库操作的冲突。其加锁粒度最小,但加锁的开销也最大。行级锁分为 共享锁 和 排他锁。

特点

开销大,加锁慢;

会出现死锁;

锁定粒度最小,发生锁冲突的概率最低,并发度也最高。

页面锁

页级锁是MySQL中锁定粒度介于行级锁和表级锁中间的一种锁。

表级锁速度快,但冲突多,行级冲突少,但速度慢。所以取了折衷的页级,一次锁定相邻的一组记录。

特点

开销和加锁时间界于表锁和行锁之间;

会出现死锁;

锁定粒度界于表锁和行锁之间,并发度一般

封锁粒度小:

好处:锁定的数据量越少,发生锁争用的可能就越小,系统的并发程度就越高;

坏处:系统开销大(加锁、释放锁、检查锁的状态都需要消耗资源)

3. MySQL常见的锁类型?

排它锁(Exclusive Lock)/ X锁

排他锁又叫做写锁。 当用户要进行数据的写入时,对数据加上排他锁。排他锁只可以加一个,他和其他的排他锁,共享锁都相斥。

事务对数据加上X锁时,只允许此事务读取和修改此数据,并且其它事务不能对该数据加任何锁;

SELECT * FROM table_name WHERE ... FOR UPDATE; # 排它锁

共享锁(Shared Lock)/ S锁

共享锁又叫做读锁。 当用户要进行数据的读取时,对数据加上共享锁。共享锁可以同时加上多个。

加了S锁后,该事务只能对数据进行读取而不能修改,并且其它事务只能加S锁,不能加X锁

场景:相当于对于同一把门,它拥有多个钥匙一样。

在执行语句后面加上lock in share mode就代表对某些资源加上共享锁了。

SELECT * FROM table_name WHERE ... LOCK IN SHARE MODE; # 共享锁


意向锁(Intention Locks)

意向锁都是表锁。意向锁的存在是为了允许事务在行级上的锁和表级上的锁同时存在。

意向共享锁(IS Lock)事务想要在获得表中某些记录的共享锁,需要在表上先加意向共享锁。

意向排他锁(IX Lock)事务想要在获得表中某些记录的互斥锁,需要在表上先加意向互斥锁。

由于InnoDB存储引擎支持的是行级别的锁,因此意向锁(因为意向锁是表锁)其实不会阻塞除全表扫以外的任何请求。故表级意向锁与行级锁的兼容性如下所示

是否兼容当前锁模式

X

IX

S

IS

X

冲突

冲突

冲突

冲突

IX

冲突

兼容

冲突

兼容

S

冲突

冲突

兼容

兼容

IS

冲突

兼容

兼容

兼容

IS/IX 锁之间都是兼容的;

好处:如果一个事务想要对整个表加X锁,就需要先检测是否有其它事务对该表或者该表中的某一行加了锁,这种检测非常耗时。有了意向锁之后,只需要检测整个表是否存在IX/IS/X/S锁就行了

意向锁到底有什么作用

innodb的意向锁主要用于解决多粒度的锁并存的情况。

比如事务A要在一个表上加S锁,如果表中的一行已被事务B加了X锁,那么该锁的申请也应被阻塞。

如果表中的数据很多,逐行检查锁标志的开销将很大,系统的性能将会受到影响。

为了解决这个问题,可以在表级上引入新的锁类型来表示其所属行的加锁情况,这就引出了“意向锁”的概念。

举个例子,如果表中记录1亿,事务A把其中有几条记录上了行锁了,这时事务B需要给这个表加表级锁,如果没有意向锁的话,那就要去表中查找这一亿条记录是否上锁了。如果存在意向锁,那么假如事务A在更新一条记录之前,先加意向锁,再加X锁,事务B先检查该表上是否存在意向锁,存在的意向锁是否与自己准备加的锁冲突,如果有冲突,则等待直到事务A释放,而无须逐条记录去检测。事务B更新表时,其实无须知道到底哪一行被锁了,它只要知道反正有一行被锁了就行了。

主要作用是处理行锁和表锁之间的矛盾,能够显示“某个事务正在某一行上持有了锁,或者准备去持有锁”

4. 知道mysql中的锁吗,说一下表锁,行锁,如何上锁(for update),举个例子在什么时候事务会进入阻塞状态

表级锁:开销小,加锁快;不会出现死锁;锁定粒度大,发生锁冲突的概率最高,并发度最低。

行级锁:开销大,加锁慢;会出现死锁;锁定粒度最小,发生锁冲突的概率最低,并发度也最高。

因为不同锁之间的兼容性关系,在有些时刻一个事务中的锁需要等待另一个事务中的锁释放它占有的资源,这就是阻塞。

5. mysql共享锁与排他锁

共享锁与排他锁是行级锁

共享锁又称为读锁,简称S锁,顾名思义,共享锁就是多个事务对于同一数据可以共享一把锁,都能访问到数据,但是只能读不能修改。

排他锁又称为写锁,简称X锁,顾名思义,排他锁就是不能与其他所并存,如一个事务获取了一个数据行的排他锁,其他事务就不能再获取该行的其他锁,包括共享锁和排他锁,但是获取排他锁的事务是可以对数据就行读取和修改。

mysql InnoDB引擎默认的修改数据语句,update,delete,insert都会自动给涉及到的数据加上排他锁,select语句默认不会加任何锁类型,如果加排他锁可以使用select ...for update语句,加共享锁可以使用select ... lock in share mode语句。所以加过排他锁的数据行在其他事务种是不能修改数据的,也不能通过for update和lock in share mode锁的方式查询数据,但可以直接通过select ...from...查询数据,因为普通查询没有任何锁机制。


image

6. 行锁的三种算法,以及解决的问题

InnoDB行锁时通过给索引上的索引项加锁来实现的,Oracle时通过在数据块中相对应数据行加锁来实现。

InnoDB这种行锁实现特点意味着,只有通过索引条件检索条件数据,InnoDB才使用行锁,否则InnoDB将使用表锁。

行锁的三种算法:

Record锁:单个记录上的锁🔒

gap锁:间隙锁,锁定一个范围,但不包含记录本身

next-key锁:Gap Lock + Record Lock,锁定一个范围,并且锁定记录本身


image

记录锁 Record Locks

单个记录上的锁

Record Lock是对索引项加锁。记录锁有两种模式,S模式和X模式。

记录锁总会锁住索引记录,锁住的是key。

即使表没有建立索引,InnoDB也会创建一个隐藏的聚簇索引(隐藏的递增主键索引),并使用此索引进行记录锁定。

场景:一个索引有10,11,13,20这4个值, Innodb可以使用记录锁 Record Locks 将10,11,13,20这四个索引锁住

间隙锁 Gap Locks

间隙锁,锁定一个范围,但不包含记录本身

间隙锁作用在索引记录之间的间隔,又或者作用在第一个索引之前,最后一个索引之后的间隙。不包括索引本身。

例如

SELECT c1 FROM t WHERE c1 BETWEEN 10 and 20 FOR UPDATE;

这条语句阻止其他事务插入10和20之间的数字,无论这个数字是否存在。 间隙可以跨越0个,单个或多个索引值。

InnoDB中的间隙锁的唯一目的是防止其他事务插入间隙。

间隙锁时针对事务隔离级别为可重复读或以上级别而配的。如果事务隔离级别改为RC,则间隙锁会被禁用。

Gap Lock在InnoDB的唯一作用就是防止其他事务的插入操作,以此防止幻读

场景:一个索引有10,11,13,20这4个值, Innodb可以使用间隙锁 Gap Lock 将(-∞,10),(10,11),(11,13),(13,20),(20, +∞)这五个范围锁住🔒

Next-key锁

Gap Lock+Record Lock,锁定一个范围,并且锁定记录本身

Next-key锁是在下一个索引记录本身和索引之前的gap加上S锁或是X锁(如果是读就加上S锁(共享锁),如果是写就加X锁(排他锁))。

InnoDB使用next-key锁对索引进行扫描和搜索,这样就读取不到幻象行,避免了幻读的发生。

当查询的索引含有唯一属性时,InnoDB存储引擎会对Next-Key Lock 进行优化,将其降级为 Record Lock,即仅锁住索引本身,而不是范围。

当查询的索引为辅助索引时,默认使用Next-Key Locking技术进行加锁,锁定范围是前一个索引到后一个索引之间范围。

场景:一个索引有10,11,13,20这4个值, 那么Next-key锁 锁住的范围 (-∞,10],(10,11],(11,13],(13,20],(20, +∞),这五个范围是左闭右开的,可以看出他不仅锁住了一个范围,也会锁定记录本身。

InnoDB这种行锁的实现特点意味着,如果不通过索引条件检索数据,那么InnoDB将对表中的所有记录加锁,实际效果跟锁表一样。

采用Next-Key Lock的锁定技术称为Next-Key Locking。这种设计的目的是为了解决幻读(Phantom Problem)。利用这种锁定技术,锁定的不是单个值,而是一个范围。

7. 实现一个读写锁

读锁(共享锁)是读取操作创建的锁。其他用户可以并发读取数据,但任何事务都不能对数据进行修改(获取数据上的排他锁),直到已释放所有共享锁。 如果事务T对数据A加上共享锁后,则其他事务只能对A再加共享锁,不能加排他锁。获准共享锁的事务只能读数据,不能修改数据。

写锁(排他锁)如果事务T对数据A加上排他锁后,则其他事务不能再对A加任任何类型的封锁。获准排他锁的事务既能读数据,又能修改数据。

事务可以通过以下语句给sql加共享锁和排他锁:

共享锁:select …… lock in share mode;

排他锁:select …… for update;

共享锁:


image

排他锁:


image

8. 什么是三级封锁协议?

一级封锁协议

修改数据加x锁直到事务结束才释放。可以解决丢失修改问题(两个事务不能同时对一个数据加X锁,避免了修改被覆盖);

在一级封锁协议中,读数据是不需要加锁的,它修改数据加x锁直到事务结束才释放。。

二级封锁协议

在一级的基础上,事务在读取数据之前必须先加S锁,读完后释放S锁。可以解决脏读问题(如果已经有事务在修改数据,就意味着已经加了X锁,此时想要读取数据的事务并不能加S锁,也就无法进行读取,避免了读取脏数据);

二级封锁协议除防止了丢失修改,还可以进一步防止读“脏”数据。但在二级封锁协议中,由于读完数据后即可释放S锁,所以它不能保证可重复读。

三级封锁协议

在二级的基础上,事务在读取数据之前必须先加S锁,直到事务结束才能释放。

可以解决不可重复读问题(避免了在事务结束前其它事务对数据加X锁进行修改,保证了事务期间数据不会被其它事务更新)

上述三级协议的主要区别在于什么操作需要申请封锁,以及何时释放。

9. 什么是两段锁协议?

事务必须严格分为两个阶段对数据进行加锁和解锁的操作,第一阶段加锁,第二阶段解锁。也就是说一个事务中一旦释放了锁,就不能再申请新锁了。

目的:引入2PL是为了保证事务的隔离性,保证并发调度的准确性,多个事务在并发的情况下依然是串行的。

可串行化调度是指,通过并发控制,使得并发执行的事务结果与某个串行执行的事务结果相同。事务遵循两段锁协议是保证可串行化调度的充分条件

传统RDBMS加锁的一个原则,就是2PL (二阶段锁):Two-Phase Locking。相对而言,2PL比较容易理解,说的是锁操作分为两个阶段:加锁阶段与解锁阶段,并且保证加锁阶段与解锁阶段不相交。下面,仍旧以MySQL为例,来简单看看2PL在MySQL中的实现。


image

从上图可以看出,2PL就是将加锁/解锁分为两个完全不相交的阶段。加锁阶段:只加锁,不放锁。解锁阶段:只放锁,不加锁。

2PL主要用于Mysql(仅限innodb)中使用的。保证事务的一致性与隔离性。

10. 什么是乐观锁和悲观锁?

乐观锁

乐观锁不是数据库自带的,需要我们自己去实现。乐观锁是指操作数据库时(更新操作),想法很乐观,认为这次的操作不会导致冲突,在操作数据时,并不进行任何其他的特殊处理(也就是不加锁),而在进行更新后,再去判断是否有冲突了。

乐观锁:总是假设最好的情况,每次去拿数据的时候都认为别人不会修改(天真),操作数据时不会上锁,但是更新时会判断在此期间有没有别的事务更新这个数据,若被更新过,则失败重试;适用于读多写少的场景。

乐观锁的实现方式有:

版本号机制:加一个版本号或者时间戳字段,每次数据更新时同时更新这个字段;

一般是在数据表中加上一个数据版本号version字段,表示数据被修改的次数,当数据被修改时,version值会加一。当线程A要更新数据值时,在读取数据的同时也会读取version值,在提交更新时,若刚才读取到的version值为当前数据库中的version值相等时才更新,否则重试更新操作,直到更新成功。

场景:

假设数据库中帐户信息表中有一个 version 字段,当前值为 1 ;而当前帐户余额字段( balance )为 $100 。

操作员 A 此时将其读出( version=1 ),并从其帐户余额中扣除

100-$50 )。

在操作员 A 操作的过程中,操作员B 也读入此用户信息( version=1 ),并从其帐户余额中扣除

100-$20 )。

操作员 A 完成了修改工作,将数据版本号加一( version=2 ),连同帐户扣除后余额( balance=$50 ),提交至数据库更新,此时由于提交数据版本大于数据库记录当前版本,数据被更新,数据库记录 version 更新为 2 。

操作员 B 完成了操作,也将版本号加一( version=2 )试图向数据库提交数据( balance=$80 ),但此时比对数据库记录版本时发现,操作员 B 提交的数据版本号为 2 ,数据库记录当前版本也为 2 ,不满足 “ 提交版本必须大于记录当前版本才能执行更新 “ 的乐观锁策略,因此,操作员 B 的提交被驳回。

这样,就避免了操作员 B 用基于 version=1 的旧数据修改的结果覆盖操作员A 的操作结果的可能。

使用实例:

1. SELECT data AS old_data, version AS old_version FROM …;

2. 根据获取的数据进行业务操作,得到new_data和new_version

3. UPDATE SET data = new_data, version = new_version WHERE version = old_version

if (updated row > 0) {

// 乐观锁获取成功,操作完成

} else {

// 乐观锁获取失败,回滚并重试

}

CAS算法:compare and swap(比较与交换),是一种有名的无锁算法。先读取想要更新的字段或者所有字段,更新的时候比较一下,只有字段没有变化才进行更新。一般情况下是一个自旋操作,即不断的重试

无锁编程,即不使用锁的情况下实现多线程之间的变量同步,也就是在没有线程被阻塞的情况下实现变量的同步,所以也叫非阻塞同步(Non-blocking Synchronization)。

CAS算法涉及到三个操作数

需要读写的内存值(内存位置) V

进行比较的值(原值) A

拟写入的新值(新值) B

当且仅当 内存位置V 的值等于 原值A 时,CAS通过原子方式用 新值B 来更新V的值,否则不会执行任何操作(比较和替换是一个原子操作)。一般情况下是一个自旋操作,即不断的重试。

CAS 有效地说明了“我认为位置 V 应该包含值 A;如果包含该值,则将 B 放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可。”

乐观锁不是锁,乐观锁是一种思想,版本号机制和CAS算法是实现这种思想的一种实现方式

乐观锁的优点:从上面的例子可以看出,乐观锁机制避免了长事务中的数据库加锁开销,大大提升了大并发量下的系统整体性能表现。

乐观锁的缺点:

ABA问题:

如果一个变量V初次读取的时候是A值,并且在准备赋值的时候检查到它仍然是A值,那我们就能说明它的值没有被其他线程修改过了吗?很明显是不能的,因为在这段时间它的值可能被改为其他值,然后又改回A,那CAS操作就会误认为它从来没有被修改过。这个问题被称为CAS操作的 "ABA"问题。

循环时间长开销大

自旋CAS(也就是不成功就一直循环执行直到成功)如果长时间不成功,会给CPU带来非常大的执行开销。

总结:读用乐观锁,写用悲观锁。

悲观锁

悲观锁的实现,往往依靠数据库提供的锁机制(也只有数据库层提供的锁机制才能真正保证数据访问的排他性,否则,即使在本系统中实现了加锁机制,也无法保证外部系统不会修改数据)

认为数据随时会被修改(被害妄想症),因此每次读取数据之前都会上锁,防止其它事务读取或修改数据(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程);

应用于数据更新比较频繁的场景;

传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。

悲观锁的实现:首先实现悲观锁时,我们必须先使用set autocommit=0; 关闭mysql的autoCommit属性。因为我们查询出数据之后就要将该数据锁定。

关闭自动提交后,我们需要手动开启事务。

// 1.开始事务

begin; 或者 start transaction;

// 2.查询出商品信息,然后通过for update锁定数据防止其他事务修改

select status from t_goods where id=1 for update;

// 3.根据商品信息生成订单

insert into t_orders (id,goods_id) values (null,1);

// 4.修改商品status为2

update t_goods set status=2;

// 5.提交事务

commit; --执行完毕,提交事务

上述就实现了悲观锁,悲观锁就是悲观主义者,它会认为我们在事务A中操作数据1的时候,一定会有事务B来修改数据1,所以,在第2步我们将数据查询出来后直接加上排它锁(X)锁,防止别的事务来修改事务1,直到我们commit后,才释放了排它锁。

优点:保证了数据处理时的安全性。

缺点:加锁造成了开销增加,并且增加了死锁的机会。降低了并发性。

乐观锁更新有可能会失败,甚至是更新几次都失败,这是有风险的。所以如果写入居多,对吞吐要求不高,可使用悲观锁。

11. 什么是死锁?

死锁是指两个或两个以上事务在执行过程中因争抢锁资源而造成的互相等待(形成环路)的现象

表级锁不会产生死锁。所以解决死锁主要还是针对于最常用的InnoDB。

死锁的关键在于:两个(或以上)的Session加锁的顺序不一致。那么对应的解决死锁问题的关键就是:让不同的session加锁有次序。

12. 死锁怎么解决?

查出的线程杀死 kill SELECT trx_MySQL_thread_id FROM information_schema.INNODB_TRX;

设置锁的超时时间:Innodb 行锁的等待时间,单位秒。可在会话级别设置,RDS 实例该参数的默认值为 50(秒)。生产环境不推荐使用过大的 innodb_lock_wait_timeout参数值该参数支持在会话级别修改,方便应用在会话级别单独设置某些特殊操作的行锁等待超时时间,如下:set innodb_lock_wait_timeout=1000; —设置当前会话 Innodb 行锁等待超时时间,单位秒。

如果不同程序会并发存取多个表,尽量约定以相同的顺序访问表,可以大大降低死锁机会。

在同一个事务中,尽可能做到一次锁定所需要的所有资源,减少死锁产生概率;

对于非常容易产生死锁的业务部分,可以尝试使用升级锁定颗粒度,通过表级锁定来减少死锁产生的概率;

13. 锁的优化策略

读写分离

分段加锁

减少锁持有的时间

多个线程尽量以相同的顺序去获取资源