Аннотация позволяет вашему проекту SpringBoot получить возможности распределенной блокировки и ограничения тока.

Spring Boot

Аннотация позволяет вашему проекту SpringBoot получить возможности распределенной блокировки и ограничения тока.


добавить зависимости

**该项目尚未上传到maven中央仓库,所以需要自行clone本项目本地编译**

pom.xml добавить зависимости

<dependency>
	<groupId>site.higgs</groupId>
	<artifactId>limiter</artifactId>
	<version>1.0-SNAPSHOT</version>
</dependency>

Этот модуль зависит отspring-context,spring-core,guava,redisson, если есть конфликт, он автоматически исключит соответствующие модули

Например

<dependency>
	<groupId>site.higgs</groupId>
	<artifactId>limiter</artifactId>
	<version>1.0-SNAPSHOT</version>
	<exclusions>
		<exclusion>
			<groupId>org.springframework</groupId>
			<artifactId>spring-context</artifactId>
		</exclusion>
		<exclusion>
			<groupId>org.springframework</groupId>
			<artifactId>spring-core</artifactId>
		</exclusion>
	</exclusions>
</dependency>

добавить аннотацию@EnableLimiter

@SpringBootApplication
@EnableLimiter
public class Application {
	public static void main(String[] args) {
		SpringApplication.run(Application.class, args);
	}
}

Пример


1. @HLock

Предположим, что существует такой сценарий: пользователи могут использовать код погашения для погашения подарков.Конечно, каждый код погашения можно использовать только один раз, тогда нам нужно понимать, что каждый код пополнения можно использовать только один раз в этом интерфейсе.Практика может быть, что разработчикам нужно вручную добавить к этим данным пессимистическую блокировку, напримерselect code from code_table where code =#{code} for update, Очень нужно писать таким образом, чтобы интерфейс не подвергался злонамеренной атаке.

Тем не менее, способ написания этого является спорным, а накладные расходы на блокировки стоят на втором месте, и разработчики должны всегда защищаться от этих неожиданных вредоносных атак, которые усложняют изначально простую логику. Теперь мы можем получить блокировку декларативно и отделить «блокировку» от бизнес-логики.

Например:

    /**
     * 限制键为 #redeemCode+#user.userId
     * 当多个请求同时到达时,只有一个会被正常处理,其他请求会被降级
     * 当正常的请求被处理完毕,锁会释放
     * 值得注意得是keys 本身不会包含方法名,最好前面加前缀同其他接口分开
     * @param redeemCode
     * @return
     */
    @RequestMapping(value = "/exchange", method = RequestMethod.GET)
    @HLock(keys = "#redeemCode+#user.userId", fallbackResolver = "busyFallback", lockManager = "redisLockManager", argInjecters = "injectUser")
    public ResponseMessage exchange(@RequestParam("redeemCode") String redeemCode) {
        try {
            // do something
            Thread.sleep(2000);
        } catch (InterruptedException e) {

        }
        return ResponseMessage.ok(null);
    }

пройти через@HLockАннотация добавляет блокировку к этому интерфейсу,keysЗначение ключа этой блокировки указано вchargeCode, только один из запросов обрабатывается правильно, а остальные запросы будут понижены, и что вернут пониженные запросы?

ответfallbackResolverсвойства, на самом делеbusyFallbackЭто Bean, управляемый Spring, нам нужно сначала реализовать его.LimiterFallbackResolverinterface для определения поведения после понижения версии интерфейса, например, в этом примере

public class BusyFallbackResolver implements LimiterFallbackResolver<ResponseMessage> {
    @Override
    public ResponseMessage resolve(Method method, Class<?> aClass, Object[] objects, 		String s) {
        //对于被降级的请求直接返回服务繁忙
        return ResponseMessage.erroe("服务繁忙");
    }
}

BusyFallbackResolverРеализация заставит интерфейс с пониженной версией напрямую возвращать «Сервис занят», что можно использовать, внедрив реализацию в контейнер Bean.

@Bean
LimiterFallbackResolver<ResponseMessage> busyFallback() {
        return new BusyFallbackResolver();
}

Теперь другой вопрос, как реализована наша так называемая блокировка? На самом деле, я абстрагирую блокировку в интерфейсsite.higgs.limiter.lock.Lock, и предоставляет две реализации: одна — реализация Lock, предоставляемая используемым Jdk.Если прикладной проект не требует многоэкземплярного развертывания, для удовлетворения требований достаточно блокировки Jdk, другая — распределенная блокировка, реализованная Redis.( redisson), который отлично работает в многоэкземплярных проектах. Конечно, если эти две блокировки не соответствуют требованиям, разработчики могут реализовать соответствующий интерфейс для добавления блокировки.Соответствующий код находится вsite.higgs.limiter.lock.supportВниз.

