Locking
谈到并发系统,不得不提锁。数据库系统一般有两种锁。
- shared lock: 写锁
- exclusive lock: 读锁。
下面是锁的特性
- read lock和 write lock不可以同时被获得。
- 多个进程可以对一个对象加写锁.
- 只有一个进程可以获得写锁。
很简单对吧,但是我们还要更深入一点,这是我们的风格,让我们显得更加专业。所以下面我们具体看一看算法的实现吧。
read lock
read_lock(X)
B: if LOCK(X) == 'unlocked'
then begin LOCK(X) <- "read-locked";
num_of_reads(X) <- 1
end
else if(LOCK) == "read-locked"
then num_of_reads(X) <- num_of_reads(X) + 1;
else begin
wait(until LOCK(X) == "unlocked"
and the lock manager wakes up the transaction);
go to B;
end;
write lock
write_lock(X):
B: if LOCK(X) == "unlocked"
then LOCK(X) <- "write-locked"
else begin
wait(until LOCK(X) == "unlocked" and
the lock manager wakes up the transaction);
go to B;
end;
unlock(X)
if LOCK(X) == "write-locked"
then begin LOCK(X) <- "unlocked";
wake up one of the waiting transactions, if any;
end;
else if LOCK(X) == "read-locked"
then begin
num_of_reads(X) <- num_of_reads(X) -1;
if num_of_reads(X) ==0
then begin LOCK(x) == "unlocked";
wake up one of the waiting transactions, if any;
end;
end;
end;
Naive Lock
下面讨论的是具体怎么应用锁。最天真的做法是读写之前获得锁,之后立刻释放。
就上边的情况假设X=20, Y=30。我们分三种执行顺序分析
-
顺序执行 T1, T2 结果:
x=50, y = 80 -
顺序执行 T2, T1 结果:
x=70, y=50 -
交叉执行
结果: x=y=50
根据执行的隔离性,显然1,2两种情况是可以接受的(第三种情况不是按顺序执行的),而第三种情况应该杜绝。
怎么解决呢?
Two Phase locking protocol
为了避免上述的错误,我们可以通过让所有获得锁的语句在任何释放锁语句之前。
好了问题解决了,但是又引入另一个问题。死锁。。。比如下面的执行顺序
Dead Lock
死锁怎么构成的?我们可以简单概括为进程之间互相等待。比如老板等员工干完活发工资。员工等老板发工资后再干活。这就是一个无尽的等待。一般情况下,我们可以使用下面几种方法解决。
- Conservative 2PL. 任何一个事务必须在开始之前声明要获得的所有锁,并且同时要获得它们。注意这是原子性的,即获得所有锁必须一次性完成,不被其他进程干扰。
- 这种方法简单容易让人理解,但是不够有效率
- TIME OUT: 这是主流的方法。如果一个事务在规定时间内没有完成,那么就要终止这个事务。
- dead-lock detection: 这个比较费时间,需要画一个waiting-for图表。如果发现图标中有互相等待现象,就终止一个事务。
像这幅图所示,T1, T2, T3互相等待,随便终止一个,比如T1,那么死锁就被解决了。
总结
今天我们讨论了一下死锁,分析了一些场景,希望你对数据库的理解更加深刻。我之前认为技术就是会用就行,可是现在发现会用有时候不能解决问题,比如我会写SQL,但是我写的和别人的相比,效率却可能差很远,这时候才意识到,其实更应该看本质。
比如之前写socket变成,学了很多API,但是总结了TCP/UDP之后,发现API就算不学,我也能猜出来。这就是学习本质带来的好处。学习本质不费时间,费时间的是学习太多语法糖。