项目 - 利用注解和Spring Aop实现Redis分布式锁

项目 - 利用注解和Spring Aop实现Redis分布式锁,第1张

文章目录
    • 1. RedisLockDto
    • 2. RedisLockService
    • 3. RedisLockServiceImpl
      • 3.1 获取锁的key
      • 3.2 获取锁的值lockValue
      • 3.3 分布式锁异常RedisLockException
      • 3.4 获取锁
      • 3.5 锁续期
      • 3.6 释放锁
    • 4. 定义注解@RedisLock
    • 5. 定义注解@BusinessId
    • 6. 读取目标方法上加的注解并加锁
    • 7. Spring boot redis分布式锁自动化配置
    • 8. 测试
    • 9. 测试使用

1. RedisLockDto

RedisLockDto定义与锁相关的信息

@Data
@Builder
public class RedisLockDto {
    @Tolerate
    public RedisLockDto() {
    }

    /**
     * redis lock的前缀
     */
    private String prefix;

    /**
     * 过期时间(单位:毫秒)
     */
    private Long expiredTime;

    /**
     * 业务ID
     */
    private String businessId;

    /**
     * 是否等待重试
     */
    private Boolean retry = true;

    /**
     * 锁的值
     */
    private String lockValue;

    /**
     * 最大重试次数
     */
    private Long maxRetryTimes;

    /**
     * 等待时间
     */
    private Long delayTime;
}
2. RedisLockService
public interface RedisLockService {
    /**
     * 获取锁
     *
     * @param lock 锁信息
     * @throws RedisLockException 获取锁失败时抛出异常
     */
    void lock(@NonNull RedisLockDto lock) throws RedisLockException;

    /**
     * 释放锁
     *
     * @param lock 锁信息
     */
    void unlock(@NonNull RedisLockDto lock);

    /**
     * 锁续命
     *
     * @param lock 锁信息
     * @return 续命是否成功
     */
    boolean extend(@NonNull RedisLockDto lock);
}
3. RedisLockServiceImpl
@AllArgsConstructor
@Slf4j
public class RedisLockServiceImpl implements RedisLockService {

    /**
     * redis template
     */
    private final RedisTemplate<String, String> redisTemplate;

    /**
     * 最大重试次数
     */
    private final int maxRetryTimes;

    /**
     * 等待时间
     */
    private final long delayTime;

    @Override
    public void lock(@NonNull RedisLockDto lock) throws RedisLockException {
        if (lock.getRetry()) {
            lockWithRetry(lock);
        } else {
            lockWithoutRetry(lock);
        }
    }

    /**
     * 不带重试的获取锁
     *
     * @param lock 锁
     * @throws RedisLockException 当获取不到锁时抛出异常
     */
    private void lockWithoutRetry(@NonNull RedisLockDto lock) throws RedisLockException {
        if (!tryLock(lock)) {
            log.warn("failed get lock , key: {}", this.getLockKey(lock));
            throw new RedisLockException(lock.getPrefix(), lock.getBusinessId(), lock, getLockValue(lock));
        }
        log.info("successfully get lock , key: {}", this.getLockKey(lock));
    }

    /**
     * 带重试的获取锁
     *
     * @param lock 锁
     * @throws RedisLockException 如果尝试获取锁超过指定次数时,抛出异常
     */
    private void lockWithRetry(RedisLockDto lock) throws RedisLockException {
        int retryTimes = 0;
        while (!tryLock(lock)) {
            ++retryTimes;

            long realMaxRetryTimes;
            if (lock.getMaxRetryTimes() != null) {
                realMaxRetryTimes = lock.getMaxRetryTimes();
            } else {
                realMaxRetryTimes = this.maxRetryTimes;
            }
            if (realMaxRetryTimes >= 0 && retryTimes >= realMaxRetryTimes) {
                log.warn("failed get lock ,key: {}", this.getLockKey(lock));
                throw new RedisLockException(lock.getPrefix(), lock.getBusinessId(), lock, getLockValue(lock));
            }

            long realDelayTime;
            if (lock.getDelayTime() != null) {
                realDelayTime = lock.getDelayTime();
            } else {
                realDelayTime = this.delayTime;
            }

            try {
                Thread.sleep(realDelayTime);
            } catch (InterruptedException | IllegalArgumentException e) {
                // 什么也不做
            }
        }
        log.info("successfully get lock ,key: {}", this.getLockKey(lock));
    }

