Redisson分布式锁

微服务项目中,很多资源需要互斥使用,比如一些分布式任务,比如下单的处理,退货的处理等等。这些都需要用到借助分布式锁来保证处理的唯一性。 一开始我们也手工实现了分布式锁,但是随着业务的发展,我们对锁的特性也要求越来越完善,最后选用了Redis官方推荐的Redisson。

一、Spring Boot中使用Redisson

Spring Boot使用Redisson特别简单,只要引入一个依赖就可以,redis的配置跟其他的redis客户端可以兼容,可以不用再额外配置

二、引入依赖

<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
    <version>3.13.2</version>
</dependency>

三、属性文件Redis配置

# Redis服务器地址
spring.redis.host=127.0.0.1
# Redis服务器连接端口
spring.redis.port=6379

四、快速入门

4.1 改造RedisDistributedLockApplication启动类

使用锁RedissonClient,并实现业务逻辑在ApplicationRunner#run()方法。

package com.erbadagang.springboot.redisdistributedlock;

import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

import javax.annotation.Resource;

@Slf4j
@SpringBootApplication
public class RedisDistributedLockApplication implements ApplicationRunner {

    public static void main(String[] args) {
        SpringApplication.run(RedisDistributedLockApplication.class, args);
    }

    /**
     * 直接注入RedissonClient就可以直接使用.
     */
    @Resource
    private RedissonClient redissonClient;

    @Override
    public void run(ApplicationArguments args) throws Exception {
        log.info("spring boot run");

        //创建锁
        RLock helloLock = redissonClient.getLock("hello");

        //加锁
        helloLock.lock();
        try {
            log.info("locked");
            Thread.sleep(1000 * 10);
        } finally {
            //释放锁
            helloLock.unlock();
        }
        log.info("finished");
    }
}

4.2 测试

启动Redis和RedisDistributedLockApplication,控制台输出:

2020-08-02 22:51:17.169  INFO 8876 --- [main] c.e.s.r.RedisDistributedLockApplication  : spring boot run
2020-08-02 22:51:36.486  INFO 8876 --- [main] c.e.s.r.RedisDistributedLockApplication  : locked
2020-08-02 22:51:46.493  INFO 8876 --- [main] c.e.s.r.RedisDistributedLockApplication  : finished

4.3 Rlock 常用的方法

void lock();
void lock(long leaseTime, TimeUnit unit);
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException;
  • 第一个方法void lock():第一表示lock表示去加锁,加锁成功,没有返回值,继续执行下面代码;但是如果redis已经有这个锁了,它会一直阻塞,直到锁的时间失效(默认30秒),再继续往下执行。这个方法是要保证一定要抢到锁的,它的默认过期时间也是30秒,和tryLock()不同的是,它如果没抢占到锁,会一直自旋。
  • 第二个方法void lock(long leaseTime, TimeUnit unit):和第一无参数lock逻辑一样,只是可以直接设置锁失效时间。用法:helloLock.lock(5, TimeUnit.SECONDS);
  • 第三个方法两个参数的boolean tryLock(long time, TimeUnit unit)表示尝试去加锁(第一个参数表示the maximum time to wait for the lock),加锁成功,返回true,继续执行true下面代码;但是如果redis已经有这个锁了他会等待,还拿不到锁它会返回false,执行false的代码块。为了实现waitTime,使用了redis的订阅发布功能。也就是没有抢到锁的线程订阅消息,直至waitTime过期返回false或者被通知新一轮的开始抢占锁。当然,它如果抢占到锁,锁的过期时间也是30秒,同样也会存在一个定时任务续过期时间,保证业务执行时间不会超过过期时间,抢占失败即返回false。
        String key ="product:001";
        RLock lock = redisson.getLock(key);
        try {
            boolean res = lock.tryLock(10,TimeUnit.SECONDS);
            if ( res){
                System.out.println("这里是你的业务代码");
            }else{
                System.out.println("系统繁忙");
            }
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            lock.unlock();
        }

如果把lock.unlock();注释,第一次执行正常加锁,可以跑到业务逻辑代码,快速第二次执行发现他等待10秒,如果拿不到锁就走else的系统繁忙逻辑。

  • 三个参数的tryLock(long waitTime, long leaseTime, TimeUnit unit)表示尝试去加锁(第一个参数表示等待时间,第二个参数表示key的失效时间),加锁成功,返回true,继续执行true下面代码;如果返回false,它会等待第一个参数设置的时间,然后去执行false下面的代码。个方法的参数leaseTime如果不是-1的话,是不会有定时任务续过期时间的,也就存在业务处理时间可能超过过期时间的风险。其他的和tryLock(long waitTime, TimeUnit unit)一致。
 boolean res = lock.tryLock(5,3, TimeUnit.SECONDS);

这种情况,锁3秒失效,我们配置的是等待5秒,在单机刷的情况下,肯定每次都能拿到锁。

