前言
事务是访问数据库的一个操作序列,数据库应用系统通过事务集来完成对数据库的存取。
1. 什么是事务?
事务必须服从ISO/IEC所制定的ACID原则。ACID是原子性(atomicity)、一致性(consistency)、隔离性(isolation)、持久性(durability)的缩写,这四种状态的意思是:
- 原子性(Atomicity):原子性是指事务包含的所有操作要么全部成功,要么全部失败回滚,这和前面两篇博客介绍事务的功能是一样的概念,因此事务的操作如果成功就必须要完全应用到数据库,如果操作失败则不能对数据库有任何影响;
- 一致性(Consistency):一致性是指事务必须使数据库从一个一致性状态变换到另一个一致性状态;
- 隔离性(Isolation):在事务正确提交之前,不允许把事务对该数据的改变提供给任何其他事务,即在事务正确提交之前,它可能的结果不应该显示给其他事务;
- 持久性(Durability):持久性是指一个事务一旦被提交了,那么对数据库中的数据的改变就是永久性的,即便是在数据库系统遇到故障的情况下也不会丢失提交事务的操作。
2. 事务的作用
当多个线程都开启事务操作数据库中的数据时,数据库系统要能进行隔离操作,以保证各个线程获取数据的准确性。
3. 遇到的并发问题
- 第一类丢失更新:A事务撤销时,把已经提交的B事务的更新数据覆盖了;
- 第二类丢失更新:A事务覆盖B事务已经提交的数据,造成B事务所做操作丢失;
- 脏读:A事务读取了事务B中未提交的数据;
- 不可重复读:A事务多次读取的值不同。因为该值被B事务修改并提交了;
- 幻读:A事务两次读之间,B事务插入了数据。
4. 如何解决上面的问题呢?
为了解决上面的问题,开发者为MySQL数据库设计了以下四种事务隔离级别:
- Read Uncommitted(未提交读):允许脏读,也就是可能读取到其他会话中未提交事务修改的数据;
- Read Committed(提交读):只能读取到已经提交的数据。Oracle等多数数据库默认都是该级别 (不重复读);
- Repeated Read(可重复读):可重复读。在同一个事务内的查询都是事务开始时刻一致的,InnoDB默认级别。在SQL标准中,该隔离级别消除了不可重复读,但是还存在幻象读,但是innoDB解决了幻读;
- Serializable(串行读):完全串行化的读,每次读都需要获得表级共享锁,读写相互都会阻塞。
隔离级别 | 脏读 | 不可重复度 | 不幻读 |
---|---|---|---|
Read Uncommitted(未提交读) | 可能 | 可能 | 可能 |
Read Committed(提交读) | 不可能 | 可能 | 可能 |
Repeated Read(可重复读) | 不可能 | 不可能 | 可能 |
Serializable(串行读) | 不可能 | 不可能 | 不可能 |
5. 小尝试
5.1 查看全局或会话的事务隔离级别
SELECT @@global.tx_isolation, @@tx_isolation;
5.2 修改全局或会话的事务隔离级别
SET [SESSION|GLOBAL] TRANSACTION ISOLATION LEVEL [READ UNCOMMITTED|READ COMMITTED|REPEATABLE READ|SERIALIZABLE]
6. 锁的基本叙述
数据库中的锁是指一种软件机制,用来控制防止某个用户(进程会话)在已经占用了某种数据资源时,其他用户做出影响本用户数据操作或导致数据非完整性和非一致性问题发生的手段。
按照锁级别划分,锁可分为共享锁、排他锁:
- 共享锁(读锁):针对同一块数据,多个读操作可以同时进行而不会互相影响。共享锁只针对UPDATE时候加锁,在未对UPDATE操作提交之前,其他事务只能够获取最新的记录但不能够UPDATE操作;
- 排他锁(写锁):当前写操作没有完成前,阻断其他写锁和读锁。
按锁粒度划分,锁可分为表级锁、行级锁、页级锁:
- 行级锁:开销大,加锁慢,会出现死锁,锁定力度最小,发生锁冲突的概率最低,并发度高;
- 表级锁:开销小,加锁快,不会出现死锁,锁定力度大,发生冲突所的概率高,并发度低;
- 页面锁:开销和加锁时间介于表锁和行锁之间,会出现死锁,锁定力度介于表和行行级锁之间,并发度一般。
7. 悲观锁和乐观锁
7.1 悲观锁
总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁、表锁等,读锁、写锁等,都是在做操作之前先上锁,所以不管冲突是否真的发生,都会使用锁机制。
悲观锁功能:
- 锁住读取的记录,防止其它事务读取和更新这些记录。其它事务会一直阻塞,直到这个事务结束;
- 悲观锁是在使用了数据库的事务隔离功能的基础上,独享占用的资源,以此保证读取数据一致性,避免修改丢失;
- 悲观锁可以使用Repeatable Read事务,它完全满足悲观锁的要求。
7.2 乐观锁
总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS算法实现。乐观锁适用于多读的应用类型,这样可以提高吞吐量。
乐观锁是一种思想,乐观锁不会锁住任何东西,也就是说,它不依赖数据库的事务机制,乐观锁完全是应用系统层面的东西。所以它不是一种锁机制.如果使用乐观锁,那么数据库就必须加版本字段,否则就只能比较所有字段,但因为浮点类型不能比较,所以实际上没有版本字段是不可行的。
7.3 版本号机制
一般是在数据表中加上一个数据版本号version字段,表示数据被修改的次数,当数据被修改时,version值会加一。当线程A要更新数据值时,在读取数据的同时也会读取version值,在提交更新时,若刚才读取到的version值为当前数据库中的version值相等时才更新,否则重试更新操作,直到更新成功。
7.4 CAS算法
CAS算法的核心思想是Compare and Swap,即比较再交换。
假设有A线程准备去修改内存中变量名为name的值,因此A线程会用以前自己读到的name变量值和此刻name的值做对比,如果一样,则表明在变量值没被修改过,因此可以更新修改,否则更新失败。
8. MySQL的重复读(Repeated Read)事务隔离级别
前面说过,MySQL默认实现了可重复读的事务隔离级别,但是不能解决幻读的问题,然而在MySQL数据库使用可重复读的事务隔离条件下,并未发生幻读。MySQL使用MVCC(多版本并发控制)进行了控制。
8.1名词简析:
- 1.MVCC——简称multiversion concurrency control,也就是多版本并发控制,是个很基本的概念。MVCC的作用是让事务在并行发生时,在一定隔离级别前提下,可以保证在某个事务中能实现一致性读,也就是该事务启动时根据某个条件读取到的数据,直到事务结束时,再次执行相同条件,还是读到同一份数据,不会发生变化(不会看到被其他并行事务修改的数据);
- read view——InnoDB MVCC使用的内部快照的意思。在不同的隔离级别下,事务启动时(有些情况下,可能是SQL语句开始时)看到的数据快照版本可能也不同。在上面介绍的几个隔离级别下会用到 read view;
- 快照读——就是所谓的根据read view去获取信息和数据,不会加任何的锁;
- 当前读——前读会获取得到所有已经提交数据,按照逻辑上来讲的话,在一个事务中第一次当前读和第二次当前读的中间有新的事务进行DML操作,这个时候俩次当前读的结果应该是不一致的,但是实际的情况却是在当前读的这个事务还没提交之前,所有针对当前读的数据修改和插入都会被阻塞,主要是因为next-key lock解决了当前读可能会发生幻读的情况。next-key lock当使用主键索引进行当前读的时候,会降级为record lock(行锁)。
8.2 Read view详析
InnoDB支持MVCC多版本控制,其中READ COMMITTED和REPEATABLE READ隔离级别是利用consistent read view(一致读视图)方式支持的。所谓的consistent read view就是在某一时刻给事务系统trx_sys打snapshot(快照),把当时的trx_sys状态(包括活跃读写事务数组)记下来,之后的所有读操作根据其事务ID(即trx_id)与snapshot中trx_sys的状态做比较,以此判断read view对事务的可见性。
REPEATABLE READ隔离级别(除了GAP锁之外)和READ COMMITTED隔离级别的差别是创建snapshot时机不同。REPEATABLE READ隔离级别是在事务开始时刻,确切的说是第一个读操作创建read view的时候,READ COMMITTED隔离级别是在语句开始时刻创建read view的。这就意味着REPEATABLE READ隔离级别下面一个事务的SELECT操作只会获取一个read view,但是READ COMMITTED隔离级别下一个事务是可以获取多个read view的。
创建/关闭read view需要持有trx_sys->mutex,会降低系统性能,5.7版本对此进行优化,在事务提交时session会cache只读事务的read view。
8.3 read view 判断当前版本数据项是否可见
在InnoDB中,创建一个新事务的时候,InnoDB会将当前系统中的活跃事务列表(trx_sys->trx_list)创建一个副本(read view),副本中保存的是系统当前不应该被本事务看到的其他事务id列表。当用户在这个事务中要读取该行记录的时候,InnoDB会将该行当前的版本号与该read view进行比较。
具体的算法如下:
- 设该行的当前事务id为trx_id,read view中最早的事务id为trx_id_min, 最迟的事务id为trx_id_max;如果trx_id< trx_id_min的话,那么表明该行记录所在的事务已经在本次新事务创建之前就提交了,所以该行记录的当前值是可见的。
- 如果trx_id>trx_id_max的话,那么表明该行记录所在的事务在本次新事务创建之后才开启,所以该行记录的当前值不可见;
- 如果trx_id_min <= trx_id <= trx_id_max, 那么表明该行记录所在事务在本次新事务创建的时候处于活动状态,从trx_id_min到trx_id_max进行遍历,如果trx_id等于他们之中的某个事务id的话,那么不可见。
如图所示:
从该行记录的DB_ROLL_PTR指针所指向的回滚段中取出最新的undo-log的版本号的数据,将该可见行的值返回。
需要注意的是,新建事务(当前事务)与正在内存中commit 的事务不在活跃事务链表中。
在具体多版本控制中我们先来看下源码:
函数:read_view_sees_trx_id。read_view中保存了当前全局的事务的范围:【low_limit_id, up_limit_id】
1.当行记录的事务ID小于当前系统的最小活动id,就是可见的。 if (trx_id < view->up_limit_id) { return(TRUE); }2.当行记录的事务ID大于当前系统的最大活动id(也就是尚未分配的下一个事务的id),就是不可见的。 if (trx_id >= view->low_limit_id) { return(FALSE); }3.当行记录的事务ID在活动范围之中时,判断是否在活动链表中,如果在就不可见,如果不在就是可见的。 for (i = 0; i < n_ids; i ) { trx_id_t view_trx_id = read_view_get_nth_trx_id(view, n_ids - i - 1); if (trx_id <= view_trx_id) { return(trx_id != view_trx_id); } }
Read view 图解:
参考:https://yq.aliyun.com/articles/560506
来源:https://segmentfault.com/a/1190000018652117