Redisson的簡單使用


  在之前的項目中分布式鎖和限流是基於redis進行的,分布式鎖基於setnx和expire命令實現,也可以基於lua腳本實現。限流是采用固定時間窗算法進行的。

  最近了解到redisson這個工具類,而且基於其分布式鎖的實現是比較常見的,簡單研究下其使用。

官網:wiki地址    https://github.com/redisson/redisson/wiki/%E7%9B%AE%E5%BD%95

1. 概述

  官網解釋如下:Redisson是一個在Redis的基礎上實現的Java駐內存數據網格(In-Memory Data Grid)。它不僅提供了一系列的分布式的Java常用對象,還提供了許多分布式服務。其中包括(BitSet, Set, Multimap, SortedSet, Map, List, Queue, BlockingQueue, Deque, BlockingDeque, Semaphore, Lock, AtomicLong, CountDownLatch, Publish / Subscribe, Bloom filter, Remote service, Spring cache, Executor service, Live Object service, Scheduler service) Redisson提供了使用Redis的最簡單和最便捷的方法。Redisson的宗旨是促進使用者對Redis的關注分離(Separation of Concern),從而讓使用者能夠將精力更集中地放在處理業務邏輯上。

2.研究其分布式鎖的用法

pom增加:

        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson</artifactId>
            <version>3.15.0</version>
        </dependency>

增加日志文件 logback.xml

<?xml version="1.0" encoding="UTF-8" ?>
<configuration scan="true" scanPeriod="60000">
    <property name="LOG_HOME" value="/export/logs/cmdb/"/>
    <property name="APP_NAME" value="cmdb"/>
    <property name="LOG_FILE_EXPIRE_TIME" value="180"/>

    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} | ${APP_NAME} - %p | %thread | %c | line:%L - %m%n</pattern>
        </encoder>
    </appender>
    <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>${LOG_HOME}${APP_NAME}.%d{yyyy-MM-dd}.log</fileNamePattern>
            <maxHistory>${LOG_FILE_EXPIRE_TIME}</maxHistory>
        </rollingPolicy>
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} | ${APP_NAME} - %p | %thread | %c | line:%L - %m%n</pattern>
        </encoder>
    </appender>

    <root>
        <level value="ERROR"/>
        <appender-ref ref="STDOUT"/>
        <!--<appender-ref ref="FILE"/>-->
    </root>
    <!-- 不同包,設置不同的日志級別 -->
    <logger name="com.xm.ggn" level="INFO"/>
</configuration>

1. 簡單的使用

package com.xm.ggn.test.redisson;

import lombok.extern.slf4j.Slf4j;
import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;

import java.util.concurrent.TimeUnit;

@Slf4j
public class Client {

    private static final Long TIME_LOCKED = 50 * 1000l;
    private static final String KEY_LOCKED = "myLock";
    private static RedissonClient redissonClient = null;

    public static void main(String[] args) {
        initRedissonClient();
        lock();
    }

    private static void initRedissonClient() {
        // 1. Create config object
        Config config = new Config();
        config.useSingleServer().setAddress("redis://127.0.0.1:6379");
        // 2. Create Redisson instance
        Client.redissonClient = Redisson.create(config);
    }

    private static void lock() {
        RLock lock1 = redissonClient.getLock(KEY_LOCKED);
        log.error("lock1 clas: {}", lock1.getClass());
        lock1.lock();
        log.info("lock, ThreadName: {} id: {} locked, 重入次數: {}", Thread.currentThread().getName(), Thread.currentThread().getId(), lock1.getHoldCount());

        // 處理業務邏輯
        try {
            Thread.sleep(TIME_LOCKED);
            reLock();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock1.unlock();
            log.info("lock, ThreadName: {} id: {} unlock, 重入次數: {}", Thread.currentThread().getName(), Thread.currentThread().getId(), lock1.getHoldCount());
        }
    }

    /**
     * 測試鎖的重入
     */
    private static void reLock() {
        RLock lock1 = redissonClient.getLock(KEY_LOCKED);
        lock1.lock();
        log.info("reLock, ThreadName: {} id: {} locked, 重入次數: {}", Thread.currentThread().getName(), Thread.currentThread().getId(), lock1.getHoldCount());

        // 處理業務邏輯
        try {
            Thread.sleep(TIME_LOCKED);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock1.unlock();
            log.info("reLock, ThreadName: {} id: {} unlock, 重入次數: {}", Thread.currentThread().getName(), Thread.currentThread().getId(), lock1.getHoldCount());
        }
    }
}

結果:

2021-02-01 16:20:23.013 | cmdb - ERROR | main | com.xm.ggn.test.redisson.Client | line:35 - lock1 clas: class org.redisson.RedissonLock
2021-02-01 16:20:23.050 | cmdb - INFO | main | com.xm.ggn.test.redisson.Client | line:37 - lock, ThreadName: main id: 1 locked, 重入次數: 1
2021-02-01 16:21:13.056 | cmdb - INFO | main | com.xm.ggn.test.redisson.Client | line:57 - reLock, ThreadName: main id: 1 locked, 重入次數: 2
2021-02-01 16:22:03.059 | cmdb - INFO | main | com.xm.ggn.test.redisson.Client | line:66 - reLock, ThreadName: main id: 1 unlock, 重入次數: 1
2021-02-01 16:22:03.061 | cmdb - INFO | main | com.xm.ggn.test.redisson.Client | line:47 - lock, ThreadName: main id: 1 unlock, 重入次數: 0

