事务
事务的四大特性
- 原子性
原子不可分割,就是说一系列操作要么全部成功,要么全部失败。在mysql中通过undo log来实现。undo log中记录了修改前的数据,如果发生异常通过undo log实现回滚操作。
- 隔离性
我们有了事务的定义之后,在mysql中有很多事务同时去操作我们的同一张表、同一条数据,会产生一些并发的干扰。对接口隔离性的定义,就应该是事务直接互不干扰
- 持久性
我们对数据库数据进行操作,只要事务提交成功,不管是宕机还是断电,数据都不会丢失,这就是事务的持久性。mysql持久性是通过redo log和double write buffer(双写缓冲)来实现的,客户端修改了数据之后,先将数据写入buffer pool中,同时记录redo log,redo log记录的是变更的数据。如果在刷脏之前出现异常,重启后就可以通过读取redo log的内容写入磁盘,保证数据的持久性。
当然,恢复成功的前提是数据页本身没有被破坏,这个通过双写缓冲保证。
- 一致性
原子性、隔离性、持久性,都是为了实现一致性
一致性指的是数据库完整性约束没有被破坏,数据执行前后都是合法的数据。预期结果与实际结果一致。
哪些引擎支持事务
InnoDB
事务的开启
- 正常执行一条sql语句,默认会自动开启提交事务、自动提交事务
- 如果需要手动开启事务,可以通过begin 或start transaction
事务提交
执行commit命令
事务回滚
执行rollback命令
事务并发带来了什么问题?
脏读
有两个事务,事务1查询id为1的数据,结果为18。
事务2修改该数据值为16,且未提交
此时事务1再次执行查询,发现两次查询结果不一致
那么,在一个事务里面,由于其它事务修改了数据并且未提交导致的两次查询结果不一致的情况,我们叫它脏读
不可重复读
还是1和2两个事务
事务1查询id为1的数据,结果为18.
事务2修改该数据值为16并且提交了
事务1再次执行查询,发现两次结果不一致
那么,在一个事务里面,由于其它事务已提交事务导致两次查询结果不一致的情况,我们叫它不可重复读
幻读
事务1执行范围查询id<5,只查询出id为1的数据
事务2执行插入数据id=2,并且提交了
事务1再次执行查询,发现多了一条数据
那么,在一个事务中,由于其它事务插入数据并提交事务导致两次查询结果不一致的情况,我们叫它幻读
不可重复读和幻读的区别
不可重复读是修改数据、删除数据导致的,幻读是插入数据导致的
事务隔离级别
- Read UnCommitted(RU,未提交读),一个事务可以读到其它事务未提交的数据,会出现脏读,它没有解决任何问题
- Read Committed(RC,已提交读),一个事务只能读到其它事务已提交的数据,不能读到其它事务未提交的数据,它解决了脏读的问题,但是未解决不可重复读的问题
- Repeatable Read(RR,可重复读),解决了不可重复读的问题,也就是一个事务执行多次查询,结果是一样的,但是这个级别下没有解决幻读的问题
- Serializable(串行化),在这个隔离级别中,所有事务都是串行执行,也就是对数据对操作需要排队,也就不存在并发操作了,它解决了所有问题
事务隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
未提交读(read-uncommitted) | 可能 | 可能 | 可能 |
已提交读(Read Committed) | 不可能 | 可能 | 可能 |
可重复读(Repeatable Read) | 不可能 | 不可能 | 对InnoDB不可能 |
串行化(Serializable) | 不可能 | 不可能 | 不可能 |
隔离级别越高,事务的并发就越低。唯一的区别就是InnoDB在RR中解决了幻读的问题。也就是说mysql即保证了数据一致性,又支持并发
解决读一致性方案
- LBCC
既然要保证数据库一致性,那么读取数据的时候锁定要操作的数据,不允许其它事务进行修改。这种是基于锁的并发控制Based Concurrency Control(LBCC)
如果只基于锁来实现事务隔离,对程序并发影响极大
- MVCC
对于LBCC对性能影响太大,所以还有另一种解决方案,基于多版本控制方案,在事务修改数据之前对该数据进行备份,其它事务读取时直接读取该备份。这种方式叫多版本的并发控制 Multi Version Concurrency Control (MVCC)
一个事务能看到的版本
- 第一次查询之前已提交的事务
- 本事务修改的
一个事务不能看到的版本
- 本次事务查询执行之后创建的事务操作(后生成的事务id大于先生成的事务id)
- 未提交的事务修改
所以不管其它事务执行什么操作,对本事务而言只能看到第一次查询时的数据
MVCC实现原理
因为事务id是递增的,所以很容易判断出比当前事务小的事务
在InnoDB中,每行数据都有两个隐藏的字段:
- DB_TRX_ID(事务ID),哪个事务插入或修改该数据时,就将事务ID记录到数据中
- DB_ROLL_PTR 回滚指针(我们把它理解为删除版本号),数据被删除或标记为旧数据时,记录修改该数据的事务ID,没有修改删除操作时,该字段为空
假设现在有多个事务,事务id也和编号一样
事务1插入两个数据,那么这两条数据的事务ID字段就标记为1,删除版本号为空
事务2执行查询操作,查询出两条数据
事务3执行插入操作,第三条数据事务ID就标记为3
事务2再次执行查询,因为2小于3,所以无法查询出事务3创建的数据
这时事务4,删除第二条数据,这时第二条数据的删除版本号将记录成事务4的事务id
这时事务2再执行查询,因为第二条数只能查找创建时间小于等于当前事务ID的数据,和删除时间大于当前事务的ID,所以第二条数据还可以查到
版本号实际存储在哪里?
我们知道redo log是记录变更数据的日志,所以版本号也记录在这里,就比如上面的例子,因为修改了多次,这些redo log会形成一条链,叫redo log链。实际上DB_TRX_ID和DB_ROLL_PTR记录的就是redo log链中的指针。
锁
锁的粒度
我们知道InnoDB支持行锁和表锁,而MyISAM只支持表锁
锁的粒度:表锁 > 行锁
加锁效率:表锁 > 行锁
冲突概率:表锁 > 行锁
并发性能:表锁 < 行锁
锁的类型
-
共享锁(Shared Locks)
共享锁也叫读锁,获取了一行数据的读锁之后,可以用来读数据。多个事务可以共享一把读锁
可以用select … lock in share mode;加上一把读锁
事务提交和事务结束可以释放锁 -
排它锁(Exclusive Locks)
又称为写锁,用它来操作数据。一个事务获取了一行数据的排它锁,其它事务不能获取这行数据的共享锁和排它锁
在操作数据的时候(增、删、改)会自动加上排它锁 -
意向锁
意向锁是数据库自己维护的
当我们给一行数据加上共享锁之前,数据库会自动在这张表上面加一个意向共享锁。
我们给一行数据加上排它锁之前,数据库也会自动在表上增加意向排它锁。
也就是说,如果表上至少有一个意向共享锁,说明其它事务给某些行数据加上了共享锁
如果表上至少有一个意向排它锁,说明其它事务给某些行数据加上了排它锁
意向锁和意向锁是不冲突的,和行锁也不冲突意向锁的作用
如果没有意向锁的话,在想对表进行加锁的时候,首先需要判断是否有加了锁的行,如果有行锁,那表锁肯定不能让它锁定成功,就必须扫描整个表去检查,非常耗费性能
有了意向锁之后,只需要看表上是否有意向锁,如果有意向锁就直接返回失败;如果没有意向锁就可以加锁成功
锁的原理
- 记录锁
当我们对唯一索引、主键索引等值查询,精准匹配到一条记录时,用的就是记录锁。锁住这一条数据
- 间隙锁
当我们查询的数据不存在时,用的就是间隙锁,不管等值查询还是范围查询
间隙锁主要阻塞插入操作,添加间隙锁不冲突
- 临键锁
当使用范围查询时,不仅命中了记录,还包含间隙。这种情况下使用的就是临键锁。它是mysql默认的行锁算法,相当于记录锁+间隙锁
等值查询匹配到一条数据时,退化成记录锁
没有匹配到任何数据,退化成间隙锁
为什么mysql RR级别就可以解决幻读问题?
- RU
不加锁 - Serializable
加锁查询添加共享锁,会和修改删除互斥 - RR
加锁的查询用MVCC实现+临键锁 - RC
加锁的查询都使用记录锁
事务隔离级别怎么选
- RR间隙锁会导致锁范围扩大
- 条件未使用索引时,RR锁表,RC锁行
- 默认使用RR