基于Redisson实现分布式锁

面试总是会被问到有没有用过分布式锁、redis 锁,大部分平时很少接触到,所以只能很无奈的回答 “没有”。本文通过 Spring Boot 整合 redisson 来实现分布式锁,并结合 demo 测试结果。
首先看下大佬总结的图:

在这里插入图片描述
下面介绍实现方式:

1. pom.xml导入需要使用的依赖

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

        <!--自动生成get set-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>

        <!-- swagger -->
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
            <version>2.9.2</version>
        </dependency>
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger-ui</artifactId>
            <version>2.9.2</version>
        </dependency>

说明:
实现基础功能只需要使用到前两个redis, redisson 的依赖
加lombok依赖是为了方便实用@Slf4j注解快速实现log日志的打印
加入swagger2是为了方便接口测试使用,实际也可以使用postman工具进行测试,不一定非要使用swagger2

2.配置application.yaml文件

spring:
  redis:
    host: localhost
    port: 6379
    password: 12345
    jedis:
      pool:
# 连接池最大连接数(使用负值表示没有限制)
        max-active: 100
# 连接池中的最小空闲连接
        max-idle: 10
# 连接池最大阻塞等待时间(使用负值表示没有限制)
        max-wait: -1
# 连接超时时间(毫秒)
    timeout: 5000
# 默认是索引为0的数据库
    database: 0

3. 创建配置类

由于我们使用的是springboot,所以使用javaconfig文件配置属性,尽量不使用xml配置文件

@Configuration
public class RedissonConfig {

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

    @Bean
    public RedissonClient redissonClient(){
        Config config =new Config();
        //单节点
        config.useSingleServer().setAddress("redis://"+host+":"+port);
        if (StringUtils.isEmpty(password)){
            config.useSingleServer().setPassword(null);
        }else {
            config.useSingleServer().setPassword(password);
        }

//        //添加主从配置
//        config.useMasterSlaveServers()
//                .setMasterAddress("")
//                .setPassword("")
//                .addSlaveAddress("","");

//        //集群模式配置 setScanInterval() 扫描间隔时间,单位是毫秒
//        //可以使用"redis://" 来启用SSL连接
//        config.useClusterServers()
//                .setScanInterval(2000)
//                .addNodeAddress("redis://127.0.0.1:7000", "redis://127.0.0.1:7001")
//                .addNodeAddress("redis://127.0.0.1:7002");

        return Redisson.create(config);

    }
}

注意: redisson 他实际上支持主从配置模式和集群模式的部署方式,为了方便测试,我只使用了单节点的方式进行配置,其他两种配置 注释掉了.

4.下面来看业务实现

接口

/**
 * @Author: shangjp
 * @Email: shangjp@163.com
 * @Date: 2020/5/20 11:08
 * @Description: 底层封装
 */
public interface DistributedLocker {

    RLock lock(String lockKey);

    RLock lock(String lockKey, int timeout);

    RLock lock(String lockKey, TimeUnit unit, int timeout);

    boolean tryLock(String lockKey, TimeUnit unit, int waitTime, int leaseTime);

    void unlock(String lockKey);

    void unlock(RLock lock);

    /**
     * 获取计数器
     *
     * @param name
     * @return
     */
    RCountDownLatch getCountDownLatch(String name);

    /**
     * 获取信号量
     *
     * @param name
     * @return
     */
    RSemaphore getSemaphore(String name);

}

实现类

/**
 * @Author: shangjp
 * @Email: shangjp@163.com
 * @Date: 2020/5/20 11:15
 * @Description:
 */
@Service
public class RedisDistributedLocker implements DistributedLocker {

    @Autowired
    private RedissonClient redissonClient;

    @Override
    public RLock lock(String lockKey) {
        RLock lock = redissonClient.getLock(lockKey);
        lock.lock();
        return lock;
    }

    /**
     * 加锁
     * @param lockKey key
     * @param leaseTime 持有锁的最长时间(单位默认是秒)
     */
    @Override
    public RLock lock(String lockKey, int leaseTime) {
        RLock lock = redissonClient.getLock(lockKey);
        lock.lock(leaseTime,TimeUnit.SECONDS);
        return lock;
    }

    @Override
    public RLock lock(String lockKey, TimeUnit unit, int leaseTime) {
        RLock lock = redissonClient.getLock(lockKey);
        lock.lock(leaseTime,unit);
        return lock;
    }

    /**
     * 尝试加锁--出错会抛异常
     * @param lockKey key
     * @param unit 单位
     * @param waitTime 请求获取锁的最大超时时间
     * @param leaseTime 上锁后对锁的最长的持有时间
     * @return
     */
    @Override
    public boolean tryLock(String lockKey, TimeUnit unit, int waitTime, int leaseTime) {
        RLock lock = redissonClient.getLock(lockKey);
        try {
           return lock.tryLock(waitTime,leaseTime,unit);
        } catch (InterruptedException e) {
            return false;
        }
    }

