• 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() + " 	 运行");
                        RLock lock1 = redissonClient.getLock(KEY_LOCKED);
                        try {
                            // 尝试获取锁60s
                            boolean b = lock1.tryLock(7, TimeUnit.SECONDS);
                            if (!b) {
                                log.info(Thread.currentThread().getName() + " 	 获取锁失败");
                                return;
                            }
                        } catch (InterruptedException e) {
                        }
    
                        log.info(Thread.currentThread().getName() + " 	 获取锁");
    
                        try {
                            // 模拟处理逻辑用时50s
                            Thread.sleep(5 * 1000);
                        } catch (InterruptedException e) {
    
                        }
    
                        lock1.unlock();
                        log.info(Thread.currentThread().getName() + " 	 释放锁");
                    }
                }).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() + " 	 运行");
                        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() + " 	 获取锁失败");
                                return;
                            }
                        } catch (InterruptedException e) {
                        }
    
                        log.info(Thread.currentThread().getName() + " 	 获取锁");
    
                        try {
                            // 模拟处理逻辑用时
                            Thread.sleep((index * 2) * 1000);
                        } catch (InterruptedException e) {
    
                        }
    
                        // 如果是当前线程持有锁,手动释放
                        if (lock1.isHeldByCurrentThread()) {
                            lock1.unlock();
                            log.info(Thread.currentThread().getName() + " 	 释放锁");
                        }
                    }
                }).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() + " 	 运行");
                        // 下面方式获取到的是非公平锁
                        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() + " 	 获取锁");
                        try {
                            Thread.sleep(TIME_LOCKED);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
    
                        log.info(Thread.currentThread().getName() + " 	 释放锁");
                        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() + " 	 运行");
                        RReadWriteLock readWriteLock = redissonClient.getReadWriteLock(KEY_LOCKED);
    
                        readWriteLock.readLock().lock();
                        log.info(Thread.currentThread().getName() + " 	 获取读锁");
                        try {
                            // 模拟处理逻辑用时5s
                            Thread.sleep(5 * 1000);
                        } catch (InterruptedException e) {
                        }
                        readWriteLock.readLock().unlock();
                        log.info(Thread.currentThread().getName() + " 	 释放读锁");
    
                        readWriteLock.writeLock().lock();
                        log.info(Thread.currentThread().getName() + " 	 获取写锁");
                        try {
                            // 模拟处理逻辑用时5s
                            Thread.sleep(5 * 1000);
                        } catch (InterruptedException e) {
                        }
                        readWriteLock.writeLock().unlock();
                        log.info(Thread.currentThread().getName() + " 	 释放写锁");
                    }
                }).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如下:

    【当你用心写完每一篇博客之后,你会发现它比你用代码实现功能更有成就感!】
  • 相关阅读:
    【转】Django部署时为什么要用 uWSGI与 Nginx? 以及 WSGI,uwsgi等协议
    django 的 uwsgi+Nginx 部署配置
    【转】详解Django DRF框架中APIView、GenericAPIView、ViewSet区别
    python导入包 相对路径踩坑
    【转】Jmeter逻辑控制器-事务控制器的使用
    my live / PC keyboard / Thinkpad Mluti Connect Bluttooth Keyboard with Trackpoint / KT-1525 / KU-1255 / 4x30k12182
    my live / PC GPU NVIDIA Quadro P1000 / Intel UHD Graphics 630 / Dell P2418HT / chumoping
    project architecture evolution
    OS + Linux DevOps
    OS + Linux MP3
  • 原文地址:https://www.cnblogs.com/qlqwjy/p/14354385.html
Copyright © 2020-2023  润新知