MySQL的事务与锁

引言

在现实生活中,人们通过银行互相转账和汇款,从数据的角度来看,这实际上就是数据库中两个不同账户之间的数据操作。例如,用户A向用户B转账了1000元,则A账户的余额减去1000元,B账户的余额加上1000元,整个过程需要使用两条SQL语句来完成操作,若其中一条语句出现异常没有被执行,则会导致两个账户的金额不同步,从而使数据出现错误。为了避免上述情况的发生,MySQL中可以通过开启事务来进行数据操作。用户转账过程中,只有事务执行成功后数据才会变更,如果事务执行失败,数据库中的值将不会变更。

一、事务

事务(Transaction):事务是DBMS中操作的基本执行单位,事务本身就是构成单一逻辑工作单元的数据库操作的有限序列。序列中的操作要么全做,要么全不做。整个序列是一个不可分割的操作单位。
事务实际上指的是数据库中的一个操作序列,由一组DML语句(INSERT,DELETE、UPDATE)组成。这些语句不可分割,只有在所有的SQL语句都执行成功后整个事务引发的操作才会更新到数据库中,如果有至少一条语句执行失败,所有操作都会被取消。以用户转账为例,将需要执行的语句定义为事务,具体的转账流程如图所示。

MySQL的事务与锁_第1张图片
●在关系型数据库中,事务可以是一条、一组SQL语句,或整个程序。  
●事务和程序的区别:程序包含多个事务。
在默认设置下,MySQL中的事务为自动提交模式,每一条语句都处于一个单独的事务中,在这条语句执行完毕后,如果执行成功则隐式提交事务,如果执行失败则隐式回滚事务。正常的事务管理是一组相关的操作处于一个事务之中,因此在执行正常的事务时可以使用相关指令关闭数据库的自动提交模式,随后用户将一直处于某个事务中,用户执行一条COMMIT(提交)或者ROLLBACK(回滚)命令后才会结束当前事务。如果用户不想关闭事务的自动提交模式,可以使用BEGIN或者STARTTRANSACTION命令开启事务,事务开启后便可以执行相关的事务语句,事务提交后自动回到自动提交模式。另外,事务的回滚操作只能撤销所有未提交的修改,对已经提交的事务不能使用ROLLBACK命令进行回滚。
其中以下是隐含事务与自动提交的SQL语句
DDL语句 :ALTER,CREATE,RENAME,DROP,TRUNCATE 
用户权限管理操作 :CREATE USER,GRANT,REVOKE,SET PASSWORD 
管理语句:ANALYZE TABLE,CHECK INDEX,REPAIR TABLE,LOAD INDEXINTO CACHE 

SQL定义事务的语句
>START TRANSACTION:显式的开启一个事务
>COMMITTRANSACTION:事务提交 
>ROLLBACKTRANSACTION:事务回滚 
>SET TRANSACTION:调整事务隔离级别
>SAVEPOINT identifier:允许事务创建一个保存点 
>ROLLBACKTO [SAVEPOINT]identifier:把事务回滚到保存点 索引类型 
>RELEASE SAVEPOINT identifier:删除一个事务的保存点 

事务的四特性
DBMS中,事务处理必须保证其ACID的特性,这样才能保证数据库中数据的安全和正
确。
原子性(Atomicity):事务中的操作,要么都做,要么都不做,是不可分割的。原子性是事务概念的本质体现和基本要求。
一致性(Consistency):事务执行的结果必须使数据库从一个一致性状态变到另一个一致性状态。
隔离性(Isolation):并发执行的各事务不能互相干扰
持续性(Durability):事务一旦提交,他对数据库的更新不再受后继操作或者故障的影响。

上述步骤中,事务的ACID特性表现如下:
(1)原子性:保证所有步骤要么都执行,要么都不执行。一旦在执行某一步骤的过程中出现问题,就需要执行回滚操作。例如账户B突然不可用(比如被注销)那么前面的所有操作都应该回滚到执行事务之前的状态。
(2)一致性:账户A向B转账1万元。在转账之后,账户A减少1万元,账户B要增加1万元。在执行该事务操作之后,数据从一个状态改变为另外一个状态。同时,一致性还能保证账户余额不会变成负数等。
(3)隔离性:在账户A向B转账的整个过程中,只要事务还没有提交,查询A和B时数量都不会有变化。如果在账户A给B转账的同时,有另外一个事务执行了账户C给B转账的操作那么当两个事务都结束时,B收到的钱应该是A转给B的钱加上C转给B的钱。
(4)持久性:一旦转账成功(事务提交),两个账户中的钱就会真正发生变化(会将数据写入数据库做持久化保存)。

