Springboot基于Redisson实现Redis分布式可重入锁【案例到源码分析】

本文涉及的产品
云数据库 Tair(兼容Redis),内存型 2GB
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
简介: Springboot基于Redisson实现Redis分布式可重入锁【案例到源码分析】

Springboot基于Redisson实现Redis分布式可重入锁【案例到源码分析】


一、前言

我们在实现使用Redis实现分布式锁,最开始一般使用SET resource-name anystring NX EX max-lock-time进行加锁,使用Lua脚本保证原子性进行实现释放锁。这样手动实现比较麻烦,对此Redis官网也明确说Java版使用Redisson来实现。小编也是看了官网慢慢的摸索清楚,特写此记录一下。从官网到整合Springboot到源码解读,以单节点为例,小编的理解都在注释里,希望可以帮助到大家!!

二、为什么使用Redisson

1. 我们打开官网

redis中文官网

2. 我们可以看到官方让我们去使用其他

3. 打开官方推荐

4. 找到文档

Redisson地址

5. Redisson结构

三、Springboot整合Redisson

1. 导入依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
</dependency>
<!--redis分布式锁-->
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.12.0</version>
</dependency>

2. 以官网为例查看如何配置

3. 编写配置类

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;
/**
 * @author wangzhenjun
 * @date 2022/2/9 9:57
 */
@Configuration
public class MyRedissonConfig {
    /**
     * 所有对redisson的使用都是通过RedissonClient来操作的
     * @return
     */
    @Bean(destroyMethod="shutdown")
    public RedissonClient redisson(){
        // 1. 创建配置
        Config config = new Config();
        // 一定要加redis://
        config.useSingleServer().setAddress("redis://192.168.17.130:6379");
        // 2. 根据config创建出redissonClient实例
        RedissonClient redissonClient = Redisson.create(config);
        return redissonClient;
    }
}

4. 官网测试加锁例子

5. 根据官网简单Controller接口编写

@ResponseBody
@GetMapping("/hello")
public String hello(){
    // 1.获取一把锁,只要锁名字一样,就是同一把锁
    RLock lock = redisson.getLock("my-lock");
    // 2. 加锁
    lock.lock();// 阻塞试等待  默认加的都是30s
    // 带参数情况
    // lock.lock(10, TimeUnit.SECONDS);// 10s自动解锁,自动解锁时间一定要大于业务的执行时间。
    try {
        System.out.println("加锁成功" + Thread.currentThread().getId());
        Thread.sleep(30000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    } finally {
        // 3. 解锁
        System.out.println("解锁成功:" + Thread.currentThread().getId());
        lock.unlock();
    }
    return "hello";
}

6. 测试

四、lock.lock()源码分析

1. 打开RedissonLock实现类

2. 找到实现方法

@Override
public void lock() {
    try {
      // 我们发现不穿过期时间源码默认过期时间为-1
        lock(-1, null, false);
    } catch (InterruptedException e) {
        throw new IllegalStateException();
    }
}

3. 按住Ctrl进去lock方法

private void lock(long leaseTime, TimeUnit unit, boolean interruptibly) throws InterruptedException {
  // 获取线程的id,占有锁的时候field的值为UUID:线程号id
    long threadId = Thread.currentThread().getId();
    // 尝试获得锁
    Long ttl = tryAcquire(leaseTime, unit, threadId);
    // lock acquired 获得锁,返回
    if (ttl == null) {
        return;
    }
  // 这里说明获取锁失败,就通过线程id订阅这个锁
    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);
    }
//        get(lockAsync(leaseTime, unit));
}

4. 进去尝试获取锁方法

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) {
    // 这里进行判断如果没有设置参数leaseTime = -1
    if (leaseTime != -1) {
        return tryLockInnerAsync(leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
    }
    // 此方法进行获得锁,过期时间为看门狗的默认时间
    // private long lockWatchdogTimeout = 30 * 1000;看门狗默认过期时间为30s
    // 加锁和过期时间要保证原子性,这个方法后面肯定调用执行了Lua脚本,我们下面在看
    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;
}

5. 查看tryLockInnerAsync()方法

<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; " +
              // hexists查看哈希表的指定字段是否存在,存在锁并且是当前线程持有锁
              "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
                  // hincrby自增一
                  "redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
                    // 锁的值大于1,说明是可重入锁,重置过期时间
                  "redis.call('pexpire', KEYS[1], ARGV[1]); " +
                  "return nil; " +
              "end; " +
              // 锁已存在,且不是本线程,则返回过期时间ttl
              "return redis.call('pttl', KEYS[1]);",
                Collections.<Object>singletonList(getName()), internalLockLeaseTime, getLockName(threadId));
}

6. 进入4留下的定时任务scheduleExpirationRenewal()方法