И как настроить какой замок использовать?

Сначала нам нужно внедрить LockManager,

 @Bean
public LockManager redisLockManager() {
       Config config = new Config();
       config.useSingleServer().setAddress("redis://127.0.0.1:6379")
                .setDatabase(1);
       //config 来源于redisson 
       config.setLockWatchdogTimeout(1000 * 60 * 30);
       RedisLockManager redisLockManager = new RedisLockManager(config);
       return redisLockManager;
}

и используя@HLockВыберите LockManager при аннотации

 @HLock(keys = "#changeCode", fallbackResolver = "busyFallback",lockManager = "redisLockManager")

Точно так же используйте блокировку Jdk

@Bean 
public LockManager jdkLockManager() {
        return  new JdkLockManager();
 }

Теперь давайте обсудим другой вопрос.Если текущее требование состоит в том, что каждый код погашения может быть активирован только один раз, но каждый код погашения может быть активирован только один раз для каждого пользователя, отличие от вышеизложенного заключается в том, что теперьkeysБыть с номером пользователяuserIdЕсть какая-то связь, и кажется, что параметры метода не имеют никаких параметров, связанных с пользователем.

на самом деле может быть настроен с помощью@HLockизargInjectersВнедрить пользовательский объект. Для этого мы сначала реализуем инжектор параметров

public class InjectUser implements ArgumentInjecter {
    @Override
    public Map<String, Object> inject(Object... objects) {
        /**
         * 大多数项目中 当前登录用户都是存放在线程级变量中
         */
        User user = new User();
        user.setUserId("123");
        user.setUserName("higgs");
        Map<String, Object> retVal = new HashMap<>();
        retVal.put("user", user);
        return retVal;
    }
}

То же самое вводится в контейнер Spring для использования

@Bean
public ArgumentInjecter injectUser() {
        return new InjectUser();
}

Последний отзыв@HLockиспользовать

@HLock(keys = "#changeCode+#user.userId", fallbackResolver = "busyFallback",lockManager = "redisLockManager",argInjecters = "injectUser")

Что касается формы выражения ключейSpelСоответствующая информация не будет повторяться здесь.


два,@HSemaphore

 `@HSemaphore` 注解用来为接口声明一个信号量,可以达到限制并发数得效果
скопировать код
@HSemaphore(keys = "'exchange2'+#user.userId", fallbackResolver = "busyFallback", semaphoreManager = "redisSemaphoreManager",permits = 5, argInjecters = "injectUser")

Настроить код менеджера

  @Bean
 public SemaphoreManager redisSemaphoreManager() {
        Config config = new Config();
        config.useSingleServer().setAddress("redis://127.0.0.1:6379").setDatabase(3);
        config.setLockWatchdogTimeout(1000 * 60 * 30);
        RedisSemaphoreManager semaphoreManager = new RedisSemaphoreManager(config);
        return semaphoreManager;
 }

три ,@HRateLimiter

@HRateLimiterАннотация используется для объявления ограничителя скорости для интерфейса, ограничивающего частоту доступа к интерфейсу.

Настройка диспетчера ограничителя скорости

 @Bean
public RateLimiterManager redisRateLimiterManager() {
        Config config = new Config();
        // 不要和 lock 使用一个db 会有冲突 ,这里选择db2
        config.useSingleServer().setAddress("redis://127.0.0.1:6379").setDatabase(2);
        config.setLockWatchdogTimeout(1000 * 60 * 30);
        RedisRateLimiterManager redisRateLimiterManager = new       RedisRateLimiterManager(config);
        return redisRateLimiterManager;
}

Другая конфигурация


Фактически необходимо настроить глобально действующую конфигурацию, чтобы обеспечить высокую доступность компонентов и параметров по умолчанию.

