数据库的事务、隔离级别和锁

一、事务的概念和特征:

       事务是数据库操作的最小单元,是将一系列操作作为整体的一个单元执行。这些操作作为一个整体一起向系统提交,要么都执行、要么都不执行。事务有四大特性(ACID原则):

     1、原子性(atomicity):

       一个事务要么全部执行成功,要么失败全部回滚,不能只执行其中的一部分操作,这就是事务的原子性。通俗的理解,事务是一组原子操作单元,从数据库角度说,就是一组SQL指令,要么全部执行成功,要么撤销不执行。若因为某个原因其中一条指令执行有错误,则撤销先前执行过的所有指令。

     2、一致性(consistency):

       一个事务或多个事务在进行数据库操作时,要保证一连串的事务执行前和执行后,数据库中的数据是一个“终态”,也就是要保证数据库中的数据关系是正确的(比如多人并发转帐,事务执行结束后,所有人的余额总和应该和事务执行前一样,不多也不少,这就是一致性)。事务的一致性是通过原子性、隔性性以及持久性等方面综合起来一起保证的,但又不能纯依靠数据库的机制来保证事务的一致性,还需要在业务层用程序来处理异常,比如捕获异常,回滚等。

     3、隔离性(isolation):       

       多个事务并发如果不进行隔离,可能会出现以下几个问题:

      1>脏读:

       事务A读取了事务B更新的数据,然后B回滚操作,那么A读取到的数据是脏数据。

       2>不可重复读:

       事务A多次读取同一条数据,事务B在事务A多次读取的过程中,对数据作了更新并事务提交,导致事务A多次读取同一数据时,数据不一样。

       3>幻读:

       事务A多次读取一批数据,事务B在事务A多次读取的过程中,对这批数据进行了新增或删除并事务提交,导致事务A多次读取这批数据的数量不一致。

       Tips:不可重复读和幻读很容易混淆,不可重复读侧重于单条记录的修改,幻读侧重于对一批数据中部分进行新增或删除。解决不可重复读的问题只需锁住满足条件的行,解决幻读需要锁表。

       事务的隔离性是指多个并发的事务是相互隔离的,不同事务并发操作相同的数据时,每个事务都有各自完成的数据空间,不会产生以上问题。

     4、持久性(durability)

       一旦事务提交,那么它对数据库中的对应数据的状态的变更就会永久保存到数据库中。即使发生系统崩溃或机器宕机等故障,只要数据库能够重新启动,那么一定能够将其恢复到事务成功结束的状态。

       如果要使用事务,需要先核实数据库是否支持事务。如果支持再使用事务, mysql事务的开启有三种方式:

begin;
start transaction;
begin work;

       mysql事务回滚可以使用:

rollback;

       mysql事务提交可以使用: 

commit;

       有几种异常情况需要注意:

       1、当事务开始后但还没结束事务时,数据库连接断开了,此时事务会自动回滚;

       2、当事务开始后但还没结束事务时,又在同事务里开始了新事务,此时原来的事务会自动提交;

       3、当事务开始后,部分sql执行成功,部分sql执行失败,再进行事务提交时,失败的sql执行失败,但成功的sql也不会回滚,除非使用rollback进行回滚;

二、隔离级别的概念:

       事务的隔离性是通过锁来保证的。使用不同的锁机制,导致并发的事务之间的隔离性也不同。在标准SQL规范中,定义了四种事务隔离级别。不同的隔离级别对事务的处理不同,分别是:

     1、读未提交(Read Uncommited):

       也称脏读(Drity Read),其隔离级别最低。事务A可以读取事务B更新的但还没事务提交的数据。由于该级别脏读到的不是最终值,可能影响到实际业务的执行结果,所以该级别在实际应用中很少使用。

     2、读已提交(Read Commited):

       也称不可重复读(Non-repeatable read,侧重的是数据修改),事务A只能读取事务B更新的且已事务提交的数据。但事务A中多次读取此数据时,此数据可能还未修改,也可能是修改后且已事务提交,导致事务A多次读取到的同一条记录数据可能不同。大多数数据库默认的隔离级别都是此隔离级别,但mysql默认隔离级别不是此级别。

     3、可重复读(Repeatable Read):

       事务A只能读取事务B更新的且已事务提交的数据,并且事务A中多次读取此数据时,每次读取的这条数据都相同。但事务A在读取一批数据时,如果事务C在这批数据中新增或删除了数据,那么事务A能读到这些新增或删除的变动(这种现象就是幻读)。mysql默认隔离级别是此隔离级别。

