分布式锁之Redisson可重入锁

前言

最近在项目中要用到分布式锁, 这里使用的Redisson作分布式锁,这里先记录一下Redisson的可重入锁。

Redisson可重入锁

可以参考官方文档:https://github.com/redisson/redisson/wiki/8.-分布式锁和同步器

可重入锁

A调用B。AB都需要同一锁,此时可重入锁就可以重入,A就可以调用B。不可重入锁时,A调用B将死锁

编写Redisson配置

MyRedissonConfig

package com.atguigu.product.config;

import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.io.IOException;

/**
 * description: MyRedissonConfig
 * date: 2022/11/15 19:32
 * author: MR.孙
 */
@Configuration
public class MyRedissonConfig {

    /**
     * 所有的Redisson的使用都是通过RedissonClient对象
     * @return
     * @throws IOException
     */
    @Bean(destroyMethod="shutdown")
    public RedissonClient redisson() throws IOException {
        // 1、创建配置
        Config config = new Config();
        config.useSingleServer().setAddress("redis://127.0.0.1:6379");

        // 2、根据Config创建出实例
        RedissonClient redissonClient = Redisson.create(config);

        return redissonClient;

    }

}

我们现在用一个Controller来测试Redisson可重入锁。

Controller

@Autowired
private RedissonClient redisson;




 @ResponseBody
    @GetMapping("/hello")
    public String hello(){

        //1. 获取一把锁, 只要锁的名字一样,就是同一把锁
        RLock lock = redisson.getLock("my-lock");

        //2. 加锁
        lock.lock();//阻塞式等待

        try {
            System.out.println("加锁成功...执行业务..."+Thread.currentThread().getId());

            Thread.sleep(30000);
        } catch (Exception e) {

        }finally {
            //3. 解锁
            lock.unlock();
            System.out.println("释放锁"+Thread.currentThread().getId());
        }

        return "hello";

    }

现在用的Redisson可重入锁默认是阻塞等待的,也就是前一个独占锁后没有释放锁,后面的将会阻塞等待上一个释放锁才能继续执行。

结论

我们现在来验证一下,这个结论。
我们这里模拟一下分布式的一个场景,现在有两个商品服务一个是10000端口,一个是100001端口。
image
现在同时对10000端口和10001端口进行访问,看看是否第二个服务会被阻塞。
image
当我们对10000端口进行访问时,进行加锁执行业务,在10001端口并没有打印出相同的提示,而是一直等待10000端口释放锁后才能进行访问。
image
image

redisson的可重入的锁是不会发生死锁的情况的:
我们这里模仿一下突然断电的操作,也就是来不及释放锁,看它会不会发生死锁
同时对10000端口和10001端口进行访问,在锁没有释放的时候关掉服务。
image
可以看到10000端口的锁还没有释放,那么10001端口能获取锁吗?答案是可以的。
image
可以看到计时我们10000端口的锁还没有释放,但是10001端口依然能获取到锁。原因就是看门狗对过期时间进行自动续期的一个机制,默认是30s,即使不手动解锁,锁默认在30s以后自动删除。

总结:

当我们去redis中去看这把锁的时候发现,这把锁的名称就是键的名称,并且值是uuid的一个形式,并且它的默认过期时间是30s,当我们的业务逻辑耗时过长时,redisson中有个叫做看门狗的东西,对锁会进行自动续期,当业务执行完后,锁的自动过期会被删掉,只要加锁的业务运行完成,就不会给当前锁续期,即使不手动解锁,锁默认在30s以后自动删除。
image

手动设置锁过期时间

    // 加锁以后10秒钟自动解锁
        // 无需调用unlock方法手动解锁
        lock.lock(10, TimeUnit.SECONDS);

手动加锁过后,看门狗将不会进行锁的自动续期,并且在指定的过期时间内自动解锁完成,无需手动解锁。
验证:
我们对10000端口进行访问时,并没有手动释放锁,但是因为设置了自动过期时间,所以最终会自动释放锁。
image

image

并且我们先发第一次对10000端口的请求,在第一次请求没有解锁的时候第二次发起对10000端口的请求,发现这个锁并没有锁住全部的服务,但是最终会被释放,因为它设置了自动过期时间。
image

image

需要注意的是,手动设置的自动过期时间必须大于业务的时间,否则将会出现释放的不是自身的锁,释放的是别人的锁,只要设置了手动过期时间大于业务的时间,那么我们在第一次请求10000端口后,在10000端口没有执行完发起第二次请求那么它也是锁住了所有的服务的。
image

源码解析

先来看看默认的不设置过期时间的上锁方法lock()

	
    @Override
    public void lock() {
        try {
            lock(-1, null, false);
        } catch (InterruptedException e) {
            throw new IllegalStateException();
        }
    }
	
	
	
	