@Bean
    GlobalConfig globalConfig() {
        LimiterGlobalConfig limiterGlobalConfig = new LimiterGlobalConfig();
        // 当组件内遇到异常时是否进行降级,比如使用分布式锁时,
        // redis 宕机后的降级策略,返回true未不降级,false为降级
        limiterGlobalConfig.setErrorHandler(new ErrorHandler() {
            @Override
            public boolean handleError(RuntimeException runtimeException) {
                throw runtimeException;
            }
        });
        // 当没有配置降级接口时使用全局配置
        limiterGlobalConfig.setLimiterFallbackResolver(new LimiterFallbackResolver() {
            @Override
            public Object resolve(Method method, Class clazz, Object[] args, String key) {
                throw new RuntimeException("");
            }
        });
        return limiterGlobalConfig;
    }

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

    @Bean
    public LockManager redisLockManager() {

        Config config = new Config();
        config.useSingleServer().setAddress("redis://127.0.0.1:6379")
                .setDatabase(1);
        config.setLockWatchdogTimeout(1000 * 60 * 30);
        RedisLockManager redisLockManager = new RedisLockManager(config);
        return redisLockManager;
    }

Параметры, которые можно настроить при использовании блокировки Jdk

  @Bean
public LockManager jdkLockManager() {
        site.higgs.limiter.lock.support.jdk.Config config = new site.higgs.limiter.lock.support.jdk.Config();
        config.setSize(2 << 10);// //缓存锁的容量,当内存中存在的锁实例超过该阈值时将会根据LUR清除最近最少用到的锁实例
        config.setDuration(30);   //在多久没获取该锁时自动解锁
        config.setTimeUnit(TimeUnit.SECONDS);
        config.setTimerduration(86400000);// //看门狗 多久进行一次大扫除  单位毫秒 主要用来清除最近未使用到的锁 减少内存消耗
        return new JdkLockManager();
}

Архитектура проекта


组件的类图如下,`Limiter`作为一个顶级接口,提供了扩展其他组件的能力
скопировать код

пример кода

package site.higgs.limiterdemo;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import site.higgs.limiter.lock.HLock;
import site.higgs.limiter.ratelimiter.HRateLimiter;
import site.higgs.limiter.semaphore.HSemaphore;

@RestController
public class Controller {





    /**
     * 限制键为 #redeemCode+#user.userId
     * 当多个请求同时到达时,只有一个会被正常处理,其他请求会被降级
     * 当正常的请求被处理完毕,锁会释放
     * 值得注意得是keys 本身不会包含方法名,最好前面加前缀同其他接口分开
     * @param redeemCode
     * @return
     */
    @RequestMapping(value = "/exchange", method = RequestMethod.GET)
    @HLock(keys = "#redeemCode+#user.userId", fallbackResolver = "busyFallback", lockManager = "redisLockManager", argInjecters = "injectUser")
    public ResponseMessage exchange(@RequestParam("redeemCode") String redeemCode) {
        try {
            // do something
            Thread.sleep(2000);
        } catch (InterruptedException e) {

        }
        return ResponseMessage.ok(null);
    }

    /**
     * 限制该接口的访问频率为 10次/秒
     * redis实现的限流器精度和网络环境和机器配置有关,自行测试效果
     * @param redeemCode
     * @return
     */
    @RequestMapping(value = "/exchange1", method = RequestMethod.GET)
    @HRateLimiter(keys = "'exchange1'+#redeemCode", fallbackResolver = "busyFallback", rateLimiterManager = "redisRateLimiterManager",pps = 10, argInjecters = "injectUser")
    public ResponseMessage exchange1(@RequestParam("redeemCode") String redeemCode) {
        try {
            // do something
            Thread.sleep(5000);
        } catch (InterruptedException e) {

        }
        return ResponseMessage.ok(null);
    }


    /**
     * 限制该接口并发数为10
     * redis实现的限流器精度和网络环境和机器配置有关,自行测试效果
     * @param redeemCode
     * @return
     */
    @RequestMapping(value = "/exchange2", method = RequestMethod.GET)
    @HSemaphore(keys = "'exchange2'+#redeemCode", fallbackResolver = "busyFallback", semaphoreManager = "redisSemaphoreManager",permits = 5, argInjecters = "injectUser")
    public ResponseMessage exchange2(@RequestParam("redeemCode") String redeemCode) {
        try {
            // do something
            Thread.sleep(2000);
        } catch (InterruptedException e) {

        }
        return ResponseMessage.ok(null);
    }




}

Application.java

package site.higgs.limiterdemo;