select @@tx_isolation;

     4、串行化(Serializable):

       是最严格的事务隔离级别,它要求所有事务被串行执行,即事务只能一个接一个的进行处理,不能并发执行。此隔离级别会在对应的表上加锁,导致此表中所有记录都不能修改,严重影响数据库并发,一般实际开发中不会用到。

事务隔离级别脏读不可重复读幻读
读未提交(read-uncommitted)✔️✔️✔️
读已提交(read-committed)✖️✔️✔️
可重复读(repeatable-read)✖️✖️✔️
串行化(serializable)✖️✖️✖️

三、锁的概念:

       锁用来解决多个事务同时查询或修改相同的资源时产生的并发问题。一个事务开始时就会自动加锁,但是只有事务结束(提交或回滚)时才释放这些锁,所以这些锁都是隐式锁定。当然也可以通过sql语句显式加锁,一个事务执行的任何过程中都可以获取锁(加锁),但也是事务结束(提交或回滚)时才释放这些锁,除非使用类似unlock tables语句,但实际上unlock tables也是隐式结束事务。

       InnoDB有行锁,但MyISAM没有行锁,只有表锁,所以MyISAM不存在死锁,InnoDB会有死锁问题。在InnoDB的锁机制中,可以通过参数innodb_deadlock_detect开启检测死锁的机制:当检测到有死锁后,会根据事务影响的数据量,选择产生死锁的事务中较少的事务回滚,而让较多的事务成功完成。但由于并不能完全自动检测到死锁,所以需要通过参数innodb_lock_wait_timeout来设置锁超时时间,超时后自动释放锁。当然也可以只依赖innodb_lock_wait_timeout解决死锁问题。

       加锁是在需要锁的时候才加,但是要等到事务结束(提交或回滚)或连接断开时所有锁同时释放,这就是两阶段加锁协议。正因为释放锁时是需要等事务结束才释放,所以加锁时要尽量晚加锁,这样让加锁的时长缩短,就可以提高并发度。举个栗子,比如用户A给用户B转账n元时,主要会发生3条数据的变更:

       ❶用户A账户金额减少n元;

       ❷用户B账户金额增加n元;

       ❸银行增加一条转账流水记录;

       也就是说,要完成这个交易需要 update 两条记录、 insert 一条记录。为了保证交易的原子性,我们要把这三个操作放在一个事务中。那么在哪个环节会加锁?如果用户A同时也向用户C转账,那么环节❶中会存在多次扣款,金额不够怎么办?所以是需要加锁的。因为三个环节是在一个事务中,所以先后执行不重要,只要保证最终都执行成功即可,此时把加锁的环节❶放在最后执行,以❸❷❶这样的顺序操作,加锁的时间会缩短,提高了系统的并发度。

     1、锁的分类:

      1>根据锁粒度划分:

       ①行级锁

       行级锁是粒度最小的锁,处理并发能力强,但性能开销也最大,加锁慢。mysql的行级锁是在引擎层由各个引擎自己实现的,但并不是所有的引擎都支持行级锁,比如InnoDB支持行级锁,但MyISAM不支持行级锁。不支持行级锁意味着并发控制只能使用表级锁,同一张表上任何时刻只能有一个操作在执行,这就会影响到业务的并发度,这也是MyISAM被InnoDB替代的重要原因之一。

       InnoDB行级锁是通过给索引上的索引项加锁来实现的,只有检索数据时走了索引,InnoDB才使用行级锁。走不走索引依赖执行计划,并不是一定会按预期走希望的索引,如果没走索引,InnoDB将使用表级锁。因此即使访问不同行的记录,但是如果使用相同的索引键,也有可能会出现锁冲突。

       InnoDB不仅会给符合条件的存在的数据索引项加锁,还会给符合在查询范围内但不存在的数据(间隙GAP)加锁,以防止新增符合条件的数据造成幻读,这种锁叫间隙锁(Next-Key锁)。

       InnoDB不是简单地使用行加锁机制,而是使用多版本并发控制(Multiversion Currency Control,简称MVCC)技术,和行加锁机制共同作用,以便应对更高的并发,当然是以消耗性能作为代价的。

       ②表级锁

       表级锁有两种,分别是:

       ❶表锁:

       表锁会将整个表加锁,不仅限制其他线程访问被锁的表,也限制本线程对被锁表的操作。如果想给InnoDB表加锁,可以执行加读锁或写锁:

