分布式锁
并发编程中的锁并发编程的锁机制: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