SpringBoot интегрирует распределенную блокировку redis

Redis

После предыдущей интеграции SpringBoot пошаговых журналов Redis и изучения распределенных блокировок Redis, зачем вам нужны распределенные блокировки?

Принцип распределенной блокировки Redis

В случае развертывания традиционных монолитных приложений на одном компьютере для управления взаимным исключением можно использовать связанные с параллелизмом блокировки Java, такие как ReentrantLcok или synchronized. Однако с учетом потребностей развития бизнеса исходная система развертывания на одном компьютере постепенно развертывается на нескольких машинах и нескольких JVM для одновременного предоставления услуг, что делает стратегию блокировки управления параллелизмом в исходном развертывании на одном компьютере недействительной. Чтобы решить эту проблему, необходим механизм взаимного исключения между JVM для управления доступом к общим ресурсам, и это проблема, которую должны решить распределенные блокировки.

Условия реализации распределенных блокировок

  • Взаимное исключение, как отдельное приложение, чтобы гарантировать, что в любое время только один клиент может удерживать блокировку.
  • Надежность, обеспечивающая стабильность системы, не может привести к взаимоблокировке.
  • Последовательность, чтобы гарантировать, что замок может быть разблокирован только человеком, который его запирает, и не может случиться так, что замок A будет разблокирован пользователем B

Реализация распределенной блокировки

У разных людей, реализующих распределенные блокировки в Redis, может быть разная логика реализации, но в основе лежат следующие три метода.

1. SETNXSETNX key val Если и только если ключ не существует, установить строку, ключ которой равен val, и вернуть 1; если ключ существует, ничего не делать и вернуть 0.

2. Expireexpire timeout key Установите таймаут ключа, единица измерения секунд, по истечении которого блокировка будет автоматически снята во избежание взаимоблокировки.

3.Deletedelete ключ удалить ключ.

Схематическая диаграмма выглядит следующим образом:

分布式锁

Битва за распределенные блокировки Redis

Схема структуры кода проекта

1570783101690

импортировать зависимости

существуетpom.xmlДобавьте зависимости starter-web, starter-aop, starter-data-redis в

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-aop</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
</dependencies>

конфигурация свойства

существуетapplication.properitesДобавьте элементы конфигурации, связанные с Redis, в файл ресурсов.

server:
  port: 1999
spring:
  datasource:
    url: jdbc:mysql://127.0.0.1:3306/mybatis-plus-test?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai
    driverClassName: com.mysql.cj.jdbc.Driver
    username: root
    password: root
  redis:
    host: 127.0.0.1
    port: 6379
    timeout: 5000ms
    password:
    database: 0
    jedis:
      pool:
        max-active: 50
        max-wait: 3000ms
        max-idle: 20
        min-idle: 2

аннотация

1. Создайте аннотацию CacheLock со следующей конфигурацией атрибутов.

  • префикс: префикс ключа в кеше
  • expire: время истечения срока действия, по умолчанию здесь 5 секунд.
  • timeUnit: единица времени ожидания, по умолчанию здесь секунды.
  • разделитель: разделитель ключа, который разделяет разные значения параметров
    package com.tuhu.twosample.chen.distributed.annotation;
    
    import java.lang.annotation.*;
    import java.util.concurrent.TimeUnit;
    
    /**
     * 锁的注解
     * @author chendesheng
     * @create 2019/10/11 16:06
     */
    @Target({ElementType.METHOD})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    @Inherited
    public @interface CacheLock {
    
        /**
         * redis 锁key的前缀
         *
         * @return redis 锁key的前缀
         */
        String prefix() default "";
    
        /**
         * 过期秒数,默认为5秒
         *
         * @return 轮询锁的时间
         */
        int expire() default 5;
    
        /**
         * 超时时间单位
         *
         * @return 秒
         */
        TimeUnit timeUnit() default TimeUnit.SECONDS;
    
        /**
         * <p>Key的分隔符(默认 :)</p>
         * <p>生成的Key:N:SO1008:500</p>
         *
         * @return String
         */
        String delimiter() default ":";
    
    }
    

