当前位置: 首页 > news >正文

MySQL锁学习笔记

事务的隔离性来实现。

概述

是计算机协调多个进程或线程并发访问某一资源的机制。在程序开发中会存在多线程同步的问题,当多个线程并发访问某个数据的时候,尤其是针对一些敏感的数据(比如订单、金额等),我们就需要保证这个数据在任何时刻最多只有一个线程在访问,保证数据的完整性一致性。在开发过程中加锁是为了保证数据的一致性,这个思想在数据库领域中同样很重要。

在数据库中,除传统的计算资源(如CPU、RAM、I/O等)的争用以外,数据也是一种供许多用户共享的资源。为保证数据的一致性,需要对并发操作进行控制,因此产生了锁。同时锁机制也为实现MySQL的各个隔离级别提供了保证。锁冲突也是影响数据库并发访问性能的一个重要因素。所以锁对数据库而言显得尤其重要,也更加复杂。

MySQL并发事务访问相同记录

并发事务访问相同记录的情况大致可以划分为3种:

读-读情况

读-读情况,即并发事务相继读取相同的记录。读取操作本身不会对记录有任何影响,并不会引起什么问题,所以允许这种情况的发生。

写-写情况

写-写情况,即并发事务相继对相同的记录做出改动。

在这种情况下会发生脏写的问题,任何一种隔离级别都不允许这种问题的发生。所以在多个未提交事务相继对一条记录做改动时,需要让它们排队执行,这个排队的过程其实是通过锁来实现的。这个所谓的锁其实是一个内存中的结构,在事务执行前本来是没有锁的,也就是说一开始是没有锁结构和记录进行关联的。

当一个事务想对这条记录做改动时,首先会看看内存中有没有与这条记录关联的锁结构,当没有的时候就会在内存中生成一个锁结构与之关联。比如,事务T1要对这条记录做改动。就需要生成一个锁结构与之关联:

在这里插入图片描述

在锁结构里有很多信息,为了简化理解,只把两个比较重要的属性拿了出来:

  • trx信息:代表这个锁结构是哪个事务生成的。
  • is_waiting:代表当前事务是否在等待

当事务 T1改动了这条记录后,就生成了一个锁结构与该记录关联,因为之前没有别的事务为这条记录加锁,所以is_waiting属性就是false,我们把这个场景就称之为获取锁成功,或者加锁成功,然后就可以继续执行操作了。

在事务T1提交之前,另一个事务T2也想对该记录做改动,那么先看看有没有锁结构与这条记录关联,发现有一个锁结构与之关联后,然后也生成了一个锁结构与这条记录关联,不过锁结构的 is_waiting 属性值为 true,表示当前事务需要等待,我们把这个场景就称之为获取锁失败,或者加锁失败,图示:

在这里插入图片描述

在事务T1提交之后,就会把该事务生成的锁结构释放掉,然后看看还有没有别的事务在等待获取锁,发现了事务 T2还在等待获取锁,所以把事务T2对应的锁结构的is_waiting属性设置为false,然后把该事务对应的线程唤醒,让它继续执行,此时事务T2就算获取到锁了。效果图就是这样:

在这里插入图片描述

小结几种说法:

  • 不加锁

    意思就是不需要在内存中生成对应的锁结构,可以直接执行操作。

  • 获取锁成功,或者加锁成功

    意思就是在内存中生成了对应的锁结构,而且锁结构的is_waiting属性为false,也就是事务可以继续执行操作。

  • 获取锁失败,或者加锁失败,或者没有获取到锁

    意思就是在内存中生成了对应的锁结构,不过锁结构的is_waiting属性为true,也就是事务需要等待,不可以继续执行操作。

读-写或写-读情况

读-写或写-读,即一个事务进行读取操作,另一个进行改动操作。这种情况下可能发生脏读不可重复读幻读的问题。

各个数据库厂商对SQL标准的支持都可能不一样。比如MySQL在REPEATABLE READ隔离级别上就已经解决了幻读问题。

并发问题的解决方案

怎么解决脏读 、不可重复读、幻读这些问题呢?其实有两种可选的解决方案:

方案一:读操作利用多版本并发控制(MVCC),写操作进行加锁

所谓的MVCC,就是生成一个ReadView,通过ReadView找到符合条件的记录版本(历史版本由undo日志构建)。查询语句只能到在生成ReadView之前已提交事务所做的更改,在生成ReadView之前未提交的事务或者之后才开启的事务所做的更改是看不到的。而写操作肯定针对的是最新版本的记录,读记录的历史版本和改动记录的最新版本本身并不冲突,也就是采用MVCC时,读-写操作并不冲突。

普通的SELECT语句在READ COMMITTED和REPEATABLE READ隔离级别下会使用到MVCC读取记录。

  • 在 READ COMMITTED 隔离级别下,一个事务在执行过程中每次执行SELECT操作时都会生成一个ReadView,ReadView的存在本身就保证了事务不可以读取到未提交的事务所做的更改,也就是避免了脏读现象;
  • 在REPEATABLE READ 隔离级别下,一个事务在执行过程中只有第一次执行SELECT操作才会生成一个ReadView,之后的SELECT操作都复用这个ReadView,这样也就避免了不可重复读和幻读的问题。

方案二:读、写操作都采用加锁的方式。

如果我们的一些业务场景不允许读取记录的旧版本,而是每次都必须去读取记录的最新版本。比如,在银行存款的事务中,你需要先把账户的余额读出来,然后将其加上本次存款的数额,最后再写到数据库中。在将账户余额读取出来后,就不想让别的事务再访问该余额,直到本次存款事务执行完成,其他事务才可以访问账户的余额。这样在读取记录的时候就需要对其进行加锁操作,这样也就意味着读操作和写操作也像写-写操作那样排队执行。

脏读的产生是因为当前事务读取了另一个未提交事务写的一条记录,如果另一个事务在写记录的时候就给这条记录加锁,那么当前事务就无法继续读取该记录了,所以也就不会有脏读问题的产生了。

不可重复读的产生是因为当前事务先读取一条记录,另外一个事务对该记录做了改动之后并提交之后,当前事务再次读取时会获得不同的值,如果在当前事务读取记录时就给该记录加锁,那么另一个事务就无法修改该记录自然也不会发生不可重复读了。

幻读问题的产生是因为当前事务读取了一个范围的记录,然后另外的事务向该范围内插入了新记录,当前事务再次读取该范围的记录时发现了新插入的新记录。采用加锁的方式解决幻读问题就有一些麻烦,因为当前事务在第一次读取记录时幻影记录并不存在,所以读取的时候加锁就有点尴尬(因为你并不知道给谁加锁)。

小结对比发现:

  • 采用MVCC方式的话,读-写操作彼此并不冲突,性能更高
  • 采用加锁方式的话,读-写操作彼此需要排队执行,影响性能

一般情况下我们当然愿意采用 MVCC 来解决读-写操作并发执行的问题,但是业务在某些特殊情况下,要求必须采用加锁的方式执行。下面就讲解下MySQL中不同类别的锁。

锁的不同角度分类

在这里插入图片描述

从数据操作的类型划分:读锁、写锁

