温馨提示×

温馨提示×

您好,登录后才能下订单哦!

密码登录×
登录注册×
其他方式登录
点击 登录注册 即表示同意《亿速云用户服务条款》

事务机制和锁机制

发布时间:2020-06-27 03:42:44 来源:网络 阅读:1004 作者:浅嫣 栏目:开发技术

事务的必要性

为了避免出现数据不一致问题,需要在存储过程中引入事务的概念,将更新语句绑在一起,让它们成为一个”原子性”的操作:更新语句要么都执行,要么都不执行。

、关闭MySQL自动提交的方法

1、显示关闭自动提交

            set autocommit=0;1

2、隐式关闭自动提交:(推荐)

            start transaction1

一般推荐使用隐式的提交方式,因为不会修改到autocommit系统变量。



二、关闭自动提交后,提交更新语句的方法有 

1.显示地提交:(推荐

            commit;1

2.隐式地提交

    包括了begin、set autocommit=1、start transaction、rename table、truncate table等语句;
    数据定义(create、alter、drop)function、alter procedure、drop database、drop table、drop function、drop index、drop procedure等;
    权限管理和账户管理语句(grant、revoke、set password、create user、drop user、rename user)
    锁语句(lock tables、unlock tables)
    更推荐用显示提交地方式。


三、事务处理使用方法

在处理错误代码地程序中使用rollback(事务回滚)。
在具有原子性操作的地方,使用start transaction隐式地关闭自动提交,并且在结束的为止上使用commit显示提交。


四、事务保存点的使用方法

在事务中使用savepoint 保存点名 的格式创建保存点,实现事务的”部分”提交或”部分”撤销(rollback to savepoint 保存点名)。
保存点是”临时状态”,既可以回滚到事务开始前的状态,也能决定事务的下一个状态,是介于两自动提交语句所引发状态中的一种临时状态。


五、锁机制的必要性

内存中的数据与外存中的数据不同步,其中的表记录之间存在“同步延迟“。


六、MyISAM表施加表级锁的语法格式

lock tables 表1 [as 别名] read  [local]
[,表2[ as 别名2][low_priority] write] ...12

其中read local与read选项的差别为 READ LOCAL允许在锁定被保持时,执行非冲突性INSERT语句(同时插入)。


七、锁的粒度、隐式锁与显示锁、锁的类型、锁的钥匙、锁的生命周期

1.锁的粒度指锁的作用范围:
    多个MySQL客户机并发访问同一个数据时,为保证数据的一致性,数据库管理系统会自动地为该数据加锁、解锁,这种是隐式锁。
    而有时单靠隐式锁无法实现数据的一致性访问要求(例如对于临界资源的争夺上),此时需要手动地加锁、解锁,这种称为显示锁。
2.锁的类型分为:读锁(共享锁)和写锁(排他锁或独占锁)
3.锁的钥匙:    当多个MySQL客户机并发访问同一个数据时、如果MySQL客户机A对该数据成功地施加了锁,那么只有MySQL客户机A拥有这把锁的钥匙。
4.锁的生命周期:在同一个MySQL会话内,对数据加锁到解锁之间的时间间隔。锁的生命周期越长,并发访问性能越低;锁的生命周期越短,并发访问性能越高。


八、InnoDB表施加行级锁的语法格式

共享锁     select * from 表 where 条件语句 lock in share mode;
排他锁     select * from 表 where 条件语句 for update;


九、InnoDB中的间隙锁、记录锁

当检索条件满足某区间范围,但表中不存在的记录,此时也有共享锁或排他锁,即行级锁会锁定相邻的键,这种机制就是间隙锁(next-key锁)
当事务隔离级别设置为repeatable read,此时InnoDB表施加行级锁,默认使用间隔锁(需要索引),

当事务的隔离级别设置为read uncommited或者read commited,此时InnoDB表施加行级锁,默认情况下使用记录锁(record lock)。
 与间隙锁不同,记录锁仅仅为满足该查询范围的记录施加共享锁或排他锁。


十、锁等待与死锁

一)锁等待:是为了保证事务可以正常地并发运行,锁等待不一定导致死锁问题的发生。而死锁问题的发生一定伴随着锁等待现象。

