SpringBoot集成Redisson分布式锁

Redisson 简介

简介

Redisson 说白了就是redis的儿子,本质上还是对redis进行加锁,不过是对redis进行了很多封装,他不仅仅提供了一系列的分布式的Java常用对象,还提供了许多的分布式服务。

与其他产品的比较

(redis审)

  • Redisson 与 Jedis 、 Lettuce 有什么区别。
  1. Redisson和他俩的区别就像一个用鼠标操作图形化界面 , 一个用命令行操作文件夹,Redisson是更高层的抽象,Jedis和Lettuce是Redis命令的封装。
  2. Jedis是Redis官方推出的用于通过Java连接Redis客户端的一个工具包,提供了Redis的各种命令支持
  3. Lettuce是一种可扩展的线程安全的 Redis 客户端,通讯框架基于Netty,支持高级的 Redis 特性,比如哨兵,集群,管道,自动重新连接和Redis数据模型。Spring Boot 2.x 开始 Lettuce 已取代 Jedis 成为首选 Redis 的客户端。
  4. Redisson是架设在Redis基础上,通讯基于Netty的综合的、新型的中间件,企业级开发中使用Redis的最佳范本
  5. Jedis把Redis命令封装好,Lettuce则进一步有了更丰富的Api,也支持集群等模式。但是两者只给了你操作Redis数据库的脚手架,而Redisson则是基于Redis、Lua和Netty建立起了成熟的分布式解决方案,甚至redis官方都推荐的一种工具集

Redisson 操作使用

引入pom.xml依赖

在引入 Redisson 的依赖后 , 就可以直接调用了.

<!-- 原生 -->
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.13.4</version>
</dependency>

项目中,增加配置文件

package com.example.spring_boot_family_meals.Redisson;

import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.codec.JsonJacksonCodec;
import org.redisson.config.Config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @Date 2023/8/24 10:38
 */
@Configuration
public class RedissonConfig {

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

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

    private int port = 6379;

    @Bean
    public RedissonClient getRedisson() {
        System.out.println("初始化redisson :" + redisHost);
        Config config = new Config();
        config.useSingleServer().
                setAddress("redis://" + redisHost + ":" + port).
                setPassword(password);
        config.setCodec(new JsonJacksonCodec());
        return Redisson.create(config);
    }
}

启动分布式锁 (项目示例)

package com.example.spring_boot_family_meals.Redisson;

import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.TimeUnit;

@RestController
@RequestMapping("/redisson")
public class RedissonController {

    @Resource
    private RedissonClient redissonClient;


    @GetMapping("/test")
    public String test() {
//      1 : 创建线程 : 执行业务 1
        new Thread(() -> business_1()).start();
//      2 : 创建线程 : 执行业务 2
        new Thread(() -> business_2()).start();
//      3 : 创建线程 : 执行业务 3
        new Thread(() -> business_3()).start();
        return "ok";
    }