2. Правила генерации ключей определяете сами, если приходится писать правила разбора самостоятельно через грамматику выражений, то это более хлопотно, поэтому метод аннотации все же используется.

package com.tuhu.twosample.chen.distributed.annotation;

import java.lang.annotation.*;

/**
 * 锁的参数
 * @author chendesheng
 * @create 2019/10/11 16:08
 */
@Target({ElementType.PARAMETER, ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface CacheParam {

    /**
     * 字段名称
     *
     * @return String
     */
    String name() default "";
}

стратегия генерации ключей

1. Интерфейс

package com.tuhu.twosample.chen.distributed.common;

import org.aspectj.lang.ProceedingJoinPoint;

/**
 * key生成器
 * @author chendesheng
 * @create 2019/10/11 16:09
 */
public interface CacheKeyGenerator {

    /**
     * 获取AOP参数,生成指定缓存Key
     *
     * @param pjp PJP
     * @return 缓存KEY
     */
    String getLockKey(ProceedingJoinPoint pjp);
}

2. Реализация интерфейса

package com.tuhu.twosample.chen.distributed.common;

import com.tuhu.twosample.chen.distributed.annotation.CacheLock;
import com.tuhu.twosample.chen.distributed.annotation.CacheParam;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;

/**
 * 通过接口注入的方式去写不同的生成规则
 * @author chendesheng
 * @create 2019/10/11 16:09
 */
public class LockKeyGenerator implements CacheKeyGenerator {

    @Override
    public String getLockKey(ProceedingJoinPoint pjp) {
        MethodSignature signature = (MethodSignature) pjp.getSignature();
        Method method = signature.getMethod();
        CacheLock lockAnnotation = method.getAnnotation(CacheLock.class);
        final Object[] args = pjp.getArgs();
        final Parameter[] parameters = method.getParameters();
        StringBuilder builder = new StringBuilder();
        //默认解析方法里面带 CacheParam 注解的属性,如果没有尝试着解析实体对象中的
        for (int i = 0; i < parameters.length; i++) {
            final CacheParam annotation = parameters[i].getAnnotation(CacheParam.class);
            if (annotation == null) {
                continue;
            }
            builder.append(lockAnnotation.delimiter()).append(args[i]);
        }
        if (StringUtils.isEmpty(builder.toString())) {
            final Annotation[][] parameterAnnotations = method.getParameterAnnotations();
            for (int i = 0; i < parameterAnnotations.length; i++) {
                final Object object = args[i];
                final Field[] fields = object.getClass().getDeclaredFields();
                for (Field field : fields) {
                    final CacheParam annotation = field.getAnnotation(CacheParam.class);
                    if (annotation == null) {
                        continue;
                    }
                    field.setAccessible(true);
                    builder.append(lockAnnotation.delimiter()).append(ReflectionUtils.getField(field, object));
                }
            }
        }
        return lockAnnotation.prefix() + builder.toString();
    }


}

Перехватчик блокировки (АОП)

Друзья, знакомые с Redis, знают, что он потокобезопасен.Мы можем легко реализовать распределенную блокировку, используя его функции, такие как opsForValue().setIfAbsent(key,value). кэш возвращает true одновременно и наоборот; когда ключ кэшируется, устанавливается время истечения срока действия, чтобы предотвратить задержку блокировки из-за сбоя системы, что приводит к взаимоблокировке; тогда можем ли мы думать, что когда он возвращает true, мы думаем, что он получил блокировку. Теперь, когда блокировка не снята, мы выбрасываем исключение....

package com.tuhu.twosample.chen.distributed.interceptor;

import com.tuhu.twosample.chen.distributed.annotation.CacheLock;
import com.tuhu.twosample.chen.distributed.common.CacheKeyGenerator;
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.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.util.StringUtils;

import java.lang.reflect.Method;

/**
 * @author chendesheng
 * @create 2019/10/11 16:11
 */
@Aspect
@Configuration
public class LockMethodInterceptor {

    @Autowired
    public LockMethodInterceptor(StringRedisTemplate lockRedisTemplate, CacheKeyGenerator cacheKeyGenerator) {
        this.lockRedisTemplate = lockRedisTemplate;
        this.cacheKeyGenerator = cacheKeyGenerator;
    }

    private final StringRedisTemplate lockRedisTemplate;
    private final CacheKeyGenerator cacheKeyGenerator;


    @Around("execution(public * *(..)) && @annotation(com.tuhu.twosample.chen.distributed.annotation.CacheLock)")
    public Object interceptor(ProceedingJoinPoint pjp) {

        MethodSignature signature = (MethodSignature) pjp.getSignature();
        Method method = signature.getMethod();
        CacheLock lock = method.getAnnotation(CacheLock.class);
        if (StringUtils.isEmpty(lock.prefix())) {
            throw new RuntimeException("lock key can't be null...");
        }
        final String lockKey = cacheKeyGenerator.getLockKey(pjp);
        try {
            //key不存在才能设置成功
            final Boolean success = lockRedisTemplate.opsForValue().setIfAbsent(lockKey, "");
            if (success) {
                lockRedisTemplate.expire(lockKey, lock.expire(), lock.timeUnit());
            } else {
                //按理来说 我们应该抛出一个自定义的 CacheLockException 异常;
                throw new RuntimeException("请勿重复请求");
            }
            try {
                return pjp.proceed();
            } catch (Throwable throwable) {
                throw new RuntimeException("系统异常");
            }
        } finally {

            //如果演示的话需要注释该代码;实际应该放开
            // lockRedisTemplate.delete(lockKey);

        }
    }
}

слой управления

Добавьте @CacheLock(prefix="test") в метод интерфейса, а затем добавьте @CacheParam в динамическое значение, новый ключ после генерации будет кэширован; (например: токен интерфейса = 1, тогда окончательное значение ключа равно test: 1 и так далее, если условий несколько)

package com.tuhu.twosample.chen.controller;

import com.tuhu.twosample.chen.distributed.annotation.CacheLock;
import com.tuhu.twosample.chen.distributed.annotation.CacheParam;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/**
 * @author chendesheng
 * @create 2019/10/11 16:13
 */
@RestController
@RequestMapping("/chen/lock")
@Slf4j
public class LockController {
    @CacheLock(prefix = "test")
    @GetMapping("/test")
    public String query(@CacheParam(name = "token") @RequestParam String token) {
        return "success - " + token;
    }
}

основная функция

Необходимо ввести ранее определенныйCacheKeyGeneratorКонкретная реализация интерфейса....

package com.tuhu.twosample;

import com.tuhu.twosample.chen.distributed.common.CacheKeyGenerator;
import com.tuhu.twosample.chen.distributed.common.LockKeyGenerator;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
/**
 * @author chendesheng
 * @since 2019-08-06
 */
@SpringBootApplication
@MapperScan("com.baomidou.mybatisplus.samples.quickstart.mapper")
@MapperScan("com.tuhu.twosample.chen.mapper")
public class TwoSampleApplication {

  	public static void main(String[] args) {
        SpringApplication.run(TwoSampleApplication.class, args);
    }
    @Bean
    public CacheKeyGenerator cacheKeyGenerator() {
        return new LockKeyGenerator();
    }
}

контрольная работа

Запустите проект и введите адрес в почтальоне: http://localhost:1999/chen/lock/test?token=1 >

Результат первого запроса:

1570783805599

Результат второго запроса:

1570783834354

Когда срок действия ключа истекает, запрос возвращается в нормальное состояние.

Наконец

Однако эта распределенная блокировка также имеет дефекты: если A успешно получает блокировку после успеха setnx, то есть блокировка была сохранена в Redis, а сервер аварийно выключен или перезапущен, то наше дело не будет выполнено. срок действия блокировки не будет установлен, поэтому блокировка не будет снята, и произойдет взаимоблокировка. Поэтому надо оптимизировать блокировку, усердно учиться и крякать.