文章目录

  • 前言
  • 一、Redisson介绍
  • 1.1 Redisson的分布式锁的特点
  • 二、Redisson的使用
  • 2.1 引入依赖
  • 2.2 编写配置
  • 2.3 示例测试
  • 三、Redisson源码分析
  • 3.1 加锁源码
  • 3.2 看门狗机制
  • 3.3 看门狗机制源码



前言

分布式锁主要是解决分布式系统下数据一致性的问题。在单机的环境下,应用是在同一进程下的,只需要保证单进程多线程环境中的线程安全性,通过 Java 提供的volatile、ReentrantLock、synchronized以及concurrent并发包下一些线程安全的类等就可以做到。

一、Redisson介绍

Redisson是一个基于Redis的分布式Java客户端。它提供了丰富的功能和工具,帮助开发者在分布式系统中解决数据共享、并发控制和任务调度等问题。通过使用Redisson,开发者可以轻松地操作Redis的分布式对象(如集合、映射、队列等),实现可靠的分布式锁机制,以及管理和调度分布式环境中的任务和服务。

1.1 Redisson的分布式锁的特点

  1. 线程安全:分布式锁可以确保在多线程和多进程环境下的数据一致性和可靠性。
  2. 可重入性: 同一个线程可以多次获取同一个锁,避免死锁的问题。
  3. 锁超时: 支持设置锁的有效期,防止锁被长时间占用而导致系统出现问题。
  4. 阻塞式获取锁: 当某个线程尝试获取锁时,如果锁已经被其他线程占用,则该线程可以选择等待直到锁释放。
  5. 无阻塞式获取锁: 当某个线程尝试获取锁时,如果锁已经被其他线程占用,则该线程不会等待,而是立即返回获取锁失败的信息。

redisson实现分布式官网文档:https:///redisson/redisson/wiki/8.-distributed-locks-and-synchronizers

二、Redisson的使用

Redisson支持单点模式、主从模式、哨兵模式、集群模式,本文以单点模式为例说明。

2.1 引入依赖

<!-- redisson -->
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.12.0</version>
</dependency>
<!-- redis -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
    <version>2.1.21.RELEASE</version>
</dependency>

2.2 编写配置

spring:
  redis:
    host: 192.168.57.129
    port: 6379
@Configuration
public class RedissonConfig {

    @Value("${spring.redis.host}")
    private String host;

    @Value("${spring.redis.port}")
    private String port;

    /**
     * 对所有redisson的使用都是通过redissonClient对象
     * @return
     */
    @Bean(destroyMethod = "shutdown")
    public RedissonClient redissonClient() {
        Config config = new Config();
        config.useSingleServer().setAddress("redis://" + host + ":" + port);
        return Redisson.create(config);
    }
}

2.3 示例测试

@ResponseBody
@GetMapping("/hello")
public String hello(){
    //1.获取一把锁,只要锁的名字一样,就是同一把锁
    String lockKey = "my-lock";
    RLock lock = redissonClient.getLock(lockKey);
    //2、加锁阻塞式等待,默认加的锁都是30s。
    lock.lock();
    
    //10秒自动解锁,自动解锁时间一定要大于业务的执行时间。问题:在锁时间到了以后,不会自动续期。
    //lock.lock(10, TimeUnit.SECONDS);

    //最佳实战:省掉了整个续期操作。手动解锁
    //1)、lock.lock(30, TimeUnit.SECONDS);
    try {
        log.info("加锁成功,执行业务ing, 线程ID = {}", Thread.currentThread().getId());
        Thread.sleep(10000);
    }catch (Exception e){
        e.printStackTrace();
    }finally {
        //3、解锁 假设解锁代码没有运行,redisson会不会出现死锁
        log.info("释放锁, 线程ID = {}", Thread.currentThread().getId());
        lock.unlock();
    }
    return "hello";
}

浏览器执行两个hello请求,只有当第一个请求业务执行完,第二个才能正常执行,不然第二个处于阻塞式等待状态。

