算法:乐观锁与悲观锁
算法:乐观锁与悲观锁
悲观锁
说明
悲观锁,正如其名,具有强烈的独占和排他特性。它指的是对数据被外界(包括本系统当前的其他事务,以及来自外部系统的事务处理)修改持保守态度,因此,在整个数据处理过程中,将数据处于锁定状态。悲观锁的实现,往往依靠数据库提供的锁机制(也只有数据库层提供的锁机制才能真正保证数据访问的排他性,否则,即使在本系统中实现了加锁机制,也无法保证外部系统不会修改数据)。
悲观锁流程
- 在对任意记录进行修改前,先尝试为该记录加上排他锁(exclusive locking)。
- 如果加锁失败,说明该记录正在被修改,那么当前查询可能要等待或者抛出异常。 具体响应方式由开发者根据实际需要决定。
- 如果成功加锁,那么就可以对记录做修改,事务完成后就会解锁了。
- 其间如果有其他对该记录做修改或加排他锁的操作,都会等待我们解锁或直接抛出异常。
其时序图如下
悲观锁举例
Java synchronized 就属于悲观锁的一种实现,每次线程要修改数据时都先获得锁,保证同一时刻只有一个线程能操作数据,其他线程则会被block。
使用同步的线程安全的计数器
public final class Counter {
private long value = 0;
public synchronized long getValue() {
return value;
}
public synchronized long increment() {
return ++value;
}
}
乐观锁
说明
乐观锁( Optimistic Locking ) 相对悲观锁而言,乐观锁机制采取了更加宽松的加锁机制。悲观锁大多数情况下依靠数据库的锁机制实现,以保证操作最大程度的独占性。但随之而来的就是数据库性能的大量开销,特别是对长事务而言,这样的开销往往无法承受。而乐观锁机制在一定程度上解决了这个问题。
乐观锁,大多是基于数据版本( Version )记录机制实现。何谓数据版本?即为数据增加一个版本标识,在基于数据库表的版本解决方案中,一般是通过为数据库表增加一个 “version” 字段来实现。读取出数据时,将此版本号一同读出,之后更新时,对此版本号加一。此时,将提交数据的版本数据与数据库表对应记录的当前版本信息进行比对,如果提交的数据版本号大于数据库表当前版本号,则予以更新,否则认为是过期数据。
乐观锁举例
- 柜员A和g柜员B同时获取了客户余额数据为{余额:100,版本:1},柜员A扣费50,柜员B扣费20,此时A认为客户余额为50,B认为客户余额为80,都是最新版本。
- 同时写入数据库,由于A的提交数据{余额:50,版本:2},版本号较大,所以系统觉得这是最新版本的数据,所以允许A进行修改。
- A修改后,B的请求数据{余额:80,版本:2}中版本号和数据库现有版本是相同的,所以认为B的数据已经是过期数据了,故不予更新,此时柜员B需要重新获取数据在进行业务操作。
CAS算法
比较并交换(compare and swap, CAS),是原子操作的一种,可用于在多线程编程中实现不被打断的数据交换操作,从而避免多线程同时改写某一数据时由于执行顺序不确定性以及中断的不可预知性产生的数据不一致问题。 该操作通过将内存中的值与指定数据进行比较,当数值一样时将内存中的数据替换为新的值。这句话怎么理解呢?
柜员A和柜员B同时获取到客户余额信息为100,柜员A记住了取数据时是100,当写会数据时,发现数据库余额依旧为100,说明在这之后没有人对数据库行操作,所以我们可以直接写回。
柜员B和A是同时取的,他认为数据库的余额也应该为100,但发现不符,说明A依旧改过数据库了,那我的目标余额计算就有问题,就不能写回,需要重新执行业务操作。
使用CAS的非阻塞算法实现的程序计数器:
public class NonblockingCounter {
private AtomicInteger value;
public int getValue() {
return value.get();
}
public int increment() {
int v;
do {
v = value.get();
while (!value.compareAndSet(v, v + 1));
return v + 1;
}
}