Redisson入门——学习笔记

发布于:2025-05-14 ⋅ 阅读:(20) ⋅ 点赞:(0)

第一章 简单入门

官网:https://redisson.org

 一 引入依赖

<!--redisson-->
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
</dependency>

二 配置:

 @Configuration
 public class RedisConfig {
    @Bean
    public RedissonClient redissonClient() {
        // 配置类
        Config config = new Config();
        // 添加redis地址,这里添加了单点的地址,也可以使用config.useClusterServers()添加集群地址 
        config.useSingleServer()
            .setAddress("redis://192.168.150.101:6379")
            .setPassowrd("123321");
        // 创建客户端
        return Redisson.create(config);
    }
 }

三 使用:

@Autowired
 private RedissonClient redissonClient;

 @Test
 void testRedisson() throws InterruptedException {
    // 1.获取锁对象,指定锁名称
    RLock lock = redissonClient.getLock("anyLock");
    // 2.尝试获取锁,参数:waitTime、leaseTime、时间单位
    boolean isLock = lock.tryLock(1, 10, TimeUnit.SECONDS);
    if (!isLock) {
            // 获取锁失败处理 ..
    }
    try {
            // 获取锁成功处理
    } finally {
        // 4.释放锁
        lock.unlock();
    }
 }

第二章 基于自定义注解的锁

上锁的过程十分麻烦,我们希望能够把上锁的过程提取出来,方便使用,下面是上锁流程:

我们发现中间的业务不一样,前后两端基本一致,所以可以使用AOP

一 定义注解

我们把锁需要的属性全部放到注解里,除了锁名字之外都设置一下默认值,这样用户要使用的时候就可以通过注解动态传递参数

package com.tianji.promotion.utils;

import com.tianji.promotion.domain.enums.LockStrategy;
import com.tianji.promotion.domain.enums.RedissonLockType;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.concurrent.TimeUnit;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyLock {

    // 锁名称
    String name();

    // 获取锁的等待时间。当获取锁失败后可以多次重试,直到waitTime时间耗尽。waitTime默认-1,即失败后立刻返回,不重试。
    long waitTime() default 1;

    // 锁超时释放时间。默认是30,同时会利用WatchDog来不断更新超时时间。需要注意的是,如果手动设置leaseTime值,会导致WatchDog失效
    long leaveTime() default -1;

    // 时间单位
    TimeUnit unit() default TimeUnit.SECONDS;

}

二 定义AOP方法

package com.tianji.promotion.utils;

import lombok.RequiredArgsConstructor;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.stereotype.Component;

@Component
@Aspect
@RequiredArgsConstructor
public class MyLockAspect implements Ordered{

    private final RedissonClient redissonClient;

    @Around("@annotation(myLock)")
    public Object tryLock(ProceedingJoinPoint pjp, MyLock myLock) throws Throwable {
        // 1.创建锁对象
        RLock lock = redissonClient.getLock(myLock.name());
        // 2.尝试获取锁
        boolean isLock = lock.tryLock(myLock.waitTime(), myLock.leaseTime(), myLock.unit());
        // 3.判断是否成功
        if(!isLock) {
            // 3.1.失败,快速结束
            throw new BizIllegalException("请求太频繁");
        }
        try {
            // 3.2.成功,执行业务
            return pjp.proceed();
        } finally {
            // 4.释放锁
            lock.unlock();
        }
    }
    
    // 保证AOP方法执行的顺序在事务之前
    @Override
    public int getOrder() {
        return 0;
    }
}

三 使用

不过呢,现在还存在几个问题:

  • Redisson中锁的种类有很多,目前的代码中把锁的类型写死了

  • Redisson中获取锁的逻辑有多种,比如获取锁失败的重试策略,目前都没有设置

  • 锁的名称目前是写死的,并不能根据方法参数动态变化

在下面的章节中我们将一一解决

第三章 实现锁的多样

一 增加锁类型的枚举类

这里只枚举了四种

package com.tianji.promotion.domain.enums;

public enum RedissonLockType {

    Default_Lock,
    Fair_Lock,
    Read_Lock,
    Write_Lock,

}

二 修改注解

由于我们要实现锁类型的多样,就必须由用户来指定所类型,因此需要把锁类型作为注解的一个参数让用户去填

package com.tianji.promotion.utils;

import com.tianji.promotion.domain.enums.LockStrategy;
import com.tianji.promotion.domain.enums.RedissonLockType;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.concurrent.TimeUnit;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyLock {

    // 锁名称
    String name();

    // 获取锁的等待时间。当获取锁失败后可以多次重试,直到waitTime时间耗尽。waitTime默认-1,即失败后立刻返回,不重试。
    long waitTime() default 1;

    // 锁超时释放时间。默认是30,同时会利用WatchDog来不断更新超时时间。需要注意的是,如果手动设置leaseTime值,会导致WatchDog失效
    long leaveTime() default -1;

    // 时间单位
    TimeUnit unit() default TimeUnit.SECONDS;

    // 锁类型
    RedissonLockType type() default RedissonLockType.Default_Lock;

   

}