redission 分布式锁 默认时间_java

控制台打印日志信息

2023-11-18 16:01:00.784  INFO 3916 --- [io-10000-exec-4] c.a.g.product.web.IndexController        : 加锁成功,执行业务ing, 线程ID = 116
2023-11-18 16:01:10.785  INFO 3916 --- [io-10000-exec-4] c.a.g.product.web.IndexController        : 释放锁, 线程ID = 116
2023-11-18 16:01:10.794  INFO 3916 --- [io-10000-exec-2] c.a.g.product.web.IndexController        : 加锁成功,执行业务ing, 线程ID = 114
2023-11-18 16:01:20.794  INFO 3916 --- [io-10000-exec-2] c.a.g.product.web.IndexController        : 释放锁, 线程ID = 114

redisson实现分布式锁解决了redis实现分布式锁的两个问题

  1. 锁的自动续期,如果业务超长,运行期间自动给锁续上新的30s。不用担心业务时间长,锁自动过期被删掉。
  2. 加锁的业务只要运行完成,就不会给当前锁续期,即使不手动解锁,锁默认在30s以后自动删除。

三、Redisson源码分析

redisson这个框架的实现依赖了Lua脚本和Netty,以及各种Future及FutureListener 的异步、同步操作转换,加锁和解锁过程中还巧妙地利用了Redis的发布订阅功能。

3.1 加锁源码

无参加锁方法

@Override
public void lock() {
    try {
        lock(-1, null, false);
    } catch (InterruptedException e) {
        throw new IllegalStateException();
    }
}

有参加锁方法

@Override
public void lock(long leaseTime, TimeUnit unit) {
    try {
        lock(leaseTime, unit, false);
    } catch (InterruptedException e) {
        throw new IllegalStateException();
    }
}

3.2 看门狗机制

Watch Dog机制其实就是一个后台定时任务线程,获取锁成功之后,会将持有锁的线程放入到一个RedissonBaseLock.EXPIRATION_RENEWAL_MAP 里面,然后每隔10 秒(internalLockLeaseTime/3)检查一下,如果客户端1还持有锁key(判断客户端是否还持有 key,其实就是遍历 EXPIRATION_RENEWAL_MAP里面线程 id 然后根据线程id去Redis中查,如果存在就会延长key的时间),那么就会不断的延长锁key的生存时间。

如果服务宕机了,Watch Dog机制线程也就没有了,此时就不会延长key的过期时间,到了30s之后就会自动过期了,其他线程就可以获取到锁。如果调用带过期时间的lock方法,则不会启动看门狗任务去自动续期。

3.3 看门狗机制源码

private void lock(long leaseTime, TimeUnit unit, boolean interruptibly) throws InterruptedException {
    //获取当前线程ID
    long threadId = Thread.currentThread().getId();
    //尝试获取锁
    Long ttl = tryAcquire(leaseTime, unit, threadId);
    //成功获取锁,过期时间为空
    // lock acquired
    if (ttl == null) {
        return;
    }
    //订阅分布式锁,解锁时进行通知
    RFuture<RedissonLockEntry> future = subscribe(threadId);
    if (interruptibly) {
        commandExecutor.syncSubscriptionInterrupted(future);
    } else {
        commandExecutor.syncSubscription(future);
    }

    try {
        while (true) {
            //再次尝试获取锁
            ttl = tryAcquire(leaseTime, unit, threadId);
            // lock acquired
            if (ttl == null) {
                break;
            }

            // waiting for message
            if (ttl >= 0) {
                try {
                    future.getNow().getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
                } catch (InterruptedException e) {
                    if (interruptibly) {
                        throw e;
                    }
                    future.getNow().getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
                }
            } else {
                if (interruptibly) {
                    future.getNow().getLatch().acquire();
                } else {
                    future.getNow().getLatch().acquireUninterruptibly();
                }
            }
        }
    } finally {
        unsubscribe(future, threadId);
    }
}
//尝试获取锁
private Long tryAcquire(long leaseTime, TimeUnit unit, long threadId) {
    return get(tryAcquireAsync(leaseTime, unit, threadId));
}

