数据库事务与隔离级别

     事务:要么什么都做,要么都不做,没有中间状态。 All or Nothing
    MySql是一个支持多引擎的系统,但是并不是所有的引擎都支持事务。比如MyISAM引擎就不支持事务,这也是MyISAM被InnoDB取代的重要原因之一。

1、数据库事务特性

事务特性
 特性 描述
原子性
Atomicity
目标:All or Nothing. 整个事务中的所有操作,要么全部完成,要么全部不做,没有中间状态。对于事务在执行中发生错误,所有的操作都会被回滚,整个事务就像从没被执行过一样;
实现原理:undo log回滚日志
一致性
Consistency
目标:AID都是为了保证数据的一致性,有时候事务的一致性还需要业务方一起配合才能保证。一致性是指事务必须使数据库 从一个一致性状态变换到另一个一致性状态。即:一个事务执行之前和执行之后都必须处于一致性状态。
实现原理:通过数据库层面AID和应用层面共同来实现;
  • 数据库方面:通过原子性/隔离性/持久性来共同保障一致性;
  • 应用层面支持:类似转账,如果只是给a扣了钱,b账户没有增加,数据库设计的再好也无法完成一致性;
转账实例:A有500元,B有300元,如果在一个事务里A成功给B转账50元,那么不管并发多少,不管发生什么,只要事务执行成功了,那么最后A账户一定是450元,B账户一定是350元。
隔离性
Isolation
目标:保证事务之间的执行不受其他的影响,一个事务的中间状态不会被其他事务感知。
实现原理:
  • 一个事务的写操作对另一个事务的读操作影响:通过 MVCC并发版本控制, undo log版本和一致性视图实现;
  • 一个事务的写操作对另一个事务的写操作影响:通过锁机制来实现;
持久性
Durability
目标:一个事务一旦被提交了,那么 对数据库中的数据的改变就是永久性的,已经写入磁盘,即便是在数据库系统遇到故障的情况下也不会丢失提交事务的操作, 即使发生停电,系统宕机也是如此。
实现原理:二阶段提交:redo log + binlog状态一致;

2、数据库事务的并发问题

下面通过一个具体的例子来了解四种隔离级别对取值的影响,假设x的初始值为1:
 
事务session-a
事务session-b
t1
启动事务
查询得到x值 1
启动事务
t2
 
查询得到x值 1;
t3
 
将x的值1改为 2;
t4
查询得到x值 v1
 
t5
 
提交事务b
t6
查询得到x值 v2
 
t7
提交事务
 
t8
查询得到x值 v3
 
数据读取结果分析:
  • 读未提交隔离:事务a在t4时间读取的值v1是: 2。虽然事务b未提交,但是对事务a已经可见了。v2和v3的值也都是2;
  • 读已提交:t4时间v1的值是1。v2和v3的值是2;只有事务b提交后新值才能被事务a看到;
  • 可重复读:t4时间v1和v2的值是1,t8时间v3的值是2;事务在启动到结束期间的数据必须保持一致,通过mvcc和视图来实现;
  • 串行化:如果是事务b先启动,那么在执行期间,会加锁,事务a需要等到事务b执行完后才可以执行。
由此可见不同的隔离级别获取的数据库的值是不一样的,出现的问题也不一样。
隔离级别下并发问题

2.1 脏读

对于读未提交隔离级别出现的脏读:
 
事务session-a
事务session-b
t1
启动事务
查询得到x=1
启动事务
t2
 
查询得到值x=1
t3
 
update值x+1=2;
t4
查询得到值 x=2
 
t5
 
回滚事务
t6
业务处理
 
t7
提交事务
   
结果分析:如上图所示,如果事务b在更新完x的值后又回滚了, 此时事务a在 t4时刻便发生了脏读x=2 ,无效数据。
解决方案: 将隔离级别调整为读一提交,这样没有提交的数据就不会被其他的事务读取到,避免了脏读的问题;

2.2 不可重复读

读已提交出现的不可重复读问题:
 
事务session-a
事务session-b
t1
启动事务
查询得到 x=1
启动事务
t2
 
查询得到值x=1
t3
 
update值x+1=2;
t4
 
事务提交
t5
查询得到 x=2
 
t6
业务处理
 
t7
提交事务
   
  结果分析:对于不可重复读和脏读的区别也比较明显:不可重复读主要是因为事务b执行的比较快,在事务a之前提交了update,但是事务a在t1和t5再次读取x的值,同一个事务前后的数据不一致。虽然不一致,但是数据有效,有没有问题需要根据具体的情况而定。
解决方案: 将隔离级别调整为可重复读;通过加锁和并发版本控制MVCC来创建一个开启事务时的数据库快照,记录开启事务时的全局事务id执行情况,之后其他的事务执行都与我无关,我不care,也称为快照读,以此来解决不可重复读的问题。

2.3 幻读

指在可重复读级别下,由于并发情况下“ 插入行记录”的影响,只有在 “当前读”的情况下才会出现。
实验表结构
CREATE TABLE `USER` (
  `id` int(11) NOT NULL,
  `num` int(11) ,
  `orderId` bigint NOT NULL,
  PRIMARY KEY (`id`),
  KEY `orderId` (`orderId`)
) ENGINE=InnoDB 

插入数据sql:

insert into USER (id, num, orderId) values (0,0,10),(1,1,11),(2,2,12),(3,3,13),(4,4,14);

 

 
事务session-a
事务session-b
session-c
t1
begin:
(1)select * from USER 
where num = 4 for update;
结果:(4,4,14);
(2)update   USER set orderId = 4
where num = 4;(binlog日志的一致性问题)
 
 
t2
 
update  USER set num = 4
where id = 3;
 
 
begin:
select * from USER 
where num = 4 for update;
结果:(4,4,14),(3,4,13);
 
 
t4
 
 
insert into USER values(5,4,15);
t5
begin:
select * from USER 
where num = 4 for update;
结果:(4,4,14),(3,4,13),(5,4,15);
 
 
t6
commit;
 
 
结果分析:
  • 由于加了for update,所以都是“当前读”;
  • 由于幻读只是针对新插入的行记录,所以session-b的修改不是幻读,session-c的插入才是幻读;
幻读主要的影响是:
  • (1) 它破坏了加锁的语义;例如session-a的(1)本想锁住num=4的这行,但是其实只锁住了一行;
  • (2) 导致了数据一致性的问题;
        这个一致性主要是因为binlog和数据库里的数据的不一致造成的。例如session-a的(2)由于在t6时刻才提交事务,在事务b-c之后,所以生成的binlog在从库中将所有的数据都更改了;
解决幻读方案:
        这里主要采用了 next-key lock 临键锁【行锁和间隙锁合称为next-key lock】 和 gap lock[间隙锁]来解决这个问题。
        tips:但是注意间隙锁也会有新的问题就是“死锁”问题。如果两个事务插入一行相等的数据,有可能产生死锁问题。 因为间隙锁互斥的是插入数据的操作,而不是增查改删

3、数据库隔离级别

  • 2.1). 读未提交(read uncommit)一个事物还未提交,它做的变更就能被别的事务看到,直接返回内存记录的最新值;
  • 2.2).提交(read commit):一个事务提交后,它做的变更才会被其他事务看到;视图是在执行的时候创建的(oracle默认);
  • 2.3).可重复读(repeatable read):一个事务在执行过程中看到的数据,总是跟这个事务在刚启动时看到的数据是一致的。未提交变更对其也是不可见的。主要用数据库的视图来实现,在事务启动的时候创建的,该视图是静态的,不受其他事务的影响;(Mysql默认的级别)。
  • 2.4).行化(serializable):读和写都会加锁,当有冲突的时候,事务必须列队等待执行,通过加锁来避免并行访问;
官网Innodb隔离级别解释资料参考: https://dev.mysql.com/doc/refman/5.7/en/innodb-transaction-model.html
查看当前数据库的隔离级别:
show variables like 'transaction_isolation';

3.1、Innodb对事务隔离级别的支持

事务隔离级别
脏读
不可重复读
幻读
未提交读(Read Uncommitted)
可能
可能
可能
已提交读(Read Committed)
不可能
可能
可能
可重复读(Repeatable Read)
不可能
不可能
对Innodb不可能
串行化(Serializable)
不可能
不可能
不可能
结论:隔离级别越高,事务的并发度越低。Innodb默认采用RR作为事务隔离级别的原因,既保证了数据的一致性,又支持较高的并发度。

3.2、Innodb事务隔离的实现方案

那么如果要解决读一致性的问题,保证一个事务中前后两次读取的数据结果一致,实现事务隔离,应该怎么做呢?
总体来说有两大类方案:
  • 第一种:加锁。在读取数据之前,对要操作的数据进行加锁,阻止其他事务对数据进行修改访问;
  • 第二种: 多版本的并发控制 Multi Version Concurrency Control (MVCC)。如果要让一个事务前后两次读取的数据保持一致, 那么我们可以在修改数据的时候给它建立一个备份或者叫快照,后面再来读取这个快照就行了,快照之后的新事务对数据的更新操作我不关心,只是记录一个日志就OK了。
但是第一种,如果仅仅是基于锁来实现事务隔离,一个事务读取的时候不允许其他时候修改,那 就意味着不支持并发的读写操作,而我们的大多数应用都是读多写少的,这样会极大地影响操作数据的效率。所以为了性能考虑,又有了第二种MVCC的并发控制方案。
需要注意, 在 InnoDB 中,MVCC 和锁是协同使用的,这两种方案并不是互斥的。
实现方案小结
事务隔离级别
实现方案
问题
读未提交RU
 
不加锁
读数据:直接返回内存记录 [InnoDB 的buffer pool]的最新值,没有视图的概念;
 
脏读
读已提交RC
(1) 普通的select都是快照读,使用MVCC版本控制+Undo Log;
(2) 加锁的select都会加行锁Record lock来保证数据安全性;
 
 
不可重复读
可重复读RR
(1) 普通的 select 使用 快照读(Snapshot Read),底层使用MVCC来实现。
(2) 加锁的 select(select ... in share mode / select ... for update)以及更新操作 update, delete等语句使用 当前读(Current Read),底层使用加锁控制:
  • 行锁:Record lock;
  • 间隙锁:Gap lock;
  • 临键锁:Next-key lock(解决幻读);
幻读
串行化
一把大锁串行 实现串行化,所有的select都会和update,delete互斥。
并发度太低

4、MVCC的实现

MVCC核心思想: 一旦我开启一个事务,我看到的数据就是我开启事务时候的数据,之后的插入和删除事务操作,我查不到。
思考: 这个快照什么时候创建?读取数据的时候,怎么保证能读取到这个快照没有更新后的新数据呢?
这里就用到了全局事务id,InnoDB 为每行记录都实现了两个隐藏字段:
  • 全局事务ID (DB_TRX_ID): 记录插入或更新行的最后一个事务的事务ID1,事务编号是自动递增的,之后的更新事务ID都大于这个事务ID1,同时也避免了表数据的拷贝备份;
  • 回滚指针( DB_ROLL_PTR): (我们把它理解为删除版本号,数据被删除或记录为旧数据的时候,记录当前事务ID),也就是Undo log。
在实现上,数据库里面会创建一个视图,访问的时候以视图的逻辑结果为准。以“可重复读”为例:
     这里在快照读的一致性视图中,每条记录在更新的时候都会同时记录一条回滚操作 undo log。通过回滚操作,我们可以得到最新值的前一个状态。例如1被顺序改为了2-3-4,在回滚日志中,我们都会记录这样一个操作。当前值是1,但是在查询的时候每个事务会有自己的视图,read-view。图中,变量x的值在A-B-C三个视图中的值分别为1-2-4。对于read-view C,如果事务A-B都commit了,此时想要读取x=1的值,就必须通过执行所有的回滚日志才能得到x=1的值,且A和B的视图也不会删除,因为有比A-B更早创建的视图C依赖它们, 引来的问题就是占用内存,尤其是长事务
     这就是数据库中多版本并发控制MVCC,通过 Undo Log和全局事务T-ID来实现一致性视图。

5、数据库隔离级别的选择

RU和Serializable肯定不能用。
RU虽然并发度高,但是数据安全得不到保障,这是最基本的保证,不可放弃。
Serializable虽然安全性能保证,但是并发度低;
 
思考: 为什么有些公司推荐用RC呢,主要有以下好处?
RC隔离级别的额外好处: https://dev.mysql.com/doc/refman/5.7/en/innodb-transaction-isolation-levels.html
Using READ COMMITTED has additional effects:
* For UPDATE or DELETE statements, InnoDB holds locks only for rows that it updates or deletes. 
Record locks for nonmatching rows are released after MySQL has evaluated the WHERE condition. 
This greatly reduces the probability of deadlocks, but they can still happen.
* For UPDATE statements, if a row is already locked,
 InnoDB performs a “semi-consistent” read, returning the latest committed version to MySQL 
so that MySQL can determine whether the row matches the WHERE condition of the UPDATE. 
If the row matches (must be updated), 
MySQL reads the row again and this time InnoDB either locks it or waits for a lock on it.
RC和RR相比的几个优点:
  • 1. 条件列未命中索引的查询,RR锁表,RC锁行, 提高update和delete的并发度
  • 2. RC的“半一致性”(semi-consistent)读可以增加update操作的并发性。在RC中,对于一个update语句,如果读到一行已经加锁的记录,此时InnoDB返回记录最近提交的版本,由MySQL上层判断此版本是否满足update的where条件。若满足(需要更新),则MySQL会重新发起一次读操作,此时会读取行的最新版本(并加锁)。
  • 3. RR的间隙锁会导致锁定范围的扩大,增加了死锁的风险;
使用RC缺点:
  • 1、RC的隔离级别需要注意不可重复读,比如在金融对账,历史账单的校验的时候,容易出现数据不一致的情况,这个也是要根据具体的应用场景来选择。
  • 2、注意binlog和主库的数据逻辑不一致性,一般设置对提交的日志格式为: binlog _format=Mixed,或者row格式。
实际上,如果能够正确地使用锁(避免不使用索引去加锁),只锁定需要的数据, 用默认的RR级别就可以了。 这也是Inndb的默认隔离级别:RR,可重复读。

6、小结

    数据库会比对安全性和效率问题:一般不会采用read uncommitted 和 serializable。例如Innodb的默认级别是RR,如果没有特殊要求,RC级别在保证数据安全性的前提下还可以提供更高的并发请求。
  • 安全性: read uncommitted   <  read committed  < repeatable read  < serializable
  • 效率:     read uncommitted  >  read committed  > repeatable read  > serializable
OK---人面不知何处去,桃花依旧笑春风。
 
 
 
水滴石穿,积少成多。学习笔记,内容简单,用于复习,梳理巩固。
 
 
资料参考:
《Innodb存储引擎》
 
 
 
 
 
 
 
 
 
  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值