数据库并发事务中的问题与解决方案

一、事务的4大特性(ACID)


1. 原子性(Atomicity)

事务是数据库的逻辑工作单位,它对数据库的修改要么全部执行,要么全部不执行。

2. 一致性(Consistemcy)

事务前后,数据库的状态都满足所有的完整性约束。

3. 隔离性(Isolation)

并发执行的事务是隔离的,不会相互影响。如果有两个事务,运行在相同的时间内,执行相同的功能,事务的隔离性将确保每一事务在系统中认为只有该事务在使用系统。这种属性有时称为串行化,为了防止事务操作间的混淆,必须串行化或序列化请求,使得在同一时间仅有一个请求用于同一数据。通过设置数据库的隔离级别,可以达到不同的隔离效果。

4. 持久性(Durability)

指的是只要事务成功结束,它对数据库所做的更新就必须永久保存下来。即使发生系统崩溃,重新启动数据库系统后,数据库还能恢复到事务成功结束时的状态。

二、并发事务引起的问题


1. 更新丢失(Lost Update)

两个事务都同时更新一行数据,但是第二个事务却中途失败退出,导致对数据的两个修改都失效了。这是因为系统没有执行任何的锁操作,因此并发事务并没有被隔离开来。

2. 脏读(Dirty Read)

又称无效数据读出。一个事务读取另外一个事务还没有提交的数据叫脏读。
例如:事务T1修改了一行数据,但是还没有提交,这时候事务T2读取了被事务T1修改后的数据,之后事务T1因为某种原因Rollback了,那么事务T2读取的数据就是脏的。

3. 不可重复读(Non-Repeatable Read)

是指在一个事务中两次读同一行数据,可是这两次读到的数据不一样。
例如:事务T1读取某一数据,事务T2读取并修改了该数据,T1为了对读取值进行检验而再次读取该数据,便得到了不同的结果。

4. 幻读

事务在操作过程中进行两次查询,第二次查询的结果包含了第一次查询中未出现的数据或者缺少了第一次查询中出现的数据
例如:系统管理员A将数据库中所有学生的成绩从具体分数改为ABCDE等级,但是系统管理员B就在这个时候插入了一条具体分数的记录,当系统管理员A改结束后发现还有一条记录没有改过来,就好像发生了幻觉一样。这就叫幻读。

不可重复读重点在于update和delete,而幻读的重点在于insert。所以说不可重复读和幻读最大的区别,就在于如何通过锁机制来解决他们产生的问题。

三、事务的隔离级别

以上的4种问题(更新丢失、脏读、不可重复读、幻读)都和事务的隔离级别有关。通过设置事务的隔离级别,可以避免上述问题的发生。


1. 读未提交(Read Uncommitted)

读事务不阻塞其他读事务和写事务,未提交的写事务阻塞其他写事务但不阻塞读事务。
此隔离级别可以防止更新丢失,但不能防止脏读、不可重复读、幻读。
此隔离级别可以通过“排他写锁”实现。

2. 读已提交(Read Committed)

读事务允许其他读事务和写事务,未提交的写事务禁止其他读事务和写事务。
此隔离级别可以防止更新丢失、脏读,但不能防止不可重复读、幻读。
此隔离级别可以通过“瞬间共享读锁”和“排他写锁”实现。

3. 可重复读取(Repeatable Read)

以操作同一行数据为前提,读事务禁止其他写事务但不阻塞读事务,未提交的写事务禁止其他读事务和写事务。
此隔离级别可以防止更新丢失、脏读、不可重复读,但不能防止幻读。
此隔离级别可以通过“共享读锁”和“排他写锁”实现。

4. 序列化(Serializable)

提供严格的事务隔离,它要求事务序列化执行,事务只能一个接着一个地执行,不能并发执行。
此隔离级别可以防止更新丢失、脏读、不可重复读、幻读。
如果仅仅通过“行级锁”是无法实现事务序列化的,必须通过其他机制保证新插入的数据不会被刚执行查询操作的事务访问到。

可串行化:如果一个并行调度的结果等价于某一个串行调度的结果,那么这个并行调度是可串行化的。

隔离级别越高,越能保证数据的完整性和一致性,但是对并发性能的影响也越大。对于多数应用程序,可以优先考虑把数据库系统的隔离级别设为Read Committed。它能够避免更新丢失、脏读,而且具有较好的并发性能。尽管它会导致不可重复读、幻读这些并发问题,在可能出现这类问题的个别场合,可以由应用程序采用悲观锁或乐观锁来控制。


事务隔离级别 回滚覆盖 脏读 不可重复读 提交覆盖 幻读
读未提交 x 可能发生 可能发生 可能发生 可能发生
读已提交 x x 可能发生 可能发生 可能发生
可重复读 x x x x 可能发生
串行化 x x x x x


四、常用的解决方案


1. 版本检查

在数据库中保留“版本”字段,跟随数据同时读写,以此判断数据版本。版本可能是时间戳或状态字段。
下例中的 WHERE 子句就实现了简单的版本检查:
UPDATE table SET status = 1 WHERE id=1 AND status = 0;

版本检查能够作为“乐观锁”,解决更新丢失的问题。


2. 锁

2.1 共享锁与排它锁

共享锁(Shared locks, S-locks)
共享锁又称读锁,是读取操作创建的锁。其他用户可以并发读取数据,但任何事务都不能对数据进行修改(获取数据上的排他锁),直到已释放所有共享锁。
能给未加锁和添加了S锁的对象添加S锁。对象可以接受添加多把S锁。
如果事务T对数据A加上共享锁后,则其他事务只能对A再加共享锁,不能加排他锁。获准共享锁的事务只能读数据,不能修改数据。
用法:
SELECT ... LOCK IN SHARE MODE;
在查询语句后面增加LOCK IN SHARE MODE,Mysql会对查询结果中的每行都加共享锁,当没有其他线程对查询结果集中的任何一行使用排他锁时,可以成功申请共享锁,否则会被阻塞。其他线程也可以读取使用了共享锁的表,而且这些线程读取的是同一个版本的数据。