    /**
     * 业务一 : 对缓存进行加锁,锁过期10分钟 :业务睡眠10秒
     */
    public void business_1() {
//      1 : 获取锁
        RLock rLock = redissonClient.getLock("business");
        System.out.println(getDate() + " : business_1 : 锁获取成功");
        try {
//          1.1 : 加锁,指定锁过期的时间 1分钟
            boolean isLocked = rLock.tryLock(10, TimeUnit.MINUTES);
            System.out.println(getDate() + " : business_1 : 加锁成功");
            if (isLocked) {
//          1.2 : 开始执行逻辑(我们这里睡眠)
                System.out.println(getDate() + " : business_1 : 开始执行业务逻辑");
                Thread.sleep(10000); //睡眠十秒
                System.out.println(getDate() + " : business_1 : 结束执行业务逻辑");
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            System.out.println(getDate() + " : business_1 : 释放锁");
            rLock.unlock();
        }
    }

    /**
     * 业务二 : 对缓存进行加锁,锁过期 1 分钟 : 业务睡眠一百秒
     */
    public void business_2() {
//      1 : 获取锁
        RLock rLock = redissonClient.getLock("business");
        System.out.println(getDate() + " : business_2 : 锁获取成功");
        try {
//          1.1 : 加锁,指定锁过期的时间 1分钟
            boolean isLocked = rLock.tryLock(1, TimeUnit.MINUTES);
            System.out.println(getDate() + " : business_2 : 加锁成功");
            if (isLocked) {
//          1.2 : 开始执行逻辑(我们这里睡眠)
                System.out.println(getDate() + " : business_2 : 开始执行业务逻辑");
                Thread.sleep(100000); // 睡眠100秒
                System.out.println(getDate() + " : business_2 : 结束执行业务逻辑");
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            System.out.println(getDate() + " : business_2 : 释放锁");
            rLock.unlock();
        }
    }

    /**
     * 业务三 : 对缓存进行加锁,锁过期 1 分钟 : 业务睡眠 10 百秒
     */
    public void business_3() {
//      1 : 获取锁
        RLock rLock = redissonClient.getLock("business");
        System.out.println(getDate() + " : business_3 : 锁获取成功");
        try {
//          1.1 : 加锁,指定锁过期的时间 1分钟
            boolean isLocked = rLock.tryLock(1, TimeUnit.MINUTES);
            System.out.println(getDate() + " : business_3 : 加锁成功");
            if (isLocked) {
//          1.2 : 开始执行逻辑(我们这里睡眠)
                System.out.println(getDate() + " : business_3 : 开始执行业务逻辑");
                Thread.sleep(10000); // 睡眠十秒
                System.out.println(getDate() + " : business_3 : 结束执行业务逻辑");
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            System.out.println(getDate() + " : business_3 : 释放锁");
            rLock.unlock();
        }
    }

    /**
     * 获取到当前时分秒
     */
    public static String getDate() {
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        String format = dateFormat.format(new Date());
        return format;
    }

}

  • 执行结果 :
#  Rediss项目启动,初始化成功
初始化redisson :127.0.0.1
2023-08-24 11:19:23.945  INFO 10100 --- [           main] org.redisson.Version                     : Redisson 3.15.5
2023-08-24 11:19:24.759  INFO 10100 --- [sson-netty-2-13] o.r.c.pool.MasterPubSubConnectionPool    : 1 connections initialized for /127.0.0.1:6379
2023-08-24 11:19:25.201  INFO 10100 --- [sson-netty-2-19] o.r.c.pool.MasterConnectionPool          : 24 connections initialized for /127.0.0.1:6379
2023-08-24 11:19:25.471  INFO 10100 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path ''
2023-08-24 11:19:25.479  INFO 10100 --- [           main] c.e.springtest.SpringTestApplication     : Started SpringTestApplication in 2.611 seconds (JVM running for 3.276)
2023-08-24 11:19:33.530  INFO 10100 --- [nio-8080-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring DispatcherServlet 'dispatcherServlet'
2023-08-24 11:19:33.530  INFO 10100 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : Initializing Servlet 'dispatcherServlet'
2023-08-24 11:19:33.534  INFO 10100 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : Completed initialization in 4 ms
# 3个线程同时争夺一把锁
2023-08-24 11:19:33 : business_2 : 锁获取成功
2023-08-24 11:19:33 : business_3 : 锁获取成功
2023-08-24 11:19:33 : business_1 : 锁获取成功
# 业务3 抢锁成功
2023-08-24 11:19:41 : business_3 : 加锁成功
2023-08-24 11:19:41 : business_3 : 开始执行业务逻辑
2023-08-24 11:19:51 : business_3 : 结束执行业务逻辑
2023-08-24 11:19:51 : business_3 : 释放锁
# 业务3 释放锁 ,业务1抢锁
2023-08-24 11:19:51 : business_1 : 加锁成功
2023-08-24 11:19:51 : business_1 : 开始执行业务逻辑
2023-08-24 11:20:01 : business_1 : 结束执行业务逻辑
2023-08-24 11:20:01 : business_1 : 释放锁
# 业务1 释放所 , 业务2抢锁
2023-08-24 11:20:02 : business_2 : 加锁成功
2023-08-24 11:20:02 : business_2 : 开始执行业务逻辑
2023-08-24 11:21:42 : business_2 : 结束执行业务逻辑
2023-08-24 11:21:42 : business_2 : 释放锁

下面是缓存中存放的内容
在这里插入图片描述

Redisson原理讲解

以上的代码简单明了 , 三个线程模拟三个服务来争抢一把锁, 我们只需要一个 Rlock ,下面我们来看看Redisson是怎么实现的

Redisson与使用JDK的ReentrantLock差不多 , 并且也支持并且也支持 ReadWriteLock(读写锁)、Reentrant Lock(可重入锁)、Fair Lock(公平锁)、RedLock(红锁)等各种锁,详细可以参照redisson官方文档来查看。

那么 Redisson到底有哪些优势呢? 锁的自动续期 (默认都是30秒) , 如果业务超长,运行期间会自动给锁续上新的30秒,不用担心业务执行较长而锁被自动删掉.

加锁的业务只要运行完成 , 就不会给当前续期 , 即便不手动解锁,锁默认在30秒后自动删除,不会照成死锁的原因.

前面也提到了锁的自动续期,那么我们来看看Redisson是怎么实现的

Redisson操作原理

下面是Redisson的底层原理

在这里插入图片描述

只要线程一加锁成功,就会启动一个Watch dog 看门狗,他是一个后台线程,会每隔十秒检查一下,如果线程一还持有锁,那么就会不断的延长锁key的生存时间, 因此,Redisson就是使用Watch dog
解决了所过期释放,业务没执行完的问题.

Rlock

Rlock是Redisson 分布式锁的核心接口,继承了concurrent包的Lock接口和自己的RlockAsync接口 , RlockAsync的返回值都是RFuture , 是Redisson执行异步实现的核心逻辑,
也是Hetty发挥的主要地方.

Rlock如何加锁

从Rlock进入 , 找打Redisson类, 找到RedissonLock类,找到 tryLock 方法再递进到干活的tryAcquireOnceAsync 方法,这是加锁的主要代码

    private RFuture<Boolean> tryAcquireOnceAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
        RFuture ttlRemainingFuture;
        if (leaseTime != -1L) {
            ttlRemainingFuture = this.tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_NULL_BOOLEAN);
        } else {
            ttlRemainingFuture = this.tryLockInnerAsync(waitTime, this.internalLockLeaseTime, TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_NULL_BOOLEAN);
        }

        ttlRemainingFuture.onComplete((ttlRemaining, e) -> {
            if (e == null) {
                if (ttlRemaining) {
                    if (leaseTime != -1L) {
                        this.internalLockLeaseTime = unit.toMillis(leaseTime);
                    } else {
                        this.scheduleExpirationRenewal(threadId);
                    }
                }

            }
        });
        return ttlRemainingFuture;
    }

此处会发现 , leaseTime时间判断的2个分枝 , 实际上加锁加锁时是否设置过期时间 , 未设置过期时间(-1)时则会有 watchDog的所续约 , 一个注册1了加锁事件的续约任务 ,
我们先来有过期时间 tryLockInnerAsync 部分

evalWriteAsync 是 eval命令执行的lua入口

    <T> RFuture<T> tryLockInnerAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
        return this.evalWriteAsync(this.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(this.getRawName()), new Object[]{unit.toMillis(leaseTime), this.getLockName(threadId)});
    }

eval命令在行动lua脚本的地方 , 此处的lua脚本展开。

-- 不存在该key时
if (redis.call('exists', KEYS[1]) == 0) then 
  -- 新增该锁并且hash中该线程id对应的count置1
  redis.call('hincrby', KEYS[1], ARGV[2], 1); 
  -- 设置过期时间
  redis.call('pexpire', KEYS[1], ARGV[1]); 
  return nil; 
end; 

-- 存在该key 并且 hash中线程id的key也存在
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]);

