深入理解悲观锁和乐观锁

概念

悲观锁(Pessimistic Lock), 顾名思义,就是很悲观,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会block直到它拿到锁。
乐观锁(Optimistic Lock), 顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库如果提供类似于write_condition机制的其实都是提供的乐观锁。

悲观锁

结合上家公司的项目,回顾一下tair的悲观锁实现。
tair有lock方法,但是该方法并不好用,所以多数都是使用带版本号的put函数,实现锁机制。注意:初始化时不能为0和1的,否则会出现冲突。
tair的悲观锁最简单的实现如下:

/**
* 修改和增加缓存中的数据
* @param key
* @param iMsgModel
* @return
*/
public boolean putMsgCache(Serializable key, String value, int version){
try {
ResultCode resultCode = tairManager.put(namespace,key, value, version, 0);
if (null != resultCode && resultCode.getCode() == 0){
return true;
}
} catch (Exception e) {
e.printStackTrace();
}
return false;
}

然而仔细推敲代码,发现上述代码存在一个比较严重的问题:对某一Key上锁后,如果此时当前进程crash后,该key会永远被锁住。
要解决这个问题,一个比较简单粗暴的方法是增加一个有效时间,让该key自动失效。但是这个有效时间需要和业务相关,不太好控制。
所以将上述代码,修改如下:

public boolean putMsgCache(Serializable key, String value, int version, long delay){
try {
ResultCode resultCode = tairManager.put(namespace,key, value, version, delay);
if (null != resultCode && resultCode.getCode() == 0){
return true;
}
} catch (Exception e) {
e.printStackTrace();
}
return false;
}

但是上述代码还是有些缺陷,如果tair的put超时怎么办?
其实超时可以分为两种情况:
1、tair已经将数据put成功,但是返回结果的时候超时
2、请求tair的put方法就已经超时,此时并没有将数据put成功。
对于第一种,下次重试的时候,肯定会失败。对于第二种,下次重试的时候,会成功。所以重点是如何解决第一类问题。
上述代码都是对key和version做文章,这里我们可以在value上做文章。

public boolean putMsgCache(Serializable key, String value, int version, long delay){
Result<DataEntry> result = tair.get(NAMESPACE, key);
if(result == null || ResultCode.DATANOTEXSITS.equals(result.getRc())){
// 说明此时的锁是空闲的
ResultCode code = ldbTairManager.put(NAMESPACE, key, getLockValue(), 2, timeout);
if(ResultCode.SUCCESS.equals(code)){
// 说明保存成功,用户成功拿到了锁
return true;
}
}else if(result.getValue() != null && getLockValue().equals(result.getValue().getValue())){
// 说明是同一个value,应该让该方法进行重试
return true;
}
return false;
}

所以该方法重点的任务是设计value,该value务必保证是重试的进来的。所以可以用使用uuid或者线程名+主机名的方法。

private String getLockValue(){
return Utils.getHostname() + ":" + Thread.currentThread().getName();
}

那么问题又来了,如果get操作也超时怎么办了??
此时也没有什么好办法,只有一次次重试了
所以,可以将上述程序修改如下:

public boolean trylock(String key, int timeout) {
Result<DataEntry> result = locker.ldbTairManager.get(NAMESPACE, key);
if (result == null || ResultCode.CONNERROR.equals(result.getRc()) || ResultCode.TIMEOUT.equals(result.getRc()|| ResultCode.UNKNOW.equals(result.getRc())) // get timeout retry case
result = locker.ldbTairManager.get(NAMESPACE, key);
if (result == null || ResultCode.CONNERROR.equals(result.getRc()) || ResultCode.TIMEOUT.equals(result.getRc()) || ResultCode.UNKNOW.equals(result.getRc())){ // 还是超时,则留下日志痕迹
logger.error("ldb tair get timeout. key:{}.",key);
return false;
}
if (ResultCode.DATANOTEXSITS.equals(result.getRc())) { // means lock is free
ResultCode code = ldbTairManager.put(NAMESPACE, key, getLockValue(), 2, timeout);
if (ResultCode.SUCCESS.equals(code))
return true;
else if(code==null || ResultCode.CONNERROR.equals(result.getRc()) || ResultCode.TIMEOUT.equals(result.getRc()) || ResultCode.UNKNOW.equals(result.getRc())){ // put timeout retry case
return trylock(key, timeout); // 递归尝试
}
}else if(result.getValue() != null && getLockValue().equals(result.getValue().getValue())){
return true;
}
return false;
}
private String getLockValue(){
return Utils.getHostname() + ":" + Thread.currentThread().getName();
}

注意此时只重试了一次,如果想保险起见,可以使用while循环,增加一个重试次数。

乐观锁

version,版本控制,是乐观锁的根基。乐观锁并不是真的锁,它只是加了一个标识,用于区分数据是否有被意料之外的更改过。若没有,那就正常提交事物结束操作;若有,则回滚事物,撤销所有操作。所以,乐观锁除了需要应用于写冲突很少的场景,还要求系统支持事物回滚或补偿。
乐观锁的大概流程如下:
1、获取版本号
2、处理事务
3、检查版本号是否更新(此时应该相当于悲观锁)
4、检测成功,提交事务
5、检测版本号失败,回滚事务,调到1
所以乐观锁并不是一个真正意义上的锁,可以认为是一个处理任务的逻辑。
乐观锁的优缺点很明确,适用于并发冲突少的环境,如果并发冲突情况特别频繁的情况下,会不断进入第5步,存在着大量的循环操作,处理该任务的线程一直得不到释放,很消耗资源。

总结

上述只是运用tair的时候总结,总之合理运用乐观锁和悲观锁即可

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值