总结一下 设定事务隔离级别是我们设置数据安全程度的主要手段
隔离级别 脏读 非重复读取 幻像
未提交读 是 是 是
提交读 否 是 是
可重复读 否 否 是
可串行读 否 否 否
而一般情况下 是不推荐设置表锁定的, 特别是低于事务隔离级别的表锁定. 例如在SERIALIZABLE 级别的事务中, 对表操作使用NOLOCK.
于并发相关的另外一个问题是死锁.
死锁是指2个以上的事务之间, 自己锁定了某表,而等待对方释放其它表的锁定这样的死循环.
举个例子:
我们有一个存储过程Pr_PostDataToDatabase
它包含的代码如下:
if @Event = 'AddUser'
begin
insert into table User ...
...
insert into table PersonalInfo ...
end
if @Event ='UpdatePersonalInfo'
begin
Update table User...
....
Update table PersonalInfo ...
end
if @Event = 'DeleteUser'
Begin
Delete PesonalInfo where ...
Delete User where ....
End
...
我们设想,当有2个事务调用这个存储过程对同一个人进行操作, 一个进行Update, 一个进行Delete. 对于Update的事务, 先update了table User并锁定, 然后试图Update PersonalInfo 表的时候, 发现该行已经被事务2锁定了, 于是该事务被阻塞, 并开始等待事务2提交后解除对PersonalInfo表的锁定; 同样,事务2先Delete了 PersonalInfo 表后, 试图去Delete User表的时候, 发现该行已经被事务1锁定了, 于是事务2被阻塞并等待事务1提交以对User表解锁. 这样2个事务之间就陷入了死锁, 谁也无法提交. 并且这个人记录在这2张表内也被死锁了. 这是一个典型的访问顺序问题引起的死锁. 所以. 我们写代码的时候, 要遵循这样一个原则: 在任何事务当中, 都以同样的顺序访问数据表. 这样可以大大降低死锁的可能性.相对于上面的例子, 就是如果3个event中, 对User表的访问顺序永远都排在PersonalInfo的前面, 那么前面说的死锁就不可能发生.
不过实际上, 完全做到所有事务中顺序一致是不可能的. 假如User和PersonalInfo表存在Foreign Key, 比如UserID, 那么Add User时的Insert和Delete User时候的Delete顺序就不能一样的. 这种时候为了避免死锁, 就可能会对事务使用低隔离级别以避免锁定争夺, 对于可能发生的脏读,幻读 就不得不用代码来补救(比如Raise Error).
有一种情况比较特殊, 就是绑定连接. 事务1可以通过先执行 sp_getbindtoken获得绑定令牌(BindToken)字符串,而后执行 sp_bindsession, 创建一个绑定连接. 事务2通过sp_bindsession @TokenString可以加入到这个绑定连接当中, 共享事务1的所有锁而不会引发冲突. 这方面我是没有用过, 有兴趣的可以上网搜一下, 可能分布氏数据库开发当中会用到的多一些.