redisson 具体参数分析

// keyName
KEYS[1] = Collections.singletonList(this.getName())
// leaseTime
ARGV[1] = this.internalLockLeaseTime
// uuid+threadId组合的唯一值
ARGV[2] = this.getLockName(threadId)

总共3个参数完成了一段逻辑:

  • 判断该锁是否已经有对应的hash表存在.
  • 没有对应的hash表 , 则set和hash表中的一个entry的key为锁名称,value为1,之后设置该hash表生效时间为leaseTime
  • 存在对应的hash表: 则将该LockName 的value执行+1操作,也就是计算进入次数,再设置失效时间 leaseTime
  • 最后返回这把所的剩余时间.

也和上述自定义锁没有区别

既然这样 , 有加锁+1的操作 , 应该也有对应 -1 的操作 , 咱们再看看unlock方法 , 同样查找方法名 , 一路到.

    protected RFuture<Boolean> unlockInnerAsync(long threadId) {
        return this.evalWriteAsync(this.getRawName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN, "if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then return nil;end; local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); if (counter > 0) then redis.call('pexpire', KEYS[1], ARGV[2]); return 0; else redis.call('del', KEYS[1]); redis.call('publish', KEYS[2], ARGV[1]); return 1; end; return nil;", Arrays.asList(this.getRawName(), this.getChannelName()), new Object[]{LockPubSub.UNLOCK_MESSAGE, this.internalLockLeaseTime, this.getLockName(threadId)});
    }

