前段时间,公司内部遇到了一个问题,就是咱们建立的同一批任务,别分配给了不一样的实例去执行,致使线上的结果出现问题。面试
另外一个组的leader说没有开启事务,设置下事务就能够。sql
数据库一出现一致性问题,就说开启事务,我就有点疑惑,数据库的事务究竟是怎么保证一致性的。数据库
在看下面的内容,咱们能够先思考几个问题。数组
数据库的隔离级别都有什么?数据库的MVVC视图是怎么实现的?session
数据库的隔离级别是为了解决什么问题的?并发
看完上面三个问题,本身能回答上来几个呢?不急。咱们继续往下看cors
数据库的事务咱们简单来讲就是用来保证数据的正确性,它只有两个操做:事务要么成功,要么失败并进行回滚。spa
为何这么作呢?这是由于通常咱们进行事务操做,都会进行一组操做。好比你常见的金融转帐。日志
在这个转帐事务里面包含2个操做:code
如今思考下,若是咱们没有添加事务,那么会出现什么样的状况呢?
因此了咱们只能在这种操做中使用事务,来保证执行的成功与失败,失败了要进行回滚,保证扣钱的操做也不执行。
事务具备四个特性,这四个特性简称为ACID
今天主要说的事务就是隔离。看看事务是怎么保证数据之间的隔离
不一样的事务隔离级别对应的不一样的数据执行效率,隔离的越严格,那么执行的效率就约低下,下面的四个隔离级别是原来越严格。
Mysql中默认的事务隔离级别是可重复读,使用下面这个命令进行查看当前的事务级别,
show variables like 'transaction_isolation'; # 下面的语句进行修改事务的级别。 SET session TRANSACTION ISOLATION LEVEL Serializable;(参数能够为:Read uncommitted,Read committed,Repeatable,Serializable)
在程序中,咱们不少时候都是默认的自动提交,也就是一个sql操做就是一条事务,但有时候须要的是多个SQL进行组合,咱们就要显式的开启事务。
显示开启的语句是用 begin或者 start transaction.一样在事务结束的时候使用commit进行提交,失败使用rollbakc进行回滚。
固然若是不想让SQL进行自动提交,咱们就将自动提交进行关闭set autocommit=0
,这样事务就不会自动提交,须要咱们手动的执行commit.
关闭自动提交事务后,就须要咱们来本身提交事务,这时候每一个语句执行都是这样的。
begin sql语句 commit
若是咱们在程序编写中,原本一个sql解决的操做,结果忘记进行事务的提交,到下下下一个SQL才进行commit,这样就会出现长事务。
而长事务每每会形成大量的堵塞与锁超时的现象,事务中若是有读写(读读不冲突,读写冲突,写写冲突)操做,那么会将数据进行锁住,其余事务也要进行等待。
因此在程序中,咱们应该尽可能避免使用大事务,一样也避免咱们写程序的时候出现偶然的大事务(失误😁)。
解决办法是咱们将自动提交打开,当须要使用事务的时候才会显示的开启事务。
在MySQL中想定位一个长事务问题仍是很方便的。
首先咱们先找到正在执行的长事务是什么。
select t.*,to_seconds(now())-to_seconds(t.trx_started) idle_time from INFORMATION_SCHEMA.INNODB_TRX t G
该语句会展现出事务的执行的开始时间,咱们能够很简单的算出,当前事务执行了多久,其中上面的idle_time就是执行的事务时间
假设咱们如今设定的超过30s执行的事务都是长事务,可使用下面语句进行过滤30s以上的长事务。
select * from information_schema.innodb_trx where TIME_TO_SEC(timediff(now(),trx_started))>30
经过information_schema.innodb_trx 咱们就能定位到长事务。
从而决定长事务是否要被杀死,仍是继续等待。若是出现死锁的状况,处理方式也是相似,查找到死锁的语句,而后进行杀死某个语句的执行(有点暴力)。
注意:本次数据隔离是创建在可重复读的场景下
在可重复读的场景下,咱们了解每次启动事务的时候,会在当前启动一个视图,而这个视图是整个数据库的视图快照。
嘿嘿,是否是想数据库那么大,为啥咱们没有感受到建立快照时间的消耗呢?
这是由于数据库建立的视图快照利用了全部数据都有多个版本的特性,来实现快速建立视图快照的能力。
那数据多个版本是怎么回事呢?
先别急,咱们准备下数据。
如今建立一个表,而且插入三条数据。
create table scores ( id int not null primary key, score float null ); INSERT INTO scores (id, score) VALUES (1, 3.5); INSERT INTO scores (id, score) VALUES (2, 3.65); INSERT INTO scores (id, Score) VALUES (3, 4);
在开始使用前咱们要了解两个小知识点。begin/start transaction 与 start transaction with consistent snapshot。
begin select source from scores; //视图是在这里开始建立 而不是在begin那里建立 commit
了解上面两个建立事务的区别后,咱们来看下视图是怎么建立出来多个数据版本的. 如下SQL在两个窗口打开。
事务A | 事务B | 结果
start transaction with consistent snapshot | 开启事务,并建立视图 |
--| start transaction with consistent snapshot |开启事务,并建立视图
select score from scors where id =2 | -- | 事务A中的值为3.65
-- | update scores set scores = 10 where id =2 | 事务B修改成10
--| select score from scores where id =2 | 事务B显示为10
select score from scores where id =2 | --| 事务A显示为3.65
select score from scores where id =2 for update | --| 会被锁住,等待事务B释放锁(间隙锁)
-- |commit | 提交事务B
select score from scores where id =2 for update | --| 这个语句能够看到变成了10(利用了当前读)
select score from scores where id =2 | --| 不加 for update 那么结果仍是3.65
commit|---|---| 提交A的结果
上述流程就是两个不一样的请求过来,对数据库同一个表的不一样操做。
当事务A执行start transaction with consistent snapshot以后,A的视图就开始被建立了,这时候是看不到事务B对其中的修改,就算事务Bcommit以后,只要事务A不结束,它看到的结果就是它启动时刻的值。
这就与不重复提交,执行过程当中看到的结果与启动的时候看到的结果是一致的这句话对应上了。
前面说了,快照是事务的启动的时候是基于整个数据库的,而整个数据库是很大,那MYSQL是怎么让咱们无感并快速建立一个快照呢。
快照多版本你能够认为是由如下两部分构成。
当一行记录存在多个数据版本的时候,那么就有多个row trx_id 。举个例子
版本 | 值|事务ID| 对应的语句操做
v1 | score =3 | 89| --
v2 | score =5| 90| update scores set score = 5 where id =3; select score from scores where id =3;|
v3 | score = 6 | 91 | update scores set score = 6 where id =3;|
v1->v2->v3 这里面涉及了三个版本的迭代。中间是经过undo log 日志来保存更新的记录的。
注意启动快照以后,可重复读隔离状况下,获取到v1的值,不是说MYSQL直接存储的该值,而是利用如今这条记录的最新版本与undo log日志计算出来的,好比经过v3 ->v2—>v1 计算出v1中score值。
上面简单说了下版本的计算规则,可是在MYSQL中,版本并非那么简单的计算的,咱们如今来看下到底怎么计算的,
这个两点咱们在注意一下:
举个简单的例子。
a. 注意一点:获取事务ID与建立数组不是一个原子操做,因此存在事务id为8,而后又存在当前MYSQL中存在活跃事务ID为9 10的事务。
b. 事务ID低于低水位那么对于当前事务确定是可见的,事务ID高于高水位的事务ID值,则对当前事务不可见.
c. 事务ID 位于低水位与高水位之间分为两种状况。
总的上面来讲就是你在我建立的时候事务结果已经提交,那么是可见的,以后提交那么就是不可见的。
上面简单说了下老版本视图中的数据是经过最新的版本与undo log 计算出来的,那到底怎么就算的呢?
事务A | 事务B | 结果
start transaction with consistent snapshot 事务 id 89| 开启事务,并建立视图 |
--| start transaction with consistent snapshot 事务id 92 |开启事务,并建立视图
select score from scors where id =2 | -- | 事务A中的值为3.65
-- | update scores set scores = 10 where id =2 | 事务B修改成10
--| select score from scores where id =2 | 事务B显示为10
select score from scores where id =2 | --| 事务A显示为3.65
commit|---|---| 提交A的结果
仍是看这个事务操做。
下面是数据变更的流程。
select score from scors where id =2
将其看到的结果认为是v1版本数据好比其如今row trx_id(注意:row trx_id是数据行被更新后事务id才会赋值给row trx id上)是86,而且保存好。你看通过简单的几步,咱们就拿到了想要读取的事务数据,因此不论事务A何时查询,它拿到的结果都是跟它读取的数据是一致的。
你看有了MVCC(多版本并发控制)计算别的事务更改了值也不会影响到当前事务读取结果的过程。
咱们常常说不要写一个长事务,经过上面的读取流程能够看到,长事务存在时间长的话,数据版本就会有不少,那么undo log日志就须要保存很久,这些回滚日志会占用大量的内存存储空间。
当没有事务须要读取该日志与版本数据的时候,这个日志才能够删除,从而释放内存空间。
事务A | 事务B | 结果
start transaction with consistent snapshot 事务 id 89| 开启事务,并建立视图 |
--| start transaction with consistent snapshot 事务id 92 |开启事务,并建立视图
select score from scors where id =2 | -- | 事务A中的值为3.65
-- | update scores set scores = 10 where id =2 | 事务B修改成10
--| select score from scores where id =2 | 事务B显示为10
select score from scores where id =2 | --| 事务A显示为3.65
select score from scores where id =2 for update | --| 会被锁住,等待事务B释放锁(间隙锁)
-- |commit | 提交事务B
select score from scores where id =2 for update | --| 这个语句能够看到变成了10(利用了当前读)
select score from scores where id =2 | --| 不加 for update 那么结果仍是3.65
commit|---|---| 提交A的结果
上面说了读取的过程,其实在事务中,咱们还有更新流程,更新流程比较简单,更新过程咱们须要保证数据的一致性,不能说别人修改了,咱们还看不到,那样就会形成数据的不一致。
为了保证看到最新的数据,会对更新行的操做加锁(行锁),加锁以后,其余事务对行进行更新操做,必须等待其余事务commit以后才能获取到最新的值,这个过程被称为当前读。
想要读取过程当中得到最新的值可使用 上面的语句select score from scores where id =2 for update ,就能够看到当前最新值。
本小节主要梳理了事务的隔离级别,事务的MVCC多版本并发控制实现原理。
事务在面试中是比较多的一个点,这样的题目能够多种变换,咱们刚开始题目提到的三个问题已经能够解答了。
你来尝试回答下?
下期会说下数据库中的幻读,幻读也是面试中常常遇到的问题哦。