库存一定的商品,在并发请求时即存在超卖问题,例如:大促、秒杀
在同一进程下,只需要通过java内置的锁Synchronized和ReentranLock即可。
其核心思想就是保证多个线程互斥访问count变量,从而解决超卖问题。
为了提升性能,减缓单机压力,会将该程序部署到多台机器上运行。这时三个用户的操作就可能打在三台机器上。在集群场景下由于资源不共享,如果依然只采用java单进程的锁,那么每个程序都会获取到一把锁,多个程序同时访问库存,造成超卖问题。
想要解决问题,依然是找到一种互斥访问库存的解决方案。
在单机单进程环境下,所有的资源都是存在jvm中,jvm具有上帝视角,因此可以实现jvm加锁互斥访问。
但在多机集群环境下,程序彼此之间互不相识,资源独立。因此需要找到一份公共的空间去沟通协调,类似于生活中的第三方组织介入,这个第三方可以通过Redis/MySql/Zookeeper来实现。这也就是分布式锁的实现思路。
当多个进程访问库存时,首先通过第三方获取到一把分布式锁,获取锁成功的才可以访问库存,否则需等待锁释放。
通过Redis中的缓存来实现分布式锁,即setNx命令,通过该命令可以设置一份key-value至redis中,多个程序访问库存时,通过判断是否设置过库存相关的key来判断是否执行命令,达到分布式锁的目的。
//加锁
private boolean tryLock(final String key, final String value) {
try {
MiddlewareCacheResult result
= middlewareCache.setNx(key, value, exipireTime);
return result.isSuccess();
} catch (Exception e) {
log.error("setNX error, key : {}", key,e);
}
return false;
}
//释放锁
public static void releaseLock(String lockKey){
delete(lockKey);
}
在设置过期时间后又会导致两个问题。
锁已经过期释放,但程序还没有执行结束。
由于程序未执行结束,过期释放了锁,同时另一个程序获取到了锁,此时第一个程序执行结束,再次释放了第二个程序的锁,导致程序逻辑错误。
解决方案:
当其他程序尝试获取锁时,如果程序未执行结束,则延长锁的过期时间。
public boolean tryLock(int expireSecond) throws InterruptedException{
//锁到期时间
if (this.setNX(lockKey, value, expireSecond)) {
// lock acquired
this.expire(lockKey,expireMsecs/1000);
return true;
}
return false;
}
设置value为当前用户相关信息,每次获取锁时判断下value值是否和当前用户信息匹配,防止锁释放的乱套。
redis锁性能提升思路
可以使用分段锁机制,当商品库存非常多时,比如库存有100个,可以每10个库存加一把锁,比如id在1-10是第一把锁,id在11-20为第二把锁,通过10把锁来增加高并发的思路。
在Redis集群中使用主从机制时,A程序在主节点获取到锁后,数据还未同步至从节点,此时主节点宕机,另一个B程序在从节点中请求锁,由于数据尚未同步,在B程序获取锁成功,A程序和B程序同时访问,依然造成超卖问题。
为了解决上述集群问题,引入红锁机制,红锁中多个节点独立,没有主从依赖关系。一般都是奇数个节点。
1.获取当前时间。
2.依次获取N个节点的锁。
3.判断是否获取锁成功。 如果client在上述步骤中获取到了(N/2 + 1)个节点锁,并且每个锁的过期时间都是大于0的,则获取锁成功,否则失败。失败时释放锁。
4.释放锁。 对所有节点发送释放锁的指令。
在上述红锁中依然会出现问题,比如对于如下五个节点,当某程序获取到A、B、C三个节点的锁后,这时C节点宕机,同时立即被人重启,这时另外一个程序获取C节点的锁时成功(由于刚刚被重启),该程序同时获取D、E的锁成功,它得到了C、D、E的锁,按照约定获取超过半数节点的锁即可执行,两个程序同时访问互斥资源,依然会造成超卖问题。
因此一般会有约定,当redis挂掉后,会通过手册约定一个时间后再重启,一般在该重启时间后程序锁会过期或者运行完毕,不会对程序造成影响,一般不能立即重启。
通过简单的超卖问题,引入一系列解决方案,伴随着场景越来越复杂,也会带来越来越多的细节问题,在实际开发中,要多思考高并发带来的问题,注意互斥资源的访问。