二)死锁:  

    1、定义: 是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。当线程进入对象的synchronized代码块时,便占有了资源,直到它退出该代码块或者调用wait方法,才释放资源,在此期间,其他线程将不能进入该代码块。当线程互相持有对方所需要的资源时,会互相等待对方释放资源,如果线程都不主动释放所占有的资源,将产生死锁。

    2、死锁的产生4个必要条件 
         1)互斥条件:进程对于所分配到的资源具有排它性,即一个资源只能被一个进程占用,直到被该进程释放 
        2)占有且等待一个进程因请求被占用资源而发生阻塞时,对已获得的资源保持不放。 
        3)不可抢占条件:任何一个资源在没被该进程释放之前,任何其他进程都无法对他剥夺占用 
        4)循环等待条件:当发生死锁时,所等待的进程必定会形成一个环路(类似于死循环),造成永久阻塞。

    3、死锁的另一种:递归死锁,举例:

递归函数就是自调用函数,在函数体内直接或间接的调用自己,即函数的嵌套是函数本身。 


递归方式有两种:直接递归和间接递归,直接递归就是在函数中出现调用函数本身。间接递归,指函数中调用了其他函数,而该其他函数又调用了本函数。

那什么时候使用递归呢?一般来说当你要在某段代码逻辑中使用循环迭代的时候但是迭代的次数在迭代之前无法知晓的情况下使用递归。打个比方你要在一个文件夹中查找某个文件,而这个文件夹底下有N多子文件夹和文件,当你在不知道有多少层文件夹和文件的情况下你就得用到递归了。


递归的优点就是让代码显得很简洁,同时有些应用场景不得不使用递归比如前面说的找文件。递归是个好东西但是在某些时候也会给你带来一些麻烦。比如在多线程的环境下使用递归,遇到了多线程那么就不得不面对同步的问题。而递归程序遇到同步的时候很容易出问题。


多线程的递归就是指递归链中的某个方法由另外一个线程来操作,以下代码的意思都是这个意思即调用recursive()和businessLogic()并非一个线程(如果是在一个线程中就不存在死锁问题,例如下面的recursive变成private就不存在问题。)

 