注意:

(1)redis中是通過HASH來存儲鎖的,key是UUID+":"+ThreadId;value 是重入的層數,例如上面過程中查看redis數據如下:

127.0.0.1:6379> hgetall myLock
1) "c20d8714-89c6-485f-ad4f-8dbb54271ebf:1"
2) "1"
127.0.0.1:6379> hgetall myLock
1) "c20d8714-89c6-485f-ad4f-8dbb54271ebf:1"
2) "2"
127.0.0.1:6379> hgetall myLock
(empty list or set)

(2)也可以使用lock(long var1, TimeUnit var3); 方法自動釋放鎖

    private static void lock3() {
        RLock lock1 = redissonClient.getLock(KEY_LOCKED);
        log.error("lock1 clas: {}", lock1.getClass());
        // 500s 后自動釋放鎖
        lock1.lock(500, TimeUnit.SECONDS);
        try {
            Thread.sleep(TIME_LOCKED);
        } catch (InterruptedException ignore) {
            // ignore
        }
    }

測試查看日志如下:

127.0.0.1:6379> ttl myLock
(integer) 493
127.0.0.1:6379> hgetall myLock
1) "3cdf7b21-1e36-4f1f-b0ba-f0339286f416:1"
2) "1"

(3)tryLock(long time, TimeUnit unit)  可以嘗試一定時間去獲取鎖,返回Boolean值

    private static void lock2() {
        for (int i = 0; i < 3; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    log.info(Thread.currentThread().getName() + " \t 運行");
                    RLock lock1 = redissonClient.getLock(KEY_LOCKED);
                    try {
                        // 嘗試獲取鎖60s
                        boolean b = lock1.tryLock(7, TimeUnit.SECONDS);
                        if (!b) {
                            log.info(Thread.currentThread().getName() + " \t 獲取鎖失敗");
                            return;
                        }
                    } catch (InterruptedException e) {
                    }

                    log.info(Thread.currentThread().getName() + " \t 獲取鎖");

                    try {
                        // 模擬處理邏輯用時50s
                        Thread.sleep(5 * 1000);
                    } catch (InterruptedException e) {

                    }

                    lock1.unlock();
                    log.info(Thread.currentThread().getName() + " \t 釋放鎖");
                }
            }).start();
        }
    }

結果:

2021-02-01 17:17:04.915 | cmdb - INFO | Thread-3 | com.xm.ggn.test.redisson.Client | line:121 - Thread-3 運行
2021-02-01 17:17:04.915 | cmdb - INFO | Thread-1 | com.xm.ggn.test.redisson.Client | line:121 - Thread-1 運行
2021-02-01 17:17:04.915 | cmdb - INFO | Thread-2 | com.xm.ggn.test.redisson.Client | line:121 - Thread-2 運行
2021-02-01 17:17:04.949 | cmdb - INFO | Thread-1 | com.xm.ggn.test.redisson.Client | line:133 - Thread-1 獲取鎖
2021-02-01 17:17:09.952 | cmdb - INFO | Thread-1 | com.xm.ggn.test.redisson.Client | line:143 - Thread-1 釋放鎖
2021-02-01 17:17:09.954 | cmdb - INFO | Thread-3 | com.xm.ggn.test.redisson.Client | line:133 - Thread-3 獲取鎖
2021-02-01 17:17:11.925 | cmdb - INFO | Thread-2 | com.xm.ggn.test.redisson.Client | line:127 - Thread-2 獲取鎖失敗
2021-02-01 17:17:14.956 | cmdb - INFO | Thread-3 | com.xm.ggn.test.redisson.Client | line:143 - Thread-3 釋放鎖

(4)  tryLock(long var1, long var3, TimeUnit var5) 接收3個參數,第一個指定最長等待時間waitTime,第二個指定最長持有鎖的時間 holdTime, 第三個是單位

    private static void lock2() {
        for (int i = 0; i < 3; i++) {
            final int index = i;
            new Thread(new Runnable() {
                @Override
                public void run() {
                    log.info(Thread.currentThread().getName() + " \t 運行");
                    RLock lock1 = redissonClient.getLock(KEY_LOCKED);
                    try {
                        // 嘗試獲取7s
//                        boolean b = lock1.tryLock(7,  TimeUnit.SECONDS);
                        // 嘗試獲取鎖7s, 最多占有鎖2s,超過后自動釋放,調用unlock可以提前釋放。
                        boolean b = lock1.tryLock(7, 2, TimeUnit.SECONDS);
                        if (!b) {
                            log.info(Thread.currentThread().getName() + " \t 獲取鎖失敗");
                            return;
                        }
                    } catch (InterruptedException e) {
                    }

                    log.info(Thread.currentThread().getName() + " \t 獲取鎖");

                    try {
                        // 模擬處理邏輯用時
                        Thread.sleep((index * 2) * 1000);
                    } catch (InterruptedException e) {

                    }

                    // 如果是當前線程持有鎖,手動釋放
                    if (lock1.isHeldByCurrentThread()) {
                        lock1.unlock();
                        log.info(Thread.currentThread().getName() + " \t 釋放鎖");
                    }
                }
            }).start();
        }
    }