4.4 异步执行分布式锁

        /**
         * 异步锁
         */
        lock = redissonClient.getLock("erbadagang-lock");
        Future<Boolean> res = null;
        try {
            // lock.lockAsync();
            // lock.lockAsync(100, TimeUnit.SECONDS);
            res = lock.tryLockAsync(3, 100, TimeUnit.SECONDS);
            if (res.get()) {
                System.out.println("这里是你的Async业务代码");
            } else {
                System.out.println("系统繁忙Async");
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (res.get()) {
                lock.unlock();
            }
        }
        log.info("finished");
    }

4.5 公平锁(Fair Lock)

Redisson分布式可重入公平锁也是实现了java.util.concurrent.locks.Lock接口的一种RLock对象。在提供了自动过期解锁功能的同时,保证了当多个Redisson客户端线程同时请求加锁时,优先分配给先发出请求的线程。

    /**
     * 公平锁测试。
     */
    @Test
    public void testFairLock() {
        RLock fairLock = redissonClient.getFairLock("anyLock");
        try {
            // 最常见的使用方法
            fairLock.lock();
            // 支持过期解锁功能, 10秒钟以后自动解锁,无需调用unlock方法手动解锁
            fairLock.lock(10, TimeUnit.SECONDS);
            // 尝试加锁,最多等待100秒,上锁以后10秒自动解锁
            boolean res = fairLock.tryLock(100, 10, TimeUnit.SECONDS);
            if (res) {
                System.out.println("这里是你的业务代码");
            } else {
                System.out.println("系统繁忙");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            fairLock.unlock();
        }
    }

Redisson同时还为分布式可重入公平锁提供了异步执行的相关方法:

RLock fairLock = redisson.getFairLock("anyLock");
fairLock.lockAsync();
fairLock.lockAsync(10, TimeUnit.SECONDS);
Future<Boolean> res = fairLock.tryLockAsync(100, 10, TimeUnit.SECONDS);

五、扩展实现

Redis单节点配置:

server:
  port: 8080

spring:
  application:
    name: redis-distributed-lock

  ################ Redis  ##############
  redis:
    host: 127.0.0.1
    port: 6379
    #password:
    timeout: 3000
    lettuce:
      pool:
        max-active: 8
        max-wait: -1
        max-idle: 8
        min-idle: 0
    redisson:
      config:
        # 单节点配置
        singleServerConfig:
          # 连接空闲超时,单位:毫秒
          idleConnectionTimeout: 10000
          pingTimeout: 1000
          # 连接超时,单位:毫秒
          connectTimeout: 10000
          # 命令等待超时,单位:毫秒
          timeout: 3000
          # 命令失败重试次数,如果尝试达到 retryAttempts(命令失败重试次数) 仍然不能将命令发送至某个指定的节点时,将抛出错误。
          # 如果尝试在此限制之内发送成功,则开始启用 timeout(命令等待超时) 计时。
          retryAttempts: 3
          # 命令重试发送时间间隔,单位:毫秒
          retryInterval: 1500
          # 重新连接时间间隔,单位:毫秒
          reconnectionTimeout: 3000
          # 执行失败最大次数
          failedAttempts: 3
          # 密码
          password: null
          # 单个连接最大订阅数量
          subscriptionsPerConnection: 5
          # 客户端名称
          clientName: null
          # 节点地址
          address: redis://127.0.0.1:6379
          # 发布和订阅连接的最小空闲连接数
          subscriptionConnectionMinimumIdleSize: 1
          # 发布和订阅连接池大小
          subscriptionConnectionPoolSize: 50
          # 最小空闲连接数
          connectionMinimumIdleSize: 32
          # 连接池大小
          connectionPoolSize: 64
          # 数据库编号
          database: 0
          # DNS监测时间间隔,单位:毫秒
          dnsMonitoringInterval: 5000
        # 线程池数量,默认值: 当前处理核数量 * 2
        threads: 0
        # Netty线程池数量,默认值: 当前处理核数量 * 2
        nettyThreads: 0
        # 编码
        #codec: !<org.redisson.codec.JsonJacksonCodec> {}
        # 传输模式
        transportMode: "NIO"

Redis集群配置:

spring:
  redis:
    redisson:
      config:
        clusterServersConfig:
          idleConnectionTimeout: 10000
          connectTimeout: 10000
          timeout: 3000
          retryAttempts: 3
          retryInterval: 1500
          failedSlaveReconnectionInterval: 3000
          failedSlaveCheckInterval: 60000
          password: null
          subscriptionsPerConnection: 5
          clientName: null
          loadBalancer: !<org.redisson.connection.balancer.RoundRobinLoadBalancer> {}
          subscriptionConnectionMinimumIdleSize: 1
          subscriptionConnectionPoolSize: 50
          slaveConnectionMinimumIdleSize: 24
          slaveConnectionPoolSize: 64
          masterConnectionMinimumIdleSize: 24
          masterConnectionPoolSize: 64
          readMode: "SLAVE"
          subscriptionMode: "SLAVE"
          nodeAddresses:
          - "redis://192.168.35.142:7002"
          - "redis://192.168.35.142:7001"
          - "redis://192.168.35.142:7000"
          scanInterval: 1000
          pingConnectionInterval: 0
          keepAlive: false
          tcpNoDelay: false
        threads: 16
        nettyThreads: 32
        #codec: !<org.redisson.codec.FstCodec> {}
        transportMode: "NIO"

多线程测试:

package com.erbadagang.springboot.redisdistributedlock;

import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.boot.test.context.SpringBootTest;

import javax.annotation.Resource;
import java.util.concurrent.CountDownLatch;

@SpringBootTest
@Slf4j
class RedisDistributedLockApplicationTests {


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

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

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

    /**
     * 直接注入RedissonClient就可以直接使用.
     */
    @Resource
    private RedissonClient redissonClient;

    /**
     * 模拟并发测试加锁和不加锁2个方法。
     *
     * @return
     */
    @Test
    public void lock() {
        // 计数器
        final CountDownLatch countDownLatch = new CountDownLatch(1);
        for (int i = 0; i < threadNum; i++) {
            MyRunnable myRunnable = new MyRunnable(countDownLatch);
            Thread myThread = new Thread(myRunnable);
            myThread.start();
        }
        // 释放所有线程
        countDownLatch.countDown();
    }

    /**
     * 加锁测试
     */
    private void testLockCount() {
        String lockKey = "lock-test";
        //创建锁
        RLock helloLock = redissonClient.getLock(lockKey);

        try {
            //加锁
            helloLock.lock();
            lockCount--;
            log.info("lockCount值:" + lockCount);
        } catch (Exception e) {
            log.error(e.getMessage(), e);
        } finally {
            // 释放锁
            helloLock.unlock();
        }
    }

    /**
     * 无锁测试
     */
    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() {
            try {
                // 阻塞当前线程,直到计时器的值为0
                countDownLatch.await();
            } catch (InterruptedException e) {
                log.error(e.getMessage(), e);
            }
            // 无锁操作
            testCount();
            // 加锁操作
            testLockCount();
        }

    }

}

