Аннотация позволяет вашему проекту 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, нам нужно сначала реализовать его.LimiterFallbackResolver
interface для определения поведения после понижения версии интерфейса, например, в этом примере
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();
}
}