事务读写中存在的问题

MySQL作为多线程并发访问的数据库,其明显的特点是资源可以被多个用户共享访问。当多个用户(多个事务)同时访问相同的数据库资源时,如果各事务之间没有采取必要的隔离措施,可能会出现以下几种不确定的情况。  
脏读 
一个事务读取了某行数据,而另外一个事务已经更新了此行的数据,但没有及时提交。例如,事务A读取了事务B更新的数据,随后事务B因为某些原因进行了回滚操作,那么事务A读取到的数据就是脏数据。这种情况是非常危险的,很可能造成所有的操作都被回滚。

不可重复读

指一个事务的修改和提交造成另一个事务在同一范围内的两次相同查询的返回结果不同。例如,事务A需要多次读取同一个数据,在事务A还没有结束时,事务B访问并修改了该数据,那么,事务A两次读取到的数据就可能不一致,因此称为不可重复读。

幻读

是指一个线程中的事务读取到了另外一个线程中提交的INSERT数据。例如:用户A将数据库中所有学生的成绩从具体分数改为ABCDE等级,但是用户B此时插入了一条具体分数的记录,用户A修改完成后发现还有一条记录没有改过来,就好像发生了幻觉一样,因此称这种情况为幻读或者虚读。 

事务的隔离级别

为了避免上述3种情况的发生,MySQL中为事务定义了不同的隔离级别,以此来保证数
据的稳定性。事务的隔离级别由低到高可分为ReadUncommittedRead(读未提交)Read Committed(读已提交) Repeatable Read(可重复读)Serializable(可串行化)。

MySQL的事务与锁_第2张图片
事务的隔离级别越高,越能保证数据的完整性和一致性,但是对并发性能的影响也会相应增大。另外,不同的隔离级别可能会造成不同的并发异常,如表所示。 

MySQL的事务与锁_第3张图片

不过MySQL的存储引擎通过多版本并发控制(MultiVersion Concurrency Control,MVCC)机制解决了数据幻读的问题。因此,当MySQL的隔离级别为Repeatable Read(可重复读)时,是可以避免幻读问题的出现的。

小结:
事务的隔离级别越高越能保证数据的完整性和一致性,但是对系统并发性能的影响也越大。对于大多数应用程序,可以优先考虑把数据库系统的隔离级别设为Read Committed,这样可以有效避免数据的脏读,而且具有较好的并发性能。合理地选用不同的隔离等级可以在不同程度上避免前面所提及的在事务处理中所面临的各种问题,在选取数据库的隔离级别时,可以参考以下几个原则
(1)首先,需要排除数据脏读的影响,在多个事务之间要避免进行“非授权的读”操作。因为事务的回滚操作或失败将会影响其他的并发事务,第一个事务的回滚会完全将其他事务的操作清除,这可能导致数据库处在一个不一致的状态。

(2)其次,绝大部分应用都无须使用“序列化”隔离,在实际的应用中,数据的幻读可以通过使用悲观锁这种强行使所有事务都序列化执行的方式来解决。
(3)对于大部分应用,可以优先考虑可重复读。这主要是因为所有的数据访问都是在统一的原子数据库事务中进行的,此隔离级别将消除一个事务在另外一个并发事务过程中覆盖数据的可能性。另外,在 MySQL的InnoDB存储引擎中也可以加入MVCC等机制来解决数据的幻读问题。

MySQL锁机制

锁的本质
数据库的锁机制主要是为了使用户对数据的访问变得有序,保证数据的一致性。锁机制是实现宏观上高并发最简单的方式,但从微观的角度来说,锁机制其实是读写串行化。
锁的粒度
锁的粒度是指锁的作用范围。InnoDB存储引擎支持表级锁以及行级锁,MyISAM存储引擎支持
表级锁。

隐式锁与显式锁
MySQL自动加锁被称为隐式锁,数据库开发人员手动加锁被称为显式锁。

锁的类型
锁的类型包括读锁(readlock)和写锁(writelock),其中读锁也被称为共享锁,用户对数据同时“读”,但不允许其他用户对数据同时“写”。写锁也被称为排他锁或者独占锁。写锁既不允许其他用户对数据同时“读”,也不允许其他用对数据同时“写”。
表级锁
表级锁指整个表被客户锁定。根据锁的类型,其他客户不能向表中插入记录,甚至从中读数据也会受到限制。表级锁分为读锁(readlock)和写锁(writelock)意向锁
LOCKTABLES语句用于锁定当前线程的表。
表级锁支持以下类型的锁定。
1、READ:读锁定,确保用户可以读取表,但是不能修改表。
2、WRITE:写锁定,只有锁定该表的用户可以修改表,其他用户无法访问该表。
3、意向锁:是一种表级锁,锁定的粒度是整张表,意向锁指如果对一个结点加意向锁,则说明该结点的下层结点正在被加锁。意向锁分为意向共享锁(IS)和意向排他锁(IX)两类。

