MySQL 锁、事物、MVCC
读写锁
如果一个用户正在读取数据库某表中的数据,而另一个用户试图删除该表或者正在被读取的某一行,显然会出现错误,因此设计了由两种类型的锁组成的锁机制,共享锁(shared lock)和排它锁(exclusive lock),也称读锁(read lock)和写锁(write lock)。
读锁:是共享的,也就是同一条数据在同一时刻可以被多个用户读取
写锁:是排他的,被加上写锁的数据在当前写锁没有释放之前不能被第二个用户读写。写锁有比读锁更高的优先级,一个写锁请求可能被插入到锁队列中读锁的前面,而读锁则不能插入到写锁的前面
锁粒度
为了提高系统的并发程度,我们尽量只锁定需要修改的部分数据,而不是所有资源,任何时候锁定的数据量越少越好。按照锁住资源的范围,分为表级锁,页级锁和行级锁。
表级锁:锁定整张表,在每个用户对表进行写操作(插入、删除、更新)前,必须获得写锁,此时会阻塞其他用户对表的读写操作。服务器会为诸如ALTER TABLE之类的语句加表锁,而忽略存储引擎的锁机制
行级锁:锁定一行数据,行级锁可以最大程度的支持并发,同时也带来最大的开销,加锁也需要消耗资源,各种锁操作:获得锁、检查锁是否已解除、释放锁等都会增加系统开销。行级锁只在存储引擎层实现,如:InnoDB,XtraDB等部分存储引擎实现了行级锁。
页级锁:表级锁速度快,但冲突多,行级冲突少,但速度慢。所以取了折衷的页级,一次锁定相邻的一组记录(相邻的多行)。MyISAM引擎实现了页级锁
看了上边大家可能会迷惑MySQL服务器和存储引擎有什么关系和?
MySQL服务器包含存储引擎,存储引擎只负责数据的存储和提取,服务器还要做链接处理、授权认证、查询解析优化等工作。
事物
事物就是一组原子性的sql操作(语句),一个独立的工作单元。事物内部的语句要么全部执行成功,要么全部执行失败。
ACID
ACID是一个运行良好的事物必须具有的标准特征,即:原子性(atomicity)、一致性(consistency)、隔离性(isolation)、持久性(durability)
相信解释之前请先记住下面的情景:
银行有张三和李四两个账户,且每个账户都对应有一个卡和一个存折,张三账户上有300元,李四账户上有400元
1.原子性(atomicity):一个事物是一个不可分割的最小工作单元,其中的所有操作,要么全部执行成功,要么全部回滚,不可能只执行其中一部分。
2.一致性(consistency):数据库总是从一个一致性状态转换到另一个一致性状态。也就是说如果有一个事物执行李四给张三转账100的操作,不会出现李四的余额变成300,张三的余额也是300的情况,结果只能是成功:(张三400,李四300),失败:(张三300,李四400),为了便于理解,我再举一个例子:如果张三去银行注销他的账户,银行只在数据库将他的银行卡状态改为注销,而没有将他的存折状态改为注销,这就不满足事物的一致性,必须两个都同时改为注销状态。事物的一致性要求程序员在编写事物代码时必须要仔细分析,而不仅是靠mysql的约束。
3.隔离性(isolation):通常一个修改事物最终执行结束之前,其结果对其他事物是不可见的。比如:在执行李四给张转账的事物中,执行完李四余额减100,还没执行张三余额加100时,其它事物所看到的仍是张三:300,李四:400。是否完全满足隔离性,这跟数据库设置的隔离级别有关。
4.持久性(durability):事物一旦提交,其所在的修改就会永久的保存到数据库中。但实际持久性也分不同的级别,数据库本身并不能做到100%的持久性,不然也不存在备份的问题了。
事物的隔离级别
SQL定义了四种事物隔离级别,每一种都规定了一个事物中做的修改,哪些在事物内和事物间是可见的,哪些是不可见的。较低的隔离级别通常可以执行更高的并发,系统的开销也更低。可以设置整个数据库的隔离级别,也可以之设置某个事物的隔离级别。
四种隔离级别如下:
1.READ UNCOMMITTED(未提交读)
在这个级别中,事务所做的修改,即使还没有提交,其他事物也是可见的,也就是说一个事物可以读取未提交的数据,这也被称为脏读(Dirty Read)。
2.READ COMMITTED(提交读):
此隔离级别满足上边所说的隔离性,一个事物执行过程中只能看见别的事物已经提交的数据,也就是说,一个事物从开始到提交之前,所做的任何修改,对其他的事物都是不可见的。这个级别也叫做不可重复读,即在同一个事物中连续两次读取同一条数据结果可能不同,因为在两条语句中间可能有别的事物修改了该事物,并完成提交。大多数数据库系统的默认隔离级别都是提交读,但mysql却不是。
3.REPEATABLE READ(可重复读):
此隔离级别既不会发生脏读,也解决了不可重复读的问题,保证了在同一个事物中多次读取同一条数据的结果是一致的。但仍然解决不了幻读的问题:当某个事物在重复读取某个范围内的数据(比如某张表)时,另一个事物在该表中插入了新的行,则会出现两次读取的行数不一样的情况,产生幻行。
mysql的默认事务隔离级别是可重复读。
4.SERIALIZABLE(可串行化):
可串行化是最高的事务隔离级别。它通过在读取的每一行数据上都加锁,强制事物串行执行,避免了幻读的问题。但是由于他导致的大量的超时和锁争用问题,所以在实际应用中很少使用。
注意:MySQL隔离级别通过下边语句设置 SET [SESSION | GLOBAL] TRANSACTION ISOLATION LEVEL 隔离级别;
默认的行为(不带session和global)从下一个(未开始)的事务(包括所有连接)开始生效。
使用SESSION 关键字为将来在当前连接上执行的事务设置默认事务级别。
如果使用GLOBAL关键字,语句在全局对从在那点已经存在的以及之后创建的所有新连接(除了不存在的连接)设置默认事务级别,但是你需要SUPER权限来做这个。
任何客户端都能*改变会话隔离级别(甚至在事务的中间),或者为下一个事务设置隔离级别。
死锁
数据库的死锁与我们在程序中常见的死锁类似,当两个或多个事物在同一个资源上相互占用,并请求锁定对方正在占用的资源时,就会出现死锁。
例如:
#事物1
START TRANSACTION;
UPDATE user SET money=8000 where user_id=1;
UPDATE user SET money=7000 where user_id=2;
COMMIT;
#事物2
START TRANSACTION;
UPDATE user SET money=4000 where user_id=2;
UPDATE user SET money=3000 where user_id=1;
COMMIT;
#当两个线程对上边两个事物并发执行时,事物1执行完第一句,给user_id=1的数据加了排它锁,事物2也执行完第一句,给user_id=2的数据加了排它锁,接下来两个事物都要去执行第二T条UPDATE语句,去发现该行数据已经被对方锁定,两个事物都在等待对方释放锁,但两个事物都无法完成,则陷入死循环,即死锁。此时,只有外不因素接入才能解除死锁。
死锁的解除
锁的行为和顺序是和存储引擎有关的,产生死锁的原因一种是因为真正的数据冲突,这种情况通常很难避免,两一种则完全由存储引擎的实现导致的,解除死锁通常是数据库引擎所做的工作。
各种数据库系统实现了不同的死锁检测和死锁超时机制。常见的有:①当检测到死锁的循环依赖时立即返回一个错误,将持有最少级排它锁的事物进行回滚(InnoDB存储引擎);②当查询的时间达到锁等待超时的设定后放弃锁请求;
隐式锁和显式锁
锁只有在执行COMMIT或者ROLLBACK的时候才会释放。
前面锁描述的所有锁都是隐式锁,就是数据库系统默认在需要的时候自动加的锁。一些数据库系统也支持通过特定的语句进行显示加锁
如:LOCK TABLES 表名;UNLOCK TABLES 表名;
事务日志
事务日志可以帮助提高事物的效率。使用事物日志,存储引擎在修改表的数据时只需要修改其内存拷贝,再把该修改行为记录到持久在硬盘上的事务日志中,而不是每次都将修改的数据本身持久到磁盘。事物日志在磁盘上的持久方式是以追加的方式写的,因此写日志的操作是磁盘上一小块区域的顺序I/O,而不像随机I/O需要在磁盘的额多个地方移动磁头,所以事务日志要快的多。目前大多数存储引擎都是这样实现的,我们也称之为预写式日志,修改数据需要写两次磁盘(日志一次,数据一次)。
多版本并发控制
MySQL的大多数的事务型存储引擎实现的都不是简单的行级锁,为了提升并发性能,它们一般都实现了多版本并发控制,即MVCC(Multi-Version Concurrency Control)。MVCC的实现是通过保存数据在某个时间点的快照来实现的,也就是说,不管执行多长时间,每个事物看到的数据都是一致的。切记,这是后边所说的过程的前提。
下边以InnoDB(mysql的默认存储引擎)来简单说明以下MVCC的工作原理:
InnoDB的MVCC是通过在每行记录的后边保存两个隐藏的列来实现的,一个保存了行创建的时间,一个保存行过期(删除)的时间。当然,存储的并非实际的时间值,而是系统的版本号,没开始一个新的事物,系统的版本号都会自动递增。事物将它开始时的系统版本号做为事物的版本号,来和查询到的每行记录的版本号进行比较。
下边说一下在REAPEATABLE READ隔离级别下,InnoDB引擎实现的MVCC具体是如何工作的。
执行SELECT 语句时:
只查找创建版本号小于或等于当前事物版本的数据行,确保事物读取的行,确保事物读取的行要么在事物开始前就已经存在了,要么是事物自身插入或者修改过的。
行的删除版本要么未定义,要么大于当前事物的版本号,可以确保事物读到的行在事物开始之前为被删除。
执行INSERT语句时:
为插入的每一行保存当前事物的版本号作为行的版本号。
执行DELETE语句时:
为删除的每一行保存当前事物的版本号作为行的删除版本号。
执行UPDATE语句时:
为新的行保存当前事物的版本号作为行版本号,同时保存当前事物的版本号作为原来的行的删除版本号。(默认不是真正从磁盘删除了旧的行)
MVCC的优点:
MVCC维持一个数据的多个版本使读写操作没有冲突,对于读而言,不用等待其他同时进行的相同数据写和读的完成。在并发事务中,数据库写只需等待正在对同一行数据进行更新的写,MVCC优化了数据库并发系统,使系统在有大量并发用户时得到最高的性能,并且可以不用关闭服务器就直接进行热备份。