import org.redisson.config.Config;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import site.higgs.limiter.annotation.EnableLimiter;
import site.higgs.limiter.config.GlobalConfig;
import site.higgs.limiter.config.LimiterGlobalConfig;
import site.higgs.limiter.interceptor.ArgumentInjecter;
import site.higgs.limiter.interceptor.ErrorHandler;
import site.higgs.limiter.interceptor.LimiterFallbackResolver;
import site.higgs.limiter.lock.LockManager;
import site.higgs.limiter.lock.support.jdk.JdkLockManager;
import site.higgs.limiter.lock.support.redis.RedisLockManager;
import site.higgs.limiter.ratelimiter.RateLimiterManager;
import site.higgs.limiter.ratelimiter.support.guava.GuavaRateLimiterManager;
import site.higgs.limiter.ratelimiter.support.redis.RedisRateLimiterManager;
import site.higgs.limiter.semaphore.SemaphoreManager;
import site.higgs.limiter.semaphore.support.jdk.JdkSemaphoreManager;
import site.higgs.limiter.semaphore.support.redis.RedisSemaphoreManager;

import java.lang.reflect.Method;
import java.util.concurrent.TimeUnit;

@SpringBootApplication
@EnableLimiter
public class Application {

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }


    /**
     * 定义一个降级接口,被拦截降级的请求将会返回 服务繁忙
     * 可以直接设置busyFallback 使用该组件
     *
     * @return
     */
    @Bean
    LimiterFallbackResolver<ResponseMessage> busyFallback() {
        return new BusyFallbackResolver();
    }

    /**
     * 定义一个参数注入器
     *
     * @return
     */
    @Bean
    public ArgumentInjecter injectUser() {
        return new UserInfoInjecter();
    }


    /**
     * 定义一个全局生效的配置
     *
     * @return
     */
    @Bean
    GlobalConfig globalConfig() {
        LimiterGlobalConfig limiterGlobalConfig = new LimiterGlobalConfig();
        // 当组件内遇到异常时是否进行降级,比如使用分布式锁时,
        // redis 宕机后的降级策略,返回true未不降级,false为降级
        limiterGlobalConfig.setErrorHandler(new ErrorHandler() {
            @Override
            public boolean handleError(RuntimeException runtimeException) {
                throw runtimeException;
            }
        });
        // 当没有配置降级接口时使用全局配置
        limiterGlobalConfig.setLimiterFallbackResolver(new LimiterFallbackResolver() {
            @Override
            public Object resolve(Method method, Class clazz, Object[] args, String key) {
                throw new RuntimeException("");
            }
        });
        return limiterGlobalConfig;
    }

    // 配置一个LockManager, 可以设置lockManager = "redisLockManager" 使用该LockManager
    @Bean
    public LockManager redisLockManager() {

        Config config = new Config();
        config.useSingleServer().setAddress("redis://127.0.0.1:6379")
                .setDatabase(1);
        config.setLockWatchdogTimeout(1000 * 60 * 30);
        RedisLockManager redisLockManager = new RedisLockManager(config);
        return redisLockManager;
    }

    @Bean
    public LockManager jdkLockManager() {
        site.higgs.limiter.lock.support.jdk.Config config = new site.higgs.limiter.lock.support.jdk.Config();
        config.setSize(2 << 10);// //缓存锁的容量,当内存中存在的锁实例超过该阈值时将会根据LUR清除最近最少用到的锁实例
        config.setDuration(30);   //在多久没获取该锁时自动解锁
        config.setTimeUnit(TimeUnit.SECONDS);
        config.setTimerduration(86400000);// //看门狗 多久进行一次大扫除  单位毫秒 主要用来清除最近未使用到的锁 减少内存消耗
        return new JdkLockManager();
    }

    @Bean
    public RateLimiterManager redisRateLimiterManager() {
        Config config = new Config();
        // 不要和 lock 使用一个db 会有冲突 ,这里选择db2
        config.useSingleServer().setAddress("redis://127.0.0.1:6379").setDatabase(2);
        config.setLockWatchdogTimeout(1000 * 60 * 30);
        RedisRateLimiterManager redisRateLimiterManager = new RedisRateLimiterManager(config);
        return redisRateLimiterManager;
    }

    @Bean
    public RateLimiterManager guavaRateLimiterManager() {
        return new GuavaRateLimiterManager();
    }

    @Bean
    public SemaphoreManager redisSemaphoreManager() {
        Config config = new Config();
        config.useSingleServer().setAddress("redis://127.0.0.1:6379").setDatabase(3);
        config.setLockWatchdogTimeout(1000 * 60 * 30);
        RedisSemaphoreManager semaphoreManager = new RedisSemaphoreManager(config);
        return semaphoreManager;
    }


    @Bean
    public SemaphoreManager jdkSemaphoreManager() {
        return new JdkSemaphoreManager();
    }


}