排它锁(Exclusive locks, X-locks)
排他锁又称写锁,如果事务T对数据A加上排他锁后,则其他事务不能再对A加任任何类型的封锁。获得排他锁的事务既能读数据,又能修改数据。
只能给未加锁的对象添加X锁。对象只能接受一把X锁。加X锁的对象不能再加任何锁。
用法:
SELECT ... FOR UPDATE;
在查询语句后面增加FOR UPDATE,Mysql会对查询结果中的每行都加排他锁,当没有其他线程对查询结果集中的任何一行使用排他锁时,可以成功申请排他锁,否则会被阻塞。

对于insert、update、delete,InnoDB会自动给涉及的数据加排他锁(X);对于一般的Select语句,InnoDB不会加任何锁,事务可以通过以下语句给显示加共享锁或排他锁。
共享锁:SELECT ... LOCK IN SHARE MODE;
排他锁:SELECT ... FOR UPDATE;

2.2 意向锁

InnoDB还有两个表锁:
意向共享锁(IS):表示事务准备给数据行加入共享锁,也就是说一个数据行加共享锁前必须先取得该表的IS锁。
意向排他锁(IX):类似上面,表示事务准备给数据行加入排他锁,说明事务在一个数据行加排他锁前必须先取得该表的IX锁。
意向锁是InnoDB自动加的,不需要用户干预。

2.3 临时锁与持续锁

锁的时效性,指明了加锁生效期是到当前语句结束还是当前事务结束。

2.4 表级锁与行级锁

锁的粒度,指明了加锁的对象是当前表还是当前行。

2.5 悲观锁与乐观锁

悲观锁(Pessimistic Locking)

悲观锁假定当前事务操纵数据资源时,肯定还会有其他事务同时访问该数据资源,为了避免当前事务的操作受到干扰,先锁定资源。悲观锁需使用数据库的锁机制实现,如使用行级排他锁或表级排它锁。
尽管悲观锁能够防止丢失更新和不可重复读这类问题,但是它非常影响并发性能,因此应该谨慎使用。

乐观锁(Optimistic Locking)

乐观锁假定当前事务操纵数据资源时,不会有其他事务同时访问该数据资源,因此不在数据库层次上的锁定。乐观锁使用由程序逻辑控制的技术来避免可能出现的并发问题。
唯一能够同时保持高并发和高可伸缩性的方法就是使用带版本检查的乐观锁。
乐观锁不能解决脏读的问题,因此仍需要数据库至少启用“读已提交”的事务隔离级别。

3. 三级加锁协议

三级加锁协议也称为三级封锁协议,是为了保证正确的调度事务的并发操作,事务在对数据库对象加锁,解锁是必须遵守的一种规则。

3.1 一级加锁协议

事务在修改数据前必须加X锁,直到事务结束(事务结束包括正常结束(COMMIT)和非正常结束(ROLLBACK))才可释放;如果仅仅是读数据,不需要加锁。
如下例:
SELECT xxx FOR UPDATE;
UPDATE xxx;
一级封锁协议可以防止丢失修改,并保证事务T是可恢复的。使用一级封锁协议可以解决丢失修改问题。
在一级封锁协议中,如果仅仅是读数据不对其进行修改,是不需要加锁的,它不能保证可重复读和不读“脏”数据。

3.2 二级加锁协议

满足一级加锁协议,且事务在读取数据之前必须先加S锁,读完后即可释放S锁。
二级封锁协议除防止了丢失修改,还可以进一步防止读“脏”数据。但在二级封锁协议中,由于读完数据后即可释放S锁,所以它不能保证可重复读。

3.3 三级加锁协议

满足一级加锁协议,且事务在读取数据之前必须先加S锁,直到事务结束才释放。
三级封锁协议除防止了丢失修改和不读“脏”数据外,还进一步防止了不可重复读。

上述三级协议的主要区别在于什么操作需要申请封锁,以及何时释放。

4. 两段锁协议(2-phase locking)

两段锁协议是指每个事务的执行可以分为两个阶段:生长阶段(加锁阶段)和衰退阶段(解锁阶段)。
加锁阶段:在该阶段可以进行加锁操作。在对任何数据进行读操作之前要申请并获得S锁,在进行写操作之前要申请并获得X锁。加锁不成功,则事务进入等待状态,直到加锁成功才继续执行。
解锁阶段:当事务释放了一个封锁以后,事务进入解锁阶段,在该阶段只能进行解锁操作不能再进行加锁操作。

若并发执行的所有事务均遵守两段锁协议,则对这些事务的任何并发调度策略都是可串行化的。
遵循两段锁协议的事务调度处理的结果是可串行化的充分条件,但是可串行化并不一定遵循两段锁协议。

两段锁协议和防止死锁的一次封锁法的异同之处,一次封锁法要求每个事务必须一次将所有要使用的数据全部加锁,否则就不能继续执行,因此一次封锁法遵守两段锁协议;但是两段锁协议并不要求事务必须一次将所有要使用的数据全部加锁,因此遵守两段锁协议的事务可能发生死锁。


五、不同的事务隔离级别与其对应可选择的加锁协议


事务隔离级别 加锁协议
读未提交 一级加锁协议
读已提交 二级加锁协议
可重复读 三级加锁协议
串行化 两段锁协议

封锁协议和隔离级别并不是严格对应的。

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