拿出lua部分

-- 不存在key
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 
  -- 过期时间重设
  redis.call('pexpire', KEYS[1], ARGV[2]); 
  return 0; 
else
  -- 删除并发布解锁消息
  redis.call('del', KEYS[1]); 
  redis.call('publish', KEYS[2], ARGV[1]); 
  return 1;
end; 
return nil;

该Lua KEYS有2个Arrays.asList(getName(),getChannelName())

  • name 锁名称
  • channeName 用于pubSub发布消息的channel名称.

ARGV变量有三个LockPubSub.UNLOCK_MESSAGE, internalLockLeaseTime, getLockName(threadId)

  • LockPubSub.UNLOCK_MESSAGE,channel发送消息的类别,此处解锁为0
  • internalLockLeaseTime,watchDog配置的超时时间,默认为30s
  • lockName 这里的lockName指的是uuid和threadId组合的唯一值

步骤如下

  • 如果该锁不存在,则返回nil
  • 如果该锁存在,则将线程的 hash key 计数器 -1
  • 计数器 count>0 ,重置下失效时间,返回0; 否则,删除该锁,发布解锁消息 unlockMessage, 返回 1;

其中 unlock的时候使用到了Redis发布订阅PubSub完成消息通知.
而订阅的步骤就在RedissonLock的加锁入口的lock方法中

long threadId = Thread.currentThread().getId();
Long ttl = this.tryAcquire(-1L, leaseTime, unit, threadId);
if (ttl != null) {
    // 订阅
    RFuture<RedissonLockEntry> future = this.subscribe(threadId);
    if (interruptibly) {
        this.commandExecutor.syncSubscriptionInterrupted(future);
    } else {
        this.commandExecutor.syncSubscription(future);
    }
    // 省略

当锁被其他线程占用时,通过监听锁的释放通知(在其他线程通过RedissonLock释放锁时,会通过发布订阅pub/sub功能发起通知),
等待锁被其他线程释放,也是为了避免自旋的一种常用效率手段

解锁消息

为了一探究竟通知了什么,通知后又做了什么,进入LockPubSub。

这里只有一个明显的监听方法onMessage,其订阅和信号量的释放都在父类PublishSubscribe,我们只关注监听事件的实际操作

protected void onMessage(RedissonLockEntry value, Long message) {
    Runnable runnableToExecute;
     if (message.equals(unlockMessage)) {
         // 从监听器队列取监听线程执行监听回调
         runnableToExecute = (Runnable)value.getListeners().poll();
         if (runnableToExecute != null) {
             runnableToExecute.run();
         }
         // getLatch()返回的是Semaphore,信号量,此处是释放信号量
         // 释放信号量后会唤醒等待的entry.getLatch().tryAcquire去再次尝试申请锁
         value.getLatch().release();
     } else if (message.equals(readUnlockMessage)) {
         while(true) {
             runnableToExecute = (Runnable)value.getListeners().poll();
             if (runnableToExecute == null) {
                 value.getLatch().release(value.getLatch().getQueueLength());
                 break;
             }
             runnableToExecute.run();
         }
     }
 }

发现一个是默认解锁消息 ,一个是读锁解锁消息 ,因为redisson是有提供读写锁的,而读写锁读读情况和读写、写写情况互斥情况不同,我们只看上面的默认解锁消息unlockMessage分支

LockPubSub监听最终执行了2件事

  • runnableToExecute.run() 执行监听回调
  • value.getLatch().release(); 释放信号量

Redisson通过LockPubSub 监听解锁消息,执行监听回调和释放信号量通知等待线程可以重新抢锁。

这时再回来看tryAcquireOnceAsync另一分支

private RFuture<Boolean> tryAcquireOnceAsync(long waitTime, long leaseTime, TimeUnit unit, long threadId) {
        if (leaseTime != -1L) {
            return this.tryLockInnerAsync(waitTime, leaseTime, unit, threadId, RedisCommands.EVAL_NULL_BOOLEAN);
        } else {
            RFuture<Boolean> ttlRemainingFuture = this.tryLockInnerAsync(waitTime, this.commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(), TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_NULL_BOOLEAN);
            ttlRemainingFuture.onComplete((ttlRemaining, e) -> {
                if (e == null) {
                    if (ttlRemaining) {
                        this.scheduleExpirationRenewal(threadId);
                    }

                }
            });
            return ttlRemainingFuture;
        }
    }

可以看到,无超时时间时,在执行加锁操作后,还执行了一段费解的逻辑

ttlRemainingFuture.onComplete((ttlRemaining, e) -> {
                if (e == null) {
                    if (ttlRemaining) {
                        this.scheduleExpirationRenewal(threadId);
                    }
                }
            })                  

此处涉及到Netty的Future/Promise-Listener模型,Redisson中几乎全部以这种方式通信(所以说Redisson是基于Netty通信机制实现的),理解这段逻辑可以试着先理解

在 Java 的 Future 中,业务逻辑为一个 Callable 或 Runnable 实现类,该类的 call()或 run() 执行完毕意味着业务逻辑的完结,
在 Promise 机制中,可以在业务逻辑中人工设置业务逻辑的成功与失败,这样更加方便的监控自己的业务逻辑。

锁续约

查看RedissonLock.this.scheduleExpirationRenewal(threadId)

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

    }

