分布式锁实践方案

分布式锁简介:

多个线程同时访问共享资源(商品库存、外卖订单),会发生数据竞争。

解决方案:同一时刻只允许一个线程访问共享资源,其他线程需要等待当前线程释放后才能访问。

悲观锁认为共享资源每次被访问的时候会出现问题(比如共享数据被修改),所以每次在获取资源操作的时候都会上锁,这样其他线程想拿到这个资源就会阻塞直到锁被上一个持有者释放。换句话说,共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程

单体架构系统下,通常使用ReentrantLock类、synchronized关键字(JDK自带的本地锁)来控制一个JVM进程内的多个线程对本地资源访问。

分布式系统下,不同的服务/客户端通常运行在独立的 JVM 进程上。如果多个 JVM 进程共享同一份资源的话,使用本地锁就没办法实现资源的互斥访问了。于是,分布式锁 就诞生了。

分布式锁特征

  • 互斥:任意一个时刻,锁只能被一个线程持有。

  • 高可用:锁服务是高可用的,当一个锁服务出现问题,能够自动切换到另外一个锁服务。并且,即使客户端的释放锁的代码逻辑出现问题,锁最终一定还是会被释放,不会影响其他线程对共享资源的访问。这一般是通过超时机制实现的。

  • 可重入:一个节点获取了锁之后,还可以再次获取锁。

  • 高性能:获取和释放锁的操作应该快速完成,并且不应该对整个系统的性能造成过大影响。

  • 非阻塞:如果获取不到锁,不能无限期等待,避免对系统正常运行造成影响。

分布式锁的常见实现方式

常见分布式锁实现方案如下:

  • 基于关系型数据库比如 MySQL 实现分布式锁。(一般通过唯一索引或者排他锁实现,存在问题较多,不推荐)
  • 基于分布式协调服务 ZooKeeper 实现分布式锁。
  • 基于分布式键值存储系统比如 Redis 、Etcd 实现分布式锁。

基于Redis实现分布式锁

在Redis中,通过SETNXSET if Not EXsts)实现互斥,如果key不存在,才会设置key的值,如果key已经存在,SETNX啥也不做。

> SETNX lockKey uniqueValue
(integer) 1 

释放锁,直接通过DEL命令删除对应key即可。

> DEL lockKey

为了防止误删其他锁,使用Lua脚本通过key对应的value(唯一值)来判断

// 释放锁时,先比较锁对应的 value 值是否相等,避免锁的误释放
if redis.call("get",KEYS[1]) == ARGV[1] then
    return redis.call("del",KEYS[1])
else
    return 0
end

存在问题:遇到释放锁的逻辑突然挂掉,会导致锁无法被释放,造成共享资源无法再被其他线程/进程访问。

锁设置过期时间

避免锁无法被释放,通过给 key(也就是锁) 设置一个过期时间 来解决

> SET lockKey uniqueValue EX 3 NX
# lockKey:加锁的锁名
# uniqueValue:能够唯一标识锁的随机字符串
# NX:只有当 lockKey 对应的 key 值不存在的时候才能 SET 成功
# EX:过期时间设置(秒为单位)EX 3 标示这个锁有一个 3 秒的自动过期时间。与 EX 对应的是 PX(毫秒为单位),这两个都是过期时间设置。

存在问题:如果操作共享资源的时间大于过期时间,就会出现锁提前过期的问题,进而导致分布式锁直接失效。如果锁的超时时间设置过长,又会影响到性能。

解决方案:锁续期

实现锁续期

解决方案:Redisson

Redisson 还支持 Redis 单机、Redis Sentinel、Redis Cluster 等多种部署架构。

Redisson 中的分布式锁自带自动续期机制,其提供一个专门用来监控和续期锁的 Watch Dog( 看门狗),如果操作共享资源的线程还未执行完成的话,Watch Dog 会不断地延长锁的过期时间,进而保证锁不会因为超时而被释放。

//默认 30秒,支持修改
private long lockWatchdogTimeout = 30 * 1000;

public Config setLockWatchdogTimeout(long lockWatchdogTimeout) {
    this.lockWatchdogTimeout = lockWatchdogTimeout;
    return this;
}
public long getLockWatchdogTimeout() {
   return lockWatchdogTimeout;
}

renewExpiration()方法包含看门狗的主要逻辑:

private void renewExpiration() {
         //......
        Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
            @Override
            public void run(Timeout timeout) throws Exception {
                //......
                // 异步续期,基于 Lua 脚本
                CompletionStage<Boolean> future = renewExpirationAsync(threadId);
                future.whenComplete((res, e) -> {
                    if (e != null) {
                        // 无法续期
                        log.error("Can't update lock " + getRawName() + " expiration", e);
                        EXPIRATION_RENEWAL_MAP.remove(getEntryName());
                        return;
                    }

                    if (res) {
                        // 递归调用实现续期
                        renewExpiration();
                    } else {
                        // 取消续期
                        cancelExpirationRenewal(null);
                    }
                });
            }
         // 延迟 internalLockLeaseTime/3(默认 10s,也就是 30/3) 再调用
        }, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);

        ee.setTimeout(task);
    }

每过 10 秒,看门狗就会执行续期操作,将锁的超时时间设置为 30 秒。看门狗续期前也会先判断是否需要执行续期操作,需要才会执行续期,否则取消续期操作。

Watch Dog 通过调用 renewExpirationAsync() 方法实现锁的异步续期:

protected CompletionStage<Boolean> renewExpirationAsync(long threadId) {
    return evalWriteAsync(getRawName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
            // 判断是否为持锁线程,如果是就执行续期操作,就锁的过期时间设置为 30s(默认)
            "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
                    "redis.call('pexpire', KEYS[1], ARGV[1]); " +
                    "return 1; " +
                    "end; " +
                    "return 0;",
            Collections.singletonList(getRawName()),
            internalLockLeaseTime, getLockName(threadId));
}

renewExpirationAsync 方法调用 Lua 脚本实现续期

以 Redisson 的分布式可重入锁 RLock 为例来说明如何使用 Redisson 实现分布式锁

// 1.获取指定的分布式锁对象
RLock lock = redisson.getLock("lock");
// 2.拿锁且不设置锁超时时间,具备 Watch Dog 自动续期机制
lock.lock();
// 3.执行业务
...
// 4.释放锁
lock.unlock();

只有未指定锁超时时间,才会使用到 Watch Dog 自动续期机制。

// 手动给锁设置过期时间,不具备 Watch Dog 自动续期机制
lock.lock(10, TimeUnit.SECONDS);

Redis 如何解决集群情况下分布式锁的可靠性?

Redis 集群下,分布式锁的实现会存在一些问题。由于 Redis 集群数据同步到各个节点时是异步的,如果在 Redis 主节点获取到锁后,在没有同步到其他节点时,Redis 主节点宕机了,此时新的 Redis 主节点依然可以获取锁,所以多个应用服务就可以同时获取到锁。

解决方案:基于 Redis 主从复制 + 哨兵模式实现分布式锁。,当主库异常宕机时,哨兵实现故障自动切换,把从库提升为主库,继续提供服务。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值