    @Override
    public void unlock(@NonNull RedisLockDto lock) {
        if (lock.getLockValue().equals(redisTemplate.opsForValue().get(this.getLockKey(lock)))) {
            redisTemplate.delete(this.getLockKey(lock));
            log.info("successfully delete lock , key:{}", this.getLockKey(lock));
        }
    }

    /**
     * 尝试获取锁
     *
     * @param lock 锁
     * @return 获取结果
     */
    private boolean tryLock(@NonNull RedisLockDto lock) {
        return Boolean.TRUE.equals(
                redisTemplate.opsForValue()
                        .setIfAbsent(this.getLockKey(lock), lock.getLockValue(), lock.getExpiredTime(), TimeUnit.MILLISECONDS)
        );
    }

    /**
     * 获取锁当前的值
     *
     * @param lock 锁
     * @return 获取结果
     */
    private String getLockValue(@NonNull RedisLockDto lock) {
        return redisTemplate.opsForValue().get(this.getLockKey(lock));
    }

    /**
     * 锁续命
     *
     * @return 续命是否成功
     */
    @Override
    public boolean extend(@NonNull RedisLockDto lock) {
        return Boolean.TRUE.equals(
                redisTemplate.expire(this.getLockKey(lock), lock.getExpiredTime(), TimeUnit.MILLISECONDS)
        );
    }

    /**
     * 获取锁的key
     *
     * @param lock 锁
     * @return 锁的key
     */
    private String getLockKey(RedisLockDto lock) {
        return lock.getPrefix() + "_" + lock.getBusinessId();
    }
}
3.1 获取锁的key
/**
 * 获取锁的key
 *
 * @param lock 锁
 * @return 锁的key
 */
private String getLockKey(RedisLockDto lock) {
    return lock.getPrefix() + "_" + lock.getBusinessId();
}
3.2 获取锁的值lockValue
/**
 * 获取锁当前的值
 *
 * @param lock 锁
 * @return 获取结果
 */
private String getLockValue(@NonNull RedisLockDto lock) {
    return redisTemplate.opsForValue().get(this.getLockKey(lock));
}
3.3 分布式锁异常RedisLockException
public class RedisLockException extends Exception {
    /**
     * redis锁前缀
     */
    @Getter
    private String prefix;

    /**
     * 业务id
     */
    @Getter
    private String businessId;

    /**
     * 锁配置对象
     */
    @Getter
    private RedisLockDto lock;

    /**
     * 锁的值
     */
    @Getter
    private String lockValue;

    public RedisLockException(String prefix, String businessId, RedisLockDto lock, String lockValue) {
        super("failed to acquire redis lock,prefix is " + prefix + ",business id is " + businessId + ",lock value is " + lockValue);
        this.prefix = prefix;
        this.businessId = businessId;
        this.lock = lock;
        this.lockValue = lockValue;
    }
}
3.4 获取锁
@Override
public void lock(@NonNull RedisLockDto lock) throws RedisLockException {
    if (lock.getRetry()) {
        lockWithRetry(lock);
    } else {
        lockWithoutRetry(lock);
    }
}

不带重试的获取锁:

/**
 * 不带重试的获取锁
 *
 * @param lock 锁
 * @throws RedisLockException 当获取不到锁时抛出异常
 */
private void lockWithoutRetry(@NonNull RedisLockDto lock) throws RedisLockException {
    if (!tryLock(lock)) {
        log.warn("failed get lock , key: {}", this.getLockKey(lock));
        throw new RedisLockException(lock.getPrefix(), lock.getBusinessId(), lock, getLockValue(lock));
    }
    log.info("successfully get lock , key: {}", this.getLockKey(lock));
}

尝试获取锁:

/**
 * 尝试获取锁
 *
 * @param lock 锁
 * @return 获取结果
 */
private boolean tryLock(@NonNull RedisLockDto lock) {
    return Boolean.TRUE.equals(redisTemplate.opsForValue().setIfAbsent(
                    this.getLockKey(lock),
                    lock.getLockValue(),
                    lock.getExpiredTime(),
                    TimeUnit.MILLISECONDS));
}

带重试的尝试获取锁:

/**
 * 带重试的获取锁
 *
 * @param lock 锁
 * @throws RedisLockException 如果尝试获取锁超过指定次数时,抛出异常
 */
private void lockWithRetry(RedisLockDto lock) throws RedisLockException {
    // 重试次数
    int retryTimes = 0;
    // 如果获取锁失败,则重试获取锁
    while (!tryLock(lock)) {
        // 重试次数+1
        ++retryTimes;
        
        // 最大重试次数
        long realMaxRetryTimes;
        if (lock.getMaxRetryTimes() != null) {
            realMaxRetryTimes = lock.getMaxRetryTimes();
        } else {
            realMaxRetryTimes = this.maxRetryTimes;
        }
        // 如果重试次数大于最大重试次数,则获取锁失败
        if (realMaxRetryTimes >= 0 && retryTimes >= realMaxRetryTimes) {
            log.warn("failed get lock ,key: {}", this.getLockKey(lock));
            throw new RedisLockException(lock.getPrefix(), lock.getBusinessId(), lock, getLockValue(lock));
        }

        // 线程等待时间realDelayTime
        long realDelayTime;
        if (lock.getDelayTime() != null) {
            realDelayTime = lock.getDelayTime();
        } else {
            realDelayTime = this.delayTime;
        }
        // 让线程等待realDelayTime后再获取锁
        try {
            Thread.sleep(realDelayTime);
        } catch (InterruptedException | IllegalArgumentException e) {
            // 什么也不做
        }
    }
    log.info("successfully get lock ,key: {}", this.getLockKey(lock));
}
3.5 锁续期

担心 joinPoint.proceed()切点执行的方法太耗时,导致Redis中的key由于超时提前释放了。

例如,线程A先获取锁,proceed() 方法耗时,超过了锁超时时间,到期释放了锁,这时另一个线程B成功获取Redis锁,两个线程同时对同一批数据进行 *** 作,导致数据不准确。

/**
 * 锁续命
 *
 * @return 续命是否成功
 */
@Override
public boolean extend(@NonNull RedisLockDto lock) {
    return Boolean.TRUE.equals(redisTemplate.expire(
            this.getLockKey(lock), 
            lock.getExpiredTime(), 
            TimeUnit.MILLISECONDS)
    );
}
3.6 释放锁
@Override
public void unlock(@NonNull RedisLockDto lock) {
    if (lock.getLockValue().equals(redisTemplate.opsForValue().get(this.getLockKey(lock)))) {
        redisTemplate.delete(this.getLockKey(lock));
        log.info("successfully delete lock , key:{}", this.getLockKey(lock));
    }
}
4. 定义注解@RedisLock

定义注解@RedisLock用于读取redis分布式锁相关的信息,和spring aop搭配使用,注解用于方法上,可以为方法添加分布式锁。

@Retention(RetentionPolicy.RUNTIME)  // 可以通过反射读取注解
@Target(ElementType.METHOD)          // 该注解只能加在方法上
public @interface RedisLock {
    /**
     * redis分布式锁的前缀,必填
     *
     * @return redis分布式锁的前缀
     */
    String prefix();

    /**
     * redis分布式锁的过期时间(毫秒)
     *
     * @return redis分布式锁的前缀
     */
    long expiredTime() default 1000L;

    /**
     * 是否等待锁释放,默认不等待直接报错
     *
     * @return 是否等待锁释放
     */
    boolean retry() default false;
}
5. 定义注解@BusinessId

和@RedisLock搭配使用,用于标注入参中的业务id。

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
public @interface BusinessId {
}
6. 读取目标方法上加的注解并加锁
@Aspect
@Component
@EnableAspectJAutoProxy
public class RedisLockAspect {
    /**
     *  声明切点
     */
    @Pointcut("@annotation(com.example.redislock.test.RedisLock)")
    private void redisLockPointCut() {
    }

