5-06 深入理解MySQL事务隔离级别与锁机制
2023-01-04 17:47:27 0 举报
MySQL事务隔离级别定义了多个事务并发执行时的可见性和影响,包括读未提交、读已提交、可重复读和串行化。锁机制是实现事务隔离的关键,MySQL提供了共享锁(读锁)和排他锁(写锁),用于保护数据的一致性。当一个事务对数据进行修改时,会加排他锁,其他事务只能等待或读取;在事务结束后释放锁。不同隔离级别下,锁的粒度不同,可能导致脏读、不可重复读和幻读等问题。通过合理设置事务隔离级别和锁策略,可以在保证数据一致性的同时提高并发性能。
作者其他创作
大纲/内容
<b>引子</b>
<b>1.</b> 在<font color="#f57c00"><b>数据库</b></font>的应用中,会存在一些<font color="#f57c00"><b>常见的场景</b></font>:
1)数据库一般都会<font color="#f44336">并发执行多个事务</font>
2)多个事务可能<font color="#f44336">并发的对相同的一批数据进行增删改查</font>
<b>2.</b> 在以上这种<b><font color="#f57c00">并发</font></b>的场景下,就可能会导致出现一些<b><font color="#f57c00">问题</font></b>:
<b>1. 脏写(或更新丢失)</b>
指:更新丢失,后更新覆盖了其他更新
<b>2. 脏读</b>
指:一个事务,读取了另一个事务中没有提交的数据,会在本事务中产生的数据不一致的问题
<b>3. 不可重复读</b>
指:一个事务中,多次读取相同的数据, 但是读取的结果不一样, 会在本事务中产生数据不一致的问题
<b>4. 幻读</b>
指:一个事务中,多次对数据进行整表数据读取(统计),但是结果不一样, 会在本事务中产生数据不一致的问题<font color="#e57373">(此现象与不可重复读相似)</font>
<b style=""><font color="#f57c00">不可重复读和幻影读的区别:</font></b><font color="#9e9e9e">(可参看:“Spring声明式事务”笔记或本笔记的下面的内容)</font>
<b>1)</b>两者问题相似
<b>2)不可重复读</b>:<font color="#2196f3">只需要<b>锁行</b></font>,且重点在于<b><font color="#f57c00">update</font></b>和<b><font color="#f57c00">delete</font></b>操作
<b>3)幻读</b>:<font color="#2196f3">需要<b>锁表</b></font>,且重点在于<b><font color="#f57c00">insert</font></b>操作
<b>3.</b> 针对这些数据库的<b><font color="#f57c00">多事务并发问题</font></b>,<b><font color="#f57c00">设计了一整套机制</font></b>来解决:
<b>1. 事务隔离机制</b>
<b>2. 锁机制</b>
<b>3. MVCC多版本并发控制隔离机制</b>
<b>事务及其属性</b>
<b>事务是</b>由一组SQL语句组成的<b><font color="#2196f3">逻辑处理单元</font></b>
事务具有以下4个属性,通常简称为<b>事务的<font color="#2196f3">ACID</font>属性</b>
<b><font color="#2196f3">原子性(</font><font color="#f57c00">A</font><font color="#2196f3">tomicity)</font> </b>:
<b>事务是<font color="#f57c00">一个原子操作单元</font></b>,其对数据的修改,<b><font color="#4caf50">要么全都执行</font></b>,<b style=""><font color="#e57373">要么全都不执行</font></b>
<b><font color="#2196f3">一致性(</font><font color="#f57c00">C</font><font color="#2196f3">onsistent)</font> </b>:
<b>在事务开始和完成时</b>,<b><font color="#4caf50">数据都必须保持一致状态</font></b>
这意味着<b>所有相关的数据规则</b>都必须应用于<b><font color="#f57c00">事务的修改</font></b>,以保持<b><font color="#4caf50">数据的完整性</font></b>
<b><font color="#2196f3">隔离性(</font><font color="#f57c00">I</font><font color="#2196f3">solation) </font></b>:
数据库系统提供<b>一定的隔离机制</b>,<font color="#4caf50"><b>保证事务</b></font>在不受外部并发操作影响的<b><font color="#4caf50">“独立”环境执行</font></b>
这意味着<b>事务处理过程中</b>的<b><font color="#f44336">中间状态</font><font color="#f57c00">对外部是</font><font color="#f44336">不可见</font><font color="#f57c00">的</font></b>,反之亦然
<b><font color="#2196f3">持久性(</font><font color="#f57c00">D</font><font color="#2196f3">urable) </font></b>:
<b>事务完成之后</b>,它<b><font color="#4caf50">对于数据的修改是永久性的</font></b>,即使出现系统故障也能够保持
<b>并发事务处理带来的问题</b><br>
<b><font color="#e57373">更新丢失(Lost Update)或脏写</font></b>
<b>1. </b>当两个或多个事务选择同一行,然后基于最初选定的值更新该行时,
<b>2. </b>由于每个事务都不知道其他事务的存在,就会发生丢失更新问题
这种现象就叫做“更新丢失或脏写”
<font color="#e57373"><b>脏读(Dirty Reads)</b></font><br>
<b>1. </b>一个事务正在对一条记录做修改,在这个事务完成并提交前,这条记录的数据就处于不一致的状态,
<b>2.</b> 这时,另一个事务也来读取同一条记录,如果不加控制,第二个事务读取了这些“脏”数据,并据此作进一步的处理,就会产生未提交的数据依赖关系
这种现象被形象的叫做“脏读”
<font color="#e57373">不可重读(Non-Repeatable Reads)</font>
<b>1. </b>一个事务在读取某些数据后的某个时间,再次读取以前读过的数据,<br>
<b>2.</b> 却发现其读出的数据已经发生了改变、或某些记录已经被删除了!
这种现象就叫做“不可重复读”
<font color="#e57373"><b>幻读(Phantom Reads)</b></font><br>
<b>1. </b>一个事务按相同的查询条件重新读取以前检索过的数据,
<b>2. </b>却发现其他事务插入了满足其查询条件的新数据!
这种现象就称为“幻读”
<b>事务隔离级别</b>
“<b><font color="#e57373">脏读</font></b>”、“<b><font color="#e57373">不可重复读</font></b>”和“<b><font color="#e57373">幻读</font></b>”,其实<b><font color="#f44336">都是数据库读一致性问题,</font></b>必须通过<b>数据库</b>提供的<b><font color="#2196f3">事务隔离机制</font></b>来<b>解决</b>
如下所示:
<b>数据库的事务隔离越严格:</b>
1. <font color="#4caf50">并发副作用越小</font>
2. <font color="#f44336">但付出的代价也就越大</font>
因为事务隔离实质上就是使事务在一定程度上“<font color="#f44336">串行化</font>”进行,<font color="#f44336">这显然与“并发”是矛盾的</font>
<b>结合应用场景采用更适合的隔离级别:</b>
<b>不同的应用</b>对<font color="#f57c00">读一致性</font>和<font color="#f57c00">事务隔离程度</font>的<b>要求是不同的</b>
<b>查看</b>当前<b>数据库</b>的<b>事务隔离级别</b>:
<b><font color="#9c27b0">mysql> </font><font color="#0000ff">show variables like '</font><font color="#4caf50">tx_isolation</font><font color="#0000ff">';</font></b>
<b>设置数据库</b>的<b>事务隔离级别</b>:
<b><font color="#9c27b0">mysql></font> <font color="#0000ff">set tx_isolation='</font><font color="#4caf50">REPEATABLE-READ</font><font color="#0000ff">';</font></b>
<b>MySQL</b><font color="#f57c00"><b>默认的事务隔离级别</b></font>是<font color="#2196f3"><b>可重复读</b></font>,用Spring开发程序时,<font color="#ff9800"><b>如果Spring设置了就用已经设置的隔离级别</b></font>
<b>数据库锁机制</b>
<b>1. 锁详解</b>
<b>锁是</b>计算机<font color="#f57c00">协调</font><font color="#00bcd4">多个进程或线程并发访问某一资源</font>的机制<br>
在数据库中,除了传统的计算资源(如CPU、RAM、I/O等)的争用以外,<font color="#f57c00">数据也是一种供需要用户共享的资源</font>
如何保证数据并发访问的一致性、有效性是所有数据库必须解决的一个问题,<font color="#2196f3">锁冲突</font><font color="#f57c00">也是影响数据库并发访问性能的一个重要因素</font>
2. 锁分类
<font color="#f57c00"><b>从性能上</b></font>分为<font color="#2196f3"><b>乐观锁</b></font>(用版本对比来实现)和<font color="#2196f3"><b>悲观锁</b></font>
<font color="#f57c00"><b>从对数据库操作的类型</b></font>分,分为<font color="#2196f3"><b>读锁</b></font>和<font color="#2196f3"><b>写锁</b></font>(都属于悲观锁)<br>
<b>读锁(共享锁,S锁(Shared))</b>:针对同一份数据,多个读操作可以同时进行而不会互相影响<br>
<b>写锁(排它锁,X锁(eXclusive))</b>:当前写操作没有完成前,它会阻断其他写锁和读锁<br>
<font color="#f57c00"><b>从对数据操作的粒度</b></font>分,分为<font color="#2196f3"><b>表锁</b></font>和<font color="#2196f3"><b>行锁</b></font>
<font color="#2196f3"><b>表锁</b></font>
每次操作<font color="#f57c00">锁住整张表,</font><font color="#2196f3">一般用在整表数据迁移的场景</font>
<b>特点:</b>
<font color="#4caf50">开销小,加锁快;</font>
<font color="#4caf50">不会出现死锁;</font>
<font color="#e57373">锁定粒度大,发生锁冲突的概率最高,并发度最低</font>
<b>基本操作:</b><br>
<b>1. 示例表:</b>
<b>2. 手动增加表锁:</b>
<b><font color="#0000ff">lock table </font><font color="#4caf50">表名称</font><font color="#0000ff"> read(write),</font><font color="#4caf50">表名称2</font><font color="#0000ff"> read(write);</font></b><br>
<b>3. 查看表上加过的锁:</b>
<b><font color="#0000ff">show open tables;</font></b>
4. 删除表锁:
<b><font color="#0000ff">unlock tables;</font></b>
<b>案例分析(加读锁):</b>
如下所示:
<font color="#4caf50"><b>当前session</b>和<b>其他session</b>都<b>可以读</b>该表</font>
<b>案例分析(加写锁):</b>
<b style="color: rgb(229, 115, 115);">当前session</b><font color="#e57373">中</font><b style="color: rgb(229, 115, 115);">插入或者更新锁定的表都会报错</b><font color="#e57373">,</font><font color="#f57c00"><b style="">其他session</b>插入或更新则<b>会等待</b> </font>
如下所示:
<font color="#4caf50"><b>当前session</b>对该表的<b>增删改查都没有问题</b></font>,<font color="#e57373"><b>其他session</b>对该表的<b>所有操作被阻塞</b></font>
<b>案例结论:</b>
1、<b>对MyISAM表的读操作(加读锁)</b><br>
<font color="#4caf50"><b>不会阻寒</b>其他进程对同一表的<b>读</b>请求,</font><b>但</b><font color="#e57373"><b>会阻赛</b>对同一表的<b>写</b>请求</font>
<font color="#f57c00">只有<b>当读锁释放后</b>,才会<b>执行其它进程</b>的<b>写</b>操作</font>
2、<b>对MylSAM表的写操作(加写锁)</b> ,<font color="#e57373"><b></b></font>
<font color="#e57373"><b>会阻塞</b>其他进程对同一表的<b>读</b>和<b>写</b>操作</font>
<font color="#f57c00">只有<b>当写锁释放后,</b>才会执行其它进程的读写操作</font>
<b><font color="#2196f3">行锁</font></b>
每次操作<font color="#f57c00">锁住一行数据</font>
<b>特点:</b>
<font color="#e57373">开销大,加锁慢;</font>
<font color="#e57373">会出现死锁;</font>
<font color="#4caf50">锁定粒度最小,发生锁冲突的概率最低,并发度最高</font>
<b><font color="#f57c00">InnoDB与MYISAM的最大不同有两点:</font></b>
<b>InnoDB</b><font color="#4caf50">支持事务(TRANSACTION)</font><br>
<b>InnoDB</b><font color="#4caf50">支持行级锁</font>
<b>行锁演示:</b>
<b>一个session</b>开启<font color="#f57c00">事务更新不提交</font>,<b>另一个session</b>更新<font color="#e57373">同一条记录会阻塞</font>,更新<font color="#4caf50">不同记录不会阻塞</font>
<b><font color="#2196f3">总结</font></b>
<b>MyISAM</b>在<font color="#f57c00"><b>执行</b>查询语句SELECT<b>前</b></font>,<font color="#f57c00">会自动给<b>涉及的所有表加读锁</b></font>,在<font color="#f57c00">执行<b>update、insert、delete操作</b></font>会自动给<b><font color="#f57c00">涉及的表加写锁</font></b><br>
<b>InnoDB</b>在<font color="#f57c00"><b>执行</b>查询语句SELECT<b>时</b></font>(非串行隔离级别),<font color="#f57c00"><b>不会加锁</b></font>。但是<font color="#f57c00"><b>update、insert、delete操作</b>会<b>加行锁</b></font><br>
<font color="#9e9e9e">简而言之</font>,就是<font color="#e57373">读锁会阻塞写</font>,但是<font color="#4caf50">不会阻塞读</font>。而<font color="#e57373">写锁则会把读和写都阻塞</font>
<b>行锁与事务隔离级别</b>
<b>示例表:</b>
<b>事务隔离级别:</b>
<b><font color="#2196f3">读未提交:</font></b>
(1)打开一个客户端A,并设置当前事务模式为read uncommitted(未提交读),查询表account的初始值:
如下所示:
(2)在客户端A的事务提交之前,打开另一个客户端B,更新表account;
如下所示:
(3)这时,虽然客户端B的事务还没提交,但是客户端A就可以查询到B已经更新的数据:
如下所示:
(4)一旦客户端B的事务因为某种原因回滚,所有的操作都将会被撤销,那客户端A查询到的数据其实就是脏数据:
如下所示:
(5)在客户端A执行更新语句update account set balance = balance - 50 where id = 1
lilei 的 balance 没有变成350,居然是400,是不是很奇怪,数据不一致
在应用程序中,我们会用400-50=350,并不知道其他会话回滚了
要想解决这个问题可以采用“读已提交”的隔离级别
如下所示:
<b><font color="#2196f3">读已提交:</font></b>
1)打开一个客户端A,并设置当前事务模式为read committed(读已提交),查询表account的所有记录:
如下所示:
2)在客户端A的事务提交之前,打开另一个客户端B,更新表account:
如下所示:
3)这时,客户端B的事务还没提交,客户端A不能查询到B已经更新的数据,解决了脏读问题:
如下所示:
4)客户端B的事务提交:
如下所示:
5)客户端A执行与上一步相同的查询,结果与上一步不一致,即产生了不可重复读的问题
如下所示:
<b><font color="#2196f3">可重复读:</font></b>
1)打开一个客户端A,并设置当前事务模式为repeatable read,查询表account的所有记录
如下所示:
2)在客户端A的事务提交之前,打开另一个客户端B,更新表account并提交
如下所示:
3)在客户端A查询表account的所有记录,与步骤(1)查询结果一致,没有出现不可重复读的问题
如下所示:
4)在客户端A,接着执行update account set balance = balance - 50 where id =1
balance 没有变成400-50=350,李磊的balance值用的是步骤2中的350来算的,所以是300,数据的一致性倒是没有被破坏
可重复读的隔离级别下使用了MVCC(multi-version concurrency control)机制,select 操作不会更新版本号,是快照读(历史版本);insert、update和 delete的会更新版本号,是当前读(当前版本)。
如下所示:
5)重新打开客户端B,插入一条新数据后提交
如下所示:
6)在客户端A查询表account的所有记录,没有查出新增数据,所以没有出现幻读
如下所示:
7)验证幻读
在客户端A执行update account set balance=888 where id = 4;能更新成功,再次查询能查到客户端B新增的数据
如下所示:
<font color="#2196f3"><b>串行化:</b></font>
1)打开一个客户端A,并设置当前事务模式为serializable,查询表account 的初始值:
如下所示:
2)打开一个客户端B,并设置当前事务模式为serializable,更新相同的id为1的记录会被阻塞等待,更新id为2的记录可以成功,说明在串行模式下innodb的查询也会被加上行锁。
如果客户端A执行的是一个范围查询,那么该范围内的所有行包括每行记录所在的间隙区间范围(就算该行数据还未被插入也会加锁,这种是间隙锁)都会被加锁。此时如果客户端B在该范围内插入数据都会被阻塞,所以就避免了幻读。
这种隔离级别并发性极低,开发中很少会用到
如下所示:
<b>行锁:</b>
<b><font color="#2196f3">间隙锁(Gap Lock)</font></b>
间隙锁,锁的就是<font color="#2196f3">两个值之间的空隙</font>
<font color="#9e9e9e">Mysql默认级别是repeatable-read</font>,<font color="#f57c00">有办法解决幻读问题吗?</font>
间隙锁在某些情况下可以解决幻读问题
假设account表里数据如下:
那么间隙就有id为(3,10),(10,20),(20,正无穷)这三个区间
在session_1下面执行update account set name = 'test' where id > 8 and id < 18;
则其他session没法在这个范围所包含的所有行记录(包括间隙行记录)以及行记录所在的间隙里插入或修改任何数据
<b><font color="#2196f3">临键锁</font></b>
<b>Next-key Locks</b>是<font color="#2196f3">行锁与间隙锁的组合</font>
像上面那个例子里的这个<font color="#f57c00">(3,20]的整个区间</font>可以叫做<font color="#2196f3">临键锁</font>
<b><font color="#2196f3">无索引行锁会升级为表锁</font></b>
<font color="#f44336">锁主要是加在索引上</font>,<font color="#f57c00">如果对非索引字段更新,行锁可能会变成表锁</font>
<font color="#000000"><b style="">session1</b> </font><font color="#f57c00">执行</font>:udpate account set balance = 800 where name = 'lilei';
<b>session2 </b><font color="#e57373">对该表任一行操作都会阻塞</font>
I<b>nnoDB的行锁</b>
<font color="#2196f3">是针对索引加的锁</font>,<font color="#e57373">不是针对记录加的锁</font>
并且该索引不能失效,否则都会从行锁升级为表锁
<b>锁定某一行</b>还可以用<font color="#2196f3">lock in share mode(共享锁)</font>和<b><font color="#2196f3">for update(排它锁)</font></b>
<font color="#9e9e9e">例如:</font>select * from test_innodb_lock where a = 2 <font color="#2196f3">for update;</font>
这样<font color="#f57c00">其他session只能读这行数据</font>,<font color="#e57373">修改则会被阻塞</font>,<font color="#4caf50">直到锁定行的session提交</font>
<b><font color="#2196f3">结论</font></b>
<b>Innodb存储引擎</b>由于<font color="#2196f3">实现了行级锁定</font>
<font color="#f57c00">虽然</font>在锁定机制的实现方面所带来的<font color="#e57373">性能损耗可能比表级锁定会要更高</font><font color="#000000">一些</font>
<font color="#f57c00">但是</font>在<font color="#4caf50">整体并发处理能力方面要远远优于MYISAM的表级锁定</font>的
<b>当系统并发量高</b>的时候,<font color="#f57c00">Innodb的整体性能</font>和MYISAM相比就会<font color="#4caf50">有比较明显的优势</font>了
<font color="#f44336"><b>但是</b></font>,Innodb的行级锁定同样也有其脆弱的一面,当我们使用不当的时候,可能会让Innodb的整体性能表现不仅不能比MYISAM高,甚至可能会更差
<b>行锁分析</b><br>
通过检查<b><font color="#2196f3">InnoDB_row_lock</font></b><font color="#2196f3"><b>状态变量</b></font>来分析系统上的<font color="#f57c00">行锁的争夺情况</font>
对<b>各个状态量的说明</b>如下:
Innodb_row_lock_current_waits: 当前正在等待锁定的数量
<b><font color="#2196f3">Innodb_row_lock_time</font><font color="#e57373">: </font><font color="#000000">从系统启动到现在锁定总时间长度</font></b>
<b style=""><font color="#2196f3">Innodb_row_lock_time_avg</font><font color="#e57373">: </font><font color="#000000">每次等待所花平均时间</font></b>
Innodb_row_lock_time_max:从系统启动到现在等待最长的一次所花时间<br>
<b><font color="#2196f3">Innodb_row_lock_waits</font><font color="#e57373">: </font><font color="#000000">系统启动后到现在总共等待的次数</font></b>
<font color="#f57c00">尤其是当等待次数很高,而且每次等待时长也不小的时候</font>,我们就<font color="#f44336">需要分析系统中为什么会有如此多的等待</font>,然后<font color="#4caf50">根据分析结果着手制定优化计划</font>
<b>查看INFORMATION_SCHEMA系统库锁相关数据表</b>
<b>查看事务</b>
<font color="#0000ff">select * from INFORMATION_SCHEMA</font><font color="#4caf50">.INNODB_TRX</font>;
<b>查看锁</b><br>
<font color="#0000ff">select * from</font> <font color="#0000ff">INFORMATION_SCHEMA</font>.<font color="#4caf50">INNODB_LOCKS</font>;
<b>查看锁等待</b>
<font color="#0000ff">select * from</font> <font color="#0000ff">INFORMATION_SCHEMA</font>.<font color="#4caf50">INNODB_LOCK_WAITS</font>;
<b>释放锁</b>
<font color="#9c27b0">mysql> </font><font color="#0000ff">kill </font><font color="#4caf50">trx_mysql_thread_id</font>
<b> 查看锁等待详细信息</b>
<font color="#9c27b0">mysql></font><font color="#0000ff"> show engine innodb </font><font color="#4caf50">status\G;</font><br>
<b>死锁</b><br>
<b>死锁示例如下所示:</b>
<font color="#9c27b0">mysql> </font><font color="#0000ff">set tx_isolation='</font><font color="#4caf50">repeatable-read</font><font color="#0000ff">';</font>
<b>Session_1执行:</b><font color="#9c27b0">mysql><b> </b></font><font color="#0000ff">select * from account where id=1 </font><font color="#4caf50">for update</font><font color="#0000ff">;</font>
<b>Session_2执行:</b><font color="#9c27b0">mysql></font><font color="#0000ff"> select * from account where id=2 </font><font color="#4caf50">for update</font><font color="#0000ff">;</font><br>
<b>Session_1执行:</b><font color="#9c27b0">mysql></font> <font color="#0000ff">select * from account where id=2 </font><font color="#4caf50">for update</font><font color="#0000ff">;</font>
<b>Session_2执行:</b><font color="#9c27b0">mysql> </font><font color="#0000ff">select * from account where id=1 </font><font color="#4caf50">for update</font><font color="#0000ff">;</font><br>
<b>查看近期死锁日志信息:</b>
<font color="#9c27b0">mysql> </font><font color="#0000ff">show engine innodb </font><font color="#4caf50">status\G</font><font color="#0000ff">;</font>
<b>大多数情况</b><font color="#2196f3">MySQL可以自动检测死锁并回滚产生死锁的那个事务</font>,<font color="#000000"><b>但是</b><b style="">有些情况</b></font><font color="#f57c00">MySQL没法自动检测死锁</font>
<b>锁优化建议</b>
尽可能<font color="#2196f3">让所有数据检索都通过索引来完成</font>,<font color="#e57373">避免无索引行锁升级为表锁</font>
<font color="#2196f3">合理设计索引</font>,<font color="#2196f3">尽量缩小锁的范围</font>
尽可能<font color="#2196f3">减少检索条件范围</font>,<font color="#e57373">避免间隙锁</font>
尽量<font color="#2196f3">控制事务大小</font>,<font color="#f57c00">减少锁定资源量和时间长度</font>,<font color="#4caf50">涉及事务加锁的sql尽量放在事务最后执行</font>
尽可能<font color="#2196f3">降低事务隔离级别</font>
0 条评论
下一页