概念
乐观锁也叫共享锁、读锁,事务T给对象A加了乐观锁,则其他事务还可以给对象A加乐观锁。但不能加被排他锁。也就是可以同时读数据,但读的过程中,不允许任何写。
悲观锁也叫排他锁、写锁,事务T给对象A加了悲观锁,则其他事务不可以给对象A加任何锁。也就是在写期间,不允许任何读、写操作。
实现
在数据库用select for update属于乐观锁,update/delete属于悲观锁。
在java中,同步块属于悲观锁。ReentrantReadWriteLock有ReadLock/WriteLock,分别实现乐观锁和悲观锁。注意:java的锁只对单个jvm有效。所以在集群环境下,必须借助数据库或者第三方组件(例如:zookeeper,redis)。
应用场景
减库存:库存为10,每一个订单库存减1。
分析:减库存分两个步骤:1.判断是否有库存(库存为0);2.如果有则减1,如果没有则返回。
并发环境下,存在两个问题:
- A/B两个用户同时判断库存都只剩一件,进行减1操作,产生超发。
- 同时进行对库存做减1操作,库存少减了1(例如库存10。两个进程同时来减1,A进程做stock=10-1,B进程也做stock=10-1,则库存变成了9,而不是期望的8)。
假设库存是一个变量stock 。
** 方案1(悲观锁)**:判断stock 是否大于0前,先将变量加一把写锁,然后再判断。这样其他进程连判断语句都进不了。
private final ReadWriteLock lock = new ReentrantReadWriteLock();
private final Lock w = lock.writeLock();
w.lock();
if(stock >0) stock --;
else return false;
方案2(乐观锁):判断A是否大于0前,先将变量加一把读锁,判断成功准备写之前,将读锁升级为写锁。
private final ReadWriteLock lock = new ReentrantReadWriteLock();
private final Lock r = lock.readLock();
r.lock();
if(a>0) {
w.lock();
a--;
}
else return false;
在集群环境下的方案:借助数据库的悲观锁和乐观锁。由于数据库的锁资源非常宝贵,为了增加系统的吞吐量,一般采用版本号的方案。
假设数据库表Stock,有两个字段num,version,初始<10,0>。
select * from Stock; //这里并没有加锁
if(Stock.num>0) {
ver = Stock.version;
update Stock set num=num-1,version=version+1 where version = ver;
if (影响的行数 == 1) {
return true;
} else {
return false;
}
} else {
return false;
}
另外,可以将上述方案结合起来。在java端做一个库存是否为空的变量(静态),这样就避免在库存已经为0的情况下还去查数据库。
private final ReadWriteLock lock = new ReentrantReadWriteLock();
private final Lock w = lock.writeLock();
static boolean isZero = false;
r.lock();
if(!isZero) {
select * from Stock; //这里并没有加锁
if(Stock.num>0) {
ver = Stock.version;
update Stock set num=num-1,version=version+1 where version = ver;
if (影响的行数 == 1) {
//判断是否还有库存并修改变量
select count(1) cnt from Stock where num = 0;
w.lock();
isZero = true;
w.unlock();
return true;
} else {
return false;
}
} else {
return false;
}
这里简化了一个问题,库存是针对某件商品的,所以应该是一个Map的锁。如果直接针对Map加锁势必导致多个商品同时被加锁。可以使用:
- java.util.concurrent.ConcurrentHashMap 类;
- 利用ReentrantReadWriteLock实现一个ReadWriteMap。见网络。