結果:

2021-02-01 20:51:24.938 | cmdb - INFO | Thread-2 | com.xm.ggn.test.redisson.Client | line:162 - Thread-2      運行
2021-02-01 20:51:24.938 | cmdb - INFO | Thread-3 | com.xm.ggn.test.redisson.Client | line:162 - Thread-3      運行
2021-02-01 20:51:24.938 | cmdb - INFO | Thread-1 | com.xm.ggn.test.redisson.Client | line:162 - Thread-1      運行
2021-02-01 20:51:24.981 | cmdb - INFO | Thread-2 | com.xm.ggn.test.redisson.Client | line:176 - Thread-2      獲取鎖
2021-02-01 20:51:26.980 | cmdb - INFO | Thread-3 | com.xm.ggn.test.redisson.Client | line:176 - Thread-3      獲取鎖
2021-02-01 20:51:28.984 | cmdb - INFO | Thread-1 | com.xm.ggn.test.redisson.Client | line:176 - Thread-1      獲取鎖
2021-02-01 20:51:28.988 | cmdb - INFO | Thread-1 | com.xm.ggn.test.redisson.Client | line:188 - Thread-1      釋放鎖

(5)RedissonLock 的ttl也不是永久的,默認是30s。

  在加鎖成功后,會注冊一個定時任務監聽這個鎖,每隔10秒就去查看這個鎖,如果還持有鎖,就對過期時間進行續期。默認過期時間30秒,過10秒檢查一次,一旦加鎖的業務沒有執行完,就會進行一次續期,把鎖的過期時間再次重置成30秒。 如果在執行過程中線程死掉,不會續期。會等ttl到期后自動消失。

(6) 查看RLock的繼承關系如下

 2. 公平鎖的使用

默認使用的是非公平鎖,不過一般情況使用的都是公平鎖,也就是先到先得。

(1)默認非公平鎖

    private static void lock1() {
        for (int i = 0; i < 5; i++) {
            // 休眠一下使線程按照順序啟動
            try {
                Thread.sleep(1 * 100);
            } catch (InterruptedException e) {
            }

            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    log.info(Thread.currentThread().getName() + " \t 運行");
                    // 下面方式獲取到的是非公平鎖
                    RLock lock1 = redissonClient.getLock(KEY_LOCKED);
//                    RLock lock1 = redissonClient.getFairLock(KEY_LOCKED);
                    log.error("lock1 clas: {}", lock1.getClass());
                    lock1.lock();

                    log.info(Thread.currentThread().getName() + " \t 獲取鎖");
                    try {
                        Thread.sleep(TIME_LOCKED);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    log.info(Thread.currentThread().getName() + " \t 釋放鎖");
                    lock1.unlock();
                }
            });
            thread.setName("MyThread: " + i);
            thread.start();
        }
    }

結果:

2021-02-01 17:34:02.226 | cmdb - INFO | MyThread: 0 | com.xm.ggn.test.redisson.Client | line:96 - MyThread: 0 運行
2021-02-01 17:34:02.233 | cmdb - ERROR | MyThread: 0 | com.xm.ggn.test.redisson.Client | line:100 - lock1 clas: class org.redisson.RedissonLock
2021-02-01 17:34:02.325 | cmdb - INFO | MyThread: 1 | com.xm.ggn.test.redisson.Client | line:96 - MyThread: 1 運行
2021-02-01 17:34:02.325 | cmdb - ERROR | MyThread: 1 | com.xm.ggn.test.redisson.Client | line:100 - lock1 clas: class org.redisson.RedissonLock
2021-02-01 17:34:02.426 | cmdb - INFO | MyThread: 2 | com.xm.ggn.test.redisson.Client | line:96 - MyThread: 2 運行
2021-02-01 17:34:02.426 | cmdb - ERROR | MyThread: 2 | com.xm.ggn.test.redisson.Client | line:100 - lock1 clas: class org.redisson.RedissonLock
2021-02-01 17:34:02.526 | cmdb - INFO | MyThread: 3 | com.xm.ggn.test.redisson.Client | line:96 - MyThread: 3 運行
2021-02-01 17:34:02.526 | cmdb - ERROR | MyThread: 3 | com.xm.ggn.test.redisson.Client | line:100 - lock1 clas: class org.redisson.RedissonLock
2021-02-01 17:34:02.627 | cmdb - INFO | MyThread: 4 | com.xm.ggn.test.redisson.Client | line:96 - MyThread: 4 運行
2021-02-01 17:34:02.627 | cmdb - ERROR | MyThread: 4 | com.xm.ggn.test.redisson.Client | line:100 - lock1 clas: class org.redisson.RedissonLock
2021-02-01 17:34:16.038 | cmdb - INFO | MyThread: 3 | com.xm.ggn.test.redisson.Client | line:103 - MyThread: 3 獲取鎖
2021-02-01 17:34:21.038 | cmdb - INFO | MyThread: 3 | com.xm.ggn.test.redisson.Client | line:110 - MyThread: 3 釋放鎖
2021-02-01 17:34:21.043 | cmdb - INFO | MyThread: 1 | com.xm.ggn.test.redisson.Client | line:103 - MyThread: 1 獲取鎖
2021-02-01 17:34:26.044 | cmdb - INFO | MyThread: 1 | com.xm.ggn.test.redisson.Client | line:110 - MyThread: 1 釋放鎖
2021-02-01 17:34:26.047 | cmdb - INFO | MyThread: 0 | com.xm.ggn.test.redisson.Client | line:103 - MyThread: 0 獲取鎖
2021-02-01 17:34:31.047 | cmdb - INFO | MyThread: 0 | com.xm.ggn.test.redisson.Client | line:110 - MyThread: 0 釋放鎖
2021-02-01 17:34:31.050 | cmdb - INFO | MyThread: 4 | com.xm.ggn.test.redisson.Client | line:103 - MyThread: 4 獲取鎖
2021-02-01 17:34:36.050 | cmdb - INFO | MyThread: 4 | com.xm.ggn.test.redisson.Client | line:110 - MyThread: 4 釋放鎖
2021-02-01 17:34:36.054 | cmdb - INFO | MyThread: 2 | com.xm.ggn.test.redisson.Client | line:103 - MyThread: 2 獲取鎖
2021-02-01 17:34:41.055 | cmdb - INFO | MyThread: 2 | com.xm.ggn.test.redisson.Client | line:110 - MyThread: 2 釋放鎖