    /**
     * 解锁
     * @param lockKey key
     */
    @Override
    public void unlock(String lockKey) {
        RLock lock = redissonClient.getLock(lockKey);
        lock.unlock();
    }

    @Override
    public void unlock(RLock lock) {
        lock.unlock();
    }

    /**
     * 获取计数器
     *
     * @param name
     * @return
     */
    @Override
    public RCountDownLatch getCountDownLatch(String name) {
        return redissonClient.getCountDownLatch(name);
    }

    /**
     * 获取信号量
     *
     * @param name
     * @return
     */
    @Override
    public RSemaphore getSemaphore(String name) {
        return redissonClient.getSemaphore(name);
    }
}

5.为了方便调用我们实现一个工具类

/**
 * @Author: shangjp
 * @Email: shangjp@163.com
 * @Date: 2020/5/20 10:57
 * @Description: redis分布式锁工具类类
 */
public class RedisLockUtil {



    private static RedisDistributedLocker distributedLocker = SpringContextHolder.getBean(RedisDistributedLocker.class);

    /**
     * 加锁
     *
     * @param lockKey
     * @return
     */
    public static RLock lock(String lockKey) {
        return distributedLocker.lock(lockKey);
    }

    /**
     * 带超时的锁
     *
     * @param lockKey
     * @param timeout 超时时间   单位:秒
     */
    public static RLock lock(String lockKey, int timeout) {
        return distributedLocker.lock(lockKey, timeout);
    }

    /**
     * 带超时的锁
     *
     * @param lockKey
     * @param unit    时间单位
     * @param timeout 超时时间
     */
    public static RLock lock(String lockKey, int timeout, TimeUnit unit) {
        return distributedLocker.lock(lockKey, unit, timeout);
    }

    /**
     * 尝试获取锁
     *
     * @param lockKey
     * @param waitTime  最多等待时间
     * @param leaseTime 上锁后自动释放锁时间
     * @return
     */
    public static boolean tryLock(String lockKey, int waitTime, int leaseTime) {
        return distributedLocker.tryLock(lockKey, TimeUnit.SECONDS, waitTime, leaseTime);
    }

    /**
     * 尝试获取锁
     *
     * @param lockKey
     * @param unit      时间单位
     * @param waitTime  最多等待时间
     * @param leaseTime 上锁后自动释放锁时间
     * @return
     */
    public static boolean tryLock(String lockKey, TimeUnit unit, int waitTime, int leaseTime) {
        return distributedLocker.tryLock(lockKey, unit, waitTime, leaseTime);
    }

    /**
     * 释放锁
     *
     * @param lockKey
     */
    public static void unlock(String lockKey) {
        distributedLocker.unlock(lockKey);
    }

    /**
     * 释放锁
     *
     * @param lock
     */
    public static void unlock(RLock lock) {
        distributedLocker.unlock(lock);
    }
    /**
     * 获取计数器
     *
     * @param name
     * @return
     */
    public static RCountDownLatch getCountDownLatch(String name) {
        return distributedLocker.getCountDownLatch(name);
    }

    /**
     * 获取信号量
     *
     * @param name
     * @return
     */
    public static RSemaphore getSemaphore(String name) {
        return distributedLocker.getSemaphore(name);
    }
}

需要注意的一点是:

private static RedisDistributedLocker distributedLocker = SpringContextHolder.getBean(RedisDistributedLocker.class);

这个地方其实是有个坑的,因为我们工具类中的方法都是static静态的但是静态方法却需要调用service中的函数,@Autowired无法注入静态bean,导致方法无法被声明为static。这种情况下找到利用Spring的使用SpringContextHolder工具类的getBean方法来使得service方法能够被声明为静态方法。

6.自定义SpringContextHolder类

/**
 * @Author: shangjp
 * @Email: shangjp@163.com
 * @Date: 2020/5/20 12:05
 * @Description: 自定义ContextHolder
 */

@Component
public class SpringContextHolder implements ApplicationContextAware, DisposableBean {

    /**
     * 以静态变量保存ApplicationContext,可在任意代码中取出ApplicaitonContext.
     */
    private static ApplicationContext context=null;