    @Autowired
    private RedisLockService redisLockService;

    /**
     * 在目标方法执行之前获取redis锁
     * 在目标方法执行之后释放锁
     */
    @Around("redisLockPointCut()")
    public Object redisLockAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
        // 1.从注解获取redis lock的配置参数
        RedisLockDto lock = this.getRedisLockFromAnnotation(joinPoint);
        // 2.尝试获取锁
        try {
            redisLockService.lock(lock);
            // 放行,目标方法的执行
            return joinPoint.proceed();
        } finally {
            // 3.释放锁
            redisLockService.unlock(lock);
        }
    }

    /**
     * 从注解获取分布式锁相关信息
     *
     * @param joinPoint 切点
     * @return redis锁对象
     */
    private RedisLockDto getRedisLockFromAnnotation(ProceedingJoinPoint joinPoint) {
        // 获取目标方法
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        Method method = methodSignature.getMethod();

        // 获取方法上指定类型的注解
        RedisLock redisLock = method.getAnnotation(RedisLock.class);
        // 从注解对象中获取元素的值
        String prefix = redisLock.prefix();
        if (StringUtils.isEmpty(redisLock.prefix())) {
            // 如果redis分布式锁的key的前缀为空,则使用目标方法的名称作为前缀
            prefix = method.getName();
        }

        // 获取方法中每个参数前加的注解的值,一个方法可能有多个参数,一个参数前可能加多个注解,所以为二维数组
        Annotation[][] annotations = method.getParameterAnnotations();
        StringBuilder businessIdBuilder = new StringBuilder();
        for (int i = 0; i < annotations.length; i++) {
            // 获取目标方法的第i个参数
            Object param = joinPoint.getArgs()[i];
            // 获取目标方法的第i个参数前所加的所有注解
            Annotation[] paramAnnotations = annotations[i];
            if (param == null || paramAnnotations.length == 0) {
                continue;
            }
            // 遍历目标方法第i个参数前的所有注解
            for (Annotation annotation : paramAnnotations) {
                // 判断注解类型是否BusinessId
                if (annotation.annotationType().equals(BusinessId.class)) {
                    businessIdBuilder.append(param);
                }
            }
        }

        return RedisLockDto.builder()
                .prefix(prefix)
                .expiredTime(redisLock.expiredTime())
                .retry(redisLock.retry())
                .lockValue(UUID.randomUUID().toString())
                .businessId(businessIdBuilder.toString())
                .build();
    }
}
7. Spring boot redis分布式锁自动化配置

application.yml文件:

ngsoc:
  redis-lock:
    maxRetryTimes: 300
    delayTime: 3000

扫描bean并注入:

@Configuration
public class RedisLockAutoConfiguration {

    /**
     * 最大重试次数
     */
    @Value("${ngsoc.redis-lock.maxRetryTimes:10}")
    @Getter
    private int maxRetryTimes;

    /**
     * 等待时间
     */
    @Value("${ngsoc.redis-lock.delayTime:100}")
    @Getter
    private long delayTime;

    /**
     * 配置RedisLockAspect
     */
    @Bean
    public RedisLockAspect redisLockAspect() {
        return new RedisLockAspect();
    }

    /**
     * 配置 RedisTemplate 及其序列化方式
     */
    @Bean(value = "redisTemplate")
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(connectionFactory);
        Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<Object>(
                Object.class
        );
        ObjectMapper objectMapper = new ObjectMapper();
        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        objectMapper.activateDefaultTyping(
                LaissezFaireSubTypeValidator.instance,
                ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY
        );
        objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
        jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
        StringRedisSerializer redisSerializer = new StringRedisSerializer();
        redisTemplate.setKeySerializer(redisSerializer);
        redisTemplate.setHashKeySerializer(redisSerializer);
        redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
        redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }

    /**
     * 配置 RedisLockService
     */
    @Bean
    public RedisLockService redisLockService(RedisTemplate<String, String> redisTemplate) {
        return new RedisLockServiceImpl(redisTemplate, maxRetryTimes, delayTime);
    }
}

