一、事务特性
ACID属性 | 含义 |
---|---|
原子性(Atomicity) | 比如下订单,减库存,扣积分是一个完整的业务逻辑操作,是不可分割的,这3个操作要么同时成功,要么同时失败,不可能存在2个成功,一个失败 |
一致性(consistency) | 在事务开始之前和事务结束以后,数据的完整性没有被破坏(A给B转账,A和B之前都是1000,A转200给B成功了,A变800,B变1200,转账前后他们的总量都是2000都是一致的,不可能出现转完账了A变800,B没加200) |
隔离性(Isolation) | (比如目前可能有100个人同时下单,一个人下单失败了,他的数据回滚不会影响到任何其他人)数据库允许多个并发事务同时对其数据进行读写和修改的能力,隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。事务隔离分为不同级别,包括读未提交(Read uncommitted)、读提交(read committed)、可重复读(repeatable read)和串行化(Serializable) |
持久性(durability) | 事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失(一旦我们上面的3个操作数据库都告诉你全成功了,就一定会成功,数据库即使返回前一刻,它告诉你成功了,刚告诉你成功,数据库咔的一下断电了,那你再次启动数据库,这个数据一定是成功在数据库里面的) |
1.1 并发事务带来的问题
问题 | 含义 |
---|---|
丢失更新(Lost Update) | 当两个或多个事务选择同一行,最初的事务修改的值,会被后面的事务修改的值覆盖 |
脏读(Dirty Reads) | 脏读就是事务2读到了事务1修改但是还未提交的数据 |
不可重复读(Non-Repeatable Reads) | 不可重复读是指在事务1内,读取了一个数据,事务1还没有结束时,事务2也访问了这个数据,修改了这个数据,并提交。紧接着,事务1又读这个数据。由于事务2的修改,那么事务1两次读到的的数据可能是不一样的,因此称为是不可重复读 |
幻读(Phantom Reads) | 幻读是指当事务不是独立执行时发生的一种现象,例如第一个事务对一个表中的数据进行了修改,比如这种修改涉及到表中的“全部数据行”。同时,第二个事务也修改这个表中的数据,这种修改是向表中插入“一行新数据”。那么,以后就会发生操作第一个事务的用户发现表中还存在没有修改的数据行盖 |
1.2 事务隔离级别
为了解决上述提到的事务并发问题,数据库提供了事务隔离机制来解决这些问题。数据库的事务隔离越严格,并发副作用越小,但付出的代价也就越大,因为事务隔离实质上就是使用事务在一定程度上“串行化”进行,这显然与”并发”是矛盾的
- 读未提交:别人的数据还没提交就被我们读出来了,脏读现象。别人没提交的数据我们拿在下面用,结果别人给回滚了,那么拿的这条数据就是假的
- 读已提交:可以读取到别人已经提交的数据(oracel和sql server默认隔离级别)
- 可重复读:在我们事务期间内,只要我们事务没结束,第一次在数据库里面读取到的1号记录比如是100,则我们在整个submitOrder事务期间无论读多少次数据库1号记录,它的值还是100,即使外面的人在操作这个数据的时候,把这个数据都删了或者把它都修改成200了,然而我们读到的都是100。即可以重复读取多次,读取到的值都是一样的
- 序列化:如果数据库的隔离级别调成序列化的,那1个事务每做一件事,都得等这个做完了才能做下一个,这样整个数据库就没有任何并发能力了
隔离级别 | 丢失更新 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|---|
读未提交Read uncommitted | x | √ | √ | √ |
读已提交Read committed | x | x | √ | √ |
可重复读Read read(默认) | x | x | x | √ |
序列化Serializable | x | x | x | x |
使用show variables like'tx_isolation'
查看当前数据库的默认事务隔离级别
innodb存储引擎默认的隔离级别是REPEATABLE READ,与标准SQL不同,innodb存储引擎在REPEATABLE READ事务隔离级别下,使用Next-Key Lock锁的算法,避免了幻读的产生,区别于其他数据库系统,因此innodb在默认的事务隔离级别下已经能够完全包住事务的SQL标准:serializable隔离级别
- 脏读:(读未提交事务隔离级别无法解决)
假设客户买东西,客户和商店初始资金都是1000
navicat开启两个窗口,并通过如下命令设置两个窗口(会话)的事务隔离级别
set session transaction isolation level read uncommitted;
查询是否设置成功
SELECT @@tx_isolation
设置两个会话,关闭自动提交事务
set autocommit =0
查询是否关闭成功
show variables like '%autocommit%'
开启一个事务(另有说法称在执行第一条查询时,才算完全开启了一个事务)
start transaction
然后执行如下操作
总结:脏读就是事务2读到了事务1修改但是还未提交的数据
如果拿到下面用就是错的,在图上的客户端B的第二步就读到了脏数据,如果他不commit,那他在这个事务中一直读到的是脏数据1100,即使客户端A已经回滚了,实际是1000,除非客户端B进行commit,它才鞥呢读取到最新的数据
- 不可重复读(读已提交事务隔离级别无法解决)
不可重复读是指在事务1内,读取了一个数据,事务1还没有结束时,事务2也访问了这个数据,修改了这个数据,并提交。紧接着,事务1又读这个数据。由于事务2的修改,那么事务1两次读到的的数据可能是不一样的,因此称为是不可重复读
不可重复读的意思就是,事务A两次读到的结果不一样,当我们将数据库事务级别改到可重复读级别以后再次测试
我们可以发现,尽管事务B提交了,在事务A没提交以前,事务A读取两次读取到的结果都是一样的,这就是可重复读
- 幻读
理论如上图所示,但是实际上不会出现在mysql的innodb引擎中,尽管mysql使用了REPEATABLE READ隔离级别,但是因为InnoDB存储引擎已经解决了幻读的问题(原因看最上面陈述),因此在第三步骤的执行结果,你会发现,结果仍然是两条记录
1.2.1 不可重复读和幻读的区别
很多人容易搞混不可重复读和幻读,确实这两者有些相似。但不可重复读重点在于update和delete,而幻读的重点在于insert
注意:不可重复读和幻读的区别是:前者是指读到了已经提交的事务的更改数据(修改或删除),后者是指读到了其他已经提交事务的新增数据。
对于这两种问题解决采用不同的办法,防止读到更改数据,只需对操作的数据添加行级锁,防止操作中的数据发生变化;而防止读到新增数据,往往需要添加表级锁,将整张表锁定,防止新增数据
1.2.2 MVCC(多版本并发控制)
Multi-Version Concurrency Control,是mysql的InnoDB存储引擎实现隔离级别的一种具体方式,用于实现RC(read commited)和RR(read repeatable)这两种隔离级别
至于读未提交RU(read uncommitted)隔离级别总是读取最新的数据行,因此无需使用MVCC,串行化颗粒级别需要对所有读取的行都加锁,单纯使用MVCC无法实现
MVCC的实现是通过保存数据在某个时间点快照来实现的,即不管实现时间多长,每个事务看到的数据都是一样的
分为乐观并发控制和悲观并发控制
MVCC是如何工作的:
InnoDB的MVCC是通过在每行记录后面保存两个隐藏的列来实现。这两个列一个保存了行的创建时间,一个保存行的过期时间(删除时间)。当然存储的并不是真实的时间而是系统版本号(system version number)。每开始一个新的事务,系统版本号都会自动新增。事务开始时刻的系统版本号会作为事务的版本号,用来查询到每行记录的版本号进行比较。
版本号
系统版本号:是一个递增的数字,每开始一个新的事务,系统版本号就会自动递增。
事务版本号:事务开始时的系统版本号。
隐藏的列
MVCC 在每行记录后面都保存着两个隐藏的列,用来存储两个版本号:
- 创建版本号:创建一行数据时,将当前系统版本号作为创建版本号赋值。
- 删除版本号:删除一行数据时,将当前系统版本号作为删除版本号赋值。如果该快照的删除版本号大于当前事务版本号表示该快照有效,否则表示该快照已经被删除了。
1.2.3 快照读与当前读
在 RR 级别,通过MVCC机制,虽然让数据变得可重复读,但我们读到的数据可能是历史数据,不是数据库最新的数据。
这种读取历史数据的方式,我们叫它 快照读(snapshot read),而读取数据库最新版本的数据的方式,我们叫它 当前读(current read)
快照读
当执行select查询操作的时候,InnoDB默认会执行 快照读 ,会记录下这次select查询到的结果,之后再select查询的适合就会返回这次快照的数据,即使其他事务提交了也不会影响当前selec到的数据,这就实现了RR。
快照的生成是在第一次执行select的适合,即假设A开启了事务,然后没有执行任何操作,这时候B insert 了一条数据然后 commit,这时候A执行 select,那么返回的数据中就会有B刚刚添加的那条数据。之后无论再有其他事务 commit 都没关系,因为快照已经生成了,后面的select都是根据快照来的。
使用MVCC读取的是快照中的数据,这样可以减少加锁带来的开销当前读
对于会对数据修改的 增删改 操作(insert,update,delete)都是采用当前读的模式。在执行这几个操作时会读取到最新的记录,即使是别的事务提交的数据也可以查询到。
比如要 update 一条记录,但是另一个事务中已经 delete 掉这条记录并且commit了,如果 update 就会产生冲突,所以update的时候一定是需要知道最新的数据的。
读取的是最新的数据,因此需要加锁。如以下第一个语句加 S 读锁,其他加 X 写锁
1.2.4 InnoDB支持三种行锁定方式
https://juejin.cn/post/6844903842505555981
- Record Locks:单个行记录上的锁
- Gap Locks:间隙锁,锁定一个范围,但不包括记录本身。GAP锁的目的,是为了防止同一事务的两次当前读,出现幻读的情况
- Next-Key Locks:1+2,锁定一个范围,并且锁定记录本身。对于行的查询,都是采用该方法,主要目的是解决幻读的问题
Record Locks
锁定一个记录上的索引,而不是记录本身。
如果表没有设置索引,InnoDB 会自动在主键上创建隐藏的聚簇索引,因此 Record Locks 依然可以使用
Gap Locks
锁定索引之间的间隙,但是不包含索引本身。例如当一个事务执行以下语句,其它事务就不能在 t.c 中插入 15。
SELECT c FROM t WHERE c BETWEEN 10 and 20 FOR UPDATE;
Next-Key Locks
Next-Key Locks 是 MySQL 的 InnoDB 存储引擎的一种锁实现。
MVCC 不能解决幻读的问题,Next-Key Locks 就是为了解决这个问题而存在的。在可重复读(REPEATABLE READ)隔离级别下,使用 MVCC + Next-Key Locks 可以解决幻读问题。
它是 Record Locks 和 Gap Locks 的结合,不仅锁定一个记录上的索引,也锁定索引之间的间隙
新建一张表:
CREATE TABLE `test` (
`id` int(11) primary key auto_increment,
`xid` int, KEY `xid` (`xid`) )
ENGINE=InnoDB DEFAULT CHARSET=utf8;
执行插入:
insert into test(xid) values (10), (11), (13), (20);
注意,这里xid上是有索引的,因为该算法总是会去锁住索引记录
该索引可能的next-key锁锁住的范围如下,其中圆括号表示排除区间端点,方括号表示包含端点:
(-∞, 10]
(10, 11]
(11, 13]
(13, 20]
(20, ∞)
1.2.5 mysql 如何解决幻读
- 使用串行化隔离级别(不推荐)
- 使用 MVCC + Next-Key Locks 解决幻读问题(现在mysql在RR级别的默认解决幻读问题的处理方式)
隐式字段
每行记录除了我们自定义的字段外,还有数据库隐式定义的DB_TRX_ID
,DB_ROLL_PTR
,DB_ROW_ID
等字段
- DB_ROW_ID(隐式主键)
6byte,隐含的自增ID(隐藏主键),如果数据表没有主键,选择唯一键,没有唯一键,InnoDB会自动会生成6字节的DB_ROW_ID - DB_TRX_ID(事务id)
6byte,记录创建这条记录/最后一次修改该记录的事务 - DB_ROLL_PTR(回滚指针)
7byte,回滚指针,指向这条记录的上一个版本
undolog
undo log主要分为两种:
- insert undo log
代表事务在insert新记录时产生的undo log, 只在事务回滚时需要,并且在事务提交后可以被立即丢弃 - update undo log
事务在进行update或delete时产生的undo log; 不仅在事务回滚时需要,在快照读时也需要;所以不能随便删除,只有在快速读或事务回滚不涉及该日志时,对应的日志才会被purge线程统一清除
Read View
读视图:当快照读的时候会生成一个事务id的列表,来保存不同的信息,通过这些信息来做可见性判断,读视图有如下3个全局属性
- trx_list(名字我随便取的)
一个数值列表,用来维护Read View生成时刻系统正活跃的事务ID - up_limit_id
记录trx_list列表中事务ID最小的ID - low_limit_id
ReadView生成时刻系统尚未分配的下一个事务ID,也就是目前已出现过的事务ID的最大值+1
MVCC就是为了实现读-写冲突不加锁,而这个读指的就是快照读, 而非当前读,当前读实际上是一种加锁的操作,是悲观锁的实现
二、myisam引擎和innodb区别
myisam是mysql5.5版本之前默认数据库引擎,innodb是mysql5.5版本开始默认的数据库引擎
- 是否支持行级锁:myisam只支持表级锁table-level locking,而innodb支持行级锁row-level locking和表级锁,默认为行级锁
- 是否支持事务:myisam不支持事务,每次查询具有原子性,执行比innodb快,但是最大的缺陷就是奔溃后无法安全恢复。而InnoDB提供事务,具有事务提交,回滚和奔溃修复能力
- 外键支持:myisam不支持外键,而innodb支持
-
是否支持mvcc:仅innodb支持,应对高并发事务,mvcc比单纯加锁高效,只在
READ COMMITED
和REPEATABLE READ
两个隔离级别下工作;mvcc可以使用乐观锁(optimistic ɒptɪˈmɪstɪk)和悲观锁(pessimistic pesɪˈmɪstɪk)来显示,各个数据库中mvcc实现不统一
innodb的AUTOCOMMIT默认是打开的,即每条SQL语句会默认被封装成一个事务,自动提交,这样会影响速度,所以最好是把多条SQL语句显示放在begin和commit之间,组成一个事务去提交。
MVCC和next key Lock相关:
https://blog.csdn.net/weixin_33888907/article/details/91398775
三、锁
3.1 乐观锁和悲观锁
乐观锁对应于生活中乐观的人,总是想着事情往好的方向发展
悲观锁对应于生活中悲观的人,总是想着事情往坏的方向发展
- 悲观锁
总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据的时候就会阻塞,直到前一个人操作数据完毕释放自己持有的锁,这个人才能拿到锁,下面要讲的行锁,表锁,S,X锁等都是在操作之前先上锁,都属于悲观锁,java中synchronized和reentrantLock都是悲观锁思想的实现 - 乐观锁
总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新数据的时候回判断一下在此期间其他人有没有更细过这个数据,可以使用version版本号机制和CAS算法实现,乐观锁适用于多读的场景,可以提高吞吐量
Q1:选择乐观锁还是悲观锁?
- 当写比较少,读比较都的时候就比较合适使用乐观锁,因为冲突真的很少发生,省去了锁的开销,加大了系统的整个吞吐量
- 但是当写比较多的时候,一般会经常发生冲突,用乐观锁就会导致上层应用不断try,反而降低了性能,因此多写的场景用悲观锁比较合适
Q2:怎样通过版本号机制实现乐观锁?
一般在数据表中加一个数据版本号version字段,表示数据被修改的次数,当数据被修改的时候,version+1。当线程A要读取数据值时,读取数据的同时也会读取version值,在线程A提交更新的时候,若刚才读取到的version值=数据库中当前的version值的时候才更新
3.2 表级锁、行级锁、读锁、写锁
从对数据操作的粒度分:
表锁:操作时,锁定整个表
行锁:操作时,锁定当前行
从对数据操作的类型分:
读锁(共享锁):针对同一份数据,多个读操作可以同时进行而不会相互影响
写锁(排它锁):当前操作没有完成之前,它会阻断其他写锁和读锁
innodb引擎支持行级锁和表级别锁,默认为行锁,而myisam只支持表级别锁
3.2.1 MyISAM测试表锁
如何加表锁?
MyISAM在执行查询语句(select)前,会自动给涉及的所有表加读锁,在执行更新(update,delete,insert等)前,会自动给涉及的所有表加写锁,该过程不需要用户干预,因此,用户一般不需要直接用lock table命令给MyISAM表显式加锁
显示加锁语法:
# 加读锁
lock table table_name read;
# 加写锁
lock table table_name write;
下面研究一下加了读锁,写锁以后有什么现象
读锁示例
准备环境,创建如下两张表,表的存储引擎采用myisam引擎
create table `tb_book`(
`id` INT(11) auto_increment,
`name` VARCHAR(50) DEFAULT NULL,
`publish_time` DATE DEFAULT NULL,
`status` CHAR(1) DEFAULT NULL,
PRIMARY KEY(`id`)
) ENGINE=myisam DEFAULT CHARSET=utf8;
insert into tb_book(id,name,publish_time,status)
values(NULL,'java编程思想','2028-01-01','1');
insert into tb_book(id,name,publish_time,status)
values(NULL,'代码整洁之道','2028-01-05','0');
create table `tb_user`(
`id` INT(11) auto_increment,
`name` VARCHAR(50) DEFAULT NULL,
PRIMARY KEY(`id`)
) ENGINE=myisam DEFAULT CHARSET=utf8;
insert into tb_user(id,name) values(NULL,'周杰伦');
insert into tb_user(id,name) values(NULL,'吴亦凡');
- 我们创建2个客户端,然后客户端1使用显式加锁语句为tb_book表加读锁,发现客户端1和客户端2均能成功读取数据,即读锁允许多个客户端一起读不受影响
- 当我们在客户端1查询tb_user表,发现出现如下错误:
原因是当前客户端已经持有了tb_book的锁,就相当于与在12306买票,抢到了一张火车票,然后你没有付款,然后你再去抢其他火车票是无法抢的。此处将tb_book表锁定了,这里还没有释放锁,是无法操作其他表的
而当我们在客户端2执行相同的查询tb_user表的语句的时候,发现可以正常查询,因为右侧的客户端当前没有持有任何锁
- 我们在客户端1,2均使用如下命令修改tb_book的一行记录
我们发现客户端1是无法修改成功的,提示当前tb_book表已经被读锁锁定了,不能执行更新
而客户端2直接进入了阻塞状态
然后我们让客户端1释放持有的表锁,客户端2的阻塞立马返回
写锁示例
客户端1显式添加写锁,更新一行数据,客户端2无法执行select操作,无法获取到select自带的读锁,只有当客户端1执行unlock tables释放锁,客户端2才能返回
结论
此外,MyISAM的读写锁调度是写优先,这也是MyISAM不适合做写为主的表的存储引擎的原因。因为写锁后,其他线程不能做任何事情,大量的更新操作会使查询很难得到锁,从而造成永远阻塞。因此只适合读为主
查看锁争用情况
show open tables
当前默认都为0,如果我们使用lock table tb_book read;
指令为tb_book显式的添加读锁以后,再次查看发现in_user变成了1,可以通过这个看到哪张表正在使用,正在加锁
in_use:表当前被查询使用的次数,如果次数为0,则表示表示打开的,当前没有被使用
Name_locked:表名称是否被锁定,名称锁定用于取消表或对表进行重命名等操作
show status like 'Table_locks%'
Table_locks_immediate:指的是能够立即获得表级锁的次数,每立即获得锁,值+1
Table_locks_waited:指的是不能立即获得表锁而需要等待的次数,每等待一次,该值+1,此值高说明存在较为严重的表锁争用情况
3.2.2 InnoDB测试行锁
InnoDB与MyISAM的最大不同有两点:一是支持事务,二是使用了行锁
行锁和事务是有关联的,InnoDB实现了两种类型的行锁
- S锁:Shared Lock --- 共享锁,又称为读锁,共享锁就是多个事务对于同一数据可以共享一把锁,都能访问到数据,但是只能读不能修改
- X锁:Exclusive Lock --- 排它锁,又称为写锁,排它锁是不能与其他锁并存,一个事务获取了一个数据行的排它锁,其他事务就不能再获取该行的其他锁,包括共享锁和排它锁,但是获取排它锁的事务是可以对数据进行读取和修改
对于update,delete,insert语句,innoDB会自动给涉及数据行加排它锁,对于普通select语句,innoDB不会加任何锁(共享锁/排它锁)
如果我们想手动的给某个sql语句加上共享锁/排它锁,可以通过如下语句显式的给记录行加共享锁或排它锁
# 加行共享锁
select * from table_name where ... lock in share mode
# 加行排它锁
select * from table_name where ... for update
案例环境准备
create table test_innodb_lock(
id int(11),
name varchar(16),
sex varchar(1)
)engine=innodb default charset=utf8;
insert into test_innodb_lock values
(1,'100','1'),(3,'3','1'),(4,'400','0'),(5,'500','1'),
(6,'600','0'),(7,'700','0'),(8,'800','1'),(9,'900','1'),(1,'200','0');
create index idx_test_innodb_lock_id on test_innodb_lock(id);
create index idx_test_innodb_lock_name on test_innodb_lock(name);
然后开两个客户端窗口,session-1和session-2
session-1 | session-2 |
---|---|
我们发现倒数第3步session1是300,session2是30,为什么session1不是30呢?因为数据库的默认隔离级别是可重复读,而session2的事务没有提交。因此不会出现脏读问题,即session1不会读取到session2已修改但未提交的数据
我们再次尝试将session2的事务提交,session1的结果还是300,仍然没有变化,如上方表格倒数第2步。还是因为数据库默认隔离级别是可重复读,不会出现不可重复读问题,即事务1还没有结束,因为我们关闭了自动提交,因此我们再书写了一遍select语句后,仍然需要提交session1的事务,这时候再次查询就正常了,如上方表格最后一行
以上操作同一行,如果session1操作第3行进行update,session2操作第6行update,则互不影响,session1拿第3行的行锁,session2拿第6行的行锁
无索引行锁升级为表锁
如果不通过索引条件检索数据,那么innoDB将对表中的所有记录加锁,实际效果跟表锁一样,会造成innoDB存储引擎能力的下降
查看当前表的索引show index from test_innodb_lock
session-1 | session-2 |
---|---|
为什么session1更新第9条记录会导致session2无法更新第3条记录呢?而需要session1在commit之后,session2才能阻塞解除?innoDB不是行级别锁,不同事务操作不同行不是应该不影响吗?
原因:行锁已经失效升级为表锁,session1中的update语句后面的where后面的name字段索引失效了,虽然name字段有索引,但是900没有加单引号,即不是'900',因此索引失效了
因此,注意:如果我们的更新语句where后面的字段没有索引,将会导致行锁升级为表锁,因此我们应该尽量避免这种情况的出现
间隙锁危害
当我们用范围条件,而不是使用相等条件检索数据,并请求共享或排它锁的时候,innoDB会给符合条件的已有数据进行加锁;对于键值在条件范围内但不存在的记录,叫做“间隙(GAP)”,InnoDB也会对这个“间隙”加锁,这种锁机制就是所谓的间隙锁(Next-Key锁)
session-1 | session-2 |
---|---|
开发中,我们应该避免间隙锁。需要做到更新的时候,对行加锁的时候,尽量缩小范围
InnoDB行锁争用情况
show status like 'innodb_row_lock%'
Innodb_row_lock_current_waits:当前正在等待行锁的线程数量
Innodb_row_lock_time:锁定的总时长
Innodb_row_lock_time_avg(重点):锁定的平均时长
Innodb_row_lock_time_max:锁定的最大时长
Innodb_row_lock_waits(重点):系统启动到现在总共等待行锁的次数
当等待的次数很高,而且每次等待的时长也不小的时候,我们就需要分析系统中为什么会有这么多的等待,然后根据分析结果指定优化计划
总结
innoDB存储引擎由于实现了行锁,虽然在锁定机制的实现方面带来了性能损耗可能比表锁更高一些,但是在整体并发处理能力方面要远远优于MyISAM的表锁的。当系统并发量较高的时候,innodb的整体性能相比MyISAM有明显优势
但是,innoDB的行锁也有脆弱的一面,当我们使用不当的时候(比如无索引行锁升级为表锁),可能会让innodb的整体性能表现不仅不能比MyISAM高,甚至可能会更差
优化建议:
- 尽可能让所有数据检索都能通过索引完成,避免无索引行锁升级为表锁
- 合理设计索引,尽量减少锁的范围
- 尽可能减少索引条件及索引范围,避免间隙锁(如id<10,id=2,4的记录不存在,但是加锁的时候也会给id=2和id=4的记录加锁,这样id为2和4的记录就插入不进去)
- 尽量控制事务大小(让一次事务不操作过多的表也不操作过多的记录),减少锁定资源量和时间长度
- 尽可能使用低级别事务隔离(但是需要业务层面满足需求)
行级别的S锁:允许事务读一行数据
表级别的S锁:允许事务读整个表的数据
行级别的X锁:允许事务删除或更新一行数据
表级别的X锁:允许事务删除或更新整个表的数据
下面的图是行级别的锁之间的兼容性
如果一个事务对某一行数据加了S锁,另一个事务还可以对相应的行加S锁,但是不能对相应的行加X锁。
如果一个事务对某一行数据加了X锁,另一个事务既不能对相应的行加S锁也不能加X锁
3.3 意向锁的由来
意向锁出来的目的是为了让表锁和行锁共存 intention Lock
For example,SELECT ... LOCK IN SHARE MODE
sets anIS
lock, andSELECT ... FOR UPDATE
sets anIX
lock.
原因:事务A申请到表的行级别锁X,事务B想要申请表的表级别锁X,如果事务B申请成功,那么他应该能表中的任意一条记录,这与事务A申请到的行级别锁是冲突的,为了避免冲突,事务B在申请表锁的时候,数据库需要做如下判断
- 判断表是否被其他事务用表锁锁住
- 判断表中每一行是否已经被行锁锁住
很明显步骤2要判断的话,要枚举表中的每一行,这样的方式效率太低,因此引入了IS(意向共享锁),IX(意向排他锁)意向锁。
- 在一个事务对一张表的某行添加S锁之前,它必须对该表获取一个IS锁或者优先级更高的锁
- 在一个事务对一张表的某行添加X锁之前,它必须对该表获取一个IX锁
申请意向锁的动作是数据库自动完成的,就是说,事务A申请某行的行锁的时候,数据库会自动先开始申请表的意向锁,不需要我们程序员使用代码来申请。
上面的步骤就变成了
- 同上面
- 发现表上有意向共享锁,说明表中有些行被共享行锁锁住了,因此,事务B申请表的写锁会被阻塞
下面是表级别的X,S锁和IS,IX锁的兼容关系图
https://dev.mysql.com/doc/refman/5.7/en/innodb-locking.html#innodb-intention-locks
3.3.1 问题总结
为何IS,IX锁和IS,IX锁之间都是兼容的呢?
事务A加了表的IX锁,或者IS锁,只代表事务A已锁定一行或者将要锁定一行。事务B当然也可以锁定其他的行,所以事务B肯定也是可以获得表的IS锁或者IX锁的IX,IS是否会和行级别的X,S锁发生冲突
IX,IS是表级锁,不会和行级的X,S锁发生冲突。只会和表级的X,S发生冲突。行级别的X和S按照普通的共享、排他规则即可。所以如果两个事务都要获得行X锁,只要写操作不是同一行,就不会发生冲突