1、事务的特征以及事务并发造成的问题
2、事务读取一致性问题的解决方案
3、举例浅谈mvcc的原理
事务的特征以及事务并发造成的问题:
什么是事务:
定义:事务是数据库管理系统(DBMS)执行过程中的一个逻辑单位,由一个有限的数据库操作序列构成。
个人理解:通俗点说就是Navicat中打开的一个查询页面,就是一个事务,执行查询要么全部成功,要么全部失败。严谨点就是,mysql事务是一组原子性的SQL查询,或者说一个独立的工作单元,事务内的语句,要么全部执行成功,要么全部执行失败。它是一个逻辑单位,整体不能分割。
事务的典型场景:
一般来说数据库的事务类似类似代码中多线程。遇到有关数据变动操作时,在代码中使用事务注解,开启事务,实现拦截。再具体些的场景好比订单系统中操作,订单,物流的信息都需要在一个事务中完成,以免造成数据由多条请求操作,导致数据不准确。
支持事务的搜索引擎:
(1)InnoDB,由于其支持事务,所以也是默认搜索引擎
(2)NDB,支持集群的搜索引擎
事务的四大特性:
l 原子性(Atomicity) [ ˌætəˈmɪsɪ****ti] undo log
l 一致性(Consistent) [kənˈsɪstənt]
l 隔离性( Isolation) [ ˌaɪsəˈleɪʃ****n]
l 持久性(Durable) [ ˈ dj ʊə r ə bl]
事务如何开启与关闭:
MySQL中如何开启事务:
set session autocommit = on/off; -- 设定事务是否自动开启
begin / start transaction -- 手工方式开启事务
commit / rollback -- 事务提交或回滚,关闭事务
ps: 可视化工具窗口关闭。事务的锁在结束后会释放。
事务读取一致性问题的解决方案:
参考www.contrib.andrew.cmu.edu/~shadow/sql…,网页中中搜 "_iso",其中p1,p2,p3问题就是这三个问题:
详细来看下这三个问题:
**脏读: **就是一个事务读到另一个事务没有提交的数据。事务B修改了一个数据,但未提交,事务A读到了事务B未提交的更新结果,事务A读到的就是脏数据。如下图示意
幻读:就是一个事务读到另一个事务新增加并提交的数据(insert)。在同一个事务中,对于同一组数据读取到的结果不一致。比如,事务B新增了一条记录,事务A 在 事务B提交前后各执行了一次查询操作,发现后一次比前一次多了一条记录。幻读出现的原因就是由于事务并发新增记录而导致的。(仅限更新和删除操作)如下图示意
**不可重复读: **就是一个事务读到另一个事务修改后并提交的数据(update)。在同一个事务中,对于同一组数据读取到的结果不一致。比如,事务A 在 事务B 提交前读到的结果,和在 事务B 提交后读到的结果可能不同。不可重复读出现的原因就是由于事务并发修改记录而导致的。读取了其他事务提交的数据(仅限插入操作)如下图示意
事务并发三大问题都是数据库读一致性问题,由数据库提供的事务隔离机制来解决。
由此可以引入事务隔离级别这个概念:
事务的隔离级别有四种,从低到高分别是:读未提交、读已提交、可重复读、序列化。
读未提交: Read Uncommitted,顾名思义,就是一个事务可以读取另一个未提交事务的数据。最低级别,它存在4个常见问题(脏读、不可重复读、幻读、丢失更新)。
读已提交: Read Committed,顾名思义,就是一个事务要等另一个事务提交后才能读取数据。 它解决了脏读问题,存在3个常见问题(不可重复读、幻读、丢失更新)。
可重复读: Repeatable Read,就是在开始读取数据(事务开启)时,不再允许修改操作 。它解决了脏读和不可重复读,还存在2个常见问题(幻读、丢失更新)。
序列化: Serializable,序列化,或串行化。就是将每个事务按一定的顺序去执行,它将隔离问题全部解决,但是这种事务隔离级别效率低下,比较耗数据库性能,一般不使用。
越高的隔离级别,能解决的数据一致性问题越多,但同时也会带来性能损耗、降低并发性。
还是上面那个网站中,贴出了事务隔离机制对数据一致性三大问题的解决程度:
翻译过来看下具体说了什么:
上述的事务隔离级别对解决事务并发三大问题的效果如下表,可能就是未解决,不可能就是已解决。
看到这里,感觉可重复读RR模式于业务于性能来说是比较合适的,于是我便打开我的数据库来验证下默认的是什么:
查看系统隔离级别:select @@global.tx_isolation;
查看会话隔离级别(5.0以上版本):select @@tx_isolation;
查看会话隔离级别(8.0以上版本):select @@transaction_isolation;
果然,那么,RR,RC的事务隔离级别是怎么实现的,不讲很底层的原理,直接引入一个大概念,有两个: lbcc,mvcc模式。
lbcc: 在读取数据前,对其加锁,阻止其他事务对数据进行修改。
*mvcc: *生成一个数据请求时间点的一致性数据快照(Snapshot),并用这个快照来提供一定级别(语句级或事务级)的一致性读取。
很显然,lbcc模式,每次读取时都要对数据加锁,绝大多数场景下都是不现实的,所以一般使用的 解决读一致性的问题的方法是mvcc。
InnoDB的mvcc(Multi-Version Concurrency Control)逻辑:
暂时放下lbcc,我们来看看MVCC。
MVCC 的目的就是多版本并发控制,在数据库中的实现,就是为了解决读写冲突,它的实现原理主要是依赖记录中的 3个隐式字段,undo日志 ,Read View 来实现的。
在这里插入图片描述DB_ROW_ID 是数据库默认为该行记录生成的唯一隐式主键,DB_TRX_ID 是当前操作该记录的事务 ID ,而 DB_ROLL_PTR 是一个回滚指针,用于配合 undo日志,指向上一个旧版本 。
1.上面说到,mvcc是为数据生成一个readView,快照,我们可以理解为一个备份,这些插入更新删除等操作出来的备份存放在undolog日志中,以后每次访问都是来针对这个备份。
2.具体实现时依据一条规则:只能查找创建时间小于等于当前事务ID的数据,和删除时间大于当前事务ID的行(或未删除)
举例来看:
我有一条数据,id:1,name:吴彦祖。DB_TRX_ID版本为null
| id | name | DB_TRX_ID | DB_ROLL_PTR | 原始数据 |
|---|---|---|---|---|
| 1 | 吴彦祖 | null | null |
这时我开启一个事务,执行select语句,就叫这个过程为:看看我是谁,结果是:id:1,name:吴彦祖。但是DB_TRX_ID版本变为了1.
| id | name | DB_TRX_ID | DB_ROLL_PTR | 我开启一个事务查看 |
|---|---|---|---|---|
| 1 | 吴彦祖 | 1 | null |
这是又有一个事务在我查看事务后开启,执行了一条更新语句,更新结果如下,同时DB_TRX_ID版本变为了2,这时执行“看看我是谁”,结果不变还是吴彦祖,原因就是遵循上面的展示规则:只能查找创建时间小于等于当前事务ID的数据,和删除时间大于当前事务ID的行(或未删除)
| 版本变味了 | name | DB_TRX_ID | DB_ROLL_PTR | 另一个事务开启了一个更新name操作 |
|---|---|---|---|---|
| 1 | mmm.c | 2 | null |
这是又有一个事务开启了删除操作,将这条字段整个删掉了,这时DB_ROLL_PTR变为了4,但是这时执行“看看我是谁”,结果不变还是吴彦祖,原因还是:只能查找创建时间小于等于当前事务ID的数据,和删除时间大于当前事务ID的行(或未删除) 。
| id | name | DB_TRX_ID | DB_ROLL_PTR | 另一个事务开启了一个删除操作 |
|---|---|---|---|---|
| 1 | mmm.c | 3 | 4 |
所以说,在开启一个事务后,mvcc帮助我们模拟锁定了这个表,这个事务之后的一切操作都与这个事务无关。在Mysql中,Read Committed 和Read Repeatable Read隔离级别的一个非常大的区别就是它们生成ReadView的时机不同。在Read Committed中每次查询都会生成一个实时的ReadView,做到保证每次提交后的数据是处于当前的可见状态。而RepeaTable Read中,在当前事务第一次查询时生成当前的ReadView,并且当前的ReadView会一直沿用到当前事务提交,以此来保证可重复度(RepeaTable Read)。
这就是mvcc通过多版本的机制来实现数据库读一致性的解决方案。
个人学习笔记,欢迎大佬指正交流。