一步步往下找源码:scheduleExpirationRenewal —>renewExpiration

根据下面源码,定时任务刷新时间为:internalLockLeaseTime / 3,是看门狗的1/3,即为10s刷新一次

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();
                }
            });
        }
    }, internalLockLeaseTime / 3, TimeUnit.MILLISECONDS);
    ee.setTimeout(task);
}

五、lock.lock(10, TimeUnit.SECONDS)源码分析

1. 打开实现类

@Override
public void lock(long leaseTime, TimeUnit unit) {
    try {
      // 这里的过期时间为我们输入的10
        lock(leaseTime, unit, false);
    } catch (InterruptedException e) {
        throw new IllegalStateException();
    }
}

2. 方法lock()实现展示,同三.3源码

3. 直接来到尝试获得锁tryAcquireAsync()方法

private <T> RFuture<Long> tryAcquireAsync(long leaseTime, TimeUnit unit, long threadId) {
    // 这里进行判断如果没有设置参数leaseTime = -1,此时我们为10
    if (leaseTime != -1) {
      // 来到此方法
        return tryLockInnerAsync(leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
    }
    // 此处省略后面内容,前面以详细说明。。。。
}

4. 打开tryLockInnerAsync()方法

我们不难发现和没有传过期时间的方法一样,只不过leaseTime的值变了。

<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; " +
              // hexists查看哈希表的指定字段是否存在,存在锁并且是当前线程持有锁
              "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then " +
                  // hincrby自增一
                  "redis.call('hincrby', KEYS[1], ARGV[2], 1); " +
                    // 锁的值大于1,说明是可重入锁,重置过期时间
                  "redis.call('pexpire', KEYS[1], ARGV[1]); " +
                  "return nil; " +
              "end; " +
              // 锁已存在,且不是本线程,则返回过期时间ttl
              "return redis.call('pttl', KEYS[1]);",
                Collections.<Object>singletonList(getName()), internalLockLeaseTime, getLockName(threadId));
}

六、lock.unlock()源码分析

1. 打开方法实现

@Override
public void unlock() {
    try {
      // 点击进入释放锁方法
        get(unlockAsync(Thread.currentThread().getId()));
    } catch (RedisException e) {
        if (e.getCause() instanceof IllegalMonitorStateException) {
            throw (IllegalMonitorStateException) e.getCause();
        } else {
            throw e;
        }
    }
//        Future<Void> future = unlockAsync();
//        future.awaitUninterruptibly();
//        if (future.isSuccess()) {
//            return;
//        }
//        if (future.cause() instanceof IllegalMonitorStateException) {
//            throw (IllegalMonitorStateException)future.cause();
//        }
//        throw commandExecutor.convertException(future);
}

2. 打开unlockAsync()方法

@Override
public RFuture<Void> unlockAsync(long threadId) {
    RPromise<Void> result = new RedissonPromise<Void>();
    // 解锁方法,后面展开说
    RFuture<Boolean> future = unlockInnerAsync(threadId);
  // 完成
    future.onComplete((opStatus, e) -> {
        if (e != null) {
          // 取消到期续订
            cancelExpirationRenewal(threadId);
            // 将这个未来标记为失败并通知所有人
            result.tryFailure(e);
            return;
        }
    // 状态为空,说明解锁的线程和当前锁不是同一个线程
        if (opStatus == null) {
            IllegalMonitorStateException cause = new IllegalMonitorStateException("attempt to unlock lock, not locked by current thread by node id: "
                    + id + " thread-id: " + threadId);
            result.tryFailure(cause);
            return;
        }
        cancelExpirationRenewal(threadId);
        result.trySuccess(null);
    });
    return result;
}

3. 打开unlockInnerAsync()方法

protected RFuture<Boolean> unlockInnerAsync(long threadId) {
    return commandExecutor.evalWriteAsync(getName(), 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); " +
            // 判断剩余数量是否大于0
            "if (counter > 0) then " +
              // 大于0 ,则刷新过期时间
                "redis.call('pexpire', KEYS[1], ARGV[2]); " +
                "return 0; " +
            "else " +
              // 释放锁,删除key并发布锁释放的消息
                "redis.call('del', KEYS[1]); " +
                "redis.call('publish', KEYS[2], ARGV[1]); " +
                "return 1; "+
            "end; " +
            "return nil;",
            Arrays.<Object>asList(getName(), getChannelName()), LockPubSub.UNLOCK_MESSAGE, internalLockLeaseTime, getLockName(threadId));
}

七、总结

这样大家就跟着小编走完了一遍底层源码,是不是感觉自己又行了,哈哈哈。小编走下来一遍觉得收货还是蛮大的,以前不敢点进去源码,进去就懵逼了,所以人要大胆的向前迈出第一步。一起加油吧,看到这里不一键三连,有点对不起小编了哦!!