    /**
     * 实现ApplicationContextAware接口的context注入函数, 将其存入静态变量.
     */
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        SpringContextHolder.context = applicationContext;
    }

    public static ApplicationContext getApplicationContext() {
        assertContextInjected();
        return context;
    }

    /**
     * 从静态变量ApplicationContext中取得Bean, 自动转型为所赋值对象的类型.
     */
    @SuppressWarnings("unchecked")
    public static <T> T getBean(String name) {
        assertContextInjected();
        return (T) context.getBean(name);
    }

    /**
     * 从静态变量applicationContext中取得Bean, 自动转型为所赋值对象的类型.
     */
    public static <T> T getBean(Class<T> requiredType) {
        assertContextInjected();
        return context.getBean(requiredType);
    }

    /**
     * 从静态变量applicationContext中取得Bean, 自动转型为所赋值对象的类型.
     */
    public static <T> T getBean(String name, Class<T> requiredType) {
        assertContextInjected();
        return context.getBean(name, requiredType);
    }

    /**
     * 清除SpringContextHolder中的ApplicationContext为Null.
     */
    public static void clearHolder() {
        context = null;
    }

    /**
     * 实现DisposableBean接口, 在Context关闭时清理静态变量.
     */
    @Override
    public void destroy() throws Exception {
        SpringContextHolder.clearHolder();
    }

    /**
     * 检查ApplicationContext不为空.
     */
    private static void assertContextInjected() {
        Assert.notNull(context, "applicaitonContext属性未注入, 请在applicationContext.xml中定义SpringContextHolder");
    }
}

到此为止,我们完成基本的配置,下面写一个controller来测试一下

/**
 * @Author: shangjp
 * @Email: shangjp@163.com
 * @Date: 2020/5/20 15:29
 * @Description: redis分布式锁控制器
 */
@RestController
@Api(tags = "redisson", description = "redis分布式锁控制器")
@RequestMapping("/redisson")
@Slf4j
public class RedissonLockController {

    /**
     * 锁测试共享变量
     */
    private Integer lockCount = 10;

    /**
     * 无锁测试共享变量
     */
    private Integer count = 10;

    /**
     * 模拟线程数
     */
    private static int threadNum =10;

    /**
     * 模拟并发测试加锁和不加锁
     * 根据打印结果可以明显看到,未加锁的 count-- 后值是乱序的,而加锁后的结果和我们预期的一样。
     * 由于条件问题没办法测试分布式的并发。只能模拟单服务的这种并发,但是原理是一样,
     * @return
     */
    @GetMapping("/test")
    @ApiOperation("模拟并发测试加锁和不加锁")
    private void lock() {
        //计数器
        final CountDownLatch countDownLatch =new CountDownLatch(1);

        for (int i = 0; i < threadNum; i++) {

            MyRunnable myRunnable = new MyRunnable(countDownLatch);
            Thread thread = new Thread(myRunnable);
            thread.start();
        }
        //释放所有的线程
        countDownLatch.countDown();
    }

    /**
     * 加锁测试
     */
    private  void  testLockCount() {
        String lockKey = "lock-test";
        try {
            RedisLockUtil.lock(lockKey,2, TimeUnit.SECONDS);
            lockCount--;
            log.info("lockCount值:"+lockCount);
        } catch (Exception e) {
            log.error(e.getMessage(),e);
        }finally {
            //必须在finally代码块中释放锁,避免产生死锁
            RedisLockUtil.unlock(lockKey);
        }
    }

    /**
     * 无锁测试
     */
    private void testCount() {
        count--;
        log.info("count值:"+count);
    }


    public class MyRunnable implements Runnable {

        /**
         * 计数器
         */
        final CountDownLatch countDownLatch;

        public MyRunnable(CountDownLatch countDownLatch) {
            this.countDownLatch = countDownLatch;
        }


        @Override
        public void run() {
            //阻塞当前线程,直到计数器的值为0
            try {
                countDownLatch.await();
            } catch (InterruptedException e) {
                log.error(e.getMessage(),e);
            }
            //无锁操作
            testCount();

            //加锁操作
            testLockCount();
        }
    }
}

启动项目,测试一下:
在这里插入图片描述
结果分析:
根据打印结果可以明显看到,未加锁的 count-- 后值是乱序的,而加锁后的结果和我们预期的一样

下面我们通过分析Redisson的源码来理解一下Redisson底层实现的原理

getLock()其实就是去获取一个锁的实例

@Override
    public RLock getLock(String name) {
        return new RedissonLock(connectionManager.getCommandExecutor(), name);
    }

RedissonLock 对获取到的锁进行初始化

public RedissonLock(CommandAsyncExecutor commandExecutor, String name) {
        super(commandExecutor, name);
        //命令执行器
        this.commandExecutor = commandExecutor;
        //uuid 字符串
        this.id = commandExecutor.getConnectionManager().getId();
        //内部锁的过期时间
        this.internalLockLeaseTime = commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout();
        this.entryName = id + ":" + name;
        this.pubSub = commandExecutor.getConnectionManager().getSubscribeService().getLockPubSub();
    }

