16.redisson分布式锁使用及注意事项

一灰灰blogSpringBootDB系列RedisRedis分布式锁约 1140 字大约 4 分钟

redis使用分布式锁,除了我们自己借助setnx来实现之外,更为推荐的是借助redisson来完成,借助redisson,可以非常方便的使用redis分布锁,但是一个使用姿势不对,将可能导致锁无法释放问题

本文将介绍一下SpringBoot中redisson分布式锁的使用姿势,以及使用不当导致锁无法释放的演示

I. 项目环境

1. pom依赖

本项目借助SpringBoot 2.2.1.RELEASE + maven 3.5.3 + IDEA + redis进行开发

下面是核心的pom.xml(源码可以再文末获取)

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    <!-- https://mvnrepository.com/artifact/org.redisson/redisson-spring-boot-starter -->
    <dependency>
        <groupId>org.redisson</groupId>
        <artifactId>redisson-spring-boot-starter</artifactId>
        <version>3.15.0</version>
    </dependency>
</dependencies>

2. 配置文件

redis的配置,我们这里采用默认的配置,本机启动一个redis实例

spring:
  redis:
    host: 127.0.0.1
    port: 6379
    password:

II. 分布式锁

1. 使用姿势

核心类就是获取一个RedissonClient实例,然后借助它来获取锁

@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(new String[]{"",""});

        // 集群模式配置 setScanInterval()扫描间隔时间,单位是毫秒, //可以用"rediss://"来启用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);
    }
}

一种非阻塞的使用方式形如

lock.tryLock();
try {
  // ...
} finally {
  lock.unlock();
}

这里没有显示设置锁的失效时间,默认持有锁30s,且由watch dog(看门狗)每隔10s续期一波,这里的自动续期,请重点关注,后面会说明因为它导致的锁无法释放

手动设置失效时间: tryLock(time, TimeUnit)

  • 当指定失效时间时,将没有看门狗的自动续期逻辑

一个具体的分布式锁使用姿势如下

private void lockReIn(int id) {
    RLock rLock = redissonClient.getLock("lock_prefix_" + id);
    if (rLock.tryLock()) {
        try {
            System.out.println("------- 执行业务逻辑 --------" + Thread.currentThread());
            Thread.sleep(100);
            System.out.println("------- 执行完毕 ----------" + Thread.currentThread());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            rLock.unlock();
        }
    } else {
        System.out.println("get lock failed for " + Thread.currentThread());
    }
}

如果希望阻塞方式获取分布式锁时,使用RLock#lock()来替换RLock#tryLock()

2. 锁无法释放场景

重点关注下上面的自动续期方式,当我们使用姿势不对的时候,可能导致锁无法释放,那么什么样的场景会导致这个问题呢?

  • 主动释放锁异常失败
  • watch dog 一直存活,不断的续期

我们借助线程池来演示这个场景

// 固定线程池,且线程不会被回收,导致时而能重入获取锁,时而不行
ExecutorService executorService = Executors.newFixedThreadPool(2, new NamedThreadFactory("fixed-"));

// 普通线程池,空闲线程会被回收,这样就会导致将不会有其他业务方能获取到锁
ExecutorService customExecutorService = new ThreadPoolExecutor(0, 1,
        1L, TimeUnit.MICROSECONDS,
        new LinkedBlockingQueue<Runnable>(), new NamedThreadFactory("custom-"));

private void unLockFailed(int id) {
    RLock rLock = redissonClient.getLock("lock_prefix_" + id);
    if (rLock.tryLock()) {
        try {
            System.out.println("------- 执行业务逻辑 " + id + " --------" + Thread.currentThread());
        } finally {
            // 模拟释放锁失败
            System.out.println(1 / 0);
            rLock.unlock();
        }
    } else {
        System.out.println("get lock failed for " + Thread.currentThread());
    }
}

public void testLock() {
    executorService.submit(new Runnable() {
        @Override
        public void run() {
            try {
                System.out.println("threadId fix : " + Thread.currentThread().getId());
                unLockFailed(2);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    });

    customExecutorService.submit(new Runnable() {
        @Override
        public void run() {
            try {
                System.out.println("threadId custom : " + Thread.currentThread().getId());
                unLockFailed(3);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    });
}

在使用锁的业务逻辑中,释放锁时模拟了释放失败的case

两个线程池

  • 一个固定大小的线程池(线程不会被回收):再次访问时,之前持有锁的线程依然可以获取锁;另外一个不行
  • 一个普通的线程池(线程会被回收):将没有线程能持有锁

上面这种case最主要的问题在于redissonClient作为单实例,这个实例不回收,看门狗的续期任务也不会取消;因此即便持有锁的业务逻辑走完了,抛异常了,但是续期任务没有感知,依然在默默的执行,从而导致分布式锁一直无法释放,直到redissonClient实例销毁

小结

  • RedissonClient公用时,主动释放锁失败,但是注意看门狗的任务不注销,分布式锁一直续期,从而导致分布式锁无法有效释放

II. 其他

0. 项目

Loading...