事务是关系型数据库的一大特性,用来保证数据的一致性。并发事务会导致数据不一致问题,MySQL针对并发事务也有相应的解决办法。下面一起来分析一下。
一、什么是数据库事务
1.1 事务的概念
维基百科的定义:事务是数据库管理系统(DBMS)执行过程中的一个逻辑单位,由一个有限的数据库操作序列构成。
这里面有两个关键点,第一个,它是数据库最小的工作单元,是不可以再分的。第二个,它可能包含了一个或者一系列的 DML 语句,包括 insert delete update。
(单条 DDL(create drop)和 DCL(grant revoke)也会有事务)
1.2 事务的四大特性 ACID
讲事务,就不得不提ACID原则。即:原子性、一致性、隔离性、持久性
1.2.1 原子性 Atomicity
事务不可再分。就是事务中的一组操作,要么全部完成,要么全部失败。在MySQL中,是通过 undo log 来实现原子性的。
1.2.2 一致性 Consistent
指的是数据库的完整性约束没有被破坏,事务执行的前后都是合法的数据。
怎么理解呢?就是对数据库的操作,一定要符合预期。原子性、隔离性、持久性都是为了保证一致性。
1.2.3 隔离性 Isolation
隔离性是指不同事物间对相同数据的操作要隔离,不能相互影响。其实就可以同Java中的并发操作共享数据类比理解。意思差不多的。
1.2.4 持久性
持久性是指对数据的操作一定要生效,不会因为数据库宕机等外力因素导致没有生效。MySQL中通过 redo log + 双写缓冲实现持久性
二、事物隔离级别
2.1 并发事务产生的问题
2.1.1 脏读
事务1读取到了事务2未提交的数据。重点未提交
2.1.2 不可重复读
事务1读取到了事务2已提交(update或delete)的数据。
2.1.3 幻读
事务1读取到了事务2已提交(insert)的数据。
注意:幻读和不可重复读的区别是,幻读是读取到了其他事务insert的数据。
2.2 事务隔离级别
针对于并发事务引发的脏读、不可重复读和幻读的问题,MySQL有相应的隔离级别来解决。
2.2.1 读未提交 Read UnCommited
在RU下,可以读取到其他事务未提交的数据,会产生脏读问题。其实就是啥也没解决,啥也不是。。。
2.2.2 读已提交 Read Commited
在RC下,可以读取到其他事务已提交的数据,解决了脏读的问题,但是不可重复读和幻读的问题依然存在。
2.2.3 可重复读 Repeatable Read
MySQL默认的隔离级别。解决了脏读、不可重复读问题,同时基于 临键锁 也解决了幻读的问题。
临键锁:会锁住记录往后的区间,避免insert
比如:update user set name='帅比' where id = 7;
那么这时临键锁,会锁住 7 和 (7,+无穷),也就是说要想往7以后insert数据是没可能了,那么就不会发生幻读的问题了。
2.2.4 串行读 Serializable
不管你什么操作,统统加锁,毫无道理可言。当然效率最低。(一顿操作猛如虎,一看效率转圈中),哈哈。。。
三、锁
思考一个问题:如何实现事务的隔离?
其实这就跟我们解决Java的并发安全性问题一样。在Java中,我们可以怎么解决?
- CAS
- 加锁 那么在MySQL中,也同样有两种解决方法:
- Multi Version Concurrency Control(MVCC)
InnoDB 为每行记录都实现了两个隐藏字段:
DB_TRX_ID,6 字节:插入或更新行的最后一个事务的事务 ID,事务编号是自动递增的(我们把它理解为创建版本号,在数据新增或者修改为新数据的时候,记录当前事务 ID)。
DB_ROLL_PTR,7 字节:回滚指针(我们把它理解为删除版本号,数据被删除或记录为旧数据的时候,记录当前事务 ID) - Based Concurrency Control(LBCC) 这个就很好理解了。在操作数据的时候,先加锁,这样其它事务要想操作这个数据,比必须等先前的事务释放锁之后才能操作。
3.1 锁类型
锁的粒度:表锁、行锁 锁类型:
- 共享锁,select * from user in share mode;
- 排他锁, update/insert/delete默认就加了排他锁,或者select * from user for update;
- 意向锁,MySQL隐式加的,在表记录加共享锁时,默认加了意向共享锁;排他锁同理。
3.2 锁算法
3.2.1 记录锁
等值查询,命中存在的记录。
3.2.2 间隙锁
查询没有命中已存在的记录。比如现在有id: 1、3、6、9这几条数据
select * from user where id in (7,8) for update;
没有命中,会锁住范围(7,8)
3.2.3 临键锁
查询包含存在的记录和不存在的记录。
select * from user where id >=6 for update;
包含已存在记录6,9,以及不存在的 7,8,这时锁范围:[6,9]和(9,+无穷)。
可以看到临键锁,会锁住最后一条记录往后的区间,这也是为什么RR隔离级别可以解决幻读的原因。
四、总结
- 事务主要是为了保证数据的一致性,而一致性是由原子性、隔离性、持久性共同来保证的。
- 原子性基于 undo log 来保证。
- 持久性基于 redo log + 双写缓冲来保证。
- MySQL在RR隔离级别下,保证了并发事务的安全性。主要是基于MVCC和LBCC两种解决方法。
- 在RR级别的临键锁,默认会锁住记录往后的下一个左开右闭的区间,所以能够解决幻读的问题。