下表展示了本人安装的MariaDB(10.1.19,MySQL的分支)所支持的所有存储引擎概况,其中支持事务的有InnoDB、SEQUENCE,另外InnoDB还支持XA事务,MyISAM不支持事务。锁可以通过SQL语句(如 LOCK TABLES )显式申请,也可以由InnoDB引擎自动为你获取。下文将讨论InnoDB和MyISAM在事务与锁定方面的相关话题
ENGINE | SUPPORT | COMMENT | TRANSACTIONS | XA | SAVEPOINTS |
---|---|---|---|---|---|
CSV | YES | CSV storage engine | NO | NO | NO |
MyISAM | YES | MyISAM storage engine | NO | NO | NO |
MRG_MyISAM | YES | Collection of identical MyISAM tables | NO | NO | NO |
Aria | YES | Crash-safe tables with MyISAM heritage | NO | NO | NO |
MEMORY | YES | Hash based, stored in memory, useful for temporary... | NO | NO | NO |
PERFORMANCE_SCHEMA | YES | Performance Schema | NO | NO | NO |
SEQUENCE | YES | Generated tables filled with sequential values | YES | NO | YES |
InnoDB | DEFAULT | Percona-XtraDB, Supports transactions, row-level l... | YES | YES | YES |
一. 事务四要素
数据库事务正确执行的四个基本要素包括原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Durability),简称ACID。目前要实现ACID主要有两种方式:一种是Write ahead logging,也就是日志式的方式(现代数据库均基于这种方式);另一种是Shadow paging。
-
原子性:整个事务中的所有操作,要么全部完成,要么全部不完成,不可能停滞在中间某个环节。事务在执行过程中发生错误,会被回滚(Rollback)到事务开始前的状态
-
一致性:事务执行前与执行后都必须始终保持系统处于一致的状态
-
隔离性:并发事务之间不会相互干扰,彼此独立执行
-
持久性:在事务完成以后,该事务对数据库所作的更改便持久的保存在数据库之中
二. MyISAM表锁定
所有用户行为最后执行的基本单位是单条操作命令(SELECT、INSERT、UPDATE、DELETE等),它们都是原子操作。按照事务术语,MyISAM表总能高效地工作在AUTOCOMMIT=1模式下,原子操作通常能提供可比较的完整性以及更好的性能。这意味着,你能确信在每个特性更新运行的同时,其他用户不能干涉它,而且不会出现自动回滚(如果你不小心,对于事务性表,这种情况可能发生),MySQL服务器还能保证不存在脏读。
一般而言,所有由事务解决的完整性问题均能用LOCK TABLES或原子更新解决,从而确保了服务器不会自动中断,后者是事务性数据库系统的常见问题。MyISAM只支持表级锁定,允许多重读或一次写。LOCK TABLES可以锁定用于当前线程的表,如果表被其它线程锁定,则造成堵塞,直到可以获取所有锁定为止。UNLOCK TABLES可以释放被当前线程保持的任何锁定。当线程发布另一个LOCK TABLES时,或当与服务器的连接被关闭时,所有由当前线程锁定的表被隐含地解锁。如果您想要确保在同一业务流中的SELECT和UPDATE之间没有其它线程访问修改数据,就必须使用LOCK TABLES。如果你获得了对某一表的READ LOCAL锁定(与写锁定相对),该表允许在表尾执行并行插入,当其他客户端执行插入操作时,允许执行读操作。新插入的记录不会被有读锁定属性的客户端看到,直至解除了该锁定为止。使用INSERT DELAYED,能够将插入项置于本地队列中,直至锁定解除,不会让客户端等待插入完成。
三. InnoDB事务与锁定
在 InnoDB 中,所有用户行为都在事务内发生,事务是其执行的基本单位,默认运行查询为非锁定持续读。如果自动提交模式被允许,即 AUTOCOMMIT = 1,每个SQL语句都将以一个单独的事务来运行;如果自动提交模式被用 SET AUTOCOMMIT = 0 关闭,那么我们可以认为一个用户总是有一个事务打开着,一个SQL COMMIT或ROLLBACK语句结束当前事务并且一个新事务开始,两个语句都释放所有在当前事务中被设置的InnoDB锁定。
InnoDB除了表级锁定之外,还支持更细粒度的行级锁定,且行锁和表锁多重粒度可共存。下面是InnoDB中与锁定有关的几个重要概念:
-
持续非锁定读:无格式SELECT语句默认模式,InnoDB使用它的多版本化来给一个查询展示某个时间点处数据库的快照。查询看到在那个时间点之前被提交的那些确切事务做的更改,并且没有其后的事务或未提交事务做的改变。这个规则的例外是,查询看到发布该查询的事务本身所做的改变,但持续读不在DROP TABLE和ALTER TABLE上作用。持续读不在任何它访问的表上设置锁定,因此,其它用户可*地在持续读在一个表上执行的同一时间修改这些表。如果你运行在默认的REPEATABLE READ隔离级别,则在同一事务内的所有持续读读取由该事务中第一个这样的读所确立的快照。你可以通过提交当前事务并在发布新查询的事务之后,为你的查询获得一个更新鲜的快照
-
Shared (S) Locks:一个共享锁允许事务获取锁来读取一行(READ)。如果事务T1持有对行 r 的 S 锁, 那么另外一个事务T2对行 r 的锁需求会如下处理,T2对S锁的请求会被马上授权,因此T1、T2都对r同时分别持有一个共享锁。但T2对r的X锁请求不会被马上授权,这看上去似乎READ优先于WRITE,以致WRITE请求被饿死
-
Exclusive (X) Locks:一个排他锁允许事务获取锁来更新或删除一行(WRITE)。如果事务T1持有一个r的X锁, 那么T2对r的任何锁类型都无法被马上授权
-
Intention Locks:意向锁在InnoDB中是表锁,他表明S或X锁将会在一个事务中对某一行使用。Intention shared (IS) 表明事务T打算设置S锁到表t上,Intention exclusive (IX) 表明事务T打算设置X锁到行上。意向锁协议如下:
1. 在一个事务获取表t的某行的S锁之前, 他必须获取表t的一个IS锁或更强的锁 2. 在一个事务获取表t某行的X锁之前, 他必须获取一个t的IX锁 3. 这些规则可以总结为如下锁类型兼容矩阵: X IX S IS X Conflict Conflict Conflict Conflict IX Conflict Compatible Conflict Compatible S Conflict Conflict Compatible Compatible IS Conflict Compatible Compatible Compatible 4. 一个锁如果和已经存在的锁兼容, 就可以授权给请求他的事务, 但如果和已存在的锁不兼容则不行
一个事务必须等待直到冲突的锁被释放。如果一个锁请求和一个已经存在的锁冲突, 并且一直不能被授权, 就会造成死锁。一旦死锁发生,InnoDB会选择其中一个报错并释放其持有的锁,直至解除死锁。意向锁并不会阻塞任何事情,除非是对全表的请求(例如, LOCK TABLES ... WRITE), IX和IS锁的主要目的是表示有人正在或者准备锁定一行
-
Next-Key锁定:InnoDB执行行级锁定将搜索或扫描表的索引,对遇到的索引记录设置共享或独占锁定。因此,Next-Key锁定是一种执行行锁定的方式,行级锁定事实上是索引记录锁定。如果一个用户对一个索引上的记录R有共享或独占的锁定,另一个用户 不能紧接在R之前以索引的顺序插入一个新索引记录,Next-Key锁定这个间隙以防止所谓的“幽灵问题”
假设你想要从有一个标识符值大于100的child读并锁定所有某些记录,并想着随后在选定行中更新一些列: SELECT * FROM child WHERE id > 100 FOR UPDATE; 假设在id列有一个索引,查询从id大于100的第一个记录开始扫描。如果设置在索引记录上的锁定不把在间隙生成的插入排除在外,一个新行可能与此同时被插进表中。如果你在同一事务内执行同样的SELECT,你可能会在该查询返回的结果包里看到一个新行。这与事务的隔离原则是相反的:一个事务应该能够运行,以便它已经读的数据在事务过程中不改变。如果我们把结果集视为一个数据项,新的“幽灵”记录出现会违反这一隔离原则。 当InnoDB扫描一个索引之时,它也锁定范围中最后一个记录之后的间隙。刚才前一个例子中:InnoDB设置的锁定防止任何表中出现id大过100的记录
-
事务隔离级别:MySQL/InnoDB 提供SQL标准所描述的所有四个事务隔离级别,默认是可重复读的(REPEATABLE READ)。你可以在命令行用--transaction-isolation选项,或在选项文件里,为所有连接设置 默认隔离级别。下面将分别介绍这四个事务隔离级别
-
· READ UNCOMMITTED SELECT语句以非锁定方式被执行,但是一个可能更早期版本的记录会被用到,读是不连贯的,也被称为“脏读”(dirty read)。另外,这个隔离级别象READ COMMITTED一样作用。 · READ COMMITTED 所有SELECT ... FOR UPDATE和SELECT ... LOCK IN SHARE MOD语句仅锁定索引记录,而不锁定记录前的间隙,因而允许随意紧挨着已锁定的记录插入新记录。UPDATE和DELETE语句使用一个带唯一搜索条件的唯一的索引仅锁定找到的索引记录,而不包括记录前的间隙。在范围类型UPDATE和DELETE语句,InnoDB必须对范围覆盖的间隙设置next-key锁定或间隙锁定其它用户做的块插入,因为要让MySQL复制和恢复起作用,“幽灵行”必须被阻止掉。 · REPEATABLE READ 这是InnoDB的默认隔离级别。带唯一搜索条件使用唯一索引的SELECT ... FOR UPDATE, SELECT ... LOCK IN SHARE MODE, UPDATE 和DELETE语句只锁定找到的索引记录,而不锁定记录前的间隙。用其它搜索条件,这些操作采用next-key锁定,用next-key锁定或者间隙锁定锁住搜索的索引范围,并且阻止其它用户的新插入。在持续读中,有一个与之前隔离级别重要的差别:在这个级别,在同一事务内所有持续读读取由第一次读所确定的同一快照。这个惯例意味着如果你在同一事务内发出数个无格式SELECT语句,这些SELECT语句对相互之间也是持续的。 · SERIALIZABLE 这个级别类似REPEATABLE READ,但是所有无格式SELECT语句被 隐式转换成SELECT ... LOCK IN SHARE MODE。
从上可知,READ UNCOMMITTED隔离约束最弱,SERIALIZABLE隔离约束最强,隔离约束逐级提高。
四. 行锁与表锁优劣对比
行级锁定的优点:
-
当在许多线程中访问不同的行时只存在少量锁定冲突
-
回滚时只有少量的更改
-
可以长时间锁定单一的行
行级锁定的缺点:
-
比页级或表级锁定占用更多的内存
-
当在表的大部分中使用时,比页级或表级锁定速度慢,因为你必须获取更多的锁
-
如果你在大部分数据上经常进行GROUP BY操作或者必须经常扫描整个表,比其它锁定明显慢很多
-
用高级别锁定,通过支持不同的类型锁定,你也可以很容易地调节应用程序,因为其锁成本小于行级锁定
MySQL表锁定机制:当一个锁定被释放时,锁定可被写锁定队列中的线程得到,然后是读锁定队列中的线程。这意味着,如果你在一个表上有许多更新,SELECT语句将等待直到没有更多的更新。表更新通常情况认为比表检索更重要,因此给予它们更高的优先级,但这应确保更新一个表的活动不能“饿死”,即使该表上有很繁重的SELECT活动
对WRITE,MySQL使用的表锁定方法原理如下:
-
如果在表上没有锁,在它上面放一个写锁
-
否则,把锁定请求放在写锁定队列中
对READ,MySQL使用的表锁定方法原理如下:
-
如果在表上没有写锁定,把一个读锁定放在它上面
-
否则,把锁请求放在读锁定队列中
在以下情况下,表锁定优于行级锁定:
-
表的大部分语句用于读取
-
对严格的unique_key进行读取和更新,你可以更新或删除可以用单一的读取的关键字来提取的一行:
UPDATE tbl_name SET column=value WHERE unique_key_col=key_value; DELETE FROM tbl_name WHERE unique_key_col=key_value;
-
SELECT 结合并行的INSERT语句,并且只有很少的UPDATE或DELETE语句
-
在整个表上有许多扫描或GROUP BY操作,没有任何写操作
-
对于大表,对于大多数应用程序,表锁定比行锁定更好,但存在部分缺陷
表锁定的有关事项:
-
使用SET LOW_PRIORITY_UPDATES=1语句可指定具体连接中的所有更新应使用低优先级;
或用LOW_PRIORITY属性给与一个特定的INSERT、UPDATE或DELETE语句较低优先级;
或用HIGH_PRIORITY属性给与一个特定的SELECT语句较高优先级;
或为max_write_lock_count系统变量指定一个低值来启动mysqld来强制MySQL在具体数量的插入完成后临时提高所有等待一个表的SELECT语句的优先级;
或用--low-priority-updates启动mysqld,这将给所有更新(修改)一个表的语句以比SELECT语句低的优先级 -
如果你有关于INSERT结合SELECT的问题,切换到使用新的MyISAM表,因为它们支持并发的SELECT和INSERT;
如果你对同一个表混合插入和删除,INSERT DELAYED将会有很大的帮助;
如果你对同一个表混合使用SELECT和DELETE语句出现问题,DELETE的LIMIT选项可以有所帮助;
对SELECT语句使用SQL_BUFFER_RESULT可以帮助使表锁定时间变短;
可以更改mysys/thr_lock.c中的锁代码以使用单一的队列,在这种情况下,写锁定和读锁定将具有相同的优先级;
如果不混合更新与需要在同一个表中检查许多行的选择,可以进行并行操作;
可以使用LOCK TABLES来提高速度,因为在一个锁定中进行许多更新比没有锁定的更新要快得多,另外将表中的内容切分为几个表也可以有所帮助
五. 选择MyISAM
一般而言,若要使用MyISAM存储引擎,应看看应用程序做什么并且混合使用什么样的读和写。例如,大多数Web应用程序执行许多select,而很少进行delete,只对key的值进行更新,并且只插入少量行,此时使用MyISAM会更佳,而且MyISAM支持并发select和insert。在MySQL中对于使用表级锁定的存储引擎,表锁定时不会死锁的,这通过总是在一个查询开始时立即请求所有必要的锁定并且总是以同样的顺序锁定表来管理。
可以通过检查table_locks_waited和table_locks_immediate状态变量来分析系统上的表锁定争夺:
mysql> SHOW STATUS LIKE 'Table%'; +-----------------------+---------+ | Variable_name | Value | +-----------------------+---------+ | Table_locks_immediate | 1151552 | | Table_locks_waited | 15324 | +-----------------------+---------+
如果数据文件不包含空块(从表的中部删除或更新的行可能导致空洞),INSERT语句不冲突,可以*为MyISAM表混合并行的INSERT和SELECT语句而不需要锁定,你可以在其它客户正读取MyISAM表的时候插入行,记录总是插入在数据文件的尾部;如果不能同时插入,为了在一个表中进行多次INSERT和SELECT操作,可以在临时表中插入行并且立即用临时表中的记录更新真正的表,这也适合做批量延迟插入:
mysql> LOCK TABLES real_table WRITE, insert_table WRITE; mysql> INSERT INTO real_table SELECT * FROM insert_table; mysql> TRUNCATE TABLE insert_table; mysql> UNLOCK TABLES;
六. 选择InnoDB
InnoDB使用行锁定,可能存在死锁。这是因为,在SQL语句处理期间,InnoDB自动获得行锁定,而不是在事务启动时获得。对于InnoDB和BDB(BerkeleyDB)表,如果你用LOCK TABLES显式锁定表,MySQL只使用表锁定,建议不要使用LOCK TABLES,因为InnoDB使用自动行级锁定而BDB使用页级锁定来保证事务隔离。
InnoDB支持外键约束。
一般说来,以多读为主也附带少量写首选MyISAM,否则选择InnoDB或其他引擎会更佳。
七. 悲观锁与乐观锁
悲观锁和乐观锁不是MySQL数据库中的标准概念,而只是一种通俗说法。
-
悲观锁:悲观锁指对数据被意外修改持保守态度,依赖数据库原生支持的锁机制来保证当前事务处理的安全性,防止其他并发事务对目标数据的破坏或破坏其他并发事务数据,将在事务开始执行前或执行中申请锁定,执行完后再释放锁定。这对于长事务来讲,可能会严重影响系统的并发处理能力
LOCK TABLES a WRITE; INSERT INTO a VALUES (1,23),(2,34),(4,33); INSERT INTO a VALUES (8,26),(6,29); UNLOCK TABLES;
锁定表可以加速用多个语句执行的INSERT操作,因为索引缓存区仅在所有INSERT语句完成后刷新到磁盘上一次。一般有多少INSERT语句即有多少索引缓存区刷新,如果能用一个语句插入所有的行,就不需要锁定;对于事务表,应使用BEGIN和COMMIT代替LOCK TABLES来加快插入
-
乐观锁:乐观锁相对悲观锁而言,先假想数据不会被并发操作修改,没有数据冲突,只在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果发现冲突了,则宣告失败,否则更新数据。这就要求避免使用长事务和锁机制,以免导致系统并发处理能力降低,保障系统生产效率。下面将说明使用乐观锁时的大致业务处理流程
首 步:执行一次查询 select some_column as old_value from some_table where id = id_value (假设该值在当前业务处理过程中不会被其他并发事务修改) ... 第n步:old_value参与中间业务处理,比如old_value被自己修改 new_value = f(old_value)。这期间可能耗时很长,但不会为持有 some_column 而申请所在的行或表锁定,因此其他并发事务可以获得该锁 ... 尾 步:执行条件更新 update some_table set some_column = new_value where id = id_value and some_column = old_value (条件更新中检查old_value是否被修改)