加锁

  @Override
    public void lock(long leaseTime, TimeUnit unit) {
        try {
            lock(leaseTime, unit, false);
        } catch (InterruptedException e) {
            throw new IllegalStateException();
        }
    }
    @Override
    public void lockInterruptibly() throws InterruptedException {
        lock(-1, null, true);
    }
    @Override
    public void lockInterruptibly(long leaseTime, TimeUnit unit) throws InterruptedException {
        lock(leaseTime, unit, true);
    }
    private void lock(long leaseTime, TimeUnit unit, boolean interruptibly) throws InterruptedException {
    	//获取当前线程的id
        long threadId = Thread.currentThread().getId();
        //尝试获取锁
        Long ttl = tryAcquire(leaseTime, unit, threadId);
        //如果获取ttl为空,则说明获取锁成功
        if (ttl == null) {
            return;
        }
		//如果获取锁失败,则订阅到对应这个锁的channel
        RFuture<RedissonLockEntry> future = subscribe(threadId);
        commandExecutor.syncSubscription(future);

        try {
            while (true) {
            	//一直尝试获取锁
                ttl = tryAcquire(leaseTime, unit, threadId);
                // lock acquired
                //ttl为空,说明成功获取锁,返回
                if (ttl == null) {
                    break;
                }
                // ttl>0 则等待ttl时间之后继续尝试重新获取锁
                if (ttl >= 0) {
                    try {
                        getEntry(threadId).getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
                    } catch (InterruptedException e) {
                        if (interruptibly) {
                            throw e;
                        }
                        getEntry(threadId).getLatch().tryAcquire(ttl, TimeUnit.MILLISECONDS);
                    }
                } else {
                    if (interruptibly) {
                        getEntry(threadId).getLatch().acquire();
                    } else {
                        getEntry(threadId).getLatch().acquireUninterruptibly();
                    }
                }
            }
        } finally {
        	//取消对channel的订阅
            unsubscribe(future, threadId);
        }
//        get(lockAsync(leaseTime, unit));
    }

获取锁

 private <T> RFuture<Long> tryAcquireAsync(long leaseTime, TimeUnit unit, long threadId) {
        //如果带有过期时间,则按照普通的方式获取锁
        if (leaseTime != -1) {
        	//采用底层加锁方式
            return tryLockInnerAsync(leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
        }
        //先按照30秒的过期时间来执行获取锁的方法
        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;
    }

下面我们看一下底层加锁的逻辑:实际上还是使用了lua脚本实现的原子性操作,他使用的是hash数据结构
主要是判断锁是否存在,如果存在的话就设置过期时间,然后对比一下线程id,如果是同一个线程,证明可以
重入,如果所存在,但不是当前线程持有锁,证明别人还没有释放锁,就把剩余的时间返回,加锁失败

  <T> RFuture<T> tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
  		//过期时间
        internalLockLeaseTime = unit.toMillis(leaseTime);

        return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, command,
                  //如果锁不存在,则通过hset设置它的值,并设置过期时间
                  "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; " +
                  //如果锁已经存在了,并且是当前线程持有锁,则通过hincrby给数值加1
                  "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; " +
                  //如果锁存在,但不是当前线程持有锁,则返回所得过期时间pttl
                  "return redis.call('pttl', KEYS[1]);",
                    Collections.<Object>singletonList(getName()), internalLockLeaseTime, getLockName(threadId));
    }

解锁
解锁时一样要去判断,是否是当前线程持有锁,是同一个线程则锁计数器减1,如果锁计数器的值大于0,说明是可重入锁
,那就要刷新过期时间,直到锁计数器值为0,删除key释,放锁
其实原理和AQS很像,AQS就是通过一个volatile修饰的status值,去查看锁的状态,判断是否是可重入的锁.

@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;
    }
protected RFuture<Boolean> unlockInnerAsync(long threadId) {
        return commandExecutor.evalWriteAsync(getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN,
               //如果锁存在,但不是不是当前线程持有锁,返回null
                "if (redis.call('hexists', KEYS[1], ARGV[3]) == 0) then " +
                    "return nil;" +
                "end; " +
                //是同一个线程,则通过hincrby递减1,释放一次锁
                "local counter = redis.call('hincrby', KEYS[1], ARGV[3], -1); " +
                //如果锁计数器的次数任然大于0,则刷新过期时间
                "if (counter > 0) then " +
                    "redis.call('pexpire', KEYS[1], ARGV[2]); " +
                    "return 0; " +
                 //否则证明锁已经释放,删除key并发布释放锁的消息
                "else " +
                    "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));

    }
  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值