[java] view plain copy

 

  1. public class Test {  

  2.     public void recursive(){  

  3.         this.businessLogic();  

  4.     }  

  5.     public synchronized void businessLogic(){  

  6.         System.out.println("处理业务逻辑");  

  7.     System.out.println("保存到<a href="http://lib.csdn.net/base/mysql" class='replace_word' title="MySQL知识库" target='_blank' style='color:#df3434; font-weight:bold;'>数据库</a>");  

  8.         this.recursive();  

  9.     }  

  10. }  

 

 

 

以上这段代码就是个能形成死锁的代码,事实上这个“synchronized”放在“businessLogic()”和“recursive()”都会形成死锁,并且是多线程的情况下就会锁住!他的逻辑顺序是先执行recursive()方法然后接下来执行businessLogic()方法同时将businessLogic()方法锁住,接下来程序进入businessLogic()方法内部执行完打印语句后开始执行recursive(),进入recursive()后准备执行businessLogic(),等等问题来了!之前执行的businessLogic()的锁还没有放开这次又执行到这里了,当然是过不去的了,形成了死锁!从这个例子我们总结出来一个规律就是在递归的时候在递归链上面的方法上加锁肯定会出现死锁(所谓递归链就是指recursive()链向businessLogic(),businessLogic()又链回recursive()),解决这个问题的方法就是避免在递归链上加锁,请看以下的例子

 

[java] view plain copy

 

  1. public class Test {  

  2.     public void recursive(){  

  3.         this.businessLogic();  

  4.     }  

  5.     public  void businessLogic(){  

  6.         System.out.println("处理业务逻辑");  

  7.         this.saveToDB();  

  8.         this.recursive();  

  9.     }  

  10.     public synchronized void saveToDB(){  

  11.         System.out.println("保存到数据库");  

  12.     }  

  13. }  

 

saveToDB()不在这条递归链上面自然不会出现死锁,所以说在递归中加锁是件很危险的事情,实在逃不过要加锁就加在最小的粒度的程序代码上以减小死锁的概率。

 

4、处理死锁的方法
    4.1 预防死锁
    4.2 避免死锁
        4.2.1 常用避免死锁的方法
                4.2.1.1 有序资源分配法
                4.2.1.2 银行家算法


        4.2.2 常用避免死锁的技术
                4.2.2.1 加锁顺序
                4.2.2.2 加锁时限
                4.2.2.3 死锁检测


    4.3 检测死锁
    4.4 解除死锁

   处理死锁的方法

4.1、死锁预防 ----- 确保系统永远不会进入死锁状态     产生死锁需要四个条件,那么,只要这四个条件中至少有一个条件得不到满足,就不可能发生死锁了。由于互斥条件是非共享资源所必须的,不仅不能改变,还应加以保证,所以,主要是破坏产生死锁的其他三个条件。

        a、破坏“占有且等待”条件     

            方法1:所有的进程在开始运行之前,必须一次性地申请其在整个运行过程中所需要的全部资源。         

                 优点:简单易实施且安全。      

                 缺点:因为某项资源不满足,进程无法启动,而其他已经满足了的资源也不会得到利用,严重降低了资源的利用率,造成资源浪费。 使进程经常发生饥饿现象。                      

            方法2:该方法是对第一种方法的改进,允许进程只获得运行初期需要的资源,便开始运行,在运行过程中逐步释放掉分配到的已经使用完毕的资源,然后再去请求新的资源。这样的话,资源的利用率会得到提高,也会减少进程的饥饿问题。

        b、破坏“不可抢占”条件      当一个已经持有了一些资源的进程在提出新的资源请求没有得到满足时,它必须释放已经保持的所有资源,待以后需要使用的时候再重新申请。这就意味着进程已占有的资源会被短暂地释放或者说是被抢占了。      该种方法实现起来比较复杂,且代价也比较大。释放已经保持的资源很有可能会导致进程之前的工作实效等,反复的申请和释放资源会导致进程的执行被无限的推迟,这不仅会延长进程的周转周期,还会影响系统的吞吐量。

        c、破坏“循环等待”条件     可以通过定义资源类型的线性顺序来预防,可将每个资源编号,当一个进程占有编号为i的资源时,那么它下一次申请资源只能申请编号大于i的资源。如图所示:这样虽然避免了循环等待,但是这种方法是比较低效的,资源的执行速度回变慢,并且可能在没有必要的情况下拒绝资源的访问,比如说,进程c想要申请资源1,如果资源1并没有被其他进程占有,此时将它分配个进程c是没有问题的,但是为了避免产生循环等待,该申请会被拒绝,这样就降低了资源的利用率

4.2、避免死锁 ----- 在使用前进行判断,只允许不会产生死锁的进程申请资源的死锁避免是利用额外的检验信息,在分配资源时判断是否会出现死锁,只在不会出现死锁的情况下才分配资源。

    两种避免办法:    

        1、如果一个进程的请求会导致死锁,则不启动该进程   

        2、如果一个进程的增加资源请求会导致死锁 ,则拒绝该申请。

注意:预防死锁和避免死锁的区别
        预防死锁是设法至少破坏产生死锁的四个必要条件之一,严格的防止死锁的出现,

        而避免死锁则不那么严格的限制产生死锁的必要条件的存在,因为即使死锁的必要条件存在,也不一定发生死锁。避免死锁是在系统运行过程中注意避免死锁的最终发生。

  4.2.1 常用避免死锁的方法

4.2.1.1 有序资源分配法

这种算法资源按某种规则系统中的所有资源统一编号(例如打印机为1、磁带机为2、磁盘为3、等等),申请时必须以上升的次序。系统要求申请进程:
  1、对它所必须使用的而且属于同一类的所有资源,必须一次申请完;
  2、在申请不同类资源时,必须按各类设备的编号依次申请。例如:进程PA,使用资源的顺序是R1,R2; 进程PB,使用资源的顺序是R2,R1;若采用动态分配有可能形成环路条件,造成死锁。
  采用有序资源分配法:R1的编号为1,R2的编号为2;
  PA:申请次序应是:R1,R2
  PB:申请次序应是:R1,R2
  这样就破坏了环路条件,避免了死锁的发生。

4.2.1.2银行家算法

详见银行家算法.

避免死锁的具体实现通常利用银行家算法    银行家算法a、银行家算法的相关数据结构    

1)可利用资源向量Available:用于表示系统里边各种资源剩余的数目。由于系统里边拥有的资源通常都是有很多种(假设有m种),所以,我们用一个有m个元素的数组来表示各种资源。数组元素的初始值为系统里边所配置的该类全部可用资源的数目,其数值随着该类资源的分配与回收动态地改变。  

 2) 最大需求矩阵Max:用于表示各个进程对各种资源的额最大需求量。进程可能会有很多个(假设为n个),那么,我们就可以用一个nxm的矩阵来表示各个进程多各种资源的最大需求量    