三 修改AOP

我们可以通过switch来枚举,但是这不太优雅,也不太方便扩展,这时候我们可以采用工厂方法,让工厂动态为我们创建锁对象

1. 工厂方法类

我们使用Map来存储,键是枚举对象,值是具体的方法,使用构造函数在对象初始化时把键值对放进去

package com.tianji.promotion.utils;

import com.tianji.promotion.domain.enums.RedissonLockType;
import lombok.RequiredArgsConstructor;
import org.checkerframework.checker.units.qual.K;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.stereotype.Component;

import java.util.EnumMap;
import java.util.Map;
import java.util.function.Function;

@Component
public class LockFactory {

    private final Map<RedissonLockType, Function<String, RLock>> redissonLockTypeMap;

    public LockFactory(RedissonClient redissonClient)
    {
        // 使用EnumMap,可以提高性能,因为枚举类型是编译时常量,所以可以减少内存消耗
        this.redissonLockTypeMap = new EnumMap<>(RedissonLockType.class);
        redissonLockTypeMap.put(RedissonLockType.Default_Lock, redissonClient::getLock);
        redissonLockTypeMap.put(RedissonLockType.Fair_Lock, redissonClient::getFairLock);
        redissonLockTypeMap.put(RedissonLockType.Read_Lock, name -> redissonClient.getReadWriteLock(name).readLock());
        redissonLockTypeMap.put(RedissonLockType.Write_Lock, name -> redissonClient.getReadWriteLock(name).writeLock());
    }

    public RLock getLock(String name, RedissonLockType type)
    {
        return redissonLockTypeMap.get(type).apply(name);
    }

}
  • MyLockFactory内部持有了一个Map,key是锁类型枚举,值是创建锁对象的Function。注意这里不是存锁对象,因为锁对象必须是多例的,不同业务用不同锁对象;同一个业务用相同锁对象。

  • MyLockFactory针对内部的Map进行了优化,采用了EnumMap。只有当Key是枚举类型时可以使用EnumMap,其底层不是hash表,而是简单的数组。由于枚举项数量固定,因此这个数组长度就等于枚举项个数,然后按照枚举项序号作为角标依次存入数组。这样就能根据枚举项序号作为角标快速定位到数组中的数据。

2. 切面AOP修改

我们将锁对象工厂注入MyLockAspect,然后就可以利用工厂来获取锁对象了:

四 使用

通过上面的修改,最终实现编码时通过注解动态传递锁类型

第四章 使用策略模式解决锁策略

首先,我们需要先分析一下锁失败的处理策略有哪些:

之后,就可以得出5种策略:

一般的策略模式大概是这样:

  • 定义策略接口

  • 定义不同策略实现类

  • 提供策略工厂,便于根据策略枚举获取不同策略实现

而在策略比较简单的情况下,我们完全可以用枚举代替策略工厂,简化策略模式。

一 增加工厂枚举

定义一个abstract方法,让每一个枚举变量都重写它

package com.tianji.promotion.domain.enums;

import com.tianji.common.exceptions.BizIllegalException;
import com.tianji.promotion.utils.MyLock;
import org.redisson.api.RLock;

public enum LockStrategy {

    FAST_SKIP(){
        @Override
        public boolean tryLock(RLock lock, MyLock myLock) throws InterruptedException {
            return lock.tryLock(0, myLock.leaveTime(), myLock.unit());
        }
    },
    FAST_FAIL(){
        @Override
        public boolean tryLock(RLock lock, MyLock myLock) throws InterruptedException {
            boolean isLock = lock.tryLock(0, myLock.leaveTime(), myLock.unit());
            if(!isLock)
            {
                throw new BizIllegalException("请求太频繁");
            }
            return true;
        }
    },
    KEEP_TRYING(){
        @Override
        public boolean tryLock(RLock lock, MyLock myLock) throws InterruptedException {
            lock.lock(myLock.leaveTime(), myLock.unit());
            return true;
        }
    },
    RETRY_TIMEOUT_SKIP(){
        @Override
        public boolean tryLock(RLock lock, MyLock myLock) throws InterruptedException {
            return lock.tryLock(myLock.waitTime(), myLock.leaveTime(), myLock.unit());
        }
    },
    RETRY_TIMEOUT_FAIL(){
        @Override
        public boolean tryLock(RLock lock, MyLock myLock) throws InterruptedException {
            boolean isLock = lock.tryLock(myLock.waitTime(), myLock.leaveTime(), myLock.unit());
            if(!isLock)
            {
                throw new BizIllegalException("请求太频繁");
            }
            return true;
        }
    };