控制台输出:

2020-08-02 23:55:39.832  INFO 4144 --- [     Thread-281] s.r.RedisDistributedLockApplicationTests : count值:3
2020-08-02 23:55:39.832  INFO 4144 --- [     Thread-283] s.r.RedisDistributedLockApplicationTests : count值:2
2020-08-02 23:55:39.832  INFO 4144 --- [     Thread-279] s.r.RedisDistributedLockApplicationTests : count值:5
2020-08-02 23:55:39.832  INFO 4144 --- [     Thread-282] s.r.RedisDistributedLockApplicationTests : count值:2
2020-08-02 23:55:39.832  INFO 4144 --- [     Thread-284] s.r.RedisDistributedLockApplicationTests : count值:2
2020-08-02 23:55:39.832  INFO 4144 --- [     Thread-278] s.r.RedisDistributedLockApplicationTests : count值:5
2020-08-02 23:55:39.832  INFO 4144 --- [     Thread-280] s.r.RedisDistributedLockApplicationTests : count值:4
2020-08-02 23:55:39.832  INFO 4144 --- [     Thread-275] s.r.RedisDistributedLockApplicationTests : count值:2
2020-08-02 23:55:39.832  INFO 4144 --- [     Thread-276] s.r.RedisDistributedLockApplicationTests : count值:2
2020-08-02 23:55:39.832  INFO 4144 --- [     Thread-277] s.r.RedisDistributedLockApplicationTests : count值:2
2020-08-02 23:55:39.848  INFO 4144 --- [     Thread-280] s.r.RedisDistributedLockApplicationTests : lockCount值:9
2020-08-02 23:55:39.848  INFO 4144 --- [     Thread-275] s.r.RedisDistributedLockApplicationTests : lockCount值:8

2020-08-02 23:55:39.883  INFO 4144 --- [     Thread-281] s.r.RedisDistributedLockApplicationTests : lockCount值:7
2020-08-02 23:55:39.885  INFO 4144 --- [extShutdownHook] o.s.s.concurrent.ThreadPoolTaskExecutor  : Shutting down ExecutorService 'applicationTaskExecutor'
2020-08-02 23:55:39.885  INFO 4144 --- [     Thread-279] s.r.RedisDistributedLockApplicationTests : lockCount值:6
2020-08-02 23:55:39.885  INFO 4144 --- [     Thread-277] s.r.RedisDistributedLockApplicationTests : lockCount值:5
2020-08-02 23:55:39.885  INFO 4144 --- [     Thread-284] s.r.RedisDistributedLockApplicationTests : lockCount值:4
2020-08-02 23:55:39.903  INFO 4144 --- [     Thread-282] s.r.RedisDistributedLockApplicationTests : lockCount值:3

根据打印结果可以明显看到,未加锁的count--后值是乱序的,而加锁后的结果和我们预期的一样。由于条件问题没办法测试分布式的并发。只能模拟单服务的这种并发,但是原理是一样。

底线


本文源代码使用 Apache License 2.0开源许可协议,这里是本文源码Gitee地址,可通过命令git clone+地址下载代码到本地,也可直接点击链接通过浏览器方式查看源代码。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,402评论 6 499
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,377评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,483评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,165评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,176评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,146评论 1 297
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,032评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,896评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,311评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,536评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,696评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,413评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,008评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,659评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,815评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,698评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,592评论 2 353