事务
事务是一组不可分割的数据操作的集合,要么全部成功,要么全部失败。mysql中事务由引擎层实现,并非所有引擎都支持事务(例如MyISAM引擎就不支持事务)
ACID
事务的ACID:
- 原子性 :事务中的操作不可分割,要么全部成功,要么全部失败;
- 一致性 :事务执行前后,数据总是从一致状态到达另一个一致状态,完整性没有被破坏;
- 隔离性 :多个事务对相同数据块的并发读写能力,定义有四个隔离级别,支持不同程度的并发;
- 持久性 :事务对数据的修改是永久的。
原子性、隔离性和持久性是手段,一致性是最终目的。在mysql(innodb引擎)中,原子性由undo log保证,隔离性由锁+MVCC保证,持久性由redo log+binlog保证。
隔离级别
事务隔离级别包括:
- 读未提交:事务还未提交的时候,它做的变更可以被其他事务看到,存在脏读;
- 读已提交:事务提交后,它做的变更才可以被其他事务看到,存在幻读;
- 可重复读(mysql默认):事务在执行过程看到数据总是和事务开始时看到数据一致,解决部分幻读;
- 串行化:对于同一数据行,读操作会加读锁,写操作会加写锁,当出现读写锁冲突时,必须等待持有锁的事务结束才能执行。
隔离级别越高,数据的一致性越能得到保障,但事务的并发程度越低,生产环境通常根据实际情况选择读已提交或可重复读级别。
幻读问题
围绕幻读主要有两个问题:1.什么是幻读?2.可重复读级别下是否解决了幻读问题?
- 什么是幻读:在同一个事务中,针对多次相同条件的查询,后一次查到了前一次不存在的数据。
- 可重复读是否解决了幻读:没有彻底解决,但解决了部分场景的幻读。
解释可重复读级别下的幻读问题,先说明下数据库读操作的两种类型:1.快照读:普通的select…语句;2.当前读:读取已提交的最新值,select…lock in share mode或select…for update,以及所有的写语句。
然后我们再看下两种类型的读操作是否会产生幻读问题:
1.如果前后两次都是快照读,在MVCC机制作用下,两次查询的结果是一致的;
2.如果第一次是当前读,因为当前读会给读到的记录加next-key lock,在提交前其他事务无法插入新记录,所以不存在幻读;
3.如果第一次是快照读,第二次是当前读,两次读操作之间另一个事务插入新记录并完成提交,那么第二次读就会读到这条新记录,产生幻读;
锁
- 锁加在索引记录上;
- 二阶段锁,sql执行时加锁,事务提交时再释放;
- 扫描过的记录都会加锁,当发生全表扫描时,在读提交模式下,不满足条件的记录会马上释放锁
服务层实现的锁
- 全局锁:flush table with read lock 命令,使全表处于只读状态,通常用语整库备份。
- 表锁:lock tables… read/write
- 元数据锁(MDL锁):执行DML语句加MDL读锁,执行DDL语句加MDL写锁,且MDL写锁优先级高于MDL读锁,当写锁进入等待,后面读锁请求也会被阻塞,直到写锁事务完成。
InnoDB实现的锁
行级锁
- 行锁:锁住一条记录;
- gap锁:锁住两个索引之间的区间,在可重复读级别下有效,防止一定程度的幻读,gap锁之间不互斥;
- next-key锁:行锁 + gap锁,前开后闭的区间,可重复读级别下加锁基本单位;
- 插入意向锁:插入时产生,包含插入区间的区间锁和插入行的行锁;
next-key锁包含两个优化:1.唯一索引的等值查询,退化为行锁,仅锁住命中的记录;2.索引的等值查询,如果向右查询到不符合条件的记录,退化为区间锁。
以表t为例,表t包含如下数据,其中id为主键,字段a为普通索引
id | a | b |
---|---|---|
5 | 5 | 5 |
10 | 10 | 10 |
15 | 15 | 15 |
在可重复读和读已提交两种级别下的加锁情况:
sql | 可重复读 | 读提交 |
---|---|---|
select * from t where a=10 | 不加锁 | 不加锁 |
select * from t where a=10 for update | 区间锁(5,15),包含行锁[10] | 行锁[10] |
select * from t where a=12 for update | 区间锁(10,15) | 不加锁 |
select * from t where id=10 for update | 行锁[10] | 行锁[10] |
select * from t where id>=10 and id<12 for update | next-key lock (10,15],行锁[10] | 行锁[10] |
表级锁
意向锁是InnoDB实现的一种表级锁,是InnoDB为了兼容行锁和表锁所引入的一种优化手段。
意向共享锁:对某一行数据加读锁时,需要先获取意向共享锁;
意向排他锁:对某一行数据加写锁时,需要先获取意向排它锁;
意向锁相互之间不互斥,是为了更好的支持行级并发度。
引入意向锁的目的是为了对使用InnoDB引擎的表进行锁表时提升性能:因为InnoDB支持行锁,当试图锁表时如果去逐行判断是否存在行锁显然太耗性能,因为存在行锁的前提示是存在意向锁,因此加表锁的判断逻辑简化为判断当前表是否存在意向锁即可,存在则锁表失败,否则成功锁表。