3)分配矩阵Allocation:顾名思义,就是用于表示已经分配给各个进程的各种资源的数目。也是一个nxm的矩阵。    

4)需求矩阵Need:用于表示进程仍然需要的资源数目,用一个nxm的矩阵表示。

系统可能没法一下就满足了某个进程的最大需求(通常进程对资源的最大需求也是指它在整个运行周期中需要的资源数目,并不是每一个时刻都需要这么多),于是,为了进程的执行能够向前推进,通常,系统会先分配个进程一部分资源保证进程能够执行起来。那么,进程的最大需求减去已经分配给进程的数目,就得到了进程仍然需要的资源数目了。

银行家算法通过对进程需求、占有和系统拥有资源的实时统计,确保系统在分配给进程资源不会造成死锁才会给与分配。


注意:死锁避免的优缺点

1)死锁避免的优点:不需要死锁预防中的抢占和重新运行进程,并且比死锁预防的限制要少。

2)死锁避免的限制: 必须事先声明每个进程请求的最大资源量    考虑的进程必须无关的,也就是说,它们执行的顺序必须没有任何同步要求的限制    分配的资源数目必须是固定的。    在占有资源时,进程不能退出


4.2.2 常用避免死锁的技术

        4.2.2.1 加锁顺序:线程按照一定的顺序加锁)        

        4.2.2.2 加锁时限(线程尝试获取锁的时候加上一定的时限,超过时限则放弃对该锁的请求,并释放自己占有的锁)       

        4.2.2.3 死锁检测

1]加锁顺序

当多个线程需要相同的一些锁,但是按照不同的顺序加锁,死锁就很容易发生。

如果能确保所有的线程都是按照相同的顺序获得锁,那么死锁就不会发生。看下面这个例子:


Thread 1:
 lock A
 lock B

Thread 2:
  wait for A
  lock C (when A locked)

Thread 3:
  wait for A
  wait for B
  wait for C

如果一个线程(比如线程3)需要一些锁,那么它必须按照确定的顺序获取锁。它只有获得了从顺序上排在前面的锁之后,才能获取后面的锁。

例如,线程2和线程3只有在获取了锁A之后才能尝试获取锁C(译者注:获取锁A是获取锁C的必要条件)。因为线程1已经拥有了锁A,所以线程2和3需要一直等到锁A被释放。然后在它们尝试对B或C加锁之前,必须成功地对A加了锁。

按照顺序加锁是一种有效的死锁预防机制。但是,这种方式需事先知道所有可能会用到的锁(译者注:并对这些锁做适当的排序),但总有些时候是无法预知的。

2]加锁时限

另外一个可以避免死锁的方法是在尝试获取锁的时候加一个超时时间,这也就意味着在尝试获取锁的过程中若超过了这个时限该线程则放弃对该锁请求。若一个线程没有在给定的时限内成功获得所有需要的锁,则会进行回退并释放所有已经获得的锁,然后等待一段随机的时间再重试。这段随机的等待时间让其它线程有机会尝试获取相同的这些锁,并且让该应用在没有获得锁的时候可以继续运行(译者注:加锁超时后可以先继续运行干点其它事情,再回头来重复之前加锁的逻辑)。

