并发控制
1. 并发冲突
当两个进程试图在同一时间修改同一数据,就会产生冲突。
2. 并发控制
有两种方式管理并发数据访问:乐观并发控制、悲观并发控制。
这两种控制模式的区别在于,是在冲突发生前进行防止,还是在发生后采用某种方法来处理冲突。
3. 悲观并发控制
悲观并发模式假定系统中存在足够多的数据修改操作,以致任何确定的读操作都可能会受到由别的用户所制造的数据修改的影响。
也就是说,悲观并发模式假定冲突总是会发生的。
悲观并发控制是通过独占正在被读取的数据来避免冲突。
但是独占数据会导致其它进程无法修改该数据,进而产生阻塞——读数据和写数据会互相阻塞。
4. 乐观并发控制
乐观并发模式假定系统的数据修改操作只会生产非常少的冲突,也就是说任何进程都不太可能修改别的进程正在访问的数据。
乐观并发模式下,读数据和写数据之间不会发生冲突,只有写数据与写数据之间会发生冲突。即读数据不会产生阻塞,只有写数据才会产生阻塞。
5. 并发冲突生产的问题
5.1. 丢失更新(Lost updates)
两个进程同时读取一笔数据,然后进行修改,那么后提交的数据会覆盖先提交的数据。
如果数据允许覆盖式更新(比如用户姓名),那么丢失更新并不算太大的问题,如果数据是累加式更新(比如库存数量),那么丢失更新是非常严重的问题,并且在非并发模式下无法重复问题的发生。
5.2. 脏读(Dirty reads)
当一个进程更新了数据,但(事务)未提交,这时候另一个进程读取同一笔数据,如果前一个进程取消了更新(事务回滚),那么后一个进程读取的就是脏数据。
脏读会产生严重的问题,在任何情况下都是不允许的。
5.3. 不可重复读(Non-repeatable reads)
当一个进程读取了一笔数据后,另一个进程更新了同一笔数据,然后第一个进程再次读取同一笔数据,却得到了与第一次读取不同的结果。
在事务A更新记录之后(update Customers set Name = 'B' where Name = 'A'),事务B读取相同记录(select Name form Customers where Name = 'A'),但事务B拿到的是事务A更新之后的数据(Customers.Name的值为'B'),在事务B读取记录之后,事务A进行了事务回滚(Customers.Name的值为'A'),导致事务B的数据是不真实的。
5.4. 幻读(Phantoms)
幻读与脏读的相似之处在于:两者都是两次读取的结果不一致。
不同之处在于:幻读是两次读取的记录数量不一致,而脏读是两次读取的记录的数据不一致。
事务A读取记录之后(select * from Customers where Name like 'A%'),事务B又插入了符合事务A读取条件的新记录(insert into Customers(Name) values('AAA')),那么当事务A再用相同条件读取记录时,得到的集合却与上一次读取不同(多了记录)。
6. 隔离级别
SQL Server2005支持5种隔离级别来控制冲突。其中三种只在悲观并发模式中使用,一种只在乐观并发模式中使用,另一个可以在两种模式中使用。
6.1. 未提交读(Uncommitted Read)
未提交读只能防止“丢失更新”问题,其它问题不能防止。
未提交读是针对阻塞太频繁的悲观并发控制,因为它只是忽略了锁,而不保障事务的一致性。
6.2. 已提交读(Read Committed)
已提交读既可以是乐观的也可以是悲观的,这取决于数据 库的read_committed_snapshot设置。默认情况下这个选项是关闭的,所以该隔离级别默认情况下是采用悲观并发控制。
已提交读可以防止脏读问题。
6.3. 可重复读(Repeatable Read)
可重复读是一种悲观的隔离级别。它在已提交读的基础上增加了新特性:确保当事务重新访问数据或查询被再一次执行时,数据将不会再发生改变。
可重复读不但可以防止脏读问题,还可以防止不可重复读问题,但是不能防止幻读问题。
注意,可重复读的资源开销是很大的,事务中所有的数据必须等待事务完成之后才能访问。
6.4. 快照(Snapshot)
快照是一种乐观隔离级别。
Snapshot事务中任何语句所读取的记录,都是事务启动时的数据。
这相当于事务启动时,数据库为事务生成了一份专用“快照”。
在当前事务中看到不其它事务在当前事务启动之后所进行的数据修改。
Snapshot事务不会读取记录时要求锁定,读取记录的Snapshot事务不会锁住其它事务写入记录,写入记录的事务也不会锁住Snapshot事务读取数据。
快照隔离级别的事务不是串行执行的,两个进程同时使用快照隔离,如果它们执行多次,可能最终产生的结果不会一致。(这段话要证实)
6.5. 可串行化(Serializable)
可串行化是一种悲观隔离级别。它在可重复读的基础上增加了新的特性:确保在两次查询的中间,不会增加新的行。
可串行化是最健壮的悲观隔离级别,因为它防止了并发冲突产生的4个问题。
可串行化也是资源开销最大的措施。当使用可串行化隔离时,如果SQL的条件字段没有索引,那么SQL Server会产生表级锁。
6.6. 总结
模式 | 脏读Dirty Read | 不可重复读Non-Repeatable Read | 幻读Phantom Read | 并发控制模式 |
---|---|---|---|---|
未提交读READ UNCOMMITTED | 会 | 会 | 会 | 悲观 |
可提交读READ COMMITTED(默认选项) | 不会 | 会 | 会 | 悲观 |
可提交读READ COMMITTED(Snapshot选项) | 不会 | 会 | 会 | 乐观 |
可重复读REPEATABLE READ | 不会 | 不会 | 会 | 悲观 |
快照SNAPSHOT | 不会 | 不会 | 不会 | 乐观 |
可串行化SERIALIZABLE | 不会 | 不会 | 不会 | 悲观 |
7. 锁
7.1. 死锁
当二或多个工作各自具有某个资源的锁定,但其它工作尝试要锁定此资源,而造成工作永久封锁彼此时,会发生死锁。例如:
1. 事务 A 取得数据列 1 的共享锁定。
2. 事务B 取得数据列 2 的共享锁定。
3. 事务A 现在要求数据列 2 的独占锁定,但会被封锁直到事务B 完成并释出对数据列 2 的共享锁定为止。
4. 事务B 现在要求数据列 1 的独占锁定,但会被封锁直到事务A 完成并释出对数据列 1 的共享锁定为止。
等到事务B 完成后,事务A 才能完成,但事务B 被事务A 封锁了。这个状况也称为「循环相依性」(Cyclic Dependency)。事务A 相依于事务B,并且事务B 也因为相依于事务A 而封闭了这个循环。
例如以下操作就会产生死锁,两个连接互相阻塞对方的update。
连接1:
begin tran
select * from customers
update customers set CompanyName = CompanyName
waitfor delay '00:00:05'
select * from Employees
–因为Employees被连接2锁住了,所以这里会阻塞。
update Employees set LastName = LastName
commit tran
连接2:
begin tran
select * from Employees
update Employees set LastName = LastName
waitfor delay '00:00:05'
select * from customers
--因为customers被连接1锁住了,所以这里会阻塞。
update customers set CompanyName = CompanyName
commit tran
SQL Server遇到死锁时会自动杀死其中一个事务,而另一个事务会正常结束(提交或回滚)。
SQL Server对杀死的连接返回错误代码是1205,异常提示是:
Your transaction (process ID #52) was deadlocked on {lock | communication buffer | thread} resources with another process and has been chosen as the deadlock victim. Rerun your transaction.
除了Read Uncommitted和Snapshot,其它类型的事务都可能产生死锁。
7.2. 悲观锁
悲观锁是指假设并发更新冲突会发生,所以不管冲突是否真的发生,都会使用锁机制。
悲观锁会完成以下功能:锁住读取的记录,防止其它事务读取和更新这些记录。其它事务会一直阻塞,直到这个事务结束。
悲观锁是在使用了数据库的事务隔离功能的基础上,独享占用的资源,以此保证读取数据一致性,避免修改丢失。
悲观锁可以使用Repeatable Read事务,它完全满足悲观锁的要求。
7.3. 乐观锁
乐观锁不会锁住任何东西,也就是说,它不依赖数据库的事务机制,乐观锁完全是应用系统层面的东西。
如果使用乐观锁,那么数据库就必须加版本字段,否则就只能比较所有字段,但因为浮点类型不能比较,所以实际上没有版本字段是不可行的。
7.4. 悲观离线锁
悲观离线锁是应用程序级别的机制,它是由应用程序实现的,不是数据库实现的。