(2)  公平鎖的使用

主要代碼同上,只是獲取鎖變為公平鎖

RLock lock1 = redissonClient.getFairLock(KEY_LOCKED);

結果:

2021-02-01 17:38:04.689 | cmdb - INFO | MyThread: 0 | com.xm.ggn.test.redisson.Client | line:96 - MyThread: 0      運行
2021-02-01 17:38:04.696 | cmdb - ERROR | MyThread: 0 | com.xm.ggn.test.redisson.Client | line:98 - lock1 clas: class org.redisson.RedissonFairLock
2021-02-01 17:38:04.721 | cmdb - INFO | MyThread: 0 | com.xm.ggn.test.redisson.Client | line:101 - MyThread: 0      獲取鎖
2021-02-01 17:38:04.787 | cmdb - INFO | MyThread: 1 | com.xm.ggn.test.redisson.Client | line:96 - MyThread: 1      運行
2021-02-01 17:38:04.787 | cmdb - ERROR | MyThread: 1 | com.xm.ggn.test.redisson.Client | line:98 - lock1 clas: class org.redisson.RedissonFairLock
2021-02-01 17:38:04.888 | cmdb - INFO | MyThread: 2 | com.xm.ggn.test.redisson.Client | line:96 - MyThread: 2      運行
2021-02-01 17:38:04.888 | cmdb - ERROR | MyThread: 2 | com.xm.ggn.test.redisson.Client | line:98 - lock1 clas: class org.redisson.RedissonFairLock
2021-02-01 17:38:04.989 | cmdb - INFO | MyThread: 3 | com.xm.ggn.test.redisson.Client | line:96 - MyThread: 3      運行
2021-02-01 17:38:04.989 | cmdb - ERROR | MyThread: 3 | com.xm.ggn.test.redisson.Client | line:98 - lock1 clas: class org.redisson.RedissonFairLock
2021-02-01 17:38:05.089 | cmdb - INFO | MyThread: 4 | com.xm.ggn.test.redisson.Client | line:96 - MyThread: 4      運行
2021-02-01 17:38:05.089 | cmdb - ERROR | MyThread: 4 | com.xm.ggn.test.redisson.Client | line:98 - lock1 clas: class org.redisson.RedissonFairLock
2021-02-01 17:38:09.723 | cmdb - INFO | MyThread: 0 | com.xm.ggn.test.redisson.Client | line:108 - MyThread: 0      釋放鎖
2021-02-01 17:38:09.729 | cmdb - INFO | MyThread: 1 | com.xm.ggn.test.redisson.Client | line:101 - MyThread: 1      獲取鎖
2021-02-01 17:38:14.729 | cmdb - INFO | MyThread: 1 | com.xm.ggn.test.redisson.Client | line:108 - MyThread: 1      釋放鎖
2021-02-01 17:38:14.732 | cmdb - INFO | MyThread: 2 | com.xm.ggn.test.redisson.Client | line:101 - MyThread: 2      獲取鎖
2021-02-01 17:38:19.732 | cmdb - INFO | MyThread: 2 | com.xm.ggn.test.redisson.Client | line:108 - MyThread: 2      釋放鎖
2021-02-01 17:38:19.734 | cmdb - INFO | MyThread: 3 | com.xm.ggn.test.redisson.Client | line:101 - MyThread: 3      獲取鎖
2021-02-01 17:38:24.734 | cmdb - INFO | MyThread: 3 | com.xm.ggn.test.redisson.Client | line:108 - MyThread: 3      釋放鎖
2021-02-01 17:38:24.737 | cmdb - INFO | MyThread: 4 | com.xm.ggn.test.redisson.Client | line:101 - MyThread: 4      獲取鎖
2021-02-01 17:38:29.738 | cmdb - INFO | MyThread: 4 | com.xm.ggn.test.redisson.Client | line:108 - MyThread: 4      釋放鎖