以下是一个例子,展示了两个线程以不同的顺序尝试获取相同的两个锁,在发生超时后回退并重试的场景:


Thread 1 locks A
Thread 2 locks B

Thread 1 attempts to lock B but is blocked
Thread 2 attempts to lock A but is blocked

Thread 1's lock attempt on B times out
Thread 1 backs up and releases A as well
Thread 1 waits randomly (e.g. 257 millis) before retrying.

Thread 2's lock attempt on A times out
Thread 2 backs up and releases B as well
Thread 2 waits randomly (e.g. 43 millis) before retrying.

在上面的例子中,线程2比线程1早200毫秒进行重试加锁,因此它可以先成功地获取到两个锁。这时,线程1尝试获取锁A并且处于等待状态。当线程2结束时,线程1也可以顺利的获得这两个锁(除非线程2或者其它线程在线程1成功获得两个锁之前又获得其中的一些锁)。

需要注意的是,由于存在锁的超时,所以我们不能认为这种场景就一定是出现了死锁。也可能是因为获得了锁的线程(导致其它线程超时)需要很长的时间去完成它的任务。

此外,如果有非常多的线程同一时间去竞争同一批资源,就算有超时和回退机制,还是可能会导致这些线程重复地尝试但却始终得不到锁。如果只有两个线程,并且重试的超时时间设定为0到500毫秒之间,这种现象可能不会发生,但是如果是10个或20个线程情况就不同了。因为这些线程等待相等的重试时间的概率就高的多(或者非常接近以至于会出现问题)。
(译者注:超时和重试机制是为了避免在同一时间出现的竞争,但是当线程很多时,其中两个或多个线程的超时时间一样或者接近的可能性就会很大,因此就算出现竞争而导致超时后,由于超时时间一样,它们又会同时开始重试,导致新一轮的竞争,带来了新的问题。)

这种机制存在一个问题,在Java中不能对synchronized同步块设置超时时间。你需要创建一个自定义锁,或使用Java5中java.util.concurrent包下的工具。写一个自定义锁类不复杂,但超出了本文的内容。后续的Java并发系列会涵盖自定义锁的内容。

3]死锁检测

死锁检测是一个更好的死锁预防机制,它主要是针对那些不可能实现按序加锁并且锁超时也不可行的场景。

每当一个线程获得了锁,会在线程和锁相关的数据结构中(map、graph等等)将其记下。除此之外,每当有线程请求锁,也需要记录在这个数据结构中。

当一个线程请求锁失败时,这个线程可以遍历锁的关系图看看是否有死锁发生。例如,线程A请求锁7,但是锁7这个时候被线程B持有,这时线程A就可以检查一下线程B是否已经请求了线程A当前所持有的锁。如果线程B确实有这样的请求,那么就是发生了死锁(线程A拥有锁1,请求锁7;线程B拥有锁7,请求锁1)。

当然,死锁一般要比两个线程互相持有对方的锁这种情况要复杂的多。线程A等待线程B,线程B等待线程C,线程C等待线程D,线程D又在等待线程A。线程A为了检测死锁,它需要递进地检测所有被B请求的锁。从线程B所请求的锁开始,线程A找到了线程C,然后又找到了线程D,发现线程D请求的锁被线程A自己持有着。这是它就知道发生了死锁。

下面是一幅关于四个线程(A,B,C和D)之间锁占有和请求的关系图。像这样的数据结构就可以被用来检测死锁。

事务机制和锁机制

4.3 检测死锁

死锁检测算法

算法一:

    算法使用的数据结构是如下这些:
          占有矩阵A:n*m阶,其中n表示并发进程的个数,m表示系统的各类资源的个数,这个矩阵记录了每一个进程当前占有各个资源类中资源的个数。
           申请矩阵R:n*m阶,其中n表示并发进程的个数,m表示系统的各类资源的个数,这个矩阵记录了每一个进程当前要完成工作需要申请的各个资源类中资源的个数。
           空闲向量T:记录当前m个资源类中空闲资源的个数。
           完成向量F:布尔型向量值为真(true)或假(false),记录当前n个并发进程能否进行完。为真即能进行完,为假则不能进行完。
           临时向量W:开始时W:=T。
