SQL锁与并发控制

并发问题

如果对数据库进行操作的事务是一个接一个地进行,不会带来什么问题,但这样效率很低,所以常常需要同时执行多个事务,如果没有什么控制的话,这种同时执行多个事务的情况就会带来许多问题。

错误的并发调度可能产生3种错误,又称为3类数据不一致。

3类数据不一致

  • 丢失更新(记忆诀窍:读与写之间被插足了)
    事务 T1 读取数据 A,然后对 A 进行运算修改,最后写回数据库。
    如果在 T1 读取和写回数据库之间,有其他事务修改了 A 值,就造成了丢失更新,因为 T1 是在旧的数据上进行的运算。
  • 脏读(记忆诀窍:读被晃点了)
    事务 T1 修改了数据 A,然后事务 T2 读取了数据 A,然后事务 T1 回滚了事务。
    由于事务 T1 回滚了事务,所以数据 A 其实最终并没有被事务 T1 修改,也就是说事务 T2 读取了被丢掉的数据。
  • 不可重复读(记忆诀窍:读与读之间被插足了)
    事务 T1 读取数据 A,然后做某些事,然后又读取数据 A。
    如果在两次读取之间,有其他事务对数据 A 进行了修改,那么就造成了同一事务中,两次读取数据 A 的值不同。

X锁和S锁

X锁(exclusive locks,排他锁)

如果事务 T 对数据 A 加了 X 锁,则:事务 T 可对数据 A 进行读取和修改;其他事务不能对数据 A 加任何锁,也不能读取和修改数据 A。

S锁(share locks,共享锁)

如果事务 T 对数据 A 加了 S 锁,则:事务 T 可对数据 A 进行读取,但不能修改;其他事务也能对数据 A 加 S 锁,但不能加 X 锁。

封锁协议

封锁协议

  1. 一级封锁协议在事务 T 修改数据 A 之前,对数据 A 加上 X 锁,直到事务结束才释放,解决了丢失更新的问题。
  2. 二级封锁协议在一级封锁协议的基础上,在事务 T 读取数据 A 之前,对数据 A 加上 S 锁,直到读取结束才释放,解决了脏读的问题。
  3. 三级封锁协议在一级封锁协议的基础上,在事务 T 读取数据 A 之前,对数据 A 加上 S 锁,直到事务结束才释放,解决了不可重复读的问题。
  4. 两段封锁协议对任何数据进行读写之前必须对该数据加锁,在释放一个封锁之后,事务不再申请和获得任何其他封锁,这样缩短了封锁时间,提高了并发性,同时解决了数据不一致的问题。

事务可分为两个阶段

  1. 生长阶段:也称为扩展阶段(申请锁)在这个阶段事务获得所有需要的封锁,并且不释放任何锁。
  2. 收缩阶段:这阶段事务释放全部锁,并且也不能再获得任何锁。
  • 所有事物均遵从两段协议,则对这些事务的并发调度一定是可串行化的。反过来,在一个可串行调度中,不一定所有事物都遵从两段协议。所有事物都遵从两段协议,是可串行化调度的成分而不是必要条件。

死锁

产生死锁的原因

  1. 产生死锁的原因很多,举个例子:事务 T1 对数据 A 上了 X 锁,现在 T1 要读取数据 B;而另一方面,事务 T2 对数据 B 上了 X 锁,现在 T2 要读取数据 A。T1、T2 都需要对方已经锁定了的数据,相互等待对方释放,这就造成了死锁。
    死锁的解决与避免
  2. 数据库管理系统(比如 SQL Server)识别到死锁后,会选择互锁的事务中的一个来作为牺牲品,回滚并结束该事务,并抛出一条消息。

现在的数据库管理系统都很优秀,死锁很少发生,但很少不代表不会发生,如果我们在事务中注意一下,又可以进一步减少死锁的发生。下面是 SQL Server 中避免死锁发生的建议:

避免死锁发生的建议

  1. 按同一顺序访问对象。比如上述示例中 T1 和 T2 都应先访问数据 A,后访问数据 B,而不是一个先访问数据 A,一个先访问数据 B。
  2. 避免事务中的用户交互。比如事务中不要等待用户输入,因为这种交互可能造成事务执行时间变长,增大了与其他事务竞争资源的可能性。
  3. 保持事务简短并在一个批处理中。
  4. 使用低隔离级别。
  5. 使用绑定连接。使用绑定连接使同一应用程序所打开的两个或多个连接可以相互合作。

参考


本站由 VITAN 使用 Stellar 主题创建。
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。