private void renewExpiration() {
        RedissonLock.ExpirationEntry ee = (RedissonLock.ExpirationEntry)EXPIRATION_RENEWAL_MAP.get(this.getEntryName());
        if (ee != null) {
            Timeout task = this.commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
                public void run(Timeout timeout) throws Exception {
                    RedissonLock.ExpirationEntry ent = (RedissonLock.ExpirationEntry)RedissonLock.EXPIRATION_RENEWAL_MAP.get(RedissonLock.this.getEntryName());
                    if (ent != null) {
                        Long threadId = ent.getFirstThreadId();
                        if (threadId != null) {
                            RFuture<Boolean> future = RedissonLock.this.renewExpirationAsync(threadId);
                            future.onComplete((res, e) -> {
                                if (e != null) {
                                    RedissonLock.log.error("Can't update lock " + RedissonLock.this.getName() + " expiration", e);
                                } else {
                                    if (res) {
                                        RedissonLock.this.renewExpiration();
                                    }

                                }
                            });
                        }
                    }
                }
            }, this.internalLockLeaseTime / 3L, TimeUnit.MILLISECONDS);
            ee.setTimeout(task);
        }
    }

拆分来看,这段连续嵌套且冗长的代码实际上做了几步:

  • 添加一个netty的Timeout回调任务,每(internalLockLeaseTime / 3)毫秒执行一次,执行的方法是renewExpirationAsync
  • renewExpirationAsync重置了锁超时时间,又注册一个监听器,监听回调又执行了renewExpiration

renewExpirationAsync 的Lua如下

protected RFuture<Boolean> renewExpirationAsync(long threadId) {
        return this.commandExecutor.evalWriteAsync(this.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.singletonList(this.getName()), new Object[]{this.internalLockLeaseTime, this.getLockName(threadId)});
    }

if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then 
  redis.call('pexpire', KEYS[1], ARGV[1]); 
  return 1; 
end; 
return 0;

重新设置了超时时间。

Redisson加这段逻辑的目的是什么?
目的是为了某种场景下保证业务不影响,如任务执行超时但未结束,锁已经释放的问题。
当一个线程持有了一把锁,由于并未设置超时时间leaseTime,Redisson 默认配置了30S,开启watchDog,每10S对该锁进行一次续约,维持30S的超时时间,直到任务完成再删除锁。
这就是Redisson的锁续约 ,也就是WatchDog 实现的基本思路。

流程总结

通过整体的介绍,流程简单概括:

  • A、B线程争抢一把锁,A获取到后,B阻塞
  • B线程阻塞时并非主动 CAS,而是PubSub方式订阅该锁的广播消息
  • A操作完成释放了锁,B线程收到订阅消息通知
  • B被唤醒开始继续抢锁,拿到锁

详细加锁解锁流程总结如下图:

在这里插入图片描述

公平锁

以上介绍的可重入锁是非公平锁 , Redisson还基于Redis队列 list 和 Zset 实现了公平锁

java中的公平锁

公平的定义是什么?

公平就是按照客户端的请求先来后到排队来获取锁,先到先得,也就是FIFO,所以队列和容器顺序编排必不可少