对于数据库中并发事务的读-读情况并不会引起什么问题。对于写-写读-写写-读这些情况可能会引起一些问题,需要使用MVCC 或者加锁的方式来解决它们。在使用加锁的方式解决问题时,由于既要允许读-读情况不受影响,又要使写-写读-写写-读情况中的操作相互阻塞,所以MySQL实现一个由两种类型的锁组成的锁系统来解决。这两种类型的锁通常被称为共享锁(Shared Lock,SLock)排他锁(Exclusive Lock,XLock)也叫读锁 (readlock) 和写锁 (write lock)。

  • 读锁:也称为共享锁、英文用 S 表示。针对同一份数据,多个事务的读操作可以同时进行而不会互相影响相互不阻塞的
  • 写锁:也称为排他锁、英文用 X 表示。当前写操作没有完成前,它会阻断其他写锁和读锁。这样就能确保在给定的时间里,只有一个事务能执行写入,并防止其他用户读取正在写入的同一资源。

需要注意的是对于InnoDB 引擎来说,读锁和写锁可以加在表上,也可以加在行上。

举例(行级读写锁):如果一个事务 T1 已经获得了某个行r的读锁,那么此时另外的一个事务T2 是可以去获得这个行r的读锁的,因为读取操作并没有改变行r的数据;但是,如果某个事务 T3 想获得行r的写锁,则它必须等待事务 T1、T2 释放掉行r上的读锁才行。

总结:这里的兼容是指对同一张表或记录的锁的兼容性情况。

X锁S锁
X锁不兼容不兼容
S锁不兼容兼容
锁定读

在采用加锁方式解决脏读不可重复读幻读这些问题时,读取一条记录时需要获取该记录的S锁,其实是不严谨的,有时候需要在读取记录时就获取记录的X锁,来禁止别的事务读写该记录,为此MySQL提出了两种比较特殊的 SELECT 语句格式如下:

  • 对读取的记录加S锁:
SELECT ... LOCK IN SHARE MODE;
#或
SELECT ... FOR SHARE; #(8.0新增语法)

在普通的SELECT语句后边加 LOCK IN SHARE MODE,如果当前事务执行了该语句,那么它会为读取到的记录加S锁,这样允许别的事务继续获取这些记录的 S锁(比方说别的事务也使用 SELECT ... LOCK IN SHARE MODE语句来读取这些记录),但是不能获取这些记录的X锁(比如使用 SELECT ... FOR UPDATE 语句来读取这些记录,或者直接修改这些记录)。如果别的事务想要获取这些记录的X锁,那么它们会阻塞,直到当前事务提交之后将这些记录上的S锁释放掉。

  • 对读取的记录加X锁:
SELECT ... FOR UPDATE;

在普通的SELECT语句后边加 FOR UPDATE,如果当前事务执行了该语句,那么它会为读取到的记录加X锁,这样既不允许别的事务获取这些记录的S锁(比方说别的事务使用 SELECT ... LOCK IN SHARE MODE语句来读取这些记录),也不允许获取这些记录的X锁 (比如使用 SELECT ... FOR UPDATE语句来读取这些记录,或者直接修改这些记录)。如果别的事务想要获取这些记录的S锁或者X锁,那么它们会阻塞,直到当前事务提交之后将这些记录上的X锁释放掉。

MySQL8.0新特性:

在5.7及之前的版本,SELECT ... FOR UPDATE,如果获取不到锁,会一直等待,直到innodb_lock_wait_timeout超时。在8.0版本中,SELECT ... FOR UPDATESELECT ... FOR SHARE添加NOWAITSKIP LOCKED 语法,跳过锁等待,或者跳过锁定。

通过添加NOWAITSKIP LOCKED语法,能够立即返回。如果查询的行已经加锁:

  • 那么NOWAIT会立即报错返回
  • 而SKIP LOCKED也会立即返回,只是返回的结果中不包含被锁定的行。
写操作

平常所用到的写操作无非是DELETEUPDATEINSERT这三种:

  • DELETE:

    对一条记录做DELETE操作的过程其实是先在 B+树中定位到这条记录的位置,然后获取这条记录的X锁,再执行 delete mark 操作。我们也可以把这个定位待删除记录在B+树中位置的过程看成是一个获取X锁锁定读

  • UPDATE:在对一条记录做UPDATE操作时分为三种情况:

    • 情况1:未修改该记录的键值,并且被更新的列占用的存储空间在修改前后未发生变化。

      则先在 B+树中定位到这条记录的位置,然后再获取一下记录的X锁,最后在原记录的位置进行修改操作。我们也可以把这个定位待修改记录在 B+树中位置的过程看成是一个获取X锁锁定读

    • 情况2:未修改该记录的键值,并且至少有一个被更新的列占用的存储空间在修改前后发生变化。

      则先在 B+树中定位到这条记录的位置,然后获取一下记录的X锁,将该记录彻底删除掉(就是把记录彻底移入垃圾链表),最后再插入一条新记录。这个定位待修改记录在 B+ 树中位置的过程看成是一个获取X锁锁定读,新插入的记录由 INSERT 操作提供的隐式锁 进行保护。

    • 情况3:修改了该记录的键值,则相当于在原记录上做 DELETE 操作之后再来一次 INSERT 操作,加锁操作就需要按照 DELETEINSERT 的规则进行了。

  • INSERT:

    一般情况下,新插入一条记录的操作并不加锁,通过一种称之为隐式锁的结构来保护这条新插入的记录在本事务提交前不被别的事务访问。

从数据操作的粒度划分:表级锁、页级锁、行锁

为了尽可能提高数据库的并发度,每次锁定的数据范围越小越好,理论上每次只锁定当前操作的数据的方案会得到最大的并发度,但是管理锁是很耗资源的事情(涉及获取、检查、释放锁等动作)。因此数据库系统需要在高并发响应系统性能两方面进行平衡,这样就产生了锁粒度(Lock granularity)的概念。

对一条记录加锁影响的也只是这条记录而已,我们就说这个锁的粒度比较细;其实一个事务也可以在表级别进行加锁,自然就被称之为表级锁或者表锁,对一个表加锁影响整个表中的记录,我们就说这个锁的粒度比较粗。锁的粒度主要分为表级锁页级锁行锁

表锁(Table Lock)

该锁会锁定整张表,它是 MySQL 中最基本的锁策略,并不依赖于存储引擎(不管你是 MySQL的什么存储引擎,对于表锁的策略都是一样的),并且表锁是开销最小的策略(因为粒度比较大)。由于表级锁一次会将整个表锁定,所以可以很好的避免死锁问题。当然,锁的粒度大所带来最大的负面影响就是出现锁资源争用的概率也会最高,导致并发率大打折扣

表级别的S锁、X锁

在对某个表执行SELECT、INSERT、DELETE、UPDATE语句时,InnoDB存储引擎是不会为这个表添加表级别的S锁或者X锁的。在对某个表执行一些诸如ALTER TABLEDROP TABLE这类的 DDL语句时,其他事务对这个表并发执行诸如SELECT、INSERT、DELETE、UPDATE的语句会发生阻塞。同理,某个事务中对某个表执行SELECT INSERT、DELETE、UPDATE语句时,在其他会话中对这个表执行DDL 语句也会发生阻塞。这个过程其实是通过在server层使用一种称之为元数据锁(英文名: Metadata Locks,简称MDL)结构来实现的。

一般情况下,不会使用InnoDB存储引擎提供的表级别的S锁和X锁。只会在一些特殊情况下,比方说崩溃恢复过程中用到。比如,在系统变量autocommit=0,innodb_table_locks =1时,手动获取InnoDB存储引擎提供的表t的S锁或者X锁可以这么写:

  • LOCK TABLES t READ:InnoDB存储引擎会对表t加表级别的S锁
  • LOCK TABLES t WRITE:InnoDB存储引擎会对表t加表级别的X锁

不过尽量避免在使用InnoDB存储引擎的表上使用LOCK TABLES这样的手动锁表语句,它们并不会提供什么额外保护,只是会降低并发能力而已。InnoDB的厉害之处还是实现了更细粒度的行锁