新增一个spring.factories文件:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.example.redislock.test.RedisLockAutoConfiguration

通过spring.factories,将RedisLockAutoConfiguration配置进去,可以实现自动加载的功能。

8. 测试
@ActiveProfiles("test")
@ExtendWith(SpringExtension.class)
@SpringBootTest(classes = RedisLockTestApp.class)
@Slf4j
@Disabled
public class RedisLockTest {
    private static final int THREAD_NUM = 16;
    private static final int CALL_NUM = 200;

    @Autowired
    private RedisLockService redisLockService;

    private volatile int sum = 0;

    @Test
    public void testSimpleLock() throws InterruptedException, ExecutionException {
        sum = 0;
        RedisLockDto lock = new RedisLockDto();
        lock.setPrefix("RedisLockTest");
        lock.setRetry(true);
        lock.setMaxRetryTimes(-1L);
        lock.setBusinessId(UUID.randomUUID().toString());
        lock.setLockValue(UUID.randomUUID().toString());
        lock.setExpiredTime(30_000L);
        
        // 利用线程池创建16个线程
        ExecutorService executor = Executors.newFixedThreadPool(THREAD_NUM);

        // 创建200个任务
        ArrayList<Callable<Integer>> callables = new ArrayList<>(CALL_NUM);
        for (int i = 0; i < CALL_NUM; i++) {
            callables.add(() -> {
                        redisLockService.lock(lock);
                        int tmpSum = sum + 1;
                        sum = tmpSum;
                        redisLockService.unlock(lock);
                        Thread.sleep((int) (Math.random() * 100));
                        return tmpSum;
                    }
            );
        }
        
        // 线程池执行任务
        List<Future<Integer>> futureIntegers = executor.invokeAll(callables);

        HashMap<Integer, Integer> map = new HashMap<>();
        for (int i = 0; i < futureIntegers.size(); i++) {
            Future<Integer> futureInteger = futureIntegers.get(i);
            Integer integer = futureInteger.get();
            Integer currentNum = map.get(integer);
            if (currentNum == null) {
                currentNum = 0;
            }
            currentNum++;
            map.put(integer, currentNum);
            System.out.println("done : " + i);
        }

        assertEquals(CALL_NUM, sum);

        for (int i = 1; i <= CALL_NUM; i++) {
            assertEquals(1, map.get(i));
        }
    }

    @Test
    public void testOneTryLock() {
        sum = 0;
        RedisLockDto lock = new RedisLockDto();
        lock.setRetry(false);
        lock.setPrefix("RedisLockTest");
        lock.setBusinessId(UUID.randomUUID().toString());
        lock.setLockValue(UUID.randomUUID().toString());
        lock.setExpiredTime(30_000L);
        ExecutorService executor = Executors.newFixedThreadPool(THREAD_NUM);

        assertThrows(
                RedisLockException.class,
                () -> {
                    redisLockService.lock(lock);
                    redisLockService.lock(lock);
                }
        );
    }
}
@EnableScheduling
@EnableAsync
@SpringBootApplication
@ContextConfiguration(classes = { RedisLockAutoConfiguration.class })
public class RedisLockTestApp {
    public static void main(String[] args) {
        SpringApplication.run(RedisLockTestApp.class, args);
    }
}
9. 测试使用
@RedisLock(prefix = "test_lock", expiredTime = 300000L)
public void completeTask(
    int riskOrderId, 
    TaskCompleteQo taskCompleteQo,
    String currentUserName, 
    @BusinessId String taskId)
}

实现方法参考文章:https://www.cnblogs.com/xiaowan7/p/14277925.html

欢迎分享,转载请注明来源:内存溢出

原文地址:https://54852.com/langs/795099.html

(0)
打赏 微信扫一扫微信扫一扫 支付宝扫一扫支付宝扫一扫
上一篇 2022-05-06
下一篇2022-05-06

发表评论

登录后才能评论

评论列表(0条)

    保存