锁:解决多个线程争抢资源的情况,保证任何时候有且只有一个线程能持有资源,并且避免死锁。
关注问题:分布式、过期、宕机、代码原子性、GC、重入(lock次数)
方案1
简单粗暴的想法,缺少锁就给他加锁
使用synchronized、队列
锁住方法、锁住代码块、使用队列使其串行
优点:简单粗暴,可以解决单机并发问题
缺点:锁粒度较高,面对单机高并发和分布式系统就无力了
方案2
数据库锁表
将资源(order_id)存储数据库,并设置UNIQUE KEY
加锁:insert into lock_table (order_id) values (orderId1)
解锁:delete from lock_table where order_id=' orderId1'
缺点:没有过期时间,如果P1获取资源后异常宕机,就会死锁,还需要定时任务解锁,
锁失效时间由程序判断,这样会额外增加应用的执行时间,不可控
方案3
缓存锁
redis锁定的原理是利用setnx命令,set if not exists,即key不存在时才能set成功。1-设置成功, 0-设置不成功
优点:性能出色,效率高
实现1:
SETNX lock_key value
del lock_key
|
P1通过setnx返回1获得锁
P2通过setnx返回0未获得锁
P1执行完del lock_key释放锁
P2通过setnx返回1获得锁
如果P1挂掉,未能释放锁,其他线程setnx返回0永远不能获取锁
实现2:
SETNX lock_key <current timestamp + lock timeout>
del lock_key
GETSET lock_key < current Unix time + lock timeout +
1
>
|
P1通过 SETNX lock_key <current timestamp + lock timeout>获得锁,value为当前时间戳+锁的时间
P2通过setnx返回0,然后通过GET key 检查value是否超时
如果没超时则等待或重试
如果已超时则 GETSET lock_key < current Unix time + lock timeout + 1>,如果拿到的时间戳是超时的,则认为P2获得锁
如果有个P3在P2之前执行了上面的操作,那么P2拿到的是未超时的值,重试或等待
问题:尽管P2没能拿到锁,但是改写了P3的过期时间,不过是微小的误差。锁失效时间有程序判断,这样会额外增加应用的执行时间。如果各节点时间不一致容易导致死锁。
实现3:
SETNX lock_key value
expire lock_key seconds
del lock_key
|
P1执行setnx返回1后获得锁,然后设置过期时间,开始执行业务代码,执行完成后del lock_key释放锁
P2执行setnx返回0然后重试或等待
如果P1挂掉没能正确释放锁,超过有效期后expire命令会自动删除key,其他线程开始竞争锁
问题:setnx和expire不是原子操作,如果setnx成功但是在expire之前程序挂掉,就可能导致死锁
实现4:
SETNX lock_key value
expire lock_key seconds
del lock_key
ttl lock_key
|
ttl:当 key 不存在时,返回 -2 。当 key 存在但没有设置剩余生存时间时,返回 -1 。否则,以秒为单位,返回 key 的剩余生存时间。
P1执行setnx成功但是在expire之前程序挂掉
P2执行setnx返回0,然后执行ttl命令返回-1,则执行expire lock_key timeout设置失效时间
如果仅P1一个节点
批次1:P1执行setnx成功,但是在expire之前程序挂掉
批次2:执行setnx返回0,然后执行ttl命令返回-1,则执行expire lock_key timeout设置失效时间
问题:虽然避免了实现3中的死锁,但是可能存在失效时间误差的情况,并且可能导致某一批次任务不会被执行
实现5:
基于Jedis实现
加锁:
/**
* 加锁:
* 存储数据到缓存中,并制定过期时间和当Key存在时是否覆盖。
* @param key
* @param value
* @param nxxx nxxx的值只能取NX或者XX,如果取NX,则只有当key不存在是才进行set,如果取XX,则只有当key已经存在时才进行set
* @param expx expx的值只能取EX或者PX,代表数据过期时间的单位,EX代表秒,PX代表毫秒。
* @param time 过期时间,单位是expx所代表的单位。
*/
Jedis.set(
final
String key,
final
String value,
final
String nxxx,
final
String expx,
final
int
time);
if
(LOCK_SUCCESS.equals(result)) {
return
true
;
}
return
false
;
/**
* 解锁:
*/
String script =
"if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"
;
Object result = jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));
if
(RELEASE_SUCCESS.equals(result)) {
return
true
;
}
return
false
;
|
实现6:
使用Redisson实现分布式锁
包含可重入锁(Reentrant Lock)公平锁(Fair Lock)联锁(MultiLock)红锁(RedLock)读写锁(ReadWriteLock)信号量(Semaphore)可过期性信号量(PermitExpirableSemaphore)闭锁(CountDownLatch)。