java-distributed-Lock

242 阅读5分钟

Distributed-Lock

分布式事务解决的数据一致性问题,分布式锁解决的是线程同步,资源竞争问题.

现有锁大部分都是基于JVM层的实现的,分布式架构下现有锁不能满足需求,分布式锁的关键是寻找分布式系统架构的单点。比如缓存,数据库,zk(中间件);

实现过程中注意问题

  • 有超时时间: key能自动删除
  • 避免死锁: 有安全的解锁逻辑(finally)

redis

实现的方案:setnxredission

setnx-ex

resis命令

# 存在则覆盖,不存在则失败
setex key valve expire_seconds
# 不存在才设置,存在则失败
setnx key value
# 加锁命令
SET key value [EX seconds] [PX milliseconds] [NX|XX]
  1. EX,超时时间秒
  2. PX,超时时间毫秒
  3. NX,操作,不存在则设置
  4. XX,操作,存在则设置
  • 加锁逻辑

先设置锁(key),在设置超时时间; 可以使用setnx一条指令实现;也可以分成2个指令,但是需要事务/lua封装下

  • 解锁逻辑

先判断锁存在,在删除;也需要保证原子性。

存在的问题

  1. 获取锁成功,可以执行任务;如果执行过程中出现redis宕机/服务异常导致没有正常的释放锁,可能阻塞其他服务正常执行; 解决的方式是:提前设置好锁的超时时间/手动删除锁(稳健的删锁逻辑)

  2. 获取锁失败,直接结束,不能排队.

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

  1. 创建:当前进程调用zkClient创建临时顺序节点,
  2. 阻塞:根据节点List排序,判断如果自己是第一顺位则获得锁,否则,等待并监听(watch)自己前一个节点,类似CLH
  3. 释放:发现前一个节点释放锁,则自己重试获取锁

问题

  • 由于网络抖动,客户端可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