lock tables table_name read
lock tables table_name write

       释放lock tables加的锁可以使用: 

unlock tables;

       需要注意的是unlock tables会隐含地提交事务,但事务提交或事务回滚,都不能释放用lock tables加的锁。所以在事务提交或事务回滚之后,才能执行unlock tables。

       ❷元数据锁(MDL):

       在 MySQL 5.5 版本中引入了MDL,用于隔离DQL、DML与DDL对同一个表的影响。当对表做增删改查操作时加MDL读锁,当对表做结构变更时加MDL写锁。MDL不需要显式添加,在访问表时会被自动加上。MDL读锁之间不互斥,因此可以同时对一张表进行增删改查;MDL读锁与MDL写锁之间、以及MDL写锁之间是互斥的,用来保证变更表结构操作的安全性,因此如果有两个线程同时给一个表加字段,其中一个线程要等另一个执行完才能开始执行。

       ③全局锁

       顾名思义,就是对整个数据库实例加锁。全局锁的典型使用场景是做全库备份时,使用mysqldump加固定参数--single-transaction启动一个事务来确保拿到一致性视图。这种方式虽然加了全局锁,但由于MVCC的支持,这个过程中数据是可以正常更新的,但一定需要引擎支持事务。

mysqldump –single-transaction

       但对于像MyISAM这种不支持事务的引擎,只能使用FTWRL加全局读锁,让整个库处于只读状态,否则就破坏了备份的一致性:

flush tables with read lock

      2>根据锁的操作类型划分:      

       ①共享锁(S):又称读锁,其他事务可以读,但不能写。如果事务A获取了数据的共享锁,那么其他事务也可以获取该数据的共享锁,但其他事务不能获取该数据的排他锁。这些事务都能读取这些数据,但是不能修改数据;MyISAM引擎的select默认会加读锁,InnoDB引擎的select默认不会加读锁。如果需要加读锁,可以使用:

select语句 lock in share mode;

       ②排他锁(X):又称写锁,其他事务不能读取,也不能写。如果事务A获取了数据的排他锁,那么其他事务就不能再获取该数据的任何锁(包括共享锁和排他锁),但是获取到排他锁的事务A是可以对该数据进行读取和修改的。mysql InnoDB引擎和MyISAM引擎默认的修改数据语句:insert、update和delete都会自动给涉及到的数据加写锁、select默认都不会加写锁,如果需要加写锁,可以使用:

select语句 for update;

       MyISAM引擎默认情况下,写锁比读锁具有更高的获取锁的优先级。可以通过设置low_priority_updates=1来降低写锁的优先级,也可以通过max_write_lock_count参数,设置读锁的数量达到该值时使读锁的优先级高于写锁。

       ③意向锁(I):属于表级锁,为了让行级锁和表级锁共存,InnoDB就使用了意向锁,并且是自动加锁的。意向锁的作用是当事务A获取了数据的排他锁时,其他事务可以在该数据上添加一个合适的意向锁。如果将来需要一个共享锁,那么在该数据上就添加意向共享锁。如果将来需要一个排他锁,那么在该数据上就添加意向共享排他锁。

       锁模式的兼容情况(X,S在这里指的是表级锁):

SXISIX
S兼容排斥兼容排斥
X排斥排斥排斥排斥
IS兼容排斥兼容兼容
IX排斥排斥兼容兼容

      3>根据锁的处理方式划分: 

       ①乐观锁(Optimistic Lock):假设并发冲突不会发生或很少发生,所以在数据提交时才检查数据,一般处理方式是如果发生了冲突就直接报错,由用户决定如何处理。

       ②悲观锁(Pessimistic Lock):假设并发冲突会经常发生,所以在取数据时就加了锁。传统的关系型数据库里就用到了很多这种锁机制。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值