3. 讀寫鎖的使用 

  類似於JDK的讀寫鎖,讀鎖共享,寫鎖排斥。

    private static void lock4() {
        for (int i = 0; i < 3; i++) {
            try {
                Thread.sleep(1 * 1000);
            } catch (InterruptedException e) {
            }

            new Thread(new Runnable() {
                @Override
                public void run() {
                    log.info(Thread.currentThread().getName() + " \t 運行");
                    RReadWriteLock readWriteLock = redissonClient.getReadWriteLock(KEY_LOCKED);

                    readWriteLock.readLock().lock();
                    log.info(Thread.currentThread().getName() + " \t 獲取讀鎖");
                    try {
                        // 模擬處理邏輯用時5s
                        Thread.sleep(5 * 1000);
                    } catch (InterruptedException e) {
                    }
                    readWriteLock.readLock().unlock();
                    log.info(Thread.currentThread().getName() + " \t 釋放讀鎖");

                    readWriteLock.writeLock().lock();
                    log.info(Thread.currentThread().getName() + " \t 獲取寫鎖");
                    try {
                        // 模擬處理邏輯用時5s
                        Thread.sleep(5 * 1000);
                    } catch (InterruptedException e) {
                    }
                    readWriteLock.writeLock().unlock();
                    log.info(Thread.currentThread().getName() + " \t 釋放寫鎖");
                }
            }).start();
        }
    }

結果:

2021-02-01 20:17:34.375 | cmdb - INFO | Thread-1 | com.xm.ggn.test.redisson.Client | line:37 - Thread-1      運行
2021-02-01 20:17:34.399 | cmdb - INFO | Thread-1 | com.xm.ggn.test.redisson.Client | line:41 - Thread-1      獲取讀鎖
2021-02-01 20:17:35.374 | cmdb - INFO | Thread-2 | com.xm.ggn.test.redisson.Client | line:37 - Thread-2      運行
2021-02-01 20:17:35.376 | cmdb - INFO | Thread-2 | com.xm.ggn.test.redisson.Client | line:41 - Thread-2      獲取讀鎖
2021-02-01 20:17:36.374 | cmdb - INFO | Thread-3 | com.xm.ggn.test.redisson.Client | line:37 - Thread-3      運行
2021-02-01 20:17:36.376 | cmdb - INFO | Thread-3 | com.xm.ggn.test.redisson.Client | line:41 - Thread-3      獲取讀鎖
2021-02-01 20:17:39.404 | cmdb - INFO | Thread-1 | com.xm.ggn.test.redisson.Client | line:48 - Thread-1      釋放讀鎖
2021-02-01 20:17:40.378 | cmdb - INFO | Thread-2 | com.xm.ggn.test.redisson.Client | line:48 - Thread-2      釋放讀鎖
2021-02-01 20:17:41.378 | cmdb - INFO | Thread-3 | com.xm.ggn.test.redisson.Client | line:48 - Thread-3      釋放讀鎖
2021-02-01 20:17:41.379 | cmdb - INFO | Thread-3 | com.xm.ggn.test.redisson.Client | line:51 - Thread-3      獲取寫鎖
2021-02-01 20:17:46.381 | cmdb - INFO | Thread-3 | com.xm.ggn.test.redisson.Client | line:58 - Thread-3      釋放寫鎖
2021-02-01 20:17:46.383 | cmdb - INFO | Thread-2 | com.xm.ggn.test.redisson.Client | line:51 - Thread-2      獲取寫鎖
2021-02-01 20:17:51.385 | cmdb - INFO | Thread-2 | com.xm.ggn.test.redisson.Client | line:58 - Thread-2      釋放寫鎖
2021-02-01 20:17:51.389 | cmdb - INFO | Thread-1 | com.xm.ggn.test.redisson.Client | line:51 - Thread-1      獲取寫鎖
2021-02-01 20:17:56.390 | cmdb - INFO | Thread-1 | com.xm.ggn.test.redisson.Client | line:58 - Thread-1      釋放寫鎖

 

  至此簡單研究下redisson分布式鎖使用。一般是基於AOP自定義注解實現分布式鎖。

3. 基於AOP封裝分布式鎖

1.配置類

package com.xm.ggn.test.redisson;

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: qlq
 * @Description
 * @Date: 21:49 2021/2/1
 */
@Configuration
public class RedissonConfiguration {

    @Bean
    public RedissonClient redissonClient() {
        // 1. Create config object
        Config config = new Config();
        config.useSingleServer().setAddress("redis://127.0.0.1:6379");
        // 2. Create Redisson instance
        return Redisson.create(config);
    }
}

2. 分布式鎖注解

package com.xm.ggn.test.redisson.anno;

import java.lang.annotation.*;

/**
 * @author: 喬利強
 * @date: 2021/2/1 20:43
 * @description:
 */
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface DistributedLock {

    /**
     * 分布式鎖的key,可以理解為前綴
     */
    String value();

    /**
     * 分布式鎖的最長等待時間
     */
    long waitTime() default 60;

    /**
     * 最長持有時間
     */
    long holdTime() default 60;
}

3.分布式鎖參數注解,用於打在參數上,針對不同的參數生成不同的鎖 (這種相當於是使鎖更加細粒度)

package com.xm.ggn.test.redisson.anno;

import java.lang.annotation.*;

/**
 * key 注解,用於特殊標記分布式鎖的key,用於對不同的參數生成不同的鎖
 */
