文章目录
一、事务是什么
先来看第一个问题:什么是事务(Transaction
)?
事务就是执行一组 SQL 语句。这些 SQL 语句就是一条绳上的蚂蚱,要么一起成功(Commit
),要么一起失败(RollBack
)。
- 比如用户 A 向用户 B 转账 10 元;
- 如果 A 先转出了 10 元,但是在 B 收到之前服务器挂了,就会造成 A 的钱没了,B 的钱也没到账;
- 如果把这两条语句封装成事务的话,就不会出现上述情况,而是整个过程执行失败,退回 A 的钱结束事务。
事务的特性:
Atomicity
(原子性):像原子一样不可分割;Consistency
(一致性):数据库永远保持一致;Isolation
(隔离性):事务之间是相互隔离的;Durability
(持久性):事务一旦提交,它的修改就是永久的。
这就是 ACID
。
下面来看一个栗子:
-- 开启一个事务
start transaction;
insert into orders(customer_id, order_date, status)
values (1, '2020-05-10', 1);
insert into order_items
values (last_insert_id(), 1, 1, 1);
-- 提交就代表结束事务了
commit;
-- 回滚事务
rollback;
其中被transaction
和commit
包裹着的语句就是事务,也可以使用 rollback
包裹,这样的话之前的修改就全部失效。
在 MYSQL
中,一条 SQL 语句默认就是一个事务:
# 每一个 SQL 语句都被设置成自动提交
show variables like 'autocommit';
二、为什么使用隔离级别
然后是第二个问题:为什么会有事务的隔离级别?
既然已经有了事务,转账啥的问题也都挺好的解决了,那隔离级别优势啥玩意?
什么是并发
在了解这个之前我们先来介绍一下什么是并发:
- 并发就是多个用户访问数据库,同时对相同的数据进行修改带来的问题。
- 比如用户 A 创建了事务,还没来得及提交,用户 B 抢先提交了,关键是他们修改的是同一个事务,那么到底应该听谁的呢?
1、默认并发处理
那么 MySQL 默认是怎么处理并发的呢?
- MySQL 默认会让其中一个事务等待另一个事务先执行完成再执行该事务。
- 他会先将数据锁定,就是给行或者数据加锁,确保在第一个事务执行完成之前,数据不会被其他事务修改。
- 所以在默认的情况下是不用担心事务的并发问题的。
2、常见并发问题
但是默认并不是万能的,总有一些问题是它解决不来的,接下来我们就来看一下这些问题。
Lost Updates
第一个问题是丢失更新
。
什么时候会出现?
两个事物试图更新相同的数据而我们不使用锁时,就会出现这种情况。
造成的结果:后面提交的数据会覆盖掉前面的提交。
但是这种情况 MySQL 会默认处理的,即放在队列中依次执行,所以一般我们不用考虑。
Dirty Reads
第二个问题就是脏读
。
什么时候会出现?
A 事务读取还没有提交时:另一个事务 B 却将 A 的还没有提交的数据当成了真实存在的数据,如果 A 最后提交了还好,如果 A 回滚了,那么 B 中的数据就是假的,也就是脏的,所以叫脏读。
造成的结果:读取了还未提交的数据,数据是脏的。
Non-repeating Reads
第三个问题是不可重复读
。
什么时候会出现?
事务 A 执行子查询,在外层查询中先获取指定的值,这个时候事务 B 过来修改了 A 刚才读的数据,但是之后 A 的子查询再次读了这个数据而这个数据两次读到的值不一样了,这就是不可重复读。
造成的结果:两次读取的内容可能会不同。
Phantom Reads
第四个问题是幻读
。
什么时候会出现?
在事务 A 中使用 where
条件查询,此时事务 B 修改了原来符合条件的数据,使得它现在不符合条件了,或者说增加了几个符合条件的数据,但是事务 A 始终读取的都是原来的数据。
造成的结果:丢失符合条件的某些行,就很玄幻,所以是幻读。
三、怎么做才能解决并发问题
那怎么解决这些并发问题呢? 答案是使用隔离级别对事务进行隔离。
该图左边是事务的隔离等级,右边是这个隔离等级可以解决的问题。标准的 SQL 定义了四个隔离级别,他们分别是:
Read Uncommitted
,不提交读:他什么问题也解决不了;Read Committed
,提交读:可以解决脏读,隔离级别是一次读取的范围,所以无法解决一次事务两次读取中间可能出现的问题;Repeatable Read
,可重复读:可以解决前三个问题,可以解决大部分的问题 ;Serializable
,序列化:可以解决所有问题,他会等待其他事务执行完毕之后再执行。
有一些注意点:
- MySQL 默认是可重复读的隔离等级,之前我们也提到了这个默认情况。
- 隔离界别越高,性能损耗越大,所以要权衡利弊选择隔离级别。
好,我们现在来回顾总结一下:
# 多个用户同时修改同一个数据称为并发, MYSQL 会自动锁定 update 的内容
# 事务带来的常见的问题
/* - 1.丢失更新: 两个事物同时修改数据,后面提交的数据会覆盖掉前面的提交 - 2.脏读(无效数据读取): 读取到未提交的数据 A事务读取还没有提交时-另一个事务B却将A的还没有提交的数据当成了真实存在的,如果A最后提交了还好,如果A回滚了,那么B中的数据就是假的; 即读取了还未提交的数据,数据是脏的; -- 为了解决这个问题,我们可以提供一定的隔离度,即事务修改的数据不会立即被其他事物看到,除非他已经提交了 -- 标准的 SQL 定义了四个隔离级别 - 针对脏读的就是 提交读 (Read Committed) 只能读取已经提交的数据 - 3.不可重复读取: 两次读取的内容可能会不同 事务A执行子查询,在外层查询中先获取指定的值,这个时候事务B过来修改了A刚才读的数据,但是之后A的子查询再次读了这个数据 而这个数据两次读到的值不一样了.这就是不可重复读 -- 这个时候应该增加隔离级别: 可重复读,达到这个目的 -> 以最开始的那个数据为准,不受事务B修改的影响,隔离级别是事务的级别. - 4.幻读: 丢失符合条件的某些行 在事务A中使用 where 查询,此时事务 B 修改了原来符合条件的数据,使得它现在不符合条件了. 需要的隔离等级: 序列化 Serializable 等级,使该事务可以知道其他事物正在修改数据. 这样他就会等待其他事务修改完之后才会执行事务,缺点就是如果并发比较多,执行会很慢 等级越高,性能消耗越大; MySQL默认是可重复读的隔离等级; */
大部分情况下都保持默认,只在特殊情况下修改隔离等级。
如何设置隔离等级?
-- 查看隔离等级
show variables like 'transaction_isolation';
-- 设置隔离等级
set transaction isolation level SERIALIZABLE ;
-- 在当前会话中设置
set session transaction isolation level serializable ;
四、MySQL 中的锁
数据库遵循的是两段锁协议,将事务分为两个阶段:
- 加锁阶段;
- 加锁阶段只允许事务加锁,在对任何事务进行
读操作
之前一定要先申请并获得S锁
(共享锁) ,进行写操作
之前要申请并获得X锁
(排它锁),如果获得失败,即加锁不成功则必须等待,直到加锁成功才继续执行。
- 加锁阶段只允许事务加锁,在对任何事务进行
- 解锁阶段;
- 当事务释放了一个封锁之后,事务进入解锁阶段,该阶段只能进行解锁操作而不能进行加锁操作。
举一个栗子:
MySQL 中的锁分为行锁
和表锁
,表锁通常是将整个的一张表锁住,会降低并发处理能力,所以一般只在 DDL
(数据库定义语言) 中使用。这里主要讨论行锁:
1. Read Committed
在 RC
级别中,数据的查询是不需要加锁的,但是数据的增删改是需要加锁的。
如果事务一直得不到锁则会一直等待,直到 wait
超时。
如果条件语句没有索引,MySQL 会将该表中的所有数据行加行锁。
但是在实际的应用中,会在 MySQL Server 中进行过滤,调用unlock_row
方法,从而将无关的数据项解锁(但是这违背了两段锁协议)。
对一个数据量很大的表做批量修改的时候,如果无法使用相应的索引,MySQL Server 过滤数据的的时候特别慢,就会出现虽然没有修改某些行的数据,但是它们还是被锁住了的现象。
2. Repeatable Read
RR
解决了在一次事务中,两次读取的内容不一致。
那么他是怎么做得到呢?
2.1 不可重复读和幻读的区别
这里穿插出一个知识点,也是大家容易记混的地方。
- 不可重复读重点在于
update
和delete
; - 而幻读的重点在于
insert
。
幻读与不可重复读类似,幻读是查询到了另一个事务已提交的新插入数据,而不可重复读是查询到了另一个事务已提交的更新数据。
简单来说,不可重复读是由于数据修改引起的,幻读是由数据插入或者删除引起的。
在可重复读中,该
sql
第一次读取到数据后,就将这些数据加锁,其它事务无法修改这些数据,就可以实现可重复读了。
但这种方法却无法锁住 insert
的数据,所以当事务 A 先前读取了数据,或者修改了全部数据,事务 B 还是可以 insert
数据提交,这时事务 A 就会发现莫名其妙多了一条之前没有的数据,这就是幻读,不能通过行锁来避免。
需要 Serializable
隔离级别 ,读用读锁,写用写锁,读锁和写锁互斥,这么做可以有效的避免幻读、不可重复读、脏读等问题,但会极大的降低数据库的并发能力。
但是 MySQL 使用了以乐观锁
为理论基础的MVCC
(多版本并发控制)来避免这两种问题。
2.2 悲观锁和乐观锁
悲观锁
指的是对于数据被外界修改持保守态度,在整个数据处理过程中,将数据处于锁定状态,它依赖于数据库底层提供的锁机制,读取数据时加锁,其它事务无法修改这些数据。修改删除数据时也要加锁,其它事务无法读取这些数据。Serializable
就是使用的悲观锁。
乐观锁
采取更加宽松的策略,相对于悲观锁的高性能开销乐观锁性能很好。大多是基于数据版本记录
机制实现的。一般就是在数据表中增加一个字段 version
字段,读取数据时将此版本号一并读出,之后每更新一次,版本号加一。然后将提交的数据版本信息与数据表记录的那个版本信息进行比对,如果大于等于则更新原来的版本号,否则认为是过期数据。
很多文章都说 RR
级别是可重复读的,但无法解决幻读,而只有在 Serializable
级别才能解决幻读。但是其实 RR
级别是可以解决幻读的读问题的。这得益于乐观锁的实现,即 MVCC 。
2.3 “读”与“读”的区别
MySQL
中的读,和事务隔离级别
中的读,是不一样的。
有的时候我们读取到的数据是历史数据,是不及时的,这种我们叫它 快照读
,而读取数据库当前版本数据的方式叫做 当前读
。
- 快照读:就是
select * from table ….
- 当前读:特殊的读操作,
插入/更新/删除
操作,属于当前读,处理的都是当前的数据,需要加锁。
事务的隔离级别实际上就是定义了当前读
的级别,MySQL
为了减少锁处理(包括等待其它锁)的时间,提升并发能力,引入了快照读的概念,使得select
不用加锁。
2.4 Next-Key 锁
MySQL 使用了 Next-Key 锁来解决当前读
中的幻读问题(也就是写)。
Next-Key 锁是行锁
和GAP
(间隙锁)的合并。
那么 GAP 锁又是什么呢?
RR
级别中,事务 A 在 update
后加锁,事务 B 无法插入新数据,这样事务 A 在 update
前后读的数据保持一致,就避免了幻读。这个锁,就是Gap锁。
行锁防止别的事务修改或删除,
GAP
锁防止别的事务新增,行锁
和GAP锁
结合形成的的Next-Key
锁共同解决了RR
级别在写数据时的幻读问题。
3. Serializable
这个级别很简单,读加共享锁,写加排他锁,读写互斥。使用的悲观锁的理论,实现简单,数据更加安全,但是并发能力非常差。如果你的业务并发的特别少或者没有并发,同时又要求数据及时可靠的话,可以使用这种模式。
参考资料:
- MySQL参考手册
- Innodb中的事务隔离级别和锁的关系
- 《高性能MySQL》中文第三版 P181