回顾JUC的ReentrantLock公平锁的实现

/**
 * Sync object for fair locks
 */
static final class FairSync extends Sync {
    private static final long serialVersionUID = -3000897897090466540L;

    final void lock() {
        acquire(1);
    }

    /**
     * Fair version of tryAcquire.  Don't grant access unless
     * recursive call or no waiters or is first.
     */
    protected final boolean tryAcquire(int acquires) {
        final Thread current = Thread.currentThread();
        int c = getState();
        if (c == 0) {
            if (!hasQueuedPredecessors() &&
                compareAndSetState(0, acquires)) {
                setExclusiveOwnerThread(current);
                return true;
            }
        }
        else if (current == getExclusiveOwnerThread()) {
            int nextc = c + acquires;
            if (nextc < 0)
                throw new Error("Maximum lock count exceeded");
            setState(nextc);
            return true;
        }
        return false;
    }
}

AQS已经提供了整个实现,是否公平取决于实现类取出节点逻辑是否顺序取

在这里插入图片描述

AbstractQueuedSynchronizer是用来构建锁或者其他同步组件的基础框架,通过内置FIFO队列来完成资源获取线程的排队工作,
自身没有实现同步接口,仅仅定义了若干同步状态获取和释放的方法来供自定义同步组件使用(上图),
支持独占和共享获取,这是基于模版方法模式的一种设计,给公平/非公平提供了土壤。

我们用2张图来简单解释AQS的等待流程

一张是同步队列(FIFO双向队列)管理 获取同步状态失败(抢锁失败)的线程引用、等待状态和前驱后继节点的流程图

在这里插入图片描述

一张是独占式获取同步状态的总流程,核心acquire(int arg)方法发调用流程

在这里插入图片描述

可以看出锁的获取流程

AQS维护一个同步队列,获取状态失败的线程都会加入到队列中进行自旋,移出队列或停止自旋的条件是前驱节点为头节点切成功获取了同步状态。而比较另一段非公平锁类NonfairSync可以发现,控制公平和非公平的关键代码,在于hasQueuedPredecessors方法。

static final class NonfairSync extends Sync {
    private static final long serialVersionUID = 7316153563782823691L;

    /**
     * Performs lock.  Try immediate barge, backing up to normal
     * acquire on failure.
     */
    final void lock() {
        if (compareAndSetState(0, 1))
            setExclusiveOwnerThread(Thread.currentThread());
        else
            acquire(1);
    }

    protected final boolean tryAcquire(int acquires) {
        return nonfairTryAcquire(acquires);
    }
}

NonfairSync减少了hasQueuedPredecessors判断条件,该方法的作用就是

  • 查看同步队列中当前节点是否有前驱节点,如果有比当前线程更早请求获取锁则返回true。
  • 保证每次都取队列的第一个节点(线程)来获取锁,这就是公平规则

为什么JUC以默认非公平锁呢?

因为当一个线程请求锁时,只要获取来同步状态即成功获取。在此前提下,刚释放的线程再次获取同步状态的几率会非常大,使得其他线程只能在同步队列中等待。但这样带来的好处是,非公平锁大大减少了系统线程上下文的切换开销。

Redis里没有AQS,但是有List和zSet,看看Redisson是怎么实现公平的