private void lock(long leaseTime, TimeUnit unit, boolean interruptibly) throws InterruptedException {
        long threadId = Thread.currentThread().getId();
        Long ttl = tryAcquire(-1, leaseTime, unit, threadId);
        // lock acquired
        if (ttl == null) {
            return;
        }
}

   private Long tryAcquire(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
        return get(tryAcquireAsync(waitTime, leaseTime, unit, threadId));
    }
	



   private <T> RFuture<Long> tryAcquireAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
        RFuture<Long> ttlRemainingFuture;
        if (leaseTime != -1) {
            ttlRemainingFuture = tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
        } else {
            ttlRemainingFuture = tryLockInnerAsync(waitTime, internalLockLeaseTime,
                    TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
        }
        ttlRemainingFuture.onComplete((ttlRemaining, e) -> {
            if (e != null) {
                return;
            }

            // lock acquired
            if (ttlRemaining == null) {
                if (leaseTime != -1) {
                    internalLockLeaseTime = unit.toMillis(leaseTime);
                } else {
                    scheduleExpirationRenewal(threadId);
                }
            }
        });
        return ttlRemainingFuture;
    }
	



  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 " + getRawName() + " expiration", e);
                        EXPIRATION_RENEWAL_MAP.remove(getEntryName());
                        return;
                    }
                    
                    if (res) {
                        // reschedule itself
                        renewExpiration();
                    }
                });
            }
        }, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);
        
        ee.setTimeout(task);
    }
    

可以看到不设置默认时间看门狗默认的自动过期时间为30s,并且通过上面的源码发现,如果业务没有执行完成,会有一个定时任务,每次自动续期,当过了internalLockLeaseTime / 3的时候。
image

接下来看一下手动设置过期时间的void lock(long leaseTime, TimeUnit unit);方法

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


  private void lock(long leaseTime, TimeUnit unit, boolean interruptibly) throws InterruptedException {
        long threadId = Thread.currentThread().getId();
        Long ttl = tryAcquire(-1, leaseTime, unit, threadId);
        // lock acquired
        if (ttl == null) {
            return;
        }
}		



   private Long tryAcquire(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
        return get(tryAcquireAsync(waitTime, leaseTime, unit, threadId));
    }
	
	
   private <T> RFuture<Long> tryAcquireAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
        RFuture<Long> ttlRemainingFuture;
        if (leaseTime != -1) {
            ttlRemainingFuture = tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
        } else {
            ttlRemainingFuture = tryLockInnerAsync(waitTime, internalLockLeaseTime,
                    TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
        }
	}
	
	
    <T> RFuture<T> tryLockInnerAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
        return evalWriteAsync(getRawName(), LongCodec.INSTANCE, command,
                "if (redis.call('exists', KEYS[1]) == 0) then " +
                        "redis.call('hincrby', 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.singletonList(getRawName()), unit.toMillis(leaseTime), getLockName(threadId));
    }
	


可以看到手动设置过期时间最终也会使用Lua的脚本。

完整代码

    @ResponseBody
    @GetMapping(value = "/hello")
    public String hello() {

        //1、获取一把锁,只要锁的名字一样,就是同一把锁
        RLock myLock = redisson.getLock("my-lock");

        //2、加锁
        myLock.lock();      //阻塞式等待。默认加的锁都是30s
        //1)、锁的自动续期,如果业务超长,运行期间自动锁上新的30s。不用担心业务时间长,锁自动过期被删掉
        //2)、加锁的业务只要运行完成,就不会给当前锁续期,即使不手动解锁,锁默认会在30s内自动过期,不会产生死锁问题
        // myLock.lock(10,TimeUnit.SECONDS);   //10秒钟自动解锁,自动解锁时间一定要大于业务执行时间
        //问题:在锁时间到了以后,不会自动续期
        //1、如果我们传递了锁的超时时间,就发送给redis执行脚本,进行占锁,默认超时就是 我们制定的时间
        //2、如果我们指定锁的超时时间,就使用 lockWatchdogTimeout = 30 * 1000 【看门狗默认时间】
        //只要占锁成功,就会启动一个定时任务【重新给锁设置过期时间,新的过期时间就是看门狗的默认时间】,每隔10秒都会自动的再次续期,续成30秒
        // internalLockLeaseTime 【看门狗时间】 / 3, 10s
        try {
            System.out.println("加锁成功,执行业务..." + Thread.currentThread().getId());
            try { TimeUnit.SECONDS.sleep(20); } catch (InterruptedException e) { e.printStackTrace(); }
        } catch (Exception ex) {
            ex.printStackTrace();
        } finally {
            //3、解锁  假设解锁代码没有运行,Redisson会不会出现死锁
            System.out.println("释放锁..." + Thread.currentThread().getId());
            myLock.unlock();
        }

        return "hello";
    }
posted @ 2022-11-15 22:12  长情c  阅读(720)  评论(0)    收藏  举报