数据库事务并发问题
数据库的操作通常为写和读,就是所说的CRUD:增加(Create)、读取(Read)、更新(Update)和删除(Delete)。
事务就是一件完整要做的事情。
事务是恢复和并发控制的基本单位。
事务必须始终保持系统处于一致的状态,不管在任何给定的时间并发事务有多少。
事务在关系数据库中,一个事务可以是一条SQL语句,一组SQL语句或整个程序。是数据库中各种数据项的一个程序执行单元。
事务是用户定义的一个操作序列(多个表同时读写)。这些操作要么都做,要么都不做,是一个不可分割的工作单位。
事务通常是以BEGIN TRANSACTION开始,以COMMIT或ROLLBACK结束。
COMMIT:表示事务完成提交,即提交事务的所有操作,具体地说就是将事务中所有对数据库的更新写回到磁盘上的物理数据库中去,事务正常结束。
ROLLBACK:表示事务的回滚,即在事务运行的过程中发生了某种故障,事务不能继续进行,系统将事务中对数据库的所有以完成的操作全部撤消,滚回到事务开始的状态或设置的回滚点。
多个事务同时访问数据库时候,会发生下列5类问题,包括3类数据读问题(脏读,不可重复读,幻读),2类数据更新问题(第一类丢失更新,第二类丢失更新):
(http://blog.csdn.net/zhangzeyuaaa/article/details/46400419 该博文有具体例子)
第一类丢失更新
A事务撤销时,把已经提交的B事务的更新数据覆盖了。这种错误可能造成很严重的问题,通过下面的账户取款转账就可以看出来:
时间 |
取款事务A |
转账事务B |
T1 |
开始事务 |
|
T2 |
|
开始事务 |
T3 |
查询账户余额为1000元 |
|
T4 |
|
查询账户余额为1000元 |
T5 |
|
汇入100元把余额改为1100元 |
T6 |
|
提交事务 |
T7 |
取出100元把余额改为900元 |
|
T8 |
撤销事务 |
|
T9 |
余额恢复为1000 元(丢失更新) |
|
A事务在撤销时,“不小心”将B事务已经转入账户的金额给抹去了。
第二类丢失更新
A事务覆盖B事务已经提交的数据,造成B事务所做操作丢失:
时间 |
转账事务A |
取款事务B |
T1 |
|
开始事务 |
T2 |
开始事务 |
|
T3 |
|
查询账户余额为1000元 |
T4 |
查询账户余额为1000元 |
|
T5 |
|
取出100元把余额改为900元 |
T6 |
|
提交事务 |
T7 |
汇入100元 |
|
T8 |
提交事务 |
|
T9 |
把余额改为1100 元(丢失更新) |
|
上面的例子里由于支票转账事务覆盖了取款事务对存款余额所做的更新,导致银行最后损失了100元,相反如果转账事务先提交,那么用户账户将损失100元。
共享锁和排它锁
为了解决并发问题,数据库系统引入锁机制。
基本的封锁类型有两种: 排它锁(Exclusive locks 简记为X锁) 和 共享锁(Share locks 简记为S锁)。
- 排它锁又称为写锁。若事务T对数据对象A加上X锁,则只允许T读取和修改A,其它任何事务都不能再对A加任何类型的锁,直到T释放A上的锁。这就保证了其它事务在T释放A上的锁之前不能再读取和修改A。
- 共享锁又称为读锁。若事务T对数据对象A加上S锁,则事务T可以读A但不能修改A,其它事务只能再对A加S锁,而不能加X锁,直到T释放A上的S锁。这就保证了其它事务可以读A,但在T释放A上的S锁之前不能对A做任何修改。
封锁粒度
数据库中为了实现并发控制而采用封锁技术。
封锁对象的大小称为封锁粒度(Granularity)。
封锁的对象可以是逻辑单元,也可以是物理单元。以关系数据库为例子,封锁对象可以是这样一些逻辑单元:属性值、属性值的集合、元组、关系、索引项、整个索引项直至整个数据库;也可以是这样的一些物理单元:页(数据页或索引页)、物理记录等。
封锁协议与隔离级别
在运用 排他锁 和 共享锁 对数据对象加锁时,还需要约定一些规则,例如何时申请 排他锁 或 共享锁、持锁时间、何时释放等。称这些规则为封锁协议(Locking Protocol)。对封锁方式规定不同的规则,就形成了各种不同的封锁协议。不同的封锁协议对应不同的隔离级别。
一级封锁协议(对应read uncommited)
一级封锁协议是:事务T在修改数据R之前必须先对其加X锁,直到事务结束才释放。事务结束包括正常结束(COMMIT)和非正常结束(ROLLBACK)。
一级封锁协议可防止丢失更新,并保证事务T是可恢复的。
在一级封锁协议中,如果仅仅是读数据不对其进行修改,是不需要加锁的,所以它不能保证可重复读和不读“脏”数据。
二级封锁协议(对应read commited)
二级封锁协议是:一级封锁协议加上事务T在读取数据R之前必须先对其加S锁,读完后即可释放S锁(瞬间S锁)。
二级封锁协议除防止了丢失更新,还可进一步防止读“脏”数据。
三级封锁协议(对应reapetable read)
三级封锁协议是:一级封锁协议加上事务T在读取数据R之前必须先对其加S锁,直到事务结束才释放。
三级封锁协议除防止了丢失更新和不读‘脏’数据外,还进一步防止了不可重复读和覆盖更新。
四级封锁协议(对应serialization)
四级封锁协议是对三级封锁协议的增强,其实现机制也最为简单,直接对 事务中 所 读取 或者 更改的数据所在的表加表锁,也就是说,其他事务不能 读写 该表中的任何数据。这样五类并发问题都得以避免!
注:封锁协议和隔离级别并不是严格对应的。
ANSI SQL 92标准定义了4个等级的事务隔离级别:SELECT @@tx_isolation;
SELECT @@global.tx_isolation;
SELECT @@session.tx_isolation;
SQL的标准定义了4个隔离级别,设定一定的规则,限定事务内部的那些改变是可见的,那些是不可见的。低级别的事务一般会支持更高的并发处理,涉及更低的系统开销。
Read Uncommitted【读未提交数据】:允许所有事务读取未被其他事务提交的数据修改。会导致脏读、不可重复读和幻读的问题的出现。
Read Committed【读已提交数据】:只允许事务读取已经被其他事务提交的数据修改。Oracle和sql server默认的级别,可以避免脏读,但不可重复读和幻读问题仍然会出现。
Repeatable Read【可重读】:是MySQL的默认事务隔离级别,它确保同一事务的多个实例在并发读取数据时,会看到同样的数据行。但幻读问题未解除。
Serializable【可串行化】:最高的隔离级别,它通过强制事务排序,使之不可能相互冲突,从而解决幻读问题。它是在每个读的数据行上加上共享锁。在这个级别,可能导致大量的超时现象和锁竞争。
悲观锁(Pessimistic Lock), 顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。
乐观锁(Optimistic Lock), 顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库如果提供类似于write_condition机制的其实都是提供的乐观锁。
两种锁各有优缺点,不可认为一种好于另一种,像乐观锁适用于写比较少的情况下,即冲突真的很少发生的时候,这样可以省去了锁的开销,加大了系统的整个吞吐量。但如果经常产生冲突,上层应用会不断的进行retry,这样反倒是降低了性能,所以这种情况下用悲观锁就比较合适。