异步的方式尝试获取锁

private <T> RFuture<Long> tryAcquireAsync(long leaseTime, TimeUnit unit, long threadId) {
    if (leaseTime != -1) {
        //如果我们传递了锁的超时时间,就发送给redis执行脚本,进行占锁,默认超时就是我们指定的时间。
        return tryLockInnerAsync(leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
    }
    //如果我们未指定锁的超时时间,就使用30*1000【LockWatchdogTimeout看门狗的默认时间】
    RFuture<Long> ttlRemainingFuture = tryLockInnerAsync(commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(), TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
    //占锁成功
    ttlRemainingFuture.onComplete((ttlRemaining, e) -> {
        //发生异常直接返回,若无异常执行下面逻辑
        if (e != null) {
            return;
        }

        // lock acquired
        if (ttlRemaining == null) {
            scheduleExpirationRenewal(threadId);
        }
    });
    return ttlRemainingFuture;
}
//默认自动续期时间30s,看门狗时间
private long lockWatchdogTimeout = 30 * 1000;
<T> RFuture<T> tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
    internalLockLeaseTime = unit.toMillis(leaseTime);

    return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, command,
              "if (redis.call('exists', KEYS[1]) == 0) then " +
                  "redis.call('hset', KEYS[1], ARGV[2], 1); " +
                  "redis.call('pexpire', KEYS[1], ARGV[1]); " +
                  "return nil; " +
              "end; " +
              "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
                  "redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
                  "redis.call('pexpire', KEYS[1], ARGV[1]); " +
                  "return nil; " +
              "end; " +
              "return redis.call('pttl', KEYS[1]);",
                Collections.<Object>singletonList(getName()), internalLockLeaseTime, getLockName(threadId));
}

重新设置超时时间

private void scheduleExpirationRenewal(long threadId) {
    ExpirationEntry entry = new ExpirationEntry();
    ExpirationEntry oldEntry = EXPIRATION_RENEWAL_MAP.putIfAbsent(getEntryName(), entry);
    if (oldEntry != null) {
        oldEntry.addThreadId(threadId);
    } else {
        entry.addThreadId(threadId);
        renewExpiration();
    }
}

开启定时任务,发送 LUA 脚本,锁的超时时间达到1/3就重新设为30s

private void renewExpiration() {
    ExpirationEntry ee = EXPIRATION_RENEWAL_MAP.get(getEntryName());
    if (ee == null) {
        return;
    }
    
    Timeout task = commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
        @Override
        public void run(Timeout timeout) throws Exception {
            ExpirationEntry ent = EXPIRATION_RENEWAL_MAP.get(getEntryName());
            if (ent == null) {
                return;
            }
            Long threadId = ent.getFirstThreadId();
            if (threadId == null) {
                return;
            }
            
            RFuture<Boolean> future = renewExpirationAsync(threadId);
            future.onComplete((res, e) -> {
                if (e != null) {
                    log.error("Can't update lock " + getName() + " expiration", e);
                    return;
                }
                
                if (res) {
                    // reschedule itself
                    renewExpiration();
                }
            });
        }
       //只要占锁成功,就会启动一个定时任务【重新给锁设置过期时间,新的过期时间就是看门狗的默认时间】,每隔10s都会自动再次续期,续成30s internalLockLeaseTime【看门狗时间】 / 3,10s
    }, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);
    
    ee.setTimeout(task);
}

重新设置超时时间 LUA 脚本

protected RFuture<Boolean> renewExpirationAsync(long threadId) {
     return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
             "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
                 "redis.call('pexpire', KEYS[1], ARGV[1]); " +
                 "return 1; " +
             "end; " +
             "return 0;",
         Collections.<Object>singletonList(getName()), 
         internalLockLeaseTime, getLockName(threadId));
 }