MyISAM在执行查询语句(SELECT)前,会给涉及的所有表加读锁,在执行增删改操作前,会给涉及的表加写锁。

InnoDB存储引擎是不会为这个表添加表级别的读锁或者写锁的。

MySQL的表级锁有两种模式:

  • 表共享读锁 (Table Read Lock)
  • 表独占写锁 (Table Write Lock)
锁类型自己可读自己可写自己可操作其他表他人可读他人可写
读锁否,等
写锁否,等否,等
意向锁(intention lock)

lnnoDB支持多粒度锁(multiple granularity locking),它允许行级锁表级锁共存,而意向锁就是其中的一种表锁。

  1. 意向锁的存在是为了协调行锁和表锁的关系,支持多粒度(表锁与行锁)的锁并存。
  2. 意向锁是一种不与行级锁冲突表级锁,这一点非常重要。
  3. 表明”某个事务正在某些行持有了锁或该事务准备去持有锁“

意向锁分为两种:

意向共享锁(intention shared lock,IS) : 事务有意向对表中的某些行加共享锁 (S锁)

-- 事务要获取某些行的 S 锁,必须先获得表的 IS 锁。
SELECT column FROM table ... LOCK IN SHARE MODE;

意向排他锁(intention exclusive lock,IX): 事务有意向对表中的某些行加排他锁 (X锁)

-- 事务要获取某些行的 X 锁,必须先获得表的 IX 锁。
SELECT column FROM table ... FOR UPDATE;

即:意向锁是由存储引擎自己维护的,用户无法手动操作意向锁,在为数据行加共享 / 排他锁之前,InooDB 会先获取该数据行**所在数据表的对应意向锁**。

意向锁要解决的问题?

现在有两个事务,分别是T1和T2,其中T2试图在该表级别上应用共享或排它锁,如果没有意向锁存在,那么T2就需要去检查各个页或行是否存在锁;如果存在意向锁,那么此时就会受到由T1控制的表级别意向锁的阻塞。T2在锁定该表前不必检查各个页或行锁,而只需检查表上的意向锁。简单来说就是给更大一级别的空间示意里面是否已经上过锁。

在数据表的场景中,如果我们给某一行数据加上了排它锁,数据库会自动给更大一级的空间,比如数据页或数据表加上意向锁,告诉其他人这个数据页或数据表已经有人上过排它锁了,这样当其他人想要获取数据表排它锁的时候,只需要了解是否有人已经获取了这个数据表的意向排他锁即可。

  • 如果事务想要获得数据表中某些记录的共享锁,就需要在数据表上添加意向共享锁
  • 如果事务想要获得数据表中某些记录的排他锁,就需要在数据表上添加意向排他锁

这时,意向锁会告诉其他事务已经有人锁定了表中的某些记录。

示例:创建表teacher,插入6条数据,事务的隔离级别默认为Repeatable-Read。

