1.概述
所谓的并发控制,就是规避多个会话并发访问数据库带来的诸如脏数据之类的数据一致性问题,MySQL中提供了一系列的机制让我们可以去进行并发控制。
本质上来说MySQL就是用的两种锁来进行并发控制,一种是表锁,锁住整张表;一种是行锁,锁住某个数据行。
平时我们使用的时候,很少会直接去操作锁,因为MySQL已经帮我们封装的很好了,直接用innodb引擎+事务就能很好的进行并发控制,事务底层其实依赖的就是行锁。
本文会先聊事务、再聊表锁、行锁,但其实总的来说MySQL进行并发控制,就是行锁和表锁,事务的底层用的就是行锁,只是事务太重要了所以单独拎出来作为一个独立的章节聊。
2.事务
2.1.什么是事务
注意:只有innodb引擎是支持事务的,所以本文与事务相关的讨论,默认都是在innodb引擎下。
在实际使用中,会存在这样一类场景,我们希望几条SQL要么同时执行成功,要么同时失败,不能有的成功,有的失败。
比如网购下单,生成订单、扣减库存两条SQL,必须保证要么全部成功,要么全部失败,不能说生成订单成功,但是扣减库存失败了,或者说扣减库存成功但是生成订单失败了,以上任何一种情况都是会产生脏数据的。如果两者中有一者失败,另外一个也需要跟着执行不成功,从而保证数据的正确性。
事务就是为了满足将多条捆绑在一起,同成功,同失败而出现的。人们在实现事务过程中,发现事务要实现上面我们说的效果,那么就必须实现四点:
原子性(Atomicity)
一致性(Consistent)
隔离性(Isolation)
持久性(Durable)
也就是大名鼎鼎的ACID,很多地方称其为事务的四大特性。
1.原子性:
事务是一个原子操作单元,其对数据的修改,要么全部执行,要么全部不执行。
2.一致性:
数据库中的数据总是从一个状态到另一个状态,不能存在中间状态。继续以网购下单为例,一开始的数据库中的数据为A状态,执行完事务,扣减库存、新增订单后的数据状态为B状态,数据库只能由A到B,不能出现诸如扣了库存,没生成订单,或者生成了订单,没扣库存这样的中间状态。
3.隔离性:
一个事务在提交之前,其所做的修改对其它事务来说是不可见的。不保证隔离性会产生脏数据,这个很好理解,举个例子:
A的银行账户有400,有两个事务,彼此之间数据可见,也就是一个事务修改数据,不管提没提交其他事务都看得见。
事务1,A向B转200:
A的账户扣减200
B的账户新增200
事务2,A向C转200:
A的账户扣减200
C的账户新增200
如果在事务1中的1、2步的时间间隙内事务2间插执行完毕,那么在事务1第2步执行前A的账户中已经被扣减了两次200,余额为0,C的账户中多了200。
这时候如果在事务1的第2步中出错了、回滚,那么A的账户又会回到事务1第一步执行之前的状态,也就是A的账户又恢复成了400。最后C的账户平白无故多了200。
4.持久性:
事务完成后,其对数据的修改是永久的,即使系统断电、重启,也不会变。
2.2.事务的隔离级别
2.2.1.三种数据一致性问题
在没有隔离性的情况下,事物之间会出现3种数据一致性问题:
脏读:事务A读取了事务B更新的数据,然后B回滚操作,那么A读取到的数据是脏数据
不可重复读:事务 A 多次读取同一数据,事务 B 在事务A多次读取的过程中,对数据作了更新并提交,导致事务A多次读取同一数据时,结果 不一致。
幻读:系统管理员A将数据库中所有学生的成绩从具体分数改为ABCDE等级,但是系统管理员B就在这个时候插入了一条具体分数的记录,当系统管理员A改结束后发现还有一条记录没有改过来,就好像发生了幻觉一样,这就叫幻读。
2.2.2.四种隔离级别
隔离级别可理解为,隔离性的严格度,MySQL并不是固定死了各个事务间就是不可读的,而是规定了各种强度的隔离级别。
观察上面3种数据一致性问题就会发现,解决它们需要的隔离性是递增的,MySQL一共给出4种隔离级别,隔离性也是递增的,对应解决以上3个问题,有3种,加上1种3种问题都能覆盖解决的:
Read Uncommitted(读未提交):最低的隔离级别,允许一个事务读取另一个事务尚未提交的数据变更。可能导致脏读(Dirty Read)问题。
Read Committed(读已提交):确保一个事务只能读取另一个事务已经提交的数据变更。防止脏读问题,但可能导致不可重复读(Non-repeatable Read)问题。
Repeatable Read(可重复读):确保在同一个事务中多次读取同一数据时,能够得到一致的结果。防止脏读和不可重复读问题,但可能导致幻读(Phantom Read)问题。
Serializable(串行化):最高的隔离级别,强制事务串行执行,确保不会发生脏读、不可重复读和幻读问题。但是并发性能较差,通常不建议在高并发环境中使用。
2.3.如何设置隔离级别
可以在连接字符串中设置隔离级别:
jdbc:mysql://localhost/mydatabase?useSSL=false&characterEncoding=utf8&transactionIsolation=隔离级别
可以通过SQL在会话中设置隔离级别:
ACTION ISOLATION LEVEL 隔离级别; SET TRAN
3.锁
3.1.锁与事务的关系
锁是计算机协调多个进程或线程并发访问某一资源的机制,用来解决并发访问带来的数据一致性问题。在数据库中,除传统的计算资源(如CPU、IO、RAM)的争用以外,数据更是会被并发访问的资源。所以MySQL数据库也用锁机制来保证数据并发访问的一致性。
不同的隔离级别底层就是用不同的锁来实现的。
3.2.分类
MySQL的锁可以从两种维度来分,
一个维度是按照锁的是一行,还是锁的是整张表,分为:
- 行锁
- 表锁
另一个维度是按照锁的操作是读操作,还是写操作,分为:
- 读锁
- 写锁
3.3.表锁
3.3.1.概述
MySQL中innodb引擎和myisam引擎均支持用lock tables指令来锁表。
3.3.2.读锁
读锁,一种共享锁,针对被锁表,所有会话都可以进行读操作,所有会话都无法进行写操作。加锁方和其他客户端的区别是,加锁方直接不允许进行写操作,而其他会话的写操作允许进行,只是会被阻塞挂起。锁解开后,所有挂起的操作线程会去重新争抢资源。
加锁指令:
lock tables 表名 read;
释放锁指令:
unlock tanles;
加锁方不允许进行写操作:
其它客户端的写操作在加锁方释放锁之前都被挂起:
3.3.3.写锁
写锁,排它锁,针对被锁表,加锁方可以读写,其他会话的写操作会直接失败,读操作会被阻塞挂起,解锁以后,被挂起的线程会重新去争抢资源。
加锁指令:
lock tables 表名 write;
其它会话的读操作、写操作在加锁方释放锁之前都被阻塞挂起:
3.3.4.保护机制
读锁、写锁中,加锁方都只能读当前被自己锁定的表,这是MySQL的一个保护机制,为的就是强制要求加锁方给出一个说法,到底准备锁多久,不给说法不让走。
3.4.行锁
3.4.1.概述
innodb和myIsam最大的不同有两点,一是支持事务,二是支持行级锁。
3.4.2.什么是MVCC
行锁没有显式的声明办法,而是藏在默认实现中,MVCC 是 MySQL InnoDB 存储引擎的默认并发控制机制,其采用的就是表锁。
并发控制有几种处理方法,
第一种: 基于锁的并发控制,程序员B开始修改数据时,给这些数据加上锁,程序员A这时再读,就发现读取不了,处于等待情况,只能等B操作完才能读数据,这保证A不会读到一个不一致的数据,但是这个会影响程序的运行效率。
第二种:MVCC,每个用户连接数据库时,看到的都是某一特定时刻的数据库快照,在B的事务没有提交之前,A始终读到的是某一特定时刻的数据库快照,不会读到B事务中的数据修改情况,直到B事务提交,才会读取B的修改内容。
MVCC其实就是实现事务的关键,后续会有文章专门深入聊事务的实现,此处暂不展开。
3.4.3.mvcc的使用
首先有一个需要纠正的是,很多地方都说mvcc是通过手动提交来触发的,这是个误导,不管手动提交还是自动提交MVCC机制都是生效的,只是手动提交用来观察mvcc过程更加直观。
此处为了直观,我们也以手动提交为例,首先通过set autocommit=0可以关闭自动提交。关闭后每次执行sql以后,通过commit命令来手动提交,才会对数据库产生影响,否则只会对当前操作方的数据快照有影响。。innodb引擎中,其他客户端想查看到最新的数据情况也必须通过commit指令来做一次同步(因为innodb默认隔离级别为可重复读)。
当一个会话修改某行数据,未commit前,其他会话对该行数据的修改会阻塞挂起,直到先改那个会话commit为止。
3.4.4.间隙锁
使用范围条件匹配时,innodb会给符合条件的已有数据记录的索引加“范围锁”(范围锁是特殊的行锁),对于键值在条件范围内但并不存在的记录,叫做间隙(GAP),innodb也会对这个间隙加锁,这种机制叫做“间隙锁”。
3.4.5.行锁变表锁
任何需要全表扫描的情况时,行锁都会升级为表锁。
因为MySQL不知道到底该锁哪行,所以会将整个表都锁起来,然后再进行全表扫描。
全表扫描的情况无非两种:
- 没建索引。
- 索引失效。