RedissonFairLock

    /**
     * 业务0 : 公平锁
     */
    public void equity() {
//      1 : 获取锁(公平锁)
        RLock equity = redissonClient.getLock("business");
        try {
//          1.1 : 加锁,指定锁过期的时间 1分钟
            boolean isLocked = equity.tryLock(10, TimeUnit.MINUTES);
            if (isLocked) {
//                1.1.1 : 开始执行逻辑(我们这里睡眠)
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            equity.unlock();
        }
    }

RedissonFairLock继承自RedissonLock,同样一路向下找到加锁实现方法tryLockInnerAsync

这里有2段冗长的Lua,但是Debug发现,公平锁的入口在 command == RedisCommands.EVAL_LONG 之后,此段Lua较长,参数也多,我们着重分析Lua的实现规则

  • 参数
-- lua中的几个参数
KEYS = Arrays.<Object>asList(getName(), threadsQueueName, timeoutSetName)
KEYS[1]: lock_name, 锁名称                   
KEYS[2]: "redisson_lock_queue:{xxx}"  线程队列
KEYS[3]: "redisson_lock_timeout:{xxx}"  线程id对应的超时集合

ARGV =  internalLockLeaseTime, getLockName(threadId), currentTime + threadWaitTime, currentTime
ARGV[1]: "{leaseTime}" 过期时间
ARGV[2]: "{Redisson.UUID}:{threadId}"   
ARGV[3] = 当前时间 + 线程等待时间:(10:00:00) + 5000毫秒 = 10:00:05
ARGV[4] = 当前时间(10:00:00)  部署服务器时间,非redis-server服务器时间

公平锁实现的Lua脚本

-- 1.死循环清除过期key
while true do 
  -- 获取头节点
    local firstThreadId2 = redis.call('lindex', KEYS[2], 0);
    -- 首次获取必空跳出循环
  if firstThreadId2 == false then 
    break;
  end;
  -- 清除过期key
  local timeout = tonumber(redis.call('zscore', KEYS[3], firstThreadId2));
  if timeout <= tonumber(ARGV[4]) then
    redis.call('zrem', KEYS[3], firstThreadId2);
    redis.call('lpop', KEYS[2]);
  else
    break;
  end;
end;

-- 2.不存在该锁 && (不存在线程等待队列 || 存在线程等待队列而且第一个节点就是此线程ID),加锁部分主要逻辑
if (redis.call('exists', KEYS[1]) == 0) and 
  ((redis.call('exists', KEYS[2]) == 0)  or (redis.call('lindex', KEYS[2], 0) == ARGV[2])) then
  -- 弹出队列中线程id元素,删除Zset中该线程id对应的元素
  redis.call('lpop', KEYS[2]);
  redis.call('zrem', KEYS[3], ARGV[2]);
  local keys = redis.call('zrange', KEYS[3], 0, -1);
  -- 遍历zSet所有key,将key的超时时间(score) - 当前时间ms
  for i = 1, #keys, 1 do 
    redis.call('zincrby', KEYS[3], -tonumber(ARGV[3]), keys[i]);
  end;
    -- 加锁设置锁过期时间
  redis.call('hset', KEYS[1], ARGV[2], 1);
  redis.call('pexpire', KEYS[1], ARGV[1]);
  return nil;
end;

-- 3.线程存在,重入判断
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;

-- 4.返回当前线程剩余存活时间
local timeout = redis.call('zscore', KEYS[3], ARGV[2]);
    if timeout ~= false then
  -- 过期时间timeout的值在下方设置,此处的减法算出的依旧是当前线程的ttl
  return timeout - tonumber(ARGV[3]) - tonumber(ARGV[4]);
end;

-- 5.尾节点剩余存活时间
local lastThreadId = redis.call('lindex', KEYS[2], -1);
local ttl;
-- 尾节点不空 && 尾节点非当前线程
if lastThreadId ~= false and lastThreadId ~= ARGV[2] then
  -- 计算队尾节点剩余存活时间
  ttl = tonumber(redis.call('zscore', KEYS[3], lastThreadId)) - tonumber(ARGV[4]);
else
  -- 获取lock_name剩余存活时间
  ttl = redis.call('pttl', KEYS[1]);
end;

-- 6.末尾排队
-- zSet 超时时间(score),尾节点ttl + 当前时间 + 5000ms + 当前时间,无则新增,有则更新
-- 线程id放入队列尾部排队,无则插入,有则不再插入
local timeout = ttl + tonumber(ARGV[3]) + tonumber(ARGV[4]);
if redis.call('zadd', KEYS[3], timeout, ARGV[2]) == 1 then
  redis.call('rpush', KEYS[2], ARGV[2]);
end;
return ttl;

公平锁加锁步骤

通过上方的lua.可以发现,lua操作的关键结构是列表(list) 和有序集合(zSet)

其中list维护了一个等待的线程队列,redisson_lock_queue:{xxx},zSet维护了一个线程超时情况的有序集合 redisson_lock_timeout:{xxx},
尽管lua较长,但是可以拆分为6个步骤

  • 队列清理

    保证队列中只有未过期的等待线程

  • 首次加锁

    hset加锁,pexpire过期时间

  • 重入判断

    此处同可重入锁lua

  • 返回ttl

  • 计算尾节点ttl

    初始值为锁的剩余过期时间

  • 末尾排队

    ttl + 2 * currentTime + waitTime是score的默认值计算公式

  • 27
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

欢乐少年1904

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值