在之前的项目中分布式锁和限流是基于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如下: