数据库事务隔离那些事儿

在高并发环境下,由于多用户同时对数据库进行读/写操作,数据的可见性和操作的原子性需要通过事务机制来保障。

下面我们通过4个典型场景来讲解数据库的事务隔离机制。

首先在Mysql数据库中创建1张表:

CREATE TABLE `account` (
  `id` int(11) NOT NULL COMMENT 'ID',
  `name` varchar(255) DEFAULT NULL COMMENT '姓名',
  `account` float(255,0) DEFAULT NULL COMMENT '账户余额',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

插入两条测试数据:

insert into account values(1, "小明", 1000);
insert into account values(1, "小强", 1000);

丢失更新

假设现在有2个线程操作account表:

        线程A                      线程B
	
读取到小明的account=1000
     
	                      读取到小明的account=1000  
							 
set account=account+200

   写回account=1200

                          set account=account+300
						        
							 写回account=1300

很明显,在这种多线程更新操作下,线程A的更新丢失了,小明本来应该收到500元,结果只收到了300元。

还我血汗钱,小明要杀程序员祭天了…

于是,聪明的程序员引入了X锁来解决更新丢失的问题。

所谓X锁,又称排他锁(Exclusive Lock)或写锁,即某线程对数据添加X锁后,则独占该数据,其他线程不能更新该数据。该线程释放X锁后,其他线程获取到X锁后才可以进行更新操作,也就是说X锁属于独占锁,比较重。

于是上述的转账操作优化为:

        线程A                      线程B
		
 获取account的X锁(成功)
	
读取到小明的account=1000
     
	                         获取account的X锁(失败)
							 
set account=account+200

   写回account=1200                 ......
   
   释放account的X锁
   
                             获取account的X锁(成功)
                	
                            读取到小明的account=1200

                            set account=account+300
						        
							   写回account=1500
                       
					           释放account的X锁

X锁优化对应的就是数据库事务隔离的最低级别Read Uncommited

即Read Uncommited可以避免丢失更新。

脏读

Read Uncommited虽然可以解决更新丢失的问题,但是X锁并不能约束其他线程并行的读取数据。

比如下述场景:

       线程A                           线程B
  
                             获取account的X锁(成功)
					 
				            读取到小明的account=1000
				   			   
				            set account=account+200
				   
				                写回account=1200 
						 
读取到小明的account=1200					 
				   
                                   Rollback	
								
					            account恢复为1000
							 
							    释放account的X锁
							   
       尴尬了!
 小明现在的数据是错误的
							   

我们用mysql模拟上述操作:

客户端A:

mysql> set session transaction isolation level read uncommitted;
mysql> start transaction;
mysql> select * from account;

在这里插入图片描述

再起一个客户端B:

mysql> set session transaction isolation level read uncommitted;
mysql> start transaction;
mysql> update account set account=account+200 where id=1

此时,客户端B的事务还未commit,通过客户端A执行select操作:

在这里插入图片描述

可以看到,客户端A的事务看到了客户端B的事务里未提交的修改数据。

此时,数据库中小明的account仍然是1000,可以起一个客户端C(未开启事务)来验证:

在这里插入图片描述

也就是说,客户端A中读取的数据与数据库中实际值不一致,出现了脏读。

出现脏读的原因主要是X锁仅对多线程的更新操作添加了约束,而对读取操作没做要求。

解决方法也就呼之欲出了,对读取操作也进行加锁呗。

那么是不是直接对读取操作也加X锁呢?

这样就太重了,而且由于X锁的独占性,当多线程环境下仅有读操作时,也需要频繁的加锁和释放锁,但实际上仅有读操作时,并发环境下并不会引发脏读(因为并没有线程更改数据嘛)。

于是,聪明的程序员引入了S锁来解决脏读的问题,同时又保证了锁的轻量性。

S锁,又称共享锁(Share Lock)或读锁,S锁与X锁的关系可以用1句话总结:

如果一个数据加了X锁,就没法加S锁;同样加了S锁,就没法加X锁。

当然,加了S锁的数据还可以继续添加S锁,因为并发读是互不影响的。

同时,在高并发环境下,为了防止单个线程长时间被S锁锁住,故有如下约定:

读数据前添加S锁,读完之后立即释放。

添加S锁机制之后,上面的流程优化如下:

      线程A                           线程B
  
                             获取account的X锁(成功)
					 
				            读取到小明的account=1000
				   			   
				            set account=account+200
				   
				                写回account=1200 
						 
  获取account的S锁(失败)				 
				   
                                    Rollback	
								
	    ......				    account恢复为1000
							 
						 	     释放account的X锁
							   
  获取account的S锁(成功)
 
 读取到小明的account=1000	
							   

很明显,S锁限制了读时写和写时读,只有当写线程commit释放X锁之后,读线程才能获取到S锁完成数据的读取。

这种只能更新数据commit之后,才能读取到最新数据的事务隔离级别称为Read Committed

即Read Committed可以避免脏读。

不可重复读

在Read Committed事务隔离级别下,我们为了防止高并发环境下读线程长时间被锁住,做了以下规定:

读数据前添加S锁,读完之后立即释放。

此时,会出现以下问题:

        线程A               线程B

 获取account的S锁(成功)
 
 读取到的account=1000
 
   释放account的S锁
   
                           获取account的X锁(成功)
	
	                      set account=account+200
	做其他事情...                       
	                          写回account=1200 
							  
						      释放account的X锁
	
 获取account的S锁(成功)
 
重新读取到的account=1200
  
        What?
   与之前读的不一样了?
  

此时,在同一个事务中重新读取的数据发生了变化,即不可重复读。

同样用mysql数据库演示上述过程:

客户端A:

mysql> set session transaction isolation level read committed;
mysql> start transaction;
mysql> select * from account;

在这里插入图片描述

此时再起一个客户端B:

mysql> set session transaction isolation level read committed;
mysql> start transaction;
mysql> update account set account=account+200 where id=1;
mysql> select * from account;
mysql> commit;

在这里插入图片描述
此时,在客户端A的事务中继续查询:

在这里插入图片描述

故客户端A同一个事务中小明的account出现了2个不同的值,即出现了不可重复读。

而解决不可重复读的方法也很简单,把S锁的规定升级一下即可:

读数据前添加S锁,事务提交之后才可以释放。

此时,上面的流程变为:

        线程A               线程B

 获取account的S锁(成功)
 
 读取到的account=1000
   
                             获取account的X锁(失败)
	                  
	做其他事情...                                	                        					
 获取account的S锁(成功)
                                
  读取到的account=1000              ......
  
       提交事务
	   
   释放account的S锁
 
 					        获取account的X锁(成功)
 
 					     set account=account+200
                  
 						    写回account=1200 
 						
 						     释放account的X锁 

此时对应的数据库事务隔离级别即为Repeatable Read

Repeatable Read解决了不可重复读的问题。

幻读

通过X锁和S锁的组合应用,我们解决了数据的更新丢失、脏读、不可重复读3个问题,但由于X锁和S锁仅是对数据的更新(修改)和读取进行了限制,而对数据的添加和删除未做限制,那么即使在Repeatable Read隔离级别下,仍然会出现如下问题:

         线程A                        线程B
		
   获取数据的S锁(成功)
   
     查询account表
   [(1, "小明", 1000)
   (2, "小强", 1000)]
	  
	 做其他事情...            插入数据(3, "小花", 1000)  
	   
	                                 提交
							   
  插入数据(3, "小花", 1000)  
  
 报错:'3' for key 'PRIMARY'	
						 
	查询account表
	[(1, "小明", 1000)
	(2, "小强", 1000)]	
								
         What?
 这哪里有id为3的数据,眼花了?
				

线程B的插入操作让线程A出现了幻觉,所以该种异常称之为幻读。

同样用mysql数据库演示上述过程:

客户端A:

mysql> set session transaction isolation level repeatable read;
mysql> start transaction;
mysql> select * from account;

此时再起一个客户端B:

mysql> set session transaction isolation level repeatable read;
mysql> start transaction;
mysql> insert into account values(3, "小红", 1000);
mysql> select * from account;
mysql> commit;

在这里插入图片描述

此时,客户端A在事务中继续执行:

mysql> insert into account values(3, "小阁", 1500);
mysql> select * from account;

在这里插入图片描述

还有一种幻读,指的是:

         线程A                        线程B
		
   获取数据的S锁(成功)
   
  查询account表中的人数
        返回2
	  
	 做其他事情...            插入数据(3, "小花", 1000)  
	   
	                                 提交

  查询account表中的人数
        返回3								
       
	   What?
    刚才还是2的?				

只是MySQL的InnoDB引擎默认的Repeatable Read级别已经通过MVCC自动帮我们解决了,所以该级别下, 我们也模拟不出该种幻读的场景。

至于MVCC是啥,后面抽空再聊,哈哈…

说实话,幻读和不可重复读很容易混淆:

  • 不可重复读,主要是说在同一事务中多次读取一条记录, 发现该记录中某些列值被修改过;
  • 幻读,主要是说在同一事务中多次读取一个范围内的记录(包括查询所有结果或者聚合统计)、插入时,发现结果不一致。

解决幻读,只能放出我们的终极大招了,对整个事务加X锁,将事务的执行串行化,对应的数据库事务隔离级别为Serializable

即Serializable解决了幻读的问题。

总结

  • Read Uncommitted通过X锁来实现,锁住数据更新的阶段;
  • Read Committed通过X锁和S锁来实现,且读完即释放S锁;
  • Repeatable Read通过X锁和S锁来实现,事务提交之后释放S锁;
  • Serializeable通过X锁来实现,锁住整个事务。
隔离级别丢失更新脏读不可重复读幻读
Read UncommittedNoYesYesYes
Read CommittedNoNoYesYes
Repeatable ReadNoNoNoYes
SerializeableNoNoNoNo
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值