算法步骤:
     (1)W:=T,
     对于所有的i=1,2,…,n,
     如果A[i]=0,则F[i]:=true;否则,F[i]:=false
     (2)找满足下面条件的下标i:
     F[i]:=false并且R[i]〈=W
     如果不存在满足上面的条件i,则转到步骤(4)。
     (3)W:=W+A[i]
     F[i]:=true
     转到步骤(2)
     (4)如果存在i,F[i]:=false,则系统处于死锁状态,且Pi进程参与了死锁。什么时候进行死锁的检测取决于死锁发生的频率。如果死锁发生的频率高,那么死锁检测的频率也要相应提高,这样一方面可以提高系统资源的利用率,一方面可以避免更多的进程卷入死锁。如果进程申请资源不能满足就立刻进行检测,那么每当死锁形成时即能被发现,这和死锁避免的算法相近,只是系统的开销较大。为了减小死锁检测带来的系统开销,一般采取每隔一段时间进行一次死锁检测,或者在CPU的利用率降低到某一数值时,进行死锁的检测。

那么当检测出死锁时,这些线程该做些什么呢?

1)一个可行的做法是释放所有锁,回退,并且等待一段随机的时间后重试。这个和简单的加锁超时类似,不一样的是只有死锁已经发生了才回退,而不会是因为加锁的请求超时了。虽然有回退和等待,但是如果有大量的线程竞争同一批锁,它们还是会重复地死锁(编者注:原因同超时类似,不能从根本上减轻竞争)。

2)一个更好的方案是给这些线程设置优先级,让一个(或几个)线程回退,剩下的线程就像没发生死锁一样继续保持着它们需要的锁。如果赋予这些线程的优先级是固定不变的,同一批线程总是会拥有更高的优先级。为避免这个问题,可以在死锁发生的时候设置随机的优先级。


算法二:

    死锁检测与解除 ----- 在检测到运行系统进入死锁,进行恢复。    允许系统进入到死锁状态    死锁检测下图截自《操作系统--精髓与设计原理》事务机制和锁机制

4.4  死锁的解除

如果利用死锁检测算法检测出系统已经出现了死锁 ,那么,此时就需要对系统采取相应的措施。

常用的解除死锁的方法:

1、抢占资源:从一个或多个进程中抢占足够数量的资源分配给死锁进程,以解除死锁状态。但应防止被挂起的进程长时间得不到资源,而处于资源匮乏的状态。

2、终止(或撤销)进程:终止或撤销系统中的一个或多个死锁进程,直至打破死锁状态。撤销的原则可以按进程优先级和撤销进程代价的高低进行

        a、终止所有的死锁进程。这种方式简单粗暴,但是代价很大,很有可能会导致一些已经运行了很久的进程前功尽弃。

        b、逐个终止进程,直至死锁状态解除。该方法的代价也很大,因为每终止一个进程就需要使用死锁检测来检测系统当前是否处于死锁状态。要求系统保持进程的历史信息,设置还原点。另外,每次终止进程的时候终止那个进程呢?每次都应该采用最优策略来选择一个“代价最小”的进程来解除死锁状态。

                一般根据如下几个方面来决定终止哪个进程:      1】进程的优先级  

                                                                                          2】进程已运行时间以及运行完成还需要的时间  

                                                                                          3】进程已占用系统资源                                                                                           

                                                                                          4】进程运行完成还需要的资源终止进程数目 

                                                                                                       5】进程是交互还是批处理


 

十一、MySQL支持的事务隔离级别

4种:

read uncommited –> 读”脏”数据现象,“脏读"
read commited –> 不可重复读现象,
repeatable read(MySQL默认)–>幻读现象
serializable->可串行化,

向AI问一下细节

免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。

AI