关于数据库事务、隔离级别、锁的理解与整理
下方查看历史精选文章
大数据测试过程、策略及挑战
事务(Transaction)
数据库的事务是数据库并发控制的基本单位,一组操作的集合、序列。要么都执行,要么都不执行,是一个不可分割的整体。比如银行的转账,钱从一个账户转移到另一个账户,账户A扣钱账户B加钱,要么都执行,要么都不执行。不可能A扣了钱B没有加钱,也不可能A没扣钱B却加了钱。
数据库的事务应当具有以下四种特性:
Atomic(原子性)
事务中包含的操作被看做一个逻辑单元,这个逻辑单元中的操作要么全部成功,要么全部失败。
Consistency(一致性)
只有合法的数据可以被写入数据库,否则事务应该将其回滚到最初状态。
Isolation(隔离性)
事务允许多个用户对同一个数据进行并发访问,而不破坏数据的正确性和完整性。
同时,并行事务的修改必须与其他并行事务的修改相互独立。
事务的隔离性一般由事务的锁来进行控制。
Durability(持久性)
事务结束后,事务处理的结果必须能够得到固化。
数据库的事务和程序的线程有相似的地方:
1.线程之间共享同一片资源,而事务共享的则是数据库内的数据。
2.多线程的意义在于并发执行,提高效率;事务并发执行也能提高程序与数据库交互的效率。
因此如何使用事务与事务相互之间的隔离级别,直接影响了数据库的并发性和数据的准确性。我们在设计事务和选择隔离级别时这些是应该要考虑的。
选择完隔离级别与设计完事务之后,在使用过程中常常会遇到以下几种情况:
1.更新丢失(Lost update):两个事务同时更新,但是第二个事务却中途失败退出,导致对数据的两个修改都失效了。
2.脏读(Dirty Reads):一个事务开始读取了某行数据,但是另外一个事务已经更新了此数据但没有能够及时提交。这是相当危险的,因为很可能所有的操作都被回滚。
3.不可重复读取(Non-repeatable Reads):一个事务两次读取,但在第二次读取前另一事务已经更新了。
4.虚读(Phantom Reads):一个事务两次读取,第二次读取到了另一事务插入的数据。
5.两次更新问题(Second lost updates problem):两个事务都读取了数据,并同时更新,第一个事务更新失败。
隔离级别(低->高)
● 未授权读取(Read Uncommitted)
允许脏读取,但不允许更新丢失。如果一个事务已经开始写数据,则另外一个数据则不允许同时进行写操作,但允许其他事务读此行数据。该隔离级别可以通过“排他写锁”实现。
● 授权读取(Read Committed)
允许不可重复读取,但不允许脏读取。这可以通过“瞬间共享读锁”和“排他写锁”实现。读取数据的事务允许其他事务继续访问该行数据,但是未提交的写事务将会禁止其他事务访问该行。
● 可重复读取(Repeatable Read)
禁止不可重复读取和脏读取,但是有时可能出现幻影数据。这可以通过“共享读锁”和“排他写锁”实现。读取数据的事务将会禁止写事务(但允许读事务),写事务则禁止任何其他事务。
● 序列化(Serializable)
提供严格的事务隔离。它要求事务序列化执行,事务只能一个接着一个地执行,但不能并发执行。如果仅仅通过“行级锁”是无法实现事务序列化的,必须通过其他机制保证新插入的数据不会被刚执行查询操作的事务访问到。
个人觉得隔离级别的翻译不是很好理解,直接按照英语的意思理解更方便。
eg:
假设账户c1有1000元,c2有1000元,c3有1000元
操作员u1执行一次转账事务m1从c1转移500元到c2,再从c1的余额中转移50%元平均分配到 c1 c2 c3 c4 c5余额中
操作员u2执行一次转账事务m2从c2转移1000元到c1
操作员u3执行一次转账事务m3从c1转移200元到c2
操作员u4开户c4
账户表为T_C,其包含字段为 账户名称cname 余额money
记录为{c1,1000},{c2,1000}
事务m1的操作包括,读c1,读c2,写c1,写c2,提交c1c2,读c1,读c3,写c3,写c3,提交c1c3
事务m2的操作包括,读c2,读c1,写c2,写c1,提交c1c2
事务m3的操作包括,读c1,读c2,写c1,写c2,提交c1c2
事务m4的操作包括,写c4,提交c4
1.若未授权读取ReadUncommitted
m1读c1,c2,写了c1但没写c2此时m2不可以写c2,可以读取c1和c2,但是c2是脏读。隔离级别使用了“排他写锁”。
2.若授权读取ReadCommitted
m1读c1,c2,写了c1但没写c2此时m2不可以写c2,可以读取c1,不能读取c2,因为c2是脏读。隔离级别使用了“排他写锁”。
m1读写了c1,c2,提交c1c2,m3提交了c1c2此时m1准备第二次c1是允许的。隔离级别使用了“瞬间共享读锁”。(但由于第二次读产生了不可重复读的问题,事务1脱力了元自行,因为逻辑上看事务1中被插入了3,影响了c1的余额50%的计算。)
3.若可重复读取RepeatableRead
m1读c1,c2,写了c1但没写c2此时m2不可以写c2,可以读取c1,不能读取c2,因为c2是脏读。隔离级别使用了“排他写锁”。
m1读写了c1,c2,提交c1c2,m4提交了c4此时m3是能读不能写c1并更新提交的。此时m4是能读能能插入c4的。隔离级别使用了“共享读锁”。(和ReadCommitted比RepeatableRead区别对已经提交的事务可以进行读,但不能写,但是同一张表可以插入新的记录。)
4.序列化Serializable
任何事务都只能等前一事务完全执行完再执行。但是失去了并发性。
对于多数应用程序,可以优先考虑把数据库系统的隔离级别设为Read Committed,它能够避免脏读取,而且具有较好的并发性能。尽管它会导致不可重复读、虚读和第二类丢失更新这些并发问题,在可能出现这类问题的个别场合,可以由应用程序采用悲观锁或乐观锁来控制。
选取数据库的隔离级别时,应该注意以下几个处理的原则:
首先,必须排除“未授权读取”,因为在多个事务之间使用它将会是非常危险的。事务的回滚操作或失败将会影响到其他并发事务。第一个事务的回滚将会完全将其他事务的操作清除,甚至使数据库处在一个不一致的状态。很可能一个已回滚为结束的事务对数据的修改最后却修改提交了,因为“未授权读取”允许其他事务读取数据,最后整个错误状态在其他事务之间传播开来。
其次,绝大部分应用都无须使用“序列化”隔离(一般来说,读取幻影数据并不是一个问题),此隔离级别也难以测量。目前使用序列化隔离的应用中,一般都使用悲观锁,这样强行使所有事务都序列化执行。
剩下的也就是在“授权读取”和“可重复读取”之间选择了。我们先考虑可重复读取。如果所有的数据访问都是在统一的原子数据库事务中,此隔离级别将消除一个事务在另外一个并发事务过程中覆盖数据的可能性(第二个事务更新丢失问题)。这是一个非常重要的问题,但是使用可重复读取并不是解决问题的唯一途径。
SQL语句可以使用SET TRANSACTION ISOLATION LEVEL来设置事务的隔离级别。
如:SET TRANSACTION ISOLATION LEVEL Read Committed。
若要在应用程序中使用更严格或较宽松的隔离级别,可以通过使用 set transaction isolation level语句设置会话的隔离级别,来自定义整个会话的锁定。指定隔离级别后,sql server会话中所有select语句的锁定行为都运行于该隔离级别上,并一直保持有效直到会话终止或者将隔离级别设置为另一个级别。
锁(并发控制的手段)
独占锁(排他锁)只允许一个事务访问数据
共享锁允许其他事务继续使用锁定的资源
更新锁
锁就是保护指定的资源,不被其他事务操作,锁定的资源包括行、页、簇、表和数据库。为了最小化锁的成本,SQL Server自动地以与任务相应等级的锁来锁定资源对象。锁定比较小的对象,例如锁定行,虽然可以提高并发性,但是却有较高的开支,因为如果锁定许多行,那么需要占有更多的锁。锁定比较大的对象,例如锁定表,会大大降低并发性,因为锁定整个表就限制了其他事务访问该表的其他部分,但是成本开支比较低,因为只需维护比较少的锁。
设置事务级别:SET TRANSACTION ISOLATION LEVEL
开始事务:begin tran
提交事务:COMMIT
回滚事务:ROLLBACK
创建事务保存点:SAVE TRANSACTION savepoint_name
回滚到事务点:ROLLBACK TRANSACTION savepoint_name