@Target({ElementType.PARAMETER, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface DistributedLockKey {
}

4.  LockAspect AOP處理

package com.xm.ggn.test.redisson;

import com.xm.ggn.test.redisson.anno.DistributedLock;
import com.xm.ggn.test.redisson.anno.DistributedLockKey;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.sql.SQLException;
import java.util.concurrent.TimeUnit;

/**
 * @author: 喬利強
 * @date: 2021/2/1 20:57
 * @description:
 */
@Component
@Aspect
@Slf4j
public class LockAspect {

    @Autowired
    private RedissonClient redissonClient;

    @Around("@annotation(com.xm.ggn.test.redisson.anno.DistributedLock)")
    public Object aroundAdvice(ProceedingJoinPoint pjp) throws Throwable {
        // 1.方法執行前的處理,相當於前置通知
        // 獲取方法簽名
        MethodSignature methodSignature = (MethodSignature) pjp.getSignature();
        // 獲取方法
        Method method = methodSignature.getMethod();
        // 獲取方法上面的注解
        DistributedLock distributedLock = method.getAnnotation(DistributedLock.class);
        // 獲取操作描述的屬性值
        String lockKey = distributedLock.value();
        long waitTime = distributedLock.waitTime();
        long holdTime = distributedLock.holdTime();

        // method獲取參數信息。 如果參數帶有DistributedLockKey 注解,lockKey 拼接參數的value
        Parameter[] parameters = method.getParameters();
        if (ArrayUtils.isNotEmpty(parameters)) {
            for (int index = 0, length_1 = parameters.length; index < length_1; index++) {
                DistributedLockKey annotation = parameters[index].getAnnotation(DistributedLockKey.class);
                if (annotation != null) {
                    // 獲取參數值
                    Object[] args = pjp.getArgs();
                    String param = String.valueOf(args[index]);
                    if (StringUtils.isNotBlank(param)) {
                        lockKey += ":" + param;
                        break;
                    }
                }
            }
        }

        log.info("lockKey: {}", lockKey);
        RLock fairLock = redissonClient.getFairLock(lockKey);
        boolean lock = fairLock.tryLock(waitTime, holdTime, TimeUnit.SECONDS);
        if (!lock) {
            throw new RuntimeException("獲取鎖失敗");
        }

        Object result = null;
        try {
            //讓代理方法執行
            result = pjp.proceed();
            // 2.相當於后置通知(方法成功執行之后走這里)
        } catch (SQLException e) {
            // 3.相當於異常通知部分
        } finally {
            // 4.相當於最終通知
            if (fairLock.isHeldByCurrentThread()) {
                fairLock.unlock();
            }
        }
        return result;
    }
}

5.測試Controller:

package com.xm.ggn.test.redisson;

import com.xm.ggn.test.redisson.anno.DistributedLock;
import com.xm.ggn.test.redisson.anno.DistributedLockKey;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @Author: qlq
 * @Description
 * @Date: 21:53 2021/2/1
 */
@RestController("redissonTestController")
@RequestMapping("/redisson")
public class TestController {

    @GetMapping("test1")
    @DistributedLock("redissonTest:test1")
    public void test1(@DistributedLockKey String value1) {
        try {
            Thread.sleep(50 * 1000);
        } catch (InterruptedException e) {
        }
    }

    @GetMapping("test2")
    @DistributedLock("redissonTest:test2")
    public void test2(String value1) {
        try {
            Thread.sleep(50 * 1000);
        } catch (InterruptedException e) {
        }
    }
}

6.訪問后查看redis

127.0.0.1:6379> keys *
1) "redissonTest:test2"
2) "redissonTest:test1:123456"
127.0.0.1:6379> hgetall "redissonTest:test2"
1) "f43c6823-021c-468b-8384-86fb2384775e:403"
2) "1"
127.0.0.1:6379> hgetall "redissonTest:test1:123456"
1) "f43c6823-021c-468b-8384-86fb2384775e:402"
2) "1"

 

補充:redisson實際也是基於lua腳本執行的。Redis 腳本使用 Lua 解釋器來執行腳本。 Redis 2.6 版本通過內嵌支持 Lua 環境。執行腳本的常用命令為 EVAL。Lua 是一種輕量小巧的腳本語言,用標准C語言編寫並以源代碼形式開放, 其設計目的是為了嵌入應用程序中,從而為應用程序提供靈活的擴展和定制功能。

(1)org.redisson.RedissonLock#tryLock() 源碼如下:

 public boolean tryLock() {
        return ((Boolean)this.get(this.tryLockAsync())).booleanValue();
    }

(2)org.redisson.RedissonLock#tryLockAsync()

    public RFuture<Boolean> tryLockAsync() {
        return this.tryLockAsync(Thread.currentThread().getId());
    }

(3)org.redisson.RedissonLock#tryLockAsync(long)

    public RFuture<Boolean> tryLockAsync(long threadId) {
        return this.tryAcquireOnceAsync(-1L, -1L, (TimeUnit)null, threadId);
    }

(4)org.redisson.RedissonLock#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.internalLockLeaseTime, TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_NULL_BOOLEAN);
            ttlRemainingFuture.onComplete((ttlRemaining, e) -> {
                if (e == null) {
                    if (ttlRemaining.booleanValue()) {
                        this.scheduleExpirationRenewal(threadId);
                    }

                }
            });
            return ttlRemainingFuture;
        }
    }

(5)org.redisson.RedissonLock#tryLockInnerAsync  看出來也是執行了個lua腳本

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