相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
6天前
|
NoSQL Java Redis
秒杀抢购场景下实战JVM级别锁与分布式锁
在电商系统中,秒杀抢购活动是一种常见的营销手段。它通过设定极低的价格和有限的商品数量,吸引大量用户在特定时间点抢购,从而迅速增加销量、提升品牌曝光度和用户活跃度。然而,这种活动也对系统的性能和稳定性提出了极高的要求。特别是在秒杀开始的瞬间,系统需要处理海量的并发请求,同时确保数据的准确性和一致性。 为了解决这些问题,系统开发者们引入了锁机制。锁机制是一种用于控制对共享资源的并发访问的技术,它能够确保在同一时间只有一个进程或线程能够操作某个资源,从而避免数据不一致或冲突。在秒杀抢购场景下,锁机制显得尤为重要,它能够保证商品库存的扣减操作是原子性的,避免出现超卖或数据不一致的情况。
39 10
|
21天前
|
存储 运维 NoSQL
分布式读写锁的奥义:上古世代 ZooKeeper 的进击
本文作者将介绍女娲对社区 ZooKeeper 在分布式读写锁实践细节上的思考,希望帮助大家理解分布式读写锁背后的原理。
|
17天前
|
缓存 NoSQL Java
Spring Boot中的分布式缓存方案
Spring Boot提供了简便的方式来集成和使用分布式缓存。通过Redis和Memcached等缓存方案,可以显著提升应用的性能和扩展性。合理配置和优化缓存策略,可以有效避免常见的缓存问题,保证系统的稳定性和高效运行。
35 3
|
24天前
|
存储 Java 关系型数据库
在Spring Boot中整合Seata框架实现分布式事务
可以在 Spring Boot 中成功整合 Seata 框架,实现分布式事务的管理和处理。在实际应用中,还需要根据具体的业务需求和技术架构进行进一步的优化和调整。同时,要注意处理各种可能出现的问题,以保障分布式事务的顺利执行。
44 6
|
2月前
|
缓存 NoSQL Java
大数据-50 Redis 分布式锁 乐观锁 Watch SETNX Lua Redisson分布式锁 Java实现分布式锁
大数据-50 Redis 分布式锁 乐观锁 Watch SETNX Lua Redisson分布式锁 Java实现分布式锁
69 3
大数据-50 Redis 分布式锁 乐观锁 Watch SETNX Lua Redisson分布式锁 Java实现分布式锁
|
1月前
|
消息中间件 NoSQL Java
springboot整合常用中间件框架案例
该项目是Spring Boot集成整合案例,涵盖多种中间件的使用示例,每个案例项目使用最小依赖,便于直接应用到自己的项目中。包括MyBatis、Redis、MongoDB、MQ、ES等的整合示例。
109 1
|
2月前
|
前端开发 Java Apache
Springboot整合shiro,带你学会shiro,入门级别教程,由浅入深,完整代码案例,各位项目想加这个模块的人也可以看这个,又或者不会mybatis-plus的也可以看这个
本文详细讲解了如何整合Apache Shiro与Spring Boot项目,包括数据库准备、项目配置、实体类、Mapper、Service、Controller的创建和配置,以及Shiro的配置和使用。
501 1
Springboot整合shiro,带你学会shiro,入门级别教程,由浅入深,完整代码案例,各位项目想加这个模块的人也可以看这个,又或者不会mybatis-plus的也可以看这个
|
2月前
|
Web App开发 JavaScript Java
elasticsearch学习五:springboot整合 rest 操作elasticsearch的 实际案例操作,编写搜索的前后端,爬取京东数据到elasticsearch中。
这篇文章是关于如何使用Spring Boot整合Elasticsearch,并通过REST客户端操作Elasticsearch,实现一个简单的搜索前后端,以及如何爬取京东数据到Elasticsearch的案例教程。
221 0
elasticsearch学习五:springboot整合 rest 操作elasticsearch的 实际案例操作,编写搜索的前后端,爬取京东数据到elasticsearch中。
|
3月前
|
消息中间件 Java Kafka
springboot项目启动报错-案例情景介绍
springboot项目启动报错-案例情景介绍
88 2
|
3月前
|
运维 NoSQL Java
SpringBoot接入轻量级分布式日志框架GrayLog技术分享
在当今的软件开发环境中,日志管理扮演着至关重要的角色,尤其是在微服务架构下,分布式日志的统一收集、分析和展示成为了开发者和运维人员必须面对的问题。GrayLog作为一个轻量级的分布式日志框架,以其简洁、高效和易部署的特性,逐渐受到广大开发者的青睐。本文将详细介绍如何在SpringBoot项目中接入GrayLog,以实现日志的集中管理和分析。
288 1