写在前面
喜欢的朋友可以关注下专栏:Java架构技术进阶。里面有大量batj面试题集锦,还有各种技术分享,如有好文章也欢迎投稿哦。
分布式锁
并发编程中的锁并发编程的锁机制:synchronized和lock。在单进程的系统中,当存在多个线程可以同时改变某个变量时,就需要对变量或代码块做同步,使其在修改这种变量时能够线性执行消除并发修改变量。
而同步的本质是通过锁来实现的。为了实现多个线程在一个时刻同一个代码块只能有一个线程可执行,那么需要在某个地方做个标记,这个标记必须每个线程都能看到,当标记不存在时可以设置该标记,其余后续线程发现已经有标记了则等待拥有标记的线程结束同步代码块取消标记后再去尝试设置标记。
分布式环境下,数据一致性问题一直是一个比较重要的话题,而又不同于单进程的情况。分布式与单机情况下最大的不同在于其不是多线程而是多进程。多线程由于可以共享堆内存,因此可以简单的采取内存作为标记存储位置。而进程之间甚至可能都不在同一台物理机上, 因此需要将标记存储在一个所有进程都能看到的地方。 常见的是秒杀场景,订单服务部署了多个实例,如
秒杀商品有4个,第一个用户购买3个,第二个用户购买2个,理想状态下第一个用户能购买成功,第二
个用户提示购买失败,反之亦然。而实际可能出现的情况是,两个用户都得到库存为4,第一个用户
买到了3个,更新库存之前,第二个用户下了2个商品的订单,更新库存为2,导致出错。
在上面的场景中,商品的库存是共享变量,面对高并发情形,需要保证对资源的访问互斥。在单机环境中,java中其实提供了很多并发处理相关的API,但是这些API在分布式场景中就无能为力了。也就是说单纯的java API 并不能提供分布式锁的能力。分布式系统中,由于分布式系统的分布性,即多线程和多进程并且分布在不同机器中,synchronized和lock这两种锁将失去原有锁的效果,需要我们自已实现分布式锁。
常见的分布式锁如下:
- 基于数据库实现分布式锁:有性能问题
- 基于缓存实现分布式锁,如redis
- 基于zookeeper实现分布式锁
使用setnx实现分布式锁
setnx key value
setnx是将key的值设为value,当且仅当key不存在。若给定的key已经存在,则setnx不做任何动作。
返回1,说明该进程获得锁,setnx将键(lock.id)的值设置为锁的超时时间,当前时间+加上锁的有效时间。
返回0,说明其他进程已经获得了锁,进程不能进入临界区。进程可以在一个循环中不断地尝试setnx操作,以获得锁。
存在死锁的问题
在线程释放锁,即执行del lock.id操作前,需要先判断锁是否已超时。如果锁已超时,那么锁可能已由其他线程获得,这时直接执行del lock.id操作会导致把其他线程已获得的锁释放掉。
获取分布式锁
public boolean lock( long timeout, TimeUnit timeUnit ) throws InterruptedException
{
timeout = timeUnit.toMillis( timeout );
long time = timeout + System.currentTimeMillis();
lock.tryLock( timeout, timeUnit );
try{
while ( true )
{
boolean hasLock = tryLock();
if ( hasLock )
{
return(true); /* 获得锁 */
}else if ( timeout < System.currentTimeMillis() )
{
break;
}
Thread.sleep( 1000 );
}
} finally {
if ( lock.isHeldByCurrentThread() )
{
lock.unlock();
}
}
return(false);
}
public boolean tryLock()
{
long time = System.currentTimeMillis();
long timeout = 2000;
String expires = String.valueOf( timeout + time );
if ( redisService.setnx( "lock.id", expires ) > 0 )
{
/* 获取锁,设置超时时间 */
setLockStatus( expires );
return(true);
}else{
String locktime = redisService.get( "lock.id" );
/* 检查锁是否超时 */
if ( locktime != null && Long.parseLong( locktime ) < time )
{
String oldlocktime = redis.getset( "lock.id", expires );
/* 旧值与当前时间比较 */
if ( oldlocktime != null && locktime.equals( oldlocktime ) )
{
/* 获取锁,设置超时时间 */
setLockStatus( expires );
return(true);
}
}
return(false);
}
}
释放锁
public boolean unlock()
{
if ( lockHolder == Thread.currentThread() )
{
/* 判断锁是否超时,没有超时才将互斥量删除 */
if ( lockExpiresTime > System.currentTimeMillis() )
{
redisService.del( "lock.id" );
}
lockHolder = null;
return(true);
}else{
throws new IllegalMonitorStateException( "无法执行解锁操作" );
}
}
喜欢的朋友可以关注下专栏:Java架构技术进阶
如果你是Java程序员,对技术提升很感兴趣,欢迎1~5年的工程师可以加入我的Java进阶之路来交流学习:878249276。里面都是同行,有资源共享,还有大量面试题以及解析。欢迎一到五年的工程师加入,合理利用自己每一分每一秒的时间来学习提升自己,不要再用"没有时间“来掩饰自己思想上的懒惰!趁年轻,使劲拼,给未来的自己一个交代!
网友评论