并发访问和锁
锁的作用
锁用于协调多个线程对同一资源的并发访问。MySQL数据库中的资源主要是指数据库中的表和表中的记录,也就是数据库中的数据。为什么需要锁呢?因为如果没有锁机制,多个并发修改数据的线程可能会使被修改的数据处于混乱的状态。而且,在修改数据期间,如果不加锁的话,查看数据的线程看到的可能是处于部分修改状态的数据。因此,必须引入锁机制使这些线程对数据的访问协调一致。
锁的访问级别
对数据库中数据的操作主要分为两种类型,一是读取(select),一是修改(insert, update, delete)。对于读取线程,因为它们不会修改数据,所以可以允许多个线程同时读取相同的数据。而对于修改线程,只能独占的访问数据,不允许其他的线程读取或修改数据。因此,从对数据操作的类型出发,可以把锁分为读锁和写锁两种访问级别,通过引入访问级别可以显著提高数据访问的并发性。读锁又称为共享锁,写锁又称为排他锁。
锁粒度
为了提高对资源的并发访问,经常采用的一种方式就是把资源划分成更小的部分(也就是更小的粒度),从而允许多个并发访问的线程同时访问不同部分。这样就可以把锁加在不同粒度的资源上,从而形成不同粒度的锁。在InnoDB中,主要定义了两种锁粒度,即表级锁和行级锁。表级锁是加在整个表上的。行级锁是加到表中一条条记录上面的,所以它具有更小的粒度,同时也具有更好的并发性。但是,锁本身也是需要开销的,因此行级锁可能会具有更大的开销。在日常使用中,我们一般只关注InnoDB中的行级锁。InnoDB中的行级锁具有上面介绍的两种访问级别,即共享锁和排他锁。
事务
事务的概念
事务是一个独立的不可分割的工作单元。一个事务可以包含多个操作(也就是多个SQL语句),这些操作是作为一个整体而存在的。事务执行时,这些操作要么全都执行成功,要么全都执行失败(错误发生时回滚所有已执行的操作),而不会出现只有一部分执行成功的情况。一个常见的例子就是从A账户转账200元到B账户,可以分为3个操作:
- 确保A账户余额高于200元;
- 从A账户扣除200元;
- 把B账户余额增加200元。
这3个操作必须同时成功,或者同时失败,如果没有事务,执行完第二个步骤后失败了,那么A账户将凭空消失200元。所以这3个操作需要作为一个整体执行,如果任何一个步骤失败了,就回滚所有之前的操作,使得系统仍然处于一致的状态。
事务的ACID属性
事务应该满足ACID属性,下面分别介绍。
原子性(Atomicity)
事务是一个不可分割的工作单元,其中的操作要么全部成功,要么全部失败。
一致性(consistency)
在事务执行后,系统是从一个一致性的状态转换到另一个一致性的状态,而不会处在被破坏的状态。一致性其实更多是从应用程序的角度来说的,因为一个事务包含哪些操作是由应用程序定义的,因此,应用程序要保证事务里面的操作本身是符合业务规则的。而数据库更多的是通过AID三个属性来保证数据库的状态符合业务规则的定义。
隔离性(isolation)
隔离性是针对多个事务而言的,简单来说就是一个事务执行时,就好像拥有了自己的独立执行环境一样,而不受其他同时执行的事务的影响。实际上隔离性受到多种因素的影响,例如事务隔离级别、锁机制等。
持久性(durability)
持久性是说一旦一个事务执行完成,那么它对数据库的修改就会被永久保存下来,而不会凭空消失。即使事务完成后系统发生崩溃,事务对数据的修改也不会丢失。
事务的隔离级别
事务的隔离级别主要规定了一个事务在执行过程中或者提交后,它对数据的修改对于其他并发执行的事务的可见程度。InnoDB中主要支持4种隔离级别,默认的隔离级别为"可重复读"。
读未提交(READ UNCOMMITTED)
一个事务修改了数据还未提交时,其他事务就能看到它对数据的修改。这种隔离级别可能会发生"脏读"的问题,因为事务还未提交,所以它对数据的修改可能最终被回滚掉,或者它在随后对数据又进行了进一步修改。这样其他事务看到的可能就不是数据最终的状态,而只是临时的状态。这种隔离级别会有很大的问题,所以在实际的生产环境中很少被使用。
读提交(READ COMMITTED)
一个事务对数据的修改只有在提交之后才能被其他事务看到。这种隔离级别可以避免脏读的问题,但是可能会产生"不可重复读"的问题。不可重复读是指一个事务两次读取同一条记录却得到不同的值。例如,有A和B两个事务,B开始时读取了记录r得到值v1,之后A事务修改了r的值为v2并提交,然后B事务再次读取r记录,发现读取到的值为v2。这种情况可能会对B事务产生不好的影响,因为B可能会基于r的值作出一些操作。
可重复读(REPEATABLE READ)
这种隔离级别解决了不可重复读的问题,即一个事务多次读取一条记录得到的值是相同的。但是可能会产生"幻读"的问题。"幻读"是指一个事务在开始时根据指定条件读取到了一些记录,之后再次以同样的条件查询时读取到的记录却比之前多,好像产生了幻影行一样。这是InnoDB的默认隔离级别,但是InnoDB通过引入MVCC(多版本并发控制)的机制解决了幻读问题。
串行化(SERIALIZABLE)
最高的隔离级别,它强制事务串行执行,从而解决了"幻读"问题。它会在读取到的每一条记录上加锁,所以可能导致大量的锁争用,极大的影响系统性能,因此在实际中很少使用。
查看MySQL事务隔离级别
show variables like 'transaction_isolation'; # MySQL >= 5.7.20
show variables like 'tx_isolation'; # MySQL < 5.7.20
事务和锁的关系
事务中包含一个或者多个SQL语句,每一个SQL语句在执行的过程中可能需要获取锁。因此,一个事务在执行过程中可能会包含多个锁请求,在事务执行完成后需要释放使用到的锁。在InnoDB中,事务获取和释放锁的过程主要有两点需要注意:
- 锁的获取是逐步进行的,执行到某条SQL时才申请相应的锁。
- 锁的释放是一次性进行的,在事务提交或者回滚时一次性释放事务占用的所有锁。