事务与锁

事务 Transaction

事务:是一系列的数据库操作 (a group of SQL statement),是数据库应用的基本逻辑单位。

事务性质:(ACID)

  • 原子性(atomicity)。即不可分割性,事务要么全部被执行,要么就全部不被执行。
  • 一致性或可串性(consistency)。事务的执行使得数据库从一种正确状态转换成另一种正确状态。
  • 隔离性(isolation)。在事务正确提交之前,不允许把该事务对数据的任何改变提供给任何其他事务。
  • 持久性(durability)。事务正确提交后,其结果将永久保存在数据库中,即使在事务提交后有了其他故障,事务的处理结果也会得到保存。
1578903852566
1578903920125
1578904033183
1578904133622
1578904156358
1578904553025
1578904582538
1578904641058
1578904660894
1578904679577

可串行化:多个事务的并发执行是正确的,当且仅当其结果与某一次序串行地执行它们时的结果相同,称这种调度策略是可串行化调度。

1578904744883

并发控制 Concurrency Control

并发控制: 所谓并发控制,是指多用户共享的系统中,许多用户可能同时对同一数据进行操作。

1578904973251
1578905068177
1578905172492
1578905192362
共享锁与排他锁(或叫互斥锁)
1578905290193
1578905458095
两段锁协议(Two-Phase Locking —— 2PL)

https://www.jianshu.com/p/5c78f5c4d57b

两段锁协议规定所有的事务应遵守的规则:

① 在对任何数据进行读、写操作之前,首先要申请并获得对该数据的封锁。

② 在释放一个封锁之后,事务不再申请和获得其它任何封锁。

即事务的执行分为两个阶段:

第一阶段是获得封锁的阶段,称为扩展阶段。

第二阶段是释放封锁的阶段,称为收缩阶段。

若所有事务均遵守两段锁协议,则这些事务的所有交叉调度都是可串行化的。

1578923170158

简单的理解两段锁,就是分为两个阶段: 第一阶段 只能去申请锁 第二阶段 只能去释放锁

https://blog.csdn.net/weixin_38118016/article/details/90271468

举个例子,假设有一个表 t,主键是 id,其中一个字段是 k,在下面的操作中,事务 B 的 update 语句执行时,会是什么现象呢 ?

1695721841348

这个问题的结论取决于事务 A 执行完前两条语句后,持有哪些锁,以及在什么时候释放。

实际上,事务 A 持有两个记录的行锁,都是在 commit 的时候才释放的,所以事务 B 的 update 就会被阻塞,直到事务 A 执行 commit 之后,事务 B 才能被继续执行。也就是说,行锁是在需要的时候才加上的,但并不是不需要了就立刻释放,需要等事务结束时才释放,这就是两阶段锁协议,分为加锁阶段和解锁阶段,所有的 lock 操作都在 unlock 操作之后。

假设你负责实现一个电影票在线交易业务,顾客 userA 要在影院 cinema 购买电影票,需要涉及以下操作:

  1. 扣除顾客 userA 账户余额

  2. 增加影院 cinema 账户余额

  3. 记录一条交易日志

也就是说,完成这次交易,需要 update 两条记录, insert 一条记录。当然为了保证交易的原子性,我们需要这三个操作放在一个事务中。与此同时,还有顾客 userB 也在影院购买电影票,那么你会怎样安排这三个语句在事务中的顺序呢?

首先发现冲突的部分是语句 2,就是两个事务都要给 cinema 的账户余额增加电影票价。根据两阶段协议,不论怎么安排语句,所有的操作需要的行锁都是在事务提交的时候才释放的,要想使行锁在事务中不会停留太长时间,最大程度的减少事务之间的锁等待,应该把语句 2 放在最后面。如下图所示:

img
死锁

死锁:事务循环等待数据锁,则会死锁。 死锁处理:预防死锁协议,死锁恢复机制。

1578905609640

https://blog.csdn.net/weixin_38118016/article/details/90271468

如下图所示,事务 A 在等待事务 B 释放 id = 2 的行锁,而事务 B 在等待 事务 A 释放 id = 1 的行锁,事务 A 和事务 B 在互相等待对方的资源释放,就是进入了死锁状态。

1695722167499

在并发系统中,不同线程出现循环资源依赖,涉及的线程都在等待别的线程释放资源时,就会导致这几个线程进入无限等待的状态,成为死锁。

当进入死锁状态时,有下列 2 种策略:

  1. 设置超时时间,第一个被锁住的事务 A 等待超过多少秒会超时退出(例如50s),其他事务得以执行。然而对于在线服务来说,这个等待时间往往是无法接受的。如果设置太短 (如1s),可能有的事务只是简单的锁等待,就被退出了,会出现很多误伤。

  2. 发起死锁检测,发现死锁之后主动回滚死锁链条中的某一个事务,让其他事务得以继续执行。比如回滚事务 A,让事务 B 继续执行。