3. 封裝為獨立的lock組件,用於其他多個項目引入

 1. 新建項目cloud-common-lock

2. 修改pom

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>cloud</artifactId>
        <groupId>cn.qz.cloud</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>cloud-common-lock</artifactId>

    <properties>
        <redisson.version>3.10.1</redisson.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <!-- common -->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-collections4</artifactId>
        </dependency>
        <dependency>
            <groupId>commons-io</groupId>
            <artifactId>commons-io</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
        </dependency>
        <!-- 引入 spring aop 依賴 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
        <dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson-spring-boot-starter</artifactId>
            <version>${redisson.version}</version>
        </dependency>
    </dependencies>

</project>

3. 增加注解類

 DistributedLock 用於AOP攔截

package cn.qz.lock.anno;

import java.lang.annotation.*;

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface DistributedLock {

    /**
     * 分布式鎖的key,可以理解為前綴
     */
    String value();

    /**
     * 分布式鎖的最長等待時間
     */
    long waitTime() default 60;

    /**
     * 最長持有時間
     */
    long holdTime() default 60;
}

 DistributedLockKey 用於標記在參數上,為不同的參數生成不同的redis的鎖的key

package cn.qz.lock.anno;

import java.lang.annotation.*;

/**
 * key 注解,用於特殊標記分布式鎖的key,用於對不同的參數生成不同的鎖
 */