DROP TABLE IF EXISTS teacher;
CREATE TABLE `teacher` (`id` int NOT NULL,`name` varchar(255) COLLATE utf8mb4_general_ci NOT NULL,PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
INSERT INTO teacher
VALUES( 1, 'zhangsan' ),( 2, 'lisi' ),( 3, 'wangwu' ),( 4, 'zhaoliu' ),( 5, 'songqi' ),( 6, 'wuba' );

假设事务A获取了某一行的排他锁,并未提交:

BEGIN;SELECT * FROM teacher WHERE id = 6 FROM UPDATE;

事务B想要获取teacher表的表读锁:

BEGIN;LOCK TABLES teacher READ;

因为共享锁与排他锁互斥,所以事务B在试图对teacher表加共享锁的时候,必须保证两个条件。

  1. 当前没有其他事务持有teacher表的排他锁
  2. 当前没有其他事务持有teacher表中任意一行的排他锁

为了检测是否满足第二个条件,事务B必须在确保teacher表不存在任何排他锁的前提下,去检测表中的每一行是否存在排他锁。很明显这是一个效率很差的做法,但是有了意向锁之后,情况就不一样了。

意向锁是怎么解决这个问题的呢?首先,我们需要知道意向锁之间的兼容互斥性,如下所示。

意向共享锁(IS)意向排他锁(IX)
意向共享锁(IS)兼容兼容
意向排他锁(IX)兼容兼容

即意向锁之间是互相兼容的,虽然意向锁和自家兄弟相互兼容,但是它会与普通的排他 / 共享锁互斥。

意向共享锁(IS)意向排他锁(IX)
共享锁(S)兼容互斥
排他锁(X)互斥互斥

注意这里的排他 / 共享锁指的都是表锁,意向锁不会与行级的共享 / 排他锁互斥。回到刚才teacher表的例子。

事务A获取了某一行的排他锁,并未提交:

BEGIN;SELECT * FROM teacher WHERE id = 6 FROM UPDATE;

此时teacher表存在两把锁:teacher表上的意向排他锁与id为6的数据行上的排他锁。事务B想要获取teacher表的共享锁。

BEGIN;LOCK TABLES teacher READ;

此时事务 B 检测事务 A 持有表的意向排他锁,就可以得知事务 A必然持有该表中某些数据行的排他锁,那么事务 B对teacher表的加锁请求就会被排斥(阻塞),而无需去检测表中的每一行数据是否存在排他锁。

意向锁的并发性

意向锁不会与行级的共享 / 排他锁互斥!正因为如此,意向锁并不会影响到多个事务对不同数据行加排他锁时的并发性。(不然我们直接用普通的表锁就行了)

我们扩展一下上面 teacher表的例子来概括一下意向锁的作用(一条数据从被锁定到被释放的过程中,可能存在多种不同锁,但是这里我们只着重表现意向锁)

事务 A先获取了某一行的排他锁,并未提交:

BEGIN;SELECT * FROM teacher WHERE id = 6 FROM UPDATE;

事务A获取了teacher表上的意向排他锁,事务A获取了id为6的数据行上的排他锁。之后事务B想要获取teacher表的共享锁。

BEGIN;LOCK TABLES teacher READ;

事务B检测到事务A持有 teacher表的意向排他锁。事务B对teacher表的加锁请求被阻塞(排斥)。最后事务C也想获取 teacher表中某一行的排他锁。

BEGIN;SELECT * FROM teacher WHERE id = 5 FROM UPDATE;

事务 C 申请teacher表的意向排他锁。事务 C 检测到事务A 持有 teacher表的意向排他锁。因为意向锁之间并不互斥,所以事务 C 获取到了teacher表的意向排他锁。因为 id 为5的数据行上不存在任何排他锁,最终事务C成功获取到了该数据行上的排他锁。

结论:

  1. InnoDB 支持多粒度锁,特定场景下,行级锁可以与表级锁共存。
  2. 意向锁之间互不排斥,但除了IS 与S 兼容外,意向锁会与共享锁 / 排他锁互斥
  3. IX,IS是表级锁,不会和行级的X,S锁发生冲突。只会和表级的X,S发生冲突。
  4. 意向锁在保证并发性的前提下,实现了行锁和表锁共存满足事务隔离性的要求。
自增锁(AUTO-INC锁)

在使用MySQL过程中,我们可以为表的某个列添加AUTO_INCREMENT属性。举例:

CREATE TABLE `teacher` (`id` int NOT NULL AUTO_INCREMENT,`name` varchar(255) COLLATE utf8mb4_general_ci NOT NULL,PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;

由于这个表的id字段声明了AUTO_INCREMENT,意味着在书写插入语句时不需要为其赋值,SQL语句修改如下所示。

INSERT INTO teacher(`name`) VALUES('zhangsan');

上边的插入语句并没有为id列显式赋值,所以系统会自动为它赋上递增的值。

现在我们看到的上面插入数据只是一种简单的插入模式,所有插入数据的方式总共分为三类,分别是”Simple inserts“,”Bulk inserts“和”Mixed-mode inserts“。

  1. “Simple inserts”(简单插入)

可以预先确定要插入的行数(当语句被初始处理时)的语句。包括没有嵌套子查询的单行和多行INSERT...VALUES()REPLACE语句。比如我们上面举的例子就属于该类插入,已经确定要插入的行数。

  1. “Bulk inserts”(批量插入)

事先不知道要插入的行数 (和所需自动递增值的数量)的语句。比如INSERT ... SELECTREPLACE ... SELECTLOAD DATA语句,但不包括纯INSERT。InnoDB在每处理一行,为AUTO_INCREMENT列分配一个新值。

  1. “Mixed-mode inserts”(混合模式插入)

这些是“Simple inserts”语句但是指定部分新行的自动递增值。例如INSERT INTO teacher (id, name) VALUES (1,'a'), (NULL,'b'), (5,'c'), (NULL,'d');只是指定了部分id的值。另一种类型的“混合模式插入”是INSERT ... ON DUPLICATE KEY UPDATE

对于上面数据插入的案例,MySQL中采用了自增锁的方式来实现,AUTO-NC锁是当向使用含有AUTO_INCREMENT列的表中插入数据时需要获取的一种特殊的表级锁,在执行插入语句时就在表级别加一个AUTO-INC锁,然后为每条待插入记录的AUTO_INCREMENT修饰的列分配递增的值,在该语句执行结束后,再把AUTO-NC锁释放掉。一个事务在持有AUTO-INC锁的过程中,其他事务的插入语句都要被阻塞,可以保证一个语句中分配的递增值是连续的。也正因为此,其并发性显然并不高,当我们向一个有AUTO_INCREMENT关键字的主键插入值的时候,每条语句都要对这个表锁进行竞争,这样的并发潜力其实是很低下的,所以innodb通过innodb_autoinc_lock_mode的不同取值来提供不同的锁定机制,来显著提高SQL语句的可伸缩性和性能。

innodb_autoinc_lock_mode有三种取值,分别对应与不同锁定模式:

  1. innodb_autoinc_lock_mode = 0(”传统“锁定模式)

在此锁定模式下,所有类型的insert语句都会获得一个特殊的表级AUTO-NC锁,用于插入具有AUTO_INCREMENT列的表。这种模式其实就如我们上面的例子,即每当执行insert的时候,都会得到一个表级锁(AUTO-INC锁),使得语句中生成的auto _increment为顺序,且在binlog中重放的时候,可以保证master与slave中数据的auto _increment是相同的。因为是表级锁,当在同一时间多个事务中执行insert的时候,对于AUTO-INC锁的争夺会限制并发能力。

  1. innodb_autoinc_lock_mode = 1(”连续“锁定模式)

在 MySQL 8.0 之前,连续锁定模式是默认的。

在这个模式下,“bulk inserts”仍然使用AUTO-INC表级锁,并保持到语句结束。这适用于所有INSERT … SELECT,REPLACE … SELECT和LOAD DATA语句。同一时刻只有一个语句可以持有AUTO-INC锁。

对于“Simple inserts”(要插入的行数事先已知),则通过在**mutex(轻量锁)**的控制下获得所需数量的自动递增值来避免表级AUTO-INC锁,它只在分配过程的持续时间内保持,而不是直到语句完成。不使用表级AUTO-INC锁,除非AUTO-INC锁由另一个事务保持。如果另一个事务保持AUTO-INC锁,则“Simple inserts”等待AUTO-INC锁,如同它是一个“bulk inserts”。

  1. innodb_autoinc_lock_mode = 2(”交错“锁定模式)

从 MySQL 8.0 开始,交错锁模式是默认设置。

在这种锁定模式下,所有类INSERT语句都不会使用表级AUTO-INC 锁,并且可以同时执行多个语句。这是最快和最可扩展的锁定模式,但是当使用基于语句的复制或恢复方案时,从二进制日志重播SQL语句时,这是不安全的。

在此锁定模式下,自动递增值保证在所有并发执行的所有类型的insert语句中是唯一单调递增的。但是,由于多个语句可以同时生成数字(即,跨语句交叉编号),为任何给定语句插入的行生成的值可能不是连续的。

如果执行的语句是“simple inserts”,其中要插入的行数已提前知道,除了“Mixed-mode inserts”之外,为单个语句生成的数字不会有间隙。然而,当执行“bulk inserts”时,在由任何给定语句分配的自动递增值中可能存在间隙。

元数据锁(MDL锁)

MySQL5.5引入了meta data lock,简称MDL锁,属于表锁范畴。MDL的作用是,保证读写的正确性。比如,如果个查询正在遍历一个表中的数据,而执行期间另一个线程对这个 表结构做变更,增加了一列,那么查询线程拿到的结果跟表结构对不上,肯定是不行的。

因此,当对一个表做增删改查操作的时候,加 MDL读锁;当要对表做结构变更操作的时候,加 MDL 写锁。

读锁之间不互斥,因此你可以有多个线程同时对一张表增删改查。读写锁之间、写锁之间是互斥的,用来保证变更表结构操作的安全性,解决了DML和DDL操作之间的一致性问题。不需要显式使用,在访问一个表的时候会被自动加上。

行锁(InnoDB)

行锁(Row Lock)也称为记录锁,顾名思义,就是锁住某一行(某条记录 row)。需要的注意的是,MySQL 服务器层并没有实现行锁机制,行级锁只在存储引擎层实现。

  • 优点:锁定力度小,发生锁冲突概率低,可以实现的并发度高
  • 缺点:对于锁的开销比较大,加锁会比较慢,容易出现死锁情况。

lnnoDB与MyISAM的最大不同有两点:一是支持事务 (TRANSACTION);二是采用了行级锁。

首先我们创建表如下:

CREATE TABLE `student` (`id` int NOT NULL,`name` varchar(20) COLLATE utf8mb4_general_ci DEFAULT NULL,`class` varchar(10) COLLATE utf8mb4_general_ci DEFAULT NULL,PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;

向这个表里插入几条记录:

INSERT INTO student VALUES
(1, '张三', '一班'),
(3, '李四', '一班'),
(8, '王五', '二班'),
(15, '赵六', '二班'),
(20, '钱七', '三班'),

student表中的聚簇索引的简图如下所示。

在这里插入图片描述

这里把B+树的索引结构做了一个超级简化,只把索引中的记录给拿了出来,下面看看都有哪些常用的行锁类型。

记录锁(Record Locks)

记录锁也就是仅仅把一条记录锁上,官方的类型名称为: LOCK_REC_NOT_GAP。比如我们把id值为8的那条记录加一个记录锁的示意图如图所示。仅仅是锁住了id值为8的记录,对周围的数据没有影响。

在这里插入图片描述

举例如下:

Session1Session2
SET autocommit=0;SET autocommit=0;
更新但是不提交,没有手写commit;
UPDATE student SET name = ‘法外狂徒张三’ WHERE id = 1;
Session2被阻塞,只能等待
UPDATE student SET name = ‘李四 1’ WHERE id = 3;
UPDATE student SET name = ‘张三 1’ WHERE id = 1;
提交更新解除阻塞,更新正常进行
UPDATE student SET name = ‘张三 1’ WHERE id = 1;

记录锁是有S锁和X锁之分的,称之为S型记录锁X型记录锁

  • 当一个事务获取了一条记录的S型记录锁后,其他事务也可以继续获取该记录的S型记录锁,但不可以继续获取X型记录锁;
  • 当一个事务获取了一条记录的X型记录锁后,其他事务既不可以继续获取该记录的S型记录锁,也不可以继续获取X型记录锁。
间隙锁(Gap Locks)

MySQL在REPEATABLE READ隔离级别下是可以解决幻读问题的,解决方案有两种,可以使用MVCC方案解决也可以采用加锁 方案解决。但是在使用加锁方案解决时有个大问题,就是事务在第一次执行读取操作时,那些幻影记录尚不存在,我们无法给这些幻影记录加上记录锁。InnoDB提出了一种称之为Gap Locks的锁,官方的类型名称为:LOCK_GAP,我们可以简称为gap锁。比如,把id值为8的那条记录加一个gap锁的示意图如下。

在这里插入图片描述

图中id值为8的记录加了gap锁,意味着不允许别的事务在id值为8的记录前边的间隙插入新记录,其实就是id列的值(3,8)这个区间的新记录是不允许立即插入的。比如,有另外一个事务再想插入一条id值为4的新记录,它定位到该条新记录的下一条记录的id值为8,而这条记录上又有一个gap锁,所以就会阻塞插入操作,直到拥有这个gap锁的事务提交了之后,id列的值在区间(3,8)中的新记录才可以被插入。

**gap锁的提出仅仅是为了防止插入幻影记录而提出的。**虽然有共享gap锁独占gap锁这样的说法,但是它们起到的作用是相同的。而且如果对一条记录加了gap锁(不论是共享gap锁还是独占gap锁),并不会限制其他事务对这条记录加记录锁或者继续加gap锁。

举例:

Session1Session2
SELECT * FROM student WHERE id = 5 LOCK IN SHARE MODE;
SELECT * FROM student WHERE id = 5 FOR UPDATE;

这里 session 2并不会被堵住。因为表里并没有id=5 这个记录,因此 session 1加的是间隙锁(3,8)。而session 2也是在这个间隙加的间隙锁。它们有共同的目标,即:保护这个间隙,不允许插入值。但,它们之间是不冲突的。

注意,给一条记录加了gap锁只是不允许其他事务往这条记录前边的间隙插入新记录,那对于最后一条记录之后的间隙,也就是student 表中id值为 20 的记录之后的间隙该咋办呢?也就是说给哪条记录加 gap锁才能阻止其他事务插入 id 值在(20,+∞)这个区间的新记录呢?这时候两条伪记录派上用场了:

  • Infimum记录,表示该页面中最小的记录。
  • Supremum记录,表示该页面中最大的记录。

为了实现阻止其他事务插入id值在(20,+∞)这个区间的新记录,我们可以给索引中的最后一条记录,也就是id值为20的那条记录所在页面的Supremum记录加上一个gap锁,如图所示。

在这里插入图片描述

这样就可以阻止其他事务插入id值在(20,+∞)这个区间的新记录。

间隙锁的引入,可能会导致同样的语句锁住更大的范围,这其实是影响了并发度的。下面的例子会产生死锁

Session1Session2
BEGIN;
SELECT * FROM student WHERE id = 5 FOR UPDATE;
BEGIN;
SELECT * FROM student WHERE id = 5 FOR UPDATE;
INSERT INTO student VALUES(5,‘老五’,‘二班’);阻塞
INSERT INTO student VALUES(5,‘老五’,‘二班’);
(ERROR 1213 (40001): Deadlock found when trying to get lock; tryrestarting transaction)
  1. session 1执行 select … for update 语句,由于id=5这一行并不存在,因此会加上间隙锁(3,8);
  2. session 2执行 select … for update 语句,同样会加上间隙锁 (3,8),间隙锁之间不会冲突,因此这个语句可以执行成功;
  3. session 2试图插入一行 (5,‘老五’,‘二班’),被 session 1的间隙锁挡住了,只好进入等待;
  4. session 1试图插入一行 (5,‘老五’,‘二班’),被 session 2的间隙锁挡住了。至此,两个session进入互相等待状态,形成死锁。当然,InnoDB的死锁检测马上就会发现这对死锁关系,让session1的insert语句报错返回。
临键锁(Next-Key Locks)

有时候我们既想锁住某条记录,又想阻止其他事务在该记录前边的间隙插入新记录,所以InnoDB就提出了一种称之为Next-Key Locks的锁,官方的类型名称为:LOCK_ORDINARY,我们也可以简称为next-key锁。Next-Key Locks是在存储引擎 innodb、事务级别在可重复读的情况下使用的数据库锁,innodb默认的锁就是Next- Key locks。比如,我们把id值为8的那条记录加一个next-key锁的示意图如下:

在这里插入图片描述

next-key锁的本质就是一个记录锁和一个gap锁的合体,它既能保护该条记录,又能阻止别的事务将新记录插入被保护记录前边的间隙

begin;
select * from student where id <= 8 and id > 3 for update;
插入意向锁(Insert Intention Locks)

我们说一个事务在插入一条记录时需要判断一下插入位置是不是被别的事务加了gap锁(next-key锁也包含gap锁),如果有的话,插入操作需要等待,直到拥有gap锁的那个事务提交。但是InnoDB规定事务在等待的时候也需要在内存中生成一个锁结构,表明有事务想在某个间隙中插入新记录,但是现在在等待。lnnoDB就把这种举型的锁命名为Insert Intention Locks,官方的举型名称为:LOCK_INSERT_INTENTION,我们称为插入意向锁。插入意向锁是一种Gap锁,不是意向锁,在insert操作时产生。

插入意向锁是在插入一条记录行前,由INSERT操作产生的一种间隙锁。该锁用以表示插入意向,当多个事务在同一区间(gap)插入位置不同的多条数据时,事务之间不需要互相等待。假设存在两条值分别为 4和7的记录,两个不同的事务分别试图插入值为5和6的两条记录,每个事务在获取插入行上独占的(排他)锁前,都会获取(4,7)之间的间隙锁,但是因为数据行之间并不冲突,所以两个事务之间并不会产生冲突(阻塞等待)。总结来说,插入意向锁的特性可以分成两部分:

  1. 插入意向锁是一种特殊的间隙锁 ——间隙锁可以锁定开区间内的部分记录
  2. 插入意向锁之间互不排斥,所以即使多个事务在同一区间插入多条记录,只要记录本身 (主键、唯一索引)不冲突,那么事务之间就不会出现冲突等待。

注意,虽然插入意向锁中含有意向锁三个字,但是它并不属于意向锁而属于间隙锁,因为意向锁是表锁而插入意向锁是行锁

比如,把id值为8的那条记录加一个插入意向锁的示意图如下:

在这里插入图片描述

比如,现在T1为id值为8的记录加了一个gap锁,然后T2和T3分别想向student表中插入id值分别为4、5的两条记录,所以现在为id值为8的记录加的锁的示意图就如下所示:

在这里插入图片描述

从图中可以看到,由于T1持有gap锁,所以T2和T3需要生成一个插入意向锁的锁结构并且处于等待状态。当T1提交后会把它获取到的锁都释放掉,这样T2和T3就能获取到对应的插入意向锁了(本质上就是把插入意向锁对应锁结构的is_waiting属性改为false),T2和T3之间也并不会相互阻塞,它们可以同时获取到id值为8的插入意向锁,然后执行插入操作。事实上插入意向锁并不会阻止别的事务继续获取该记录上任何类型的锁

页锁

页锁就是在页的粒度上进行锁定,锁定的数据资源比行锁要多,因为一个页中可以有多个行记录,当我们使用页锁的时候,会出现数据浪费的现象,但这样的浪费最多也就是一个页上的数据行。页锁的开销介于表锁和行锁之间,会出现死锁。锁定粒度介于表锁和行锁之间,并发度一般。

每个层级的锁数量是有限制的,因为锁会占用内存空间,锁空间的大小是有限的。当某个层级的锁数量超过了这个层级的阈值时,就会进行锁升级。锁升级就是用更大粒度的锁替代多个更小粒度的锁,比如InnoDB中行锁升级为表锁,这样做的好处是占用的锁空间降低了,但同时数据的并发度也下降了。

从对待锁的态度划分:乐观锁、悲观锁

从对待锁的态度来看锁的话,可以将锁分成乐观锁和悲观锁,从名字中也可以看出这两种锁是两种看待数据并发的思维方式。需要注意的是,乐观锁和悲观锁并不是锁,而是锁的设计思想。

悲观锁(Pessimistic Locking)

悲观锁是一种思想,顾名思义,就是很悲观,对数据被其他事务的修改持保守态度,会通过数据库自身的锁机制来实现,从而保证数据操作的排它性。

悲观锁总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁,当其他线程想要访问数据时都需要阻塞挂起。Java中synchronizedReentrantLock等独占锁就是悲观锁思想的实现。

秒杀案例1

商品秒杀过程中,库存数量的减少,避免出现超卖的情况。比如,商品表中有一个字段为quantity表示当前该商品的库存量。假设商品为XXX手机,id为1001,quantity=100个。如果不使用锁的情况下,操作方法如下所示

#第1步:查出商品库存
select quantity from items where id = 1001;
#第2步:如果库存大于0,则根据商品信息生产订单
insert into orders(item_id) values(1001);
#第3步:修改商品的库存,num表示购买数量
update items set quantity = quantity-num where id = 1001;

这样写的话,在并发量小的公司没有大的问题,但是如果在高并发环境下可能出现以下问题:

线程A线程B
1step1(查询还有100部手机)step1(查询还有100部手机)
2step2(生成订单)
3step2(生成订单)
4Step3(减库存1)
5Step3(减库存2)

其中线程B此时已经下单并且减完库存,这个时候线程A依然去执行step3,就造成了超卖。

我们使用悲观锁可以解决这个问题,商品信息从查询出来到修改,中间有一个生成订单的过程,使用悲观锁的原理就是,当我们在查询items信息后就把当前的数据锁定,直到我们修改完毕后再解锁。那么整个过程中,因为数据被锁定了,就不会出现有第三者来对其进行修改了。而这样做的前提是需要将要执行的SQL语句放在同一个事务中,否则达不到锁定数据行的目的

修改如下:

#第1步:查出商品库存
select quantity from items where id = 1001 for update;
#第2步:如果库存大于0,则根据商品信息生产订单
insert into orders(item_id) values(1001);
#第3步:修改商品的库存,num表示购买数量
update items set quantity = quantity-num where id = 1001;

select ... for update是MySQL中悲观锁。此时在items表中,id为1001的那条数据就被我们锁定了,其他的要执行select quantity from items where id = 1001 for update;语句的事务必须等本次事务提交之后才能执行。这样我们可以保证当前的数据不会被其它事务修改。

注意,当执行select quantity from items where id = 1001 for update;语句之后,如果在其他事务中执行select quantity from items where id = 1001;语句,并不会受第一个事务的影响,仍然可以正常查询出数据。

注意:select … for update语句执行过程中所有扫描的行都会被锁上,因此在MySQL中用悲观锁必须确定使用了索引,而不是全表扫描,否则将会把整个表锁住。

悲观锁不适用的场景较多,它存在一些不足,因为悲观锁大多数情况下依靠数据库的锁机制来实现,以保证程序的并发访问性,同时这样对数据库性能开销影响也很大,特别是长事务而言,这样的开销往往无法承受,这时就需要乐观锁。

乐观锁(Optimistic Locking)

乐观锁认为对同一数据的并发操作不会总发生,属于小概率事件,不用每次都对数据上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,也就是不采用数据库自身的锁机制,而是通过程序来实现。在程序上,我们可以采用版本号机制或者CAS机制实现。乐观锁适用于多读的应用类型,这样可以提高吞吐量。在Java中 java.util.concurrent.atomic包下的原子变量类就是使用了乐观锁的一种实现方式:CAS实现的。

乐观锁的版本号机制

在表中设计一个版本字段 version,第一次读的时候,会获取 version 字段的取值。然后对数据进行更新或删除操作时,会执行UPDATE ... SET version=version+1 WHERE version=version。此时如果已经有事务对这条数据进行了更改,修改就不会成功。

这种方式类似我们熟悉的 SVN、CVS 版本管理系统,当我们修改了代码进行提交时,首先会检查当前版本号与服务器上的版本号是否一致,如果一致就可以直接提交,如果不一致就需要更新服务器上的最新代码,然后再进行提交。

乐观锁的时间戳机制

时间戳和版本号机制一样,也是在更新提交的时候,将当前数据的时间戳和更新之前取得的时间戳进行比较,如果两者一致则更新成功,否则就是版本冲突。

你能看到乐观锁就是程序员自己控制数据并发操作的权限,基本是通过给数据行增加一个戳(版本号或者时间戳),从而证明当前拿到的数据是否最新。

秒杀案例2:

依然使用上面秒杀的案例,执行流程如下:

#第1步:查出商品库存
select quantity from items where id = 1001;
#第2步:如果库存大于0,则根据商品信息生产订单
insert into orders(item_id) values(1001);
#第3步:修改商品的库存,num表示购买数量
update items set quantity = quantity-num, version = version+1 where id = 1001 and version = #{version};

注意,如果数据表是读写分离的表,当matser表中写人的数据没有及时同步到slave表中时,会造成更新一直失败的问题,此时需要强制读取master表中的数据(即将select语句放到事务中即可,这时候查询的就是master主库了。)

如果我们对同一条数据进行频繁的修改的话,那么就会出现这么一种场景,每次修改都只有一个事务能更新成功,在业务感知上面就有大量的失败操作。我们把代码修改如下:

#第1步:查出商品库存
select quantity from items where id = 1001;
#第2步:如果库存大于0,则根据商品信息生产订单
insert into orders(item_id) values(1001);
#第3步:修改商品的库存,num表示购买数量
update items set quantity = quantity-num where id = 1001 and quantity-num > 0;

这样就会使每次修改都能成功,而且不会出现超卖的现象。

两种锁的适用场景

从这两种锁的设计思想中,我们总结一下乐观锁和悲观锁的适用场景:

  1. 乐观锁适合读操作多的场景,相对来说写的操作比较少。它的优点在于程序实现,不存在死锁问题,不过适用场景也会相对乐观,因为它阻止不了除了程序以外的数据库操作。
  2. 悲观锁适合写操作多的场景,因为写的操作具有排它性。采用悲观锁的方式,可以在数据库层面阻止其他事务对该数据的操作权限,防止读 - 写写 - 写的冲突。

我们把乐观锁和悲观锁总结如下图所示。

在这里插入图片描述

按加锁的方式划分:显式锁、隐式锁
隐式锁

一个事务在执行INSERT操作时,如果即将插入的间隙已经被其他事务加了gap锁,那么本次INSERT操作会阻塞,并且当前事务会在该间隙上加一个插入意向锁,否则一般情况下INSERT操作是不加锁的。那如果一个事务首先插入了一条记录(此时并没有在内存生产与该记录关联的锁结构),然后另一个事务:

  • 立即使用SELECT ... LOCK IN SHARE MODE语句读取这条记录,也就是要获取这条记录的S锁,或者使用SELECT … FOR UPDATE 语句读取这条记录,也就是要获取这条记录的 X锁,怎么办?

    如果允许这种情况的发生,那么可能产生脏读问题。

  • 立即修改这条记录,也就是要获取这条记录的 X锁,怎么办?

    如果允许这种情况的发生,那么可能产生脏写问题。

这时候我们前边提过的事务id又要起作用了。我们把聚簇索引和二级索引中的记录分开看一下。

  • 情景一:对于聚簇索引记录来说,有一个trx_id隐藏列,该隐藏列记录着最后改动该记录的事务id。那么如果在当前事务中新插入一条聚簇索引记录后,该记录的trx_id隐藏列代表的的就是当前事务的事务id,如果其他事务此时想对该记录添加S锁或者X锁时,首先会看一下该记录的trx_id隐藏列代表的事务是否是当前的活跃事务,如果是的话,那么就帮助当前事务创建一个X锁(也就是为当前事务创建一个锁结构,is_waiting属性是 false),然后自己进入等待状态(也就是为自己也创建一个锁结构,is_waiting属性是true )。
  • 情景二:对于二级索引记录来说,本身并没有trx_id隐藏列,但是在二级索引页面的 Page Header部分有个PAGE_MAX_TRX_ID属性,该属性代表对该页面做改动的最大的事务id,如果PAGE_MAX_TRX_ID属性值小于当前最小的活跃事务id,那么说明对该页面做修改的事务都已经提交了,否则就需要在页面中定位到对应的二级索引记录,然后回表找到它对应的聚簇索引记录,然后再重复情景一的做法。

即:一个事务对新插入的记录可以不显式的加锁(生成一个锁结构),但是由于事务id的存在,相当于加了一个隐式锁。别的事务在对这条记录加S锁或者X锁 时,由于隐式锁的存在,会先帮助当前事务生成一个锁结构,然后自己再生成一个锁结构后进入等待状态。隐式锁是一种延迟加锁的机制,从而来减少加锁的数量。

隐式锁在实际内存对象中并不含有这个锁信息。只有当产生锁等待时,隐式锁转化为显式锁。

lnnoDB 的insert操作,对插入的记录不加锁,但是此时如如果另一个线程进行当前读,类似以下的用例,session 2会锁等待 session 1,那么这是如何实现的呢?

session1:

BEGIN;
INSERT INTO student VALUES(34, '周八皮', '三班');

session2:

BEGIN;
SELECT * FROM student LOCK IN SHARE MODE; #执行完,当前事务被阻塞

查看结果:

SELECT * FROM `performance_schema`.data_lock_waits;

在这里插入图片描述

隐式锁的逻辑过程如下:

A. InnoDB的每条记录中都一个隐含的trx_id字段,这个字段存在于聚簇索引的B+Tree中。

B. 在操作一条记录前,首先根据记录中的trx_id检查该事务是否是活动的事务(未提交或回滚)。如果是活动的事务,首先将隐式锁转换为显式锁(就是为该事务添加一个锁)。

C. 检查是否有锁冲突,如果有冲突,创建锁,并设置为waiting状态。如果没有冲突不加锁,跳到E.

D. 等待加锁成功,被唤醒,或者超时。

E. 写数据,并将自己的trx_id写入trx _id字段。

显式锁

通过特定的语句进行加锁,我们一般称之为显示加锁,例如:

显示加共享锁:

select ... lock in share mode;

显示加排它锁:

select ... for update;

相关文章:

MySQL锁学习笔记

锁 事务的隔离性由锁来实现。 概述 锁是计算机协调多个进程或线程并发访问某一资源的机制。在程序开发中会存在多线程同步的问题&#xff0c;当多个线程并发访问某个数据的时候&#xff0c;尤其是针对一些敏感的数据&#xff08;比如订单、金额等&#xff09;&#xff0c;我…...

如何将前后端分离项目部署到本地的Docker Desktop容器运行并且访问

文章目录 前言 完成了客户的一个前后端分离项目&#xff0c;要求部署到客户电脑上去展示&#xff0c;那肯定不能直接把代码弄上去跑呀~~~&#xff0c;于是我就想把他们都打包部署到本地的docker容器里面&#xff0c;方便运行和访问&#xff0c;so&#xff0c;以下内容就详细介…...

前端开发中的try...catch

首先try...catch 结构可以用来处理 Promise 中的异常。在 JavaScript 中&#xff0c;Promise 提供了一种处理异步操作的机制&#xff0c;并且可以通过 .catch() 方法捕获并处理异步操作中抛出的异常。 async function someAsyncFunction() {try {const result await someProm…...

数据加密中,采用密钥管理系统相比加密机的好处

密钥管理系统与加密机都能提供数据加解密&#xff0c;那么针对具体的应用加密&#xff0c;采用密钥管理系统比单纯使用加密机有哪些优点&#xff0c;列表如下&#xff1a; 集中化管理&#xff1a;密钥管理系统可以对加密算法和密钥进行集中化管理&#xff0c;使得企业可以对加…...

Elasticsearch:什么是大语言模型 (LLMs)?

假设你想参加流行的游戏节目 Jeopardy&#xff08;这是一个美国电视游戏节目&#xff0c;参赛者将获得答案并必须猜测问题&#xff09;。 要参加演出&#xff0c;你需要了解任何事情的一切。 所以你决定在接下来的三年里每天都花时间阅读互联网上的所有内容。 你很快就会意识到…...

神奇的python的生成器

函数生成器代码 def num():print("message 1")yield 1print("message 2")yield 2print("message 3")yield 3f num() x next(f) # message 1 print(x) # 输出1x next(f) # message 2 print(x) # 输出2x next(f) # message 3 print(x) …...

【来点小剧场--项目测试报告】个人博客项目自动化测试

前述 针对个人博客项目进行测试&#xff0c;个人博客主要由七个页面构成&#xff1a;注册页、登录页、个人博客列表页、博客发布页、博客修改页、博客列表页、博客详情页&#xff0c;主要功能包括&#xff1a;注册、登录、编辑并发布博客、修改已发布的博客、查看详情、删除博…...

【安卓环境搭建报错的解决】

安卓环境搭建报错的解决 问题描述解决方法 问题描述 电脑中新安装的 Android Studio Giraffe | 2022.3.1 Patch &#xff0c;运行 studio 系统工程&#xff0c;提示如下错误 Duplicate class kotlin.collections.jdk8.CollectionsJDK8Kt found in modules kotlin-stdlib-1.8.…...

Pruning Pre-trained Language Models Without Fine-Tuning

本文是LLM系列文章&#xff0c;针对《Pruning Pre-trained Language Models Without Fine-Tuning》的翻译。 修剪未微调的预训练语言模型 摘要1 引言2 相关工作3 背景4 静态模型剪枝5 实验6 分析7 结论8 局限性 摘要 为了克服预训练语言模型(PLMs)中的过度参数化问题&#xf…...

Java内存模型-Java Memory Model(JMM)-可见性、原子性、有序性

5. Java内存模型之JMM 5.1 先从大场面试开始 你知道什么是Java内存模型JMM吗&#xff1f; JMM和volatile他们两个之间的关系&#xff1f; JMM没有那些特征或者它的三大特征是什么&#xff1f; 为什么要有JMM&#xff0c;它为什么出现&#xff1f;作用和功能是什么&#xf…...

基于Springboot实现在线答疑平台系统项目【项目源码+论文说明】

基于Springboot实现在线答疑平台系统演示 摘要 社会的发展和科学技术的进步&#xff0c;互联网技术越来越受欢迎。网络计算机的生活方式逐渐受到广大师生的喜爱&#xff0c;也逐渐进入了每个学生的使用。互联网具有便利性&#xff0c;速度快&#xff0c;效率高&#xff0c;成本…...

前端工程化知识系列(1)

目录 1. 什么是前端工程化&#xff0c;以及它为前端开发带来了哪些好处&#xff1f;2. 你使用过哪些版本控制系统&#xff1f;描述一下你在团队中如何处理代码合并和冲突解决的经验。3. 什么是Git&#xff0c;它的工作原理是什么&#xff1f;可以解释一下常用的Git命令吗&#…...

xml文件报错 ORA-00907: 缺失右括号

原来的sql 更改之后 加一个select * from &#xff08;&#xff09;...

VScode platformio的使用

一、platformio 工程创建 打开vscode界面你会发现左下多了个家的小图标&#xff0c;点击这里就可以进入platformio。 在右侧Quick Access栏中&#xff0c;有4个选项。可以看得出来&#xff0c;我们这里直接点击创建一个新的工程。 点击New Project打开project配置界面&#x…...

transformer_01

一、传统RNN存在的问题 1.序列前序太长&#xff0c;每个xi要记住前面的特征&#xff0c;而且一直在学&#xff0c;没有忘记&#xff0c;可能特征不能学的太好 2.串行&#xff0c;层越多越慢&#xff0c;难以堆叠很多层&#xff1b; 3.只能看到过去&#xff0c;不能看到未来 搞…...

JavaSE入门---认识方法

文章目录 什么是方法&#xff1f;方法定义实参和形参的关系没有返回值的方法 方法重载方法签名 什么是方法&#xff1f; 在编程中某段功能的代码可能频繁使用到&#xff0c;如果在每个位置都重新实现一遍&#xff0c;会有一些缺点&#xff0c;比如&#xff1a; 使程序变得繁琐…...

编译[Bug]——too few arguments for template template parameter “Tuple“ detected

项目场景&#xff1a; 当使用高版本的cuda去安装低版本pytorch&#xff0c;并且编译用低版本pytorch写的cuda算子时&#xff0c;或者说是VS的版本过高如2022和2019&#xff0c;都有可能会出现某个.h文件或者.c文件报错&#xff0c;如&#xff1a; error: too few arguments f…...

网工内推 | 南天软件,base北京,需持有CCIE认证,最高25k

01 北京南天软件有限公司 招聘岗位&#xff1a;IPT运维工程师 职责描述&#xff1a; 负责客户Cisco语音网络IPT ,CUCM的日常运维&#xff0c;扩容和项目支持&#xff0c;支持路由交换&#xff0c;无线等项目&#xff0c;实施工作以及相关实施文档。 任职要求&#xff1a; 1、…...

Unity有限状态机的简易实现

本人嘴笨&#xff0c;不会说。 该代码实现一个功能较为齐全的有限状态机&#xff0c;可用于大部分的应用场景。 大致实现几个功能 状态更新状态转换状态消息处理全局状态转换和反转状态&#xff08;转换为前一个状态&#xff09; 代码分为 状态类状态管理类枚举&#xff08;…...

什么是NetApp的DQP和如何安装DQP?

首先看看什么是DQP&#xff0c;DQPDisk Qualification Package&#xff0c;文字翻译就是磁盘验证包。按照NetApp的最佳实践&#xff0c;要定期升级DQP包&#xff0c;保证对最新磁盘和磁盘扩展柜的兼容。 本文主要介绍7-mode下如何升级DQP&#xff0c;至于cluster mode另外文章…...

Vue之Vue的介绍安装开发实例生命周期钩子

博主心得&#xff1a; keyup必须与change一起使用v-on.click可以直接写成clickclick“setVal”里的setVal换成数字之后有惊喜VS Code是真的狗&#xff0c;一些报错根本不会直接显示总结&#xff1a;VS code太狗了 1.vue介绍 1.1 什么是vue vue是一个构建用户界面UI的渐进式jav…...

【计网】计算机网络概述

目录 一、计算机网络的概念 二、计算机网络的组成 1、从组成部分上看 2、从工作方式上看 3、从功能组成上看 三、计算机网络的功能 1、数据通信 2、资源共享 3、分布式处理 4、提高可用性 5、负载均衡 四、计算机网络的分类 1、按分布范围 1.广域网 2.城域网 3.…...

初识Java 14-1 测试

目录 测试 单元测试 JUnit 测试覆盖率 前置条件 断言 Java提供的断言语法 Guava提供的更方便的断言 契约式设计中的断言 DbC 单元测试 Guava中的前置条件 本笔记参考自&#xff1a; 《On Java 中文版》 测试 ||| 如果没有经过测试&#xff0c;代码就不可能正常工作…...

react常用的hooks有哪些?

React常用的Hooks包括以下几种&#xff1a; 1.useState&#xff1a;用于在函数组件中创建和管理状态。它返回一个数组&#xff0c;第一个值是当前状态的值&#xff0c;第二个值是更新状态的函数。 使用时&#xff0c;首先通过解构赋值获取状态值和更新函数&#xff0c;并设置初…...

Nacos config 配置中心详解

目录 一、Nacos启动 1.预备环境准备 2.下载编译后压缩包方式 3. 启动服务器 二、 配置管理 多配置格式编辑器 编辑DIFF 示例代码 监听者查询 配置的版本及一键回滚 命名空间管理 登录管理 修改默认用户名/密码方法 关闭登录功能 会话时间 三、启动配置管理 添加依赖&#xff1a…...

C++ UDP通信

#pragma once #ifndef XUDP_H #define XUDP_H#ifdef WIN32 #include <windows.h> #define socklen_t int #else #include <arpa/inet.h> #define closesocket close //替换close函数 #include <unistd.h>#include<iostream> #endifclass XUdp { pub…...

自由程序员想接私活?那你还不得知道这几个接单平台!最后一个就是宝藏!!

相信喜欢搞钱的程序员都知道&#xff0c;平常在平台上接点私活&#xff0c;利用闲暇时间接单是搞钱的常用套路&#xff0c;可是你确定你选对平台了吗&#xff1f;不管你是刚准备接单的小白&#xff0c;还是已经干了一段时间的老油条&#xff0c;都建议你看完本期文章&#xff0…...

二叉树与递归的相爱相杀

数据结构之二叉树 一、基于二叉树的基础操作1.二叉树的构建2.二叉树的遍历①前序遍历&#xff08;深度遍历&#xff09;②中序遍历③后序遍历④层序遍历判断一棵二叉树是否是完全二叉树&#xff08;基于层序遍历的思想&#xff09; 3.二叉树的数量问题①求二叉树结点个数②求二…...

Docker 安装 reids

docker run -itd --name myredis -p 6379:6379 redis --requirepass “123456” --restartalways --appendonly yes...

opensl学习——base16编码解码、base64编码解码、ASCII码表、扩展ASCII码

文章目录 ASCII表概述base家族简单说明 Hex(十六进制)编码、Base32编码、Base64编码、base256编码base16编码与解码base64编码概述转换过程不足 3 字节处理方法例子一,不足3字节&#xff0c;只有一个字节例子二,不足3字节&#xff0c;只有两个字节 base64示例代码1代码分析 acl…...