    abstract public boolean tryLock(RLock lock, MyLock myLock) throws InterruptedException;

}

二 给注解增加策略枚举

增加一个策略枚举变量

package com.tianji.promotion.utils;

import com.tianji.promotion.domain.enums.LockStrategy;
import com.tianji.promotion.domain.enums.RedissonLockType;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.concurrent.TimeUnit;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyLock {

    // 锁名称
    String name();

    // 获取锁的等待时间。当获取锁失败后可以多次重试,直到waitTime时间耗尽。waitTime默认-1,即失败后立刻返回,不重试。
    long waitTime() default 1;

    // 锁超时释放时间。默认是30,同时会利用WatchDog来不断更新超时时间。需要注意的是,如果手动设置leaseTime值,会导致WatchDog失效
    long leaveTime() default -1;

    // 时间单位
    TimeUnit unit() default TimeUnit.SECONDS;

    // 锁类型
    RedissonLockType type() default RedissonLockType.Default_Lock;

    // 锁策略
    LockStrategy lockStrategy() default LockStrategy.RETRY_TIMEOUT_FAIL;

}

三 修改AOP切面类

修改一下获取锁的代码,改成通过用户传递的枚举变量获取锁

四 使用

在注解上直接填入对应的策略就行

第五章 基于SPEL的动态锁名

在当前业务中,我们的锁对象本来应该是当前登录用户,是动态获取的。而加锁是基于注解参数添加的,在编码时就需要指定。怎么办?

Spring中提供了一种表达式语法,称为SPEL表达式,可以执行java代码,获取任意参数。

一 使用

首先,在使用锁注解时,锁名称可以利用SPEL表达式,例如我们指定锁名称中要包含参数中的用户id,则可以这样写:

二 AOP解析锁名

三 解析代码


/**
 * SPEL的正则规则
 */
private static final Pattern pattern = Pattern.compile("\\#\\{([^\\}]*)\\}");
/**
 * 方法参数解析器
 */
private static final ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();

/**
 * 解析锁名称
 * @param name 原始锁名称
 * @param pjp 切入点
 * @return 解析后的锁名称
 */
private String getLockName(String name, ProceedingJoinPoint pjp) {
    // 1.判断是否存在spel表达式
    if (StringUtils.isBlank(name) || !name.contains("#")) {
        // 不存在,直接返回
        return name;
    }
    // 2.构建context,也就是SPEL表达式获取参数的上下文环境,这里上下文就是切入点的参数列表
    EvaluationContext context = new MethodBasedEvaluationContext(
            TypedValue.NULL, resolveMethod(pjp), pjp.getArgs(), parameterNameDiscoverer);
    // 3.构建SPEL解析器
    ExpressionParser parser = new SpelExpressionParser();
    // 4.循环处理,因为表达式中可以包含多个表达式
    Matcher matcher = pattern.matcher(name);
    while (matcher.find()) {
        // 4.1.获取表达式
        String tmp = matcher.group();
        String group = matcher.group(1);
        // 4.2.这里要判断表达式是否以 T字符开头,这种属于解析静态方法,不走上下文
        Expression expression = parser.parseExpression(group.charAt(0) == 'T' ? group : "#" + group);
        // 4.3.解析出表达式对应的值
        Object value = expression.getValue(context);
        // 4.4.用值替换锁名称中的SPEL表达式
        name = name.replace(tmp, ObjectUtils.nullSafeToString(value));
    }
    return name;
}

private Method resolveMethod(ProceedingJoinPoint pjp) {
    // 1.获取方法签名
    MethodSignature signature = (MethodSignature)pjp.getSignature();
    // 2.获取字节码
    Class<?> clazz = pjp.getTarget().getClass();
    // 3.方法名称
    String name = signature.getName();
    // 4.方法参数列表
    Class<?>[] parameterTypes = signature.getMethod().getParameterTypes();
    return tryGetDeclaredMethod(clazz, name, parameterTypes);
}

private Method tryGetDeclaredMethod(Class<?> clazz, String name, Class<?> ... parameterTypes){
    try {
        // 5.反射获取方法
        return clazz.getDeclaredMethod(name, parameterTypes);
    } catch (NoSuchMethodException e) {
        Class<?> superClass = clazz.getSuperclass();
        if (superClass != null) {
            // 尝试从父类寻找
            return tryGetDeclaredMethod(superClass, name, parameterTypes);
        }
    }
    return null;
}


网站公告

今日签到

点亮在社区的每一天
去签到