【举例】事务A锁住了表中的一行,让这一行只能读,不能写。之后,事务B申请整个表的写锁。
如果事务B申请成功,那么理论上它就能修改表中的任意一行,这与A持有的行锁是冲突的。数据 
库需要避免这种冲突,就是说要让B的申请被阻塞,直到A释放了行锁。数据库要怎么判断这个冲突呢?
step1:判断表是否已被其他事务用表锁锁表。
step2:判断表中的每一行是否已被行锁锁住。 
注意step2,这样的判断方法效率实在不高,因为需要谝历整个表,干是就有了意向锁,在意向锁 
存在的情况下,事务A必须先申请表的意向共享锁,成功后再申请一行的行锁。在意向锁存在的情况下,上面的判断可以改成

step1:不变
step2:发现表上有意向共享锁,说明表中有些行被共享行锁锁住了,因此,事务B申请表的写锁 型 
会被阻塞。注意:申请意向锁的动作是数据库完成的,就是说,事务A申请一行的行锁的时候,数据库会自动先开始申请表的意向锁,不需要程序员使用代码来申请。

行级锁
行级锁相比表级锁或页级锁,对锁定过程提供了更精细的控制。在这种情况下,只有线程使用的行是被锁定的。表中的其他行对于其他线程都是可用的。行级锁并不是由MySQL提供的锁定机制,而是由存储引擎实现的,其中InnoDB的锁定机制就是行级锁定。
行级锁的类型包括:
1、共享锁(share lock)/读锁
2、排他锁(exclusive lock)/写锁  

MySQL的事务与锁_第4张图片

注意:如果表中某行加了排他锁,其他事务既不能读也不能写,注意用select可以进行查询该行数据,因为select查询不涉及任何锁机制,如果用其他方式去读,则可能不能成功,因为其他方式的读可能涉及加锁的机制,不能进行增删改的原因也是这些操作涉及到了加锁的机制。


间隙锁(Gap Lock)是InnoDB引擎在可重复读(RepeatableRead)的隔离级别下为了解决幻读和数据误删问题而引入的锁机制。当使用范围条件检索数据并请求共享或排他锁时,InnoDB引擎会给符合条件的已有数据记录的索引项加锁。键值在条件范围内但不存在的记录叫作“间隙(GAP)”,InnoDB 引擎也会对这个“间隙”加锁。

锁等待

是指在一个事务执行过程中,一个锁需要等到上一个事务的锁释放后才可以使用该资源。如果事务一直不释放,就需要持续等待下去,直到超过锁等待时间,此时系统会报出超时错误。MySQL中锁等待时间是通过innodb_lock_wait_timout参数控制的,该参数的单位为秒。

死锁产生的原因

两个或两个以上的事务分别申请封锁对方已经封锁的数据对象,导致长期等待而无法继续运行下去的现象被称为死锁。死锁是指事务永远不会释放它们所占用的锁,死锁中的两个或两个以上的事务都将无限期地等待下去。

对死锁的处理

锁的行为和顺序是和存储引擎相关的。思索的产生有些是因为真正的数据冲突,有些则是因为存储引擎实现的方式导致的。在MySQL的InnoDB存储引擎中,当检测到死锁时,通常会使一个持有最少行级排它锁的事务释放锁并回滚,而让另一个事务获得锁并继续完成事务。

避免死锁的方法
(1)不同程序会并发存取多个表或者涉及多行记录时,尽量约定以相同的顺序访问表,可以大大降低死锁的机会。
(2)对应用程序进行调整,在某些情况下,通过把大事务分解成多个小事务,使得锁能够更快被释放,及时提交或者回滚事务,可减少死锁发生的概率。
(3)在同一个事务中,尽可能做到一次锁定所需要的所有资源,减少死锁产生的概率。
(4)为表添加合理的索引,不用索引将会为表的每一行记录加上锁,死锁的概率大大增大。

(5)对非常容易产生死锁的业务,可尝试升级锁粒度,通过表锁定来减少死锁产生的概率。

你可能感兴趣的:(mysql,数据库,java)