@Target({ElementType.PARAMETER, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface DistributedLockKey {
}

EnableDistributedLock 用於引入分布式鎖的相關配置,開啟分布式鎖,使AOP生效

package cn.qz.lock.anno;

import cn.qz.lock.config.LockAutoConfiguration;
import org.springframework.context.annotation.Import;

import java.lang.annotation.*;

/**
 * 此注解的作用是開啟分布式鎖開關, 加此注解實際為了將bean注入到Spring。如果包名可以被掃描到不需要打此注解也可以
 *
 * @Import 引入cn.qz.lock.config.LockAutoConfiguration,使其自動配置
 */
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
// @Import用來導入@Configuration注解的配置類、聲明@Bean注解的bean方法、導入ImportSelector的實現類或導入ImportBeanDefinitionRegistrar的實現類。
@Import({LockAutoConfiguration.class})
public @interface EnableDistributedLock {
}

4. AOP處理類

package cn.qz.lock.aspect;

import cn.qz.lock.anno.DistributedLock;
import cn.qz.lock.anno.DistributedLockKey;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.sql.SQLException;
import java.util.concurrent.TimeUnit;

/**
 * @author: 喬利強
 * @date: 2021/2/2 11:03
 * @description:
 */
@Component
@Aspect
@Slf4j
public class LockAspect {

    @Autowired
    private RedissonClient redissonClient;

    @Around("@annotation(cn.qz.lock.anno.DistributedLock)")
    public Object aroundAdvice(ProceedingJoinPoint pjp) throws Throwable {
        // 1.方法執行前的處理,相當於前置通知
        // 獲取方法簽名
        MethodSignature methodSignature = (MethodSignature) pjp.getSignature();
        // 獲取方法
        Method method = methodSignature.getMethod();
        // 獲取方法上面的注解
        DistributedLock distributedLock = method.getAnnotation(DistributedLock.class);
        // 獲取操作描述的屬性值
        String lockKey = distributedLock.value();
        long waitTime = distributedLock.waitTime();
        long holdTime = distributedLock.holdTime();

        // method獲取參數信息。 如果參數帶有DistributedLockKey 注解,lockKey 拼接參數的value
        Parameter[] parameters = method.getParameters();
        if (ArrayUtils.isNotEmpty(parameters)) {
            for (int index = 0, length_1 = parameters.length; index < length_1; index++) {
                DistributedLockKey annotation = parameters[index].getAnnotation(DistributedLockKey.class);
                if (annotation != null) {
                    // 獲取參數值
                    Object[] args = pjp.getArgs();
                    String param = String.valueOf(args[index]);
                    if (StringUtils.isNotBlank(param)) {
                        lockKey += ":" + param;
                        break;
                    }
                }
            }
        }

        log.info("lockKey: {}", lockKey);
        RLock fairLock = redissonClient.getFairLock(lockKey);
        boolean lock = fairLock.tryLock(waitTime, holdTime, TimeUnit.SECONDS);
        if (!lock) {
            throw new RuntimeException("獲取鎖失敗");
        }

        Object result = null;
        try {
            //讓代理方法執行
            result = pjp.proceed();
            // 2.相當於后置通知(方法成功執行之后走這里)
        } catch (SQLException e) {
            // 3.相當於異常通知部分
        } finally {
            // 4.相當於最終通知
            if (fairLock.isHeldByCurrentThread()) {
                fairLock.unlock();
            }
        }
        return result;
    }
}

5. 自動配置類

package cn.qz.lock.config;

import cn.qz.lock.aspect.LockAspect;
import org.redisson.spring.starter.RedissonAutoConfiguration;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.context.annotation.Import;

/**
 * Lock 自動配置類。 該類的Configuration 注解沒有被掃描到是因為和Boot的主啟動類不在同一個子包下,所以掃描不到。通過cn.qz.lock.anno.EnableDistributedLock中的@Import可以引入
 * 該類的主要作用是引入LockAspect,注冊到Spring中,使其AOP生效;也可以做一些其他的自動配置。
 */
@Configuration
@AutoConfigureAfter({RedissonAutoConfiguration.class})
@EnableAspectJAutoProxy(
        exposeProxy = true
)
// @Import用來導入@Configuration注解的配置類、聲明@Bean注解的bean方法、導入ImportSelector的實現類或導入ImportBeanDefinitionRegistrar的實現類。
@Import({RedissonAutoConfiguration.class, LockAspect.class})
public class LockAutoConfiguration {
    public LockAutoConfiguration() {
    }
}

6. 測試

(1) 其他項目引入依賴

        <!--引入分布式鎖-->
        <dependency>
            <groupId>cn.qz.cloud</groupId>
            <artifactId>cloud-common-lock</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>

(2) 主啟動類開啟分布式鎖

package cn.qz.cloud;

import cn.qz.lock.anno.EnableDistributedLock;
import com.netflix.hystrix.contrib.metrics.eventstream.HystrixMetricsStreamServlet;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.netflix.hystrix.EnableHystrix;
import org.springframework.context.annotation.Bean;

/**
 * @Author: qlq
 * @Description
 * @Date: 22:08 2020/10/17
 */
@SpringBootApplication
@EnableEurekaClient
@EnableCircuitBreaker
@EnableHystrix
// 開啟分布式鎖注解(此注解實際為了將bean注入到Spring,如果包名可以被掃描到不需要打此注解也可以)
@EnableDistributedLock
public class PaymentHystrixMain8081 {

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

    /**
     * 此配置是為了服務監控而配置,與服務容錯本身無關,springcloud升級后的坑
     * ServletRegistrationBean因為SpringBoot的默認路徑不是 “/hystrix.stream"
     * 只要在自己的項目里配置上下的servlet就可以了
     */
    @Bean
    public ServletRegistrationBean getServlet() {
        HystrixMetricsStreamServlet streamServlet = new HystrixMetricsStreamServlet();
        ServletRegistrationBean registrationBean = new ServletRegistrationBean(streamServlet);
        registrationBean.setLoadOnStartup(1);
        registrationBean.addUrlMappings("/hystrix.stream");
        registrationBean.setName("HystrixMetricsStreamServlet");
        return registrationBean;
    }
}

(3) 建立 Controller進行測試

    /********S 測試分布式鎖*****/
    @GetMapping("test1")
    @DistributedLock("redissonTest:test1")
    public void test1(@DistributedLockKey String value1) {
        try {
            Thread.sleep(50 * 1000);
        } catch (InterruptedException e) {
        }
    }

    @GetMapping("test2")
    @DistributedLock("redissonTest:test2")
    public void test2(String value1) {
        try {
            Thread.sleep(50 * 1000);
        } catch (InterruptedException e) {
        }
    }
    /********E 測試分布式鎖*****/

(4) 訪問Controller后,查看redis生成的數據即可驗證分布式鎖是否生效

 

  需要注意AOP的自調用問題。 

 

補充:關於鎖的刪除問題 

  刪除的時候肯定不是任何一個線程都可以刪除,只有加鎖的線程可以刪除,這也就明白了上面用hash存儲並且key設計為UUID:ThreadId的原因,是為了在刪除的時候便於判斷刪除的線程是否是加鎖時候的線程。

 比如:key里面包含了當前lock的id信息和加鎖時候的線程ID,value 是重入的次數。如下是lock的信息

 查看其刪除方法:org.redisson.RedissonFairLock#unlockInnerAsync

    protected RFuture<Boolean> unlockInnerAsync(long threadId) {
        return this.commandExecutor.evalWriteAsync(this.getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN, "while true do local firstThreadId2 = redis.call('lindex', KEYS[2], 0);if firstThreadId2 == false then break;end; 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;if (redis.call('exists', KEYS[1]) == 0) then local nextThreadId = redis.call('lindex', KEYS[2], 0); if nextThreadId ~= false then redis.call('publish', KEYS[4] .. ':' .. nextThreadId, ARGV[1]); end; return 1; end;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; end; redis.call('del', KEYS[1]); local nextThreadId = redis.call('lindex', KEYS[2], 0); if nextThreadId ~= false then redis.call('publish', KEYS[4] .. ':' .. nextThreadId, ARGV[1]); end; return 1; ", Arrays.asList(this.getName(), this.threadsQueueName, this.timeoutSetName, this.getChannelName()), new Object[]{LockPubSub.unlockMessage, this.internalLockLeaseTime, this.getLockName(threadId), System.currentTimeMillis()});
    }

單獨拿出來其腳本如下:

while true do local firstThreadId2 = redis.call('lindex', KEYS[2], 0);if firstThreadId2 == false then break;end; 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;if (redis.call('exists', KEYS[1]) == 0) then local nextThreadId = redis.call('lindex', KEYS[2], 0); if nextThreadId ~= false then redis.call('publish', KEYS[4] .. ':' .. nextThreadId, ARGV[1]); end; return 1; end;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; end; redis.call('del', KEYS[1]); local nextThreadId = redis.call('lindex', KEYS[2], 0); if nextThreadId ~= false then redis.call('publish', KEYS[4] .. ':' .. nextThreadId, ARGV[1]); end; return 1; 

keys如下:

 params如下:

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM