1.事务的四大特点
- 原子性:事务中的所有操作要么都成功,要么都失败。所有的操作是一个不可分割的单位。
- 一致性:一致性指的是事务执行前后,数据从一个合法性状态转移到另一个合法性状态。这个状态和业务有关,是自己定义的。比如余额不能为负数。一旦事务执行完不满足这个合法性状态,就违反了一致性原则。
- 隔离性:并发环境下,多个事务同时操作数据库,应该保证多个事物之间互不影响。
- 持久性:事务一旦提交,那么就会被永久的写入到磁盘中。
总的来说,原子性是基础,隔离性是手段,一致性是约束条件,持久性是目标。
2.事务并发访问数据库会产生的问题
- 脏写问题:Session A修改了session B中已经修改但是还没有提交的记录,Session A提交了,但是Session B回滚了,Session A再次访问该记录发现自己修改的记录没有生效。
- 脏读问题:事务A读取了已经被事务B修改但是还没有提交的数据。之后如果事务B进行回滚,那么事务A读取到的数据就是无效的。
- 不可重复读:事务A读取了某一条记录,事务B修改了这个记录,然后事务A再次读取发现结果变了,这就是不可重复度。
- 幻读:事务A查询年龄大于10岁的人,事务B正好修改这一部分记录,事务A再次按照相同条件查询发现结果和之前查询不一致了,这就是幻读。
3.SQL中的四种隔离级别
- 读未提交:所有的事务都可以访问其他事务中修改但是未提交的数据
- 读已提交:一个事务只能读取到其他事务已经提交的数据;可以避免脏读
- 可重复读:事务A在读到一条数据之后,此时事务B对该数据进行了修改并提交,那么事务A再读该数据,读到的还是原来的内容。可以避免脏读、不可重复读,但幻读问题仍然存在。在这个事务执行期间,禁止其他事务对相应字段写操作
- 序列化:可串行化,确保事务可以从一个表中读取相同的行。在这个事务持续期间,禁止其他事务对该表执行插入、更新和删除操作。所有的并发问题都可以避免,但性能十分低下。在这个事务执行期间,禁止其他事务对整个表进行写操作
4.锁结构
- 锁结构包含两个重要属性:创建锁的事务id和is_waiting字段
- 当一个事务要访问某一条记录的时候,会首先生成一个锁结构,然后看一看有没有其他锁结构和这条记录关联,如果有,那么is_waiting字段设置为true,否则设置为false
- 当T1执行完毕后,会释放锁结构,然后看一下这条记录有无关联的锁结构,然后将is_waiting设置为false,唤醒这个事务
5.MySQL中解决并发访问问题的方法
- MVCC,对写操作进行加锁,读-写 操作彼此并不冲突, 性能更高 。
- 采用加锁的方式,读-写 操作彼此需要 排队执行 ,影响性能。
6.按照数据类型分类的锁
- 读锁(S锁 共享锁):允许多个事务同时读取数据。但是读取的时候不允许写入,因此和X锁不兼容
- 写锁(X锁 排他锁):写的时候即不允许其他的事务写,也不允许其他的事务读。因此和X以及S锁都不兼容
7.什么是锁定读
- 在使用select语句的时候可以使用lock in share mode 和 for update 分别个这个记录添加读锁和写锁
注意:for update for share 针对的都是select语句,如果是insert update
等操作会自动添加相应的锁,不需要写for …
8.按照锁的粒度去划分,划分为什么锁
- 表级锁:表级锁和之前的行级锁是一样的区别在于表级锁需要自己手动的去开启和释放,
- 页锁
- 行锁:行级锁在commit之后就自动释放了;而行锁需要手动的去释放锁以后锁才会消失
9.意向锁
使用场景
- 现在又两个事物T1和T2,T2想在表上添加一个表级锁,如果没有意向锁,那么它需要遍历表中所有记录,来查看是否有互斥的行锁存在,如果没有则加表锁。这个遍历过程是非常耗时的。
- 有了意向锁以后,当T1针对表中的某一条记录添加锁以后,还会在其上层结构中(比如行的上层就是表),添加意向锁IX,此时,当T2要添加表锁时候,直接查看是否存在意向锁即可,大大提高了效率。
特点
- InnoDB支持不同粒度的锁共存,意向锁就是一个例子,实现了表级锁和行锁的共存
- 意向锁是表级锁,不会和行级锁发生冲突。只会和表级的X,S锁发生冲突
- 意向锁在保证并发性的前提下,实现了行锁和表锁的共存,其满足事务的隔离要求
10.其他锁
- 自增锁
- MDL元数据锁:如果一个查询正在遍历一个表中的数据,而执行期间另一个线程对这个 表结构做变更 ,增加了一列,那么查询线程拿到的结果跟表结构对不上,肯定是不行的。因此,当对一个表做增删改查操作的时候,加 MDL读锁;当要对表做结构变更操作的时候,加 MDL 写锁。
11.间隔锁
- 数据库中有四种隔离级别,读未提交,读已提交,可重复读,和串行化,在不同的隔离级别下会出现脏读,不可重复读和幻读的问题。
- 前三种问题可以通过行锁来解决,但是幻读问题不可以。原因是无法给还没有插入的数据加锁。
- 为了解决这一个问题,间隔锁就被提出来了。间隔锁实现了在可重复读的隔离级别下避免了幻读的产生
age字段中的值1 4 7 10
那么产生的区间为 (-∞,1) 1 (1,4) 4 (4,7) 7 (7,10) 10 (10,+∞)
select * from student where age > 5 and age < 8
锁的并不是整个数据库表,而是查找离下限(5)最大的id值和上限(10)最小的id值,分别为4和15;然后锁的区间范围为(4,15],
此时会给(4,7] (7,10)加间隙锁,这样其他的事务要想在这个区间插入就会被阻塞,从而保证了幻读不会发生
1 4 7 9 15 18
- 间隔锁只能去锁间隔,而不能锁记录,因此又推出了临键锁。临键锁是间隔所与行锁相结合的产物。在上面的例子中,如果我们查找大于4的所有记录,那么除了(4,7)(7,10)(10,+∞)区间需要加锁以外,7,10记录也需要加锁,都加锁就是临键锁。
- 不是只要保证不在间隙内部插入数据就可以了吗,为什么还需要锁记录?原因是如果不锁这些在范围内的记录的话,一旦其他的事务修改了这条记录的age,是得它不在这个区间内了,那么也会产生幻读现象
12.乐观锁和悲观锁
- 悲观锁总是往最坏的情况打算,当一个事务要修改数据时,都要加锁。其他事务阻塞。比如Java中的同步代码块就是应用了悲观锁的原理
- 乐观锁是假设某一个事务读取数据的时候,都假设他不会修改数据,所以不加锁。只有当这个事务要进行写操作的是时候会判断一些之前有没有人更新过这个数据。实现方式主要有版本号机制,CAS算法
- 乐观锁适用于读操作比较多的情况,而悲观锁适用于写操作比较频繁的情景。
MVCC多版本并发控制
1.好处
- 在没有MVCC的时候,如果一个事务要访问另一个正在被更新的事务是不可以访问的,因为有排他锁。只有等这个更新的事务提交以后才可以访问相应的记录,导致并发性差。
- 引入了MVCC以后,即使某一行数据正在被修改,那么其他的读事务也是可以进行访问的,并且不会出现脏读的问题。
常见的快照读就是MVCC的一个应用
2.快照读和当前读
- 快照读读取到的可能不是最新的数据,而是之前版本的一个快照。读取的时候不加锁。
- 当前读读取的时候会给记录加锁,每次读取到的数据一定是最新数据。
3.MVCC实现的基础
-
隐藏字段(最后一次修改记录的事务ID,指向undo日志的指针)、undoLog、ReadView。
-
每一条记录除了我们自己写的字段以外,还会有其他的一些隐藏字段,比如记录最后一次修改该记录的事务ID字段,指向undoLog的字段。
-
undoLog。他是一个链表,里面记录了该字段每一次修改后的版本信息。
-
在MVCC机制中,多个事务对同一个行记录进行更新会产生多个历史快照,这些历史快照保存在Undo Log里。如果一个事务想要查询这个行记录,需要读取哪个版本的行记录呢?这时就需要用到ReadView了,它帮我们解决了行的可见性问题。
4.ReadView的结构
- creator_trx_id,创建这个 Read View 的事务 ID。查询操作默认为0;
- trx_ids,表示在生成ReadView时当前系统中活跃的读写事务的事务id列表。
- up_limit_id,活跃的事务中最小的事务 ID。
- low_limit_id,表示生成ReadView时系统中应该分配给下一个事务的id值。low_limit_id 是系统最大的事务id值,这里要注意是系统中的事务id,需要区别于正在活跃的事务ID。
5.ReadView规则
- 如果被访问版本的trx_id属性值与ReadView中的creator_trx_id值相同,意味着当前事务在访问它自己修改过的记录,所以该版本可以被当前事务访问。
- 如果被访问版本的trx_id属性值小于ReadView中的up_limit_id值,表明生成该版本的事务在当前事务生成ReadView前已经提交,所以该版本可以被当前事务访问。
- 如果被访问版本的trx_id属性值大于或等于ReadView中的low_limit_id值,表明生成该版本的事务在当前事务生成ReadView后才开启,所以该版本不可以被当前事务访问。
- 如果被访问版本的trx_id属性值在ReadView的up_limit_id和low_limit_id之间,那就需要判断一下trx_id属性值是不是在trx_ids列表中。
- 如果在,说明创建ReadView时生成该版本的事务还是活跃的,该版本不可以被访问。
如果不在,说明创建ReadView时生成该版本的事务已经被提交,该版本可以被访问。
6.MVCC具体流程
- 首先获取事务自己的版本号,也就是事务 ID;
- 生成 ReadView;
- 查询得到的数据,然后与 ReadView 中的事务版本号进行比较;
- 如果不符合 ReadView 规则,就需要从 Undo Log 中获取历史快照;
- 最后返回符合规则的数据。
7.ReadView刷新规则
- 在读已提交隔离级别下,每一次select都会重新生成一个新的ReadView
- 在可重复隔离级别下,一个事务中只有第一次select会刷新ReadView
8.MVCC如何解决幻读问题
假设现在查询下面的SQL语句
select * from student where grade > 10
当这条语句执行之前,会生成一个ReadView,然后根据ReadView规则,将数据查询出来。之后事务B又添加了2条符合要求的数据。
再次查询,根据可重复读的规则,ReadView不会再次刷新,实际上查询出来3条记录,然后再根据ReadView规则逐一筛选这3条记录是否符合条件,然后决定是否对该事务可见。从而在可重复读的隔离级别下解决了幻读问题。