1、前言
这次我们只谈数据库。以往遇到并发事务问题,总想着站在应用层面考虑问题,该如何实现悲观锁和乐观锁?但并不很清楚数据库中底层的实现原理。找几个晚上重新温习了一下大学课堂上的《数据库系统概览》,借这篇文章记下来这次读书的概要。
2、四个特性 ACID
如果想要说明一个数据库或者一个框架支持事务性操作,则必须要满足下面的四大特性。
- 原子性(Atomicity):原子性是指事务是一个不可再分割的工作单元,事务中的操作要么都发生,要么都不发生。
- 一致性(Consistency):一致性是指在事务开始之前和事务结束以后,数据库的完整性约束没有被破坏。这是说数据库事务不能破坏关系数据的完整性以及业务逻辑上的一致性。
- 隔离性(Isolation):多个事务并发访问时,事务之间是隔离的,一个事务不应该影响其它事务运行效果。
- 持久性(Durability):持久性是指事务的操作,一旦提交,对于数据库中数据的改变是永久性的,即使数据库发生故障也不能丢失已提交事务所完成的改变。
3、四种隔离级别
在数据库事务的ACID四个属性中,隔离性是一个最常放松的一个。可以在数据操作过程中利用数据库的锁机制或者多版本并发控制机制获取更高的隔离等级。但是,随着数据库隔离级别的提高,数据的并发能力也会有所下降。所以,如何在并发性和隔离性之间做一个很好的权衡就成了一个至关重要的问题。
3.1、并发问题
实际开发过程中,我们绝大部分的事务都是有并发情况。当多个事务并发运行,经常会操作相同的数据来完成各自的任务。在这种情况下可能会导致以下的问题:
- 脏读:指一个事务A正在访问数据,并且对该数据进行了修改,但是这种修改还没有提交到数据库中(也可能因为某些原因Rollback了)。这时候另外一个事务B也访问这个数据,然后使用了这个被A修改的数据,那么这个数据就是脏的,并不是数据库中真实的数据。这就被称作脏读。
- 不可重复读: 事务 A 多次读取同一数据,事务 B 在事务A多次读取的过程中,对数据作了更新并提交,导致事务A多次读取同一数据时,结果不一致。
- 幻读:指一个事务A对一个表中的数据进行了修改,而且该修改涉及到表中所有的数据行;同时另一个事务B也在修改表中的数据,该修改是向表中插入一行新数据。那么经过这一番操作之后,操作事务A的用户就会发现表中还有没修改的数据行,就像发生了幻觉一样。
不可重复读的和幻读很容易混淆,不可重复读侧重于修改,幻读侧重于新增或删除。
3.2、隔离级别
ANSI/ISO SQL定义的标准隔离级别有四种,从低到高依次为:未提交读(Read uncommitted) < 提交读(Read committed) < 可重复读(Repeatable reads) < 序列化(Serializable)。
- 未提交读(Read Uncommitted):一个事务在执行过程中,既可以访问其他事务未提交的新插入的数据,又可以访问未提交的修改数据。如果一个事务已经开始写数据,则另外一个事务不允许同时进行写操作,但允许其他事务读此行数据。此隔离级别可防止丢失更新。
- 已提交读(Read Committed):一个事务在执行过程中,既可以访问其他事务成功提交的新插入的数据,又可以访问成功修改的数据。读取数据的事务允许其他事务继续访问该行数据,但是未提交的写事务将会禁止其他事务访问该行。此隔离级别可有效防止脏读。
- 可重复读(Repeatable Read):一个事务在执行过程中,可以访问其他事务成功提交的新插入的数据,但不可以访问成功修改的数据。读取数据的事务将会禁止写事务(但允许读事务),写事务则禁止任何其他事务。此隔离级别可有效防止不可重复读和脏读。
- 序列化(Serializable):提供严格的事务隔离。它要求事务序列化执行,事务只能一个接着一个地执行,不能并发执行。此隔离级别可有效防止脏读、不可重复读和幻读。但这个级别可能导致大量的超时现象和锁竞争,在实际应用中很少使用。
mysql:默认隔离级别是 Repeatable Read,会出现幻读的问题。oracle/sql server:默认隔离级别是 Read Committed,会出现不可重复读和幻读的问题。
3.3、间隙锁
产生幻读的原因是,行锁只能锁住行,但是新插入记录这个动作,要更新的是记录之间的“间隙”。因此,为了解决幻读问题,mysql InnoDB 只好引入新的锁,也就是间隙锁 (GapLock)。间隙锁,锁的就是两个值之间的空隙。值得注意的是,间隙锁只在隔离级别是Repeatable read 下才会生效。
- 行锁(Record Lock):锁直接加在索引记录上面。
- 间隙锁(Gap Lock):锁加在不存在的空闲空间,可以是两个索引记录之间,也可能是第一个索引记录之前或者最后一个索引之后的空间。
- Next-Key Lock:行锁与间隙锁组合起来用就叫做Next-Key Lock。
默认情况下,InnoDB工作在"可重复读"的隔离情况下,并且以Next-Key Lock的方式对数据进行加锁,这样就可以有效地防止"幻读"的发生。Next-key Lock是行锁与间隙锁的组合,这样,当InnoDB扫描索引记录的时候,会首先对选中的索引记录加上行锁(Record Lock),再对索引记录两边的间隙加上间隙锁(Gap Lock)。如果一个间隙被事务T1加了锁,其他事务是不能在这个间隙插入记录的。例如下面的sql,会对id取值范围大于100的间隙加锁。当增删数据库时如果id大于100则会阻塞住,如果小于100则不会有影响。
select * from emp where id > 100 for update;
4、锁
锁是数据库系统区别于文件系统的一个关键特性,锁机制用于管理对共享资源的并发访问。粗略的分有行级锁、表级锁和页级锁,行级锁再分共享锁和排他锁等。要记住悲观锁和乐观锁是两个抽象的概念,并非实际数据库中的锁。
4.1、共享锁
共享锁(S锁):允许一个事务去读一行,会阻止其他事务获取相同数据集的排他锁(读取数据的时候不允许修改),也被称为读锁。当没有其他线程对查询结果集中的任何一行使用排他锁时,可以成功申请共享锁,否则会被阻塞。其他线程也可以读取使用了共享锁的表,而且这些线程读取的是同一个版本的数据。
mysql的共享锁支持行级锁,在查询语句后面增加LOCK IN SHARE MODE,mysql会对查询结果中的每行都加共享锁。
SELECT ... LOCK IN SHARE MODE;
oracle的共享锁不支持行级锁,只能是表级锁。
LOCK TABLE <表名>[,<表名>]... IN SHARE MODE [NOWAIT]
有一些误区需要记住,单纯的查询SELECT不是共享锁,实际上是没有上任何锁。而 SELECT ... FOR UPDATE 是排他锁。
4.2、排他锁
排他锁(X锁):允许获取排他锁去做更新操作,阻止其他事务获取相同数据的共享锁和排他锁(一个事务修改数据的时候,阻止其他事务对相同数据集做更新或者查询操作),也被称为写锁。
排他锁的使用方式比较一致,新增、更新和删除等DML操作语句会自动加上排他锁。而且SELECT ... FOR UPDATE 也是排他锁。
4.3、意向锁
为了允许行锁和表锁共存,实现多粒度锁机制,InnoDB还有两种内部使用的意向锁(Intention Locks),这两种意向锁都是表锁:
意向共享锁(IS):事务打算给数据行加共享锁,事务在给一个数据行加共享锁前必须先取得该表的IS锁。
意向排他锁(IX):事务打算给数据行加排他锁,事务再给一个数据行加排他锁前必须先取得该表的IX锁。
意向锁是数据库隐式帮我们做了,我们不需要操心。
4.4、悲观锁和乐观锁(抽象)
悲观锁:是从数据库层面加锁。总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它释放锁。
乐观锁:总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据。
那么在数据库层面上,我们利用锁来实现事务的,无论是共享锁还是排他锁,都是悲观锁。而乐观锁是通过业务来实现,并不会利用到数据库的锁。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。