Distributed-Lock
分布式事务解决的数据一致性问题,分布式锁解决的是线程同步,资源竞争问题.
现有锁大部分都是基于JVM层的实现的,分布式架构下现有锁不能满足需求,分布式锁的关键是寻找分布式系统架构的单点。比如缓存,数据库,zk(中间件);
实现过程中注意问题
- 有超时时间: key能自动删除
- 避免死锁: 有安全的解锁逻辑(finally)
redis
实现的方案:setnx 和 redission
setnx-ex
resis命令
# 存在则覆盖,不存在则失败
setex key valve expire_seconds
# 不存在才设置,存在则失败
setnx key value
# 加锁命令
SET key value [EX seconds] [PX milliseconds] [NX|XX]
- EX,超时时间秒
- PX,超时时间毫秒
- NX,操作,不存在则设置
- XX,操作,存在则设置
- 加锁逻辑
先设置锁(key),在设置超时时间; 可以使用setnx一条指令实现;也可以分成2个指令,但是需要事务/lua封装下
- 解锁逻辑
先判断锁存在,在删除;也需要保证原子性。
存在的问题
-
获取锁成功,可以执行任务;如果执行过程中出现
redis宕机/服务异常导致没有正常的释放锁,可能阻塞其他服务正常执行; 解决的方式是:提前设置好锁的超时时间/手动删除锁(稳健的删锁逻辑) -
获取锁失败,直接结束,
不能排队.
redisson
[redission-github]github.com/redisson/re…
// RedissonLock 实现类
RLock lock = redisson.getLock(key);
lock.lock(expire, TimeUnit.SECONDS);
try{
...
}finally{
lock.unlock();
}
加锁
# redis结构Hash
# KEYS[1] = 锁的key(hask-key)
# ARGV[1] = 默认超时时间,watchdog超时时间30S,lockWatchdogTimeout = 30 * 1000;
# ARGV[2] = 客户端ID = UUID:threadId(hash-value)
if (redis.call('exists', KEYS[1]) == 0)
then
# 无锁:setnx-ex
redis.call('hset', KEYS[1], ARGV[2], 1); # 记录threadId,
redis.call('pexpire', KEYS[1], ARGV[1]); # 记录expiretime
return nil;
end;
if (redis.call('hexists', KEYS[1], ARGV[2]) == 1)
then
# 存在锁:客户端相同则计数器+1
redis.call('hincrby', KEYS[1], ARGV[2], 1); # thread + 1
redis.call('pexpire', KEYS[1], ARGV[1]); # 更新时间
return nil;
end;
# 返回剩余时间
return redis.call('pttl', KEYS[1]); // 锁时间
# 后台线程watchdog,每个10S不断检查锁是否被占有,并更新超时时间
- 加锁成功返回nil,失败返回超时时间
- watchdog后台线程,更新超时时间(30s周期)
- pub/sub订阅信息,用于解锁信号量的阻塞
redis-hash
# lock-key=> xxx_id
[hset|hincrby] xxx_id uuid_threadId 1
pexire xxxx_id
锁等待
public void lockInterruptibly(long leaseTime, TimeUnit unit) throws InterruptedException {
long threadId = Thread.currentThread().getId();
Long ttl = tryAcquire(leaseTime, unit, threadId);
// 获取锁成功则结束
if (ttl == null) {
return;
}
// 获取锁失败,阻塞逻辑
// 1. 订阅解锁信息
RFuture<RedissonLockEntry> future = subscribe(threadId);
commandExecutor.syncSubscription(future);
// 2. 等待逻辑
try {
// 重试获取锁
while (true) {
ttl = tryAcquire(leaseTime, unit, threadId);
if (ttl == null) {
break;
}
// 过去锁失败,使用信号量阻塞
if (ttl >= 0) {
// this.latch = new Semaphore(0);
// 使用newConcurrentHashMap根据线程id,保存Semaphore
getEntry(threadId).getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
} else {
// Semaphore 阻塞
// 3. 通过异步线程 + pubsub 处理订阅解锁信息时,等待调用Semaphore.release
// 不是通过CLH队列实现,是非公平的
getEntry(threadId).getLatch().acquire();
}
}
} finally {
// 不在订阅解锁信息
unsubscribe(future, threadId);
}
}
解锁
# KEYS[1] = 锁的key
# KEYS[2] = redisson_lock__channel:{keys[1]}
# ARGV[1] = 解锁消息,0L
# ARGV[2] = 看门狗时间:30S
# ARGV[3] = 当前客户端ID = UUID:threadId
if (redis.call('exists', KEYS[1]) == 0)
then
# key已经不存在,则发布解锁消息
redis.call('publish', KEYS[2], ARGV[1]); # unlock
return 1;
end;
if (redis.call('hexists', KEYS[1], ARGV[3]) == 0)
then
# 持锁的线程不存在,返回,解锁失败
return nil;
end;
# 计数器-1
local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1);
if (counter > 0)
then
# 计数器>0,更新超时
redis.call('pexpire', KEYS[1], ARGV[2]);
return 0;
else
# 计数器<=0,删除key,发布解锁信息
redis.call('del', KEYS[1]);
redis.call('publish', KEYS[2], ARGV[1]);
return 1; # unlock
end;
return nil;
redis-hash
# lock_key => xxx_id
[hset|hincrby] xxx_id uuid:threadId x -1
del xxx_id
zookeeper
是分布式文件存储系统,支持观察者模式的watch机制,主要应用场景:服务发布订阅,分布式锁。
zkNode节点类型
| 节点类型 | note |
|---|---|
| PERSISTENT | 持久节点 |
| PERSISTENT_SEQUENTIAL | 持久顺序节点 |
| EPHEMERAL | 临时节点,可以自动删除 |
| EPHEMERAL_SEQUENTIAL | 临时顺序节点,分布式锁 |
zk节点有个唯一的特性,就是我们创建过这个节点了,你再创建zk是会报错的.
临时节点可删除,在客户端断开连接(sesssion超时)节点自动删除。这样不会出现死锁.
ZK中创建和删除节点只能通过Leader服务器来执行.
分布式锁的基本实现流程,可用的开源框架:
Curator
创建:当前进程调用zkClient创建临时顺序节点,阻塞:根据节点List排序,判断如果自己是第一顺位则获得锁,否则,等待并监听(watch)自己前一个节点,类似CLH释放:发现前一个节点释放锁,则自己重试获取锁
问题
- 由于网络抖动,客户端可ZK集群的session连接断了,会出现并发问题(节点都被删除了)
- 没有redis性能高qps;
- redission是非公平的,锁本身性能高,zk是公平的
- zook是强一致性,分布式事务的缺点
zookpeer集群一致性
mysql
-- 数据库编程实现,不适合迁移
-- 失败则更新丢失
update tb_user set age = age + 1, version = version + 1 where id = 1 and version = 2
interview
悲观锁-乐观锁
悲观锁,线程持有锁就绝对占有, 直到自己释放,其他线程才可以占用,例如synchronized,Lock
乐观锁,通过cas(compare and swap,也叫自旋锁)管理的锁,compare的对象可以是值,也可以是值的版本。
cas存在的问题:
- ABA问题
- 循环等待的问题
update user set name='x',version=version+1 where id=1 and version = expected_version;
fairLock-notfairLock
公平锁:FIFO得到锁
- 优点:不会出现线程饿死
- 缺点:后到的等待时间更长
非公平锁:谁都可以竞争锁,看运气得到,
- 优点:减轻cpu唤醒线程的开销
- 缺点:可能出现饿死(某个线程永远得不到锁)
// Sync 默认都是非公平锁
abstract static class Sync extends AbstractQueuedSynchronizer{}
static final class NonfairSync extends Sync{}
static final class FairSync extends Sync {}
线程同步-通信的机制有那些
- 锁及其相关,例如synchronized+wait/notify,Lock_Condition/Semapore
- volatile